Merge branch 'main' into fix-2197-authority-key-lookup

This commit is contained in:
uofmsean
2023-04-19 17:25:54 -05:00
committed by GitHub
35 changed files with 323 additions and 248 deletions

View File

@@ -17,6 +17,7 @@ export const AuthActionTypes = {
AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'), AUTHENTICATED_SUCCESS: type('dspace/auth/AUTHENTICATED_SUCCESS'),
CHECK_AUTHENTICATION_TOKEN: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN'), CHECK_AUTHENTICATION_TOKEN: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN'),
CHECK_AUTHENTICATION_TOKEN_COOKIE: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_COOKIE'), CHECK_AUTHENTICATION_TOKEN_COOKIE: type('dspace/auth/CHECK_AUTHENTICATION_TOKEN_COOKIE'),
SET_AUTH_COOKIE_STATUS: type('dspace/auth/SET_AUTH_COOKIE_STATUS'),
RETRIEVE_AUTH_METHODS: type('dspace/auth/RETRIEVE_AUTH_METHODS'), RETRIEVE_AUTH_METHODS: type('dspace/auth/RETRIEVE_AUTH_METHODS'),
RETRIEVE_AUTH_METHODS_SUCCESS: type('dspace/auth/RETRIEVE_AUTH_METHODS_SUCCESS'), RETRIEVE_AUTH_METHODS_SUCCESS: type('dspace/auth/RETRIEVE_AUTH_METHODS_SUCCESS'),
RETRIEVE_AUTH_METHODS_ERROR: type('dspace/auth/RETRIEVE_AUTH_METHODS_ERROR'), RETRIEVE_AUTH_METHODS_ERROR: type('dspace/auth/RETRIEVE_AUTH_METHODS_ERROR'),
@@ -150,6 +151,19 @@ export class CheckAuthenticationTokenCookieAction implements Action {
public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE; public type: string = AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE;
} }
/**
* Sets the authentication cookie status to flag an external authentication response.
*/
export class SetAuthCookieStatus implements Action {
public type: string = AuthActionTypes.SET_AUTH_COOKIE_STATUS;
payload = false;
constructor(exists: boolean) {
this.payload = exists;
}
}
/** /**
* Sign out. * Sign out.
* @class LogOutAction * @class LogOutAction
@@ -425,6 +439,7 @@ export type AuthActions
| AuthenticationSuccessAction | AuthenticationSuccessAction
| CheckAuthenticationTokenAction | CheckAuthenticationTokenAction
| CheckAuthenticationTokenCookieAction | CheckAuthenticationTokenCookieAction
| SetAuthCookieStatus
| RedirectWhenAuthenticationIsRequiredAction | RedirectWhenAuthenticationIsRequiredAction
| RedirectWhenTokenExpiredAction | RedirectWhenTokenExpiredAction
| AddAuthenticationMessageAction | AddAuthenticationMessageAction

View File

@@ -214,12 +214,15 @@ describe('AuthEffects', () => {
authenticated: true authenticated: true
}) })
); );
spyOn((authEffects as any).authService, 'setExternalAuthStatus');
actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } }); actions = hot('--a-', { a: { type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN_COOKIE } });
const expected = cold('--b-', { b: new RetrieveTokenAction() }); const expected = cold('--b-', { b: new RetrieveTokenAction() });
expect(authEffects.checkTokenCookie$).toBeObservable(expected); expect(authEffects.checkTokenCookie$).toBeObservable(expected);
authEffects.checkTokenCookie$.subscribe(() => { authEffects.checkTokenCookie$.subscribe(() => {
expect(authServiceStub.setExternalAuthStatus).toHaveBeenCalled();
expect(authServiceStub.isExternalAuthentication).toBeTrue();
expect((authEffects as any).authorizationsService.invalidateAuthorizationsRequestCache).toHaveBeenCalled(); expect((authEffects as any).authorizationsService.invalidateAuthorizationsRequestCache).toHaveBeenCalled();
}); });
}); });

View File

@@ -153,6 +153,7 @@ export class AuthEffects {
return this.authService.checkAuthenticationCookie().pipe( return this.authService.checkAuthenticationCookie().pipe(
map((response: AuthStatus) => { map((response: AuthStatus) => {
if (response.authenticated) { if (response.authenticated) {
this.authService.setExternalAuthStatus(true);
this.authorizationsService.invalidateAuthorizationsRequestCache(); this.authorizationsService.invalidateAuthorizationsRequestCache();
return new RetrieveTokenAction(); return new RetrieveTokenAction();
} else { } else {

View File

@@ -8,6 +8,7 @@ import {
AuthenticationErrorAction, AuthenticationErrorAction,
AuthenticationSuccessAction, AuthenticationSuccessAction,
CheckAuthenticationTokenAction, CheckAuthenticationTokenAction,
SetAuthCookieStatus,
CheckAuthenticationTokenCookieAction, CheckAuthenticationTokenCookieAction,
LogOutAction, LogOutAction,
LogOutErrorAction, LogOutErrorAction,
@@ -219,6 +220,28 @@ describe('authReducer', () => {
expect(newState).toEqual(state); expect(newState).toEqual(state);
}); });
it('should set the authentication cookie status in response to a SET_AUTH_COOKIE_STATUS action', () => {
initialState = {
authenticated: true,
loaded: false,
blocking: false,
loading: true,
externalAuth: false,
idle: false
};
const action = new SetAuthCookieStatus(true);
const newState = authReducer(initialState, action);
state = {
authenticated: true,
loaded: false,
blocking: false,
loading: true,
externalAuth: true,
idle: false
};
expect(newState).toEqual(state);
});
it('should properly set the state, in response to a LOG_OUT action', () => { it('should properly set the state, in response to a LOG_OUT action', () => {
initialState = { initialState = {
authenticated: true, authenticated: true,

View File

@@ -10,7 +10,7 @@ import {
RedirectWhenTokenExpiredAction, RedirectWhenTokenExpiredAction,
RefreshTokenSuccessAction, RefreshTokenSuccessAction,
RetrieveAuthenticatedEpersonSuccessAction, RetrieveAuthenticatedEpersonSuccessAction,
RetrieveAuthMethodsSuccessAction, RetrieveAuthMethodsSuccessAction, SetAuthCookieStatus,
SetRedirectUrlAction SetRedirectUrlAction
} from './auth.actions'; } from './auth.actions';
// import models // import models
@@ -59,6 +59,8 @@ export interface AuthState {
// all authentication Methods enabled at the backend // all authentication Methods enabled at the backend
authMethods?: AuthMethod[]; authMethods?: AuthMethod[];
externalAuth?: boolean,
// true when the current user is idle // true when the current user is idle
idle: boolean; idle: boolean;
@@ -73,6 +75,7 @@ const initialState: AuthState = {
blocking: true, blocking: true,
loading: false, loading: false,
authMethods: [], authMethods: [],
externalAuth: false,
idle: false idle: false
}; };
@@ -104,6 +107,11 @@ export function authReducer(state: any = initialState, action: AuthActions): Aut
loading: true, loading: true,
}); });
case AuthActionTypes.SET_AUTH_COOKIE_STATUS:
return Object.assign({}, state, {
externalAuth: (action as SetAuthCookieStatus).payload
});
case AuthActionTypes.AUTHENTICATED_ERROR: case AuthActionTypes.AUTHENTICATED_ERROR:
case AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_ERROR: case AuthActionTypes.RETRIEVE_AUTHENTICATED_EPERSON_ERROR:
return Object.assign({}, state, { return Object.assign({}, state, {

View File

@@ -25,7 +25,7 @@ import {
import { CookieService } from '../services/cookie.service'; import { CookieService } from '../services/cookie.service';
import { import {
getAuthenticatedUserId, getAuthenticatedUserId,
getAuthenticationToken, getAuthenticationToken, getExternalAuthCookieStatus,
getRedirectUrl, getRedirectUrl,
isAuthenticated, isAuthenticated,
isAuthenticatedLoaded, isAuthenticatedLoaded,
@@ -36,7 +36,7 @@ import { AppState } from '../../app.reducer';
import { import {
CheckAuthenticationTokenAction, CheckAuthenticationTokenAction,
RefreshTokenAction, RefreshTokenAction,
ResetAuthenticationMessagesAction, ResetAuthenticationMessagesAction, SetAuthCookieStatus,
SetRedirectUrlAction, SetRedirectUrlAction,
SetUserAsIdleAction, SetUserAsIdleAction,
UnsetUserAsIdleAction UnsetUserAsIdleAction
@@ -156,6 +156,24 @@ export class AuthService {
return this.store.pipe(select(isAuthenticatedLoaded)); return this.store.pipe(select(isAuthenticatedLoaded));
} }
/**
* Used to set the external authentication status when authenticating via an
* external authentication system (e.g. Shibboleth).
* @param external
*/
public setExternalAuthStatus(external: boolean) {
this.store.dispatch(new SetAuthCookieStatus(external));
}
/**
* Returns true if an external authentication system (e.g. Shibboleth) is being used
* for authentication. Returns false otherwise.
*/
public isExternalAuthentication(): Observable<boolean> {
return this.store.pipe(
select(getExternalAuthCookieStatus));
}
/** /**
* Returns the href link to authenticated user * Returns the href link to authenticated user
* @returns {string} * @returns {string}

View File

@@ -116,6 +116,8 @@ const _getRedirectUrl = (state: AuthState) => state.redirectUrl;
const _getAuthenticationMethods = (state: AuthState) => state.authMethods; const _getAuthenticationMethods = (state: AuthState) => state.authMethods;
const _getExternalAuthCookieStatus = (state: AuthState) => state.externalAuth;
/** /**
* Returns true if the user is idle. * Returns true if the user is idle.
* @function _isIdle * @function _isIdle
@@ -178,6 +180,16 @@ export const isAuthenticated = createSelector(getAuthState, _isAuthenticated);
*/ */
export const isAuthenticatedLoaded = createSelector(getAuthState, _isAuthenticatedLoaded); export const isAuthenticatedLoaded = createSelector(getAuthState, _isAuthenticatedLoaded);
/**
* Returns the authentication cookie status. Expect to be true when external authentication
* is used.
* @function getExternalAuthCookieStatus
* @param {AuthState} state
* @param {any} props
* @return {boolean}
*/
export const getExternalAuthCookieStatus = createSelector(getAuthState, _getExternalAuthCookieStatus);
/** /**
* Returns true if the authentication request is loading. * Returns true if the authentication request is loading.
* @function isAuthenticationLoading * @function isAuthenticationLoading

View File

@@ -7,7 +7,7 @@ import { FooterComponent } from './footer.component';
*/ */
@Component({ @Component({
selector: 'ds-themed-footer', selector: 'ds-themed-footer',
styleUrls: ['footer.component.scss'], styleUrls: [],
templateUrl: '../shared/theme-support/themed.component.html', templateUrl: '../shared/theme-support/themed.component.html',
}) })
export class ThemedFooterComponent extends ThemedComponent<FooterComponent> { export class ThemedFooterComponent extends ThemedComponent<FooterComponent> {
@@ -20,6 +20,6 @@ export class ThemedFooterComponent extends ThemedComponent<FooterComponent> {
} }
protected importUnthemedComponent(): Promise<any> { protected importUnthemedComponent(): Promise<any> {
return import(`./footer.component`); return import('./footer.component');
} }
} }

