mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-18 15:33:04 +00:00
Request-a-copy: Use wrapped ItemWithSupp.. and base item comps, except download link
This commit is contained in:
@@ -17,6 +17,15 @@ import {
|
|||||||
} from '../shared/operators';
|
} from '../shared/operators';
|
||||||
import { AuthService } from './auth.service';
|
import { AuthService } from './auth.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve an ItemRequest based on the accessToken in the query params
|
||||||
|
* Used in item-page-routes.ts to resolve the item request for all Item page components
|
||||||
|
* @param route
|
||||||
|
* @param state
|
||||||
|
* @param router
|
||||||
|
* @param authService
|
||||||
|
* @param itemRequestDataService
|
||||||
|
*/
|
||||||
export const accessTokenResolver: ResolveFn<ItemRequest> = (
|
export const accessTokenResolver: ResolveFn<ItemRequest> = (
|
||||||
route,
|
route,
|
||||||
state,
|
state,
|
||||||
@@ -25,21 +34,23 @@ export const accessTokenResolver: ResolveFn<ItemRequest> = (
|
|||||||
itemRequestDataService: ItemRequestDataService = inject(ItemRequestDataService),
|
itemRequestDataService: ItemRequestDataService = inject(ItemRequestDataService),
|
||||||
): Observable<ItemRequest> => {
|
): Observable<ItemRequest> => {
|
||||||
const accessToken = route.queryParams.accessToken;
|
const accessToken = route.queryParams.accessToken;
|
||||||
|
// Set null object if accesstoken is empty
|
||||||
if ( !hasValue(accessToken) ) {
|
if ( !hasValue(accessToken) ) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
// Get
|
// Get the item request from the server
|
||||||
return itemRequestDataService.getSanitizedRequestByAccessToken(accessToken).pipe(
|
return itemRequestDataService.getSanitizedRequestByAccessToken(accessToken).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
|
// Handle authorization errors, not found errors and forbidden errors as normal
|
||||||
redirectOn4xx(router, authService),
|
redirectOn4xx(router, authService),
|
||||||
|
// Get payload of the item request
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
tap(request => {
|
tap(request => {
|
||||||
if (!hasValue(request)) {
|
if (!hasValue(request)) {
|
||||||
console.dir('no request found for access token', accessToken);
|
// If the request is not found, redirect to 403 Forbidden
|
||||||
router.navigateByUrl(getForbiddenRoute());
|
router.navigateByUrl(getForbiddenRoute());
|
||||||
}
|
}
|
||||||
console.dir('found request: ', request);
|
// Return the resolved item request object
|
||||||
console.dir(request);
|
|
||||||
return request;
|
return request;
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
|
20
src/app/core/shared/item-with-supplementary-data.model.ts
Normal file
20
src/app/core/shared/item-with-supplementary-data.model.ts
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
import { Item } from './item.model';
|
||||||
|
import { ItemRequest } from './item-request.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This model represents an item with supplementary data, e.g. an ItemRequest object
|
||||||
|
* to help components determine how the Item or its data/bitstream should be delivered
|
||||||
|
* and presented to the users, but not part of the actual database model.
|
||||||
|
*/
|
||||||
|
export class ItemWithSupplementaryData extends Item {
|
||||||
|
/**
|
||||||
|
* An item request. This is used to determine how the item should be delivered.
|
||||||
|
* A valid accessToken is resolved to this object in the accessTokenResolver
|
||||||
|
*/
|
||||||
|
itemRequest: ItemRequest;
|
||||||
|
|
||||||
|
constructor(itemRequest: ItemRequest) {
|
||||||
|
super();
|
||||||
|
this.itemRequest = itemRequest;
|
||||||
|
}
|
||||||
|
}
|
@@ -23,4 +23,9 @@ export class MediaViewerItem {
|
|||||||
* Incoming Bitsream thumbnail
|
* Incoming Bitsream thumbnail
|
||||||
*/
|
*/
|
||||||
thumbnail: string;
|
thumbnail: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Access token, if accessed via a Request-a-Copy link
|
||||||
|
*/
|
||||||
|
accessToken: string;
|
||||||
}
|
}
|
||||||
|
@@ -1,4 +1,8 @@
|
|||||||
<a [routerLink]="(bitstreamPath$| async)?.routerLink" class="dont-break-out" [queryParams]="(bitstreamPath$| async)?.queryParams" [target]="isBlank ? '_blank': '_self'" [ngClass]="cssClasses">
|
<a [routerLink]="(bitstreamPath$| async)?.routerLink" class="dont-break-out"
|
||||||
|
[queryParams]="(bitstreamPath$| async)?.queryParams"
|
||||||
|
[target]="isBlank ? '_blank': '_self'"
|
||||||
|
[ngClass]="cssClasses"
|
||||||
|
[attr.aria-label]="('file-download-link.download' | translate) + dsoNameService.getName(bitstream)">
|
||||||
<!-- If the user cannot download the file by auth or token, show a lock icon -->
|
<!-- If the user cannot download the file by auth or token, show a lock icon -->
|
||||||
<span role="img" *ngIf="(canDownload$ | async) === false && (canDownloadWithToken$ | async) === false" [attr.aria-label]="'file-download-link.restricted' | translate" class="pr-1"><i class="fas fa-lock"></i></span>
|
<span role="img" *ngIf="(canDownload$ | async) === false && (canDownloadWithToken$ | async) === false" [attr.aria-label]="'file-download-link.restricted' | translate" class="pr-1"><i class="fas fa-lock"></i></span>
|
||||||
<!-- If the user can download the file by token, and NOT normally show a lock open icon -->
|
<!-- If the user can download the file by token, and NOT normally show a lock open icon -->
|
||||||
|
@@ -23,11 +23,13 @@ import {
|
|||||||
getBitstreamDownloadWithAccessTokenRoute,
|
getBitstreamDownloadWithAccessTokenRoute,
|
||||||
getBitstreamRequestACopyRoute,
|
getBitstreamRequestACopyRoute,
|
||||||
} from '../../../../app-routing-paths';
|
} from '../../../../app-routing-paths';
|
||||||
|
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
||||||
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../../../core/data/feature-authorization/feature-id';
|
||||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
import { ItemRequest } from '../../../../core/shared/item-request.model';
|
import { ItemRequest } from '../../../../core/shared/item-request.model';
|
||||||
|
import { ItemWithSupplementaryData } from '../../../../core/shared/item-with-supplementary-data.model';
|
||||||
import {
|
import {
|
||||||
hasValue,
|
hasValue,
|
||||||
isNotEmpty,
|
isNotEmpty,
|
||||||
@@ -54,6 +56,9 @@ export class ItemSecureFileDownloadLinkComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() bitstream: Bitstream;
|
@Input() bitstream: Bitstream;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Item that owns the linked bitstream
|
||||||
|
*/
|
||||||
@Input() item: Item;
|
@Input() item: Item;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -66,15 +71,14 @@ export class ItemSecureFileDownloadLinkComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() isBlank = false;
|
@Input() isBlank = false;
|
||||||
|
|
||||||
@Input() itemRequest: ItemRequest;
|
|
||||||
|
|
||||||
@Input() enableRequestACopy = true;
|
@Input() enableRequestACopy = true;
|
||||||
|
|
||||||
bitstreamPath$: Observable<{
|
bitstreamPath$: Observable<{
|
||||||
routerLink: string,
|
routerLink: string,
|
||||||
queryParams: any,
|
queryParams: any,
|
||||||
}>;
|
}>;
|
||||||
|
// ItemRequest object with access token, expiry, etc.
|
||||||
|
itemRequest: ItemRequest;
|
||||||
// authorized to download normally
|
// authorized to download normally
|
||||||
canDownload$: Observable<boolean>;
|
canDownload$: Observable<boolean>;
|
||||||
// authorized to download with token
|
// authorized to download with token
|
||||||
@@ -84,6 +88,7 @@ export class ItemSecureFileDownloadLinkComponent implements OnInit {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
private authorizationService: AuthorizationDataService,
|
private authorizationService: AuthorizationDataService,
|
||||||
|
protected dsoNameService: DSONameService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -92,6 +97,9 @@ export class ItemSecureFileDownloadLinkComponent implements OnInit {
|
|||||||
* (for a given bitstream), and ability to request a copy of a bitstream.
|
* (for a given bitstream), and ability to request a copy of a bitstream.
|
||||||
*/
|
*/
|
||||||
ngOnInit() {
|
ngOnInit() {
|
||||||
|
if (this.item instanceof ItemWithSupplementaryData) {
|
||||||
|
this.itemRequest = this.item.itemRequest;
|
||||||
|
}
|
||||||
this.canDownload$ = this.authorizationService.isAuthorized(FeatureID.CanDownload, isNotEmpty(this.bitstream) ? this.bitstream.self : undefined);
|
this.canDownload$ = this.authorizationService.isAuthorized(FeatureID.CanDownload, isNotEmpty(this.bitstream) ? this.bitstream.self : undefined);
|
||||||
this.canDownloadWithToken$ = observableOf(this.itemRequest ? (this.itemRequest.allfiles !== false || this.itemRequest.bitstreamId === this.bitstream.uuid) : false);
|
this.canDownloadWithToken$ = observableOf(this.itemRequest ? (this.itemRequest.allfiles !== false || this.itemRequest.bitstreamId === this.bitstream.uuid) : false);
|
||||||
this.canRequestACopy$ = this.authorizationService.isAuthorized(FeatureID.CanRequestACopy, isNotEmpty(this.bitstream) ? this.bitstream.self : undefined);
|
this.canRequestACopy$ = this.authorizationService.isAuthorized(FeatureID.CanRequestACopy, isNotEmpty(this.bitstream) ? this.bitstream.self : undefined);
|
||||||
|
@@ -1,87 +0,0 @@
|
|||||||
<ds-metadata-field-wrapper [label]="label | translate">
|
|
||||||
<div *ngVar="(originals$ | async)?.payload as originals">
|
|
||||||
<div *ngIf="hasValuesInBundle(originals)">
|
|
||||||
<h3 class="h5 simple-view-element-header">{{"item.page.filesection.original.bundle" | translate}}</h3>
|
|
||||||
<ds-pagination *ngIf="originals?.page?.length > 0"
|
|
||||||
[hideGear]="true"
|
|
||||||
[hidePagerWhenSinglePage]="true"
|
|
||||||
[paginationOptions]="originalOptions"
|
|
||||||
[collectionSize]="originals?.totalElements"
|
|
||||||
[retainScrollPosition]="true">
|
|
||||||
|
|
||||||
|
|
||||||
<div class="file-section row mb-3" *ngFor="let file of originals?.page;">
|
|
||||||
<div class="col-3">
|
|
||||||
<ds-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-thumbnail>
|
|
||||||
</div>
|
|
||||||
<div class="col-7">
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-md-4">{{"item.page.filesection.name" | translate}}</dt>
|
|
||||||
<dd class="col-md-8">{{ dsoNameService.getName(file) }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-md-4">{{"item.page.filesection.size" | translate}}</dt>
|
|
||||||
<dd class="col-md-8">{{(file.sizeBytes) | dsFileSize }}</dd>
|
|
||||||
|
|
||||||
|
|
||||||
<dt class="col-md-4">{{"item.page.filesection.format" | translate}}</dt>
|
|
||||||
<dd class="col-md-8">{{(file.format | async)?.payload?.description}}</dd>
|
|
||||||
|
|
||||||
<ng-container *ngIf="file.hasMetadata('dc.description')">
|
|
||||||
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
|
|
||||||
<dd class="col-md-8">{{file.firstMetadataValue("dc.description")}}</dd>
|
|
||||||
</ng-container>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
<div class="col-2">
|
|
||||||
<!-- <ds-themed-file-download-link [bitstream]="file" [item]="item">-->
|
|
||||||
<!-- {{"item.page.filesection.download" | translate}}-->
|
|
||||||
<!-- </ds-themed-file-download-link>-->
|
|
||||||
<ds-item-secure-file-download-link [bitstream]="file" [item]="item" [itemRequest]="itemRequest">
|
|
||||||
{{"item.page.filesection.download" | translate}}
|
|
||||||
</ds-item-secure-file-download-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ds-pagination>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div *ngVar="(licenses$ | async)?.payload as licenses">
|
|
||||||
<div *ngIf="hasValuesInBundle(licenses)">
|
|
||||||
<h3 class="h5 simple-view-element-header">{{"item.page.filesection.license.bundle" | translate}}</h3>
|
|
||||||
<ds-pagination *ngIf="licenses?.page?.length > 0"
|
|
||||||
[hideGear]="true"
|
|
||||||
[hidePagerWhenSinglePage]="true"
|
|
||||||
[paginationOptions]="licenseOptions"
|
|
||||||
[collectionSize]="licenses?.totalElements"
|
|
||||||
[retainScrollPosition]="true">
|
|
||||||
|
|
||||||
|
|
||||||
<div class="file-section row" *ngFor="let file of licenses?.page;">
|
|
||||||
<div class="col-3">
|
|
||||||
<ds-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-thumbnail>
|
|
||||||
</div>
|
|
||||||
<div class="col-7">
|
|
||||||
<dl class="row">
|
|
||||||
<dt class="col-md-4">{{"item.page.filesection.name" | translate}}</dt>
|
|
||||||
<dd class="col-md-8">{{ dsoNameService.getName(file) }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-md-4">{{"item.page.filesection.size" | translate}}</dt>
|
|
||||||
<dd class="col-md-8">{{(file.sizeBytes) | dsFileSize }}</dd>
|
|
||||||
|
|
||||||
<dt class="col-md-4">{{"item.page.filesection.format" | translate}}</dt>
|
|
||||||
<dd class="col-md-8">{{(file.format | async)?.payload?.description}}</dd>
|
|
||||||
|
|
||||||
|
|
||||||
<dt class="col-md-4">{{"item.page.filesection.description" | translate}}</dt>
|
|
||||||
<dd class="col-md-8">{{file.firstMetadataValue("dc.description")}}</dd>
|
|
||||||
</dl>
|
|
||||||
</div>
|
|
||||||
<div class="col-2">
|
|
||||||
<ds-file-download-link [bitstream]="file" [item]="item">
|
|
||||||
{{"item.page.filesection.download" | translate}}
|
|
||||||
</ds-file-download-link>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ds-pagination>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ds-metadata-field-wrapper>
|
|
@@ -1,5 +0,0 @@
|
|||||||
@media screen and (min-width: map-get($grid-breakpoints, md)) {
|
|
||||||
dt {
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,96 +0,0 @@
|
|||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
|
||||||
import {
|
|
||||||
ComponentFixture,
|
|
||||||
TestBed,
|
|
||||||
waitForAsync,
|
|
||||||
} from '@angular/core/testing';
|
|
||||||
import { By } from '@angular/platform-browser';
|
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
|
||||||
import {
|
|
||||||
TranslateLoader,
|
|
||||||
TranslateModule,
|
|
||||||
} from '@ngx-translate/core';
|
|
||||||
import { of as observableOf } from 'rxjs';
|
|
||||||
import { APP_CONFIG } from 'src/config/app-config.interface';
|
|
||||||
import { environment } from 'src/environments/environment';
|
|
||||||
|
|
||||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
|
||||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
|
||||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
|
||||||
import { ThemedFileDownloadLinkComponent } from '../../../../shared/file-download-link/themed-file-download-link.component';
|
|
||||||
import { MetadataFieldWrapperComponent } from '../../../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
|
|
||||||
import { MockBitstreamFormat1 } from '../../../../shared/mocks/item.mock';
|
|
||||||
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
|
||||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
|
||||||
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
|
||||||
import { NotificationsServiceStub } from '../../../../shared/testing/notifications-service.stub';
|
|
||||||
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
|
||||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
|
||||||
import { FileSizePipe } from '../../../../shared/utils/file-size-pipe';
|
|
||||||
import { VarDirective } from '../../../../shared/utils/var.directive';
|
|
||||||
import { ThemedThumbnailComponent } from '../../../../thumbnail/themed-thumbnail.component';
|
|
||||||
import { ItemSecureFileDownloadLinkComponent } from '../file-download-link/item-secure-file-download-link.component';
|
|
||||||
import { ItemSecureFileSectionComponent } from './item-secure-file-section.component';
|
|
||||||
|
|
||||||
describe('FullFileSectionComponent', () => {
|
|
||||||
let comp: ItemSecureFileSectionComponent;
|
|
||||||
let fixture: ComponentFixture<ItemSecureFileSectionComponent>;
|
|
||||||
|
|
||||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
|
|
||||||
sizeBytes: 10201,
|
|
||||||
content: 'test-content-url',
|
|
||||||
format: observableOf(MockBitstreamFormat1),
|
|
||||||
bundleName: 'ORIGINAL',
|
|
||||||
id: 'test-id',
|
|
||||||
_links: {
|
|
||||||
self: { href: 'test-href' },
|
|
||||||
content: { href: 'test-content-href' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
|
||||||
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(createPaginatedList([mockBitstream, mockBitstream, mockBitstream])),
|
|
||||||
});
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
TranslateModule.forRoot({
|
|
||||||
loader: {
|
|
||||||
provide: TranslateLoader,
|
|
||||||
useClass: TranslateLoaderMock,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
BrowserAnimationsModule,
|
|
||||||
ItemSecureFileSectionComponent,
|
|
||||||
VarDirective,
|
|
||||||
FileSizePipe,
|
|
||||||
MetadataFieldWrapperComponent,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
provideMockStore(),
|
|
||||||
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
|
||||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
|
||||||
{ provide: PaginationService, useValue: new PaginationServiceStub() },
|
|
||||||
{ provide: APP_CONFIG, useValue: environment },
|
|
||||||
],
|
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
|
||||||
}).overrideComponent(ItemSecureFileSectionComponent, {
|
|
||||||
remove: { imports: [PaginationComponent, MetadataFieldWrapperComponent,ItemSecureFileDownloadLinkComponent, ThemedThumbnailComponent, ThemedFileDownloadLinkComponent] },
|
|
||||||
}).compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(ItemSecureFileSectionComponent);
|
|
||||||
comp = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the full file section gets loaded with bitstreams available', () => {
|
|
||||||
it('should contain a list with bitstreams', () => {
|
|
||||||
const fileSection = fixture.debugElement.queryAll(By.css('.file-section'));
|
|
||||||
expect(fileSection.length).toEqual(6);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,156 +0,0 @@
|
|||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
Inject,
|
|
||||||
Input,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
TranslateModule,
|
|
||||||
TranslateService,
|
|
||||||
} from '@ngx-translate/core';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import {
|
|
||||||
switchMap,
|
|
||||||
tap,
|
|
||||||
} from 'rxjs/operators';
|
|
||||||
import {
|
|
||||||
APP_CONFIG,
|
|
||||||
AppConfig,
|
|
||||||
} from 'src/config/app-config.interface';
|
|
||||||
|
|
||||||
import { DSONameService } from '../../../../core/breadcrumbs/dso-name.service';
|
|
||||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
|
||||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
|
||||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
|
||||||
import { ItemRequest } from '../../../../core/shared/item-request.model';
|
|
||||||
import {
|
|
||||||
hasValue,
|
|
||||||
isEmpty,
|
|
||||||
} from '../../../../shared/empty.util';
|
|
||||||
import { ThemedFileDownloadLinkComponent } from '../../../../shared/file-download-link/themed-file-download-link.component';
|
|
||||||
import { ThemedLoadingComponent } from '../../../../shared/loading/themed-loading.component';
|
|
||||||
import { MetadataFieldWrapperComponent } from '../../../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
|
|
||||||
import { NotificationsService } from '../../../../shared/notifications/notifications.service';
|
|
||||||
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
|
||||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
|
||||||
import { FileSizePipe } from '../../../../shared/utils/file-size-pipe';
|
|
||||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
|
||||||
import { VarDirective } from '../../../../shared/utils/var.directive';
|
|
||||||
import { ThemedThumbnailComponent } from '../../../../thumbnail/themed-thumbnail.component';
|
|
||||||
import { FileSectionComponent } from '../../../simple/field-components/file-section/file-section.component';
|
|
||||||
import { ItemSecureFileDownloadLinkComponent } from '../file-download-link/item-secure-file-download-link.component';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This component renders the file section of the item
|
|
||||||
* inside a 'ds-metadata-field-wrapper' component.
|
|
||||||
*/
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-item-secure-full-file-section',
|
|
||||||
styleUrls: ['./item-secure-file-section.component.scss'],
|
|
||||||
templateUrl: './item-secure-file-section.component.html',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
ItemSecureFileDownloadLinkComponent,
|
|
||||||
CommonModule,
|
|
||||||
ThemedFileDownloadLinkComponent,
|
|
||||||
MetadataFieldWrapperComponent,
|
|
||||||
ThemedLoadingComponent,
|
|
||||||
TranslateModule,
|
|
||||||
FileSizePipe,
|
|
||||||
VarDirective,
|
|
||||||
PaginationComponent,
|
|
||||||
ThemedThumbnailComponent,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class ItemSecureFileSectionComponent extends FileSectionComponent implements OnDestroy, OnInit {
|
|
||||||
|
|
||||||
@Input() item: Item;
|
|
||||||
@Input() itemRequest: ItemRequest;
|
|
||||||
|
|
||||||
label: string;
|
|
||||||
|
|
||||||
originals$: Observable<RemoteData<PaginatedList<Bitstream>>>;
|
|
||||||
licenses$: Observable<RemoteData<PaginatedList<Bitstream>>>;
|
|
||||||
|
|
||||||
originalOptions = Object.assign(new PaginationComponentOptions(), {
|
|
||||||
id: 'obo',
|
|
||||||
currentPage: 1,
|
|
||||||
pageSize: this.appConfig.item.bitstream.pageSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
licenseOptions = Object.assign(new PaginationComponentOptions(), {
|
|
||||||
id: 'lbo',
|
|
||||||
currentPage: 1,
|
|
||||||
pageSize: this.appConfig.item.bitstream.pageSize,
|
|
||||||
});
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
bitstreamDataService: BitstreamDataService,
|
|
||||||
protected notificationsService: NotificationsService,
|
|
||||||
protected translateService: TranslateService,
|
|
||||||
protected paginationService: PaginationService,
|
|
||||||
public dsoNameService: DSONameService,
|
|
||||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
|
||||||
) {
|
|
||||||
super(bitstreamDataService, notificationsService, translateService, dsoNameService, appConfig);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.initialize();
|
|
||||||
}
|
|
||||||
|
|
||||||
initialize(): void {
|
|
||||||
this.originals$ = this.paginationService.getCurrentPagination(this.originalOptions.id, this.originalOptions).pipe(
|
|
||||||
switchMap((options: PaginationComponentOptions) => this.bitstreamDataService.findAllByItemAndBundleName(
|
|
||||||
this.item,
|
|
||||||
'ORIGINAL',
|
|
||||||
{ elementsPerPage: options.pageSize, currentPage: options.currentPage },
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
followLink('format'),
|
|
||||||
followLink('thumbnail'),
|
|
||||||
)),
|
|
||||||
tap((rd: RemoteData<PaginatedList<Bitstream>>) => {
|
|
||||||
if (hasValue(rd.errorMessage)) {
|
|
||||||
this.notificationsService.error(this.translateService.get('file-section.error.header'), `${rd.statusCode} ${rd.errorMessage}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.licenses$ = this.paginationService.getCurrentPagination(this.licenseOptions.id, this.licenseOptions).pipe(
|
|
||||||
switchMap((options: PaginationComponentOptions) => this.bitstreamDataService.findAllByItemAndBundleName(
|
|
||||||
this.item,
|
|
||||||
'LICENSE',
|
|
||||||
{ elementsPerPage: options.pageSize, currentPage: options.currentPage },
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
followLink('format'),
|
|
||||||
followLink('thumbnail'),
|
|
||||||
)),
|
|
||||||
tap((rd: RemoteData<PaginatedList<Bitstream>>) => {
|
|
||||||
if (hasValue(rd.errorMessage)) {
|
|
||||||
this.notificationsService.error(this.translateService.get('file-section.error.header'), `${rd.statusCode} ${rd.errorMessage}`);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
hasValuesInBundle(bundle: PaginatedList<Bitstream>) {
|
|
||||||
return hasValue(bundle) && !isEmpty(bundle.page);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.paginationService.clearPagination(this.originalOptions.id);
|
|
||||||
this.paginationService.clearPagination(this.licenseOptions.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -1,35 +0,0 @@
|
|||||||
<ng-container *ngVar="mediaList$ | async as mediaList">
|
|
||||||
<ds-loading
|
|
||||||
*ngIf="isLoading"
|
|
||||||
message="{{ 'loading.default' | translate }}"
|
|
||||||
[showMessage]="false"
|
|
||||||
></ds-loading>
|
|
||||||
<div class="media-viewer" *ngIf="!isLoading">
|
|
||||||
<ng-container *ngIf="mediaList.length > 0; else showThumbnail">
|
|
||||||
<ng-container *ngVar="mediaOptions.video && ['audio', 'video'].includes(mediaList[0]?.format) as showVideo">
|
|
||||||
<ng-container *ngVar="mediaOptions.image && mediaList[0]?.format === 'image' as showImage">
|
|
||||||
<ds-secure-media-viewer-video *ngIf="showVideo"
|
|
||||||
[medias]="mediaList"
|
|
||||||
[captions]="captions$ | async"
|
|
||||||
[accessToken]="accessToken"
|
|
||||||
></ds-secure-media-viewer-video>
|
|
||||||
<ds-secure-media-viewer-image *ngIf="showImage"
|
|
||||||
[images]="mediaList"
|
|
||||||
[accessToken]="accessToken"
|
|
||||||
></ds-secure-media-viewer-image>
|
|
||||||
<ng-container *ngIf="showImage || showVideo; else showThumbnail"></ng-container>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
<ng-template #showThumbnail>
|
|
||||||
<ds-secure-media-viewer-image *ngIf="mediaOptions.image && mediaOptions.video"
|
|
||||||
[image]="(thumbnailsRD$ | async)?.payload?.page[0]?._links.content.href || thumbnailPlaceholder"
|
|
||||||
[accessToken]="accessToken"
|
|
||||||
[preview]="false"
|
|
||||||
></ds-secure-media-viewer-image>
|
|
||||||
<ds-thumbnail *ngIf="!(mediaOptions.image && mediaOptions.video)"
|
|
||||||
[thumbnail]="(thumbnailsRD$ | async)?.payload?.page[0]">
|
|
||||||
</ds-thumbnail>
|
|
||||||
</ng-template>
|
|
||||||
</ng-container>
|
|
@@ -1 +0,0 @@
|
|||||||
|
|
@@ -1,160 +0,0 @@
|
|||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
|
||||||
import {
|
|
||||||
ComponentFixture,
|
|
||||||
TestBed,
|
|
||||||
waitForAsync,
|
|
||||||
} from '@angular/core/testing';
|
|
||||||
import { By } from '@angular/platform-browser';
|
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
|
||||||
import {
|
|
||||||
TranslateLoader,
|
|
||||||
TranslateModule,
|
|
||||||
} from '@ngx-translate/core';
|
|
||||||
import { of as observableOf } from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../../../core/auth/auth.service';
|
|
||||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
|
||||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
|
||||||
import { MediaViewerItem } from '../../../../core/shared/media-viewer-item.model';
|
|
||||||
import { MetadataFieldWrapperComponent } from '../../../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
|
|
||||||
import { AuthServiceMock } from '../../../../shared/mocks/auth.service.mock';
|
|
||||||
import { MockBitstreamFormat1 } from '../../../../shared/mocks/item.mock';
|
|
||||||
import { getMockThemeService } from '../../../../shared/mocks/theme-service.mock';
|
|
||||||
import { TranslateLoaderMock } from '../../../../shared/mocks/translate-loader.mock';
|
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
|
||||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
|
||||||
import { ThemeService } from '../../../../shared/theme-support/theme.service';
|
|
||||||
import { FileSizePipe } from '../../../../shared/utils/file-size-pipe';
|
|
||||||
import { VarDirective } from '../../../../shared/utils/var.directive';
|
|
||||||
import { ItemSecureMediaViewerComponent } from './item-secure-media-viewer.component';
|
|
||||||
|
|
||||||
describe('ItemSecureMediaViewerComponent', () => {
|
|
||||||
let comp: ItemSecureMediaViewerComponent;
|
|
||||||
let fixture: ComponentFixture<ItemSecureMediaViewerComponent>;
|
|
||||||
|
|
||||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
|
|
||||||
sizeBytes: 10201,
|
|
||||||
content:
|
|
||||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
|
||||||
format: observableOf(MockBitstreamFormat1),
|
|
||||||
bundleName: 'ORIGINAL',
|
|
||||||
_links: {
|
|
||||||
self: {
|
|
||||||
href:
|
|
||||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
href:
|
|
||||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
|
||||||
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
|
||||||
type: 'bitstream',
|
|
||||||
metadata: {
|
|
||||||
'dc.title': [
|
|
||||||
{
|
|
||||||
language: null,
|
|
||||||
value: 'test_word.docx',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const bitstreamDataService = jasmine.createSpyObj('bitstreamDataService', {
|
|
||||||
findAllByItemAndBundleName: createSuccessfulRemoteDataObject$(
|
|
||||||
createPaginatedList([mockBitstream]),
|
|
||||||
),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockMediaViewerItem: MediaViewerItem = Object.assign(
|
|
||||||
new MediaViewerItem(),
|
|
||||||
{ bitstream: mockBitstream, format: 'image', thumbnail: null },
|
|
||||||
);
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
return TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
TranslateModule.forRoot({
|
|
||||||
loader: {
|
|
||||||
provide: TranslateLoader,
|
|
||||||
useClass: TranslateLoaderMock,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
BrowserAnimationsModule,
|
|
||||||
ItemSecureMediaViewerComponent,
|
|
||||||
VarDirective,
|
|
||||||
FileSizePipe,
|
|
||||||
MetadataFieldWrapperComponent,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
|
||||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
|
||||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
|
||||||
],
|
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
|
||||||
}).compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(ItemSecureMediaViewerComponent);
|
|
||||||
comp = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the bitstreams are loading', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
comp.mediaList$.next([mockMediaViewerItem]);
|
|
||||||
comp.mediaOptions = {
|
|
||||||
image: true,
|
|
||||||
video: true,
|
|
||||||
};
|
|
||||||
comp.isLoading = true;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call the createMediaViewerItem', () => {
|
|
||||||
const mediaItem = comp.createMediaViewerItem(
|
|
||||||
mockBitstream,
|
|
||||||
MockBitstreamFormat1,
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
expect(mediaItem).toBeTruthy();
|
|
||||||
expect(mediaItem.thumbnail).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display a loading component', () => {
|
|
||||||
const loading = fixture.debugElement.query(By.css('ds-loading'));
|
|
||||||
expect(loading.nativeElement).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the bitstreams loading is failed', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
comp.mediaList$.next([]);
|
|
||||||
comp.mediaOptions = {
|
|
||||||
image: true,
|
|
||||||
video: true,
|
|
||||||
};
|
|
||||||
comp.isLoading = false;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should call the createMediaViewerItem', () => {
|
|
||||||
const mediaItem = comp.createMediaViewerItem(
|
|
||||||
mockBitstream,
|
|
||||||
MockBitstreamFormat1,
|
|
||||||
undefined,
|
|
||||||
);
|
|
||||||
expect(mediaItem).toBeTruthy();
|
|
||||||
expect(mediaItem.thumbnail).toBe(null);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should display a default, thumbnail', () => {
|
|
||||||
const defaultThumbnail = fixture.debugElement.query(
|
|
||||||
By.css('ds-secure-media-viewer-image'),
|
|
||||||
);
|
|
||||||
expect(defaultThumbnail.nativeElement).toBeDefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,171 +0,0 @@
|
|||||||
import {
|
|
||||||
AsyncPipe,
|
|
||||||
NgIf,
|
|
||||||
} from '@angular/common';
|
|
||||||
import {
|
|
||||||
ChangeDetectorRef,
|
|
||||||
Component,
|
|
||||||
Input,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
|
||||||
} from '@angular/core';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
|
||||||
import {
|
|
||||||
BehaviorSubject,
|
|
||||||
Observable,
|
|
||||||
Subscription,
|
|
||||||
} from 'rxjs';
|
|
||||||
import {
|
|
||||||
filter,
|
|
||||||
take,
|
|
||||||
} from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { MediaViewerConfig } from '../../../../../config/media-viewer-config.interface';
|
|
||||||
import { environment } from '../../../../../environments/environment';
|
|
||||||
import { BitstreamDataService } from '../../../../core/data/bitstream-data.service';
|
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
|
||||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
|
||||||
import { BitstreamFormat } from '../../../../core/shared/bitstream-format.model';
|
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
|
||||||
import { MediaViewerItem } from '../../../../core/shared/media-viewer-item.model';
|
|
||||||
import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators';
|
|
||||||
import { hasValue } from '../../../../shared/empty.util';
|
|
||||||
import { ThemedLoadingComponent } from '../../../../shared/loading/themed-loading.component';
|
|
||||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
|
||||||
import { VarDirective } from '../../../../shared/utils/var.directive';
|
|
||||||
import { ThemedThumbnailComponent } from '../../../../thumbnail/themed-thumbnail.component';
|
|
||||||
import { SecureMediaViewerImageComponent } from './media-viewer-image/secure-media-viewer-image.component';
|
|
||||||
import { SecureMediaViewerVideoComponent } from './media-viewer-video/secure-media-viewer-video.component';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This component renders the media viewers
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-item-secure-media-viewer',
|
|
||||||
templateUrl: './item-secure-media-viewer.component.html',
|
|
||||||
styleUrls: ['./item-secure-media-viewer.component.scss'],
|
|
||||||
imports: [
|
|
||||||
AsyncPipe,
|
|
||||||
NgIf,
|
|
||||||
TranslateModule,
|
|
||||||
ThemedLoadingComponent,
|
|
||||||
VarDirective,
|
|
||||||
SecureMediaViewerVideoComponent,
|
|
||||||
SecureMediaViewerImageComponent,
|
|
||||||
ThemedThumbnailComponent,
|
|
||||||
],
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
export class ItemSecureMediaViewerComponent implements OnDestroy, OnInit {
|
|
||||||
@Input() item: Item;
|
|
||||||
|
|
||||||
@Input() mediaOptions: MediaViewerConfig = environment.mediaViewer;
|
|
||||||
|
|
||||||
@Input() accessToken: string;
|
|
||||||
|
|
||||||
mediaList$: BehaviorSubject<MediaViewerItem[]> = new BehaviorSubject([]);
|
|
||||||
|
|
||||||
captions$: BehaviorSubject<Bitstream[]> = new BehaviorSubject([]);
|
|
||||||
|
|
||||||
isLoading = true;
|
|
||||||
|
|
||||||
thumbnailPlaceholder = './assets/images/replacement_document.svg';
|
|
||||||
|
|
||||||
thumbnailsRD$: Observable<RemoteData<PaginatedList<Bitstream>>>;
|
|
||||||
|
|
||||||
subs: Subscription[] = [];
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected bitstreamDataService: BitstreamDataService,
|
|
||||||
protected changeDetectorRef: ChangeDetectorRef,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
|
||||||
this.subs.forEach((subscription: Subscription) => subscription.unsubscribe());
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method loads all the Bitstreams and Thumbnails and converts it to {@link MediaViewerItem}s
|
|
||||||
*/
|
|
||||||
ngOnInit(): void {
|
|
||||||
const types: string[] = [
|
|
||||||
...(this.mediaOptions.image ? ['image'] : []),
|
|
||||||
...(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) {
|
|
||||||
this.isLoading = false;
|
|
||||||
this.mediaList$.next([]);
|
|
||||||
} else {
|
|
||||||
this.subs.push(this.thumbnailsRD$.subscribe((thumbnailsRD: RemoteData<PaginatedList<Bitstream>>) => {
|
|
||||||
for (
|
|
||||||
let index = 0;
|
|
||||||
index < bitstreamsRD.payload.page.length;
|
|
||||||
index++
|
|
||||||
) {
|
|
||||||
this.subs.push(bitstreamsRD.payload.page[index].format
|
|
||||||
.pipe(getFirstSucceededRemoteDataPayload())
|
|
||||||
.subscribe((format: BitstreamFormat) => {
|
|
||||||
const mediaItem = this.createMediaViewerItem(
|
|
||||||
bitstreamsRD.payload.page[index],
|
|
||||||
format,
|
|
||||||
thumbnailsRD.payload && thumbnailsRD.payload.page[index],
|
|
||||||
);
|
|
||||||
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.changeDetectorRef.detectChanges();
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method will retrieve the next page of Bitstreams from the external BitstreamDataService call.
|
|
||||||
* @param bundleName Bundle name
|
|
||||||
*/
|
|
||||||
loadRemoteData(
|
|
||||||
bundleName: string,
|
|
||||||
): Observable<RemoteData<PaginatedList<Bitstream>>> {
|
|
||||||
return this.bitstreamDataService
|
|
||||||
.findAllByItemAndBundleName(
|
|
||||||
this.item,
|
|
||||||
bundleName,
|
|
||||||
{},
|
|
||||||
true,
|
|
||||||
true,
|
|
||||||
followLink('format'),
|
|
||||||
)
|
|
||||||
.pipe(
|
|
||||||
filter(
|
|
||||||
(bitstreamsRD: RemoteData<PaginatedList<Bitstream>>) =>
|
|
||||||
hasValue(bitstreamsRD) &&
|
|
||||||
(hasValue(bitstreamsRD.errorMessage) || hasValue(bitstreamsRD.payload)),
|
|
||||||
),
|
|
||||||
take(1),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method creates a {@link MediaViewerItem} from incoming {@link Bitstream}s
|
|
||||||
* @param original original bitstream
|
|
||||||
* @param format original bitstream format
|
|
||||||
* @param thumbnail thumbnail bitstream
|
|
||||||
*/
|
|
||||||
createMediaViewerItem(original: Bitstream, format: BitstreamFormat, thumbnail: Bitstream): MediaViewerItem {
|
|
||||||
const mediaItem = new MediaViewerItem();
|
|
||||||
mediaItem.bitstream = original;
|
|
||||||
mediaItem.format = format.mimetype.split('/')[0];
|
|
||||||
mediaItem.mimetype = format.mimetype;
|
|
||||||
mediaItem.thumbnail = thumbnail ? thumbnail._links.content.href : null;
|
|
||||||
return mediaItem;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,7 +0,0 @@
|
|||||||
<div [class.change-gallery]="isAuthenticated$ | async">
|
|
||||||
<ngx-gallery
|
|
||||||
class="ngx-gallery"
|
|
||||||
[options]="galleryOptions"
|
|
||||||
[images]="galleryImages"
|
|
||||||
></ngx-gallery>
|
|
||||||
</div>
|
|
@@ -1,20 +0,0 @@
|
|||||||
:host ::ng-deep {
|
|
||||||
.ngx-gallery {
|
|
||||||
width: unset !important;
|
|
||||||
height: unset !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
ngx-gallery-image {
|
|
||||||
max-width: 340px !important;
|
|
||||||
|
|
||||||
.ngx-gallery-image {
|
|
||||||
background-position: left;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngx-gallery-image:after {
|
|
||||||
padding-top: 75%;
|
|
||||||
display: block;
|
|
||||||
content: '';
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,91 +0,0 @@
|
|||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
|
||||||
import {
|
|
||||||
ComponentFixture,
|
|
||||||
TestBed,
|
|
||||||
waitForAsync,
|
|
||||||
} from '@angular/core/testing';
|
|
||||||
import { NgxGalleryOptions } from '@kolkov/ngx-gallery';
|
|
||||||
import { of as observableOf } from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../../../../core/auth/auth.service';
|
|
||||||
import { Bitstream } from '../../../../../core/shared/bitstream.model';
|
|
||||||
import { MediaViewerItem } from '../../../../../core/shared/media-viewer-item.model';
|
|
||||||
import { MockBitstreamFormat1 } from '../../../../../shared/mocks/item.mock';
|
|
||||||
import { SecureMediaViewerImageComponent } from './secure-media-viewer-image.component';
|
|
||||||
|
|
||||||
describe('ItemSecureMediaViewerImageComponent', () => {
|
|
||||||
let component: SecureMediaViewerImageComponent;
|
|
||||||
let fixture: ComponentFixture<SecureMediaViewerImageComponent>;
|
|
||||||
|
|
||||||
const authService = jasmine.createSpyObj('authService', {
|
|
||||||
isAuthenticated: observableOf(false),
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
|
|
||||||
sizeBytes: 10201,
|
|
||||||
content:
|
|
||||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
|
||||||
format: observableOf(MockBitstreamFormat1),
|
|
||||||
bundleName: 'ORIGINAL',
|
|
||||||
_links: {
|
|
||||||
self: {
|
|
||||||
href:
|
|
||||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
href:
|
|
||||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
|
||||||
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
|
||||||
type: 'bitstream',
|
|
||||||
metadata: {
|
|
||||||
'dc.title': [
|
|
||||||
{
|
|
||||||
language: null,
|
|
||||||
value: 'test_word.docx',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockMediaViewerItems: MediaViewerItem[] = Object.assign(
|
|
||||||
new Array<MediaViewerItem>(),
|
|
||||||
[
|
|
||||||
{ bitstream: mockBitstream, format: 'image', thumbnail: null },
|
|
||||||
{ bitstream: mockBitstream, format: 'image', thumbnail: null },
|
|
||||||
],
|
|
||||||
);
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [SecureMediaViewerImageComponent],
|
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
|
||||||
providers: [
|
|
||||||
{ provide: AuthService, useValue: authService },
|
|
||||||
],
|
|
||||||
}).compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(SecureMediaViewerImageComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
component.galleryOptions = [new NgxGalleryOptions({})];
|
|
||||||
component.galleryImages = component.convertToGalleryImage(
|
|
||||||
mockMediaViewerItems,
|
|
||||||
);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain a gallery options', () => {
|
|
||||||
expect(component.galleryOptions.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should contain an image array', () => {
|
|
||||||
expect(component.galleryImages.length).toBeGreaterThan(0);
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,109 +0,0 @@
|
|||||||
import { AsyncPipe } from '@angular/common';
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
Input,
|
|
||||||
OnChanges,
|
|
||||||
OnInit,
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
NgxGalleryAnimation,
|
|
||||||
NgxGalleryImage,
|
|
||||||
NgxGalleryModule,
|
|
||||||
NgxGalleryOptions,
|
|
||||||
} from '@kolkov/ngx-gallery';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
|
|
||||||
import { AuthService } from '../../../../../core/auth/auth.service';
|
|
||||||
import { MediaViewerItem } from '../../../../../core/shared/media-viewer-item.model';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This componenet render an image gallery for the image viewer
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-secure-media-viewer-image',
|
|
||||||
templateUrl: './secure-media-viewer-image.component.html',
|
|
||||||
styleUrls: ['./secure-media-viewer-image.component.scss'],
|
|
||||||
imports: [
|
|
||||||
NgxGalleryModule,
|
|
||||||
AsyncPipe,
|
|
||||||
],
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
export class SecureMediaViewerImageComponent implements OnChanges, OnInit {
|
|
||||||
@Input() images: MediaViewerItem[];
|
|
||||||
@Input() preview?: boolean;
|
|
||||||
@Input() image?: string;
|
|
||||||
@Input() accessToken: string;
|
|
||||||
|
|
||||||
thumbnailPlaceholder = './assets/images/replacement_image.svg';
|
|
||||||
|
|
||||||
galleryOptions: NgxGalleryOptions[] = [];
|
|
||||||
|
|
||||||
galleryImages: NgxGalleryImage[] = [];
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether or not the current user is authenticated
|
|
||||||
*/
|
|
||||||
isAuthenticated$: Observable<boolean>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected authService: AuthService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges(): void {
|
|
||||||
this.galleryOptions = [
|
|
||||||
{
|
|
||||||
preview: this.preview !== undefined ? this.preview : true,
|
|
||||||
image: true,
|
|
||||||
imageSize: 'contain',
|
|
||||||
thumbnails: false,
|
|
||||||
imageArrows: false,
|
|
||||||
startIndex: 0,
|
|
||||||
imageAnimation: NgxGalleryAnimation.Slide,
|
|
||||||
previewCloseOnEsc: true,
|
|
||||||
previewZoom: true,
|
|
||||||
previewRotate: true,
|
|
||||||
previewFullscreen: true,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
if (this.image) {
|
|
||||||
this.galleryImages = [
|
|
||||||
{
|
|
||||||
small: this.image,
|
|
||||||
medium: this.image,
|
|
||||||
big: this.image,
|
|
||||||
},
|
|
||||||
];
|
|
||||||
} else {
|
|
||||||
this.galleryImages = this.convertToGalleryImage(this.images);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.isAuthenticated$ = this.authService.isAuthenticated();
|
|
||||||
this.ngOnChanges();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method convert an array of MediaViewerItem into NgxGalleryImage array
|
|
||||||
* @param medias input NgxGalleryImage array
|
|
||||||
*/
|
|
||||||
convertToGalleryImage(medias: MediaViewerItem[]): NgxGalleryImage[] {
|
|
||||||
const mappedImages = [];
|
|
||||||
for (const image of medias) {
|
|
||||||
if (image.format === 'image') {
|
|
||||||
mappedImages.push({
|
|
||||||
small: image.thumbnail
|
|
||||||
? image.thumbnail
|
|
||||||
: this.thumbnailPlaceholder,
|
|
||||||
medium: image.thumbnail
|
|
||||||
? image.thumbnail
|
|
||||||
: this.thumbnailPlaceholder,
|
|
||||||
big: image.bitstream._links.content.href + '?accessToken=' + this.accessToken,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return mappedImages;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,54 +0,0 @@
|
|||||||
<video
|
|
||||||
crossorigin="anonymous"
|
|
||||||
[src]="medias[currentIndex].bitstream._links.content.href + '?accessToken=' + accessToken"
|
|
||||||
id="singleVideo"
|
|
||||||
[poster]="
|
|
||||||
medias[currentIndex].thumbnail ||
|
|
||||||
replacements[medias[currentIndex].format]
|
|
||||||
"
|
|
||||||
preload="none"
|
|
||||||
controls
|
|
||||||
>
|
|
||||||
<ng-container *ngIf="getMediaCap(medias[currentIndex].bitstream.name, captions) as capInfos">
|
|
||||||
<ng-container *ngFor="let capInfo of capInfos">
|
|
||||||
<track [src]="capInfo.src" [label]="capInfo.langLabel" [srclang]="capInfo.srclang" />
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
|
|
||||||
</video>
|
|
||||||
<div class="buttons" *ngIf="medias?.length > 1">
|
|
||||||
<button
|
|
||||||
class="btn btn-primary previous"
|
|
||||||
[dsBtnDisabled]="currentIndex === 0"
|
|
||||||
(click)="prevMedia()"
|
|
||||||
>
|
|
||||||
{{ "media-viewer.previous" | translate }}
|
|
||||||
</button>
|
|
||||||
|
|
||||||
<button
|
|
||||||
class="btn btn-primary next"
|
|
||||||
[dsBtnDisabled]="currentIndex === medias.length - 1"
|
|
||||||
(click)="nextMedia()"
|
|
||||||
>
|
|
||||||
{{ "media-viewer.next" | translate }}
|
|
||||||
</button>
|
|
||||||
<div ngbDropdown class="d-inline-block">
|
|
||||||
<button
|
|
||||||
class="btn btn-outline-primary playlist"
|
|
||||||
id="dropdownBasic1"
|
|
||||||
ngbDropdownToggle
|
|
||||||
>
|
|
||||||
{{ "media-viewer.playlist" | translate }}
|
|
||||||
</button>
|
|
||||||
<div ngbDropdownMenu aria-labelledby="dropdownBasic1">
|
|
||||||
<button
|
|
||||||
ngbDropdownItem
|
|
||||||
*ngFor="let item of medias; index as indexOfelement"
|
|
||||||
class="list-element"
|
|
||||||
(click)="selectedMedia(indexOfelement)"
|
|
||||||
>
|
|
||||||
{{ dsoNameService.getName(item.bitstream) }}
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@@ -1,10 +0,0 @@
|
|||||||
video {
|
|
||||||
width: 100%;
|
|
||||||
height: auto;
|
|
||||||
max-width: 340px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.buttons {
|
|
||||||
display: flex;
|
|
||||||
gap: .25rem;
|
|
||||||
}
|
|
@@ -1,149 +0,0 @@
|
|||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
|
||||||
import {
|
|
||||||
ComponentFixture,
|
|
||||||
TestBed,
|
|
||||||
waitForAsync,
|
|
||||||
} from '@angular/core/testing';
|
|
||||||
import { By } from '@angular/platform-browser';
|
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
|
||||||
import {
|
|
||||||
TranslateLoader,
|
|
||||||
TranslateModule,
|
|
||||||
} from '@ngx-translate/core';
|
|
||||||
import { of as observableOf } from 'rxjs';
|
|
||||||
|
|
||||||
import { Bitstream } from '../../../../../core/shared/bitstream.model';
|
|
||||||
import { MediaViewerItem } from '../../../../../core/shared/media-viewer-item.model';
|
|
||||||
import { MetadataFieldWrapperComponent } from '../../../../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
|
|
||||||
import { MockBitstreamFormat1 } from '../../../../../shared/mocks/item.mock';
|
|
||||||
import { TranslateLoaderMock } from '../../../../../shared/mocks/translate-loader.mock';
|
|
||||||
import { FileSizePipe } from '../../../../../shared/utils/file-size-pipe';
|
|
||||||
import { VarDirective } from '../../../../../shared/utils/var.directive';
|
|
||||||
import { SecureMediaViewerVideoComponent } from './secure-media-viewer-video.component';
|
|
||||||
|
|
||||||
describe('SecureMediaViewerVideoComponent', () => {
|
|
||||||
let component: SecureMediaViewerVideoComponent;
|
|
||||||
let fixture: ComponentFixture<SecureMediaViewerVideoComponent>;
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
TranslateModule.forRoot({
|
|
||||||
loader: {
|
|
||||||
provide: TranslateLoader,
|
|
||||||
useClass: TranslateLoaderMock,
|
|
||||||
},
|
|
||||||
}),
|
|
||||||
BrowserAnimationsModule,
|
|
||||||
SecureMediaViewerVideoComponent,
|
|
||||||
VarDirective,
|
|
||||||
FileSizePipe,
|
|
||||||
MetadataFieldWrapperComponent,
|
|
||||||
],
|
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
|
||||||
}).compileComponents();
|
|
||||||
}));
|
|
||||||
|
|
||||||
const mockBitstream: Bitstream = Object.assign(new Bitstream(), {
|
|
||||||
sizeBytes: 10201,
|
|
||||||
content:
|
|
||||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
|
||||||
format: observableOf(MockBitstreamFormat1),
|
|
||||||
bundleName: 'ORIGINAL',
|
|
||||||
_links: {
|
|
||||||
self: {
|
|
||||||
href:
|
|
||||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
|
||||||
},
|
|
||||||
content: {
|
|
||||||
href:
|
|
||||||
'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
|
||||||
},
|
|
||||||
},
|
|
||||||
id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
|
||||||
uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713',
|
|
||||||
type: 'bitstream',
|
|
||||||
metadata: {
|
|
||||||
'dc.title': [
|
|
||||||
{
|
|
||||||
language: null,
|
|
||||||
value: 'test_word.docx',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockMediaViewerItems: MediaViewerItem[] = Object.assign(
|
|
||||||
new Array<MediaViewerItem>(),
|
|
||||||
[
|
|
||||||
{ bitstream: mockBitstream, format: 'video', thumbnail: null },
|
|
||||||
{ bitstream: mockBitstream, format: 'video', thumbnail: null },
|
|
||||||
],
|
|
||||||
);
|
|
||||||
const mockMediaViewerItem: MediaViewerItem[] = Object.assign(
|
|
||||||
new Array<MediaViewerItem>(),
|
|
||||||
[{ bitstream: mockBitstream, format: 'video', thumbnail: null }],
|
|
||||||
);
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(SecureMediaViewerVideoComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
component.medias = mockMediaViewerItem;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should create', () => {
|
|
||||||
expect(component).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('should show controller buttons when the having mode then one video', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
component.medias = mockMediaViewerItems;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should show buttons', () => {
|
|
||||||
const controllerButtons = fixture.debugElement.query(By.css('.buttons'));
|
|
||||||
expect(controllerButtons).toBeTruthy();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the "Next" button is clicked', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
component.currentIndex = 0;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should increase the index', () => {
|
|
||||||
const viewMore = fixture.debugElement.query(By.css('.next'));
|
|
||||||
viewMore.triggerEventHandler('click', null);
|
|
||||||
expect(component.currentIndex).toBe(1);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the "Previous" button is clicked', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
component.currentIndex = 1;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should decrease the index', () => {
|
|
||||||
const viewMore = fixture.debugElement.query(By.css('.previous'));
|
|
||||||
viewMore.triggerEventHandler('click', null);
|
|
||||||
expect(component.currentIndex).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('when the "Playlist element" button is clicked', () => {
|
|
||||||
beforeEach(() => {
|
|
||||||
component.isCollapsed = true;
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set the the index with the selected one', () => {
|
|
||||||
const viewMore = fixture.debugElement.query(By.css('.list-element'));
|
|
||||||
viewMore.triggerEventHandler('click', null);
|
|
||||||
expect(component.currentIndex).toBe(0);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
@@ -1,103 +0,0 @@
|
|||||||
import {
|
|
||||||
NgForOf,
|
|
||||||
NgIf,
|
|
||||||
} from '@angular/common';
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
Input,
|
|
||||||
} from '@angular/core';
|
|
||||||
import { NgbDropdownModule } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
|
||||||
import { Bitstream } from 'src/app/core/shared/bitstream.model';
|
|
||||||
|
|
||||||
import { DSONameService } from '../../../../../core/breadcrumbs/dso-name.service';
|
|
||||||
import { MediaViewerItem } from '../../../../../core/shared/media-viewer-item.model';
|
|
||||||
import { BtnDisabledDirective } from '../../../../../shared/btn-disabled.directive';
|
|
||||||
import { CaptionInfo } from '../../../../media-viewer/media-viewer-video/caption-info';
|
|
||||||
import { languageHelper } from '../../../../media-viewer/media-viewer-video/language-helper';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This component renders a video viewer and playlist for the media viewer
|
|
||||||
*/
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-secure-media-viewer-video',
|
|
||||||
templateUrl: './secure-media-viewer-video.component.html',
|
|
||||||
styleUrls: ['./secure-media-viewer-video.component.scss'],
|
|
||||||
imports: [
|
|
||||||
NgForOf,
|
|
||||||
NgbDropdownModule,
|
|
||||||
TranslateModule,
|
|
||||||
NgIf,
|
|
||||||
BtnDisabledDirective,
|
|
||||||
],
|
|
||||||
standalone: true,
|
|
||||||
})
|
|
||||||
export class SecureMediaViewerVideoComponent {
|
|
||||||
@Input() medias: MediaViewerItem[];
|
|
||||||
|
|
||||||
@Input() captions: Bitstream[] = [];
|
|
||||||
|
|
||||||
@Input() accessToken: string;
|
|
||||||
|
|
||||||
isCollapsed = false;
|
|
||||||
|
|
||||||
currentIndex = 0;
|
|
||||||
|
|
||||||
replacements = {
|
|
||||||
video: './assets/images/replacement_video.svg',
|
|
||||||
audio: './assets/images/replacement_audio.svg',
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
public dsoNameService: DSONameService,
|
|
||||||
) {
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method check if there is caption file for the media
|
|
||||||
* The caption file name is the media name plus "-" following two letter
|
|
||||||
* language code and .vtt suffix
|
|
||||||
*
|
|
||||||
* html5 video only support WEBVTT format
|
|
||||||
*
|
|
||||||
* Two letter language code reference
|
|
||||||
* https://www.w3schools.com/tags/ref_language_codes.asp
|
|
||||||
*/
|
|
||||||
getMediaCap(name: string, captions: Bitstream[]): CaptionInfo[] {
|
|
||||||
const capInfos: CaptionInfo[] = [];
|
|
||||||
const filteredCapMedias: Bitstream[] = captions
|
|
||||||
.filter((media: Bitstream) => media.name.substring(0, (media.name.length - 7)).toLowerCase() === name.toLowerCase());
|
|
||||||
|
|
||||||
for (const media of filteredCapMedias) {
|
|
||||||
const srclang: string = media.name.slice(-6, -4).toLowerCase();
|
|
||||||
capInfos.push(new CaptionInfo(
|
|
||||||
media._links.content.href + '?accessToken=' + this.accessToken,
|
|
||||||
srclang,
|
|
||||||
languageHelper[srclang],
|
|
||||||
));
|
|
||||||
}
|
|
||||||
return capInfos;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method sets the received index into currentIndex
|
|
||||||
* @param index Selected index
|
|
||||||
*/
|
|
||||||
selectedMedia(index: number) {
|
|
||||||
this.currentIndex = index;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method increases the number of the currentIndex
|
|
||||||
*/
|
|
||||||
nextMedia() {
|
|
||||||
this.currentIndex++;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* This method decreases the number of the currentIndex
|
|
||||||
*/
|
|
||||||
prevMedia() {
|
|
||||||
this.currentIndex--;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,8 +0,0 @@
|
|||||||
<div class="container">
|
|
||||||
<div class="item-page">
|
|
||||||
<ds-item-access-by-token-view [object]="(itemRD$ | async)?.payload"
|
|
||||||
[itemRequest$]="itemRequest$"
|
|
||||||
[viewMode]="viewMode">
|
|
||||||
</ds-item-access-by-token-view>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
@@ -1,299 +0,0 @@
|
|||||||
import { KeyValuePipe } from '@angular/common';
|
|
||||||
import { PLATFORM_ID } from '@angular/core';
|
|
||||||
import {
|
|
||||||
ComponentFixture,
|
|
||||||
fakeAsync,
|
|
||||||
TestBed,
|
|
||||||
} from '@angular/core/testing';
|
|
||||||
import {
|
|
||||||
ActivatedRoute,
|
|
||||||
Router,
|
|
||||||
} from '@angular/router';
|
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
|
||||||
import { Store } from '@ngrx/store';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
|
||||||
import {
|
|
||||||
BehaviorSubject,
|
|
||||||
of as observableOf,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import { getForbiddenRoute } from '../../app-routing-paths';
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
|
||||||
import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service';
|
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
|
||||||
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
|
||||||
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
|
||||||
import { LinkHeadService } from '../../core/services/link-head.service';
|
|
||||||
import { ServerResponseService } from '../../core/services/server-response.service';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
|
||||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
|
||||||
import { ItemAccessByTokenPageComponent } from './item-access-by-token-page.component';
|
|
||||||
import { ItemAccessByTokenViewComponent } from './item-access-by-token-view.component';
|
|
||||||
|
|
||||||
describe('ItemAccessByTokenPageComponent', () => {
|
|
||||||
let component: ItemAccessByTokenPageComponent;
|
|
||||||
let fixture: ComponentFixture<ItemAccessByTokenPageComponent>;
|
|
||||||
let itemRequestService: jasmine.SpyObj<ItemRequestDataService>;
|
|
||||||
let router: jasmine.SpyObj<Router>;
|
|
||||||
let authorizationService: AuthorizationDataService;
|
|
||||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
|
||||||
isAuthorized: observableOf(false),
|
|
||||||
});
|
|
||||||
let signpostingDataService: SignpostingDataService;
|
|
||||||
|
|
||||||
const mocklink = {
|
|
||||||
href: 'http://test.org',
|
|
||||||
rel: 'test',
|
|
||||||
type: 'test',
|
|
||||||
};
|
|
||||||
|
|
||||||
const mocklink2 = {
|
|
||||||
href: 'http://test2.org',
|
|
||||||
rel: 'test',
|
|
||||||
type: 'test',
|
|
||||||
};
|
|
||||||
signpostingDataService = jasmine.createSpyObj('SignpostingDataService', {
|
|
||||||
getLinks: observableOf([mocklink, mocklink2]),
|
|
||||||
});
|
|
||||||
const linkHeadService = jasmine.createSpyObj('linkHeadService', {
|
|
||||||
addTag: '',
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockItem = Object.assign(new Item(), {
|
|
||||||
uuid: 'test-item-uuid',
|
|
||||||
id: 'test-item-id',
|
|
||||||
metadata: {
|
|
||||||
'dspace.entity.type': [{
|
|
||||||
value: 'Publication',
|
|
||||||
language: 'en',
|
|
||||||
place: 0,
|
|
||||||
authority: null,
|
|
||||||
confidence: -1,
|
|
||||||
}],
|
|
||||||
},
|
|
||||||
_links: {
|
|
||||||
self: { href: 'obj-selflink' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
const mockItemRequest = Object.assign(new ItemRequest(), {
|
|
||||||
token: 'valid-token',
|
|
||||||
accessToken: 'valid-token',
|
|
||||||
itemId: mockItem.uuid,
|
|
||||||
});
|
|
||||||
|
|
||||||
const queryParams = { accessToken: 'valid-token' };
|
|
||||||
const mockActivatedRoute = {
|
|
||||||
queryParams: new BehaviorSubject(queryParams),
|
|
||||||
data: observableOf({
|
|
||||||
dso: createSuccessfulRemoteDataObject(mockItem),
|
|
||||||
}),
|
|
||||||
params: observableOf({ itemId: mockItem.uuid, queryParams: [ { accessToken: 'valid-token' } ] }),
|
|
||||||
children: [],
|
|
||||||
};
|
|
||||||
itemRequestService = jasmine.createSpyObj('ItemRequestDataService', {
|
|
||||||
getSanitizedRequestByAccessToken: observableOf(createSuccessfulRemoteDataObject(mockItemRequest)),
|
|
||||||
});
|
|
||||||
router = jasmine.createSpyObj('Router', ['navigateByUrl'], {
|
|
||||||
events: observableOf([]),
|
|
||||||
});
|
|
||||||
beforeEach(async () => {
|
|
||||||
await TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
TranslateModule.forRoot(),
|
|
||||||
KeyValuePipe,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: ItemRequestDataService, useValue: itemRequestService },
|
|
||||||
{ provide: Router, useValue: router },
|
|
||||||
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
|
|
||||||
{ provide: ItemDataService, useValue: {} },
|
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
|
||||||
{ provide: ServerResponseService, useValue: {} },
|
|
||||||
{ provide: SignpostingDataService, useValue: signpostingDataService },
|
|
||||||
{ provide: LinkHeadService, useValue: linkHeadService },
|
|
||||||
{ provide: NotifyInfoService, useValue: { isCoarConfigEnabled: () => observableOf(false) } },
|
|
||||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
|
||||||
KeyValuePipe,
|
|
||||||
{
|
|
||||||
provide: Store,
|
|
||||||
useValue: {
|
|
||||||
pipe: () => observableOf({}),
|
|
||||||
dispatch: () => {
|
|
||||||
},
|
|
||||||
select: () => observableOf({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: AuthService, useValue: {
|
|
||||||
isAuthenticated: () => observableOf(true),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).overrideComponent(ItemAccessByTokenPageComponent, {
|
|
||||||
set: {
|
|
||||||
template: '<div></div>',
|
|
||||||
},
|
|
||||||
}).overrideComponent(ItemAccessByTokenViewComponent, {
|
|
||||||
set: {
|
|
||||||
template: '<div></div>',
|
|
||||||
},
|
|
||||||
}).compileComponents();
|
|
||||||
});
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(ItemAccessByTokenPageComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Tests in this component are concerned only with successful access token processing (or error handling)
|
|
||||||
* and a resulting item request object. Testing of template elements is out of scope and left for child components.
|
|
||||||
*/
|
|
||||||
describe('ngOnInit - basic component testing', () => {
|
|
||||||
it('should find valid access token and sanitize it', fakeAsync(() => {
|
|
||||||
TestBed.resetTestingModule();
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
TranslateModule.forRoot(),
|
|
||||||
KeyValuePipe,
|
|
||||||
RouterTestingModule.withRoutes([]),
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: ItemRequestDataService, useValue: itemRequestService },
|
|
||||||
{ provide: Router, useValue: router },
|
|
||||||
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
|
|
||||||
{ provide: AuthService, useValue: {} },
|
|
||||||
{ provide: ItemDataService, useValue: {} },
|
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
|
||||||
{ provide: ServerResponseService, useValue: {} },
|
|
||||||
{ provide: SignpostingDataService, useValue: signpostingDataService },
|
|
||||||
{ provide: LinkHeadService, useValue: linkHeadService },
|
|
||||||
{ provide: NotifyInfoService, useValue: { isCoarConfigEnabled: () => observableOf(false) } },
|
|
||||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
|
||||||
KeyValuePipe,
|
|
||||||
{
|
|
||||||
provide: Store,
|
|
||||||
useValue: {
|
|
||||||
pipe: () => observableOf({}),
|
|
||||||
dispatch: () => {},
|
|
||||||
select: () => observableOf({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ provide: AuthService, useValue: {
|
|
||||||
isAuthenticated: () => observableOf(false ) },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).overrideComponent(ItemAccessByTokenViewComponent, {
|
|
||||||
set: { template: '<div></div>' } } ).compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ItemAccessByTokenPageComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
expect(itemRequestService.getSanitizedRequestByAccessToken).toHaveBeenCalledWith('valid-token');
|
|
||||||
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should process valid access token and load item request', fakeAsync(() => {
|
|
||||||
TestBed.resetTestingModule();
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
TranslateModule.forRoot(),
|
|
||||||
KeyValuePipe,
|
|
||||||
RouterTestingModule.withRoutes([]),
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: ItemRequestDataService, useValue: itemRequestService },
|
|
||||||
{ provide: Router, useValue: router },
|
|
||||||
{ provide: ActivatedRoute, useValue: mockActivatedRoute },
|
|
||||||
{ provide: AuthService, useValue: {} },
|
|
||||||
{ provide: ItemDataService, useValue: {} },
|
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
|
||||||
{ provide: ServerResponseService, useValue: {} },
|
|
||||||
{ provide: SignpostingDataService, useValue: signpostingDataService },
|
|
||||||
{ provide: LinkHeadService, useValue: linkHeadService },
|
|
||||||
{ provide: NotifyInfoService, useValue: { isCoarConfigEnabled: () => observableOf(false) } },
|
|
||||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
|
||||||
KeyValuePipe,
|
|
||||||
{
|
|
||||||
provide: Store,
|
|
||||||
useValue: {
|
|
||||||
pipe: () => observableOf({}),
|
|
||||||
dispatch: () => {},
|
|
||||||
select: () => observableOf({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ provide: AuthService, useValue: {
|
|
||||||
isAuthenticated: () => observableOf(false ) },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).overrideComponent(ItemAccessByTokenViewComponent, {
|
|
||||||
set: { template: '<div></div>' } } ).compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ItemAccessByTokenPageComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
component.itemRequest$.subscribe((request) => {
|
|
||||||
expect(request).toBeTruthy();
|
|
||||||
});
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should redirect to forbidden route when access token is missing', fakeAsync(() => {
|
|
||||||
const routeWithoutToken = {
|
|
||||||
queryParams: observableOf({}),
|
|
||||||
data: observableOf({
|
|
||||||
dso: createSuccessfulRemoteDataObject(mockItem),
|
|
||||||
}),
|
|
||||||
params: observableOf({ itemId: mockItem.uuid }),
|
|
||||||
children: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
TestBed.resetTestingModule();
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
TranslateModule.forRoot(),
|
|
||||||
KeyValuePipe,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: ItemRequestDataService, useValue: itemRequestService },
|
|
||||||
{ provide: Router, useValue: router },
|
|
||||||
{ provide: ActivatedRoute, useValue: routeWithoutToken },
|
|
||||||
{ provide: AuthService, useValue: {} },
|
|
||||||
{ provide: ItemDataService, useValue: {} },
|
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
|
||||||
{ provide: ServerResponseService, useValue: {} },
|
|
||||||
{ provide: SignpostingDataService, useValue: signpostingDataService },
|
|
||||||
{ provide: LinkHeadService, useValue: linkHeadService },
|
|
||||||
{ provide: NotifyInfoService, useValue: { isCoarConfigEnabled: () => observableOf(false) } },
|
|
||||||
{ provide: PLATFORM_ID, useValue: 'browser' },
|
|
||||||
{
|
|
||||||
provide: Store,
|
|
||||||
useValue: {
|
|
||||||
pipe: () => observableOf({}),
|
|
||||||
dispatch: () => {},
|
|
||||||
select: () => observableOf({}),
|
|
||||||
},
|
|
||||||
},
|
|
||||||
{ provide: AuthService, useValue: {
|
|
||||||
isAuthenticated: () => observableOf(false ) },
|
|
||||||
},
|
|
||||||
],
|
|
||||||
}).overrideComponent(ItemAccessByTokenViewComponent, {
|
|
||||||
set: {
|
|
||||||
template: '<div></div>',
|
|
||||||
} })
|
|
||||||
.compileComponents();
|
|
||||||
|
|
||||||
fixture = TestBed.createComponent(ItemAccessByTokenPageComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
fixture.detectChanges();
|
|
||||||
expect(router.navigateByUrl).toHaveBeenCalledWith(getForbiddenRoute(), { skipLocationChange: false });
|
|
||||||
}));
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@@ -1,178 +0,0 @@
|
|||||||
import {
|
|
||||||
AsyncPipe,
|
|
||||||
KeyValuePipe,
|
|
||||||
Location,
|
|
||||||
NgForOf,
|
|
||||||
NgIf,
|
|
||||||
} from '@angular/common';
|
|
||||||
import {
|
|
||||||
ChangeDetectionStrategy,
|
|
||||||
Component,
|
|
||||||
Inject,
|
|
||||||
OnDestroy,
|
|
||||||
OnInit,
|
|
||||||
PLATFORM_ID,
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
ActivatedRoute,
|
|
||||||
Router,
|
|
||||||
RouterLink,
|
|
||||||
} from '@angular/router';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
|
||||||
import { Observable } from 'rxjs';
|
|
||||||
import {
|
|
||||||
filter,
|
|
||||||
map,
|
|
||||||
switchMap,
|
|
||||||
take,
|
|
||||||
tap,
|
|
||||||
} from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { getForbiddenRoute } from '../../app-routing-paths';
|
|
||||||
import { AuthService } from '../../core/auth/auth.service';
|
|
||||||
import { NotifyInfoService } from '../../core/coar-notify/notify-info/notify-info.service';
|
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
|
||||||
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
|
||||||
import { SignpostingDataService } from '../../core/data/signposting-data.service';
|
|
||||||
import { LinkHeadService } from '../../core/services/link-head.service';
|
|
||||||
import { ServerResponseService } from '../../core/services/server-response.service';
|
|
||||||
import { redirectOn4xx } from '../../core/shared/authorized.operators';
|
|
||||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
|
||||||
import {
|
|
||||||
getFirstCompletedRemoteData,
|
|
||||||
getFirstSucceededRemoteDataPayload,
|
|
||||||
} from '../../core/shared/operators';
|
|
||||||
import { fadeInOut } from '../../shared/animations/fade';
|
|
||||||
import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
|
|
||||||
import { hasValue } from '../../shared/empty.util';
|
|
||||||
import { ErrorComponent } from '../../shared/error/error.component';
|
|
||||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
|
||||||
import { MetadataFieldWrapperComponent } from '../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
|
|
||||||
import { ThemedResultsBackButtonComponent } from '../../shared/results-back-button/themed-results-back-button.component';
|
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
|
||||||
import { ViewTrackerComponent } from '../../statistics/angulartics/dspace/view-tracker.component';
|
|
||||||
import { ThemedThumbnailComponent } from '../../thumbnail/themed-thumbnail.component';
|
|
||||||
import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component';
|
|
||||||
import { CollectionsComponent } from '../field-components/collections/collections.component';
|
|
||||||
import { ThemedFullFileSectionComponent } from '../full/field-components/file-section/themed-full-file-section.component';
|
|
||||||
import { ThemedMediaViewerComponent } from '../media-viewer/themed-media-viewer.component';
|
|
||||||
import { MiradorViewerComponent } from '../mirador-viewer/mirador-viewer.component';
|
|
||||||
import { ThemedFileSectionComponent } from '../simple/field-components/file-section/themed-file-section.component';
|
|
||||||
import { ItemPageAbstractFieldComponent } from '../simple/field-components/specific-field/abstract/item-page-abstract-field.component';
|
|
||||||
import { ItemPageDateFieldComponent } from '../simple/field-components/specific-field/date/item-page-date-field.component';
|
|
||||||
import { GenericItemPageFieldComponent } from '../simple/field-components/specific-field/generic/generic-item-page-field.component';
|
|
||||||
import { ThemedItemPageTitleFieldComponent } from '../simple/field-components/specific-field/title/themed-item-page-field.component';
|
|
||||||
import { ItemPageUriFieldComponent } from '../simple/field-components/specific-field/uri/item-page-uri-field.component';
|
|
||||||
import { ItemPageComponent } from '../simple/item-page.component';
|
|
||||||
import { ThemedMetadataRepresentationListComponent } from '../simple/metadata-representation-list/themed-metadata-representation-list.component';
|
|
||||||
import { ItemVersionsComponent } from '../versions/item-versions.component';
|
|
||||||
import { ItemVersionsNoticeComponent } from '../versions/notice/item-versions-notice.component';
|
|
||||||
import { ItemSecureFileSectionComponent } from './field-components/file-section/item-secure-file-section.component';
|
|
||||||
import { ItemAccessByTokenViewComponent } from './item-access-by-token-view.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-access-by-token-item-page',
|
|
||||||
styleUrls: ['./item-access-by-token-page.component.scss'],
|
|
||||||
templateUrl: './item-access-by-token-page.component.html',
|
|
||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
|
||||||
animations: [fadeInOut],
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
ErrorComponent,
|
|
||||||
ThemedLoadingComponent,
|
|
||||||
TranslateModule,
|
|
||||||
ThemedFullFileSectionComponent,
|
|
||||||
CollectionsComponent,
|
|
||||||
ItemVersionsComponent,
|
|
||||||
NgIf,
|
|
||||||
NgForOf,
|
|
||||||
AsyncPipe,
|
|
||||||
KeyValuePipe,
|
|
||||||
RouterLink,
|
|
||||||
ThemedItemPageTitleFieldComponent,
|
|
||||||
DsoEditMenuComponent,
|
|
||||||
ItemVersionsNoticeComponent,
|
|
||||||
ViewTrackerComponent,
|
|
||||||
ThemedItemAlertsComponent,
|
|
||||||
VarDirective,
|
|
||||||
ItemSecureFileSectionComponent,
|
|
||||||
GenericItemPageFieldComponent,
|
|
||||||
ItemPageAbstractFieldComponent,
|
|
||||||
ItemPageDateFieldComponent,
|
|
||||||
ItemPageUriFieldComponent,
|
|
||||||
MetadataFieldWrapperComponent,
|
|
||||||
MiradorViewerComponent,
|
|
||||||
ThemedFileSectionComponent,
|
|
||||||
ThemedMediaViewerComponent,
|
|
||||||
ThemedMetadataRepresentationListComponent,
|
|
||||||
ThemedResultsBackButtonComponent,
|
|
||||||
ThemedThumbnailComponent,
|
|
||||||
ItemAccessByTokenViewComponent,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class ItemAccessByTokenPageComponent extends ItemPageComponent implements OnInit, OnDestroy {
|
|
||||||
|
|
||||||
itemRequest$: Observable<ItemRequest>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected route: ActivatedRoute,
|
|
||||||
protected router: Router,
|
|
||||||
protected items: ItemDataService,
|
|
||||||
protected authService: AuthService,
|
|
||||||
protected authorizationService: AuthorizationDataService,
|
|
||||||
protected _location: Location,
|
|
||||||
protected responseService: ServerResponseService,
|
|
||||||
protected signpostingDataService: SignpostingDataService,
|
|
||||||
protected linkHeadService: LinkHeadService,
|
|
||||||
protected notifyInfoService: NotifyInfoService,
|
|
||||||
private itemRequestDataService: ItemRequestDataService,
|
|
||||||
@Inject(PLATFORM_ID) protected platformId: string,
|
|
||||||
) {
|
|
||||||
super(route, router, items, authorizationService, responseService, signpostingDataService, linkHeadService, notifyInfoService, platformId);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected readonly hasValue = hasValue;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Initialise this component
|
|
||||||
* 1. take the access token from the query params and complete the stream
|
|
||||||
* 2. test for access token or redirect to forbidden page
|
|
||||||
* 3. get the sanitized token, make sure it is valid (if not, redirect to forbidden page)
|
|
||||||
* 4. return observable to itemRequest$ for the view to subscribe to
|
|
||||||
*/
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.itemRequest$ = this.route.queryParams.pipe(
|
|
||||||
take(1),
|
|
||||||
map(params => {
|
|
||||||
if (!hasValue(params?.accessToken)) {
|
|
||||||
this.router.navigateByUrl(getForbiddenRoute(), { skipLocationChange: false });
|
|
||||||
return null;
|
|
||||||
}
|
|
||||||
return params.accessToken;
|
|
||||||
}),
|
|
||||||
filter(token => hasValue(token)),
|
|
||||||
switchMap(token => this.itemRequestDataService.getSanitizedRequestByAccessToken(token)),
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
redirectOn4xx(this.router, this.authService),
|
|
||||||
getFirstSucceededRemoteDataPayload(),
|
|
||||||
tap(request => {
|
|
||||||
if (!hasValue(request)) {
|
|
||||||
this.router.navigateByUrl(getForbiddenRoute());
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Call item page component initialization.
|
|
||||||
super.ngOnInit();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Navigate back in browser history.
|
|
||||||
*/
|
|
||||||
back() {
|
|
||||||
this._location.back();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
@@ -1,101 +0,0 @@
|
|||||||
<ng-container *ngIf="itemRequest$| async as itemRequest">
|
|
||||||
|
|
||||||
<div class="row" *ngIf="iiifEnabled">
|
|
||||||
<div class="col-12">
|
|
||||||
<ds-mirador-viewer id="iiif-viewer"
|
|
||||||
[object]="object"
|
|
||||||
[searchable]="iiifSearchEnabled"
|
|
||||||
[query]="iiifQuery$ | async">
|
|
||||||
</ds-mirador-viewer>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="d-flex flex-row">
|
|
||||||
<ds-item-page-title-field [item]="object" class="mr-auto">
|
|
||||||
</ds-item-page-title-field>
|
|
||||||
<ds-dso-edit-menu></ds-dso-edit-menu>
|
|
||||||
</div>
|
|
||||||
<div class="alert alert-warning wb-100 mb-2">
|
|
||||||
<p><i class="fa-solid fa-lock-open mr-2" style="color: #26a269;"></i>{{'bitstream-request-a-copy.access-by-token.warning' | translate}}</p>
|
|
||||||
<p *ngIf="hasValue(this.itemRequest?.accessPeriod) && this.itemRequest?.accessPeriod > 0">{{ 'bitstream-request-a-copy.access-by-token.expiry-label' | translate }} {{ getAccessPeriodEndDate() }}</p>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<div class="col-xs-12 col-md-4">
|
|
||||||
<ng-container *ngIf="!(mediaViewer.image || mediaViewer.video)">
|
|
||||||
<ds-metadata-field-wrapper [hideIfNoTextContent]="false">
|
|
||||||
<ds-thumbnail [thumbnail]="object?.thumbnail | async"></ds-thumbnail>
|
|
||||||
</ds-metadata-field-wrapper>
|
|
||||||
</ng-container>
|
|
||||||
<div *ngIf="mediaViewer.image || mediaViewer.video" class="mb-2">
|
|
||||||
<ds-item-secure-media-viewer [item]="object" [accessToken]="itemRequest.accessToken">
|
|
||||||
</ds-item-secure-media-viewer>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<ds-item-page-date-field [item]="object"></ds-item-page-date-field>
|
|
||||||
<ds-metadata-representation-list class="ds-item-page-mixed-author-field"
|
|
||||||
[parentItem]="object"
|
|
||||||
[itemType]="'Person'"
|
|
||||||
[metadataFields]="['dc.contributor.author', 'dc.creator']"
|
|
||||||
[label]="'relationships.isAuthorOf' | translate">
|
|
||||||
</ds-metadata-representation-list>
|
|
||||||
<ds-generic-item-page-field [item]="object"
|
|
||||||
[fields]="['journal.title']"
|
|
||||||
[label]="'item.page.journal-title'">
|
|
||||||
</ds-generic-item-page-field>
|
|
||||||
<ds-generic-item-page-field [item]="object"
|
|
||||||
[fields]="['journal.identifier.issn']"
|
|
||||||
[label]="'item.page.journal-issn'">
|
|
||||||
</ds-generic-item-page-field>
|
|
||||||
<ds-generic-item-page-field [item]="object"
|
|
||||||
[fields]="['journalvolume.identifier.name']"
|
|
||||||
[label]="'item.page.volume-title'">
|
|
||||||
</ds-generic-item-page-field>
|
|
||||||
<ds-generic-item-page-field [item]="object"
|
|
||||||
[fields]="['dc.publisher']"
|
|
||||||
[label]="'item.page.publisher'">
|
|
||||||
</ds-generic-item-page-field>
|
|
||||||
</div>
|
|
||||||
<div class="col-xs-12 col-md-6">
|
|
||||||
<ds-item-page-abstract-field [item]="object"></ds-item-page-abstract-field>
|
|
||||||
<ds-generic-item-page-field [item]="object"
|
|
||||||
[fields]="['dc.description']"
|
|
||||||
[label]="'item.page.description'">
|
|
||||||
</ds-generic-item-page-field>
|
|
||||||
|
|
||||||
<ds-generic-item-page-field [item]="object"
|
|
||||||
[fields]="['dc.subject']"
|
|
||||||
[separator]="', '"
|
|
||||||
[label]="'item.page.subject'">
|
|
||||||
</ds-generic-item-page-field>
|
|
||||||
<ds-generic-item-page-field [item]="object"
|
|
||||||
[fields]="['dc.identifier.citation']"
|
|
||||||
[label]="'item.page.citation'">
|
|
||||||
</ds-generic-item-page-field>
|
|
||||||
<ds-item-page-uri-field [item]="object"
|
|
||||||
[fields]="['dc.identifier.uri']"
|
|
||||||
[label]="'item.page.uri'">
|
|
||||||
</ds-item-page-uri-field>
|
|
||||||
<ds-item-page-collections [item]="object"></ds-item-page-collections>
|
|
||||||
<ds-item-page-uri-field [item]="object"
|
|
||||||
[fields]="['notify.relation.endorsedBy']"
|
|
||||||
[label]="'item.page.endorsement'">
|
|
||||||
</ds-item-page-uri-field>
|
|
||||||
<ds-item-page-uri-field [item]="object"
|
|
||||||
[fields]="['datacite.relation.isReviewedBy']"
|
|
||||||
[label]="'item.page.review'">
|
|
||||||
</ds-item-page-uri-field>
|
|
||||||
<ds-item-page-uri-field [item]="object"
|
|
||||||
[fields]="['datacite.relation.isSupplementedBy']"
|
|
||||||
[label]="'item.page.dataset'">
|
|
||||||
</ds-item-page-uri-field>
|
|
||||||
<ds-item-page-uri-field [item]="object"
|
|
||||||
[fields]="['datacite.relation.isReferencedBy']"
|
|
||||||
[label]="'item.page.dataset'">
|
|
||||||
</ds-item-page-uri-field>
|
|
||||||
</div>
|
|
||||||
<div class="col-12">
|
|
||||||
<ds-item-secure-full-file-section [item]="object" [itemRequest]="itemRequest">
|
|
||||||
</ds-item-secure-full-file-section>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
@@ -1,213 +0,0 @@
|
|||||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
|
||||||
import {
|
|
||||||
ComponentFixture,
|
|
||||||
TestBed,
|
|
||||||
waitForAsync,
|
|
||||||
} from '@angular/core/testing';
|
|
||||||
import { ActivatedRoute } from '@angular/router';
|
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
|
||||||
import {
|
|
||||||
of as observableOf,
|
|
||||||
of,
|
|
||||||
} from 'rxjs';
|
|
||||||
|
|
||||||
import {
|
|
||||||
APP_CONFIG,
|
|
||||||
APP_DATA_SERVICES_MAP,
|
|
||||||
} from '../../../config/app-config.interface';
|
|
||||||
import { environment } from '../../../environments/environment';
|
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { ItemRequestDataService } from '../../core/data/item-request-data.service';
|
|
||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
|
||||||
import { Item } from '../../core/shared/item.model';
|
|
||||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
|
||||||
import { ITEM_REQUEST } from '../../core/shared/item-request.resource-type';
|
|
||||||
import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
|
|
||||||
import { ErrorComponent } from '../../shared/error/error.component';
|
|
||||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
|
||||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
|
||||||
import { ThemedResultsBackButtonComponent } from '../../shared/results-back-button/themed-results-back-button.component';
|
|
||||||
import { RouterLinkDirectiveStub } from '../../shared/testing/router-link-directive.stub';
|
|
||||||
import { ViewTrackerComponent } from '../../statistics/angulartics/dspace/view-tracker.component';
|
|
||||||
import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component';
|
|
||||||
import { CollectionsComponent } from '../field-components/collections/collections.component';
|
|
||||||
import { ThemedFullFileSectionComponent } from '../full/field-components/file-section/themed-full-file-section.component';
|
|
||||||
import { ThemedMediaViewerComponent } from '../media-viewer/themed-media-viewer.component';
|
|
||||||
import { MiradorViewerComponent } from '../mirador-viewer/mirador-viewer.component';
|
|
||||||
import { ThemedFileSectionComponent } from '../simple/field-components/file-section/themed-file-section.component';
|
|
||||||
import { ThemedMetadataRepresentationListComponent } from '../simple/metadata-representation-list/themed-metadata-representation-list.component';
|
|
||||||
import { ItemVersionsComponent } from '../versions/item-versions.component';
|
|
||||||
import { ItemVersionsNoticeComponent } from '../versions/notice/item-versions-notice.component';
|
|
||||||
import { ItemSecureFileDownloadLinkComponent } from './field-components/file-download-link/item-secure-file-download-link.component';
|
|
||||||
import { ItemSecureFileSectionComponent } from './field-components/file-section/item-secure-file-section.component';
|
|
||||||
import { ItemSecureMediaViewerComponent } from './field-components/media-viewer/item-secure-media-viewer.component';
|
|
||||||
import { ItemAccessByTokenViewComponent } from './item-access-by-token-view.component';
|
|
||||||
|
|
||||||
|
|
||||||
describe('ItemAccessByTokenViewComponent', () => {
|
|
||||||
let authorizationService: AuthorizationDataService;
|
|
||||||
let itemRequestDataService: ItemRequestDataService;
|
|
||||||
let bitstream: Bitstream;
|
|
||||||
let item: Item;
|
|
||||||
let itemRequest: ItemRequest;
|
|
||||||
let component: ItemAccessByTokenViewComponent;
|
|
||||||
let fixture: ComponentFixture<ItemAccessByTokenViewComponent>;
|
|
||||||
let routeStub: any;
|
|
||||||
|
|
||||||
function init() {
|
|
||||||
itemRequestDataService = jasmine.createSpyObj('itemRequestDataService', {
|
|
||||||
canDownload: observableOf(true),
|
|
||||||
});
|
|
||||||
bitstream = Object.assign(new Bitstream(), {
|
|
||||||
uuid: 'bitstreamUuid',
|
|
||||||
});
|
|
||||||
item = Object.assign(new Item(), {
|
|
||||||
uuid: 'itemUuid',
|
|
||||||
metadata: {
|
|
||||||
'dspace.entity.type': [
|
|
||||||
{
|
|
||||||
value: 'Publication',
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
|
||||||
_links: {
|
|
||||||
self: { href: 'obj-selflink' },
|
|
||||||
},
|
|
||||||
});
|
|
||||||
routeStub = {
|
|
||||||
data: observableOf({
|
|
||||||
dso: createSuccessfulRemoteDataObject(item),
|
|
||||||
}),
|
|
||||||
children: [],
|
|
||||||
};
|
|
||||||
|
|
||||||
const mockItemRequest: ItemRequest = Object.assign(new ItemRequest(), {
|
|
||||||
|
|
||||||
});
|
|
||||||
itemRequest = Object.assign(new ItemRequest(),
|
|
||||||
{
|
|
||||||
itemId: item.uuid,
|
|
||||||
bitstreamId: bitstream.uuid,
|
|
||||||
allfiles: false,
|
|
||||||
requestEmail: 'user@name.org',
|
|
||||||
requestName: 'User Name',
|
|
||||||
requestMessage: 'I would like to request a copy',
|
|
||||||
accessPeriod: 3600,
|
|
||||||
decisionDate: new Date().toISOString(),
|
|
||||||
token: 'test-token',
|
|
||||||
type: ITEM_REQUEST,
|
|
||||||
requestDate: new Date().toISOString(),
|
|
||||||
accessToken: 'test-token',
|
|
||||||
expires: null,
|
|
||||||
acceptRequest: true,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTestbed() {
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
imports: [
|
|
||||||
TranslateModule.forRoot(), ItemSecureFileDownloadLinkComponent,
|
|
||||||
RouterLinkDirectiveStub,
|
|
||||||
],
|
|
||||||
providers: [
|
|
||||||
{ provide: AuthorizationDataService, useValue: authorizationService },
|
|
||||||
{ provide: ActivatedRoute, useValue: routeStub },
|
|
||||||
{ provide: RouterLinkDirectiveStub },
|
|
||||||
{ provide: ItemRequestDataService, useValue: itemRequestDataService },
|
|
||||||
provideMockStore(),
|
|
||||||
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
|
|
||||||
{ provide: APP_CONFIG, useValue: environment },
|
|
||||||
],
|
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
|
||||||
}).overrideComponent(ItemAccessByTokenViewComponent, {
|
|
||||||
remove: {
|
|
||||||
imports: [
|
|
||||||
ErrorComponent,
|
|
||||||
ThemedLoadingComponent,
|
|
||||||
ThemedFullFileSectionComponent,
|
|
||||||
CollectionsComponent,
|
|
||||||
ItemVersionsComponent,
|
|
||||||
DsoEditMenuComponent,
|
|
||||||
ItemVersionsNoticeComponent,
|
|
||||||
ViewTrackerComponent,
|
|
||||||
ThemedItemAlertsComponent,
|
|
||||||
ItemSecureFileSectionComponent,
|
|
||||||
MiradorViewerComponent,
|
|
||||||
ThemedFileSectionComponent,
|
|
||||||
ThemedMediaViewerComponent,
|
|
||||||
ThemedMetadataRepresentationListComponent,
|
|
||||||
ThemedResultsBackButtonComponent,
|
|
||||||
ItemSecureMediaViewerComponent,
|
|
||||||
],
|
|
||||||
},
|
|
||||||
}).compileComponents();
|
|
||||||
}
|
|
||||||
|
|
||||||
const mockItem = Object.assign(new Item(), {
|
|
||||||
uuid: 'test-item-uuid',
|
|
||||||
id: 'test-item-id',
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
init();
|
|
||||||
authorizationService = jasmine.createSpyObj('authorizationService', {
|
|
||||||
isAuthorized: observableOf(true),
|
|
||||||
});
|
|
||||||
initTestbed();
|
|
||||||
}));
|
|
||||||
beforeEach(() => {
|
|
||||||
fixture = TestBed.createComponent(ItemAccessByTokenViewComponent);
|
|
||||||
component = fixture.componentInstance;
|
|
||||||
component.object = item;
|
|
||||||
component.itemRequest$ = of(itemRequest);
|
|
||||||
component.itemRequestSubject.next(itemRequest);
|
|
||||||
fixture.detectChanges();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('Component and inputs initialised properly', () => {
|
|
||||||
it('should initialize with valid ItemRequest input', () => {
|
|
||||||
//component.itemRequestSubject.next(itemRequest);
|
|
||||||
component.itemRequest$.subscribe(request => {
|
|
||||||
expect(request).toBeDefined();
|
|
||||||
expect(request.accessPeriod).toBe(3600);
|
|
||||||
expect(request.token).toBe('test-token');
|
|
||||||
expect(request.requestName).toBe('User Name');
|
|
||||||
expect(request.requestEmail).toBe('user@name.org');
|
|
||||||
expect(request.requestMessage).toBe('I would like to request a copy');
|
|
||||||
expect(request.allfiles).toBe(false);
|
|
||||||
expect(request.bitstreamId).toBe(bitstream.uuid);
|
|
||||||
expect(request.acceptRequest).toBe(true);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('getAccessPeriodEndDate', () => {
|
|
||||||
it('should calculate correct end date based on decision date and access period', () => {
|
|
||||||
const testDecisionDate = '2024-01-01T00:00:00Z';
|
|
||||||
const testAccessPeriod = 3600;
|
|
||||||
|
|
||||||
const testRequest = {
|
|
||||||
...itemRequest,
|
|
||||||
decisionDate: testDecisionDate,
|
|
||||||
accessPeriod: testAccessPeriod,
|
|
||||||
};
|
|
||||||
component.itemRequest$ = of(testRequest);
|
|
||||||
component.itemRequestSubject.next(testRequest);
|
|
||||||
const expectedDate = new Date(testDecisionDate);
|
|
||||||
expectedDate.setUTCSeconds(expectedDate.getUTCSeconds() + testAccessPeriod);
|
|
||||||
|
|
||||||
expect(component.getAccessPeriodEndDate()).toEqual(expectedDate);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return undefined when access period is 0', () => {
|
|
||||||
component.itemRequestSubject.next({ ...itemRequest, accessPeriod: 0 });
|
|
||||||
expect(component.getAccessPeriodEndDate()).toBeUndefined();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
@@ -1,128 +0,0 @@
|
|||||||
import {
|
|
||||||
AsyncPipe,
|
|
||||||
KeyValuePipe,
|
|
||||||
NgForOf,
|
|
||||||
NgIf,
|
|
||||||
} from '@angular/common';
|
|
||||||
import {
|
|
||||||
Component,
|
|
||||||
Input,
|
|
||||||
OnInit,
|
|
||||||
} from '@angular/core';
|
|
||||||
import {
|
|
||||||
Router,
|
|
||||||
RouterLink,
|
|
||||||
} from '@angular/router';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
|
||||||
import {
|
|
||||||
BehaviorSubject,
|
|
||||||
Observable,
|
|
||||||
} from 'rxjs';
|
|
||||||
import { filter } from 'rxjs/operators';
|
|
||||||
|
|
||||||
import { RouteService } from '../../core/services/route.service';
|
|
||||||
import { ItemRequest } from '../../core/shared/item-request.model';
|
|
||||||
import { DsoEditMenuComponent } from '../../shared/dso-page/dso-edit-menu/dso-edit-menu.component';
|
|
||||||
import { hasValue } from '../../shared/empty.util';
|
|
||||||
import { ErrorComponent } from '../../shared/error/error.component';
|
|
||||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
|
||||||
import { MetadataFieldWrapperComponent } from '../../shared/metadata-field-wrapper/metadata-field-wrapper.component';
|
|
||||||
import { ThemedResultsBackButtonComponent } from '../../shared/results-back-button/themed-results-back-button.component';
|
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
|
||||||
import { ViewTrackerComponent } from '../../statistics/angulartics/dspace/view-tracker.component';
|
|
||||||
import { ThemedThumbnailComponent } from '../../thumbnail/themed-thumbnail.component';
|
|
||||||
import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component';
|
|
||||||
import { CollectionsComponent } from '../field-components/collections/collections.component';
|
|
||||||
import { ThemedFullFileSectionComponent } from '../full/field-components/file-section/themed-full-file-section.component';
|
|
||||||
import { ThemedMediaViewerComponent } from '../media-viewer/themed-media-viewer.component';
|
|
||||||
import { MiradorViewerComponent } from '../mirador-viewer/mirador-viewer.component';
|
|
||||||
import { ThemedFileSectionComponent } from '../simple/field-components/file-section/themed-file-section.component';
|
|
||||||
import { ItemPageAbstractFieldComponent } from '../simple/field-components/specific-field/abstract/item-page-abstract-field.component';
|
|
||||||
import { ItemPageDateFieldComponent } from '../simple/field-components/specific-field/date/item-page-date-field.component';
|
|
||||||
import { GenericItemPageFieldComponent } from '../simple/field-components/specific-field/generic/generic-item-page-field.component';
|
|
||||||
import { ThemedItemPageTitleFieldComponent } from '../simple/field-components/specific-field/title/themed-item-page-field.component';
|
|
||||||
import { ItemPageUriFieldComponent } from '../simple/field-components/specific-field/uri/item-page-uri-field.component';
|
|
||||||
import { ItemComponent } from '../simple/item-types/shared/item.component';
|
|
||||||
import { ThemedMetadataRepresentationListComponent } from '../simple/metadata-representation-list/themed-metadata-representation-list.component';
|
|
||||||
import { ItemVersionsComponent } from '../versions/item-versions.component';
|
|
||||||
import { ItemVersionsNoticeComponent } from '../versions/notice/item-versions-notice.component';
|
|
||||||
import { ItemSecureFileSectionComponent } from './field-components/file-section/item-secure-file-section.component';
|
|
||||||
import { ItemSecureMediaViewerComponent } from './field-components/media-viewer/item-secure-media-viewer.component';
|
|
||||||
|
|
||||||
@Component({
|
|
||||||
selector: 'ds-item-access-by-token-view',
|
|
||||||
styleUrls: ['./item-access-by-token-view.component.scss'],
|
|
||||||
templateUrl: './item-access-by-token-view.component.html',
|
|
||||||
standalone: true,
|
|
||||||
imports: [
|
|
||||||
ErrorComponent,
|
|
||||||
ThemedLoadingComponent,
|
|
||||||
TranslateModule,
|
|
||||||
ThemedFullFileSectionComponent,
|
|
||||||
CollectionsComponent,
|
|
||||||
ItemVersionsComponent,
|
|
||||||
NgIf,
|
|
||||||
NgForOf,
|
|
||||||
AsyncPipe,
|
|
||||||
KeyValuePipe,
|
|
||||||
RouterLink,
|
|
||||||
ThemedItemPageTitleFieldComponent,
|
|
||||||
DsoEditMenuComponent,
|
|
||||||
ItemVersionsNoticeComponent,
|
|
||||||
ViewTrackerComponent,
|
|
||||||
ThemedItemAlertsComponent,
|
|
||||||
VarDirective,
|
|
||||||
ItemSecureFileSectionComponent,
|
|
||||||
GenericItemPageFieldComponent,
|
|
||||||
ItemPageAbstractFieldComponent,
|
|
||||||
ItemPageDateFieldComponent,
|
|
||||||
ItemPageUriFieldComponent,
|
|
||||||
MetadataFieldWrapperComponent,
|
|
||||||
MiradorViewerComponent,
|
|
||||||
ThemedFileSectionComponent,
|
|
||||||
ThemedMediaViewerComponent,
|
|
||||||
ThemedMetadataRepresentationListComponent,
|
|
||||||
ThemedResultsBackButtonComponent,
|
|
||||||
ThemedThumbnailComponent,
|
|
||||||
ItemSecureMediaViewerComponent,
|
|
||||||
//ItemPageTitleFieldComponent,
|
|
||||||
//ThumbnailComponent,
|
|
||||||
//MetadataRepresentationListComponent,
|
|
||||||
],
|
|
||||||
})
|
|
||||||
export class ItemAccessByTokenViewComponent extends ItemComponent implements OnInit {
|
|
||||||
|
|
||||||
@Input() itemRequest$: Observable<ItemRequest>;
|
|
||||||
itemRequestSubject = new BehaviorSubject<ItemRequest>(null);
|
|
||||||
expiryDate: Date;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected routeService: RouteService,
|
|
||||||
protected router: Router,
|
|
||||||
) {
|
|
||||||
super(routeService, router);
|
|
||||||
}
|
|
||||||
|
|
||||||
protected readonly hasValue = hasValue;
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.itemRequest$.pipe(
|
|
||||||
filter(request => hasValue(request)),
|
|
||||||
).subscribe(request => {
|
|
||||||
this.itemRequestSubject.next(request);
|
|
||||||
super.ngOnInit();
|
|
||||||
});
|
|
||||||
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
getAccessPeriodEndDate(): Date {
|
|
||||||
const request = this.itemRequestSubject.getValue();
|
|
||||||
// Set expiry, if not 0
|
|
||||||
if (hasValue(request) && request.accessPeriod > 0) {
|
|
||||||
const date = new Date(request.decisionDate);
|
|
||||||
date.setUTCSeconds(date.getUTCSeconds() + request.accessPeriod);
|
|
||||||
return date;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
@@ -1,6 +1,7 @@
|
|||||||
import { Route } from '@angular/router';
|
import { Route } from '@angular/router';
|
||||||
|
|
||||||
import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
|
import { REQUEST_COPY_MODULE_PATH } from '../app-routing-paths';
|
||||||
|
import { accessTokenResolver } from '../core/auth/access-token.resolver';
|
||||||
import { authenticatedGuard } from '../core/auth/authenticated.guard';
|
import { authenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
import { itemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
|
import { itemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
|
||||||
import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
|
import { dsoEditMenuResolver } from '../shared/dso-page/dso-edit-menu.resolver';
|
||||||
@@ -21,13 +22,13 @@ import { orcidPageGuard } from './orcid-page/orcid-page.guard';
|
|||||||
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
import { ThemedItemPageComponent } from './simple/themed-item-page.component';
|
||||||
import { versionResolver } from './version-page/version.resolver';
|
import { versionResolver } from './version-page/version.resolver';
|
||||||
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
import { VersionPageComponent } from './version-page/version-page/version-page.component';
|
||||||
import { accessTokenResolver } from '../core/auth/access-token.resolver';
|
|
||||||
|
|
||||||
export const ROUTES: Route[] = [
|
export const ROUTES: Route[] = [
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
resolve: {
|
resolve: {
|
||||||
dso: itemPageResolver,
|
dso: itemPageResolver,
|
||||||
|
itemRequest: accessTokenResolver,
|
||||||
breadcrumb: itemBreadcrumbResolver,
|
breadcrumb: itemBreadcrumbResolver,
|
||||||
},
|
},
|
||||||
runGuardsAndResolvers: 'always',
|
runGuardsAndResolvers: 'always',
|
||||||
|
@@ -15,6 +15,7 @@ import { Observable } from 'rxjs';
|
|||||||
|
|
||||||
import { AuthService } from '../../../core/auth/auth.service';
|
import { AuthService } from '../../../core/auth/auth.service';
|
||||||
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component render an image gallery for the image viewer
|
* This component render an image gallery for the image viewer
|
||||||
@@ -99,7 +100,7 @@ export class MediaViewerImageComponent implements OnChanges, OnInit {
|
|||||||
medium: image.thumbnail
|
medium: image.thumbnail
|
||||||
? image.thumbnail
|
? image.thumbnail
|
||||||
: this.thumbnailPlaceholder,
|
: this.thumbnailPlaceholder,
|
||||||
big: image.bitstream._links.content.href,
|
big: image.bitstream._links.content.href + (hasValue(image.accessToken) ? ('?accessToken=' + image.accessToken) : ''),
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
<video
|
<video
|
||||||
crossorigin="anonymous"
|
crossorigin="anonymous"
|
||||||
[src]="medias[currentIndex].bitstream._links.content.href"
|
[src]="constructHref(medias[currentIndex].bitstream._links.content.href)"
|
||||||
id="singleVideo"
|
id="singleVideo"
|
||||||
[poster]="
|
[poster]="
|
||||||
medias[currentIndex].thumbnail ||
|
medias[currentIndex].thumbnail ||
|
||||||
|
@@ -10,6 +10,7 @@ import { Bitstream } from 'src/app/core/shared/bitstream.model';
|
|||||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
import { MediaViewerItem } from '../../../core/shared/media-viewer-item.model';
|
||||||
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { CaptionInfo } from './caption-info';
|
import { CaptionInfo } from './caption-info';
|
||||||
import { languageHelper } from './language-helper';
|
import { languageHelper } from './language-helper';
|
||||||
|
|
||||||
@@ -64,7 +65,7 @@ export class MediaViewerVideoComponent {
|
|||||||
for (const media of filteredCapMedias) {
|
for (const media of filteredCapMedias) {
|
||||||
const srclang: string = media.name.slice(-6, -4).toLowerCase();
|
const srclang: string = media.name.slice(-6, -4).toLowerCase();
|
||||||
capInfos.push(new CaptionInfo(
|
capInfos.push(new CaptionInfo(
|
||||||
media._links.content.href,
|
this.constructHref(media._links.content.href),
|
||||||
srclang,
|
srclang,
|
||||||
languageHelper[srclang],
|
languageHelper[srclang],
|
||||||
));
|
));
|
||||||
@@ -93,4 +94,15 @@ export class MediaViewerVideoComponent {
|
|||||||
prevMedia() {
|
prevMedia() {
|
||||||
this.currentIndex--;
|
this.currentIndex--;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Construct a URL with Request-a-Copy access token appended, if present
|
||||||
|
* @param baseHref
|
||||||
|
*/
|
||||||
|
constructHref(baseHref) {
|
||||||
|
if (hasValue(this.medias) && this.medias.length >= 1 && hasValue(this.medias[0].accessToken)) {
|
||||||
|
return baseHref + '?accessToken=' + this.medias[0].accessToken;
|
||||||
|
}
|
||||||
|
return baseHref;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -25,6 +25,7 @@ import { RemoteData } from '../../core/data/remote-data';
|
|||||||
import { Bitstream } from '../../core/shared/bitstream.model';
|
import { Bitstream } from '../../core/shared/bitstream.model';
|
||||||
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
import { BitstreamFormat } from '../../core/shared/bitstream-format.model';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { ItemWithSupplementaryData } from '../../core/shared/item-with-supplementary-data.model';
|
||||||
import { MediaViewerItem } from '../../core/shared/media-viewer-item.model';
|
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';
|
||||||
@@ -120,6 +121,7 @@ export class MediaViewerComponent implements OnDestroy, OnInit {
|
|||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -160,6 +162,17 @@ export class MediaViewerComponent implements OnDestroy, OnInit {
|
|||||||
mediaItem.format = format.mimetype.split('/')[0];
|
mediaItem.format = format.mimetype.split('/')[0];
|
||||||
mediaItem.mimetype = format.mimetype;
|
mediaItem.mimetype = format.mimetype;
|
||||||
mediaItem.thumbnail = thumbnail ? thumbnail._links.content.href : null;
|
mediaItem.thumbnail = thumbnail ? thumbnail._links.content.href : null;
|
||||||
|
mediaItem.accessToken = this.accessToken;
|
||||||
return mediaItem;
|
return mediaItem;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get access token, if this is accessed via a Request-a-Copy link
|
||||||
|
*/
|
||||||
|
get accessToken() {
|
||||||
|
if (this.item instanceof ItemWithSupplementaryData && hasValue(this.item.itemRequest)) {
|
||||||
|
return this.item.itemRequest.accessToken;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -21,6 +21,7 @@ import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
|||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
import { Bitstream } from '../../../../core/shared/bitstream.model';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
|
import { ItemWithSupplementaryData } from '../../../../core/shared/item-with-supplementary-data.model';
|
||||||
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
import { getFirstCompletedRemoteData } from '../../../../core/shared/operators';
|
||||||
import { hasValue } from '../../../../shared/empty.util';
|
import { hasValue } from '../../../../shared/empty.util';
|
||||||
import { ThemedFileDownloadLinkComponent } from '../../../../shared/file-download-link/themed-file-download-link.component';
|
import { ThemedFileDownloadLinkComponent } from '../../../../shared/file-download-link/themed-file-download-link.component';
|
||||||
@@ -30,6 +31,7 @@ import { NotificationsService } from '../../../../shared/notifications/notificat
|
|||||||
import { FileSizePipe } from '../../../../shared/utils/file-size-pipe';
|
import { FileSizePipe } from '../../../../shared/utils/file-size-pipe';
|
||||||
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../../shared/utils/follow-link-config.model';
|
||||||
import { VarDirective } from '../../../../shared/utils/var.directive';
|
import { VarDirective } from '../../../../shared/utils/var.directive';
|
||||||
|
import { ItemSecureFileDownloadLinkComponent } from '../../../access-by-token/field-components/file-download-link/item-secure-file-download-link.component';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders the file section of the item
|
* This component renders the file section of the item
|
||||||
@@ -46,6 +48,7 @@ import { VarDirective } from '../../../../shared/utils/var.directive';
|
|||||||
TranslateModule,
|
TranslateModule,
|
||||||
FileSizePipe,
|
FileSizePipe,
|
||||||
VarDirective,
|
VarDirective,
|
||||||
|
ItemSecureFileDownloadLinkComponent,
|
||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
})
|
})
|
||||||
@@ -123,4 +126,8 @@ export class FileSectionComponent implements OnInit {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
hasAccessToken(): boolean {
|
||||||
|
return (this.item instanceof ItemWithSupplementaryData && hasValue(this.item.itemRequest));
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -39,10 +39,15 @@ import {
|
|||||||
} from '../../core/services/link-head.service';
|
} from '../../core/services/link-head.service';
|
||||||
import { ServerResponseService } from '../../core/services/server-response.service';
|
import { ServerResponseService } from '../../core/services/server-response.service';
|
||||||
import { Item } from '../../core/shared/item.model';
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { ItemRequest } from '../../core/shared/item-request.model';
|
||||||
|
import { ItemWithSupplementaryData } from '../../core/shared/item-with-supplementary-data.model';
|
||||||
import { getAllSucceededRemoteDataPayload } from '../../core/shared/operators';
|
import { getAllSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||||
import { fadeInOut } from '../../shared/animations/fade';
|
import { fadeInOut } from '../../shared/animations/fade';
|
||||||
import { isNotEmpty } from '../../shared/empty.util';
|
import {
|
||||||
|
hasValue,
|
||||||
|
isNotEmpty,
|
||||||
|
} from '../../shared/empty.util';
|
||||||
import { ErrorComponent } from '../../shared/error/error.component';
|
import { ErrorComponent } from '../../shared/error/error.component';
|
||||||
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
|
||||||
import { ListableObjectComponentLoaderComponent } from '../../shared/object-collection/shared/listable-object/listable-object-component-loader.component';
|
import { ListableObjectComponentLoaderComponent } from '../../shared/object-collection/shared/listable-object/listable-object-component-loader.component';
|
||||||
@@ -94,6 +99,8 @@ export class ItemPageComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
itemRD$: Observable<RemoteData<Item>>;
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
|
item$: Observable<Item>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The view-mode we're currently on
|
* The view-mode we're currently on
|
||||||
*/
|
*/
|
||||||
@@ -123,6 +130,8 @@ export class ItemPageComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
coarRestApiUrls: string[] = [];
|
coarRestApiUrls: string[] = [];
|
||||||
|
|
||||||
|
protected readonly hasValue = hasValue;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected router: Router,
|
protected router: Router,
|
||||||
@@ -141,8 +150,20 @@ export class ItemPageComponent implements OnInit, OnDestroy {
|
|||||||
* Initialize instance variables
|
* Initialize instance variables
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
// Get item request
|
||||||
this.itemRD$ = this.route.data.pipe(
|
this.itemRD$ = this.route.data.pipe(
|
||||||
map((data) => data.dso as RemoteData<Item>),
|
map((data) => {
|
||||||
|
const itemRD = data.dso;
|
||||||
|
// If the item has a valid itemRequest, add it to the item and set the itemRD payload to the
|
||||||
|
// modified ItemWithSupplementaryData object
|
||||||
|
if (hasValue(data.itemRequest)) {
|
||||||
|
const itemRequest = data.itemRequest;
|
||||||
|
itemRD.payload = Object.assign(new ItemWithSupplementaryData(itemRequest), itemRD.payload);
|
||||||
|
}
|
||||||
|
// Return itemRD
|
||||||
|
return itemRD;
|
||||||
|
},
|
||||||
|
),
|
||||||
);
|
);
|
||||||
this.itemPageRoute$ = this.itemRD$.pipe(
|
this.itemPageRoute$ = this.itemRD$.pipe(
|
||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
@@ -236,6 +257,30 @@ export class ItemPageComponent implements OnInit, OnDestroy {
|
|||||||
return links;
|
return links;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to return the item request from an item for use in templates
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
getAccessByToken(item): ItemRequest {
|
||||||
|
if (item instanceof ItemWithSupplementaryData) {
|
||||||
|
return item.itemRequest;
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Helper function to return the expiry date from an item request for use in templates, alerts, etc.
|
||||||
|
* @param itemRequest
|
||||||
|
*/
|
||||||
|
getAccessPeriodEndDate(itemRequest): Date {
|
||||||
|
// Set expiry, if not 0
|
||||||
|
if (hasValue(itemRequest) && itemRequest.accessPeriod > 0) {
|
||||||
|
const date = new Date(itemRequest.decisionDate);
|
||||||
|
date.setUTCSeconds(date.getUTCSeconds() + itemRequest.accessPeriod);
|
||||||
|
return date;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.signpostingLinks.forEach((link: SignpostingLink) => {
|
this.signpostingLinks.forEach((link: SignpostingLink) => {
|
||||||
this.linkHeadService.removeTag(`href='${link.href}'`);
|
this.linkHeadService.removeTag(`href='${link.href}'`);
|
||||||
@@ -244,4 +289,5 @@ export class ItemPageComponent implements OnInit, OnDestroy {
|
|||||||
this.linkHeadService.removeTag(`href='${link.href}'`);
|
this.linkHeadService.removeTag(`href='${link.href}'`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ import { CommonModule } from '@angular/common';
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
|
import { ItemSecureFileDownloadLinkComponent } from '../../../../../../../app/item-page/access-by-token/field-components/file-download-link/item-secure-file-download-link.component';
|
||||||
import { FileSectionComponent as BaseComponent } from '../../../../../../../app/item-page/simple/field-components/file-section/file-section.component';
|
import { FileSectionComponent as BaseComponent } from '../../../../../../../app/item-page/simple/field-components/file-section/file-section.component';
|
||||||
import { slideSidebarPadding } from '../../../../../../../app/shared/animations/slide';
|
import { slideSidebarPadding } from '../../../../../../../app/shared/animations/slide';
|
||||||
import { ThemedFileDownloadLinkComponent } from '../../../../../../../app/shared/file-download-link/themed-file-download-link.component';
|
import { ThemedFileDownloadLinkComponent } from '../../../../../../../app/shared/file-download-link/themed-file-download-link.component';
|
||||||
@@ -24,6 +25,7 @@ import { VarDirective } from '../../../../../../../app/shared/utils/var.directiv
|
|||||||
TranslateModule,
|
TranslateModule,
|
||||||
FileSizePipe,
|
FileSizePipe,
|
||||||
VarDirective,
|
VarDirective,
|
||||||
|
ItemSecureFileDownloadLinkComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class FileSectionComponent extends BaseComponent {
|
export class FileSectionComponent extends BaseComponent {
|
||||||
|
@@ -8,6 +8,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import { AlertComponent } from 'src/app/shared/alert/alert.component';
|
import { AlertComponent } from 'src/app/shared/alert/alert.component';
|
||||||
import { GoogleRecaptchaComponent } from 'src/app/shared/google-recaptcha/google-recaptcha.component';
|
import { GoogleRecaptchaComponent } from 'src/app/shared/google-recaptcha/google-recaptcha.component';
|
||||||
|
|
||||||
|
import { AltchaCaptchaComponent } from '../../../../app/item-page/bitstreams/request-a-copy/altcha-captcha.component';
|
||||||
import { RegisterEmailFormComponent as BaseComponent } from '../../../../app/register-email-form/register-email-form.component';
|
import { RegisterEmailFormComponent as BaseComponent } from '../../../../app/register-email-form/register-email-form.component';
|
||||||
import { BtnDisabledDirective } from '../../../../app/shared/btn-disabled.directive';
|
import { BtnDisabledDirective } from '../../../../app/shared/btn-disabled.directive';
|
||||||
|
|
||||||
|
@@ -15,8 +15,6 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
import { ScrollToModule } from '@nicky-lenaers/ngx-scroll-to';
|
||||||
|
|
||||||
import { ItemSecureFileDownloadLinkComponent } from '../../app/item-page/access-by-token/field-components/file-download-link/item-secure-file-download-link.component';
|
import { ItemSecureFileDownloadLinkComponent } from '../../app/item-page/access-by-token/field-components/file-download-link/item-secure-file-download-link.component';
|
||||||
import { ItemSecureFileSectionComponent } from '../../app/item-page/access-by-token/field-components/file-section/item-secure-file-section.component';
|
|
||||||
import { ItemAccessByTokenPageComponent } from '../../app/item-page/access-by-token/item-access-by-token-page.component';
|
|
||||||
import { RootModule } from '../../app/root.module';
|
import { RootModule } from '../../app/root.module';
|
||||||
import { SearchResultsSkeletonComponent } from '../../app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component';
|
import { SearchResultsSkeletonComponent } from '../../app/shared/search/search-results/search-results-skeleton/search-results-skeleton.component';
|
||||||
import { MetadataImportPageComponent } from './app/admin/admin-import-metadata-page/metadata-import-page.component';
|
import { MetadataImportPageComponent } from './app/admin/admin-import-metadata-page/metadata-import-page.component';
|
||||||
@@ -136,8 +134,6 @@ const DECLARATIONS = [
|
|||||||
CollectionPageComponent,
|
CollectionPageComponent,
|
||||||
ItemPageComponent,
|
ItemPageComponent,
|
||||||
FullItemPageComponent,
|
FullItemPageComponent,
|
||||||
ItemAccessByTokenPageComponent,
|
|
||||||
ItemSecureFileSectionComponent,
|
|
||||||
ItemSecureFileDownloadLinkComponent,
|
ItemSecureFileDownloadLinkComponent,
|
||||||
LoginPageComponent,
|
LoginPageComponent,
|
||||||
LogoutPageComponent,
|
LogoutPageComponent,
|
||||||
|
Reference in New Issue
Block a user