mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-17 23:13:04 +00:00
Merge remote-tracking branch 'upstream/main' into #885-media-viewer
This commit is contained in:
@@ -24,6 +24,10 @@ export function getCollectionEditRolesRoute(id) {
|
||||
return new URLCombiner(getCollectionPageRoute(id), COLLECTION_EDIT_PATH, COLLECTION_EDIT_ROLES_PATH).toString();
|
||||
}
|
||||
|
||||
export function getCollectionItemTemplateRoute(id) {
|
||||
return new URLCombiner(getCollectionPageRoute(id), ITEMTEMPLATE_PATH).toString();
|
||||
}
|
||||
|
||||
export const COLLECTION_CREATE_PATH = 'create';
|
||||
export const COLLECTION_EDIT_PATH = 'edit';
|
||||
export const COLLECTION_EDIT_ROLES_PATH = 'roles';
|
||||
|
@@ -15,6 +15,7 @@ import { Collection } from '../../../core/shared/collection.model';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||
import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths';
|
||||
|
||||
describe('CollectionMetadataComponent', () => {
|
||||
let comp: CollectionMetadataComponent;
|
||||
@@ -35,11 +36,13 @@ describe('CollectionMetadataComponent', () => {
|
||||
self: { href: 'collection-selflink' }
|
||||
}
|
||||
});
|
||||
const collectionTemplateHref = 'rest/api/test/collections/template';
|
||||
|
||||
const itemTemplateServiceStub = Object.assign({
|
||||
findByCollectionID: () => createSuccessfulRemoteDataObject$(template),
|
||||
create: () => createSuccessfulRemoteDataObject$(template),
|
||||
deleteByCollectionID: () => observableOf(true)
|
||||
const itemTemplateServiceStub = jasmine.createSpyObj('itemTemplateService', {
|
||||
findByCollectionID: createSuccessfulRemoteDataObject$(template),
|
||||
create: createSuccessfulRemoteDataObject$(template),
|
||||
deleteByCollectionID: observableOf(true),
|
||||
getCollectionEndpoint: observableOf(collectionTemplateHref),
|
||||
});
|
||||
|
||||
const notificationsService = jasmine.createSpyObj('notificationsService', {
|
||||
@@ -50,7 +53,7 @@ describe('CollectionMetadataComponent', () => {
|
||||
remove: {}
|
||||
});
|
||||
const requestService = jasmine.createSpyObj('requestService', {
|
||||
removeByHrefSubstring: {}
|
||||
setStaleByHrefSubstring: {}
|
||||
});
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
@@ -87,14 +90,14 @@ describe('CollectionMetadataComponent', () => {
|
||||
it('should navigate to the collection\'s itemtemplate page', () => {
|
||||
spyOn(router, 'navigate');
|
||||
comp.addItemTemplate();
|
||||
expect(router.navigate).toHaveBeenCalledWith(['collections', collection.uuid, 'itemtemplate']);
|
||||
expect(router.navigate).toHaveBeenCalledWith([getCollectionItemTemplateRoute(collection.uuid)]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('deleteItemTemplate', () => {
|
||||
describe('when delete returns a success', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(itemTemplateService, 'deleteByCollectionID').and.returnValue(observableOf(true));
|
||||
(itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(true));
|
||||
comp.deleteItemTemplate();
|
||||
});
|
||||
|
||||
@@ -103,14 +106,15 @@ describe('CollectionMetadataComponent', () => {
|
||||
});
|
||||
|
||||
it('should reset related object and request cache', () => {
|
||||
expect(objectCache.remove).toHaveBeenCalledWith(template.self);
|
||||
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(collection.self);
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(collectionTemplateHref);
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(template.self);
|
||||
expect(requestService.setStaleByHrefSubstring).toHaveBeenCalledWith(collection.self);
|
||||
});
|
||||
});
|
||||
|
||||
describe('when delete returns a failure', () => {
|
||||
beforeEach(() => {
|
||||
spyOn(itemTemplateService, 'deleteByCollectionID').and.returnValue(observableOf(false));
|
||||
(itemTemplateService.deleteByCollectionID as jasmine.Spy).and.returnValue(observableOf(false));
|
||||
comp.deleteItemTemplate();
|
||||
});
|
||||
|
||||
|
@@ -7,12 +7,13 @@ import { ItemTemplateDataService } from '../../../core/data/item-template-data.s
|
||||
import { combineLatest as combineLatestObservable, Observable } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { getRemoteDataPayload, getFirstSucceededRemoteData } from '../../../core/shared/operators';
|
||||
import { switchMap, take } from 'rxjs/operators';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||
import { switchMap, tap } from 'rxjs/operators';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||
import { RequestService } from '../../../core/data/request.service';
|
||||
import { getCollectionItemTemplateRoute } from '../../collection-page-routing-paths';
|
||||
|
||||
/**
|
||||
* Component for editing a collection's metadata
|
||||
@@ -53,8 +54,7 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
||||
*/
|
||||
initTemplateItem() {
|
||||
this.itemTemplateRD$ = this.dsoRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid))
|
||||
);
|
||||
}
|
||||
@@ -64,19 +64,20 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
||||
*/
|
||||
addItemTemplate() {
|
||||
const collection$ = this.dsoRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
take(1)
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
const template$ = collection$.pipe(
|
||||
switchMap((collection: Collection) => this.itemTemplateService.create(new Item(), collection.uuid)),
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
take(1)
|
||||
switchMap((collection: Collection) => this.itemTemplateService.create(new Item(), collection.uuid).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
)),
|
||||
);
|
||||
const templateHref$ = collection$.pipe(
|
||||
switchMap((collection) => this.itemTemplateService.getCollectionEndpoint(collection.id)),
|
||||
);
|
||||
|
||||
combineLatestObservable(collection$, template$).subscribe(([collection, template]) => {
|
||||
this.router.navigate(['collections', collection.uuid, 'itemtemplate']);
|
||||
combineLatestObservable(collection$, template$, templateHref$).subscribe(([collection, template, templateHref]) => {
|
||||
this.requestService.setStaleByHrefSubstring(templateHref);
|
||||
this.router.navigate([getCollectionItemTemplateRoute(collection.uuid)]);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -85,23 +86,30 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent<Collect
|
||||
*/
|
||||
deleteItemTemplate() {
|
||||
const collection$ = this.dsoRD$.pipe(
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
take(1)
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
);
|
||||
const template$ = collection$.pipe(
|
||||
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid)),
|
||||
getFirstSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
take(1)
|
||||
switchMap((collection: Collection) => this.itemTemplateService.findByCollectionID(collection.uuid).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
)),
|
||||
);
|
||||
const templateHref$ = collection$.pipe(
|
||||
switchMap((collection) => this.itemTemplateService.getCollectionEndpoint(collection.id)),
|
||||
);
|
||||
|
||||
combineLatestObservable(collection$, template$).pipe(
|
||||
switchMap(([collection, template]) => {
|
||||
const success$ = this.itemTemplateService.deleteByCollectionID(template, collection.uuid);
|
||||
this.objectCache.remove(template.self);
|
||||
this.requestService.removeByHrefSubstring(collection.self);
|
||||
return success$;
|
||||
combineLatestObservable(collection$, template$, templateHref$).pipe(
|
||||
switchMap(([collection, template, templateHref]) => {
|
||||
return this.itemTemplateService.deleteByCollectionID(template, collection.uuid).pipe(
|
||||
tap((success: boolean) => {
|
||||
if (success) {
|
||||
this.objectCache.remove(templateHref);
|
||||
this.objectCache.remove(template.self);
|
||||
this.requestService.setStaleByHrefSubstring(template.self);
|
||||
this.requestService.setStaleByHrefSubstring(templateHref);
|
||||
this.requestService.setStaleByHrefSubstring(collection.self);
|
||||
}
|
||||
})
|
||||
);
|
||||
})
|
||||
).subscribe((success: boolean) => {
|
||||
if (success) {
|
||||
|
@@ -1,9 +1,13 @@
|
||||
<div class="container" *ngVar="(collectionRD$ | async)?.payload as collection">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2>
|
||||
<ds-item-metadata [updateService]="itemTemplateService"></ds-item-metadata>
|
||||
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
|
||||
<div class="col-12" *ngVar="(itemRD$ | async) as itemRD">
|
||||
<ng-container *ngIf="itemRD?.hasSucceeded">
|
||||
<h2 class="border-bottom">{{ 'collection.edit.template.head' | translate:{ collection: collection?.name } }}</h2>
|
||||
<ds-item-metadata [updateService]="itemTemplateService" [item]="itemRD?.payload"></ds-item-metadata>
|
||||
<button [routerLink]="getCollectionEditUrl(collection)" class="btn btn-outline-secondary">{{ 'collection.edit.template.cancel' | translate }}</button>
|
||||
</ng-container>
|
||||
<ds-loading *ngIf="itemRD?.isLoading" [message]="'collection.edit.template.loading' | translate"></ds-loading>
|
||||
<ds-alert *ngIf="itemRD?.hasFailed" [type]="AlertTypeEnum.Error" [content]="'collection.edit.template.error' | translate"></ds-alert>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -9,7 +9,7 @@ import { ActivatedRoute } from '@angular/router';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { getCollectionEditRoute } from '../collection-page-routing-paths';
|
||||
|
||||
describe('EditItemTemplatePageComponent', () => {
|
||||
@@ -24,11 +24,14 @@ describe('EditItemTemplatePageComponent', () => {
|
||||
id: 'collection-id',
|
||||
name: 'Fake Collection'
|
||||
});
|
||||
itemTemplateService = jasmine.createSpyObj('itemTemplateService', {
|
||||
findByCollectionID: createSuccessfulRemoteDataObject$({})
|
||||
});
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
|
||||
declarations: [EditItemTemplatePageComponent],
|
||||
providers: [
|
||||
{ provide: ItemTemplateDataService, useValue: {} },
|
||||
{ provide: ItemTemplateDataService, useValue: itemTemplateService },
|
||||
{ provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: createSuccessfulRemoteDataObject(collection) }) } } }
|
||||
],
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
@@ -38,7 +41,6 @@ describe('EditItemTemplatePageComponent', () => {
|
||||
beforeEach(() => {
|
||||
fixture = TestBed.createComponent(EditItemTemplatePageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
itemTemplateService = (comp as any).itemTemplateService;
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
||||
|
@@ -3,9 +3,12 @@ import { Observable } from 'rxjs';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Collection } from '../../core/shared/collection.model';
|
||||
import { ActivatedRoute } from '@angular/router';
|
||||
import { first, map } from 'rxjs/operators';
|
||||
import { first, map, switchMap } from 'rxjs/operators';
|
||||
import { ItemTemplateDataService } from '../../core/data/item-template-data.service';
|
||||
import { getCollectionEditRoute } from '../collection-page-routing-paths';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { getFirstSucceededRemoteDataPayload } from '../../core/shared/operators';
|
||||
import { AlertType } from '../../shared/alert/aletr-type';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-item-template-page',
|
||||
@@ -21,12 +24,27 @@ export class EditItemTemplatePageComponent implements OnInit {
|
||||
*/
|
||||
collectionRD$: Observable<RemoteData<Collection>>;
|
||||
|
||||
/**
|
||||
* The template item
|
||||
*/
|
||||
itemRD$: Observable<RemoteData<Item>>;
|
||||
|
||||
/**
|
||||
* The AlertType enumeration
|
||||
* @type {AlertType}
|
||||
*/
|
||||
AlertTypeEnum = AlertType;
|
||||
|
||||
constructor(protected route: ActivatedRoute,
|
||||
public itemTemplateService: ItemTemplateDataService) {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.collectionRD$ = this.route.parent.data.pipe(first(), map((data) => data.dso));
|
||||
this.itemRD$ = this.collectionRD$.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((collection) => this.itemTemplateService.findByCollectionID(collection.id)),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Component, OnInit, OnDestroy } from '@angular/core';
|
||||
import { Component, OnInit, OnDestroy, Input } from '@angular/core';
|
||||
import {
|
||||
FieldUpdate,
|
||||
FieldUpdates
|
||||
@@ -30,7 +30,7 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
||||
/**
|
||||
* The item to display the edit page for
|
||||
*/
|
||||
item: Item;
|
||||
@Input() item: Item;
|
||||
/**
|
||||
* The current values and updates for all this item's fields
|
||||
* Should be initialized in the initializeUpdates method of the child component
|
||||
@@ -63,22 +63,24 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
||||
* Initialize common properties between item-update components
|
||||
*/
|
||||
ngOnInit(): void {
|
||||
this.itemUpdateSubscription = observableCombineLatest([this.route.data, this.route.parent.data]).pipe(
|
||||
map(([data, parentData]: [Data, Data]) => Object.assign({}, data, parentData)),
|
||||
map((data: any) => data.dso),
|
||||
tap((rd: RemoteData<Item>) => {
|
||||
this.item = rd.payload;
|
||||
}),
|
||||
switchMap((rd: RemoteData<Item>) => {
|
||||
return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...ITEM_PAGE_LINKS_TO_FOLLOW);
|
||||
}),
|
||||
getAllSucceededRemoteData()
|
||||
).subscribe((rd: RemoteData<Item>) => {
|
||||
this.item = rd.payload;
|
||||
this.itemPageRoute = getItemPageRoute(this.item);
|
||||
this.postItemInit();
|
||||
this.initializeUpdates();
|
||||
});
|
||||
if (hasValue(this.item)) {
|
||||
this.setItem(this.item);
|
||||
} else {
|
||||
// The item wasn't provided through an input, retrieve it from the route instead.
|
||||
this.itemUpdateSubscription = observableCombineLatest([this.route.data, this.route.parent.data]).pipe(
|
||||
map(([data, parentData]: [Data, Data]) => Object.assign({}, data, parentData)),
|
||||
map((data: any) => data.dso),
|
||||
tap((rd: RemoteData<Item>) => {
|
||||
this.item = rd.payload;
|
||||
}),
|
||||
switchMap((rd: RemoteData<Item>) => {
|
||||
return this.itemService.findByHref(rd.payload._links.self.href, true, true, ...ITEM_PAGE_LINKS_TO_FOLLOW);
|
||||
}),
|
||||
getAllSucceededRemoteData()
|
||||
).subscribe((rd: RemoteData<Item>) => {
|
||||
this.setItem(rd.payload);
|
||||
});
|
||||
}
|
||||
|
||||
this.discardTimeOut = environment.item.edit.undoTimeout;
|
||||
this.url = this.router.url;
|
||||
@@ -97,6 +99,13 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
||||
this.initializeUpdates();
|
||||
}
|
||||
|
||||
setItem(item: Item) {
|
||||
this.item = item;
|
||||
this.itemPageRoute = getItemPageRoute(this.item);
|
||||
this.postItemInit();
|
||||
this.initializeUpdates();
|
||||
}
|
||||
|
||||
ngOnDestroy() {
|
||||
if (hasValue(this.itemUpdateSubscription)) {
|
||||
this.itemUpdateSubscription.unsubscribe();
|
||||
|
@@ -5,11 +5,18 @@
|
||||
<div class="pt-2">
|
||||
<ul class="nav nav-tabs justify-content-start">
|
||||
<li *ngFor="let page of pages" class="nav-item">
|
||||
<a class="nav-link"
|
||||
[ngClass]="{'active' : page === currentPage}"
|
||||
[routerLink]="['./' + page]">
|
||||
{{'item.edit.tabs.' + page + '.head' | translate}}
|
||||
<a *ngIf="(page.enabled | async)"
|
||||
class="nav-link"
|
||||
[ngClass]="{'active' : page.page === currentPage}"
|
||||
[routerLink]="['./' + page.page]">
|
||||
{{'item.edit.tabs.' + page.page + '.head' | translate}}
|
||||
</a>
|
||||
<span [ngbTooltip]="'item.edit.tabs.disabled.tooltip' | translate">
|
||||
<button *ngIf="!(page.enabled | async)"
|
||||
class="nav-link disabled">
|
||||
{{'item.edit.tabs.' + page.page + '.head' | translate}}
|
||||
</button>
|
||||
</span>
|
||||
</li>
|
||||
</ul>
|
||||
<div class="tab-pane active">
|
||||
|
@@ -0,0 +1,107 @@
|
||||
import { ComponentFixture, fakeAsync, TestBed, waitForAsync } from '@angular/core/testing';
|
||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
|
||||
import { ActivatedRoute, ActivatedRouteSnapshot, CanActivate, Router, RouterStateSnapshot, UrlTree } from '@angular/router';
|
||||
import { EditItemPageComponent } from './edit-item-page.component';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { createSuccessfulRemoteDataObject } from '../../shared/remote-data.utils';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
|
||||
describe('ItemPageComponent', () => {
|
||||
let comp: EditItemPageComponent;
|
||||
let fixture: ComponentFixture<EditItemPageComponent>;
|
||||
|
||||
class AcceptAllGuard implements CanActivate {
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||
return observableOf(true);
|
||||
}
|
||||
}
|
||||
|
||||
// tslint:disable-next-line:max-classes-per-file
|
||||
class AcceptNoneGuard implements CanActivate {
|
||||
canActivate(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<boolean | UrlTree> | Promise<boolean | UrlTree> | boolean | UrlTree {
|
||||
console.log('BLA');
|
||||
return observableOf(false);
|
||||
}
|
||||
}
|
||||
|
||||
const accesiblePages = ['accessible'];
|
||||
const inaccesiblePages = ['inaccessible', 'inaccessibleDoubleGuard'];
|
||||
const mockRoute = {
|
||||
snapshot: {
|
||||
firstChild: {
|
||||
routeConfig: {
|
||||
path: accesiblePages[0]
|
||||
}
|
||||
},
|
||||
routerState: {
|
||||
snapshot: undefined
|
||||
}
|
||||
},
|
||||
routeConfig: {
|
||||
children: [
|
||||
{
|
||||
path: accesiblePages[0],
|
||||
canActivate: [AcceptAllGuard]
|
||||
}, {
|
||||
path: inaccesiblePages[0],
|
||||
canActivate: [AcceptNoneGuard]
|
||||
}, {
|
||||
path: inaccesiblePages[1],
|
||||
canActivate: [AcceptAllGuard, AcceptNoneGuard]
|
||||
},
|
||||
]
|
||||
},
|
||||
data: observableOf({dso: createSuccessfulRemoteDataObject(new Item())})
|
||||
};
|
||||
|
||||
const mockRouter = {
|
||||
routerState: {
|
||||
snapshot: undefined
|
||||
},
|
||||
events: observableOf(undefined)
|
||||
};
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
TestBed.configureTestingModule({
|
||||
imports: [TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
})],
|
||||
declarations: [EditItemPageComponent],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: mockRoute },
|
||||
{ provide: Router, useValue: mockRouter },
|
||||
AcceptAllGuard,
|
||||
AcceptNoneGuard,
|
||||
],
|
||||
|
||||
schemas: [NO_ERRORS_SCHEMA]
|
||||
}).overrideComponent(EditItemPageComponent, {
|
||||
set: { changeDetection: ChangeDetectionStrategy.Default }
|
||||
}).compileComponents();
|
||||
}));
|
||||
|
||||
beforeEach(waitForAsync(() => {
|
||||
fixture = TestBed.createComponent(EditItemPageComponent);
|
||||
comp = fixture.componentInstance;
|
||||
spyOn((comp as any).injector, 'get').and.callFake((a) => new a());
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
describe('ngOnInit', () => {
|
||||
it('should enable tabs that the user can activate', fakeAsync(() => {
|
||||
const enabledItems = fixture.debugElement.queryAll(By.css('a.nav-link'));
|
||||
expect(enabledItems.length).toBe(accesiblePages.length);
|
||||
}));
|
||||
|
||||
it('should disable tabs that the user can not activate', () => {
|
||||
const disabledItems = fixture.debugElement.queryAll(By.css('button.nav-link.disabled'));
|
||||
expect(disabledItems.length).toBe(inaccesiblePages.length);
|
||||
});
|
||||
});
|
||||
});
|
@@ -1,12 +1,13 @@
|
||||
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { ChangeDetectionStrategy, Component, Injector, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, CanActivate, Route, Router } from '@angular/router';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { Observable } from 'rxjs';
|
||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||
import { map } from 'rxjs/operators';
|
||||
import { isNotEmpty } from '../../shared/empty.util';
|
||||
import { getItemPageRoute } from '../item-page-routing-paths';
|
||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-edit-item-page',
|
||||
@@ -35,9 +36,9 @@ export class EditItemPageComponent implements OnInit {
|
||||
/**
|
||||
* All possible page outlet strings
|
||||
*/
|
||||
pages: string[];
|
||||
pages: { page: string, enabled: Observable<boolean> }[];
|
||||
|
||||
constructor(private route: ActivatedRoute, private router: Router) {
|
||||
constructor(private route: ActivatedRoute, private router: Router, private injector: Injector) {
|
||||
this.router.events.subscribe(() => {
|
||||
this.currentPage = this.route.snapshot.firstChild.routeConfig.path;
|
||||
});
|
||||
@@ -45,8 +46,20 @@ export class EditItemPageComponent implements OnInit {
|
||||
|
||||
ngOnInit(): void {
|
||||
this.pages = this.route.routeConfig.children
|
||||
.map((child: any) => child.path)
|
||||
.filter((path: string) => isNotEmpty(path)); // ignore reroutes
|
||||
.filter((child: Route) => isNotEmpty(child.path))
|
||||
.map((child: Route) => {
|
||||
let enabled = observableOf(true);
|
||||
if (isNotEmpty(child.canActivate)) {
|
||||
enabled = observableCombineLatest(child.canActivate.map((guardConstructor: GenericConstructor<CanActivate>) => {
|
||||
const guard: CanActivate = this.injector.get<CanActivate>(guardConstructor);
|
||||
return guard.canActivate(this.route.snapshot, this.router.routerState.snapshot);
|
||||
})
|
||||
).pipe(
|
||||
map((canActivateOutcomes: any[]) => canActivateOutcomes.every((e) => e === true))
|
||||
);
|
||||
}
|
||||
return { page: child.path, enabled: enabled };
|
||||
}); // ignore reroutes
|
||||
this.itemRD$ = this.route.data.pipe(map((data) => data.dso));
|
||||
}
|
||||
|
||||
|
@@ -22,15 +22,17 @@ import { ResourcePolicyEditComponent } from '../../shared/resource-policies/edit
|
||||
import { I18nBreadcrumbsService } from '../../core/breadcrumbs/i18n-breadcrumbs.service';
|
||||
import {
|
||||
ITEM_EDIT_AUTHORIZATIONS_PATH,
|
||||
ITEM_EDIT_MOVE_PATH,
|
||||
ITEM_EDIT_DELETE_PATH,
|
||||
ITEM_EDIT_PUBLIC_PATH,
|
||||
ITEM_EDIT_MOVE_PATH,
|
||||
ITEM_EDIT_PRIVATE_PATH,
|
||||
ITEM_EDIT_PUBLIC_PATH,
|
||||
ITEM_EDIT_REINSTATE_PATH,
|
||||
ITEM_EDIT_WITHDRAW_PATH
|
||||
} from './edit-item-page.routing-paths';
|
||||
import { ItemPageReinstateGuard } from './item-page-reinstate.guard';
|
||||
import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||
import { ItemPageEditMetadataGuard } from '../item-page-edit-metadata.guard';
|
||||
import { ItemPageAdministratorGuard } from '../item-page-administrator.guard';
|
||||
|
||||
/**
|
||||
* Routing module that handles the routing for the Edit Item page administrator functionality
|
||||
@@ -57,22 +59,26 @@ import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||
{
|
||||
path: 'status',
|
||||
component: ItemStatusComponent,
|
||||
data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.status.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: 'bitstreams',
|
||||
component: ItemBitstreamsComponent,
|
||||
data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.bitstreams.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: 'metadata',
|
||||
component: ItemMetadataComponent,
|
||||
data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.metadata.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageEditMetadataGuard]
|
||||
},
|
||||
{
|
||||
path: 'relationships',
|
||||
component: ItemRelationshipsComponent,
|
||||
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.relationships.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageEditMetadataGuard]
|
||||
},
|
||||
/* TODO - uncomment & fix when view page exists
|
||||
{
|
||||
@@ -89,12 +95,14 @@ import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||
{
|
||||
path: 'versionhistory',
|
||||
component: ItemVersionHistoryComponent,
|
||||
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.versionhistory.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: 'mapper',
|
||||
component: ItemCollectionMapperComponent,
|
||||
data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true }
|
||||
data: { title: 'item.edit.tabs.item-mapper.title', showBreadcrumbs: true },
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
}
|
||||
]
|
||||
},
|
||||
@@ -165,7 +173,9 @@ import { ItemPageWithdrawGuard } from './item-page-withdraw.guard';
|
||||
ResourcePolicyResolver,
|
||||
ResourcePolicyTargetResolver,
|
||||
ItemPageReinstateGuard,
|
||||
ItemPageWithdrawGuard
|
||||
ItemPageWithdrawGuard,
|
||||
ItemPageAdministratorGuard,
|
||||
ItemPageEditMetadataGuard,
|
||||
]
|
||||
})
|
||||
export class EditItemPageRoutingModule {
|
||||
|
31
src/app/+item-page/item-page-edit-metadata.guard.ts
Normal file
31
src/app/+item-page/item-page-edit-metadata.guard.ts
Normal file
@@ -0,0 +1,31 @@
|
||||
import { Injectable } from '@angular/core';
|
||||
import { ActivatedRouteSnapshot, Router, RouterStateSnapshot } from '@angular/router';
|
||||
import { AuthorizationDataService } from '../core/data/feature-authorization/authorization-data.service';
|
||||
import { ItemPageResolver } from './item-page.resolver';
|
||||
import { Item } from '../core/shared/item.model';
|
||||
import { DsoPageFeatureGuard } from '../core/data/feature-authorization/feature-authorization-guard/dso-page-feature.guard';
|
||||
import { Observable, of as observableOf } from 'rxjs';
|
||||
import { FeatureID } from '../core/data/feature-authorization/feature-id';
|
||||
import { AuthService } from '../core/auth/auth.service';
|
||||
|
||||
@Injectable({
|
||||
providedIn: 'root'
|
||||
})
|
||||
/**
|
||||
* Guard for preventing unauthorized access to certain {@link Item} pages requiring edit metadata rights
|
||||
*/
|
||||
export class ItemPageEditMetadataGuard extends DsoPageFeatureGuard<Item> {
|
||||
constructor(protected resolver: ItemPageResolver,
|
||||
protected authorizationService: AuthorizationDataService,
|
||||
protected router: Router,
|
||||
protected authService: AuthService) {
|
||||
super(resolver, authorizationService, router, authService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check edit metadata authorization rights
|
||||
*/
|
||||
getFeatureID(next: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<FeatureID> {
|
||||
return observableOf(FeatureID.CanEditMetadata);
|
||||
}
|
||||
}
|
@@ -37,7 +37,6 @@ import { ThemedFullItemPageComponent } from './full/themed-full-item-page.compon
|
||||
path: ITEM_EDIT_PATH,
|
||||
loadChildren: () => import('./edit-item-page/edit-item-page.module')
|
||||
.then((m) => m.EditItemPageModule),
|
||||
canActivate: [ItemPageAdministratorGuard]
|
||||
},
|
||||
{
|
||||
path: UPLOAD_BITSTREAM_PATH,
|
||||
@@ -67,7 +66,7 @@ import { ThemedFullItemPageComponent } from './full/themed-full-item-page.compon
|
||||
ItemBreadcrumbResolver,
|
||||
DSOBreadcrumbsService,
|
||||
LinkService,
|
||||
ItemPageAdministratorGuard
|
||||
ItemPageAdministratorGuard,
|
||||
]
|
||||
|
||||
})
|
||||
|
@@ -32,6 +32,8 @@ describe('DsoPageAdministratorGuard', () => {
|
||||
let authService: AuthService;
|
||||
let resolver: Resolve<RemoteData<any>>;
|
||||
let object: DSpaceObject;
|
||||
let route;
|
||||
let parentRoute;
|
||||
|
||||
function init() {
|
||||
object = {
|
||||
@@ -50,6 +52,16 @@ describe('DsoPageAdministratorGuard', () => {
|
||||
authService = jasmine.createSpyObj('authService', {
|
||||
isAuthenticated: observableOf(true)
|
||||
});
|
||||
parentRoute = {
|
||||
params: {
|
||||
id: '3e1a5327-dabb-41ff-af93-e6cab9d032f0'
|
||||
}
|
||||
};
|
||||
route = {
|
||||
params: {
|
||||
},
|
||||
parent: parentRoute
|
||||
};
|
||||
guard = new DsoPageFeatureGuardImpl(resolver, authorizationService, router, authService, undefined);
|
||||
}
|
||||
|
||||
@@ -59,10 +71,17 @@ describe('DsoPageAdministratorGuard', () => {
|
||||
|
||||
describe('getObjectUrl', () => {
|
||||
it('should return the resolved object\'s selflink', (done) => {
|
||||
guard.getObjectUrl(undefined, undefined).subscribe((selflink) => {
|
||||
guard.getObjectUrl(route, undefined).subscribe((selflink) => {
|
||||
expect(selflink).toEqual(object.self);
|
||||
done();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('getRouteWithDSOId', () => {
|
||||
it('should return the route that has the UUID of the DSO', () => {
|
||||
const foundRoute = (guard as any).getRouteWithDSOId(route);
|
||||
expect(foundRoute).toBe(parentRoute);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -7,6 +7,7 @@ import { map } from 'rxjs/operators';
|
||||
import { DSpaceObject } from '../../../shared/dspace-object.model';
|
||||
import { FeatureAuthorizationGuard } from './feature-authorization.guard';
|
||||
import { AuthService } from '../../../auth/auth.service';
|
||||
import { hasNoValue, hasValue } from '../../../../shared/empty.util';
|
||||
|
||||
/**
|
||||
* Abstract Guard for preventing unauthorized access to {@link DSpaceObject} pages that require rights for a specific feature
|
||||
@@ -24,9 +25,22 @@ export abstract class DsoPageFeatureGuard<T extends DSpaceObject> extends Featur
|
||||
* Check authorization rights for the object resolved using the provided resolver
|
||||
*/
|
||||
getObjectUrl(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<string> {
|
||||
return (this.resolver.resolve(route, state) as Observable<RemoteData<T>>).pipe(
|
||||
const routeWithObjectID = this.getRouteWithDSOId(route);
|
||||
return (this.resolver.resolve(routeWithObjectID, state) as Observable<RemoteData<T>>).pipe(
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
map((dso) => dso.self)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to resolve resolve (parent) route that contains the UUID of the DSO
|
||||
* @param route The current route
|
||||
*/
|
||||
protected getRouteWithDSOId(route: ActivatedRouteSnapshot): ActivatedRouteSnapshot {
|
||||
let routeWithDSOId = route;
|
||||
while (hasNoValue(routeWithDSOId.params.id) && hasValue(routeWithDSOId.parent)) {
|
||||
routeWithDSOId = routeWithDSOId.parent;
|
||||
}
|
||||
return routeWithDSOId;
|
||||
}
|
||||
}
|
||||
|
@@ -22,6 +22,7 @@ import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { NoContent } from '../shared/NoContent.model';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { Operation } from 'fast-json-patch';
|
||||
import { getFirstCompletedRemoteData } from '../shared/operators';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
/**
|
||||
@@ -57,15 +58,23 @@ class DataServiceImpl extends ItemDataService {
|
||||
super(requestService, rdbService, store, bs, objectCache, halService, notificationsService, http, comparator, bundleService);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint based on a collection
|
||||
* @param collectionID The ID of the collection to base the endpoint on
|
||||
*/
|
||||
public getCollectionEndpoint(collectionID: string): Observable<string> {
|
||||
return this.collectionService.getIDHrefObs(collectionID).pipe(
|
||||
switchMap((href: string) => this.halService.getEndpoint(this.collectionLinkPath, href))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set the endpoint to be based on a collection
|
||||
* @param collectionID The ID of the collection to base the endpoint on
|
||||
*/
|
||||
private setCollectionEndpoint(collectionID: string) {
|
||||
this.collectionEndpoint = true;
|
||||
this.endpoint$ = this.collectionService.getIDHrefObs(collectionID).pipe(
|
||||
switchMap((href: string) => this.halService.getEndpoint(this.collectionLinkPath, href))
|
||||
);
|
||||
this.endpoint$ = this.getCollectionEndpoint(collectionID);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -130,6 +139,7 @@ class DataServiceImpl extends ItemDataService {
|
||||
deleteByCollectionID(item: Item, collectionID: string): Observable<boolean> {
|
||||
this.setRegularEndpoint();
|
||||
return super.delete(item.uuid).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((response: RemoteData<NoContent>) => hasValue(response) && response.hasSucceeded)
|
||||
);
|
||||
}
|
||||
@@ -209,5 +219,13 @@ export class ItemTemplateDataService implements UpdateDataService<Item> {
|
||||
deleteByCollectionID(item: Item, collectionID: string): Observable<boolean> {
|
||||
return this.dataService.deleteByCollectionID(item, collectionID);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint based on a collection
|
||||
* @param collectionID The ID of the collection to base the endpoint on
|
||||
*/
|
||||
getCollectionEndpoint(collectionID: string): Observable<string> {
|
||||
return this.dataService.getCollectionEndpoint(collectionID);
|
||||
}
|
||||
}
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
|
@@ -1,5 +1,5 @@
|
||||
<a *ngIf="isAuthorized$ | async"
|
||||
[routerLink]="[pageRoute, 'edit']"
|
||||
[routerLink]="[pageRoute, 'edit', 'metadata']"
|
||||
class="edit-button btn btn-dark text-light btn-sm"
|
||||
[ngbTooltip]="tooltipMsg | translate">
|
||||
<i class="fas fa-pencil-alt fa-fw"></i>
|
||||
|
@@ -12,7 +12,7 @@ import { SubmissionSubmitComponent } from './submission-submit.component';
|
||||
})
|
||||
export class ThemedSubmissionSubmitComponent extends ThemedComponent<SubmissionSubmitComponent> {
|
||||
protected getComponentName(): string {
|
||||
return 'SubmissionImportExternalComponent';
|
||||
return 'SubmissionSubmitComponent';
|
||||
}
|
||||
|
||||
protected importThemedComponent(themeName: string): Promise<any> {
|
||||
|
@@ -777,10 +777,14 @@
|
||||
|
||||
"collection.edit.template.edit-button": "Edit",
|
||||
|
||||
"collection.edit.template.error": "An error occurred retrieving the template item",
|
||||
|
||||
"collection.edit.template.head": "Edit Template Item for Collection \"{{ collection }}\"",
|
||||
|
||||
"collection.edit.template.label": "Template item",
|
||||
|
||||
"collection.edit.template.loading": "Loading template item...",
|
||||
|
||||
"collection.edit.template.notifications.delete.error": "Failed to delete the item template",
|
||||
|
||||
"collection.edit.template.notifications.delete.success": "Successfully deleted the item template",
|
||||
@@ -1471,6 +1475,8 @@
|
||||
|
||||
"item.edit.breadcrumbs": "Edit Item",
|
||||
|
||||
"item.edit.tabs.disabled.tooltip": "You don't have permission to access this tab",
|
||||
|
||||
|
||||
"item.edit.tabs.mapper.head": "Collection Mapper",
|
||||
|
||||
|
Reference in New Issue
Block a user