mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-14 13:33:03 +00:00
Request-a-copy: Error handling (review feedback)
* New accessExpired flag can be checked along with acceptRequest, so expired or not-granted links will show an error and draw 'normal' download links instead of doing a hard 4xx redirect * Old ItemWithSupplementaryData component removed * Notifications moved to sub-component of item page and display a warning on valid access token, or a danger alert if expired or not-granted, with an appropriate error message * Date formatted in messages as yyyy-MM-dd
This commit is contained in:
@@ -4,11 +4,15 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { tap } from 'rxjs/operators';
|
import {
|
||||||
|
map,
|
||||||
|
tap,
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { getForbiddenRoute } from '../../app-routing-paths';
|
import { getForbiddenRoute } from '../../app-routing-paths';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { ItemRequestDataService } from '../data/item-request-data.service';
|
import { ItemRequestDataService } from '../data/item-request-data.service';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
import { redirectOn4xx } from '../shared/authorized.operators';
|
import { redirectOn4xx } from '../shared/authorized.operators';
|
||||||
import { ItemRequest } from '../shared/item-request.model';
|
import { ItemRequest } from '../shared/item-request.model';
|
||||||
import {
|
import {
|
||||||
@@ -43,6 +47,7 @@ export const accessTokenResolver: ResolveFn<ItemRequest> = (
|
|||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
// Handle authorization errors, not found errors and forbidden errors as normal
|
// Handle authorization errors, not found errors and forbidden errors as normal
|
||||||
redirectOn4xx(router, authService),
|
redirectOn4xx(router, authService),
|
||||||
|
map((rd: RemoteData<ItemRequest>) => rd),
|
||||||
// Get payload of the item request
|
// Get payload of the item request
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
tap(request => {
|
tap(request => {
|
||||||
|
@@ -91,6 +91,8 @@ export class ItemRequest implements CacheableObject {
|
|||||||
@autoserialize
|
@autoserialize
|
||||||
accessExpiry: string;
|
accessExpiry: string;
|
||||||
|
|
||||||
|
@autoserialize
|
||||||
|
accessExpired: boolean;
|
||||||
/**
|
/**
|
||||||
* The {@link HALLink}s for this ItemRequest
|
* The {@link HALLink}s for this ItemRequest
|
||||||
*/
|
*/
|
||||||
|
@@ -1,20 +0,0 @@
|
|||||||
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;
|
|
||||||
}
|
|
||||||
}
|
|
@@ -6,6 +6,7 @@ import {
|
|||||||
} from '@angular/core/testing';
|
} from '@angular/core/testing';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import {
|
import {
|
||||||
TranslateLoader,
|
TranslateLoader,
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
@@ -22,6 +23,7 @@ import { MockBitstreamFormat1 } from '../../shared/mocks/item.mock';
|
|||||||
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||||
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
import { ThemeService } from '../../shared/theme-support/theme.service';
|
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||||
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
import { FileSizePipe } from '../../shared/utils/file-size-pipe';
|
||||||
@@ -91,6 +93,7 @@ describe('MediaViewerComponent', () => {
|
|||||||
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
{ provide: BitstreamDataService, useValue: bitstreamDataService },
|
||||||
{ provide: ThemeService, useValue: getMockThemeService() },
|
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||||
{ provide: AuthService, useValue: new AuthServiceMock() },
|
{ provide: AuthService, useValue: new AuthServiceMock() },
|
||||||
|
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
@@ -6,6 +6,7 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
@@ -25,7 +26,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 { ItemRequest } from '../../core/shared/item-request.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';
|
||||||
@@ -71,9 +72,12 @@ export class MediaViewerComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
subs: Subscription[] = [];
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
|
itemRequest: ItemRequest;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected bitstreamDataService: BitstreamDataService,
|
protected bitstreamDataService: BitstreamDataService,
|
||||||
protected changeDetectorRef: ChangeDetectorRef,
|
protected changeDetectorRef: ChangeDetectorRef,
|
||||||
|
protected route: ActivatedRoute,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -85,6 +89,7 @@ export class MediaViewerComponent implements OnDestroy, OnInit {
|
|||||||
* This method loads all the Bitstreams and Thumbnails and converts it to {@link MediaViewerItem}s
|
* This method loads all the Bitstreams and Thumbnails and converts it to {@link MediaViewerItem}s
|
||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
this.itemRequest = this.route.snapshot.data.itemRequest;
|
||||||
const types: string[] = [
|
const types: string[] = [
|
||||||
...(this.mediaOptions.image ? ['image'] : []),
|
...(this.mediaOptions.image ? ['image'] : []),
|
||||||
...(this.mediaOptions.video ? ['audio', 'video'] : []),
|
...(this.mediaOptions.video ? ['audio', 'video'] : []),
|
||||||
@@ -170,9 +175,10 @@ export class MediaViewerComponent implements OnDestroy, OnInit {
|
|||||||
* Get access token, if this is accessed via a Request-a-Copy link
|
* Get access token, if this is accessed via a Request-a-Copy link
|
||||||
*/
|
*/
|
||||||
get accessToken() {
|
get accessToken() {
|
||||||
if (this.item instanceof ItemWithSupplementaryData && hasValue(this.item.itemRequest)) {
|
if (hasValue(this.itemRequest) && this.itemRequest.accessToken && !this.itemRequest.accessExpired) {
|
||||||
return this.item.itemRequest.accessToken;
|
return this.itemRequest.accessToken;
|
||||||
}
|
}
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,25 @@
|
|||||||
|
<ng-container *ngVar="(itemRequest$ | async) as itemRequest">
|
||||||
|
@if (hasValue(itemRequest)) {
|
||||||
|
@if (!itemRequest.acceptRequest) {
|
||||||
|
<!-- The request has NOT been accepted, display an error -->
|
||||||
|
<div class="alert alert-danger wb-100 mb-2 request-a-copy-access-success">
|
||||||
|
<p><span role="img" class="request-a-copy-access-error-icon" [attr.aria-label]="'bitstream-request-a-copy.access-by-token.alt-text' | translate"><i class="fa-solid fa-lock"></i></span>{{'bitstream-request-a-copy.access-by-token.not-granted' | translate}}</p>
|
||||||
|
<p>{{'bitstream-request-a-copy.access-by-token.re-request' | translate}}</p>
|
||||||
|
</div>
|
||||||
|
} @else if (itemRequest.accessExpired) {
|
||||||
|
<!-- The request is accepted, but the access period has expired, display an error -->
|
||||||
|
<div class="alert alert-danger wb-100 mb-2 request-a-copy-access-expired">
|
||||||
|
<p><span role="img" class="request-a-copy-access-error-icon" [attr.aria-label]="'bitstream-request-a-copy.access-by-token.alt-text' | translate"><i class="fa-solid fa-lock"></i></span>{{'bitstream-request-a-copy.access-by-token.expired' | translate}} {{ formatDate(itemRequest.accessExpiry) }}</p>
|
||||||
|
<p>{{'bitstream-request-a-copy.access-by-token.re-request' | translate}}</p>
|
||||||
|
</div>
|
||||||
|
} @else {
|
||||||
|
<div class="alert alert-warning wb-100 mb-2 request-a-copy-access-denied">
|
||||||
|
<p><span role="img" class="request-a-copy-access-icon" [attr.aria-label]="'bitstream-request-a-copy.access-by-token.alt-text' | translate"><i class="fa-solid fa-lock-open"></i></span>{{'bitstream-request-a-copy.access-by-token.warning' | translate}}</p>
|
||||||
|
<!-- Only show the expiry date if it's not null, and doesn't start with the "FOREVER" year -->
|
||||||
|
@if (hasValue(itemRequest.accessExpiry) && !itemRequest.accessExpiry.startsWith('+294276')) {
|
||||||
|
<p>{{ 'bitstream-request-a-copy.access-by-token.expiry-label' | translate }} {{ formatDate(itemRequest.accessExpiry) }}</p>
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</ng-container>
|
@@ -0,0 +1,7 @@
|
|||||||
|
.request-a-copy-access-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
color: var(--bs-success);
|
||||||
|
}
|
||||||
|
.request-a-copy-access-error-icon {
|
||||||
|
margin-right: 4px;
|
||||||
|
}
|
@@ -0,0 +1,114 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import {
|
||||||
|
ComponentFixture,
|
||||||
|
TestBed,
|
||||||
|
} from '@angular/core/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { BehaviorSubject } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { SplitPipe } from 'src/app/shared/utils/split.pipe';
|
||||||
|
|
||||||
|
import { APP_DATA_SERVICES_MAP } from '../../../../config/app-config.interface';
|
||||||
|
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
|
||||||
|
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||||
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
|
import { HALEndpointService } from '../../../core/shared/hal-endpoint.service';
|
||||||
|
import { ItemRequest } from '../../../core/shared/item-request.model';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||||
|
import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub';
|
||||||
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
|
import { AccessByTokenNotificationComponent } from './access-by-token-notification.component';
|
||||||
|
|
||||||
|
describe('AccessByTokenNotificationComponent', () => {
|
||||||
|
let component: AccessByTokenNotificationComponent;
|
||||||
|
let fixture: ComponentFixture<AccessByTokenNotificationComponent>;
|
||||||
|
let activatedRouteStub: ActivatedRouteStub;
|
||||||
|
let itemRequestSubject: BehaviorSubject<ItemRequest>;
|
||||||
|
|
||||||
|
const createItemRequest = (acceptRequest: boolean, accessExpired: boolean, accessExpiry?: string): ItemRequest => {
|
||||||
|
const itemRequest = new ItemRequest();
|
||||||
|
itemRequest.acceptRequest = acceptRequest;
|
||||||
|
itemRequest.accessExpired = accessExpired;
|
||||||
|
itemRequest.accessExpiry = accessExpiry;
|
||||||
|
return itemRequest;
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
itemRequestSubject = new BehaviorSubject<ItemRequest>(null);
|
||||||
|
activatedRouteStub = new ActivatedRouteStub({}, { itemRequest: null });
|
||||||
|
(activatedRouteStub as any).data = itemRequestSubject.asObservable().pipe(
|
||||||
|
map(itemRequest => ({ itemRequest })),
|
||||||
|
);
|
||||||
|
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
CommonModule,
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
AccessByTokenNotificationComponent,
|
||||||
|
SplitPipe,
|
||||||
|
VarDirective,
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: APP_DATA_SERVICES_MAP, useValue: {} },
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
{ provide: RequestService, useValue: {} },
|
||||||
|
{ provide: NotificationsService, useValue: {} },
|
||||||
|
{ provide: HALEndpointService, useValue: new HALEndpointServiceStub('test') },
|
||||||
|
ObjectCacheService,
|
||||||
|
RemoteDataBuildService,
|
||||||
|
provideMockStore({}),
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(AccessByTokenNotificationComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not display any alert when no itemRequest is present', () => {
|
||||||
|
itemRequestSubject.next(null);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
const alertElements = fixture.debugElement.queryAll(By.css('.alert'));
|
||||||
|
expect(alertElements.length).toBe(0);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display an error alert when request has not been accepted', () => {
|
||||||
|
// Set up a request that has not been accepted
|
||||||
|
const itemRequest = createItemRequest(false, false);
|
||||||
|
itemRequestSubject.next(itemRequest);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// Check for the error alert with the correct class
|
||||||
|
const alertElement = fixture.debugElement.query(By.css('.alert.alert-danger.request-a-copy-access-success'));
|
||||||
|
expect(alertElement).toBeTruthy();
|
||||||
|
|
||||||
|
// Verify the content includes the lock icon
|
||||||
|
const lockIcon = alertElement.query(By.css('.fa-lock'));
|
||||||
|
expect(lockIcon).toBeTruthy();
|
||||||
|
|
||||||
|
// Verify the text content mentions re-requesting
|
||||||
|
const paragraphs = alertElement.queryAll(By.css('p'));
|
||||||
|
expect(paragraphs.length).toBe(2);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display an expired access alert when access period has expired', () => {
|
||||||
|
// Set up a request that has been accepted but expired
|
||||||
|
const itemRequest = createItemRequest(true, true, '2023-01-01');
|
||||||
|
itemRequestSubject.next(itemRequest);
|
||||||
|
fixture.detectChanges();
|
||||||
|
|
||||||
|
// Check for the expired alert with the correct class
|
||||||
|
const alertElement = fixture.debugElement.query(By.css('.alert.alert-danger.request-a-copy-access-expired'));
|
||||||
|
expect(alertElement).toBeTruthy();
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,56 @@
|
|||||||
|
import { AsyncPipe } from '@angular/common';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
|
import { ActivatedRoute } from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { ItemRequest } from '../../../core/shared/item-request.model';
|
||||||
|
import {
|
||||||
|
dateToString,
|
||||||
|
stringToNgbDateStruct,
|
||||||
|
} from '../../../shared/date.util';
|
||||||
|
import {
|
||||||
|
hasValue,
|
||||||
|
isNotEmpty,
|
||||||
|
} from '../../../shared/empty.util';
|
||||||
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-access-by-token-notification',
|
||||||
|
templateUrl: './access-by-token-notification.component.html',
|
||||||
|
styleUrls: ['./access-by-token-notification.component.scss'],
|
||||||
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
TranslateModule,
|
||||||
|
VarDirective,
|
||||||
|
],
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
export class AccessByTokenNotificationComponent implements OnInit {
|
||||||
|
|
||||||
|
itemRequest$: Observable<ItemRequest>;
|
||||||
|
protected readonly hasValue = hasValue;
|
||||||
|
|
||||||
|
constructor(protected route: ActivatedRoute) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit() {
|
||||||
|
this.itemRequest$ = this.route.data.pipe(
|
||||||
|
map((data) => data.itemRequest as ItemRequest),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Returns a date in simplified format (YYYY-MM-DD).
|
||||||
|
*
|
||||||
|
* @param date
|
||||||
|
* @return a string with formatted date
|
||||||
|
*/
|
||||||
|
formatDate(date: string): string {
|
||||||
|
return isNotEmpty(date) ? dateToString(stringToNgbDateStruct(date)) : '';
|
||||||
|
}
|
||||||
|
}
|
@@ -3,18 +3,9 @@
|
|||||||
<div class="item-page" @fadeInOut>
|
<div class="item-page" @fadeInOut>
|
||||||
@if (itemRD?.payload; as item) {
|
@if (itemRD?.payload; as item) {
|
||||||
<div>
|
<div>
|
||||||
<ng-container *ngVar="(itemRequest$ | async) as itemRequest">
|
|
||||||
@if (hasValue(itemRequest)) {
|
|
||||||
<div class="alert alert-warning wb-100 mb-2">
|
|
||||||
<p><span role="img" class="request-a-copy-access-icon" [attr.aria-label]="'bitstream-request-a-copy.access-by-token.alt-text' | translate"><i class="fa-solid fa-lock-open"></i></span>{{'bitstream-request-a-copy.access-by-token.warning' | translate}}</p>
|
|
||||||
<!-- Only show the expiry date if it's not null, and doesn't start with the "FOREVER" year -->
|
|
||||||
@if (hasValue(itemRequest.accessExpiry) && !itemRequest.accessExpiry.startsWith('+294276')) {
|
|
||||||
<p>{{ 'bitstream-request-a-copy.access-by-token.expiry-label' | translate }} {{ itemRequest.accessExpiry }}</p>
|
|
||||||
}
|
|
||||||
</div>
|
|
||||||
}
|
|
||||||
</ng-container>
|
|
||||||
<ds-item-alerts [item]="item"></ds-item-alerts>
|
<ds-item-alerts [item]="item"></ds-item-alerts>
|
||||||
|
<ds-access-by-token-notification></ds-access-by-token-notification>
|
||||||
<ds-qa-event-notification [item]="item"></ds-qa-event-notification>
|
<ds-qa-event-notification [item]="item"></ds-qa-event-notification>
|
||||||
<ds-notify-requests-status [itemUuid]="item.uuid"></ds-notify-requests-status>
|
<ds-notify-requests-status [itemUuid]="item.uuid"></ds-notify-requests-status>
|
||||||
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
|
||||||
|
@@ -4,7 +4,4 @@
|
|||||||
max-width: none;
|
max-width: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
.request-a-copy-access-icon {
|
|
||||||
margin-right: 4px;
|
|
||||||
color: #26a269;
|
|
||||||
}
|
|
||||||
|
@@ -56,6 +56,7 @@ import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.componen
|
|||||||
import { getItemPageRoute } from '../item-page-routing-paths';
|
import { getItemPageRoute } from '../item-page-routing-paths';
|
||||||
import { ItemVersionsComponent } from '../versions/item-versions.component';
|
import { ItemVersionsComponent } from '../versions/item-versions.component';
|
||||||
import { ItemVersionsNoticeComponent } from '../versions/notice/item-versions-notice.component';
|
import { ItemVersionsNoticeComponent } from '../versions/notice/item-versions-notice.component';
|
||||||
|
import { AccessByTokenNotificationComponent } from './access-by-token-notification/access-by-token-notification.component';
|
||||||
import { NotifyRequestsStatusComponent } from './notify-requests-status/notify-requests-status-component/notify-requests-status.component';
|
import { NotifyRequestsStatusComponent } from './notify-requests-status/notify-requests-status-component/notify-requests-status.component';
|
||||||
import { QaEventNotificationComponent } from './qa-event-notification/qa-event-notification.component';
|
import { QaEventNotificationComponent } from './qa-event-notification/qa-event-notification.component';
|
||||||
|
|
||||||
@@ -84,6 +85,7 @@ import { QaEventNotificationComponent } from './qa-event-notification/qa-event-n
|
|||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
NotifyRequestsStatusComponent,
|
NotifyRequestsStatusComponent,
|
||||||
QaEventNotificationComponent,
|
QaEventNotificationComponent,
|
||||||
|
AccessByTokenNotificationComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ItemPageComponent implements OnInit, OnDestroy {
|
export class ItemPageComponent implements OnInit, OnDestroy {
|
||||||
@@ -155,9 +157,7 @@ export class ItemPageComponent implements OnInit, OnDestroy {
|
|||||||
this.itemRD$ = this.route.data.pipe(
|
this.itemRD$ = this.route.data.pipe(
|
||||||
map((data) => data.dso as RemoteData<Item>),
|
map((data) => data.dso as RemoteData<Item>),
|
||||||
);
|
);
|
||||||
this.itemRequest$ = this.route.data.pipe(
|
|
||||||
map((data) => data.itemRequest as ItemRequest),
|
|
||||||
);
|
|
||||||
this.itemPageRoute$ = this.itemRD$.pipe(
|
this.itemPageRoute$ = this.itemRD$.pipe(
|
||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
map((item) => getItemPageRoute(item)),
|
map((item) => getItemPageRoute(item)),
|
||||||
|
@@ -1,3 +1,3 @@
|
|||||||
.request-a-copy-access-icon {
|
.request-a-copy-access-icon {
|
||||||
color: #26a269;
|
color: var(--bs-success);
|
||||||
}
|
}
|
||||||
|
@@ -44,6 +44,8 @@ describe('FileDownloadLinkComponent', () => {
|
|||||||
token: 'item-request-token',
|
token: 'item-request-token',
|
||||||
requestName: 'requester name',
|
requestName: 'requester name',
|
||||||
accessToken: 'abc123',
|
accessToken: 'abc123',
|
||||||
|
acceptRequest: true,
|
||||||
|
accessExpired: false,
|
||||||
allfiles: true,
|
allfiles: true,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -99,7 +99,7 @@ export class FileDownloadLinkComponent implements OnInit {
|
|||||||
this.itemRequest = this.route.snapshot.data.itemRequest;
|
this.itemRequest = this.route.snapshot.data.itemRequest;
|
||||||
// Set up observables to test access rights to a normal bitstream download, a valid token download, and the request-a-copy feature
|
// Set up observables to test access rights to a normal bitstream download, a valid token download, and the request-a-copy feature
|
||||||
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.acceptRequest && !this.itemRequest.accessExpired) ? (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);
|
||||||
// Set up observable to determine the path to the bitstream based on the user's access rights and features as above
|
// Set up observable to determine the path to the bitstream based on the user's access rights and features as above
|
||||||
this.bitstreamPath$ = observableCombineLatest([this.canDownload$, this.canDownloadWithToken$, this.canRequestACopy$]).pipe(
|
this.bitstreamPath$ = observableCombineLatest([this.canDownload$, this.canDownloadWithToken$, this.canRequestACopy$]).pipe(
|
||||||
|
@@ -1002,6 +1002,12 @@
|
|||||||
|
|
||||||
"bitstream-request-a-copy.access-by-token.expiry-label": "Access provided by this link will expire on",
|
"bitstream-request-a-copy.access-by-token.expiry-label": "Access provided by this link will expire on",
|
||||||
|
|
||||||
|
"bitstream-request-a-copy.access-by-token.expired": "Access provided by this link is no longer possible. Access expired on",
|
||||||
|
|
||||||
|
"bitstream-request-a-copy.access-by-token.not-granted": "Access provided by this link is not possible. Access has either not been granted, or has been revoked.",
|
||||||
|
|
||||||
|
"bitstream-request-a-copy.access-by-token.re-request": "Follow restricted download links to submit a new request for access.",
|
||||||
|
|
||||||
"bitstream-request-a-copy.access-by-token.alt-text": "Access to this item is provided by a secure token",
|
"bitstream-request-a-copy.access-by-token.alt-text": "Access to this item is provided by a secure token",
|
||||||
|
|
||||||
"browse.back.all-results": "All browse results",
|
"browse.back.all-results": "All browse results",
|
||||||
|
@@ -6,6 +6,7 @@ import {
|
|||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
|
||||||
import { ThemedItemAlertsComponent } from '../../../../../app/item-page/alerts/themed-item-alerts.component';
|
import { ThemedItemAlertsComponent } from '../../../../../app/item-page/alerts/themed-item-alerts.component';
|
||||||
|
import { AccessByTokenNotificationComponent } from '../../../../../app/item-page/simple/access-by-token-notification/access-by-token-notification.component';
|
||||||
import { ItemPageComponent as BaseComponent } from '../../../../../app/item-page/simple/item-page.component';
|
import { ItemPageComponent as BaseComponent } from '../../../../../app/item-page/simple/item-page.component';
|
||||||
import { NotifyRequestsStatusComponent } from '../../../../../app/item-page/simple/notify-requests-status/notify-requests-status-component/notify-requests-status.component';
|
import { NotifyRequestsStatusComponent } from '../../../../../app/item-page/simple/notify-requests-status/notify-requests-status-component/notify-requests-status.component';
|
||||||
import { QaEventNotificationComponent } from '../../../../../app/item-page/simple/qa-event-notification/qa-event-notification.component';
|
import { QaEventNotificationComponent } from '../../../../../app/item-page/simple/qa-event-notification/qa-event-notification.component';
|
||||||
@@ -45,6 +46,7 @@ import { ViewTrackerComponent } from '../../../../../app/statistics/angulartics/
|
|||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
NotifyRequestsStatusComponent,
|
NotifyRequestsStatusComponent,
|
||||||
QaEventNotificationComponent,
|
QaEventNotificationComponent,
|
||||||
|
AccessByTokenNotificationComponent,
|
||||||
],
|
],
|
||||||
})
|
})
|
||||||
export class ItemPageComponent extends BaseComponent {
|
export class ItemPageComponent extends BaseComponent {
|
||||||
|
Reference in New Issue
Block a user