88248: #346: Withdrawn item tombstone page

This commit is contained in:
Yana De Pauw
2022-03-14 16:33:10 +01:00
parent 9fc7b57157
commit b21f76456d
7 changed files with 178 additions and 30 deletions

View File

@@ -4,19 +4,21 @@
<ds-item-alerts [item]="item"></ds-item-alerts> <ds-item-alerts [item]="item"></ds-item-alerts>
<ds-item-versions-notice [item]="item"></ds-item-versions-notice> <ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker> <ds-view-tracker [object]="item"></ds-view-tracker>
<div class="d-flex flex-row"> <div *ngIf="!item.isWithdrawn || (isAdmin$|async)" class="full-item-info">
<ds-item-page-title-field class="mr-auto" [item]="item"></ds-item-page-title-field> <div class="d-flex flex-row">
<div class="pl-2"> <ds-item-page-title-field class="mr-auto" [item]="item"></ds-item-page-title-field>
<ds-dso-page-edit-button [pageRoute]="itemPageRoute$ | async" [dso]="item" [tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button> <div class="pl-2">
<ds-dso-page-edit-button [pageRoute]="itemPageRoute$ | async" [dso]="item"
[tooltipMsg]="'item.page.edit'"></ds-dso-page-edit-button>
</div>
</div> </div>
</div> <div class="simple-view-link my-3" *ngIf="!fromWfi">
<div class="simple-view-link my-3" *ngIf="!fromWfi"> <a class="btn btn-outline-primary" [routerLink]="[(itemPageRoute$ | async)]">
<a class="btn btn-outline-primary" [routerLink]="[(itemPageRoute$ | async)]"> {{"item.page.link.simple" | translate}}
{{"item.page.link.simple" | translate}} </a>
</a> </div>
</div> <table class="table table-responsive table-striped">
<table class="table table-responsive table-striped"> <tbody>
<tbody>
<ng-container *ngFor="let mdEntry of (metadata$ | async) | keyvalue"> <ng-container *ngFor="let mdEntry of (metadata$ | async) | keyvalue">
<tr *ngFor="let mdValue of mdEntry.value"> <tr *ngFor="let mdValue of mdEntry.value">
<td>{{mdEntry.key}}</td> <td>{{mdEntry.key}}</td>
@@ -24,14 +26,16 @@
<td>{{mdValue.language}}</td> <td>{{mdValue.language}}</td>
</tr> </tr>
</ng-container> </ng-container>
</tbody> </tbody>
</table> </table>
<ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section> <ds-item-page-full-file-section [item]="item"></ds-item-page-full-file-section>
<ds-item-page-collections [item]="item"></ds-item-page-collections> <ds-item-page-collections [item]="item"></ds-item-page-collections>
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions> <ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
<div class="button-row bottom" *ngIf="fromWfi"> <div class="button-row bottom" *ngIf="fromWfi">
<div class="text-right"> <div class="text-right">
<button class="btn btn-outline-secondary mr-1" (click)="back()"><i class="fas fa-arrow-left"></i> {{'item.page.return' | translate}}</button> <button class="btn btn-outline-secondary mr-1" (click)="back()"><i
class="fas fa-arrow-left"></i> {{'item.page.return' | translate}}</button>
</div>
</div> </div>
</div> </div>
</div> </div>

View File

@@ -11,12 +11,15 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { of as observableOf } from 'rxjs'; import { BehaviorSubject, of as observableOf } from 'rxjs';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { createRelationshipsObservable } from '../simple/item-types/shared/item.component.spec';
import { RemoteData } from '../../core/data/remote-data';
const mockItem: Item = Object.assign(new Item(), { const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
@@ -30,6 +33,13 @@ const mockItem: Item = Object.assign(new Item(), {
} }
}); });
const mockWithdrawnItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
metadata: [],
relationships: createRelationshipsObservable(),
isWithdrawn: true
});
const metadataServiceStub = { const metadataServiceStub = {
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
processRemoteData: () => { processRemoteData: () => {
@@ -44,6 +54,7 @@ describe('FullItemPageComponent', () => {
let authService: AuthService; let authService: AuthService;
let routeStub: ActivatedRouteStub; let routeStub: ActivatedRouteStub;
let routeData; let routeData;
let authorizationDataService: AuthorizationDataService;
@@ -61,6 +72,10 @@ describe('FullItemPageComponent', () => {
data: observableOf(routeData) data: observableOf(routeData)
}); });
authorizationDataService = jasmine.createSpyObj('authorizationDataService', {
isAuthorized: observableOf(false),
});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({ imports: [TranslateModule.forRoot({
loader: { loader: {
@@ -74,6 +89,7 @@ describe('FullItemPageComponent', () => {
{ provide: ItemDataService, useValue: {} }, { provide: ItemDataService, useValue: {} },
{ provide: MetadataService, useValue: metadataServiceStub }, { provide: MetadataService, useValue: metadataServiceStub },
{ provide: AuthService, useValue: authService }, { provide: AuthService, useValue: authService },
{ provide: AuthorizationDataService, useValue: authorizationDataService },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -111,4 +127,53 @@ describe('FullItemPageComponent', () => {
expect(simpleViewBtn).toBeFalsy(); expect(simpleViewBtn).toBeFalsy();
}); });
})); }));
describe('when the item is withdrawn and the user is an admin', () => {
beforeEach(() => {
comp.isAdmin$ = observableOf(true);
comp.itemRD$ = new BehaviorSubject<RemoteData<Item>>(createSuccessfulRemoteDataObject(mockWithdrawnItem));
fixture.detectChanges();
});
it('should display the item', () => {
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
expect(objectLoader.nativeElement).toBeDefined();
});
});
describe('when the item is withdrawn and the user is not an admin', () => {
beforeEach(() => {
comp.itemRD$ = new BehaviorSubject<RemoteData<Item>>(createSuccessfulRemoteDataObject(mockWithdrawnItem));
fixture.detectChanges();
});
it('should not display the item', () => {
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
expect(objectLoader).toBeNull();
});
});
describe('when the item is not withdrawn and the user is an admin', () => {
beforeEach(() => {
comp.isAdmin$ = observableOf(true);
comp.itemRD$ = new BehaviorSubject<RemoteData<Item>>(createSuccessfulRemoteDataObject(mockItem));
fixture.detectChanges();
});
it('should display the item', () => {
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
expect(objectLoader.nativeElement).toBeDefined();
});
});
describe('when the item is not withdrawn and the user is not an admin', () => {
beforeEach(() => {
comp.itemRD$ = new BehaviorSubject<RemoteData<Item>>(createSuccessfulRemoteDataObject(mockItem));
fixture.detectChanges();
});
it('should display the item', () => {
const objectLoader = fixture.debugElement.query(By.css('.full-item-info'));
expect(objectLoader.nativeElement).toBeDefined();
});
});
}); });

View File

@@ -15,6 +15,7 @@ import { fadeInOut } from '../../shared/animations/fade';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
/** /**
@@ -46,8 +47,9 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit,
router: Router, router: Router,
items: ItemDataService, items: ItemDataService,
authService: AuthService, authService: AuthService,
authorizationService: AuthorizationDataService,
private _location: Location) { private _location: Location) {
super(route, router, items, authService); super(route, router, items, authService, authorizationService);
} }
/*** AoT inheritance fix, will hopefully be resolved in the near future **/ /*** AoT inheritance fix, will hopefully be resolved in the near future **/

View File

@@ -4,7 +4,7 @@
<ds-item-alerts [item]="item"></ds-item-alerts> <ds-item-alerts [item]="item"></ds-item-alerts>
<ds-item-versions-notice [item]="item"></ds-item-versions-notice> <ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker> <ds-view-tracker [object]="item"></ds-view-tracker>
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader> <ds-listable-object-component-loader *ngIf="!item.isWithdrawn || (isAdmin$|async)" [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
<ds-item-versions class="mt-2" [item]="item"></ds-item-versions> <ds-item-versions class="mt-2" [item]="item"></ds-item-versions>
</div> </div>
</div> </div>

View File

@@ -21,6 +21,7 @@ import {
} from '../../shared/remote-data.utils'; } from '../../shared/remote-data.utils';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
const mockItem: Item = Object.assign(new Item(), { const mockItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])), bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
@@ -28,10 +29,18 @@ const mockItem: Item = Object.assign(new Item(), {
relationships: createRelationshipsObservable() relationships: createRelationshipsObservable()
}); });
const mockWithdrawnItem: Item = Object.assign(new Item(), {
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([])),
metadata: [],
relationships: createRelationshipsObservable(),
isWithdrawn: true
});
describe('ItemPageComponent', () => { describe('ItemPageComponent', () => {
let comp: ItemPageComponent; let comp: ItemPageComponent;
let fixture: ComponentFixture<ItemPageComponent>; let fixture: ComponentFixture<ItemPageComponent>;
let authService: AuthService; let authService: AuthService;
let authorizationDataService: AuthorizationDataService;
const mockMetadataService = { const mockMetadataService = {
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
@@ -48,6 +57,9 @@ describe('ItemPageComponent', () => {
isAuthenticated: observableOf(true), isAuthenticated: observableOf(true),
setRedirectUrl: {} setRedirectUrl: {}
}); });
authorizationDataService = jasmine.createSpyObj('authorizationDataService', {
isAuthorized: observableOf(false),
});
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({ imports: [TranslateModule.forRoot({
@@ -63,6 +75,7 @@ describe('ItemPageComponent', () => {
{ provide: MetadataService, useValue: mockMetadataService }, { provide: MetadataService, useValue: mockMetadataService },
{ provide: Router, useValue: {} }, { provide: Router, useValue: {} },
{ provide: AuthService, useValue: authService }, { provide: AuthService, useValue: authService },
{ provide: AuthorizationDataService, useValue: authorizationDataService },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
@@ -102,4 +115,53 @@ describe('ItemPageComponent', () => {
}); });
}); });
describe('when the item is withdrawn and the user is an admin', () => {
beforeEach(() => {
comp.isAdmin$ = observableOf(true);
comp.itemRD$ = createSuccessfulRemoteDataObject$(mockWithdrawnItem);
fixture.detectChanges();
});
it('should display the item', () => {
const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader'));
expect(objectLoader.nativeElement).toBeDefined();
});
});
describe('when the item is withdrawn and the user is not an admin', () => {
beforeEach(() => {
comp.itemRD$ = createSuccessfulRemoteDataObject$(mockWithdrawnItem);
fixture.detectChanges();
});
it('should not display the item', () => {
const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader'));
expect(objectLoader).toBeNull();
});
});
describe('when the item is not withdrawn and the user is an admin', () => {
beforeEach(() => {
comp.isAdmin$ = observableOf(true);
comp.itemRD$ = createSuccessfulRemoteDataObject$(mockItem);
fixture.detectChanges();
});
it('should display the item', () => {
const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader'));
expect(objectLoader.nativeElement).toBeDefined();
});
});
describe('when the item is not withdrawn and the user is not an admin', () => {
beforeEach(() => {
comp.itemRD$ = createSuccessfulRemoteDataObject$(mockItem);
fixture.detectChanges();
});
it('should display the item', () => {
const objectLoader = fixture.debugElement.query(By.css('ds-listable-object-component-loader'));
expect(objectLoader.nativeElement).toBeDefined();
});
});
}); });

View File

@@ -1,4 +1,4 @@
import { map } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
@@ -13,6 +13,8 @@ import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../../core/shar
import { ViewMode } from '../../core/shared/view-mode.model'; import { ViewMode } from '../../core/shared/view-mode.model';
import { AuthService } from '../../core/auth/auth.service'; import { AuthService } from '../../core/auth/auth.service';
import { getItemPageRoute } from '../item-page-routing-paths'; import { getItemPageRoute } from '../item-page-routing-paths';
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
/** /**
* This component renders a simple item page. * This component renders a simple item page.
@@ -48,11 +50,17 @@ export class ItemPageComponent implements OnInit {
*/ */
itemPageRoute$: Observable<string>; itemPageRoute$: Observable<string>;
/**
* Whether the current user is an admin or not
*/
isAdmin$: Observable<boolean>;
constructor( constructor(
protected route: ActivatedRoute, protected route: ActivatedRoute,
private router: Router, private router: Router,
private items: ItemDataService, private items: ItemDataService,
private authService: AuthService, private authService: AuthService,
private authorizationService: AuthorizationDataService
) { } ) { }
/** /**
@@ -67,5 +75,7 @@ export class ItemPageComponent implements OnInit {
getAllSucceededRemoteDataPayload(), getAllSucceededRemoteDataPayload(),
map((item) => getItemPageRoute(item)) map((item) => getItemPageRoute(item))
); );
this.isAdmin$ = this.authorizationService.isAuthorized(FeatureID.AdministratorOf);
} }
} }

View File

@@ -1,8 +1,13 @@
<div> <div>
<div *ngIf="item && !item.isDiscoverable" class="private-warning"> <div *ngIf="item && !item.isDiscoverable" class="private-warning">
<ds-alert [type]="AlertTypeEnum.Warning" [content]="'item.alerts.private' | translate"></ds-alert> <ds-alert [type]="AlertTypeEnum.Warning" [content]="'item.alerts.private' | translate"></ds-alert>
</div> </div>
<div *ngIf="item && item.isWithdrawn" class="withdrawn-warning"> <div *ngIf="item && item.isWithdrawn" class="withdrawn-warning">
<ds-alert [type]="AlertTypeEnum.Warning" [content]="'item.alerts.withdrawn' | translate"></ds-alert> <ds-alert [type]="AlertTypeEnum.Warning">
</div> <div class="d-flex justify-content-between flex-wrap">
<span>{{'item.alerts.withdrawn' | translate}}</span>
<a routerLink="/home" class="btn btn-primary btn-sm">{{"404.link.home-page" | translate}}</a>
</div>
</ds-alert>
</div>
</div> </div>