1
0

Merge pull request #1503 from atmire/fix-delete-item-with-empty-entity-type

Fix deleting an item with an empty or invalid entity type
This commit is contained in:
Tim Donohue
2022-01-28 09:47:05 -06:00
committed by GitHub
2 changed files with 92 additions and 39 deletions

View File

@@ -3,7 +3,7 @@ import { ItemType } from '../../../core/shared/item-relationships/item-type.mode
import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { RouterStub } from '../../../shared/testing/router.stub'; import { RouterStub } from '../../../shared/testing/router.stub';
import { of as observableOf } from 'rxjs'; import { of as observableOf, EMPTY } from 'rxjs';
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
@@ -24,6 +24,8 @@ import { RelationshipType } from '../../../core/shared/item-relationships/relati
import { EntityTypeService } from '../../../core/data/entity-type.service'; import { EntityTypeService } from '../../../core/data/entity-type.service';
import { getItemEditRoute } from '../../item-page-routing-paths'; import { getItemEditRoute } from '../../item-page-routing-paths';
import { createPaginatedList } from '../../../shared/testing/utils.test'; import { createPaginatedList } from '../../../shared/testing/utils.test';
import { RelationshipTypeService } from '../../../core/data/relationship-type.service';
import { LinkService } from '../../../core/cache/builders/link.service';
let comp: ItemDeleteComponent; let comp: ItemDeleteComponent;
let fixture: ComponentFixture<ItemDeleteComponent>; let fixture: ComponentFixture<ItemDeleteComponent>;
@@ -40,6 +42,7 @@ let mockItemDataService: ItemDataService;
let routeStub; let routeStub;
let objectUpdatesServiceStub; let objectUpdatesServiceStub;
let relationshipService; let relationshipService;
let linkService;
let entityTypeService; let entityTypeService;
let notificationsServiceStub; let notificationsServiceStub;
let typesSelection; let typesSelection;
@@ -52,7 +55,12 @@ describe('ItemDeleteComponent', () => {
uuid: 'fake-uuid', uuid: 'fake-uuid',
handle: 'fake/handle', handle: 'fake/handle',
lastModified: '2018', lastModified: '2018',
isWithdrawn: true isWithdrawn: true,
metadata: {
'dspace.entity.type': [
{ value: 'Person' }
]
}
}); });
itemType = Object.assign(new ItemType(), { itemType = Object.assign(new ItemType(), {
@@ -129,6 +137,12 @@ describe('ItemDeleteComponent', () => {
} }
); );
linkService = jasmine.createSpyObj('linkService',
{
resolveLinks: relationships[0],
}
);
notificationsServiceStub = new NotificationsServiceStub(); notificationsServiceStub = new NotificationsServiceStub();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
@@ -142,6 +156,8 @@ describe('ItemDeleteComponent', () => {
{ provide: ObjectUpdatesService, useValue: objectUpdatesServiceStub }, { provide: ObjectUpdatesService, useValue: objectUpdatesServiceStub },
{ provide: RelationshipService, useValue: relationshipService }, { provide: RelationshipService, useValue: relationshipService },
{ provide: EntityTypeService, useValue: entityTypeService }, { provide: EntityTypeService, useValue: entityTypeService },
{ provide: RelationshipTypeService, useValue: {} },
{ provide: LinkService, useValue: linkService },
], schemas: [ ], schemas: [
CUSTOM_ELEMENTS_SCHEMA CUSTOM_ELEMENTS_SCHEMA
] ]
@@ -166,25 +182,45 @@ describe('ItemDeleteComponent', () => {
}); });
describe('performAction', () => { describe('performAction', () => {
it('should call delete function from the ItemDataService', () => { describe(`when there are entitytypes`, () => {
spyOn(comp, 'notify'); it('should call delete function from the ItemDataService', () => {
comp.performAction(); spyOn(comp, 'notify');
expect(mockItemDataService.delete) comp.performAction();
.toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id)); expect(mockItemDataService.delete)
expect(comp.notify).toHaveBeenCalled(); .toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id));
expect(comp.notify).toHaveBeenCalled();
});
it('should call delete function from the ItemDataService with empty types', () => {
spyOn(comp, 'notify');
jasmine.getEnv().allowRespy(true);
spyOn(entityTypeService, 'getEntityTypeRelationships').and.returnValue([]);
comp.ngOnInit();
comp.performAction();
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem.id, []);
expect(comp.notify).toHaveBeenCalled();
});
}); });
it('should call delete function from the ItemDataService with empty types', () => { describe(`when there are no entity types`, () => {
beforeEach(() => {
(comp as any).entityTypeService = jasmine.createSpyObj('entityTypeService',
{
getEntityTypeByLabel: EMPTY,
}
);
});
spyOn(comp, 'notify'); it('should call delete function from the ItemDataService', () => {
jasmine.getEnv().allowRespy(true); spyOn(comp, 'notify');
spyOn(entityTypeService, 'getEntityTypeRelationships').and.returnValue([]); comp.performAction();
comp.ngOnInit(); expect(mockItemDataService.delete)
.toHaveBeenCalledWith(mockItem.id, types.filter((type) => typesSelection[type]).map((type) => type.id));
comp.performAction(); expect(comp.notify).toHaveBeenCalled();
});
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem.id, []);
expect(comp.notify).toHaveBeenCalled();
}); });
}); });
describe('notify', () => { describe('notify', () => {

View File

@@ -1,12 +1,14 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit, OnDestroy } from '@angular/core';
import {defaultIfEmpty, filter, map, switchMap, take} from 'rxjs/operators'; import { defaultIfEmpty, filter, map, switchMap, take } from 'rxjs/operators';
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component'; import {
AbstractSimpleItemActionComponent
} from '../simple-item-action/abstract-simple-item-action.component';
import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap';
import { import {
combineLatest as observableCombineLatest, combineLatest as observableCombineLatest,
combineLatest, combineLatest,
Observable, Observable,
of as observableOf of as observableOf, Subscription
} from 'rxjs'; } from 'rxjs';
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
import { VirtualMetadata } from '../virtual-metadata/virtual-metadata.component'; import { VirtualMetadata } from '../virtual-metadata/virtual-metadata.component';
@@ -32,6 +34,7 @@ import { followLink } from '../../../shared/utils/follow-link-config.model';
import { getItemEditRoute } from '../../item-page-routing-paths'; import { getItemEditRoute } from '../../item-page-routing-paths';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { NoContent } from '../../../core/shared/NoContent.model'; import { NoContent } from '../../../core/shared/NoContent.model';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
@Component({ @Component({
selector: 'ds-item-delete', selector: 'ds-item-delete',
@@ -42,7 +45,7 @@ import { NoContent } from '../../../core/shared/NoContent.model';
*/ */
export class ItemDeleteComponent export class ItemDeleteComponent
extends AbstractSimpleItemActionComponent extends AbstractSimpleItemActionComponent
implements OnInit { implements OnInit, OnDestroy {
/** /**
* The current url of this page * The current url of this page
@@ -60,7 +63,7 @@ export class ItemDeleteComponent
* A list of the relationship types for which this item has relations as an observable. * A list of the relationship types for which this item has relations as an observable.
* The list doesn't contain duplicates. * The list doesn't contain duplicates.
*/ */
types$: Observable<RelationshipType[]>; types$: BehaviorSubject<RelationshipType[]> = new BehaviorSubject([]);
/** /**
* A map which stores the relationships of this item for each type as observable lists * A map which stores the relationships of this item for each type as observable lists
@@ -84,6 +87,11 @@ export class ItemDeleteComponent
*/ */
public modalRef: NgbModalRef; public modalRef: NgbModalRef;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
*/
private subs: Subscription[] = [];
constructor(protected route: ActivatedRoute, constructor(protected route: ActivatedRoute,
protected router: Router, protected router: Router,
protected notificationsService: NotificationsService, protected notificationsService: NotificationsService,
@@ -113,8 +121,8 @@ export class ItemDeleteComponent
this.url = this.router.url; this.url = this.router.url;
const label = this.item.firstMetadataValue('dspace.entity.type'); const label = this.item.firstMetadataValue('dspace.entity.type');
if (label !== undefined) { if (isNotEmpty(label)) {
this.types$ = this.entityTypeService.getEntityTypeByLabel(label).pipe( this.subs.push(this.entityTypeService.getEntityTypeByLabel(label).pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
switchMap((entityType) => this.entityTypeService.getEntityTypeRelationships(entityType.id)), switchMap((entityType) => this.entityTypeService.getEntityTypeRelationships(entityType.id)),
@@ -138,16 +146,14 @@ export class ItemDeleteComponent
), ),
); );
}) })
); ).subscribe((types: RelationshipType[]) => this.types$.next(types)));
} else {
this.types$ = observableOf([]);
} }
this.types$.pipe( this.subs.push(this.types$.pipe(
take(1), take(1),
).subscribe((types) => ).subscribe((types) =>
this.objectUpdatesService.initialize(this.url, types, this.item.lastModified) this.objectUpdatesService.initialize(this.url, types, this.item.lastModified)
); ));
} }
/** /**
@@ -327,7 +333,7 @@ export class ItemDeleteComponent
*/ */
performAction() { performAction() {
this.types$.pipe( this.subs.push(this.types$.pipe(
switchMap((types) => switchMap((types) =>
combineLatest( combineLatest(
types.map((type) => this.isSelected(type)) types.map((type) => this.isSelected(type))
@@ -339,13 +345,14 @@ export class ItemDeleteComponent
map((selectedTypes) => selectedTypes.map((type) => type.id)), map((selectedTypes) => selectedTypes.map((type) => type.id)),
) )
), ),
).subscribe((types) => { switchMap((types) =>
this.itemDataService.delete(this.item.id, types).pipe(getFirstCompletedRemoteData()).subscribe( this.itemDataService.delete(this.item.id, types).pipe(getFirstCompletedRemoteData())
(rd: RemoteData<NoContent>) => { )
this.notify(rd.hasSucceeded); ).subscribe(
} (rd: RemoteData<NoContent>) => {
); this.notify(rd.hasSucceeded);
}); }
));
} }
/** /**
@@ -361,4 +368,14 @@ export class ItemDeleteComponent
this.router.navigate([getItemEditRoute(this.item)]); this.router.navigate([getItemEditRoute(this.item)]);
} }
} }
/**
* Unsubscribe from all subscriptions
*/
ngOnDestroy(): void {
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
}
} }