forked from hazza/dspace-angular
Merge remote-tracking branch 'origin/main' into #1171
# Conflicts: # src/app/submission/sections/upload/file/section-upload-file.component.html
This commit is contained in:
@@ -167,7 +167,6 @@ export class BrowseByMetadataPageComponent implements OnInit {
|
|||||||
* @param value The value of the browse-entry to display items for
|
* @param value The value of the browse-entry to display items for
|
||||||
*/
|
*/
|
||||||
updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) {
|
updatePageWithItems(searchOptions: BrowseEntrySearchOptions, value: string) {
|
||||||
console.log('updatePAge', searchOptions);
|
|
||||||
this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions);
|
this.items$ = this.browseService.getBrowseItemsFor(value, searchOptions);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,6 +1,11 @@
|
|||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs';
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
combineLatest as observableCombineLatest,
|
||||||
|
Observable,
|
||||||
|
Subject
|
||||||
|
} from 'rxjs';
|
||||||
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
|
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
|
||||||
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
||||||
import { SearchService } from '../core/shared/search/search.service';
|
import { SearchService } from '../core/shared/search/search.service';
|
||||||
@@ -8,8 +13,6 @@ import { SortDirection, SortOptions } from '../core/cache/models/sort-options.mo
|
|||||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||||
import { PaginatedList } from '../core/data/paginated-list.model';
|
import { PaginatedList } from '../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../core/data/remote-data';
|
import { RemoteData } from '../core/data/remote-data';
|
||||||
|
|
||||||
import { MetadataService } from '../core/metadata/metadata.service';
|
|
||||||
import { Bitstream } from '../core/shared/bitstream.model';
|
import { Bitstream } from '../core/shared/bitstream.model';
|
||||||
|
|
||||||
import { Collection } from '../core/shared/collection.model';
|
import { Collection } from '../core/shared/collection.model';
|
||||||
@@ -65,7 +68,6 @@ export class CollectionPageComponent implements OnInit {
|
|||||||
constructor(
|
constructor(
|
||||||
private collectionDataService: CollectionDataService,
|
private collectionDataService: CollectionDataService,
|
||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private metadata: MetadataService,
|
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
@@ -122,10 +124,6 @@ export class CollectionPageComponent implements OnInit {
|
|||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
map((collection) => getCollectionPageRoute(collection.id))
|
map((collection) => getCollectionPageRoute(collection.id))
|
||||||
);
|
);
|
||||||
|
|
||||||
this.route.queryParams.pipe(take(1)).subscribe((params) => {
|
|
||||||
this.metadata.processRemoteData(this.collectionRD$);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
isNotEmpty(object: any) {
|
isNotEmpty(object: any) {
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import {filter, map} from 'rxjs/operators';
|
import { filter, map } from 'rxjs/operators';
|
||||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
|
||||||
import { Observable , BehaviorSubject } from 'rxjs';
|
import { Observable, BehaviorSubject } from 'rxjs';
|
||||||
|
|
||||||
import { ItemPageComponent } from '../simple/item-page.component';
|
import { ItemPageComponent } from '../simple/item-page.component';
|
||||||
import { MetadataMap } from '../../core/shared/metadata.models';
|
import { MetadataMap } from '../../core/shared/metadata.models';
|
||||||
@@ -11,8 +11,6 @@ import { ItemDataService } from '../../core/data/item-data.service';
|
|||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
|
||||||
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 { AuthService } from '../../core/auth/auth.service';
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
@@ -35,8 +33,8 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
|||||||
|
|
||||||
metadata$: Observable<MetadataMap>;
|
metadata$: Observable<MetadataMap>;
|
||||||
|
|
||||||
constructor(route: ActivatedRoute, router: Router, items: ItemDataService, metadataService: MetadataService, authService: AuthService) {
|
constructor(route: ActivatedRoute, router: Router, items: ItemDataService, authService: AuthService) {
|
||||||
super(route, router, items, metadataService, authService);
|
super(route, router, items, authService);
|
||||||
}
|
}
|
||||||
|
|
||||||
/*** AoT inheritance fix, will hopefully be resolved in the near future **/
|
/*** AoT inheritance fix, will hopefully be resolved in the near future **/
|
||||||
|
@@ -8,8 +8,6 @@ import { RemoteData } from '../../core/data/remote-data';
|
|||||||
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
|
||||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
|
||||||
|
|
||||||
import { fadeInOut } from '../../shared/animations/fade';
|
import { fadeInOut } from '../../shared/animations/fade';
|
||||||
import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../../core/shared/operators';
|
import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../../core/shared/operators';
|
||||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||||
@@ -54,7 +52,6 @@ export class ItemPageComponent implements OnInit {
|
|||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
private router: Router,
|
private router: Router,
|
||||||
private items: ItemDataService,
|
private items: ItemDataService,
|
||||||
private metadataService: MetadataService,
|
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
) { }
|
) { }
|
||||||
|
|
||||||
@@ -66,7 +63,6 @@ export class ItemPageComponent implements OnInit {
|
|||||||
map((data) => data.dso as RemoteData<Item>),
|
map((data) => data.dso as RemoteData<Item>),
|
||||||
redirectOn4xx(this.router, this.authService)
|
redirectOn4xx(this.router, this.authService)
|
||||||
);
|
);
|
||||||
this.metadataService.processRemoteData(this.itemRD$);
|
|
||||||
this.itemPageRoute$ = this.itemRD$.pipe(
|
this.itemPageRoute$ = this.itemRD$.pipe(
|
||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
map((item) => getItemPageRoute(item))
|
map((item) => getItemPageRoute(item))
|
||||||
|
@@ -1,4 +1,4 @@
|
|||||||
import { delay, distinctUntilChanged, filter, take } from 'rxjs/operators';
|
import { delay, distinctUntilChanged, filter, take, withLatestFrom } from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
AfterViewInit,
|
AfterViewInit,
|
||||||
ChangeDetectionStrategy,
|
ChangeDetectionStrategy,
|
||||||
@@ -38,6 +38,8 @@ import { ThemeService } from './shared/theme-support/theme.service';
|
|||||||
import { BASE_THEME_NAME } from './shared/theme-support/theme.constants';
|
import { BASE_THEME_NAME } from './shared/theme-support/theme.constants';
|
||||||
import { DEFAULT_THEME_CONFIG } from './shared/theme-support/theme.effects';
|
import { DEFAULT_THEME_CONFIG } from './shared/theme-support/theme.effects';
|
||||||
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
import { BreadcrumbsService } from './breadcrumbs/breadcrumbs.service';
|
||||||
|
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-app',
|
selector: 'ds-app',
|
||||||
@@ -70,6 +72,11 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
isThemeLoading$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
isThemeLoading$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not the idle modal is is currently open
|
||||||
|
*/
|
||||||
|
idleModalOpen: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
@Inject(NativeWindowService) private _window: NativeWindowRef,
|
||||||
@Inject(DOCUMENT) private document: any,
|
@Inject(DOCUMENT) private document: any,
|
||||||
@@ -87,6 +94,7 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService,
|
||||||
private localeService: LocaleService,
|
private localeService: LocaleService,
|
||||||
private breadcrumbsService: BreadcrumbsService,
|
private breadcrumbsService: BreadcrumbsService,
|
||||||
|
private modalService: NgbModal,
|
||||||
@Optional() private cookiesService: KlaroService,
|
@Optional() private cookiesService: KlaroService,
|
||||||
@Optional() private googleAnalyticsService: GoogleAnalyticsService,
|
@Optional() private googleAnalyticsService: GoogleAnalyticsService,
|
||||||
) {
|
) {
|
||||||
@@ -108,6 +116,11 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
|
this.authService.trackTokenExpiration();
|
||||||
|
this.trackIdleModal();
|
||||||
|
}
|
||||||
|
|
||||||
// Load all the languages that are defined as active from the config file
|
// Load all the languages that are defined as active from the config file
|
||||||
translate.addLangs(environment.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code));
|
translate.addLangs(environment.languages.filter((LangConfig) => LangConfig.active === true).map((a) => a.code));
|
||||||
|
|
||||||
@@ -130,7 +143,6 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
console.info(environment);
|
console.info(environment);
|
||||||
}
|
}
|
||||||
this.storeCSSVariables();
|
this.storeCSSVariables();
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
@@ -229,4 +241,23 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
};
|
};
|
||||||
head.appendChild(link);
|
head.appendChild(link);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
private trackIdleModal() {
|
||||||
|
const isIdle$ = this.authService.isUserIdle();
|
||||||
|
const isAuthenticated$ = this.authService.isAuthenticated();
|
||||||
|
isIdle$.pipe(withLatestFrom(isAuthenticated$))
|
||||||
|
.subscribe(([userIdle, authenticated]) => {
|
||||||
|
if (userIdle && authenticated) {
|
||||||
|
if (!this.idleModalOpen) {
|
||||||
|
const modalRef = this.modalService.open(IdleModalComponent, { ariaLabelledBy: 'idle-modal.header' });
|
||||||
|
this.idleModalOpen = true;
|
||||||
|
modalRef.componentInstance.response.pipe(take(1)).subscribe((closed: boolean) => {
|
||||||
|
if (closed) {
|
||||||
|
this.idleModalOpen = false;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -47,6 +47,7 @@ import { ThemedHeaderComponent } from './header/themed-header.component';
|
|||||||
import { ThemedFooterComponent } from './footer/themed-footer.component';
|
import { ThemedFooterComponent } from './footer/themed-footer.component';
|
||||||
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
|
import { ThemedBreadcrumbsComponent } from './breadcrumbs/themed-breadcrumbs.component';
|
||||||
import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component';
|
import { ThemedHeaderNavbarWrapperComponent } from './header-nav-wrapper/themed-header-navbar-wrapper.component';
|
||||||
|
import { IdleModalComponent } from './shared/idle-modal/idle-modal.component';
|
||||||
|
|
||||||
export function getBase() {
|
export function getBase() {
|
||||||
return environment.ui.nameSpace;
|
return environment.ui.nameSpace;
|
||||||
@@ -144,6 +145,7 @@ const DECLARATIONS = [
|
|||||||
ThemedBreadcrumbsComponent,
|
ThemedBreadcrumbsComponent,
|
||||||
ForbiddenComponent,
|
ForbiddenComponent,
|
||||||
ThemedForbiddenComponent,
|
ThemedForbiddenComponent,
|
||||||
|
IdleModalComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const EXPORTS = [
|
const EXPORTS = [
|
||||||
|
@@ -34,7 +34,9 @@ export const AuthActionTypes = {
|
|||||||
RETRIEVE_AUTHENTICATED_EPERSON: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON'),
|
RETRIEVE_AUTHENTICATED_EPERSON: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON'),
|
||||||
RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS'),
|
RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_SUCCESS'),
|
||||||
RETRIEVE_AUTHENTICATED_EPERSON_ERROR: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_ERROR'),
|
RETRIEVE_AUTHENTICATED_EPERSON_ERROR: type('dspace/auth/RETRIEVE_AUTHENTICATED_EPERSON_ERROR'),
|
||||||
REDIRECT_AFTER_LOGIN_SUCCESS: type('dspace/auth/REDIRECT_AFTER_LOGIN_SUCCESS')
|
REDIRECT_AFTER_LOGIN_SUCCESS: type('dspace/auth/REDIRECT_AFTER_LOGIN_SUCCESS'),
|
||||||
|
SET_USER_AS_IDLE: type('dspace/auth/SET_USER_AS_IDLE'),
|
||||||
|
UNSET_USER_AS_IDLE: type('dspace/auth/UNSET_USER_AS_IDLE')
|
||||||
};
|
};
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@@ -404,6 +406,24 @@ export class RetrieveAuthenticatedEpersonErrorAction implements Action {
|
|||||||
this.payload = payload ;
|
this.payload = payload ;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set the current user as being idle.
|
||||||
|
* @class SetUserAsIdleAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class SetUserAsIdleAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.SET_USER_AS_IDLE;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unset the current user as being idle.
|
||||||
|
* @class UnsetUserAsIdleAction
|
||||||
|
* @implements {Action}
|
||||||
|
*/
|
||||||
|
export class UnsetUserAsIdleAction implements Action {
|
||||||
|
public type: string = AuthActionTypes.UNSET_USER_AS_IDLE;
|
||||||
|
}
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -434,4 +454,7 @@ export type AuthActions
|
|||||||
| RetrieveAuthenticatedEpersonErrorAction
|
| RetrieveAuthenticatedEpersonErrorAction
|
||||||
| RetrieveAuthenticatedEpersonSuccessAction
|
| RetrieveAuthenticatedEpersonSuccessAction
|
||||||
| SetRedirectUrlAction
|
| SetRedirectUrlAction
|
||||||
| RedirectAfterLoginSuccessAction;
|
| RedirectAfterLoginSuccessAction
|
||||||
|
| SetUserAsIdleAction
|
||||||
|
| UnsetUserAsIdleAction;
|
||||||
|
|
||||||
|
@@ -1,7 +1,13 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable, NgZone } from '@angular/core';
|
||||||
|
|
||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
import {
|
||||||
import { catchError, filter, map, switchMap, take, tap } from 'rxjs/operators';
|
combineLatest as observableCombineLatest,
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
timer,
|
||||||
|
asyncScheduler, queueScheduler
|
||||||
|
} from 'rxjs';
|
||||||
|
import { catchError, filter, map, switchMap, take, tap, observeOn } from 'rxjs/operators';
|
||||||
// import @ngrx
|
// import @ngrx
|
||||||
import { Actions, Effect, ofType } from '@ngrx/effects';
|
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||||
import { Action, select, Store } from '@ngrx/store';
|
import { Action, select, Store } from '@ngrx/store';
|
||||||
@@ -37,9 +43,19 @@ import {
|
|||||||
RetrieveAuthMethodsAction,
|
RetrieveAuthMethodsAction,
|
||||||
RetrieveAuthMethodsErrorAction,
|
RetrieveAuthMethodsErrorAction,
|
||||||
RetrieveAuthMethodsSuccessAction,
|
RetrieveAuthMethodsSuccessAction,
|
||||||
RetrieveTokenAction
|
RetrieveTokenAction, SetUserAsIdleAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { RequestActionTypes } from '../data/request.actions';
|
||||||
|
import { NotificationsActionTypes } from '../../shared/notifications/notifications.actions';
|
||||||
|
import { LeaveZoneScheduler } from '../utilities/leave-zone.scheduler';
|
||||||
|
import { EnterZoneScheduler } from '../utilities/enter-zone.scheduler';
|
||||||
|
|
||||||
|
// Action Types that do not break/prevent the user from an idle state
|
||||||
|
const IDLE_TIMER_IGNORE_TYPES: string[]
|
||||||
|
= [...Object.values(AuthActionTypes).filter((t: string) => t !== AuthActionTypes.UNSET_USER_AS_IDLE),
|
||||||
|
...Object.values(RequestActionTypes), ...Object.values(NotificationsActionTypes)];
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthEffects {
|
export class AuthEffects {
|
||||||
@@ -242,13 +258,35 @@ export class AuthEffects {
|
|||||||
})
|
})
|
||||||
);
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For any action that is not in {@link IDLE_TIMER_IGNORE_TYPES} that comes in => Start the idleness timer
|
||||||
|
* If the idleness timer runs out (so no un-ignored action come through for that amount of time)
|
||||||
|
* => Return the action to set the user as idle ({@link SetUserAsIdleAction})
|
||||||
|
* @method trackIdleness
|
||||||
|
*/
|
||||||
|
@Effect()
|
||||||
|
public trackIdleness$: Observable<Action> = this.actions$.pipe(
|
||||||
|
filter((action: Action) => !IDLE_TIMER_IGNORE_TYPES.includes(action.type)),
|
||||||
|
// Using switchMap the effect will stop subscribing to the previous timer if a new action comes
|
||||||
|
// in, and start a new timer
|
||||||
|
switchMap(() =>
|
||||||
|
// Start a timer outside of Angular's zone
|
||||||
|
timer(environment.auth.ui.timeUntilIdle, new LeaveZoneScheduler(this.zone, asyncScheduler))
|
||||||
|
),
|
||||||
|
// Re-enter the zone to dispatch the action
|
||||||
|
observeOn(new EnterZoneScheduler(this.zone, queueScheduler)),
|
||||||
|
map(() => new SetUserAsIdleAction()),
|
||||||
|
);
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @constructor
|
* @constructor
|
||||||
* @param {Actions} actions$
|
* @param {Actions} actions$
|
||||||
|
* @param {NgZone} zone
|
||||||
* @param {AuthService} authService
|
* @param {AuthService} authService
|
||||||
* @param {Store} store
|
* @param {Store} store
|
||||||
*/
|
*/
|
||||||
constructor(private actions$: Actions,
|
constructor(private actions$: Actions,
|
||||||
|
private zone: NgZone,
|
||||||
private authService: AuthService,
|
private authService: AuthService,
|
||||||
private store: Store<AppState>) {
|
private store: Store<AppState>) {
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
|
||||||
|
|
||||||
import { catchError, filter, map } from 'rxjs/operators';
|
import { catchError, map } from 'rxjs/operators';
|
||||||
import { Injectable, Injector } from '@angular/core';
|
import { Injectable, Injector } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
HttpErrorResponse,
|
HttpErrorResponse,
|
||||||
@@ -12,14 +12,13 @@ import {
|
|||||||
HttpResponse,
|
HttpResponse,
|
||||||
HttpResponseBase
|
HttpResponseBase
|
||||||
} from '@angular/common/http';
|
} from '@angular/common/http';
|
||||||
import { find } from 'lodash';
|
|
||||||
|
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
import { AuthStatus } from './models/auth-status.model';
|
import { AuthStatus } from './models/auth-status.model';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
import { hasValue, isNotEmpty, isNotNull, isUndefined } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty, isNotNull } from '../../shared/empty.util';
|
||||||
import { RedirectWhenTokenExpiredAction, RefreshTokenAction } from './auth.actions';
|
import { RedirectWhenTokenExpiredAction } from './auth.actions';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { AuthMethod } from './models/auth.method';
|
import { AuthMethod } from './models/auth.method';
|
||||||
@@ -28,7 +27,7 @@ import { AuthMethodType } from './models/auth.method-type';
|
|||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthInterceptor implements HttpInterceptor {
|
export class AuthInterceptor implements HttpInterceptor {
|
||||||
|
|
||||||
// Intercetor is called twice per request,
|
// Interceptor is called twice per request,
|
||||||
// so to prevent RefreshTokenAction is dispatched twice
|
// so to prevent RefreshTokenAction is dispatched twice
|
||||||
// we're creating a refresh token request list
|
// we're creating a refresh token request list
|
||||||
protected refreshTokenRequestUrls = [];
|
protected refreshTokenRequestUrls = [];
|
||||||
@@ -216,23 +215,8 @@ export class AuthInterceptor implements HttpInterceptor {
|
|||||||
let authorization: string;
|
let authorization: string;
|
||||||
|
|
||||||
if (authService.isTokenExpired()) {
|
if (authService.isTokenExpired()) {
|
||||||
authService.setRedirectUrl(this.router.url);
|
|
||||||
// The access token is expired
|
|
||||||
// Redirect to the login route
|
|
||||||
this.store.dispatch(new RedirectWhenTokenExpiredAction('auth.messages.expired'));
|
|
||||||
return observableOf(null);
|
return observableOf(null);
|
||||||
} else if ((!this.isAuthRequest(req) || this.isLogoutResponse(req)) && isNotEmpty(token)) {
|
} else if ((!this.isAuthRequest(req) || this.isLogoutResponse(req)) && isNotEmpty(token)) {
|
||||||
// Intercept a request that is not to the authentication endpoint
|
|
||||||
authService.isTokenExpiring().pipe(
|
|
||||||
filter((isExpiring) => isExpiring))
|
|
||||||
.subscribe(() => {
|
|
||||||
// If the current request url is already in the refresh token request list, skip it
|
|
||||||
if (isUndefined(find(this.refreshTokenRequestUrls, req.url))) {
|
|
||||||
// When a token is about to expire, refresh it
|
|
||||||
this.store.dispatch(new RefreshTokenAction(token));
|
|
||||||
this.refreshTokenRequestUrls.push(req.url);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
// Get the auth header from the service.
|
// Get the auth header from the service.
|
||||||
authorization = authService.buildAuthHeader(token);
|
authorization = authService.buildAuthHeader(token);
|
||||||
let newHeaders = req.headers.set('authorization', authorization);
|
let newHeaders = req.headers.set('authorization', authorization);
|
||||||
|
@@ -23,7 +23,7 @@ import {
|
|||||||
RetrieveAuthMethodsAction,
|
RetrieveAuthMethodsAction,
|
||||||
RetrieveAuthMethodsErrorAction,
|
RetrieveAuthMethodsErrorAction,
|
||||||
RetrieveAuthMethodsSuccessAction,
|
RetrieveAuthMethodsSuccessAction,
|
||||||
SetRedirectUrlAction
|
SetRedirectUrlAction, SetUserAsIdleAction, UnsetUserAsIdleAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { AuthTokenInfo } from './models/auth-token-info.model';
|
import { AuthTokenInfo } from './models/auth-token-info.model';
|
||||||
import { EPersonMock } from '../../shared/testing/eperson.mock';
|
import { EPersonMock } from '../../shared/testing/eperson.mock';
|
||||||
@@ -44,6 +44,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticateAction('user', 'password');
|
const action = new AuthenticateAction('user', 'password');
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -53,7 +54,8 @@ describe('authReducer', () => {
|
|||||||
blocking: true,
|
blocking: true,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
@@ -66,7 +68,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticationSuccessAction(mockTokenInfo);
|
const action = new AuthenticationSuccessAction(mockTokenInfo);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -81,7 +84,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticationErrorAction(mockError);
|
const action = new AuthenticationErrorAction(mockError);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -92,7 +96,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
error: 'Test error message'
|
error: 'Test error message',
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
@@ -105,7 +110,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticatedAction(mockTokenInfo);
|
const action = new AuthenticatedAction(mockTokenInfo);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -115,7 +121,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -127,7 +134,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock._links.self.href);
|
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock._links.self.href);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -138,7 +146,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -150,7 +159,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AuthenticatedErrorAction(mockError);
|
const action = new AuthenticatedErrorAction(mockError);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -161,7 +171,8 @@ describe('authReducer', () => {
|
|||||||
loaded: true,
|
loaded: true,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -172,6 +183,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new CheckAuthenticationTokenAction();
|
const action = new CheckAuthenticationTokenAction();
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -180,6 +192,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -190,6 +203,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new CheckAuthenticationTokenCookieAction();
|
const action = new CheckAuthenticationTokenCookieAction();
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -198,6 +212,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -211,7 +226,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new LogOutAction();
|
const action = new LogOutAction();
|
||||||
@@ -229,7 +245,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new LogOutSuccessAction();
|
const action = new LogOutSuccessAction();
|
||||||
@@ -243,7 +260,8 @@ describe('authReducer', () => {
|
|||||||
loading: true,
|
loading: true,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
userId: undefined
|
userId: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -257,7 +275,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new LogOutErrorAction(mockError);
|
const action = new LogOutErrorAction(mockError);
|
||||||
@@ -270,7 +289,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -283,7 +303,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id);
|
const action = new RetrieveAuthenticatedEpersonSuccessAction(EPersonMock.id);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -295,7 +316,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -307,7 +329,8 @@ describe('authReducer', () => {
|
|||||||
error: undefined,
|
error: undefined,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new RetrieveAuthenticatedEpersonErrorAction(mockError);
|
const action = new RetrieveAuthenticatedEpersonErrorAction(mockError);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -318,7 +341,8 @@ describe('authReducer', () => {
|
|||||||
loaded: true,
|
loaded: true,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -332,7 +356,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
||||||
const action = new RefreshTokenAction(newTokenInfo);
|
const action = new RefreshTokenAction(newTokenInfo);
|
||||||
@@ -346,7 +371,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
refreshing: true
|
refreshing: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -361,7 +387,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
refreshing: true
|
refreshing: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
const newTokenInfo = new AuthTokenInfo('Refreshed token');
|
||||||
const action = new RefreshTokenSuccessAction(newTokenInfo);
|
const action = new RefreshTokenSuccessAction(newTokenInfo);
|
||||||
@@ -375,7 +402,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
refreshing: false
|
refreshing: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -390,7 +418,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id,
|
userId: EPersonMock.id,
|
||||||
refreshing: true
|
refreshing: true,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new RefreshTokenErrorAction();
|
const action = new RefreshTokenErrorAction();
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -403,7 +432,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
userId: undefined
|
userId: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -417,7 +447,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: undefined,
|
info: undefined,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
state = {
|
state = {
|
||||||
@@ -428,7 +459,8 @@ describe('authReducer', () => {
|
|||||||
loading: false,
|
loading: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
info: 'Message',
|
info: 'Message',
|
||||||
userId: undefined
|
userId: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -450,6 +482,7 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new AddAuthenticationMessageAction('Message');
|
const action = new AddAuthenticationMessageAction('Message');
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -458,7 +491,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
info: 'Message'
|
info: 'Message',
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -470,7 +504,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: 'Error',
|
error: 'Error',
|
||||||
info: 'Message'
|
info: 'Message',
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new ResetAuthenticationMessagesAction();
|
const action = new ResetAuthenticationMessagesAction();
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -480,7 +515,8 @@ describe('authReducer', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
error: undefined,
|
error: undefined,
|
||||||
info: undefined
|
info: undefined,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -490,7 +526,8 @@ describe('authReducer', () => {
|
|||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new SetRedirectUrlAction('redirect.url');
|
const action = new SetRedirectUrlAction('redirect.url');
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -499,7 +536,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
redirectUrl: 'redirect.url'
|
redirectUrl: 'redirect.url',
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -510,7 +548,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const action = new RetrieveAuthMethodsAction(new AuthStatus(), true);
|
const action = new RetrieveAuthMethodsAction(new AuthStatus(), true);
|
||||||
const newState = authReducer(initialState, action);
|
const newState = authReducer(initialState, action);
|
||||||
@@ -519,7 +558,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -530,7 +570,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const authMethods = [
|
const authMethods = [
|
||||||
new AuthMethod(AuthMethodType.Password),
|
new AuthMethod(AuthMethodType.Password),
|
||||||
@@ -543,7 +584,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: authMethods
|
authMethods: authMethods,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -554,7 +596,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
const authMethods = [
|
const authMethods = [
|
||||||
new AuthMethod(AuthMethodType.Password),
|
new AuthMethod(AuthMethodType.Password),
|
||||||
@@ -567,7 +610,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: authMethods
|
authMethods: authMethods,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -578,7 +622,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new RetrieveAuthMethodsErrorAction(false);
|
const action = new RetrieveAuthMethodsErrorAction(false);
|
||||||
@@ -588,7 +633,50 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
authMethods: [new AuthMethod(AuthMethodType.Password)],
|
||||||
|
idle: false
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a SET_USER_AS_IDLE action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
|
loading: false,
|
||||||
|
idle: false
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = new SetUserAsIdleAction();
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
|
loading: false,
|
||||||
|
idle: true
|
||||||
|
};
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should properly set the state, in response to a UNSET_USER_AS_IDLE action', () => {
|
||||||
|
initialState = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
|
loading: false,
|
||||||
|
idle: true
|
||||||
|
};
|
||||||
|
|
||||||
|
const action = new UnsetUserAsIdleAction();
|
||||||
|
const newState = authReducer(initialState, action);
|
||||||
|
state = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
blocking: false,
|
||||||
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
@@ -599,7 +687,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: true,
|
loading: true,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
const action = new RetrieveAuthMethodsErrorAction(true);
|
const action = new RetrieveAuthMethodsErrorAction(true);
|
||||||
@@ -609,7 +698,8 @@ describe('authReducer', () => {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: [new AuthMethod(AuthMethodType.Password)]
|
authMethods: [new AuthMethod(AuthMethodType.Password)],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
expect(newState).toEqual(state);
|
expect(newState).toEqual(state);
|
||||||
});
|
});
|
||||||
|
@@ -59,6 +59,9 @@ export interface AuthState {
|
|||||||
// all authentication Methods enabled at the backend
|
// all authentication Methods enabled at the backend
|
||||||
authMethods?: AuthMethod[];
|
authMethods?: AuthMethod[];
|
||||||
|
|
||||||
|
// true when the current user is idle
|
||||||
|
idle: boolean;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -69,7 +72,8 @@ const initialState: AuthState = {
|
|||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: true,
|
blocking: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
authMethods: []
|
authMethods: [],
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -189,6 +193,7 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
authToken: (action as RefreshTokenSuccessAction).payload,
|
authToken: (action as RefreshTokenSuccessAction).payload,
|
||||||
refreshing: false,
|
refreshing: false,
|
||||||
|
blocking: false
|
||||||
});
|
});
|
||||||
|
|
||||||
case AuthActionTypes.ADD_MESSAGE:
|
case AuthActionTypes.ADD_MESSAGE:
|
||||||
@@ -234,6 +239,16 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
|
|||||||
blocking: true,
|
blocking: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.SET_USER_AS_IDLE:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
idle: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
case AuthActionTypes.UNSET_USER_AS_IDLE:
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
idle: false,
|
||||||
|
});
|
||||||
|
|
||||||
default:
|
default:
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
|
@@ -27,6 +27,11 @@ import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.util
|
|||||||
import { authMethodsMock } from '../../shared/testing/auth-service.stub';
|
import { authMethodsMock } from '../../shared/testing/auth-service.stub';
|
||||||
import { AuthMethod } from './models/auth.method';
|
import { AuthMethod } from './models/auth.method';
|
||||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service.stub';
|
||||||
|
import { SetUserAsIdleAction, UnsetUserAsIdleAction } from './auth.actions';
|
||||||
|
|
||||||
describe('AuthService test', () => {
|
describe('AuthService test', () => {
|
||||||
|
|
||||||
@@ -47,6 +52,7 @@ describe('AuthService test', () => {
|
|||||||
let token: AuthTokenInfo;
|
let token: AuthTokenInfo;
|
||||||
let authenticatedState;
|
let authenticatedState;
|
||||||
let unAuthenticatedState;
|
let unAuthenticatedState;
|
||||||
|
let idleState;
|
||||||
let linkService;
|
let linkService;
|
||||||
let hardRedirectService;
|
let hardRedirectService;
|
||||||
|
|
||||||
@@ -64,14 +70,24 @@ describe('AuthService test', () => {
|
|||||||
loaded: true,
|
loaded: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
authToken: token,
|
authToken: token,
|
||||||
user: EPersonMock
|
user: EPersonMock,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
unAuthenticatedState = {
|
unAuthenticatedState = {
|
||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: true,
|
loaded: true,
|
||||||
loading: false,
|
loading: false,
|
||||||
authToken: undefined,
|
authToken: undefined,
|
||||||
user: undefined
|
user: undefined,
|
||||||
|
idle: false
|
||||||
|
};
|
||||||
|
idleState = {
|
||||||
|
authenticated: true,
|
||||||
|
loaded: true,
|
||||||
|
loading: false,
|
||||||
|
authToken: token,
|
||||||
|
user: EPersonMock,
|
||||||
|
idle: true
|
||||||
};
|
};
|
||||||
authRequest = new AuthRequestServiceStub();
|
authRequest = new AuthRequestServiceStub();
|
||||||
routeStub = new ActivatedRouteStub();
|
routeStub = new ActivatedRouteStub();
|
||||||
@@ -107,6 +123,8 @@ describe('AuthService test', () => {
|
|||||||
{ provide: Store, useValue: mockStore },
|
{ provide: Store, useValue: mockStore },
|
||||||
{ provide: EPersonDataService, useValue: mockEpersonDataService },
|
{ provide: EPersonDataService, useValue: mockEpersonDataService },
|
||||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||||
|
{ provide: NotificationsService, useValue: NotificationsServiceStub },
|
||||||
|
{ provide: TranslateService, useValue: getMockTranslateService() },
|
||||||
CookieService,
|
CookieService,
|
||||||
AuthService
|
AuthService
|
||||||
],
|
],
|
||||||
@@ -180,6 +198,26 @@ describe('AuthService test', () => {
|
|||||||
expect(authMethods.length).toBe(2);
|
expect(authMethods.length).toBe(2);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('setIdle true', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authService.setIdle(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('store should dispatch SetUserAsIdleAction', () => {
|
||||||
|
expect(mockStore.dispatch).toHaveBeenCalledWith(new SetUserAsIdleAction());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setIdle false', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
authService.setIdle(false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('store should dispatch UnsetUserAsIdleAction', () => {
|
||||||
|
expect(mockStore.dispatch).toHaveBeenCalledWith(new UnsetUserAsIdleAction());
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('', () => {
|
describe('', () => {
|
||||||
@@ -207,13 +245,13 @@ describe('AuthService test', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService) => {
|
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
|
||||||
store
|
store
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
(state as any).core = Object.create({});
|
(state as any).core = Object.create({});
|
||||||
(state as any).core.auth = authenticatedState;
|
(state as any).core.auth = authenticatedState;
|
||||||
});
|
});
|
||||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should return true when user is logged in', () => {
|
it('should return true when user is logged in', () => {
|
||||||
@@ -250,6 +288,12 @@ describe('AuthService test', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('isUserIdle should return false when user is not yet idle', () => {
|
||||||
|
authService.isUserIdle().subscribe((status: boolean) => {
|
||||||
|
expect(status).toBe(false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('', () => {
|
describe('', () => {
|
||||||
@@ -277,7 +321,7 @@ describe('AuthService test', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(inject([ClientCookieService, AuthRequestService, Store, Router, RouteService], (cookieService: ClientCookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService) => {
|
beforeEach(inject([ClientCookieService, AuthRequestService, Store, Router, RouteService], (cookieService: ClientCookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
|
||||||
const expiredToken: AuthTokenInfo = new AuthTokenInfo('test_token');
|
const expiredToken: AuthTokenInfo = new AuthTokenInfo('test_token');
|
||||||
expiredToken.expires = Date.now() - (1000 * 60 * 60);
|
expiredToken.expires = Date.now() - (1000 * 60 * 60);
|
||||||
authenticatedState = {
|
authenticatedState = {
|
||||||
@@ -292,7 +336,7 @@ describe('AuthService test', () => {
|
|||||||
(state as any).core = Object.create({});
|
(state as any).core = Object.create({});
|
||||||
(state as any).core.auth = authenticatedState;
|
(state as any).core.auth = authenticatedState;
|
||||||
});
|
});
|
||||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
|
||||||
storage = (authService as any).storage;
|
storage = (authService as any).storage;
|
||||||
routeServiceMock = TestBed.inject(RouteService);
|
routeServiceMock = TestBed.inject(RouteService);
|
||||||
routerStub = TestBed.inject(Router);
|
routerStub = TestBed.inject(Router);
|
||||||
@@ -493,13 +537,13 @@ describe('AuthService test', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService) => {
|
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
|
||||||
store
|
store
|
||||||
.subscribe((state) => {
|
.subscribe((state) => {
|
||||||
(state as any).core = Object.create({});
|
(state as any).core = Object.create({});
|
||||||
(state as any).core.auth = unAuthenticatedState;
|
(state as any).core.auth = unAuthenticatedState;
|
||||||
});
|
});
|
||||||
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService);
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('should return null for the shortlived token', () => {
|
it('should return null for the shortlived token', () => {
|
||||||
@@ -508,4 +552,44 @@ describe('AuthService test', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('when user is idle', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
init();
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
StoreModule.forRoot({ authReducer }, {
|
||||||
|
runtimeChecks: {
|
||||||
|
strictStateImmutability: false,
|
||||||
|
strictActionImmutability: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: AuthRequestService, useValue: authRequest },
|
||||||
|
{ provide: REQUEST, useValue: {} },
|
||||||
|
{ provide: Router, useValue: routerStub },
|
||||||
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
|
{ provide: RemoteDataBuildService, useValue: linkService },
|
||||||
|
CookieService,
|
||||||
|
AuthService
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(inject([CookieService, AuthRequestService, Store, Router, RouteService], (cookieService: CookieService, authReqService: AuthRequestService, store: Store<AppState>, router: Router, routeService: RouteService, notificationsService: NotificationsService, translateService: TranslateService) => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).core = Object.create({});
|
||||||
|
(state as any).core.auth = idleState;
|
||||||
|
});
|
||||||
|
authService = new AuthService({}, window, undefined, authReqService, mockEpersonDataService, router, routeService, cookieService, store, hardRedirectService, notificationsService, translateService);
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('isUserIdle should return true when user is not idle', () => {
|
||||||
|
authService.isUserIdle().subscribe((status: boolean) => {
|
||||||
|
expect(status).toBe(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -29,14 +29,17 @@ import {
|
|||||||
getRedirectUrl,
|
getRedirectUrl,
|
||||||
isAuthenticated,
|
isAuthenticated,
|
||||||
isAuthenticatedLoaded,
|
isAuthenticatedLoaded,
|
||||||
|
isIdle,
|
||||||
isTokenRefreshing
|
isTokenRefreshing
|
||||||
} from './selectors';
|
} from './selectors';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import {
|
import {
|
||||||
CheckAuthenticationTokenAction,
|
CheckAuthenticationTokenAction, RefreshTokenAction,
|
||||||
ResetAuthenticationMessagesAction,
|
ResetAuthenticationMessagesAction,
|
||||||
RetrieveAuthMethodsAction,
|
RetrieveAuthMethodsAction,
|
||||||
SetRedirectUrlAction
|
SetRedirectUrlAction,
|
||||||
|
SetUserAsIdleAction,
|
||||||
|
UnsetUserAsIdleAction
|
||||||
} from './auth.actions';
|
} from './auth.actions';
|
||||||
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
import { NativeWindowRef, NativeWindowService } from '../services/window.service';
|
||||||
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
|
||||||
@@ -46,6 +49,9 @@ import { getAllSucceededRemoteDataPayload } from '../shared/operators';
|
|||||||
import { AuthMethod } from './models/auth.method';
|
import { AuthMethod } from './models/auth.method';
|
||||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
export const LOGIN_ROUTE = '/login';
|
export const LOGIN_ROUTE = '/login';
|
||||||
export const LOGOUT_ROUTE = '/logout';
|
export const LOGOUT_ROUTE = '/logout';
|
||||||
@@ -64,6 +70,11 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
protected _authenticated: boolean;
|
protected _authenticated: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timer to track time until token refresh
|
||||||
|
*/
|
||||||
|
private tokenRefreshTimer;
|
||||||
|
|
||||||
constructor(@Inject(REQUEST) protected req: any,
|
constructor(@Inject(REQUEST) protected req: any,
|
||||||
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
@Inject(NativeWindowService) protected _window: NativeWindowRef,
|
||||||
@Optional() @Inject(RESPONSE) private response: any,
|
@Optional() @Inject(RESPONSE) private response: any,
|
||||||
@@ -73,7 +84,9 @@ export class AuthService {
|
|||||||
protected routeService: RouteService,
|
protected routeService: RouteService,
|
||||||
protected storage: CookieService,
|
protected storage: CookieService,
|
||||||
protected store: Store<AppState>,
|
protected store: Store<AppState>,
|
||||||
protected hardRedirectService: HardRedirectService
|
protected hardRedirectService: HardRedirectService,
|
||||||
|
private notificationService: NotificationsService,
|
||||||
|
private translateService: TranslateService
|
||||||
) {
|
) {
|
||||||
this.store.pipe(
|
this.store.pipe(
|
||||||
select(isAuthenticated),
|
select(isAuthenticated),
|
||||||
@@ -187,7 +200,7 @@ export class AuthService {
|
|||||||
return this.store.pipe(
|
return this.store.pipe(
|
||||||
select(getAuthenticatedUserId),
|
select(getAuthenticatedUserId),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
switchMap((id: string) => this.epersonService.findById(id) ),
|
switchMap((id: string) => this.epersonService.findById(id)),
|
||||||
getAllSucceededRemoteDataPayload()
|
getAllSucceededRemoteDataPayload()
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
@@ -298,7 +311,7 @@ export class AuthService {
|
|||||||
*/
|
*/
|
||||||
public getToken(): AuthTokenInfo {
|
public getToken(): AuthTokenInfo {
|
||||||
let token: AuthTokenInfo;
|
let token: AuthTokenInfo;
|
||||||
this.store.pipe(select(getAuthenticationToken))
|
this.store.pipe(take(1), select(getAuthenticationToken))
|
||||||
.subscribe((authTokenInfo: AuthTokenInfo) => {
|
.subscribe((authTokenInfo: AuthTokenInfo) => {
|
||||||
// Retrieve authentication token info and check if is valid
|
// Retrieve authentication token info and check if is valid
|
||||||
token = authTokenInfo || null;
|
token = authTokenInfo || null;
|
||||||
@@ -306,6 +319,44 @@ export class AuthService {
|
|||||||
return token;
|
return token;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method that checks when the session token from store expires and refreshes it when needed
|
||||||
|
*/
|
||||||
|
public trackTokenExpiration(): void {
|
||||||
|
let token: AuthTokenInfo;
|
||||||
|
let currentlyRefreshingToken = false;
|
||||||
|
this.store.pipe(select(getAuthenticationToken)).subscribe((authTokenInfo: AuthTokenInfo) => {
|
||||||
|
// If new token is undefined an it wasn't previously => Refresh failed
|
||||||
|
if (currentlyRefreshingToken && token !== undefined && authTokenInfo === undefined) {
|
||||||
|
// Token refresh failed => Error notification => 10 second wait => Page reloads & user logged out
|
||||||
|
this.notificationService.error(this.translateService.get('auth.messages.token-refresh-failed'));
|
||||||
|
setTimeout(() => this.navigateToRedirectUrl(this.hardRedirectService.getCurrentRoute()), 10000);
|
||||||
|
currentlyRefreshingToken = false;
|
||||||
|
}
|
||||||
|
// If new token.expires is different => Refresh succeeded
|
||||||
|
if (currentlyRefreshingToken && authTokenInfo !== undefined && token.expires !== authTokenInfo.expires) {
|
||||||
|
currentlyRefreshingToken = false;
|
||||||
|
}
|
||||||
|
// Check if/when token needs to be refreshed
|
||||||
|
if (!currentlyRefreshingToken) {
|
||||||
|
token = authTokenInfo || null;
|
||||||
|
if (token !== undefined && token !== null) {
|
||||||
|
let timeLeftBeforeRefresh = token.expires - new Date().getTime() - environment.auth.rest.timeLeftBeforeTokenRefresh;
|
||||||
|
if (timeLeftBeforeRefresh < 0) {
|
||||||
|
timeLeftBeforeRefresh = 0;
|
||||||
|
}
|
||||||
|
if (hasValue(this.tokenRefreshTimer)) {
|
||||||
|
clearTimeout(this.tokenRefreshTimer);
|
||||||
|
}
|
||||||
|
this.tokenRefreshTimer = setTimeout(() => {
|
||||||
|
this.store.dispatch(new RefreshTokenAction(token));
|
||||||
|
currentlyRefreshingToken = true;
|
||||||
|
}, timeLeftBeforeRefresh);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check if a token is next to be expired
|
* Check if a token is next to be expired
|
||||||
* @returns {boolean}
|
* @returns {boolean}
|
||||||
@@ -346,7 +397,7 @@ export class AuthService {
|
|||||||
|
|
||||||
// Set the cookie expire date
|
// Set the cookie expire date
|
||||||
const expires = new Date(expireDate);
|
const expires = new Date(expireDate);
|
||||||
const options: CookieAttributes = { expires: expires };
|
const options: CookieAttributes = {expires: expires};
|
||||||
|
|
||||||
// Save cookie with the token
|
// Save cookie with the token
|
||||||
return this.storage.set(TOKENITEM, token, options);
|
return this.storage.set(TOKENITEM, token, options);
|
||||||
@@ -396,11 +447,14 @@ export class AuthService {
|
|||||||
* @param redirectUrl
|
* @param redirectUrl
|
||||||
*/
|
*/
|
||||||
public navigateToRedirectUrl(redirectUrl: string) {
|
public navigateToRedirectUrl(redirectUrl: string) {
|
||||||
let url = `/reload/${new Date().getTime()}`;
|
// Don't do redirect if already on reload url
|
||||||
if (isNotEmpty(redirectUrl) && !redirectUrl.startsWith(LOGIN_ROUTE)) {
|
if (!hasValue(redirectUrl) || !redirectUrl.includes('/reload/')) {
|
||||||
url += `?redirect=${encodeURIComponent(redirectUrl)}`;
|
let url = `/reload/${new Date().getTime()}`;
|
||||||
|
if (isNotEmpty(redirectUrl) && !redirectUrl.startsWith(LOGIN_ROUTE)) {
|
||||||
|
url += `?redirect=${encodeURIComponent(redirectUrl)}`;
|
||||||
|
}
|
||||||
|
this.hardRedirectService.redirect(url);
|
||||||
}
|
}
|
||||||
this.hardRedirectService.redirect(url);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -435,7 +489,7 @@ export class AuthService {
|
|||||||
|
|
||||||
// Set the cookie expire date
|
// Set the cookie expire date
|
||||||
const expires = new Date(expireDate);
|
const expires = new Date(expireDate);
|
||||||
const options: CookieAttributes = { expires: expires };
|
const options: CookieAttributes = {expires: expires};
|
||||||
this.storage.set(REDIRECT_COOKIE, url, options);
|
this.storage.set(REDIRECT_COOKIE, url, options);
|
||||||
this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : ''));
|
this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : ''));
|
||||||
}
|
}
|
||||||
@@ -528,4 +582,24 @@ export class AuthService {
|
|||||||
return new RetrieveAuthMethodsAction(authStatus, false);
|
return new RetrieveAuthMethodsAction(authStatus, false);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Determines if current user is idle
|
||||||
|
* @returns {Observable<boolean>}
|
||||||
|
*/
|
||||||
|
public isUserIdle(): Observable<boolean> {
|
||||||
|
return this.store.pipe(select(isIdle));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Set idle of auth state
|
||||||
|
* @returns {Observable<boolean>}
|
||||||
|
*/
|
||||||
|
public setIdle(idle: boolean): void {
|
||||||
|
if (idle) {
|
||||||
|
this.store.dispatch(new SetUserAsIdleAction());
|
||||||
|
} else {
|
||||||
|
this.store.dispatch(new UnsetUserAsIdleAction());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -115,6 +115,14 @@ const _getRedirectUrl = (state: AuthState) => state.redirectUrl;
|
|||||||
|
|
||||||
const _getAuthenticationMethods = (state: AuthState) => state.authMethods;
|
const _getAuthenticationMethods = (state: AuthState) => state.authMethods;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user is idle.
|
||||||
|
* @function _isIdle
|
||||||
|
* @param {State} state
|
||||||
|
* @returns {boolean}
|
||||||
|
*/
|
||||||
|
const _isIdle = (state: AuthState) => state.idle;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Returns the authentication methods enabled at the backend
|
* Returns the authentication methods enabled at the backend
|
||||||
* @function getAuthenticationMethods
|
* @function getAuthenticationMethods
|
||||||
@@ -231,3 +239,12 @@ export const getRegistrationError = createSelector(getAuthState, _getRegistratio
|
|||||||
* @return {string}
|
* @return {string}
|
||||||
*/
|
*/
|
||||||
export const getRedirectUrl = createSelector(getAuthState, _getRedirectUrl);
|
export const getRedirectUrl = createSelector(getAuthState, _getRedirectUrl);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns true if the user is idle
|
||||||
|
* @function isIdle
|
||||||
|
* @param {AuthState} state
|
||||||
|
* @param {any} props
|
||||||
|
* @return {boolean}
|
||||||
|
*/
|
||||||
|
export const isIdle = createSelector(getAuthState, _isIdle);
|
||||||
|
@@ -13,6 +13,7 @@ import {
|
|||||||
BitstreamFormatRegistryState
|
BitstreamFormatRegistryState
|
||||||
} from '../+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
|
} from '../+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
|
||||||
import { historyReducer, HistoryState } from './history/history.reducer';
|
import { historyReducer, HistoryState } from './history/history.reducer';
|
||||||
|
import { metaTagReducer, MetaTagState } from './metadata/meta-tag.reducer';
|
||||||
|
|
||||||
export interface CoreState {
|
export interface CoreState {
|
||||||
'bitstreamFormats': BitstreamFormatRegistryState;
|
'bitstreamFormats': BitstreamFormatRegistryState;
|
||||||
@@ -24,6 +25,7 @@ export interface CoreState {
|
|||||||
'index': MetaIndexState;
|
'index': MetaIndexState;
|
||||||
'auth': AuthState;
|
'auth': AuthState;
|
||||||
'json/patch': JsonPatchOperationsState;
|
'json/patch': JsonPatchOperationsState;
|
||||||
|
'metaTag': MetaTagState;
|
||||||
'route': RouteState;
|
'route': RouteState;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,5 +39,6 @@ export const coreReducers: ActionReducerMap<CoreState> = {
|
|||||||
'index': indexReducer,
|
'index': indexReducer,
|
||||||
'auth': authReducer,
|
'auth': authReducer,
|
||||||
'json/patch': jsonPatchOperationsReducer,
|
'json/patch': jsonPatchOperationsReducer,
|
||||||
|
'metaTag': metaTagReducer,
|
||||||
'route': routeReducer
|
'route': routeReducer
|
||||||
};
|
};
|
||||||
|
23
src/app/core/metadata/meta-tag.actions.ts
Normal file
23
src/app/core/metadata/meta-tag.actions.ts
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
import { type } from '../../shared/ngrx/type';
|
||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
|
||||||
|
// tslint:disable:max-classes-per-file
|
||||||
|
export const MetaTagTypes = {
|
||||||
|
ADD: type('dspace/meta-tag/ADD'),
|
||||||
|
CLEAR: type('dspace/meta-tag/CLEAR')
|
||||||
|
};
|
||||||
|
|
||||||
|
export class AddMetaTagAction implements Action {
|
||||||
|
type = MetaTagTypes.ADD;
|
||||||
|
payload: string;
|
||||||
|
|
||||||
|
constructor(property: string) {
|
||||||
|
this.payload = property;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export class ClearMetaTagAction implements Action {
|
||||||
|
type = MetaTagTypes.CLEAR;
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MetaTagAction = AddMetaTagAction | ClearMetaTagAction;
|
50
src/app/core/metadata/meta-tag.reducer.spec.ts
Normal file
50
src/app/core/metadata/meta-tag.reducer.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
/**
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { metaTagReducer } from './meta-tag.reducer';
|
||||||
|
import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions';
|
||||||
|
|
||||||
|
const nullAction = { type: null };
|
||||||
|
|
||||||
|
describe('metaTagReducer', () => {
|
||||||
|
it('should start with an empty array', () => {
|
||||||
|
const state0 = metaTagReducer(undefined, nullAction);
|
||||||
|
expect(state0.tagsInUse).toEqual([]);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return the current state on invalid action', () => {
|
||||||
|
const state0 = {
|
||||||
|
tagsInUse: ['foo', 'bar'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const state1 = metaTagReducer(state0, nullAction);
|
||||||
|
expect(state1).toEqual(state0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add tags on AddMetaTagAction', () => {
|
||||||
|
const state0 = {
|
||||||
|
tagsInUse: ['foo'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const state1 = metaTagReducer(state0, new AddMetaTagAction('bar'));
|
||||||
|
const state2 = metaTagReducer(state1, new AddMetaTagAction('baz'));
|
||||||
|
|
||||||
|
expect(state1.tagsInUse).toEqual(['foo', 'bar']);
|
||||||
|
expect(state2.tagsInUse).toEqual(['foo', 'bar', 'baz']);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should clear tags on ClearMetaTagAction', () => {
|
||||||
|
const state0 = {
|
||||||
|
tagsInUse: ['foo', 'bar'],
|
||||||
|
};
|
||||||
|
|
||||||
|
const state1 = metaTagReducer(state0, new ClearMetaTagAction());
|
||||||
|
|
||||||
|
expect(state1.tagsInUse).toEqual([]);
|
||||||
|
});
|
||||||
|
});
|
38
src/app/core/metadata/meta-tag.reducer.ts
Normal file
38
src/app/core/metadata/meta-tag.reducer.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import {
|
||||||
|
MetaTagAction,
|
||||||
|
MetaTagTypes,
|
||||||
|
AddMetaTagAction,
|
||||||
|
ClearMetaTagAction,
|
||||||
|
} from './meta-tag.actions';
|
||||||
|
|
||||||
|
export interface MetaTagState {
|
||||||
|
tagsInUse: string[];
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialstate: MetaTagState = {
|
||||||
|
tagsInUse: []
|
||||||
|
};
|
||||||
|
|
||||||
|
export const metaTagReducer = (state: MetaTagState = initialstate, action: MetaTagAction): MetaTagState => {
|
||||||
|
switch (action.type) {
|
||||||
|
case MetaTagTypes.ADD: {
|
||||||
|
return addMetaTag(state, action as AddMetaTagAction);
|
||||||
|
}
|
||||||
|
case MetaTagTypes.CLEAR: {
|
||||||
|
return clearMetaTags(state, action as ClearMetaTagAction);
|
||||||
|
}
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const addMetaTag = (state: MetaTagState, action: AddMetaTagAction): MetaTagState => {
|
||||||
|
return {
|
||||||
|
tagsInUse: [...state.tagsInUse, action.payload]
|
||||||
|
};
|
||||||
|
};
|
||||||
|
|
||||||
|
const clearMetaTags = (state: MetaTagState, action: ClearMetaTagAction): MetaTagState => {
|
||||||
|
return Object.assign({}, initialstate);
|
||||||
|
};
|
@@ -1,82 +1,28 @@
|
|||||||
import { CommonModule, Location } from '@angular/common';
|
import { fakeAsync, tick } from '@angular/core/testing';
|
||||||
import { HttpClient } from '@angular/common/http';
|
import { Meta, Title } from '@angular/platform-browser';
|
||||||
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
import { NavigationEnd, Router } from '@angular/router';
|
||||||
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
|
||||||
import { Meta, MetaDefinition, Title } from '@angular/platform-browser';
|
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
|
||||||
|
|
||||||
import { Store, StoreModule } from '@ngrx/store';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { Observable, of } from 'rxjs';
|
||||||
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
|
||||||
import { EmptyError, Observable, of } from 'rxjs';
|
|
||||||
|
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
|
|
||||||
import {
|
import { ItemMock, MockBitstream1, MockBitstream3 } from '../../shared/mocks/item.mock';
|
||||||
ItemMock,
|
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
MockBitstream1,
|
import { PaginatedList } from '../data/paginated-list.model';
|
||||||
MockBitstream2,
|
|
||||||
MockBitstreamFormat1,
|
|
||||||
MockBitstreamFormat2
|
|
||||||
} from '../../shared/mocks/item.mock';
|
|
||||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
|
||||||
import { AuthService } from '../auth/auth.service';
|
|
||||||
import { BrowseService } from '../browse/browse.service';
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
|
||||||
|
|
||||||
import { CoreState } from '../core.reducers';
|
|
||||||
import { BitstreamDataService } from '../data/bitstream-data.service';
|
|
||||||
import { BitstreamFormatDataService } from '../data/bitstream-format-data.service';
|
|
||||||
import { CommunityDataService } from '../data/community-data.service';
|
|
||||||
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
|
||||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
|
||||||
|
|
||||||
import { ItemDataService } from '../data/item-data.service';
|
|
||||||
import { buildPaginatedList, PaginatedList } from '../data/paginated-list.model';
|
|
||||||
import { FindListOptions } from '../data/request.models';
|
|
||||||
import { RequestService } from '../data/request.service';
|
|
||||||
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
|
||||||
import { Bitstream } from '../shared/bitstream.model';
|
import { Bitstream } from '../shared/bitstream.model';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
|
||||||
import { MetadataValue } from '../shared/metadata.models';
|
import { MetadataValue } from '../shared/metadata.models';
|
||||||
import { PageInfo } from '../shared/page-info.model';
|
|
||||||
import { UUIDService } from '../shared/uuid.service';
|
|
||||||
|
|
||||||
import { MetadataService } from './metadata.service';
|
import { MetadataService } from './metadata.service';
|
||||||
import { environment } from '../../../environments/environment';
|
|
||||||
import { storeModuleConfig } from '../../app.reducer';
|
|
||||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
|
||||||
import { RootDataService } from '../data/root-data.service';
|
import { RootDataService } from '../data/root-data.service';
|
||||||
import { Root } from '../data/root.model';
|
import { Bundle } from '../shared/bundle.model';
|
||||||
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
/* tslint:disable:max-classes-per-file */
|
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||||
@Component({
|
import { DSONameService } from '../breadcrumbs/dso-name.service';
|
||||||
template: `
|
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||||
<router-outlet></router-outlet>`
|
import { getMockStore } from '@ngrx/store/testing';
|
||||||
})
|
import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions';
|
||||||
class TestComponent {
|
|
||||||
constructor(private metadata: MetadataService) {
|
|
||||||
metadata.listenForRouteChange();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@Component({ template: '' })
|
|
||||||
class DummyItemComponent {
|
|
||||||
constructor(private route: ActivatedRoute, private items: ItemDataService, private metadata: MetadataService) {
|
|
||||||
this.route.params.subscribe((params) => {
|
|
||||||
this.metadata.processRemoteData(this.items.findById(params.id));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
|
||||||
|
|
||||||
describe('MetadataService', () => {
|
describe('MetadataService', () => {
|
||||||
let metadataService: MetadataService;
|
let metadataService: MetadataService;
|
||||||
@@ -85,188 +31,339 @@ describe('MetadataService', () => {
|
|||||||
|
|
||||||
let title: Title;
|
let title: Title;
|
||||||
|
|
||||||
let store: Store<CoreState>;
|
let dsoNameService: DSONameService;
|
||||||
|
|
||||||
let objectCacheService: ObjectCacheService;
|
let bundleDataService;
|
||||||
let requestService: RequestService;
|
let bitstreamDataService;
|
||||||
let uuidService: UUIDService;
|
|
||||||
let remoteDataBuildService: RemoteDataBuildService;
|
|
||||||
let itemDataService: ItemDataService;
|
|
||||||
let authService: AuthService;
|
|
||||||
let rootService: RootDataService;
|
let rootService: RootDataService;
|
||||||
let translateService: TranslateService;
|
let translateService: TranslateService;
|
||||||
|
let hardRedirectService: HardRedirectService;
|
||||||
|
|
||||||
let location: Location;
|
|
||||||
let router: Router;
|
let router: Router;
|
||||||
let fixture: ComponentFixture<TestComponent>;
|
let store;
|
||||||
|
|
||||||
|
const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] }}};
|
||||||
|
|
||||||
let tagStore: Map<string, MetaDefinition[]>;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
rootService = jasmine.createSpyObj({
|
||||||
|
findRoot: createSuccessfulRemoteDataObject$({ dspaceVersion: 'mock-dspace-version' })
|
||||||
|
});
|
||||||
|
bitstreamDataService = jasmine.createSpyObj({
|
||||||
|
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([MockBitstream3]))
|
||||||
|
});
|
||||||
|
bundleDataService = jasmine.createSpyObj({
|
||||||
|
findByItemAndName: mockBundleRD$([MockBitstream3])
|
||||||
|
});
|
||||||
|
translateService = getMockTranslateService();
|
||||||
|
meta = jasmine.createSpyObj('meta', {
|
||||||
|
addTag: {},
|
||||||
|
removeTag: {}
|
||||||
|
});
|
||||||
|
title = jasmine.createSpyObj({
|
||||||
|
setTitle: {}
|
||||||
|
});
|
||||||
|
dsoNameService = jasmine.createSpyObj({
|
||||||
|
getName: ItemMock.firstMetadataValue('dc.title')
|
||||||
|
});
|
||||||
|
router = {
|
||||||
|
url: '/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||||
|
events: of(new NavigationEnd(1, '', '')),
|
||||||
|
routerState: {
|
||||||
|
root: {}
|
||||||
|
}
|
||||||
|
} as any as Router;
|
||||||
|
hardRedirectService = jasmine.createSpyObj( {
|
||||||
|
getRequestOrigin: 'https://request.org',
|
||||||
|
});
|
||||||
|
|
||||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
// @ts-ignore
|
||||||
|
store = getMockStore({ initialState });
|
||||||
spyOn(store, 'dispatch');
|
spyOn(store, 'dispatch');
|
||||||
|
|
||||||
objectCacheService = new ObjectCacheService(store, undefined);
|
metadataService = new MetadataService(
|
||||||
uuidService = new UUIDService();
|
router,
|
||||||
requestService = new RequestService(objectCacheService, uuidService, store, undefined);
|
translateService,
|
||||||
remoteDataBuildService = new RemoteDataBuildService(objectCacheService, undefined, requestService);
|
meta,
|
||||||
const mockBitstreamDataService = {
|
title,
|
||||||
findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, ...linksToFollow: FollowLinkConfig<Bitstream>[]): Observable<RemoteData<PaginatedList<Bitstream>>> {
|
dsoNameService,
|
||||||
if (item.equals(ItemMock)) {
|
bundleDataService,
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [MockBitstream1, MockBitstream2]));
|
bitstreamDataService,
|
||||||
} else {
|
undefined,
|
||||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
|
rootService,
|
||||||
}
|
store,
|
||||||
},
|
hardRedirectService
|
||||||
};
|
);
|
||||||
const mockBitstreamFormatDataService = {
|
|
||||||
findByBitstream(bitstream: Bitstream): Observable<RemoteData<BitstreamFormat>> {
|
|
||||||
switch (bitstream) {
|
|
||||||
case MockBitstream1:
|
|
||||||
return createSuccessfulRemoteDataObject$(MockBitstreamFormat1);
|
|
||||||
break;
|
|
||||||
case MockBitstream2:
|
|
||||||
return createSuccessfulRemoteDataObject$(MockBitstreamFormat2);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
return createSuccessfulRemoteDataObject$(new BitstreamFormat());
|
|
||||||
}
|
|
||||||
}
|
|
||||||
};
|
|
||||||
rootService = jasmine.createSpyObj('rootService', {
|
|
||||||
findRoot: createSuccessfulRemoteDataObject$(Object.assign(new Root(), {
|
|
||||||
dspaceVersion: 'mock-dspace-version'
|
|
||||||
}))
|
|
||||||
});
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
CommonModule,
|
|
||||||
StoreModule.forRoot({}, storeModuleConfig),
|
|
||||||
TranslateModule.forRoot({
|
|
||||||
loader: {
|
|
||||||
provide: TranslateLoader,
|
|
||||||
useClass: TranslateLoaderMock
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
RouterTestingModule.withRoutes([
|
|
||||||
{ path: 'items/:id', component: DummyItemComponent, pathMatch: 'full' },
|
|
||||||
{
|
|
||||||
path: 'other',
|
|
||||||
component: DummyItemComponent,
|
|
||||||
pathMatch: 'full',
|
|
||||||
data: { title: 'Dummy Title', description: 'This is a dummy item component for testing!' }
|
|
||||||
}
|
|
||||||
])
|
|
||||||
],
|
|
||||||
declarations: [
|
|
||||||
TestComponent,
|
|
||||||
DummyItemComponent
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: ObjectCacheService, useValue: objectCacheService },
|
|
||||||
{ provide: RequestService, useValue: requestService },
|
|
||||||
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
|
|
||||||
{ provide: HALEndpointService, useValue: {} },
|
|
||||||
{ provide: AuthService, useValue: {} },
|
|
||||||
{ provide: NotificationsService, useValue: {} },
|
|
||||||
{ provide: HttpClient, useValue: {} },
|
|
||||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
|
||||||
{ provide: CommunityDataService, useValue: {} },
|
|
||||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
|
||||||
{ provide: BitstreamFormatDataService, useValue: mockBitstreamFormatDataService },
|
|
||||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
|
||||||
{ provide: RootDataService, useValue: rootService },
|
|
||||||
Meta,
|
|
||||||
Title,
|
|
||||||
// tslint:disable-next-line:no-empty
|
|
||||||
{ provide: ItemDataService, useValue: { findById: () => {} } },
|
|
||||||
BrowseService,
|
|
||||||
MetadataService
|
|
||||||
],
|
|
||||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
|
||||||
});
|
|
||||||
meta = TestBed.inject(Meta);
|
|
||||||
title = TestBed.inject(Title);
|
|
||||||
itemDataService = TestBed.inject(ItemDataService);
|
|
||||||
metadataService = TestBed.inject(MetadataService);
|
|
||||||
authService = TestBed.inject(AuthService);
|
|
||||||
translateService = TestBed.inject(TranslateService);
|
|
||||||
|
|
||||||
router = TestBed.inject(Router);
|
|
||||||
location = TestBed.inject(Location);
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(TestComponent);
|
|
||||||
|
|
||||||
tagStore = metadataService.getTagStore();
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('items page should set meta tags', fakeAsync(() => {
|
it('items page should set meta tags', fakeAsync(() => {
|
||||||
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock));
|
(metadataService as any).processRouteChange({
|
||||||
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
|
data: {
|
||||||
|
value: {
|
||||||
|
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
tick();
|
tick();
|
||||||
expect(title.getTitle()).toEqual('Test PowerPoint Document');
|
expect(title.setTitle).toHaveBeenCalledWith('Test PowerPoint Document');
|
||||||
expect(tagStore.get('citation_title')[0].content).toEqual('Test PowerPoint Document');
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
expect(tagStore.get('citation_author')[0].content).toEqual('Doe, Jane');
|
property: 'citation_title',
|
||||||
expect(tagStore.get('citation_date')[0].content).toEqual('1650-06-26');
|
content: 'Test PowerPoint Document'
|
||||||
expect(tagStore.get('citation_issn')[0].content).toEqual('123456789');
|
});
|
||||||
expect(tagStore.get('citation_language')[0].content).toEqual('en');
|
expect(meta.addTag).toHaveBeenCalledWith({ property: 'citation_author', content: 'Doe, Jane' });
|
||||||
expect(tagStore.get('citation_keywords')[0].content).toEqual('keyword1; keyword2; keyword3');
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'citation_publication_date',
|
||||||
|
content: '1650-06-26'
|
||||||
|
});
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({ property: 'citation_issn', content: '123456789' });
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({ property: 'citation_language', content: 'en' });
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'citation_keywords',
|
||||||
|
content: 'keyword1; keyword2; keyword3'
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('items page should set meta tags as published Thesis', fakeAsync(() => {
|
it('items page should set meta tags as published Thesis', fakeAsync(() => {
|
||||||
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(ItemMock, 'Thesis'))));
|
(metadataService as any).processRouteChange({
|
||||||
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
|
data: {
|
||||||
|
value: {
|
||||||
|
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
tick();
|
tick();
|
||||||
expect(tagStore.get('citation_dissertation_name')[0].content).toEqual('Test PowerPoint Document');
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
expect(tagStore.get('citation_dissertation_institution')[0].content).toEqual('Mock Publisher');
|
property: 'citation_dissertation_name',
|
||||||
expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual([environment.ui.baseUrl, router.url].join(''));
|
content: 'Test PowerPoint Document'
|
||||||
expect(tagStore.get('citation_pdf_url')[0].content).toEqual('/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download');
|
});
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'citation_pdf_url',
|
||||||
|
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download'
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
it('items page should set meta tags as published Technical Report', fakeAsync(() => {
|
it('items page should set meta tags as published Technical Report', fakeAsync(() => {
|
||||||
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(ItemMock, 'Technical Report'))));
|
(metadataService as any).processRouteChange({
|
||||||
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
|
data: {
|
||||||
tick();
|
value: {
|
||||||
expect(tagStore.get('citation_technical_report_institution')[0].content).toEqual('Mock Publisher');
|
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))),
|
||||||
}));
|
}
|
||||||
|
}
|
||||||
it('other navigation should add title, description and Generator', fakeAsync(() => {
|
|
||||||
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock));
|
|
||||||
spyOn(translateService, 'get').and.returnValues(of('DSpace :: '), of('Dummy Title'), of('This is a dummy item component for testing!'));
|
|
||||||
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
|
|
||||||
tick();
|
|
||||||
expect(tagStore.size).toBeGreaterThan(0);
|
|
||||||
router.navigate(['/other']);
|
|
||||||
tick();
|
|
||||||
expect(tagStore.size).toEqual(3);
|
|
||||||
expect(title.getTitle()).toEqual('DSpace :: Dummy Title');
|
|
||||||
expect(tagStore.get('title')[0].content).toEqual('DSpace :: Dummy Title');
|
|
||||||
expect(tagStore.get('description')[0].content).toEqual('This is a dummy item component for testing!');
|
|
||||||
expect(tagStore.get('Generator')[0].content).toEqual('mock-dspace-version');
|
|
||||||
}));
|
|
||||||
|
|
||||||
describe('when the item has no bitstreams', () => {
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
// this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL')
|
|
||||||
// spyOn(MockItem, 'getFiles').and.returnValue(observableOf([]));
|
|
||||||
});
|
});
|
||||||
|
tick();
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'citation_technical_report_institution',
|
||||||
|
content: 'Mock Publisher'
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
it('processRemoteData should not produce an EmptyError', fakeAsync(() => {
|
it('other navigation should add title and description', fakeAsync(() => {
|
||||||
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock));
|
(translateService.get as jasmine.Spy).and.returnValues(of('DSpace :: '), of('Dummy Title'), of('This is a dummy item component for testing!'));
|
||||||
spyOn(metadataService, 'processRemoteData').and.callThrough();
|
(metadataService as any).processRouteChange({
|
||||||
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
|
data: {
|
||||||
|
value: {
|
||||||
|
title: 'Dummy Title',
|
||||||
|
description: 'This is a dummy item component for testing!'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
expect(title.setTitle).toHaveBeenCalledWith('DSpace :: Dummy Title');
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'title',
|
||||||
|
content: 'DSpace :: Dummy Title'
|
||||||
|
});
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'description',
|
||||||
|
content: 'This is a dummy item component for testing!'
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe(`listenForRouteChange`, () => {
|
||||||
|
it(`should call processRouteChange`, fakeAsync(() => {
|
||||||
|
spyOn(metadataService as any, 'processRouteChange').and.callFake(() => undefined);
|
||||||
|
metadataService.listenForRouteChange();
|
||||||
tick();
|
tick();
|
||||||
expect(metadataService.processRemoteData).not.toThrow(new EmptyError());
|
expect((metadataService as any).processRouteChange).toHaveBeenCalled();
|
||||||
|
}));
|
||||||
|
it(`should add Generator`, fakeAsync(() => {
|
||||||
|
spyOn(metadataService as any, 'processRouteChange').and.callFake(() => undefined);
|
||||||
|
metadataService.listenForRouteChange();
|
||||||
|
tick();
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'Generator',
|
||||||
|
content: 'mock-dspace-version'
|
||||||
|
});
|
||||||
}));
|
}));
|
||||||
|
|
||||||
});
|
});
|
||||||
|
|
||||||
const mockRemoteData = (mockItem: Item): Observable<RemoteData<Item>> => {
|
describe('citation_abstract_html_url', () => {
|
||||||
return createSuccessfulRemoteDataObject$(ItemMock);
|
it('should use dc.identifier.uri if available', fakeAsync(() => {
|
||||||
};
|
(metadataService as any).processRouteChange({
|
||||||
|
data: {
|
||||||
|
value: {
|
||||||
|
dso: createSuccessfulRemoteDataObject(mockUri(ItemMock, 'https://ddg.gg')),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'citation_abstract_html_url',
|
||||||
|
content: 'https://ddg.gg'
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should use current route as fallback', fakeAsync(() => {
|
||||||
|
(metadataService as any).processRouteChange({
|
||||||
|
data: {
|
||||||
|
value: {
|
||||||
|
dso: createSuccessfulRemoteDataObject(mockUri(ItemMock)),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'citation_abstract_html_url',
|
||||||
|
content: 'https://request.org/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357'
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('citation_*_institution / citation_publisher', () => {
|
||||||
|
it('should use citation_dissertation_institution tag for dissertations', fakeAsync(() => {
|
||||||
|
(metadataService as any).processRouteChange({
|
||||||
|
data: {
|
||||||
|
value: {
|
||||||
|
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'citation_dissertation_institution',
|
||||||
|
content: 'Mock Publisher'
|
||||||
|
});
|
||||||
|
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_technical_report_institution' }));
|
||||||
|
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_publisher' }));
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should use citation_tech_report_institution tag for tech reports', fakeAsync(() => {
|
||||||
|
(metadataService as any).processRouteChange({
|
||||||
|
data: {
|
||||||
|
value: {
|
||||||
|
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_dissertation_institution' }));
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'citation_technical_report_institution',
|
||||||
|
content: 'Mock Publisher'
|
||||||
|
});
|
||||||
|
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_publisher' }));
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should use citation_publisher for other item types', fakeAsync(() => {
|
||||||
|
(metadataService as any).processRouteChange({
|
||||||
|
data: {
|
||||||
|
value: {
|
||||||
|
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Some Other Type'))),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_dissertation_institution' }));
|
||||||
|
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_technical_report_institution' }));
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'citation_publisher',
|
||||||
|
content: 'Mock Publisher'
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('citation_pdf_url', () => {
|
||||||
|
it('should link to primary Bitstream URL regardless of format', fakeAsync(() => {
|
||||||
|
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([], MockBitstream3));
|
||||||
|
|
||||||
|
(metadataService as any).processRouteChange({
|
||||||
|
data: {
|
||||||
|
value: {
|
||||||
|
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'citation_pdf_url',
|
||||||
|
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download'
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
describe('no primary Bitstream', () => {
|
||||||
|
it('should link to first and only Bitstream regardless of format', fakeAsync(() => {
|
||||||
|
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([MockBitstream3]));
|
||||||
|
|
||||||
|
(metadataService as any).processRouteChange({
|
||||||
|
data: {
|
||||||
|
value: {
|
||||||
|
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'citation_pdf_url',
|
||||||
|
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download'
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should link to first Bitstream with allowed format', fakeAsync(() => {
|
||||||
|
const bitstreams = [MockBitstream3, MockBitstream3, MockBitstream1];
|
||||||
|
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams));
|
||||||
|
(bitstreamDataService.findAllByHref as jasmine.Spy).and.returnValues(
|
||||||
|
...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)),
|
||||||
|
);
|
||||||
|
|
||||||
|
(metadataService as any).processRouteChange({
|
||||||
|
data: {
|
||||||
|
value: {
|
||||||
|
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
expect(meta.addTag).toHaveBeenCalledWith({
|
||||||
|
property: 'citation_pdf_url',
|
||||||
|
content: 'https://request.org/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/download'
|
||||||
|
});
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tagstore', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
(metadataService as any).processRouteChange({
|
||||||
|
data: {
|
||||||
|
value: {
|
||||||
|
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should remove previous tags on route change', fakeAsync(() => {
|
||||||
|
expect(meta.removeTag).toHaveBeenCalledWith('property=\'title\'');
|
||||||
|
expect(meta.removeTag).toHaveBeenCalledWith('property=\'description\'');
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should clear all tags and add new ones on route change', () => {
|
||||||
|
expect(store.dispatch.calls.argsFor(0)).toEqual([new ClearMetaTagAction()]);
|
||||||
|
expect(store.dispatch.calls.argsFor(1)).toEqual([new AddMetaTagAction('title')]);
|
||||||
|
expect(store.dispatch.calls.argsFor(2)).toEqual([new AddMetaTagAction('description')]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
const mockType = (mockItem: Item, type: string): Item => {
|
const mockType = (mockItem: Item, type: string): Item => {
|
||||||
const typedMockItem = Object.assign(new Item(), mockItem) as Item;
|
const typedMockItem = Object.assign(new Item(), mockItem) as Item;
|
||||||
@@ -285,4 +382,30 @@ describe('MetadataService', () => {
|
|||||||
return publishedMockItem;
|
return publishedMockItem;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
const mockUri = (mockItem: Item, uri?: string): Item => {
|
||||||
|
const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
|
||||||
|
publishedMockItem.metadata['dc.identifier.uri'] = [{ value: uri }] as MetadataValue[];
|
||||||
|
return publishedMockItem;
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockBundleRD$ = (bitstreams: Bitstream[], primary?: Bitstream): Observable<RemoteData<Bundle>> => {
|
||||||
|
return createSuccessfulRemoteDataObject$(
|
||||||
|
Object.assign(new Bundle(), {
|
||||||
|
name: 'ORIGINAL',
|
||||||
|
bitstreams: createSuccessfulRemoteDataObject$(mockBitstreamPages$(bitstreams)[0]),
|
||||||
|
primaryBitstream: createSuccessfulRemoteDataObject$(primary),
|
||||||
|
})
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const mockBitstreamPages$ = (bitstreams: Bitstream[]): PaginatedList<Bitstream>[] => {
|
||||||
|
return bitstreams.map((bitstream, index) => Object.assign(createPaginatedList([bitstream]), {
|
||||||
|
pageInfo: {
|
||||||
|
totalElements: bitstreams.length, // announce multiple elements/pages
|
||||||
|
},
|
||||||
|
_links: index < bitstreams.length - 1
|
||||||
|
? { next: { href: 'not empty' }} // fake link to the next bitstream page
|
||||||
|
: { next: { href: undefined }}, // last page has no link
|
||||||
|
}));
|
||||||
|
};
|
||||||
});
|
});
|
||||||
|
@@ -5,12 +5,11 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
|||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
import { BehaviorSubject, combineLatest, EMPTY, Observable, of as observableOf } from 'rxjs';
|
||||||
import { catchError, distinctUntilKeyChanged, filter, first, map, take } from 'rxjs/operators';
|
import { expand, filter, map, switchMap, take } from 'rxjs/operators';
|
||||||
|
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||||
import { DSONameService } from '../breadcrumbs/dso-name.service';
|
import { DSONameService } from '../breadcrumbs/dso-name.service';
|
||||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
|
||||||
import { BitstreamDataService } from '../data/bitstream-data.service';
|
import { BitstreamDataService } from '../data/bitstream-data.service';
|
||||||
import { BitstreamFormatDataService } from '../data/bitstream-format-data.service';
|
import { BitstreamFormatDataService } from '../data/bitstream-format-data.service';
|
||||||
|
|
||||||
@@ -19,22 +18,57 @@ import { BitstreamFormat } from '../shared/bitstream-format.model';
|
|||||||
import { Bitstream } from '../shared/bitstream.model';
|
import { Bitstream } from '../shared/bitstream.model';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
import {
|
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||||
getFirstSucceededRemoteDataPayload,
|
|
||||||
getFirstSucceededRemoteListPayload
|
|
||||||
} from '../shared/operators';
|
|
||||||
import { environment } from '../../../environments/environment';
|
|
||||||
import { RootDataService } from '../data/root-data.service';
|
import { RootDataService } from '../data/root-data.service';
|
||||||
import { getBitstreamDownloadRoute } from '../../app-routing-paths';
|
import { getBitstreamDownloadRoute } from '../../app-routing-paths';
|
||||||
|
import { BundleDataService } from '../data/bundle-data.service';
|
||||||
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
|
import { Bundle } from '../shared/bundle.model';
|
||||||
|
import { PaginatedList } from '../data/paginated-list.model';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||||
|
import { MetaTagState } from './meta-tag.reducer';
|
||||||
|
import { createSelector, select, Store } from '@ngrx/store';
|
||||||
|
import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions';
|
||||||
|
import { coreSelector } from '../core.selectors';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The base selector function to select the metaTag section in the store
|
||||||
|
*/
|
||||||
|
const metaTagSelector = createSelector(
|
||||||
|
coreSelector,
|
||||||
|
(state: CoreState) => state.metaTag
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Selector function to select the tags in use from the MetaTagState
|
||||||
|
*/
|
||||||
|
const tagsInUseSelector =
|
||||||
|
createSelector(
|
||||||
|
metaTagSelector,
|
||||||
|
(state: MetaTagState) => state.tagsInUse,
|
||||||
|
);
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class MetadataService {
|
export class MetadataService {
|
||||||
|
|
||||||
private initialized: boolean;
|
private currentObject: BehaviorSubject<DSpaceObject> = new BehaviorSubject<DSpaceObject>(undefined);
|
||||||
|
|
||||||
private tagStore: Map<string, MetaDefinition[]>;
|
/**
|
||||||
|
* When generating the citation_pdf_url meta tag for Items with more than one Bitstream (and no primary Bitstream),
|
||||||
private currentObject: BehaviorSubject<DSpaceObject>;
|
* the first Bitstream to match one of the following MIME types is selected.
|
||||||
|
* See {@linkcode getFirstAllowedFormatBitstreamLink}
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private readonly CITATION_PDF_URL_MIMETYPES = [
|
||||||
|
'application/pdf', // .pdf
|
||||||
|
'application/postscript', // .ps
|
||||||
|
'application/msword', // .doc
|
||||||
|
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
|
||||||
|
'application/rtf', // .rtf
|
||||||
|
'application/epub+zip', // .epub
|
||||||
|
];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private router: Router,
|
private router: Router,
|
||||||
@@ -42,21 +76,19 @@ export class MetadataService {
|
|||||||
private meta: Meta,
|
private meta: Meta,
|
||||||
private title: Title,
|
private title: Title,
|
||||||
private dsoNameService: DSONameService,
|
private dsoNameService: DSONameService,
|
||||||
|
private bundleDataService: BundleDataService,
|
||||||
private bitstreamDataService: BitstreamDataService,
|
private bitstreamDataService: BitstreamDataService,
|
||||||
private bitstreamFormatDataService: BitstreamFormatDataService,
|
private bitstreamFormatDataService: BitstreamFormatDataService,
|
||||||
private rootService: RootDataService
|
private rootService: RootDataService,
|
||||||
|
private store: Store<CoreState>,
|
||||||
|
private hardRedirectService: HardRedirectService,
|
||||||
) {
|
) {
|
||||||
// TODO: determine what open graph meta tags are needed and whether
|
|
||||||
// the differ per route. potentially add image based on DSpaceObject
|
|
||||||
this.meta.addTags([
|
|
||||||
{ property: 'og:title', content: 'DSpace Angular Universal' },
|
|
||||||
{ property: 'og:description', content: 'The modern front-end for DSpace 7.' }
|
|
||||||
]);
|
|
||||||
this.initialized = false;
|
|
||||||
this.tagStore = new Map<string, MetaDefinition[]>();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public listenForRouteChange(): void {
|
public listenForRouteChange(): void {
|
||||||
|
// This never changes, set it only once
|
||||||
|
this.setGenerator();
|
||||||
|
|
||||||
this.router.events.pipe(
|
this.router.events.pipe(
|
||||||
filter((event) => event instanceof NavigationEnd),
|
filter((event) => event instanceof NavigationEnd),
|
||||||
map(() => this.router.routerState.root),
|
map(() => this.router.routerState.root),
|
||||||
@@ -68,22 +100,9 @@ export class MetadataService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
public processRemoteData(remoteData: Observable<RemoteData<CacheableObject>>): void {
|
|
||||||
remoteData.pipe(map((rd: RemoteData<CacheableObject>) => rd.payload),
|
|
||||||
filter((co: CacheableObject) => hasValue(co)),
|
|
||||||
take(1))
|
|
||||||
.subscribe((dspaceObject: DSpaceObject) => {
|
|
||||||
if (!this.initialized) {
|
|
||||||
this.initialize(dspaceObject);
|
|
||||||
}
|
|
||||||
this.currentObject.next(dspaceObject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
private processRouteChange(routeInfo: any): void {
|
private processRouteChange(routeInfo: any): void {
|
||||||
if (routeInfo.params.value.id === undefined) {
|
this.clearMetaTags();
|
||||||
this.clearMetaTags();
|
|
||||||
}
|
|
||||||
if (routeInfo.data.value.title) {
|
if (routeInfo.data.value.title) {
|
||||||
const titlePrefix = this.translate.get('repository.title.prefix');
|
const titlePrefix = this.translate.get('repository.title.prefix');
|
||||||
const title = this.translate.get(routeInfo.data.value.title, routeInfo.data.value);
|
const title = this.translate.get(routeInfo.data.value.title, routeInfo.data.value);
|
||||||
@@ -98,15 +117,10 @@ export class MetadataService {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
this.setGenerator();
|
if (hasValue(routeInfo.data.value.dso) && hasValue(routeInfo.data.value.dso.payload)) {
|
||||||
}
|
this.currentObject.next(routeInfo.data.value.dso.payload);
|
||||||
|
this.setDSOMetaTags();
|
||||||
private initialize(dspaceObject: DSpaceObject): void {
|
}
|
||||||
this.currentObject = new BehaviorSubject<DSpaceObject>(dspaceObject);
|
|
||||||
this.currentObject.asObservable().pipe(distinctUntilKeyChanged('uuid')).subscribe(() => {
|
|
||||||
this.setMetaTags();
|
|
||||||
});
|
|
||||||
this.initialized = true;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
private getCurrentRoute(route: ActivatedRoute): ActivatedRoute {
|
private getCurrentRoute(route: ActivatedRoute): ActivatedRoute {
|
||||||
@@ -116,16 +130,14 @@ export class MetadataService {
|
|||||||
return route;
|
return route;
|
||||||
}
|
}
|
||||||
|
|
||||||
private setMetaTags(): void {
|
private setDSOMetaTags(): void {
|
||||||
|
|
||||||
this.clearMetaTags();
|
|
||||||
|
|
||||||
this.setTitleTag();
|
this.setTitleTag();
|
||||||
this.setDescriptionTag();
|
this.setDescriptionTag();
|
||||||
|
|
||||||
this.setCitationTitleTag();
|
this.setCitationTitleTag();
|
||||||
this.setCitationAuthorTags();
|
this.setCitationAuthorTags();
|
||||||
this.setCitationDateTag();
|
this.setCitationPublicationDateTag();
|
||||||
this.setCitationISSNTag();
|
this.setCitationISSNTag();
|
||||||
this.setCitationISBNTag();
|
this.setCitationISBNTag();
|
||||||
|
|
||||||
@@ -134,14 +146,10 @@ export class MetadataService {
|
|||||||
|
|
||||||
this.setCitationAbstractUrlTag();
|
this.setCitationAbstractUrlTag();
|
||||||
this.setCitationPdfUrlTag();
|
this.setCitationPdfUrlTag();
|
||||||
|
this.setCitationPublisherTag();
|
||||||
|
|
||||||
if (this.isDissertation()) {
|
if (this.isDissertation()) {
|
||||||
this.setCitationDissertationNameTag();
|
this.setCitationDissertationNameTag();
|
||||||
this.setCitationDissertationInstitutionTag();
|
|
||||||
}
|
|
||||||
|
|
||||||
if (this.isTechReport()) {
|
|
||||||
this.setCitationTechReportInstitutionTag();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// this.setCitationJournalTitleTag();
|
// this.setCitationJournalTitleTag();
|
||||||
@@ -176,7 +184,7 @@ export class MetadataService {
|
|||||||
private setDescriptionTag(): void {
|
private setDescriptionTag(): void {
|
||||||
// TODO: truncate abstract
|
// TODO: truncate abstract
|
||||||
const value = this.getMetaTagValue('dc.description.abstract');
|
const value = this.getMetaTagValue('dc.description.abstract');
|
||||||
this.addMetaTag('desciption', value);
|
this.addMetaTag('description', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -196,11 +204,11 @@ export class MetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add <meta name="citation_date" ... > to the <head>
|
* Add <meta name="citation_publication_date" ... > to the <head>
|
||||||
*/
|
*/
|
||||||
private setCitationDateTag(): void {
|
private setCitationPublicationDateTag(): void {
|
||||||
const value = this.getFirstMetaTagValue(['dc.date.copyright', 'dc.date.issued', 'dc.date.available', 'dc.date.accessioned']);
|
const value = this.getFirstMetaTagValue(['dc.date.copyright', 'dc.date.issued', 'dc.date.available', 'dc.date.accessioned']);
|
||||||
this.addMetaTag('citation_date', value);
|
this.addMetaTag('citation_publication_date', value);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -236,19 +244,17 @@ export class MetadataService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add <meta name="citation_dissertation_institution" ... > to the <head>
|
* Add dc.publisher to the <head>. The tag name depends on the item type.
|
||||||
*/
|
*/
|
||||||
private setCitationDissertationInstitutionTag(): void {
|
private setCitationPublisherTag(): void {
|
||||||
const value = this.getMetaTagValue('dc.publisher');
|
const value = this.getMetaTagValue('dc.publisher');
|
||||||
this.addMetaTag('citation_dissertation_institution', value);
|
if (this.isDissertation()) {
|
||||||
}
|
this.addMetaTag('citation_dissertation_institution', value);
|
||||||
|
} else if (this.isTechReport()) {
|
||||||
/**
|
this.addMetaTag('citation_technical_report_institution', value);
|
||||||
* Add <meta name="citation_technical_report_institution" ... > to the <head>
|
} else {
|
||||||
*/
|
this.addMetaTag('citation_publisher', value);
|
||||||
private setCitationTechReportInstitutionTag(): void {
|
}
|
||||||
const value = this.getMetaTagValue('dc.publisher');
|
|
||||||
this.addMetaTag('citation_technical_report_institution', value);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -264,8 +270,11 @@ export class MetadataService {
|
|||||||
*/
|
*/
|
||||||
private setCitationAbstractUrlTag(): void {
|
private setCitationAbstractUrlTag(): void {
|
||||||
if (this.currentObject.value instanceof Item) {
|
if (this.currentObject.value instanceof Item) {
|
||||||
const value = [environment.ui.baseUrl, this.router.url].join('');
|
let url = this.getMetaTagValue('dc.identifier.uri');
|
||||||
this.addMetaTag('citation_abstract_html_url', value);
|
if (hasNoValue(url)) {
|
||||||
|
url = new URLCombiner(this.hardRedirectService.getRequestOrigin(), this.router.url).toString();
|
||||||
|
}
|
||||||
|
this.addMetaTag('citation_abstract_html_url', url);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -275,35 +284,126 @@ export class MetadataService {
|
|||||||
private setCitationPdfUrlTag(): void {
|
private setCitationPdfUrlTag(): void {
|
||||||
if (this.currentObject.value instanceof Item) {
|
if (this.currentObject.value instanceof Item) {
|
||||||
const item = this.currentObject.value as Item;
|
const item = this.currentObject.value as Item;
|
||||||
this.bitstreamDataService.findAllByItemAndBundleName(item, 'ORIGINAL')
|
|
||||||
.pipe(
|
// Retrieve the ORIGINAL bundle for the item
|
||||||
getFirstSucceededRemoteListPayload(),
|
this.bundleDataService.findByItemAndName(
|
||||||
first((files) => isNotEmpty(files)),
|
item,
|
||||||
catchError((error) => {
|
'ORIGINAL',
|
||||||
console.debug(error.message);
|
true,
|
||||||
return [];
|
true,
|
||||||
}))
|
followLink('primaryBitstream'),
|
||||||
.subscribe((bitstreams: Bitstream[]) => {
|
followLink('bitstreams', {}, followLink('format')),
|
||||||
for (const bitstream of bitstreams) {
|
).pipe(
|
||||||
this.bitstreamFormatDataService.findByBitstream(bitstream).pipe(
|
getFirstSucceededRemoteDataPayload(),
|
||||||
getFirstSucceededRemoteDataPayload()
|
switchMap((bundle: Bundle) =>
|
||||||
).subscribe((format: BitstreamFormat) => {
|
|
||||||
if (format.mimetype === 'application/pdf') {
|
// First try the primary bitstream
|
||||||
const bitstreamLink = getBitstreamDownloadRoute(bitstream);
|
bundle.primaryBitstream.pipe(
|
||||||
this.addMetaTag('citation_pdf_url', bitstreamLink);
|
getFirstCompletedRemoteData(),
|
||||||
|
map((rd: RemoteData<Bitstream>) => {
|
||||||
|
if (hasValue(rd.payload)) {
|
||||||
|
return rd.payload;
|
||||||
|
} else {
|
||||||
|
return null;
|
||||||
}
|
}
|
||||||
});
|
}),
|
||||||
|
// return the bundle as well so we can use it again if there's no primary bitstream
|
||||||
|
map((bitstream: Bitstream) => [bundle, bitstream])
|
||||||
|
)
|
||||||
|
),
|
||||||
|
switchMap(([bundle, primaryBitstream]: [Bundle, Bitstream]) => {
|
||||||
|
if (hasValue(primaryBitstream)) {
|
||||||
|
// If there was a primary bitstream, emit its link
|
||||||
|
return [getBitstreamDownloadRoute(primaryBitstream)];
|
||||||
|
} else {
|
||||||
|
// Otherwise consider the regular bitstreams in the bundle
|
||||||
|
return bundle.bitstreams.pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
switchMap((bitstreamRd: RemoteData<PaginatedList<Bitstream>>) => {
|
||||||
|
if (hasValue(bitstreamRd.payload) && bitstreamRd.payload.totalElements === 1) {
|
||||||
|
// If there's only one bitstream in the bundle, emit its link
|
||||||
|
return [getBitstreamDownloadRoute(bitstreamRd.payload.page[0])];
|
||||||
|
} else {
|
||||||
|
// Otherwise check all bitstreams to see if one matches the format whitelist
|
||||||
|
return this.getFirstAllowedFormatBitstreamLink(bitstreamRd);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
});
|
}),
|
||||||
|
take(1)
|
||||||
|
).subscribe((link: string) => {
|
||||||
|
// Use the found link to set the <meta> tag
|
||||||
|
this.addMetaTag(
|
||||||
|
'citation_pdf_url',
|
||||||
|
new URLCombiner(this.hardRedirectService.getRequestOrigin(), link).toString()
|
||||||
|
);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* For Items with more than one Bitstream (and no primary Bitstream), link to the first Bitstream with a MIME type
|
||||||
|
* included in {@linkcode CITATION_PDF_URL_MIMETYPES}
|
||||||
|
* @param bitstreamRd
|
||||||
|
* @private
|
||||||
|
*/
|
||||||
|
private getFirstAllowedFormatBitstreamLink(bitstreamRd: RemoteData<PaginatedList<Bitstream>>): Observable<string> {
|
||||||
|
return observableOf(bitstreamRd.payload).pipe(
|
||||||
|
// Because there can be more than one page of bitstreams, this expand operator
|
||||||
|
// will retrieve them in turn. Due to the take(1) at the bottom, it will only
|
||||||
|
// retrieve pages until a match is found
|
||||||
|
expand((paginatedList: PaginatedList<Bitstream>) => {
|
||||||
|
if (hasNoValue(paginatedList.next)) {
|
||||||
|
// If there's no next page, stop.
|
||||||
|
return EMPTY;
|
||||||
|
} else {
|
||||||
|
// Otherwise retrieve the next page
|
||||||
|
return this.bitstreamDataService.findAllByHref(
|
||||||
|
paginatedList.next,
|
||||||
|
undefined,
|
||||||
|
true,
|
||||||
|
true,
|
||||||
|
followLink('format')
|
||||||
|
).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((next: RemoteData<PaginatedList<Bitstream>>) => {
|
||||||
|
if (hasValue(next.payload)) {
|
||||||
|
return next.payload;
|
||||||
|
} else {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// Return the array of bitstreams inside each paginated list
|
||||||
|
map((paginatedList: PaginatedList<Bitstream>) => paginatedList.page),
|
||||||
|
// Emit the bitstreams in the list one at a time
|
||||||
|
switchMap((bitstreams: Bitstream[]) => bitstreams),
|
||||||
|
// Retrieve the format for each bitstream
|
||||||
|
switchMap((bitstream: Bitstream) => bitstream.format.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
// Keep the original bitstream, because it, not the format, is what we'll need
|
||||||
|
// for the link at the end
|
||||||
|
map((format: BitstreamFormat) => [bitstream, format])
|
||||||
|
)),
|
||||||
|
// Filter out only pairs with whitelisted formats
|
||||||
|
filter(([, format]: [Bitstream, BitstreamFormat]) =>
|
||||||
|
hasValue(format) && this.CITATION_PDF_URL_MIMETYPES.includes(format.mimetype)),
|
||||||
|
// We only need 1
|
||||||
|
take(1),
|
||||||
|
// Emit the link of the match
|
||||||
|
map(([bitstream, ]: [Bitstream, BitstreamFormat]) => getBitstreamDownloadRoute(bitstream))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Add <meta name="Generator" ... > to the <head> containing the current DSpace version
|
* Add <meta name="Generator" ... > to the <head> containing the current DSpace version
|
||||||
*/
|
*/
|
||||||
private setGenerator(): void {
|
private setGenerator(): void {
|
||||||
this.rootService.findRoot().pipe(getFirstSucceededRemoteDataPayload()).subscribe((root) => {
|
this.rootService.findRoot().pipe(getFirstSucceededRemoteDataPayload()).subscribe((root) => {
|
||||||
this.addMetaTag('Generator', root.dspaceVersion);
|
this.meta.addTag({ property: 'Generator', content: root.dspaceVersion });
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -351,7 +451,7 @@ export class MetadataService {
|
|||||||
if (content) {
|
if (content) {
|
||||||
const tag = { property, content } as MetaDefinition;
|
const tag = { property, content } as MetaDefinition;
|
||||||
this.meta.addTag(tag);
|
this.meta.addTag(tag);
|
||||||
this.storeTag(property, tag);
|
this.storeTag(property);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,33 +461,21 @@ export class MetadataService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private storeTag(key: string, tag: MetaDefinition): void {
|
private storeTag(key: string): void {
|
||||||
const tags: MetaDefinition[] = this.getTags(key);
|
this.store.dispatch(new AddMetaTagAction(key));
|
||||||
tags.push(tag);
|
|
||||||
this.setTags(key, tags);
|
|
||||||
}
|
|
||||||
|
|
||||||
private getTags(key: string): MetaDefinition[] {
|
|
||||||
let tags: MetaDefinition[] = this.tagStore.get(key);
|
|
||||||
if (tags === undefined) {
|
|
||||||
tags = [];
|
|
||||||
}
|
|
||||||
return tags;
|
|
||||||
}
|
|
||||||
|
|
||||||
private setTags(key: string, tags: MetaDefinition[]): void {
|
|
||||||
this.tagStore.set(key, tags);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public clearMetaTags() {
|
public clearMetaTags() {
|
||||||
this.tagStore.forEach((tags: MetaDefinition[], property: string) => {
|
this.store.pipe(
|
||||||
this.meta.removeTag('property=\'' + property + '\'');
|
select(tagsInUseSelector),
|
||||||
|
take(1)
|
||||||
|
).subscribe((tagsInUse: string[]) => {
|
||||||
|
for (const property of tagsInUse) {
|
||||||
|
this.meta.removeTag('property=\'' + property + '\'');
|
||||||
|
}
|
||||||
|
this.store.dispatch(new ClearMetaTagAction());
|
||||||
});
|
});
|
||||||
this.tagStore.clear();
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public getTagStore(): Map<string, MetaDefinition[]> {
|
|
||||||
return this.tagStore;
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
19
src/app/core/utilities/enter-zone.scheduler.ts
Normal file
19
src/app/core/utilities/enter-zone.scheduler.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { SchedulerLike, Subscription } from 'rxjs';
|
||||||
|
import { NgZone } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An RXJS scheduler that will re-enter the Angular zone to run what's scheduled
|
||||||
|
*/
|
||||||
|
export class EnterZoneScheduler implements SchedulerLike {
|
||||||
|
constructor(private zone: NgZone, private scheduler: SchedulerLike) { }
|
||||||
|
|
||||||
|
schedule(...args: any[]): Subscription {
|
||||||
|
return this.zone.run(() =>
|
||||||
|
this.scheduler.schedule.apply(this.scheduler, args)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
now (): number {
|
||||||
|
return this.scheduler.now();
|
||||||
|
}
|
||||||
|
}
|
19
src/app/core/utilities/leave-zone.scheduler.ts
Normal file
19
src/app/core/utilities/leave-zone.scheduler.ts
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
import { SchedulerLike, Subscription } from 'rxjs';
|
||||||
|
import { NgZone } from '@angular/core';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An RXJS scheduler that will run what's scheduled outside of the Angular zone
|
||||||
|
*/
|
||||||
|
export class LeaveZoneScheduler implements SchedulerLike {
|
||||||
|
constructor(private zone: NgZone, private scheduler: SchedulerLike) { }
|
||||||
|
|
||||||
|
schedule(...args: any[]): Subscription {
|
||||||
|
return this.zone.runOutsideAngular(() =>
|
||||||
|
this.scheduler.schedule.apply(this.scheduler, args)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
now (): number {
|
||||||
|
return this.scheduler.now();
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
import { Component, Inject, OnInit, Optional, Input } from '@angular/core';
|
import { Component, Inject, OnInit, Input } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
|
|
||||||
import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs';
|
import { combineLatest as combineLatestObservable, Observable, of } from 'rxjs';
|
||||||
@@ -18,8 +18,6 @@ import { HostWindowService } from '../shared/host-window.service';
|
|||||||
import { ThemeConfig } from '../../config/theme.model';
|
import { ThemeConfig } from '../../config/theme.model';
|
||||||
import { Angulartics2DSpace } from '../statistics/angulartics/dspace-provider';
|
import { Angulartics2DSpace } from '../statistics/angulartics/dspace-provider';
|
||||||
import { environment } from '../../environments/environment';
|
import { environment } from '../../environments/environment';
|
||||||
import { LocaleService } from '../core/locale/locale.service';
|
|
||||||
import { KlaroService } from '../shared/cookies/klaro.service';
|
|
||||||
import { slideSidebarPadding } from '../shared/animations/slide';
|
import { slideSidebarPadding } from '../shared/animations/slide';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -58,9 +56,7 @@ export class RootComponent implements OnInit {
|
|||||||
private router: Router,
|
private router: Router,
|
||||||
private cssService: CSSVariableService,
|
private cssService: CSSVariableService,
|
||||||
private menuService: MenuService,
|
private menuService: MenuService,
|
||||||
private windowService: HostWindowService,
|
private windowService: HostWindowService
|
||||||
private localeService: LocaleService,
|
|
||||||
@Optional() private cookiesService: KlaroService
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -44,7 +44,8 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
authenticated: false,
|
authenticated: false,
|
||||||
loaded: false,
|
loaded: false,
|
||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false
|
loading: false,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
authState = {
|
authState = {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
@@ -52,7 +53,8 @@ describe('AuthNavMenuComponent', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authToken: new AuthTokenInfo('test_token'),
|
authToken: new AuthTokenInfo('test_token'),
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -37,7 +37,8 @@ describe('UserMenuComponent', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: false,
|
loading: false,
|
||||||
authToken: new AuthTokenInfo('test_token'),
|
authToken: new AuthTokenInfo('test_token'),
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
authStateLoading = {
|
authStateLoading = {
|
||||||
authenticated: true,
|
authenticated: true,
|
||||||
@@ -45,7 +46,8 @@ describe('UserMenuComponent', () => {
|
|||||||
blocking: false,
|
blocking: false,
|
||||||
loading: true,
|
loading: true,
|
||||||
authToken: null,
|
authToken: null,
|
||||||
userId: EPersonMock.id
|
userId: EPersonMock.id,
|
||||||
|
idle: false
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
<a [href]="bitstreamPath"><ng-container *ngTemplateOutlet="content"></ng-container></a>
|
<a [href]="bitstreamPath" [target]="isBlank ? '_blank': '_self'" [ngClass]="cssClasses">
|
||||||
|
<ng-container *ngTemplateOutlet="content"></ng-container>
|
||||||
|
</a>
|
||||||
|
|
||||||
<ng-template #content>
|
<ng-template #content>
|
||||||
<ng-content></ng-content>
|
<ng-content></ng-content>
|
||||||
|
@@ -18,6 +18,17 @@ export class FileDownloadLinkComponent implements OnInit {
|
|||||||
* Optional bitstream instead of href and file name
|
* Optional bitstream instead of href and file name
|
||||||
*/
|
*/
|
||||||
@Input() bitstream: Bitstream;
|
@Input() bitstream: Bitstream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Additional css classes to apply to link
|
||||||
|
*/
|
||||||
|
@Input() cssClasses = '';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A boolean representing if link is shown in same tab or in a new one.
|
||||||
|
*/
|
||||||
|
@Input() isBlank = false;
|
||||||
|
|
||||||
bitstreamPath: string;
|
bitstreamPath: string;
|
||||||
|
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
18
src/app/shared/idle-modal/idle-modal.component.html
Normal file
18
src/app/shared/idle-modal/idle-modal.component.html
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
<div>
|
||||||
|
<div class="modal-header" id="idle-modal.header">{{ "idle-modal.header" | translate }}
|
||||||
|
<button type="button" class="close" (click)="closePressed()" aria-label="Close">
|
||||||
|
<span aria-hidden="true">×</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
<div class="modal-body">
|
||||||
|
<p>{{ "idle-modal.info" | translate:{timeToExpire: timeToExpire} }}</p>
|
||||||
|
</div>
|
||||||
|
<div class="modal-footer">
|
||||||
|
<button type="button" class="cancel btn btn-danger" (click)="logOutPressed()" aria-label="Log out">
|
||||||
|
<i class="fas fa-sign-out-alt"></i> {{ "idle-modal.log-out" | translate }}
|
||||||
|
</button>
|
||||||
|
<button type="button" class="confirm btn btn-primary" (click)="extendSessionPressed()" aria-label="Extend session" ngbAutofocus>
|
||||||
|
<i class="fas fa-redo-alt"></i> {{ "idle-modal.extend-session" | translate }}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
135
src/app/shared/idle-modal/idle-modal.component.spec.ts
Normal file
135
src/app/shared/idle-modal/idle-modal.component.spec.ts
Normal file
@@ -0,0 +1,135 @@
|
|||||||
|
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { ComponentFixture, fakeAsync, TestBed, tick, waitForAsync } from '@angular/core/testing';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { IdleModalComponent } from './idle-modal.component';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { LogOutAction } from '../../core/auth/auth.actions';
|
||||||
|
|
||||||
|
describe('IdleModalComponent', () => {
|
||||||
|
let component: IdleModalComponent;
|
||||||
|
let fixture: ComponentFixture<IdleModalComponent>;
|
||||||
|
let debugElement: DebugElement;
|
||||||
|
|
||||||
|
let modalStub;
|
||||||
|
let authServiceStub;
|
||||||
|
let storeStub;
|
||||||
|
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
modalStub = jasmine.createSpyObj('modalStub', ['close']);
|
||||||
|
authServiceStub = jasmine.createSpyObj('authService', ['setIdle']);
|
||||||
|
storeStub = jasmine.createSpyObj('store', ['dispatch']);
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot()],
|
||||||
|
declarations: [IdleModalComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: NgbActiveModal, useValue: modalStub },
|
||||||
|
{ provide: AuthService, useValue: authServiceStub },
|
||||||
|
{ provide: Store, useValue: storeStub }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(IdleModalComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
debugElement = fixture.debugElement;
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('extendSessionPressed', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
spyOn(component.response, 'next');
|
||||||
|
component.extendSessionPressed();
|
||||||
|
}));
|
||||||
|
it('should set idle to false', () => {
|
||||||
|
expect(authServiceStub.setIdle).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
it('should close the modal', () => {
|
||||||
|
expect(modalStub.close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('response \'closed\' should have true as next', () => {
|
||||||
|
expect(component.response.next).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('logOutPressed', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
component.logOutPressed();
|
||||||
|
});
|
||||||
|
it('should close the modal', () => {
|
||||||
|
expect(modalStub.close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('should send logout action', () => {
|
||||||
|
expect(storeStub.dispatch).toHaveBeenCalledWith(new LogOutAction());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('closePressed', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
spyOn(component.response, 'next');
|
||||||
|
component.closePressed();
|
||||||
|
}));
|
||||||
|
it('should set idle to false', () => {
|
||||||
|
expect(authServiceStub.setIdle).toHaveBeenCalledWith(false);
|
||||||
|
});
|
||||||
|
it('should close the modal', () => {
|
||||||
|
expect(modalStub.close).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
it('response \'closed\' should have true as next', () => {
|
||||||
|
expect(component.response.next).toHaveBeenCalledWith(true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the click method emits on extend session button', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
spyOn(component, 'extendSessionPressed');
|
||||||
|
debugElement.query(By.css('button.confirm')).triggerEventHandler('click', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('should call the extendSessionPressed method on the component', () => {
|
||||||
|
expect(component.extendSessionPressed).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the click method emits on log out button', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
spyOn(component, 'logOutPressed');
|
||||||
|
debugElement.query(By.css('button.cancel')).triggerEventHandler('click', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('should call the logOutPressed method on the component', () => {
|
||||||
|
expect(component.logOutPressed).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the click method emits on close button', () => {
|
||||||
|
beforeEach(fakeAsync(() => {
|
||||||
|
spyOn(component, 'closePressed');
|
||||||
|
debugElement.query(By.css('.close')).triggerEventHandler('click', {
|
||||||
|
preventDefault: () => {/**/
|
||||||
|
}
|
||||||
|
});
|
||||||
|
tick();
|
||||||
|
fixture.detectChanges();
|
||||||
|
}));
|
||||||
|
it('should call the closePressed method on the component', () => {
|
||||||
|
expect(component.closePressed).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
89
src/app/shared/idle-modal/idle-modal.component.ts
Normal file
89
src/app/shared/idle-modal/idle-modal.component.ts
Normal file
@@ -0,0 +1,89 @@
|
|||||||
|
import { Component, OnInit, Output } from '@angular/core';
|
||||||
|
import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { environment } from '../../../environments/environment';
|
||||||
|
import { AuthService } from '../../core/auth/auth.service';
|
||||||
|
import { Subject } from 'rxjs';
|
||||||
|
import { hasValue } from '../empty.util';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import { LogOutAction } from '../../core/auth/auth.actions';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-idle-modal',
|
||||||
|
templateUrl: 'idle-modal.component.html',
|
||||||
|
})
|
||||||
|
export class IdleModalComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Total time of idleness before session expires (in minutes)
|
||||||
|
* (environment.auth.ui.timeUntilIdle + environment.auth.ui.idleGracePeriod / 1000 / 60)
|
||||||
|
*/
|
||||||
|
timeToExpire: number;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Timer to track time grace period
|
||||||
|
*/
|
||||||
|
private graceTimer;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the modal is closed
|
||||||
|
*/
|
||||||
|
@Output()
|
||||||
|
response: Subject<boolean> = new Subject();
|
||||||
|
|
||||||
|
constructor(private activeModal: NgbActiveModal,
|
||||||
|
private authService: AuthService,
|
||||||
|
private store: Store<AppState>) {
|
||||||
|
this.timeToExpire = (environment.auth.ui.timeUntilIdle + environment.auth.ui.idleGracePeriod) / 1000 / 60; // ms => min
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
if (hasValue(this.graceTimer)) {
|
||||||
|
clearTimeout(this.graceTimer);
|
||||||
|
}
|
||||||
|
this.graceTimer = setTimeout(() => {
|
||||||
|
this.logOutPressed();
|
||||||
|
}, environment.auth.ui.idleGracePeriod);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When extend session is pressed
|
||||||
|
*/
|
||||||
|
extendSessionPressed() {
|
||||||
|
this.extendSessionAndCloseModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close modal and logout
|
||||||
|
*/
|
||||||
|
logOutPressed() {
|
||||||
|
this.closeModal();
|
||||||
|
this.store.dispatch(new LogOutAction());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When close is pressed
|
||||||
|
*/
|
||||||
|
closePressed() {
|
||||||
|
this.extendSessionAndCloseModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the modal and extend session
|
||||||
|
*/
|
||||||
|
extendSessionAndCloseModal() {
|
||||||
|
if (hasValue(this.graceTimer)) {
|
||||||
|
clearTimeout(this.graceTimer);
|
||||||
|
}
|
||||||
|
this.authService.setIdle(false);
|
||||||
|
this.closeModal();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Close the modal and set the response to true so RootComponent knows the modal was closed
|
||||||
|
*/
|
||||||
|
closeModal() {
|
||||||
|
this.activeModal.close();
|
||||||
|
this.response.next(true);
|
||||||
|
}
|
||||||
|
}
|
@@ -19,4 +19,11 @@ export class AuthServiceMock {
|
|||||||
|
|
||||||
public setRedirectUrl(url: string) {
|
public setRedirectUrl(url: string) {
|
||||||
}
|
}
|
||||||
|
|
||||||
|
public trackTokenExpiration(): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
public isUserIdle(): Observable<boolean> {
|
||||||
|
return observableOf(false);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -5,6 +5,7 @@ import { Bitstream } from '../../core/shared/bitstream.model';
|
|||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||||
import { createPaginatedList } from '../testing/utils.test';
|
import { createPaginatedList } from '../testing/utils.test';
|
||||||
|
import { Bundle } from '../../core/shared/bundle.model';
|
||||||
|
|
||||||
export const MockBitstreamFormat1: BitstreamFormat = Object.assign(new BitstreamFormat(), {
|
export const MockBitstreamFormat1: BitstreamFormat = Object.assign(new BitstreamFormat(), {
|
||||||
shortDescription: 'Microsoft Word XML',
|
shortDescription: 'Microsoft Word XML',
|
||||||
@@ -34,11 +35,25 @@ export const MockBitstreamFormat2: BitstreamFormat = Object.assign(new Bitstream
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const MockBitstreamFormat3: BitstreamFormat = Object.assign(new BitstreamFormat(), {
|
||||||
|
shortDescription: 'Binary',
|
||||||
|
description: 'Some scary unknown binary file',
|
||||||
|
mimetype: 'application/octet-stream',
|
||||||
|
supportLevel: 0,
|
||||||
|
internal: false,
|
||||||
|
extensions: null,
|
||||||
|
_links:{
|
||||||
|
self: {
|
||||||
|
href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/17'
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
export const MockBitstream1: Bitstream = Object.assign(new Bitstream(),
|
export const MockBitstream1: Bitstream = Object.assign(new Bitstream(),
|
||||||
{
|
{
|
||||||
sizeBytes: 10201,
|
sizeBytes: 10201,
|
||||||
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||||
format: observableOf(MockBitstreamFormat1),
|
format: createSuccessfulRemoteDataObject$(MockBitstreamFormat1),
|
||||||
bundleName: 'ORIGINAL',
|
bundleName: 'ORIGINAL',
|
||||||
_links:{
|
_links:{
|
||||||
self: {
|
self: {
|
||||||
@@ -61,7 +76,7 @@ export const MockBitstream1: Bitstream = Object.assign(new Bitstream(),
|
|||||||
export const MockBitstream2: Bitstream = Object.assign(new Bitstream(), {
|
export const MockBitstream2: Bitstream = Object.assign(new Bitstream(), {
|
||||||
sizeBytes: 31302,
|
sizeBytes: 31302,
|
||||||
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content',
|
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content',
|
||||||
format: observableOf(MockBitstreamFormat2),
|
format: createSuccessfulRemoteDataObject$(MockBitstreamFormat2),
|
||||||
bundleName: 'ORIGINAL',
|
bundleName: 'ORIGINAL',
|
||||||
id: '99b00f3c-1cc6-4689-8158-91965bee6b28',
|
id: '99b00f3c-1cc6-4689-8158-91965bee6b28',
|
||||||
uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28',
|
uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28',
|
||||||
@@ -82,6 +97,68 @@ export const MockBitstream2: Bitstream = Object.assign(new Bitstream(), {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
export const MockBitstream3: Bitstream = Object.assign(new Bitstream(), {
|
||||||
|
sizeBytes: 4975123,
|
||||||
|
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/content',
|
||||||
|
format: createSuccessfulRemoteDataObject$(MockBitstreamFormat3),
|
||||||
|
bundleName: 'ORIGINAL',
|
||||||
|
id: '4db100c1-e1f5-4055-9404-9bc3e2d15f29',
|
||||||
|
uuid: '4db100c1-e1f5-4055-9404-9bc3e2d15f29',
|
||||||
|
type: 'bitstream',
|
||||||
|
_links: {
|
||||||
|
self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29' },
|
||||||
|
content: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/content' },
|
||||||
|
format: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/17' },
|
||||||
|
bundle: { href: '' }
|
||||||
|
},
|
||||||
|
metadata: {
|
||||||
|
'dc.title': [
|
||||||
|
{
|
||||||
|
language: null,
|
||||||
|
value: 'scary'
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
export const MockOriginalBundle: Bundle = Object.assign(new Bundle(), {
|
||||||
|
name: 'ORIGINAL',
|
||||||
|
primaryBitstream: createSuccessfulRemoteDataObject$(MockBitstream2),
|
||||||
|
bitstreams: observableOf(Object.assign({
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'dspace-angular://aggregated/object/1507836003548',
|
||||||
|
}
|
||||||
|
},
|
||||||
|
requestPending: false,
|
||||||
|
responsePending: false,
|
||||||
|
isSuccessful: true,
|
||||||
|
errorMessage: '',
|
||||||
|
state: '',
|
||||||
|
error: undefined,
|
||||||
|
isRequestPending: false,
|
||||||
|
isResponsePending: false,
|
||||||
|
isLoading: false,
|
||||||
|
hasFailed: false,
|
||||||
|
hasSucceeded: true,
|
||||||
|
statusCode: '202',
|
||||||
|
pageInfo: {},
|
||||||
|
payload: {
|
||||||
|
pageInfo: {
|
||||||
|
elementsPerPage: 20,
|
||||||
|
totalElements: 3,
|
||||||
|
totalPages: 1,
|
||||||
|
currentPage: 2
|
||||||
|
},
|
||||||
|
page: [
|
||||||
|
MockBitstream1,
|
||||||
|
MockBitstream2
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}))
|
||||||
|
});
|
||||||
|
|
||||||
|
|
||||||
/* tslint:disable:no-shadowed-variable */
|
/* tslint:disable:no-shadowed-variable */
|
||||||
export const ItemMock: Item = Object.assign(new Item(), {
|
export const ItemMock: Item = Object.assign(new Item(), {
|
||||||
handle: '10673/6',
|
handle: '10673/6',
|
||||||
@@ -90,41 +167,7 @@ export const ItemMock: Item = Object.assign(new Item(), {
|
|||||||
isDiscoverable: true,
|
isDiscoverable: true,
|
||||||
isWithdrawn: false,
|
isWithdrawn: false,
|
||||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([
|
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([
|
||||||
{
|
MockOriginalBundle,
|
||||||
name: 'ORIGINAL',
|
|
||||||
bitstreams: observableOf(Object.assign({
|
|
||||||
_links: {
|
|
||||||
self: {
|
|
||||||
href: 'dspace-angular://aggregated/object/1507836003548',
|
|
||||||
}
|
|
||||||
},
|
|
||||||
requestPending: false,
|
|
||||||
responsePending: false,
|
|
||||||
isSuccessful: true,
|
|
||||||
errorMessage: '',
|
|
||||||
state: '',
|
|
||||||
error: undefined,
|
|
||||||
isRequestPending: false,
|
|
||||||
isResponsePending: false,
|
|
||||||
isLoading: false,
|
|
||||||
hasFailed: false,
|
|
||||||
hasSucceeded: true,
|
|
||||||
statusCode: '202',
|
|
||||||
pageInfo: {},
|
|
||||||
payload: {
|
|
||||||
pageInfo: {
|
|
||||||
elementsPerPage: 20,
|
|
||||||
totalElements: 3,
|
|
||||||
totalPages: 1,
|
|
||||||
currentPage: 2
|
|
||||||
},
|
|
||||||
page: [
|
|
||||||
MockBitstream1,
|
|
||||||
MockBitstream2
|
|
||||||
]
|
|
||||||
}
|
|
||||||
}))
|
|
||||||
}
|
|
||||||
])),
|
])),
|
||||||
_links:{
|
_links:{
|
||||||
self: {
|
self: {
|
||||||
|
@@ -10,6 +10,8 @@ export class SectionsServiceStub {
|
|||||||
isSectionEnabled = jasmine.createSpy('isSectionEnabled');
|
isSectionEnabled = jasmine.createSpy('isSectionEnabled');
|
||||||
isSectionReadOnly = jasmine.createSpy('isSectionReadOnly');
|
isSectionReadOnly = jasmine.createSpy('isSectionReadOnly');
|
||||||
isSectionAvailable = jasmine.createSpy('isSectionAvailable');
|
isSectionAvailable = jasmine.createSpy('isSectionAvailable');
|
||||||
|
isSectionTypeAvailable = jasmine.createSpy('isSectionTypeAvailable');
|
||||||
|
isSectionType = jasmine.createSpy('isSectionType');
|
||||||
addSection = jasmine.createSpy('addSection');
|
addSection = jasmine.createSpy('addSection');
|
||||||
removeSection = jasmine.createSpy('removeSection');
|
removeSection = jasmine.createSpy('removeSection');
|
||||||
updateSectionData = jasmine.createSpy('updateSectionData');
|
updateSectionData = jasmine.createSpy('updateSectionData');
|
||||||
|
@@ -120,7 +120,7 @@ describe('SubmissionFormCollectionComponent Component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
const sectionsService: any = jasmine.createSpyObj('sectionsService', {
|
const sectionsService: any = jasmine.createSpyObj('sectionsService', {
|
||||||
isSectionAvailable: of(true)
|
isSectionTypeAvailable: of(true)
|
||||||
});
|
});
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
beforeEach(waitForAsync(() => {
|
||||||
|
@@ -28,6 +28,7 @@ import { CollectionDataService } from '../../../core/data/collection-data.servic
|
|||||||
import { CollectionDropdownComponent } from '../../../shared/collection-dropdown/collection-dropdown.component';
|
import { CollectionDropdownComponent } from '../../../shared/collection-dropdown/collection-dropdown.component';
|
||||||
import { SectionsService } from '../../sections/sections.service';
|
import { SectionsService } from '../../sections/sections.service';
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||||
|
import { SectionsType } from '../../sections/sections-type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component allows to show the current collection the submission belonging to and to change it.
|
* This component allows to show the current collection the submission belonging to and to change it.
|
||||||
@@ -142,7 +143,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit {
|
|||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', 'collection');
|
this.pathCombiner = new JsonPatchOperationPathCombiner('sections', 'collection');
|
||||||
this.available$ = this.sectionsService.isSectionAvailable(this.submissionId, 'collection');
|
this.available$ = this.sectionsService.isSectionTypeAvailable(this.submissionId, SectionsType.collection);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -3,7 +3,6 @@
|
|||||||
<div *ngIf="(uploadEnabled$ | async)" class="w-100">
|
<div *ngIf="(uploadEnabled$ | async)" class="w-100">
|
||||||
<ds-submission-upload-files [submissionId]="submissionId"
|
<ds-submission-upload-files [submissionId]="submissionId"
|
||||||
[collectionId]="collectionId"
|
[collectionId]="collectionId"
|
||||||
[sectionId]="'upload'"
|
|
||||||
[uploadFilesOptions]="uploadFilesOptions"></ds-submission-upload-files>
|
[uploadFilesOptions]="uploadFilesOptions"></ds-submission-upload-files>
|
||||||
<div class="clearfix"></div>
|
<div class="clearfix"></div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -83,7 +83,6 @@ describe('SubmissionUploadFilesComponent Component', () => {
|
|||||||
const html = `
|
const html = `
|
||||||
<ds-submission-upload-files [submissionId]="submissionId"
|
<ds-submission-upload-files [submissionId]="submissionId"
|
||||||
[collectionId]="collectionId"
|
[collectionId]="collectionId"
|
||||||
[sectionId]="'upload'"
|
|
||||||
[uploadFilesOptions]="uploadFilesOptions"></ds-submission-upload-files>`;
|
[uploadFilesOptions]="uploadFilesOptions"></ds-submission-upload-files>`;
|
||||||
|
|
||||||
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
|
||||||
@@ -108,11 +107,11 @@ describe('SubmissionUploadFilesComponent Component', () => {
|
|||||||
compAsAny = comp;
|
compAsAny = comp;
|
||||||
submissionServiceStub = TestBed.inject(SubmissionService as any);
|
submissionServiceStub = TestBed.inject(SubmissionService as any);
|
||||||
sectionsServiceStub = TestBed.inject(SectionsService as any);
|
sectionsServiceStub = TestBed.inject(SectionsService as any);
|
||||||
|
sectionsServiceStub.isSectionTypeAvailable.and.returnValue(observableOf(true));
|
||||||
notificationsServiceStub = TestBed.inject(NotificationsService as any);
|
notificationsServiceStub = TestBed.inject(NotificationsService as any);
|
||||||
translateService = TestBed.inject(TranslateService);
|
translateService = TestBed.inject(TranslateService);
|
||||||
comp.submissionId = submissionId;
|
comp.submissionId = submissionId;
|
||||||
comp.collectionId = collectionId;
|
comp.collectionId = collectionId;
|
||||||
comp.sectionId = 'upload';
|
|
||||||
comp.uploadFilesOptions = Object.assign(new UploaderOptions(),{
|
comp.uploadFilesOptions = Object.assign(new UploaderOptions(),{
|
||||||
url: '',
|
url: '',
|
||||||
authToken: null,
|
authToken: null,
|
||||||
@@ -133,7 +132,7 @@ describe('SubmissionUploadFilesComponent Component', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should init uploadEnabled properly', () => {
|
it('should init uploadEnabled properly', () => {
|
||||||
sectionsServiceStub.isSectionAvailable.and.returnValue(hot('-a-b', {
|
sectionsServiceStub.isSectionTypeAvailable.and.returnValue(hot('-a-b', {
|
||||||
a: false,
|
a: false,
|
||||||
b: true
|
b: true
|
||||||
}));
|
}));
|
||||||
@@ -149,53 +148,54 @@ describe('SubmissionUploadFilesComponent Component', () => {
|
|||||||
expect(compAsAny.uploadEnabled).toBeObservable(expected);
|
expect(compAsAny.uploadEnabled).toBeObservable(expected);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should show a success notification and call updateSectionData on upload complete', () => {
|
describe('on upload complete', () => {
|
||||||
|
beforeEach(() => {
|
||||||
const expectedErrors: any = mockUploadResponse1ParsedErrors;
|
sectionsServiceStub.isSectionType.and.callFake((_, sectionId, __) => observableOf(sectionId === 'upload'));
|
||||||
compAsAny.uploadEnabled = observableOf(true);
|
compAsAny.uploadEnabled = observableOf(true);
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
comp.onCompleteItem(Object.assign({}, uploadRestResponse, { sections: mockSectionsData }));
|
|
||||||
|
|
||||||
Object.keys(mockSectionsData).forEach((sectionId) => {
|
|
||||||
expect(sectionsServiceStub.updateSectionData).toHaveBeenCalledWith(
|
|
||||||
submissionId,
|
|
||||||
sectionId,
|
|
||||||
mockSectionsData[sectionId],
|
|
||||||
expectedErrors[sectionId]
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(notificationsServiceStub.success).toHaveBeenCalled();
|
it('should show a success notification and call updateSectionData if successful', () => {
|
||||||
|
const expectedErrors: any = mockUploadResponse1ParsedErrors;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
});
|
comp.onCompleteItem(Object.assign({}, uploadRestResponse, { sections: mockSectionsData }));
|
||||||
|
|
||||||
it('should show an error notification and call updateSectionData on upload complete', () => {
|
Object.keys(mockSectionsData).forEach((sectionId) => {
|
||||||
|
expect(sectionsServiceStub.updateSectionData).toHaveBeenCalledWith(
|
||||||
|
submissionId,
|
||||||
|
sectionId,
|
||||||
|
mockSectionsData[sectionId],
|
||||||
|
expectedErrors[sectionId]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
const responseErrors = mockUploadResponse2Errors;
|
expect(notificationsServiceStub.success).toHaveBeenCalled();
|
||||||
|
|
||||||
const expectedErrors: any = mockUploadResponse2ParsedErrors;
|
|
||||||
compAsAny.uploadEnabled = observableOf(true);
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
comp.onCompleteItem(Object.assign({}, uploadRestResponse, {
|
|
||||||
sections: mockSectionsData,
|
|
||||||
errors: responseErrors.errors
|
|
||||||
}));
|
|
||||||
|
|
||||||
Object.keys(mockSectionsData).forEach((sectionId) => {
|
|
||||||
expect(sectionsServiceStub.updateSectionData).toHaveBeenCalledWith(
|
|
||||||
submissionId,
|
|
||||||
sectionId,
|
|
||||||
mockSectionsData[sectionId],
|
|
||||||
expectedErrors[sectionId]
|
|
||||||
);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
expect(notificationsServiceStub.success).not.toHaveBeenCalled();
|
it('should show an error notification and call updateSectionData if unsuccessful', () => {
|
||||||
|
const responseErrors = mockUploadResponse2Errors;
|
||||||
|
const expectedErrors: any = mockUploadResponse2ParsedErrors;
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
comp.onCompleteItem(Object.assign({}, uploadRestResponse, {
|
||||||
|
sections: mockSectionsData,
|
||||||
|
errors: responseErrors.errors
|
||||||
|
}));
|
||||||
|
|
||||||
|
Object.keys(mockSectionsData).forEach((sectionId) => {
|
||||||
|
expect(sectionsServiceStub.updateSectionData).toHaveBeenCalledWith(
|
||||||
|
submissionId,
|
||||||
|
sectionId,
|
||||||
|
mockSectionsData[sectionId],
|
||||||
|
expectedErrors[sectionId]
|
||||||
|
);
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(notificationsServiceStub.success).not.toHaveBeenCalled();
|
||||||
|
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -208,7 +208,6 @@ class TestComponent {
|
|||||||
|
|
||||||
submissionId = mockSubmissionId;
|
submissionId = mockSubmissionId;
|
||||||
collectionId = mockSubmissionCollectionId;
|
collectionId = mockSubmissionCollectionId;
|
||||||
sectionId = 'upload';
|
|
||||||
uploadFilesOptions = Object.assign(new UploaderOptions(), {
|
uploadFilesOptions = Object.assign(new UploaderOptions(), {
|
||||||
url: '',
|
url: '',
|
||||||
authToken: null,
|
authToken: null,
|
||||||
|
@@ -2,7 +2,7 @@ import { Component, Input, OnChanges } from '@angular/core';
|
|||||||
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { Observable, of as observableOf, Subscription } from 'rxjs';
|
import { Observable, of as observableOf, Subscription } from 'rxjs';
|
||||||
import { first } from 'rxjs/operators';
|
import { first, take } from 'rxjs/operators';
|
||||||
|
|
||||||
import { SectionsService } from '../../sections/sections.service';
|
import { SectionsService } from '../../sections/sections.service';
|
||||||
import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
import { hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||||
@@ -13,6 +13,7 @@ import { UploaderOptions } from '../../../shared/uploader/uploader-options.model
|
|||||||
import parseSectionErrors from '../../utils/parseSectionErrors';
|
import parseSectionErrors from '../../utils/parseSectionErrors';
|
||||||
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
|
import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service';
|
||||||
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
|
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
|
||||||
|
import { SectionsType } from '../../sections/sections-type';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component represents the drop zone that provides to add files to the submission.
|
* This component represents the drop zone that provides to add files to the submission.
|
||||||
@@ -35,12 +36,6 @@ export class SubmissionUploadFilesComponent implements OnChanges {
|
|||||||
*/
|
*/
|
||||||
@Input() submissionId: string;
|
@Input() submissionId: string;
|
||||||
|
|
||||||
/**
|
|
||||||
* The upload section id
|
|
||||||
* @type {string}
|
|
||||||
*/
|
|
||||||
@Input() sectionId: string;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The uploader configuration options
|
* The uploader configuration options
|
||||||
* @type {UploaderOptions}
|
* @type {UploaderOptions}
|
||||||
@@ -110,7 +105,7 @@ export class SubmissionUploadFilesComponent implements OnChanges {
|
|||||||
* Check if upload functionality is enabled
|
* Check if upload functionality is enabled
|
||||||
*/
|
*/
|
||||||
ngOnChanges() {
|
ngOnChanges() {
|
||||||
this.uploadEnabled = this.sectionService.isSectionAvailable(this.submissionId, this.sectionId);
|
this.uploadEnabled = this.sectionService.isSectionTypeAvailable(this.submissionId, SectionsType.Upload);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -136,14 +131,18 @@ export class SubmissionUploadFilesComponent implements OnChanges {
|
|||||||
.forEach((sectionId) => {
|
.forEach((sectionId) => {
|
||||||
const sectionData = normalizeSectionData(sections[sectionId]);
|
const sectionData = normalizeSectionData(sections[sectionId]);
|
||||||
const sectionErrors = errorsList[sectionId];
|
const sectionErrors = errorsList[sectionId];
|
||||||
if (sectionId === 'upload') {
|
this.sectionService.isSectionType(this.submissionId, sectionId, SectionsType.Upload)
|
||||||
// Look for errors on upload
|
.pipe(take(1))
|
||||||
if ((isEmpty(sectionErrors))) {
|
.subscribe((isUpload) => {
|
||||||
this.notificationsService.success(null, this.translate.get('submission.sections.upload.upload-successful'));
|
if (isUpload) {
|
||||||
} else {
|
// Look for errors on upload
|
||||||
this.notificationsService.error(null, this.translate.get('submission.sections.upload.upload-failed'));
|
if ((isEmpty(sectionErrors))) {
|
||||||
}
|
this.notificationsService.success(null, this.translate.get('submission.sections.upload.upload-successful'));
|
||||||
}
|
} else {
|
||||||
|
this.notificationsService.error(null, this.translate.get('submission.sections.upload.upload-failed'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
this.sectionService.updateSectionData(this.submissionId, sectionId, sectionData, sectionErrors);
|
this.sectionService.updateSectionData(this.submissionId, sectionId, sectionData, sectionErrors);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@@ -334,6 +334,38 @@ describe('SectionsService test suite', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('isSectionType', () => {
|
||||||
|
it('should return true if the section matches the provided type', () => {
|
||||||
|
store.select.and.returnValue(observableOf(submissionState));
|
||||||
|
|
||||||
|
const expected = cold('(b|)', {
|
||||||
|
b: true
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(service.isSectionType(submissionId, 'upload', SectionsType.Upload)).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if the section doesn\'t match the provided type', () => {
|
||||||
|
store.select.and.returnValue(observableOf(submissionState));
|
||||||
|
|
||||||
|
const expected = cold('(b|)', {
|
||||||
|
b: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(service.isSectionType(submissionId, sectionId, SectionsType.Upload)).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return false if the provided sectionId doesn\'t exist', () => {
|
||||||
|
store.select.and.returnValue(observableOf(submissionState));
|
||||||
|
|
||||||
|
const expected = cold('(b|)', {
|
||||||
|
b: false
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(service.isSectionType(submissionId, 'no-such-id', SectionsType.Upload)).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
describe('addSection', () => {
|
describe('addSection', () => {
|
||||||
it('should dispatch a new EnableSectionAction a move target to new section', () => {
|
it('should dispatch a new EnableSectionAction a move target to new section', () => {
|
||||||
|
|
||||||
|
@@ -328,6 +328,22 @@ export class SectionsService {
|
|||||||
distinctUntilChanged());
|
distinctUntilChanged());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if given section id is of a given section type
|
||||||
|
* @param submissionId
|
||||||
|
* @param sectionId
|
||||||
|
* @param sectionType
|
||||||
|
*/
|
||||||
|
public isSectionType(submissionId: string, sectionId: string, sectionType: SectionsType): Observable<boolean> {
|
||||||
|
return this.store.select(submissionObjectFromIdSelector(submissionId)).pipe(
|
||||||
|
filter((submissionState: SubmissionObjectEntry) => isNotUndefined(submissionState)),
|
||||||
|
map((submissionState: SubmissionObjectEntry) => {
|
||||||
|
return isNotUndefined(submissionState.sections) && isNotUndefined(submissionState.sections[sectionId])
|
||||||
|
&& submissionState.sections[sectionId].sectionType === sectionType;
|
||||||
|
}),
|
||||||
|
distinctUntilChanged());
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Dispatch a new [EnableSectionAction] to add a new section and move page target to it
|
* Dispatch a new [EnableSectionAction] to add a new section and move page target to it
|
||||||
*
|
*
|
||||||
|
@@ -10,12 +10,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<div class="float-right w-15" [class.sticky-buttons]="!readMode">
|
<div class="float-right w-15" [class.sticky-buttons]="!readMode">
|
||||||
<ng-container *ngIf="readMode">
|
<ng-container *ngIf="readMode">
|
||||||
<button class="btn btn-link"
|
<ds-file-download-link [cssClasses]="'btn btn-link'" [isBlank]="true" [bitstream]="getBitstream()">
|
||||||
[attr.aria-label]="'submission.sections.upload.download.title' | translate"
|
|
||||||
title="{{ 'submission.sections.upload.download.title' | translate }}"
|
|
||||||
(click)="downloadBitstreamFile(); $event.stopPropagation()">
|
|
||||||
<i class="fa fa-download fa-2x text-normal" aria-hidden="true"></i>
|
<i class="fa fa-download fa-2x text-normal" aria-hidden="true"></i>
|
||||||
</button>
|
</ds-file-download-link>
|
||||||
<button class="btn btn-link"
|
<button class="btn btn-link"
|
||||||
[attr.aria-label]="'submission.sections.upload.edit.title' | translate"
|
[attr.aria-label]="'submission.sections.upload.edit.title' | translate"
|
||||||
title="{{ 'submission.sections.upload.edit.title' | translate }}"
|
title="{{ 'submission.sections.upload.edit.title' | translate }}"
|
||||||
|
@@ -6,7 +6,6 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { FileService } from '../../../../core/shared/file.service';
|
|
||||||
import { FormService } from '../../../../shared/form/form.service';
|
import { FormService } from '../../../../shared/form/form.service';
|
||||||
import { getMockFormService } from '../../../../shared/mocks/form-service.mock';
|
import { getMockFormService } from '../../../../shared/mocks/form-service.mock';
|
||||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||||
@@ -19,7 +18,6 @@ import { SubmissionSectionUploadFileComponent } from './section-upload-file.comp
|
|||||||
import { SubmissionServiceStub } from '../../../../shared/testing/submission-service.stub';
|
import { SubmissionServiceStub } from '../../../../shared/testing/submission-service.stub';
|
||||||
import {
|
import {
|
||||||
mockFileFormData,
|
mockFileFormData,
|
||||||
mockGroup,
|
|
||||||
mockSubmissionCollectionId,
|
mockSubmissionCollectionId,
|
||||||
mockSubmissionId,
|
mockSubmissionId,
|
||||||
mockSubmissionObject,
|
mockSubmissionObject,
|
||||||
@@ -35,17 +33,9 @@ import { POLICY_DEFAULT_WITH_LIST } from '../section-upload.component';
|
|||||||
import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner';
|
import { JsonPatchOperationPathCombiner } from '../../../../core/json-patch/builder/json-patch-operation-path-combiner';
|
||||||
import { getMockSectionUploadService } from '../../../../shared/mocks/section-upload.service.mock';
|
import { getMockSectionUploadService } from '../../../../shared/mocks/section-upload.service.mock';
|
||||||
import { FormFieldMetadataValueObject } from '../../../../shared/form/builder/models/form-field-metadata-value.model';
|
import { FormFieldMetadataValueObject } from '../../../../shared/form/builder/models/form-field-metadata-value.model';
|
||||||
import { Group } from '../../../../core/eperson/models/group.model';
|
|
||||||
import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component';
|
import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component';
|
||||||
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
import { FormBuilderService } from '../../../../shared/form/builder/form-builder.service';
|
||||||
|
|
||||||
function getMockFileService(): FileService {
|
|
||||||
return jasmine.createSpyObj('FileService', {
|
|
||||||
retrieveFileDownloadLink: jasmine.createSpy('retrieveFileDownloadLink'),
|
|
||||||
getFileNameFromResponseContentDisposition: jasmine.createSpy('getFileNameFromResponseContentDisposition')
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('SubmissionSectionUploadFileComponent test suite', () => {
|
describe('SubmissionSectionUploadFileComponent test suite', () => {
|
||||||
|
|
||||||
let comp: SubmissionSectionUploadFileComponent;
|
let comp: SubmissionSectionUploadFileComponent;
|
||||||
@@ -53,7 +43,6 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
|
|||||||
let fixture: ComponentFixture<SubmissionSectionUploadFileComponent>;
|
let fixture: ComponentFixture<SubmissionSectionUploadFileComponent>;
|
||||||
let submissionServiceStub: SubmissionServiceStub;
|
let submissionServiceStub: SubmissionServiceStub;
|
||||||
let uploadService: any;
|
let uploadService: any;
|
||||||
let fileService: any;
|
|
||||||
let formService: any;
|
let formService: any;
|
||||||
let halService: any;
|
let halService: any;
|
||||||
let operationsBuilder: any;
|
let operationsBuilder: any;
|
||||||
@@ -64,10 +53,6 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
|
|||||||
const sectionId = 'upload';
|
const sectionId = 'upload';
|
||||||
const collectionId = mockSubmissionCollectionId;
|
const collectionId = mockSubmissionCollectionId;
|
||||||
const availableAccessConditionOptions = mockUploadConfigResponse.accessConditionOptions;
|
const availableAccessConditionOptions = mockUploadConfigResponse.accessConditionOptions;
|
||||||
const availableGroupsMap: Map<string, Group[]> = new Map([
|
|
||||||
[mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
|
|
||||||
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
|
|
||||||
]);
|
|
||||||
const collectionPolicyType = POLICY_DEFAULT_WITH_LIST;
|
const collectionPolicyType = POLICY_DEFAULT_WITH_LIST;
|
||||||
const fileIndex = '0';
|
const fileIndex = '0';
|
||||||
const fileName = '123456-test-upload.jpg';
|
const fileName = '123456-test-upload.jpg';
|
||||||
@@ -95,7 +80,6 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
|
|||||||
TestComponent
|
TestComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: FileService, useValue: getMockFileService() },
|
|
||||||
{ provide: FormService, useValue: getMockFormService() },
|
{ provide: FormService, useValue: getMockFormService() },
|
||||||
{ provide: HALEndpointService, useValue: new HALEndpointServiceStub('workspaceitems') },
|
{ provide: HALEndpointService, useValue: new HALEndpointServiceStub('workspaceitems') },
|
||||||
{ provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder },
|
{ provide: JsonPatchOperationsBuilder, useValue: jsonPatchOpBuilder },
|
||||||
@@ -152,7 +136,6 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
|
|||||||
compAsAny = comp;
|
compAsAny = comp;
|
||||||
submissionServiceStub = TestBed.inject(SubmissionService as any);
|
submissionServiceStub = TestBed.inject(SubmissionService as any);
|
||||||
uploadService = TestBed.inject(SectionUploadService);
|
uploadService = TestBed.inject(SectionUploadService);
|
||||||
fileService = TestBed.inject(FileService);
|
|
||||||
formService = TestBed.inject(FormService);
|
formService = TestBed.inject(FormService);
|
||||||
halService = TestBed.inject(HALEndpointService);
|
halService = TestBed.inject(HALEndpointService);
|
||||||
operationsBuilder = TestBed.inject(JsonPatchOperationsBuilder);
|
operationsBuilder = TestBed.inject(JsonPatchOperationsBuilder);
|
||||||
@@ -226,15 +209,6 @@ describe('SubmissionSectionUploadFileComponent test suite', () => {
|
|||||||
pathCombiner.subRootElement);
|
pathCombiner.subRootElement);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should download Bitstream File properly', fakeAsync(() => {
|
|
||||||
comp.fileData = fileData;
|
|
||||||
comp.downloadBitstreamFile();
|
|
||||||
|
|
||||||
tick();
|
|
||||||
|
|
||||||
expect(fileService.retrieveFileDownloadLink).toHaveBeenCalled();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should save Bitstream File data properly when form is valid', fakeAsync(() => {
|
it('should save Bitstream File data properly when form is valid', fakeAsync(() => {
|
||||||
compAsAny.fileEditComp = TestBed.inject(SubmissionSectionUploadFileEditComponent);
|
compAsAny.fileEditComp = TestBed.inject(SubmissionSectionUploadFileEditComponent);
|
||||||
compAsAny.fileEditComp.formRef = {formGroup: null};
|
compAsAny.fileEditComp.formRef = {formGroup: null};
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
|
import { ChangeDetectorRef, Component, Input, OnChanges, OnInit, ViewChild } from '@angular/core';
|
||||||
|
|
||||||
import { BehaviorSubject, Subscription } from 'rxjs';
|
import { BehaviorSubject, Subscription } from 'rxjs';
|
||||||
import { filter, first, mergeMap, take } from 'rxjs/operators';
|
import { filter, mergeMap, take } from 'rxjs/operators';
|
||||||
import { DynamicFormControlModel, } from '@ng-dynamic-forms/core';
|
import { DynamicFormControlModel, } from '@ng-dynamic-forms/core';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
|
||||||
@@ -14,12 +14,12 @@ import { WorkspaceitemSectionUploadFileObject } from '../../../../core/submissio
|
|||||||
import { SubmissionFormsModel } from '../../../../core/config/models/config-submission-forms.model';
|
import { SubmissionFormsModel } from '../../../../core/config/models/config-submission-forms.model';
|
||||||
import { dateToISOFormat } from '../../../../shared/date.util';
|
import { dateToISOFormat } from '../../../../shared/date.util';
|
||||||
import { SubmissionService } from '../../../submission.service';
|
import { SubmissionService } from '../../../submission.service';
|
||||||
import { FileService } from '../../../../core/shared/file.service';
|
|
||||||
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../../../core/shared/hal-endpoint.service';
|
||||||
import { SubmissionJsonPatchOperationsService } from '../../../../core/submission/submission-json-patch-operations.service';
|
import { SubmissionJsonPatchOperationsService } from '../../../../core/submission/submission-json-patch-operations.service';
|
||||||
import { SubmissionObject } from '../../../../core/submission/models/submission-object.model';
|
import { SubmissionObject } from '../../../../core/submission/models/submission-object.model';
|
||||||
import { WorkspaceitemSectionUploadObject } from '../../../../core/submission/models/workspaceitem-section-upload.model';
|
import { WorkspaceitemSectionUploadObject } from '../../../../core/submission/models/workspaceitem-section-upload.model';
|
||||||
import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component';
|
import { SubmissionSectionUploadFileEditComponent } from './edit/section-upload-file-edit.component';
|
||||||
|
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component represents a single bitstream contained in the submission
|
* This component represents a single bitstream contained in the submission
|
||||||
@@ -139,7 +139,6 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
|
|||||||
* Initialize instance variables
|
* Initialize instance variables
|
||||||
*
|
*
|
||||||
* @param {ChangeDetectorRef} cdr
|
* @param {ChangeDetectorRef} cdr
|
||||||
* @param {FileService} fileService
|
|
||||||
* @param {FormService} formService
|
* @param {FormService} formService
|
||||||
* @param {HALEndpointService} halService
|
* @param {HALEndpointService} halService
|
||||||
* @param {NgbModal} modalService
|
* @param {NgbModal} modalService
|
||||||
@@ -149,7 +148,6 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
|
|||||||
* @param {SectionUploadService} uploadService
|
* @param {SectionUploadService} uploadService
|
||||||
*/
|
*/
|
||||||
constructor(private cdr: ChangeDetectorRef,
|
constructor(private cdr: ChangeDetectorRef,
|
||||||
private fileService: FileService,
|
|
||||||
private formService: FormService,
|
private formService: FormService,
|
||||||
private halService: HALEndpointService,
|
private halService: HALEndpointService,
|
||||||
private modalService: NgbModal,
|
private modalService: NgbModal,
|
||||||
@@ -217,15 +215,14 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform bitstream download
|
* Build a Bitstream object by the current file uuid
|
||||||
|
*
|
||||||
|
* @return Bitstream object
|
||||||
*/
|
*/
|
||||||
public downloadBitstreamFile() {
|
public getBitstream(): Bitstream {
|
||||||
this.halService.getEndpoint('bitstreams').pipe(
|
return Object.assign(new Bitstream(), {
|
||||||
first())
|
uuid: this.fileData.uuid
|
||||||
.subscribe((url) => {
|
});
|
||||||
const fileUrl = `${url}/${this.fileData.uuid}/content`;
|
|
||||||
this.fileService.retrieveFileDownloadLink(fileUrl);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -291,14 +288,13 @@ export class SubmissionSectionUploadFileComponent implements OnChanges, OnInit {
|
|||||||
this.pathCombiner.subRootElement);
|
this.pathCombiner.subRootElement);
|
||||||
})
|
})
|
||||||
).subscribe((result: SubmissionObject[]) => {
|
).subscribe((result: SubmissionObject[]) => {
|
||||||
if (result[0].sections.upload) {
|
if (result[0].sections[this.sectionId]) {
|
||||||
Object.keys((result[0].sections.upload as WorkspaceitemSectionUploadObject).files)
|
const uploadSection = (result[0].sections[this.sectionId] as WorkspaceitemSectionUploadObject);
|
||||||
.filter((key) => (result[0].sections.upload as WorkspaceitemSectionUploadObject).files[key].uuid === this.fileId)
|
Object.keys(uploadSection.files)
|
||||||
|
.filter((key) => uploadSection.files[key].uuid === this.fileId)
|
||||||
.forEach((key) => this.uploadService.updateFileData(
|
.forEach((key) => this.uploadService.updateFileData(
|
||||||
this.submissionId,
|
this.submissionId, this.sectionId, this.fileId, uploadSection.files[key])
|
||||||
this.sectionId,
|
);
|
||||||
this.fileId,
|
|
||||||
(result[0].sections.upload as WorkspaceitemSectionUploadObject).files[key]));
|
|
||||||
}
|
}
|
||||||
this.switchMode();
|
this.switchMode();
|
||||||
}));
|
}));
|
||||||
|
@@ -526,6 +526,8 @@
|
|||||||
|
|
||||||
"auth.messages.expired": "Your session has expired. Please log in again.",
|
"auth.messages.expired": "Your session has expired. Please log in again.",
|
||||||
|
|
||||||
|
"auth.messages.token-refresh-failed": "Refreshing your session token failed. Please log in again.",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
"bitstream.download.page": "Now downloading {{bitstream}}..." ,
|
"bitstream.download.page": "Now downloading {{bitstream}}..." ,
|
||||||
@@ -3713,5 +3715,15 @@
|
|||||||
|
|
||||||
"workflow-item.send-back.button.cancel": "Cancel",
|
"workflow-item.send-back.button.cancel": "Cancel",
|
||||||
|
|
||||||
"workflow-item.send-back.button.confirm": "Send back"
|
"workflow-item.send-back.button.confirm": "Send back",
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
"idle-modal.header": "Session will expire soon",
|
||||||
|
|
||||||
|
"idle-modal.info": "For security reasons, user sessions expire after {{ timeToExpire }} minutes of inactivity. Your session will expire soon. Would you like to extend it or log out?",
|
||||||
|
|
||||||
|
"idle-modal.log-out": "Log out",
|
||||||
|
|
||||||
|
"idle-modal.extend-session": "Extend session"
|
||||||
}
|
}
|
||||||
|
@@ -6,5 +6,18 @@ export interface AuthTarget {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export interface AuthConfig extends Config {
|
export interface AuthConfig extends Config {
|
||||||
target: AuthTarget;
|
target?: AuthTarget;
|
||||||
|
|
||||||
|
ui: {
|
||||||
|
// The amount of time before the idle warning is shown
|
||||||
|
timeUntilIdle: number;
|
||||||
|
// The amount of time the user has to react after the idle warning is shown before they are logged out.
|
||||||
|
idleGracePeriod: number;
|
||||||
|
};
|
||||||
|
|
||||||
|
rest: {
|
||||||
|
// If the rest token expires in less than this amount of time, it will be refreshed automatically.
|
||||||
|
// This is independent from the idle warning.
|
||||||
|
timeLeftBeforeTokenRefresh: number;
|
||||||
|
};
|
||||||
}
|
}
|
||||||
|
@@ -2,7 +2,6 @@ import { GlobalConfig } from '../config/global-config.interface';
|
|||||||
import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type';
|
import { NotificationAnimationsType } from '../app/shared/notifications/models/notification-animations-type';
|
||||||
import { BrowseByType } from '../app/+browse-by/+browse-by-switcher/browse-by-decorator';
|
import { BrowseByType } from '../app/+browse-by/+browse-by-switcher/browse-by-decorator';
|
||||||
import { RestRequestMethod } from '../app/core/data/rest-request-method';
|
import { RestRequestMethod } from '../app/core/data/rest-request-method';
|
||||||
import { BASE_THEME_NAME } from '../app/shared/theme-support/theme.constants';
|
|
||||||
|
|
||||||
export const environment: GlobalConfig = {
|
export const environment: GlobalConfig = {
|
||||||
production: true,
|
production: true,
|
||||||
@@ -43,6 +42,22 @@ export const environment: GlobalConfig = {
|
|||||||
timePerMethod: {[RestRequestMethod.PATCH]: 3} as any // time in seconds
|
timePerMethod: {[RestRequestMethod.PATCH]: 3} as any // time in seconds
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Authentication settings
|
||||||
|
auth: {
|
||||||
|
// Authentication UI settings
|
||||||
|
ui: {
|
||||||
|
// the amount of time before the idle warning is shown
|
||||||
|
timeUntilIdle: 15 * 60 * 1000, // 15 minutes
|
||||||
|
// the amount of time the user has to react after the idle warning is shown before they are logged out.
|
||||||
|
idleGracePeriod: 5 * 60 * 1000, // 5 minutes
|
||||||
|
},
|
||||||
|
// Authentication REST settings
|
||||||
|
rest: {
|
||||||
|
// If the rest token expires in less than this amount of time, it will be refreshed automatically.
|
||||||
|
// This is independent from the idle warning.
|
||||||
|
timeLeftBeforeTokenRefresh: 2 * 60 * 1000, // 2 minutes
|
||||||
|
},
|
||||||
|
},
|
||||||
// Form settings
|
// Form settings
|
||||||
form: {
|
form: {
|
||||||
// NOTE: Map server-side validators to comparative Angular form validators
|
// NOTE: Map server-side validators to comparative Angular form validators
|
||||||
|
@@ -35,6 +35,22 @@ export const environment: Partial<GlobalConfig> = {
|
|||||||
timePerMethod: {[RestRequestMethod.PATCH]: 3} as any // time in seconds
|
timePerMethod: {[RestRequestMethod.PATCH]: 3} as any // time in seconds
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
// Authentication settings
|
||||||
|
auth: {
|
||||||
|
// Authentication UI settings
|
||||||
|
ui: {
|
||||||
|
// the amount of time before the idle warning is shown
|
||||||
|
timeUntilIdle: 20000, // 20 sec
|
||||||
|
// the amount of time the user has to react after the idle warning is shown before they are logged out.
|
||||||
|
idleGracePeriod: 20000, // 20 sec
|
||||||
|
},
|
||||||
|
// Authentication REST settings
|
||||||
|
rest: {
|
||||||
|
// If the rest token expires in less than this amount of time, it will be refreshed automatically.
|
||||||
|
// This is independent from the idle warning.
|
||||||
|
timeLeftBeforeTokenRefresh: 20000, // 20 sec
|
||||||
|
},
|
||||||
|
},
|
||||||
// Form settings
|
// Form settings
|
||||||
form: {
|
form: {
|
||||||
// NOTE: Map server-side validators to comparative Angular form validators
|
// NOTE: Map server-side validators to comparative Angular form validators
|
||||||
|
Reference in New Issue
Block a user