View File

@@ -3,11 +3,11 @@ import { ThemedComponent } from '../shared/theme-support/themed.component';
import { HeaderNavbarWrapperComponent } from './header-navbar-wrapper.component'; import { HeaderNavbarWrapperComponent } from './header-navbar-wrapper.component';
/** /**
* Themed wrapper for BreadcrumbsComponent * Themed wrapper for {@link HeaderNavbarWrapperComponent}
*/ */
@Component({ @Component({
selector: 'ds-themed-header-navbar-wrapper', selector: 'ds-themed-header-navbar-wrapper',
styleUrls: ['./themed-header-navbar-wrapper.component.scss'], styleUrls: [],
templateUrl: '../shared/theme-support/themed.component.html', templateUrl: '../shared/theme-support/themed.component.html',
}) })
export class ThemedHeaderNavbarWrapperComponent extends ThemedComponent<HeaderNavbarWrapperComponent> { export class ThemedHeaderNavbarWrapperComponent extends ThemedComponent<HeaderNavbarWrapperComponent> {
@@ -20,6 +20,6 @@ export class ThemedHeaderNavbarWrapperComponent extends ThemedComponent<HeaderNa
} }
protected importUnthemedComponent(): Promise<any> { protected importUnthemedComponent(): Promise<any> {
return import(`./header-navbar-wrapper.component`); return import('./header-navbar-wrapper.component');
} }
} }

View File

@@ -8,8 +8,6 @@ import { Component } from '@angular/core';
templateUrl: '../shared/theme-support/themed.component.html', templateUrl: '../shared/theme-support/themed.component.html',
}) })
export class ThemedHomePageComponent extends ThemedComponent<HomePageComponent> { export class ThemedHomePageComponent extends ThemedComponent<HomePageComponent> {
protected inAndOutputNames: (keyof HomePageComponent & keyof this)[];
protected getComponentName(): string { protected getComponentName(): string {
return 'HomePageComponent'; return 'HomePageComponent';

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnChanges, OnInit } from '@angular/core';
import { NgxGalleryImage, NgxGalleryOptions } from '@kolkov/ngx-gallery'; import { NgxGalleryImage, NgxGalleryOptions } from '@kolkov/ngx-gallery';
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model'; import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
import { NgxGalleryAnimation } from '@kolkov/ngx-gallery'; import { NgxGalleryAnimation } from '@kolkov/ngx-gallery';
@@ -13,15 +13,16 @@ import { AuthService } from '../../../core/auth/auth.service';
templateUrl: './media-viewer-image.component.html', templateUrl: './media-viewer-image.component.html',
styleUrls: ['./media-viewer-image.component.scss'], styleUrls: ['./media-viewer-image.component.scss'],
}) })
export class MediaViewerImageComponent implements OnInit { export class MediaViewerImageComponent implements OnChanges, OnInit {
@Input() images: MediaViewerItem[]; @Input() images: MediaViewerItem[];
@Input() preview?: boolean; @Input() preview?: boolean;
@Input() image?: string; @Input() image?: string;
thumbnailPlaceholder = './assets/images/replacement_image.svg'; thumbnailPlaceholder = './assets/images/replacement_image.svg';
galleryOptions: NgxGalleryOptions[]; galleryOptions: NgxGalleryOptions[] = [];
galleryImages: NgxGalleryImage[];
galleryImages: NgxGalleryImage[] = [];
/** /**
* Whether or not the current user is authenticated * Whether or not the current user is authenticated
@@ -33,11 +34,7 @@ export class MediaViewerImageComponent implements OnInit {
) { ) {
} }
/** ngOnChanges(): void {
* Thi method sets up the gallery settings and data
*/
ngOnInit(): void {
this.isAuthenticated$ = this.authService.isAuthenticated();
this.galleryOptions = [ this.galleryOptions = [
{ {
preview: this.preview !== undefined ? this.preview : true, preview: this.preview !== undefined ? this.preview : true,
@@ -53,7 +50,6 @@ export class MediaViewerImageComponent implements OnInit {
previewFullscreen: true, previewFullscreen: true,
}, },
]; ];
if (this.image) { if (this.image) {
this.galleryImages = [ this.galleryImages = [
{ {
@@ -67,6 +63,11 @@ export class MediaViewerImageComponent implements OnInit {
} }
} }
ngOnInit(): void {
this.isAuthenticated$ = this.authService.isAuthenticated();
this.ngOnChanges();
}
/** /**
* This method convert an array of MediaViewerItem into NgxGalleryImage array * This method convert an array of MediaViewerItem into NgxGalleryImage array
* @param medias input NgxGalleryImage array * @param medias input NgxGalleryImage array

View File

@@ -1,23 +1,22 @@
<video <video
crossorigin="anonymous" crossorigin="anonymous"
#media [src]="medias[currentIndex].bitstream._links.content.href"
[src]="filteredMedias[currentIndex].bitstream._links.content.href"
id="singleVideo" id="singleVideo"
[poster]=" [poster]="
filteredMedias[currentIndex].thumbnail || medias[currentIndex].thumbnail ||
replacements[filteredMedias[currentIndex].format] replacements[medias[currentIndex].format]
" "
preload="none" preload="none"
controls controls
> >
<ng-container *ngIf="getMediaCap(filteredMedias[currentIndex].bitstream.name) as capInfos"> <ng-container *ngIf="getMediaCap(medias[currentIndex].bitstream.name, captions) as capInfos">
<ng-container *ngFor="let capInfo of capInfos"> <ng-container *ngFor="let capInfo of capInfos">
<track [src]="capInfo.src" [label]="capInfo.langLabel" [srclang]="capInfo.srclang" /> <track [src]="capInfo.src" [label]="capInfo.langLabel" [srclang]="capInfo.srclang" />
</ng-container> </ng-container>
</ng-container> </ng-container>
</video> </video>
<div class="buttons" *ngIf="filteredMedias?.length > 1"> <div class="buttons" *ngIf="medias?.length > 1">
<button <button
class="btn btn-primary previous" class="btn btn-primary previous"
[disabled]="currentIndex === 0" [disabled]="currentIndex === 0"
@@ -28,7 +27,7 @@
<button <button
class="btn btn-primary next" class="btn btn-primary next"
[disabled]="currentIndex === filteredMedias.length - 1" [disabled]="currentIndex === medias.length - 1"
(click)="nextMedia()" (click)="nextMedia()"
> >
{{ "media-viewer.next" | translate }} {{ "media-viewer.next" | translate }}
@@ -44,7 +43,7 @@
<div ngbDropdownMenu aria-labelledby="dropdownBasic1"> <div ngbDropdownMenu aria-labelledby="dropdownBasic1">
<button <button
ngbDropdownItem ngbDropdownItem
*ngFor="let item of filteredMedias; index as indexOfelement" *ngFor="let item of medias; index as indexOfelement"
class="list-element" class="list-element"
(click)="selectedMedia(indexOfelement)" (click)="selectedMedia(indexOfelement)"
> >

View File

@@ -83,7 +83,6 @@ describe('MediaViewerVideoComponent', () => {
fixture = TestBed.createComponent(MediaViewerVideoComponent); fixture = TestBed.createComponent(MediaViewerVideoComponent);
component = fixture.componentInstance; component = fixture.componentInstance;
component.medias = mockMediaViewerItem; component.medias = mockMediaViewerItem;
component.filteredMedias = mockMediaViewerItem;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -94,7 +93,6 @@ describe('MediaViewerVideoComponent', () => {
describe('should show controller buttons when the having mode then one video', () => { describe('should show controller buttons when the having mode then one video', () => {
beforeEach(() => { beforeEach(() => {
component.medias = mockMediaViewerItems; component.medias = mockMediaViewerItems;
component.filteredMedias = mockMediaViewerItems;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -1,7 +1,8 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input } from '@angular/core';
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model'; import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
import { languageHelper } from './language-helper'; import { languageHelper } from './language-helper';
import { CaptionInfo } from './caption-info'; import { CaptionInfo } from './caption-info';
import { Bitstream } from 'src/app/core/shared/bitstream.model';
/** /**
* This component renders a video viewer and playlist for the media viewer * This component renders a video viewer and playlist for the media viewer
@@ -11,12 +12,13 @@ import { CaptionInfo} from './caption-info';
templateUrl: './media-viewer-video.component.html', templateUrl: './media-viewer-video.component.html',
styleUrls: ['./media-viewer-video.component.scss'], styleUrls: ['./media-viewer-video.component.scss'],
}) })
export class MediaViewerVideoComponent implements OnInit { export class MediaViewerVideoComponent {
@Input() medias: MediaViewerItem[]; @Input() medias: MediaViewerItem[];
filteredMedias: MediaViewerItem[]; @Input() captions: Bitstream[] = [];
isCollapsed = false;
isCollapsed: boolean;
currentIndex = 0; currentIndex = 0;
replacements = { replacements = {
@@ -24,11 +26,6 @@ export class MediaViewerVideoComponent implements OnInit {
audio: './assets/images/replacement_audio.svg', audio: './assets/images/replacement_audio.svg',
}; };
ngOnInit() {
this.isCollapsed = false;
this.filteredMedias = this.medias.filter((media) => media.format === 'audio' || media.format === 'video');
}
/** /**
* This method check if there is caption file for the media * This method check if there is caption file for the media
* The caption file name is the media name plus "-" following two letter * The caption file name is the media name plus "-" following two letter
@@ -39,29 +36,24 @@ export class MediaViewerVideoComponent implements OnInit {
* Two letter language code reference * Two letter language code reference
* https://www.w3schools.com/tags/ref_language_codes.asp * https://www.w3schools.com/tags/ref_language_codes.asp
*/ */
getMediaCap(name: string): CaptionInfo[] { getMediaCap(name: string, captions: Bitstream[]): CaptionInfo[] {
let filteredCapMedias: MediaViewerItem[]; const capInfos: CaptionInfo[] = [];
let capInfos: CaptionInfo[] = []; const filteredCapMedias: Bitstream[] = captions
filteredCapMedias = this.medias .filter((media: Bitstream) => media.name.substring(0, (media.name.length - 7)).toLowerCase() === name.toLowerCase());
.filter((media) => media.mimetype === 'text/vtt')
.filter((media) => media.bitstream.name.substring(0, (media.bitstream.name.length - 7) ).toLowerCase() === name.toLowerCase());
if (filteredCapMedias) { for (const media of filteredCapMedias) {
filteredCapMedias let srclang: string = media.name.slice(-6, -4).toLowerCase();
.forEach((media, index) => {
let srclang: string = media.bitstream.name.slice(-6, -4).toLowerCase();
capInfos.push(new CaptionInfo( capInfos.push(new CaptionInfo(
media.bitstream._links.content.href, media._links.content.href,
srclang, srclang,
languageHelper[srclang] languageHelper[srclang],
)); ));
});
} }
return capInfos; return capInfos;
} }
/** /**
* This method sets the reviced index into currentIndex * This method sets the received index into currentIndex
* @param index Selected index * @param index Selected index
*/ */
selectedMedia(index: number) { selectedMedia(index: number) {
@@ -69,14 +61,14 @@ export class MediaViewerVideoComponent implements OnInit {
} }
/** /**
* This method increade the number of the currentIndex * This method increases the number of the currentIndex
*/ */
nextMedia() { nextMedia() {
this.currentIndex++; this.currentIndex++;
} }
/** /**
* This method decrese the number of the currentIndex * This method decreases the number of the currentIndex
*/ */
prevMedia() { prevMedia() {
this.currentIndex--; this.currentIndex--;

View File

@@ -2,6 +2,7 @@ import { Component, Input } from '@angular/core';
import { ThemedComponent } from '../../../shared/theme-support/themed.component'; import { ThemedComponent } from '../../../shared/theme-support/themed.component';
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model'; import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
import { MediaViewerVideoComponent } from './media-viewer-video.component'; import { MediaViewerVideoComponent } from './media-viewer-video.component';
import { Bitstream } from '../../../core/shared/bitstream.model';
/** /**
* Themed wrapper for {@link MediaViewerVideoComponent}. * Themed wrapper for {@link MediaViewerVideoComponent}.
@@ -15,8 +16,11 @@ export class ThemedMediaViewerVideoComponent extends ThemedComponent<MediaViewer
@Input() medias: MediaViewerItem[]; @Input() medias: MediaViewerItem[];
@Input() captions: Bitstream[];
protected inAndOutputNames: (keyof MediaViewerVideoComponent & keyof this)[] = [ protected inAndOutputNames: (keyof MediaViewerVideoComponent & keyof this)[] = [
'medias', 'medias',
'captions',
]; ];
protected getComponentName(): string { protected getComponentName(): string {

View File

@@ -5,32 +5,23 @@
[showMessage]="false" [showMessage]="false"
></ds-themed-loading> ></ds-themed-loading>
<div class="media-viewer" *ngIf="!isLoading"> <div class="media-viewer" *ngIf="!isLoading">
<ng-container *ngIf="mediaList.length > 0"> <ng-container *ngIf="mediaList.length > 0; else showThumbnail">
<ng-container *ngIf="videoOptions"> <ng-container *ngVar="mediaOptions.video && ['audio', 'video'].includes(mediaList[0]?.format) as showVideo">
<ng-container <ng-container *ngVar="mediaOptions.image && mediaList[0]?.format === 'image' as showImage">
*ngIf=" <ds-themed-media-viewer-video *ngIf="showVideo" [medias]="mediaList" [captions]="captions$ | async"></ds-themed-media-viewer-video>
mediaList[0]?.format === 'video' || mediaList[0]?.format === 'audio' <ds-themed-media-viewer-image *ngIf="showImage" [images]="mediaList"></ds-themed-media-viewer-image>
" <ng-container *ngIf="showImage || showVideo; else showThumbnail"></ng-container>
>
<ds-themed-media-viewer-video [medias]="mediaList"></ds-themed-media-viewer-video>
</ng-container> </ng-container>
</ng-container> </ng-container>
<ng-container *ngIf="mediaList[0]?.format === 'image'">
<ds-themed-media-viewer-image [images]="mediaList"></ds-themed-media-viewer-image>
</ng-container>
</ng-container>
<ng-container
*ngIf="
((mediaList[0]?.format !== 'image') &&
(!videoOptions || mediaList[0]?.format !== 'video') &&
(!videoOptions || mediaList[0]?.format !== 'audio')) ||
mediaList.length === 0
"
>
<ds-themed-media-viewer-image
[image]="mediaList[0]?.thumbnail || thumbnailPlaceholder"
[preview]="false"
></ds-themed-media-viewer-image>
</ng-container> </ng-container>
</div> </div>
<ng-template #showThumbnail>
<ds-themed-media-viewer-image *ngIf="mediaOptions.image && mediaOptions.video"
[image]="(thumbnailsRD$ | async)?.payload?.page[0]?._links.content.href || thumbnailPlaceholder"
[preview]="false"
></ds-themed-media-viewer-image>
<ds-thumbnail *ngIf="!(mediaOptions.image && mediaOptions.video)"
[thumbnail]="(thumbnailsRD$ | async)?.payload?.page[0]">
</ds-thumbnail>
</ng-template>
</ng-container> </ng-container>

View File

@@ -61,7 +61,7 @@ describe('MediaViewerComponent', () => {
); );
beforeEach(waitForAsync(() => { beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({ return TestBed.configureTestingModule({
imports: [ imports: [
TranslateModule.forRoot({ TranslateModule.forRoot({
loader: { loader: {
@@ -94,7 +94,10 @@ describe('MediaViewerComponent', () => {
describe('when the bitstreams are loading', () => { describe('when the bitstreams are loading', () => {
beforeEach(() => { beforeEach(() => {
comp.mediaList$.next([mockMediaViewerItem]); comp.mediaList$.next([mockMediaViewerItem]);
comp.videoOptions = true; comp.mediaOptions = {
image: true,
video: true,
};
comp.isLoading = true; comp.isLoading = true;
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -118,7 +121,10 @@ describe('MediaViewerComponent', () => {
describe('when the bitstreams loading is failed', () => { describe('when the bitstreams loading is failed', () => {
beforeEach(() => { beforeEach(() => {
comp.mediaList$.next([]); comp.mediaList$.next([]);
comp.videoOptions = true; comp.mediaOptions = {
image: true,
video: true,
};
comp.isLoading = false; comp.isLoading = false;
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { BehaviorSubject, Observable } from 'rxjs'; import { BehaviorSubject, Observable } from 'rxjs';
import { filter, take } from 'rxjs/operators'; import { filter, take } from 'rxjs/operators';
import { BitstreamDataService } from '../../core/data/bitstream-data.service'; import { BitstreamDataService } from '../../core/data/bitstream-data.service';
@@ -11,6 +11,9 @@ import { MediaViewerItem } from '../../core/shared/media-viewer-item.model';
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators'; import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { followLink } from '../../shared/utils/follow-link-config.model'; import { followLink } from '../../shared/utils/follow-link-config.model';
import { MediaViewerConfig } from '../../../config/media-viewer-config.interface';
import { environment } from '../../../environments/environment';
import { Subscription } from 'rxjs/internal/Subscription';
/** /**
* This component renders the media viewers * This component renders the media viewers
@@ -20,51 +23,71 @@ import { followLink } from '../../shared/utils/follow-link-config.model';
templateUrl: './media-viewer.component.html', templateUrl: './media-viewer.component.html',
styleUrls: ['./media-viewer.component.scss'], styleUrls: ['./media-viewer.component.scss'],
}) })
export class MediaViewerComponent implements OnInit { export class MediaViewerComponent implements OnDestroy, OnInit {
@Input() item: Item; @Input() item: Item;
@Input() videoOptions: boolean;
mediaList$: BehaviorSubject<MediaViewerItem[]>; @Input() mediaOptions: MediaViewerConfig = environment.mediaViewer;
isLoading: boolean; mediaList$: BehaviorSubject<MediaViewerItem[]> = new BehaviorSubject([]);
captions$: BehaviorSubject<Bitstream[]> = new BehaviorSubject([]);
isLoading = true;
thumbnailPlaceholder = './assets/images/replacement_document.svg'; thumbnailPlaceholder = './assets/images/replacement_document.svg';
constructor(protected bitstreamDataService: BitstreamDataService) {} thumbnailsRD$: Observable<RemoteData<PaginatedList<Bitstream>>>;
subs: Subscription[] = [];
constructor(
protected bitstreamDataService: BitstreamDataService,
) {
}
ngOnDestroy(): void {
this.subs.forEach((subscription: Subscription) => subscription.unsubscribe());
}
/** /**
* This metod loads all the Bitstreams and Thumbnails and contert it to media item * This method loads all the Bitstreams and Thumbnails and converts it to {@link MediaViewerItem}s
*/ */
ngOnInit(): void { ngOnInit(): void {
this.mediaList$ = new BehaviorSubject([]); const types: string[] = [
this.isLoading = true; ...(this.mediaOptions.image ? ['image'] : []),
this.loadRemoteData('ORIGINAL').subscribe((bitstreamsRD) => { ...(this.mediaOptions.video ? ['audio', 'video'] : []),
];
this.thumbnailsRD$ = this.loadRemoteData('THUMBNAIL');
this.subs.push(this.loadRemoteData('ORIGINAL').subscribe((bitstreamsRD: RemoteData<PaginatedList<Bitstream>>) => {
if (bitstreamsRD.payload.page.length === 0) { if (bitstreamsRD.payload.page.length === 0) {
this.isLoading = false; this.isLoading = false;
this.mediaList$.next([]); this.mediaList$.next([]);
} else { } else {
this.loadRemoteData('THUMBNAIL').subscribe((thumbnailsRD) => { this.subs.push(this.thumbnailsRD$.subscribe((thumbnailsRD: RemoteData<PaginatedList<Bitstream>>) => {
for ( for (
let index = 0; let index = 0;
index < bitstreamsRD.payload.page.length; index < bitstreamsRD.payload.page.length;
index++ index++
) { ) {
bitstreamsRD.payload.page[index].format this.subs.push(bitstreamsRD.payload.page[index].format
.pipe(getFirstSucceededRemoteDataPayload()) .pipe(getFirstSucceededRemoteDataPayload())
.subscribe((format) => { .subscribe((format: BitstreamFormat) => {
const current = this.mediaList$.getValue();
const mediaItem = this.createMediaViewerItem( const mediaItem = this.createMediaViewerItem(
bitstreamsRD.payload.page[index], bitstreamsRD.payload.page[index],
format, format,
thumbnailsRD.payload && thumbnailsRD.payload.page[index] thumbnailsRD.payload && thumbnailsRD.payload.page[index]
); );
this.mediaList$.next([...current, mediaItem]); if (types.includes(mediaItem.format)) {
}); this.mediaList$.next([...this.mediaList$.getValue(), mediaItem]);
} else if (format.mimetype === 'text/vtt') {
this.captions$.next([...this.captions$.getValue(), bitstreamsRD.payload.page[index]]);
}
}));
} }
this.isLoading = false; this.isLoading = false;
}); }));
} }
}); }));
} }
/** /**
@@ -94,16 +117,12 @@ export class MediaViewerComponent implements OnInit {
} }
/** /**
* This method create MediaViewerItem from incoming bitstreams * This method creates a {@link MediaViewerItem} from incoming {@link Bitstream}s
* @param original original remote data bitstream * @param original original bitstream
* @param format original bitstream format * @param format original bitstream format
* @param thumbnail trunbnail remote data bitstream * @param thumbnail thumbnail bitstream
*/ */
createMediaViewerItem( createMediaViewerItem(original: Bitstream, format: BitstreamFormat, thumbnail: Bitstream): MediaViewerItem {
original: Bitstream,
format: BitstreamFormat,
thumbnail: Bitstream
): MediaViewerItem {
const mediaItem = new MediaViewerItem(); const mediaItem = new MediaViewerItem();
mediaItem.bitstream = original; mediaItem.bitstream = original;
mediaItem.format = format.mimetype.split('/')[0]; mediaItem.format = format.mimetype.split('/')[0];

View File

@@ -2,6 +2,7 @@ import { Component, Input } from '@angular/core';
import { ThemedComponent } from '../../shared/theme-support/themed.component'; import { ThemedComponent } from '../../shared/theme-support/themed.component';
import { MediaViewerComponent } from './media-viewer.component'; import { MediaViewerComponent } from './media-viewer.component';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { MediaViewerConfig } from '../../../config/media-viewer-config.interface';
/** /**
* Themed wrapper for {@link MediaViewerComponent}. * Themed wrapper for {@link MediaViewerComponent}.
@@ -14,11 +15,11 @@ import { Item } from '../../core/shared/item.model';
export class ThemedMediaViewerComponent extends ThemedComponent<MediaViewerComponent> { export class ThemedMediaViewerComponent extends ThemedComponent<MediaViewerComponent> {
@Input() item: Item; @Input() item: Item;
@Input() videoOptions: boolean; @Input() mediaOptions: MediaViewerConfig;
protected inAndOutputNames: (keyof MediaViewerComponent & keyof this)[] = [ protected inAndOutputNames: (keyof MediaViewerComponent & keyof this)[] = [
'item', 'item',
'videoOptions', 'mediaOptions',
]; ];
protected getComponentName(): string { protected getComponentName(): string {

View File

@@ -15,13 +15,13 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ng-container *ngIf="!mediaViewer.image"> <ng-container *ngIf="!(mediaViewer.image || mediaViewer.video)">
<ds-metadata-field-wrapper [hideIfNoTextContent]="false"> <ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail> <ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
</ng-container> </ng-container>
<div *ngIf="mediaViewer.image" class="mb-2"> <div *ngIf="mediaViewer.image || mediaViewer.video" class="mb-2">
<ds-themed-media-viewer [item]="object" [videoOptions]="mediaViewer.video"></ds-themed-media-viewer> <ds-themed-media-viewer [item]="object"></ds-themed-media-viewer>
</div> </div>
<ds-themed-item-page-file-section [item]="object"></ds-themed-item-page-file-section> <ds-themed-item-page-file-section [item]="object"></ds-themed-item-page-file-section>
<ds-item-page-date-field [item]="object"></ds-item-page-date-field> <ds-item-page-date-field [item]="object"></ds-item-page-date-field>

View File

@@ -16,13 +16,13 @@
</div> </div>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ng-container *ngIf="!mediaViewer.image"> <ng-container *ngIf="!(mediaViewer.image || mediaViewer.video)">
<ds-metadata-field-wrapper [hideIfNoTextContent]="false"> <ds-metadata-field-wrapper [hideIfNoTextContent]="false">
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail> <ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
</ng-container> </ng-container>
<div *ngIf="mediaViewer.image" class="mb-2"> <div *ngIf="mediaViewer.image || mediaViewer.video" class="mb-2">
<ds-themed-media-viewer [item]="object" [videoOptions]="mediaViewer.video"></ds-themed-media-viewer> <ds-themed-media-viewer [item]="object"></ds-themed-media-viewer>
</div> </div>
<ds-themed-item-page-file-section [item]="object"></ds-themed-item-page-file-section> <ds-themed-item-page-file-section [item]="object"></ds-themed-item-page-file-section>
<ds-item-page-date-field [item]="object"></ds-item-page-date-field> <ds-item-page-date-field [item]="object"></ds-item-page-date-field>

View File

@@ -19,7 +19,7 @@ export class ThemedMetadataRepresentationListComponent extends ThemedComponent<M
@Input() label: string; @Input() label: string;
@Input() incrementBy = 10; @Input() incrementBy: number;
protected getComponentName(): string { protected getComponentName(): string {
return 'MetadataRepresentationListComponent'; return 'MetadataRepresentationListComponent';

View File

@@ -11,7 +11,6 @@ import { MyDSpacePageComponent } from './my-dspace-page.component';
templateUrl: './../shared/theme-support/themed.component.html' templateUrl: './../shared/theme-support/themed.component.html'
}) })
export class ThemedMyDSpacePageComponent extends ThemedComponent<MyDSpacePageComponent> { export class ThemedMyDSpacePageComponent extends ThemedComponent<MyDSpacePageComponent> {
protected inAndOutputNames: (keyof MyDSpacePageComponent & keyof this)[];
protected getComponentName(): string { protected getComponentName(): string {
return 'MyDSpacePageComponent'; return 'MyDSpacePageComponent';

View File

@@ -28,19 +28,18 @@ export class ThemedConfigurationSearchPageComponent extends ThemedComponent<Conf
/** /**
* True when the search component should show results on the current page * True when the search component should show results on the current page
*/ */
@Input() inPlaceSearch = true; @Input() inPlaceSearch: boolean;
/** /**
* Whether or not the search bar should be visible * Whether or not the search bar should be visible
*/ */
@Input() @Input() searchEnabled: boolean;
searchEnabled = true;
/** /**
* The width of the sidebar (bootstrap columns) * The width of the sidebar (bootstrap columns)
*/ */
@Input() @Input()
sideBarWidth = 3; sideBarWidth: number;
/** /**
* The currently applied configuration (determines title of search) * The currently applied configuration (determines title of search)
@@ -66,7 +65,7 @@ export class ThemedConfigurationSearchPageComponent extends ThemedComponent<Conf
} }
protected importUnthemedComponent(): Promise<any> { protected importUnthemedComponent(): Promise<any> {
return import(`./configuration-search-page.component`); return import('./configuration-search-page.component');
} }
} }

View File

@@ -11,11 +11,11 @@ export class ThemedCollectionDropdownComponent extends ThemedComponent<Collectio
@Input() entityType: string; @Input() entityType: string;
@Output() searchComplete = new EventEmitter<any>(); @Output() searchComplete: EventEmitter<any> = new EventEmitter();
@Output() theOnlySelectable = new EventEmitter<CollectionListEntry>(); @Output() theOnlySelectable: EventEmitter<CollectionListEntry> = new EventEmitter();
@Output() selectionChange = new EventEmitter<CollectionListEntry>(); @Output() selectionChange = new EventEmitter();
protected inAndOutputNames: (keyof CollectionDropdownComponent & keyof this)[] = ['entityType', 'searchComplete', 'theOnlySelectable', 'selectionChange']; protected inAndOutputNames: (keyof CollectionDropdownComponent & keyof this)[] = ['entityType', 'searchComplete', 'theOnlySelectable', 'selectionChange'];

View File

@@ -4,27 +4,14 @@ import { RelationshipOptions } from '../../../models/relationship-options.model'
import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; import { ListableObject } from '../../../../../object-collection/shared/listable-object.model';
import { Context } from '../../../../../../core/shared/context.model'; import { Context } from '../../../../../../core/shared/context.model';
import { Item } from '../../../../../../core/shared/item.model'; import { Item } from '../../../../../../core/shared/item.model';
import { SEARCH_CONFIG_SERVICE } from '../../../../../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
import { Collection } from '../../../../../../core/shared/collection.model'; import { Collection } from '../../../../../../core/shared/collection.model';
import { ExternalSource } from '../../../../../../core/shared/external-source.model'; import { ExternalSource } from '../../../../../../core/shared/external-source.model';
import { DsDynamicLookupRelationExternalSourceTabComponent } from './dynamic-lookup-relation-external-source-tab.component'; import { DsDynamicLookupRelationExternalSourceTabComponent } from './dynamic-lookup-relation-external-source-tab.component';
import { fadeIn, fadeInOut } from '../../../../../animations/fade';
@Component({ @Component({
selector: 'ds-themed-dynamic-lookup-relation-external-source-tab', selector: 'ds-themed-dynamic-lookup-relation-external-source-tab',
styleUrls: [], styleUrls: [],
templateUrl: '../../../../../theme-support/themed.component.html', templateUrl: '../../../../../theme-support/themed.component.html',
providers: [
{
provide: SEARCH_CONFIG_SERVICE,
useClass: SearchConfigurationService
}
],
animations: [
fadeIn,
fadeInOut
]
}) })
export class ThemedDynamicLookupRelationExternalSourceTabComponent extends ThemedComponent<DsDynamicLookupRelationExternalSourceTabComponent> { export class ThemedDynamicLookupRelationExternalSourceTabComponent extends ThemedComponent<DsDynamicLookupRelationExternalSourceTabComponent> {
protected inAndOutputNames: (keyof DsDynamicLookupRelationExternalSourceTabComponent & keyof this)[] = ['label', 'listId', protected inAndOutputNames: (keyof DsDynamicLookupRelationExternalSourceTabComponent & keyof this)[] = ['label', 'listId',
@@ -44,7 +31,7 @@ export class ThemedDynamicLookupRelationExternalSourceTabComponent extends Theme
@Input() repeatable: boolean; @Input() repeatable: boolean;
@Output() importedObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() importedObject: EventEmitter<ListableObject> = new EventEmitter();
@Input() externalSource: ExternalSource; @Input() externalSource: ExternalSource;

View File

@@ -10,19 +10,11 @@ import { Item } from '../../../../../../core/shared/item.model';
import { SearchResult } from '../../../../../search/models/search-result.model'; import { SearchResult } from '../../../../../search/models/search-result.model';
import { SearchObjects } from '../../../../../search/models/search-objects.model'; import { SearchObjects } from '../../../../../search/models/search-objects.model';
import { DSpaceObject } from '../../../../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../../../../core/shared/dspace-object.model';
import { SEARCH_CONFIG_SERVICE } from '../../../../../../my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
@Component({ @Component({
selector: 'ds-themed-dynamic-lookup-relation-search-tab', selector: 'ds-themed-dynamic-lookup-relation-search-tab',
styleUrls: [], styleUrls: [],
templateUrl: '../../../../../theme-support/themed.component.html', templateUrl: '../../../../../theme-support/themed.component.html',
providers: [
{
provide: SEARCH_CONFIG_SERVICE,
useClass: SearchConfigurationService
}
]
}) })
export class ThemedDynamicLookupRelationSearchTabComponent extends ThemedComponent<DsDynamicLookupRelationSearchTabComponent> { export class ThemedDynamicLookupRelationSearchTabComponent extends ThemedComponent<DsDynamicLookupRelationSearchTabComponent> {
protected inAndOutputNames: (keyof DsDynamicLookupRelationSearchTabComponent & keyof this)[] = ['relationship', 'listId', protected inAndOutputNames: (keyof DsDynamicLookupRelationSearchTabComponent & keyof this)[] = ['relationship', 'listId',
@@ -51,11 +43,11 @@ export class ThemedDynamicLookupRelationSearchTabComponent extends ThemedCompone
@Input() isEditRelationship: boolean; @Input() isEditRelationship: boolean;
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter();
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() selectObject: EventEmitter<ListableObject> = new EventEmitter();
@Output() resultFound: EventEmitter<SearchObjects<DSpaceObject>> = new EventEmitter<SearchObjects<DSpaceObject>>(); @Output() resultFound: EventEmitter<SearchObjects<DSpaceObject>> = new EventEmitter();
protected getComponentName(): string { protected getComponentName(): string {
return 'DsDynamicLookupRelationSearchTabComponent'; return 'DsDynamicLookupRelationSearchTabComponent';

View File

@@ -14,8 +14,8 @@ import { ThemeService } from '../theme-support/theme.service';
export class ThemedLoadingComponent extends ThemedComponent<LoadingComponent> { export class ThemedLoadingComponent extends ThemedComponent<LoadingComponent> {
@Input() message: string; @Input() message: string;
@Input() showMessage = true; @Input() showMessage: boolean;
@Input() spinner = false; @Input() spinner: boolean;
protected inAndOutputNames: (keyof LoadingComponent & keyof this)[] = ['message', 'showMessage', 'spinner']; protected inAndOutputNames: (keyof LoadingComponent & keyof this)[] = ['message', 'showMessage', 'spinner'];

View File

@@ -1,11 +1,10 @@
import { ChangeDetectorRef, Component, ComponentFactoryResolver, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { ThemedComponent } from '../../../theme-support/themed.component'; import { ThemedComponent } from '../../../theme-support/themed.component';
import { ItemListPreviewComponent } from './item-list-preview.component'; import { ItemListPreviewComponent } from './item-list-preview.component';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type';
import { SearchResult } from '../../../search/models/search-result.model'; import { SearchResult } from '../../../search/models/search-result.model';
import { WorkflowItem } from 'src/app/core/submission/models/workflowitem.model'; import { WorkflowItem } from 'src/app/core/submission/models/workflowitem.model';
import { ThemeService } from 'src/app/shared/theme-support/theme.service';
/** /**
* Themed wrapper for ItemListPreviewComponent * Themed wrapper for ItemListPreviewComponent
@@ -24,22 +23,10 @@ export class ThemedItemListPreviewComponent extends ThemedComponent<ItemListPrev
@Input() status: MyDspaceItemStatusType; @Input() status: MyDspaceItemStatusType;
@Input() showSubmitter = false; @Input() showSubmitter: boolean;
@Input() workflowItem: WorkflowItem; @Input() workflowItem: WorkflowItem;
constructor(
protected resolver: ComponentFactoryResolver,
protected cdr: ChangeDetectorRef,
protected themeService: ThemeService,
) {
super(resolver, cdr, themeService);
}
ngOnInit() {
super.ngOnInit();
}
protected getComponentName(): string { protected getComponentName(): string {
return 'ItemListPreviewComponent'; return 'ItemListPreviewComponent';
} }

View File

@@ -1,7 +1,6 @@
import {Component, EventEmitter, Input, Output} from '@angular/core'; import {Component, EventEmitter, Input, Output} from '@angular/core';
import { ObjectListComponent } from './object-list.component'; import { ObjectListComponent } from './object-list.component';
import { ThemedComponent } from '../theme-support/themed.component'; import { ThemedComponent } from '../theme-support/themed.component';
import {ViewMode} from '../../core/shared/view-mode.model';
import {PaginationComponentOptions} from '../pagination/pagination-component-options.model'; import {PaginationComponentOptions} from '../pagination/pagination-component-options.model';
import {SortDirection, SortOptions} from '../../core/cache/models/sort-options.model'; import {SortDirection, SortOptions} from '../../core/cache/models/sort-options.model';
import {CollectionElementLinkType} from '../object-collection/collection-element-link.type'; import {CollectionElementLinkType} from '../object-collection/collection-element-link.type';
@@ -19,10 +18,6 @@ import {ListableObject} from '../object-collection/shared/listable-object.model'
templateUrl: '../theme-support/themed.component.html', templateUrl: '../theme-support/themed.component.html',
}) })
export class ThemedObjectListComponent extends ThemedComponent<ObjectListComponent> { export class ThemedObjectListComponent extends ThemedComponent<ObjectListComponent> {
/**
* The view mode of the this component
*/
viewMode = ViewMode.ListElement;
/** /**
* The current pagination configuration * The current pagination configuration
@@ -37,18 +32,20 @@ export class ThemedObjectListComponent extends ThemedComponent<ObjectListCompone
/** /**
* Whether or not the list elements have a border * Whether or not the list elements have a border
*/ */
@Input() hasBorder = false; @Input() hasBorder: boolean;
/** /**
* The whether or not the gear is hidden * The whether or not the gear is hidden
*/ */
@Input() hideGear = false; @Input() hideGear: boolean;
/** /**
* Whether or not the pager is visible when there is only a single page of results * Whether or not the pager is visible when there is only a single page of results
*/ */
@Input() hidePagerWhenSinglePage = true; @Input() hidePagerWhenSinglePage: boolean;
@Input() selectable = false;
@Input() selectable: boolean;
@Input() selectionConfig: { repeatable: boolean, listId: string }; @Input() selectionConfig: { repeatable: boolean, listId: string };
/** /**
@@ -64,12 +61,12 @@ export class ThemedObjectListComponent extends ThemedComponent<ObjectListCompone
/** /**
* Option for hiding the pagination detail * Option for hiding the pagination detail
*/ */
@Input() hidePaginationDetail = false; @Input() hidePaginationDetail: boolean;
/** /**
* Whether or not to add an import button to the object * Whether or not to add an import button to the object
*/ */
@Input() importable = false; @Input() importable: boolean;
/** /**
* Config used for the import button * Config used for the import button
@@ -79,42 +76,24 @@ export class ThemedObjectListComponent extends ThemedComponent<ObjectListCompone
/** /**
* Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination * Whether or not the pagination should be rendered as simple previous and next buttons instead of the normal pagination
*/ */
@Input() showPaginator = true; @Input() showPaginator: boolean;
/** /**
* Emit when one of the listed object has changed. * Emit when one of the listed object has changed.
*/ */
@Output() contentChange = new EventEmitter<any>(); @Output() contentChange: EventEmitter<any> = new EventEmitter();
/** /**
* If showPaginator is set to true, emit when the previous button is clicked * If showPaginator is set to true, emit when the previous button is clicked
*/ */
@Output() prev = new EventEmitter<boolean>(); @Output() prev: EventEmitter<boolean> = new EventEmitter();
/** /**
* If showPaginator is set to true, emit when the next button is clicked * If showPaginator is set to true, emit when the next button is clicked
*/ */
@Output() next = new EventEmitter<boolean>(); @Output() next: EventEmitter<boolean> = new EventEmitter();
/** @Input() objects: RemoteData<PaginatedList<ListableObject>>;
* The current listable objects
*/
private _objects: RemoteData<PaginatedList<ListableObject>>;
/**
* Setter for the objects
* @param objects The new objects
*/
@Input() set objects(objects: RemoteData<PaginatedList<ListableObject>>) {
this._objects = objects;
}
/**
* Getter to return the current objects
*/
get objects() {
return this._objects;
}
/** /**
* An event fired when the page is changed. * An event fired when the page is changed.
@@ -123,48 +102,45 @@ export class ThemedObjectListComponent extends ThemedComponent<ObjectListCompone
@Output() change: EventEmitter<{ @Output() change: EventEmitter<{
pagination: PaginationComponentOptions, pagination: PaginationComponentOptions,
sort: SortOptions sort: SortOptions
}> = new EventEmitter<{ }> = new EventEmitter();
pagination: PaginationComponentOptions,
sort: SortOptions
}>();
/** /**
* An event fired when the page is changed. * An event fired when the page is changed.
* Event's payload equals to the newly selected page. * Event's payload equals to the newly selected page.
*/ */
@Output() pageChange: EventEmitter<number> = new EventEmitter<number>(); @Output() pageChange: EventEmitter<number> = new EventEmitter();
/** /**
* An event fired when the page wsize is changed. * An event fired when the page wsize is changed.
* Event's payload equals to the newly selected page size. * Event's payload equals to the newly selected page size.
*/ */
@Output() pageSizeChange: EventEmitter<number> = new EventEmitter<number>(); @Output() pageSizeChange: EventEmitter<number> = new EventEmitter();
/** /**
* An event fired when the sort direction is changed. * An event fired when the sort direction is changed.
* Event's payload equals to the newly selected sort direction. * Event's payload equals to the newly selected sort direction.
*/ */
@Output() sortDirectionChange: EventEmitter<SortDirection> = new EventEmitter<SortDirection>(); @Output() sortDirectionChange: EventEmitter<SortDirection> = new EventEmitter();
/** /**
* An event fired when on of the pagination parameters changes * An event fired when on of the pagination parameters changes
*/ */
@Output() paginationChange: EventEmitter<any> = new EventEmitter<any>(); @Output() paginationChange: EventEmitter<any> = new EventEmitter();
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter();
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() selectObject: EventEmitter<ListableObject> = new EventEmitter();
/** /**
* Send an import event to the parent component * Send an import event to the parent component
*/ */
@Output() importObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() importObject: EventEmitter<ListableObject> = new EventEmitter();
/** /**
* An event fired when the sort field is changed. * An event fired when the sort field is changed.
* Event's payload equals to the newly selected sort field. * Event's payload equals to the newly selected sort field.
*/ */
@Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>(); @Output() sortFieldChange: EventEmitter<string> = new EventEmitter();
inAndOutputNames: (keyof ObjectListComponent & keyof this)[] = [ inAndOutputNames: (keyof ObjectListComponent & keyof this)[] = [
'config', 'config',

View File

@@ -29,7 +29,7 @@ export class ThemedSearchResultsComponent extends ThemedComponent<SearchResultsC
@Input() searchConfig: PaginatedSearchOptions; @Input() searchConfig: PaginatedSearchOptions;
@Input() showCsvExport = false; @Input() showCsvExport: boolean;
@Input() sortConfig: SortOptions; @Input() sortConfig: SortOptions;
@@ -37,21 +37,21 @@ export class ThemedSearchResultsComponent extends ThemedComponent<SearchResultsC
@Input() configuration: string; @Input() configuration: string;
@Input() disableHeader = false; @Input() disableHeader: boolean;
@Input() selectable = false; @Input() selectable: boolean;
@Input() context: Context; @Input() context: Context;
@Input() hidePaginationDetail = false; @Input() hidePaginationDetail: boolean;
@Input() selectionConfig: SelectionConfig = null; @Input() selectionConfig: SelectionConfig;
@Output() contentChange: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() contentChange: EventEmitter<ListableObject> = new EventEmitter();
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter();
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() selectObject: EventEmitter<ListableObject> = new EventEmitter();
protected getComponentName(): string { protected getComponentName(): string {
return 'SearchResultsComponent'; return 'SearchResultsComponent';

View File

@@ -136,7 +136,7 @@ export class SearchComponent implements OnInit {
/** /**
* List of available view mode * List of available view mode
*/ */
@Input() useUniquePageId: false; @Input() useUniquePageId: boolean;
/** /**
* List of available view mode * List of available view mode

View File

@@ -11,7 +11,7 @@ import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { ListableObject } from '../object-collection/shared/listable-object.model'; import { ListableObject } from '../object-collection/shared/listable-object.model';
/** /**
* Themed wrapper for SearchComponent * Themed wrapper for {@link SearchComponent}
*/ */
@Component({ @Component({
selector: 'ds-themed-search', selector: 'ds-themed-search',
@@ -21,53 +21,53 @@ import { ListableObject } from '../object-collection/shared/listable-object.mode
export class ThemedSearchComponent extends ThemedComponent<SearchComponent> { export class ThemedSearchComponent extends ThemedComponent<SearchComponent> {
protected inAndOutputNames: (keyof SearchComponent & keyof this)[] = ['configurationList', 'context', 'configuration', 'fixedFilterQuery', 'useCachedVersionIfAvailable', 'inPlaceSearch', 'linkType', 'paginationId', 'searchEnabled', 'sideBarWidth', 'searchFormPlaceholder', 'selectable', 'selectionConfig', 'showCsvExport', 'showSidebar', 'showViewModes', 'useUniquePageId', 'viewModeList', 'showScopeSelector', 'resultFound', 'deselectObject', 'selectObject', 'trackStatistics', 'query']; protected inAndOutputNames: (keyof SearchComponent & keyof this)[] = ['configurationList', 'context', 'configuration', 'fixedFilterQuery', 'useCachedVersionIfAvailable', 'inPlaceSearch', 'linkType', 'paginationId', 'searchEnabled', 'sideBarWidth', 'searchFormPlaceholder', 'selectable', 'selectionConfig', 'showCsvExport', 'showSidebar', 'showViewModes', 'useUniquePageId', 'viewModeList', 'showScopeSelector', 'resultFound', 'deselectObject', 'selectObject', 'trackStatistics', 'query'];
@Input() configurationList: SearchConfigurationOption[] = []; @Input() configurationList: SearchConfigurationOption[];
@Input() context: Context = Context.Search; @Input() context: Context;
@Input() configuration = 'default'; @Input() configuration: string;
@Input() fixedFilterQuery: string; @Input() fixedFilterQuery: string;
@Input() useCachedVersionIfAvailable = true; @Input() useCachedVersionIfAvailable: boolean;
@Input() inPlaceSearch = true; @Input() inPlaceSearch: boolean;
@Input() linkType: CollectionElementLinkType; @Input() linkType: CollectionElementLinkType;
@Input() paginationId = 'spc'; @Input() paginationId: string;
@Input() searchEnabled = true; @Input() searchEnabled: boolean;
@Input() sideBarWidth = 3; @Input() sideBarWidth: number;
@Input() searchFormPlaceholder = 'search.search-form.placeholder'; @Input() searchFormPlaceholder: string;
@Input() selectable = false; @Input() selectable: boolean;
@Input() selectionConfig: SelectionConfig; @Input() selectionConfig: SelectionConfig;
@Input() showCsvExport = false; @Input() showCsvExport: boolean;
@Input() showSidebar = true; @Input() showSidebar: boolean;
@Input() showViewModes = true; @Input() showViewModes: boolean;
@Input() useUniquePageId: false; @Input() useUniquePageId: boolean;
@Input() viewModeList: ViewMode[]; @Input() viewModeList: ViewMode[];
@Input() showScopeSelector = true; @Input() showScopeSelector: boolean;
@Input() trackStatistics = false; @Input() trackStatistics: boolean;
@Input() query: string; @Input() query: string;
@Output() resultFound: EventEmitter<SearchObjects<DSpaceObject>> = new EventEmitter<SearchObjects<DSpaceObject>>(); @Output() resultFound: EventEmitter<SearchObjects<DSpaceObject>> = new EventEmitter();
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter();
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>(); @Output() selectObject: EventEmitter<ListableObject> = new EventEmitter();
protected getComponentName(): string { protected getComponentName(): string {
return 'SearchComponent'; return 'SearchComponent';

View File

@@ -17,6 +17,7 @@ export class AuthServiceStub {
token: AuthTokenInfo = new AuthTokenInfo('token_test'); token: AuthTokenInfo = new AuthTokenInfo('token_test');
impersonating: string; impersonating: string;
private _tokenExpired = false; private _tokenExpired = false;
private _isExternalAuth = false;
private redirectUrl; private redirectUrl;
constructor() { constructor() {
@@ -122,6 +123,13 @@ export class AuthServiceStub {
checkAuthenticationCookie() { checkAuthenticationCookie() {
return; return;
} }
setExternalAuthStatus(externalCookie: boolean) {
this._isExternalAuth = externalCookie;
}
isExternalAuthentication(): Observable<boolean> {
return observableOf(this._isExternalAuth);
}
retrieveAuthMethodsFromAuthStatus(status: AuthStatus) { retrieveAuthMethodsFromAuthStatus(status: AuthStatus) {
return observableOf(authMethodsMock); return observableOf(authMethodsMock);

View File

@@ -26,16 +26,21 @@ import { AuthService } from '../../app/core/auth/auth.service';
import { ThemeService } from '../../app/shared/theme-support/theme.service'; import { ThemeService } from '../../app/shared/theme-support/theme.service';
import { StoreAction, StoreActionTypes } from '../../app/store.actions'; import { StoreAction, StoreActionTypes } from '../../app/store.actions';
import { coreSelector } from '../../app/core/core.selectors'; import { coreSelector } from '../../app/core/core.selectors';
import { find, map } from 'rxjs/operators'; import { filter, find, map } from 'rxjs/operators';
import { isNotEmpty } from '../../app/shared/empty.util'; import { isNotEmpty } from '../../app/shared/empty.util';
import { logStartupMessage } from '../../../startup-message'; import { logStartupMessage } from '../../../startup-message';
import { MenuService } from '../../app/shared/menu/menu.service'; import { MenuService } from '../../app/shared/menu/menu.service';
import { RootDataService } from '../../app/core/data/root-data.service';
import { firstValueFrom, Subscription } from 'rxjs';
/** /**
* Performs client-side initialization. * Performs client-side initialization.
*/ */
@Injectable() @Injectable()
export class BrowserInitService extends InitService { export class BrowserInitService extends InitService {
sub: Subscription;
constructor( constructor(
protected store: Store<AppState>, protected store: Store<AppState>,
protected correlationIdService: CorrelationIdService, protected correlationIdService: CorrelationIdService,
@@ -51,6 +56,7 @@ export class BrowserInitService extends InitService {
protected authService: AuthService, protected authService: AuthService,
protected themeService: ThemeService, protected themeService: ThemeService,
protected menuService: MenuService, protected menuService: MenuService,
private rootDataService: RootDataService
) { ) {
super( super(
store, store,
@@ -80,6 +86,7 @@ export class BrowserInitService extends InitService {
return async () => { return async () => {
await this.loadAppState(); await this.loadAppState();
this.checkAuthenticationToken(); this.checkAuthenticationToken();
this.externalAuthCheck();
this.initCorrelationId(); this.initCorrelationId();
this.checkEnvironment(); this.checkEnvironment();
@@ -134,4 +141,35 @@ export class BrowserInitService extends InitService {
protected initGoogleAnalytics() { protected initGoogleAnalytics() {
this.googleAnalyticsService.addTrackingIdToPage(); this.googleAnalyticsService.addTrackingIdToPage();
} }
/**
* During an external authentication flow invalidate the SSR transferState
* data in the cache. This allows the app to fetch fresh content.
* @private
*/
private externalAuthCheck() {
this.sub = this.authService.isExternalAuthentication().pipe(
filter((externalAuth: boolean) => externalAuth)
).subscribe(() => {
// Clear the transferState data.
this.rootDataService.invalidateRootCache();
this.authService.setExternalAuthStatus(false);
}
);
this.closeAuthCheckSubscription();
}
/**
* Unsubscribe the external authentication subscription
* when authentication is no longer blocking.
* @private
*/
private closeAuthCheckSubscription() {
firstValueFrom(this.authenticationReady$()).then(() => {
this.sub.unsubscribe();
});
}
} }