Skip to content

Commit 8c6df7a

Browse files
authored
Merge pull request DSpace#3089 from DSpace/backport-3079-to-dspace-7_x
[Port dspace-7_x] Fixed delete item page freezing when having relationships
2 parents 8c35ca8 + 7993020 commit 8c6df7a

10 files changed

Lines changed: 103 additions & 70 deletions

src/app/core/data/base/delete-data.spec.ts

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -198,6 +198,11 @@ describe('DeleteDataImpl', () => {
198198
method: RestRequestMethod.DELETE,
199199
href: 'some-href?copyVirtualMetadata=a&copyVirtualMetadata=b&copyVirtualMetadata=c',
200200
}));
201+
202+
const callback = (rdbService.buildFromRequestUUIDAndAwait as jasmine.Spy).calls.argsFor(0)[1];
203+
callback();
204+
expect(service.invalidateByHref).toHaveBeenCalledWith('some-href');
205+
201206
done();
202207
});
203208
});

src/app/core/data/base/delete-data.ts

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -68,15 +68,16 @@ export class DeleteDataImpl<T extends CacheableObject> extends IdentifiableDataS
6868
deleteByHref(href: string, copyVirtualMetadata?: string[]): Observable<RemoteData<NoContent>> {
6969
const requestId = this.requestService.generateRequestId();
7070

71+
let deleteHref: string = href;
7172
if (copyVirtualMetadata) {
7273
copyVirtualMetadata.forEach((id) =>
73-
href += (href.includes('?') ? '&' : '?')
74+
deleteHref += (deleteHref.includes('?') ? '&' : '?')
7475
+ 'copyVirtualMetadata='
7576
+ id,
7677
);
7778
}
7879

79-
const request = new DeleteRequest(requestId, href);
80+
const request = new DeleteRequest(requestId, deleteHref);
8081
if (hasValue(this.responseMsToLive)) {
8182
request.responseMsToLive = this.responseMsToLive;
8283
}

src/app/item-page/edit-item-page/item-delete/item-delete.component.html

Lines changed: 14 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -6,30 +6,29 @@ <h1>{{headerMessage | translate: {id: item.handle} }}</h1>
66
<p>{{descriptionMessage | translate}}</p>
77
<ds-modify-item-overview [item]="item"></ds-modify-item-overview>
88

9-
<ng-container *ngVar="(types$ | async) as types">
9+
<ng-container *ngVar="(typeDTOs$ | async) as types">
1010

1111
<div *ngIf="types && types.length > 0" class="mb-4">
1212

1313
{{'virtual-metadata.delete-item.info' | translate}}
1414

15-
<div *ngFor="let type of types" class="mb-4">
16-
17-
<div *ngVar="(isSelected(type) | async) as selected"
15+
<div *ngFor="let typeDto of types" class="mb-4">
16+
<div *ngVar="(typeDto.isSelected$ | async) as selected"
1817
class="d-flex flex-row">
1918

20-
<div class="m-2" (click)="setSelected(type, !selected)">
19+
<div class="m-2" (click)="setSelected(typeDto.relationshipType, !selected)">
2120
<label>
22-
<input type="checkbox" [checked]="selected">
21+
<input type="checkbox" [checked]="selected" [disabled]="isDeleting$ | async">
2322
</label>
2423
</div>
2524

2625
<div class="flex-column flex-grow-1">
27-
<h5 (click)="setSelected(type, !selected)">
28-
{{getRelationshipMessageKey(getLabel(type) | async) | translate}}
26+
<h5 (click)="setSelected(typeDto.relationshipType, !selected)">
27+
{{getRelationshipMessageKey(typeDto.label$ | async) | translate}}
2928
</h5>
30-
<div *ngFor="let relationship of (getRelationships(type) | async)"
29+
<div *ngFor="let relationshipDto of (typeDto.relationshipDTOs$ | async)"
3130
class="d-flex flex-row">
32-
<ng-container *ngVar="(getRelatedItem(relationship) | async) as relatedItem">
31+
<ng-container *ngVar="(relationshipDto.relatedItem$ | async) as relatedItem">
3332

3433
<ds-listable-object-component-loader
3534
*ngIf="relatedItem"
@@ -46,7 +45,7 @@ <h5 (click)="setSelected(type, !selected)">
4645
</div>
4746

4847
<ng-template #virtualMetadataModal>
49-
<div>
48+
<div class="thumb-font-1">
5049
<div class="modal-header">
5150
{{'virtual-metadata.delete-item.modal-head' | translate}}
5251
<button type="button" class="close"
@@ -60,7 +59,7 @@ <h5 (click)="setSelected(type, !selected)">
6059
[object]="relatedItem"
6160
[viewMode]="viewMode">
6261
</ds-listable-object-component-loader>
63-
<div *ngFor="let metadata of (getVirtualMetadata(relationship) | async)">
62+
<div *ngFor="let metadata of (relationshipDto.virtualMetadata$ | async)">
6463
<div>
6564
<div class="font-weight-bold">
6665
{{metadata.metadataField}}
@@ -87,10 +86,11 @@ <h5 (click)="setSelected(type, !selected)">
8786
</ng-container>
8887

8988
<div class="space-children-mr">
90-
<button (click)="performAction()"
89+
<button [disabled]="isDeleting$ | async" (click)="performAction()"
9190
class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
9291
</button>
93-
<button [routerLink]="[itemPageRoute, 'edit']" class="btn btn-outline-secondary cancel">
92+
<button [disabled]="isDeleting$ | async" [routerLink]="[itemPageRoute, 'edit']"
93+
class="btn btn-outline-secondary cancel">
9494
{{cancelMessage| translate}}
9595
</button>
9696
</div>

src/app/item-page/edit-item-page/item-delete/item-delete.component.ts

Lines changed: 67 additions & 26 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,4 @@
1+
// eslint-disable-next-line max-classes-per-file
12
import { Component, Input, OnInit, OnDestroy } from '@angular/core';
23
import { defaultIfEmpty, filter, map, switchMap, take } from 'rxjs/operators';
34
import {
@@ -37,6 +38,34 @@ import { getItemEditRoute } from '../../item-page-routing-paths';
3738
import { RemoteData } from '../../../core/data/remote-data';
3839
import { NoContent } from '../../../core/shared/NoContent.model';
3940

41+
/**
42+
* Data Transfer Object used to prevent the HTML template to call function returning Observables
43+
*/
44+
class RelationshipTypeDTO {
45+
46+
relationshipType: RelationshipType;
47+
48+
isSelected$: Observable<boolean>;
49+
50+
label$: Observable<string>;
51+
52+
relationshipDTOs$: Observable<RelationshipDTO[]>;
53+
54+
}
55+
56+
/**
57+
* Data Transfer Object used to prevent the HTML template to call function returning Observables
58+
*/
59+
class RelationshipDTO {
60+
61+
relationship: Relationship;
62+
63+
relatedItem$: Observable<Item>;
64+
65+
virtualMetadata$: Observable<VirtualMetadata[]>;
66+
67+
}
68+
4069
@Component({
4170
selector: 'ds-item-delete',
4271
templateUrl: '../item-delete/item-delete.component.html'
@@ -64,7 +93,7 @@ export class ItemDeleteComponent
6493
* A list of the relationship types for which this item has relations as an observable.
6594
* The list doesn't contain duplicates.
6695
*/
67-
types$: BehaviorSubject<RelationshipType[]> = new BehaviorSubject([]);
96+
typeDTOs$: BehaviorSubject<RelationshipTypeDTO[]> = new BehaviorSubject([]);
6897

6998
/**
7099
* A map which stores the relationships of this item for each type as observable lists
@@ -93,6 +122,8 @@ export class ItemDeleteComponent
93122
*/
94123
private subs: Subscription[] = [];
95124

125+
public isDeleting$: BehaviorSubject<boolean> = new BehaviorSubject(false);
126+
96127
constructor(protected route: ActivatedRoute,
97128
protected router: Router,
98129
protected notificationsService: NotificationsService,
@@ -146,14 +177,25 @@ export class ItemDeleteComponent
146177
}, [])
147178
),
148179
);
149-
})
150-
).subscribe((types: RelationshipType[]) => this.types$.next(types)));
180+
}),
181+
).subscribe((types: RelationshipType[]) => this.typeDTOs$.next(types.map((relationshipType: RelationshipType) => Object.assign(new RelationshipTypeDTO(), {
182+
relationshipType: relationshipType,
183+
isSelected$: this.isSelected(relationshipType),
184+
label$: this.getLabel(relationshipType),
185+
relationshipDTOs$: this.getRelationships(relationshipType).pipe(
186+
map((relationships: Relationship[]) => relationships.map((relationship: Relationship) => Object.assign(new RelationshipDTO(), {
187+
relationship: relationship,
188+
relatedItem$: this.getRelatedItem(relationship),
189+
virtualMetadata$: this.getVirtualMetadata(relationship),
190+
} as RelationshipDTO))),
191+
),
192+
})))));
151193
}
152194

153-
this.subs.push(this.types$.pipe(
195+
this.subs.push(this.typeDTOs$.pipe(
154196
take(1),
155-
).subscribe((types) =>
156-
this.objectUpdatesService.initialize(this.url, types, this.item.lastModified)
197+
).subscribe((types: RelationshipTypeDTO[]) =>
198+
this.objectUpdatesService.initialize(this.url, types.map((relationshipTypeDto: RelationshipTypeDTO) => relationshipTypeDto.relationshipType), this.item.lastModified),
157199
));
158200
}
159201

@@ -326,34 +368,33 @@ export class ItemDeleteComponent
326368
* @param selected whether the type should be selected
327369
*/
328370
setSelected(type: RelationshipType, selected: boolean): void {
329-
this.objectUpdatesService.setSelectedVirtualMetadata(this.url, this.item.uuid, type.uuid, selected);
371+
if (this.isDeleting$.value === false) {
372+
this.objectUpdatesService.setSelectedVirtualMetadata(this.url, this.item.uuid, type.uuid, selected);
373+
}
330374
}
331375

332376
/**
333377
* Perform the delete operation
334378
*/
335-
performAction() {
336-
337-
this.subs.push(this.types$.pipe(
338-
switchMap((types) =>
379+
performAction(): void {
380+
this.isDeleting$.next(true);
381+
this.subs.push(this.typeDTOs$.pipe(
382+
switchMap((types: RelationshipTypeDTO[]) =>
339383
combineLatest(
340-
types.map((type) => this.isSelected(type))
384+
types.map((type: RelationshipTypeDTO) => type.isSelected$),
341385
).pipe(
342386
defaultIfEmpty([]),
343-
map((selection) => types.filter(
344-
(type, index) => selection[index]
387+
map((selection: boolean[]) => types.filter(
388+
(type: RelationshipTypeDTO, index: number) => selection[index],
345389
)),
346-
map((selectedTypes) => selectedTypes.map((type) => type.id)),
347-
)
390+
map((selectedDtoTypes: RelationshipTypeDTO[]) => selectedDtoTypes.map((typeDto: RelationshipTypeDTO) => typeDto.relationshipType.id)),
391+
),
348392
),
349-
switchMap((types) =>
350-
this.itemDataService.delete(this.item.id, types).pipe(getFirstCompletedRemoteData())
351-
)
352-
).subscribe(
353-
(rd: RemoteData<NoContent>) => {
354-
this.notify(rd.hasSucceeded);
355-
}
356-
));
393+
switchMap((types: string[]) => this.itemDataService.delete(this.item.id, types)),
394+
getFirstCompletedRemoteData(),
395+
).subscribe((rd: RemoteData<NoContent>) => {
396+
this.notify(rd.hasSucceeded);
397+
}));
357398
}
358399

359400
/**
@@ -363,10 +404,10 @@ export class ItemDeleteComponent
363404
notify(succeeded: boolean) {
364405
if (succeeded) {
365406
this.notificationsService.success(this.translateService.get('item.edit.' + this.messageKey + '.success'));
366-
this.router.navigate(['']);
407+
void this.router.navigate(['']);
367408
} else {
368409
this.notificationsService.error(this.translateService.get('item.edit.' + this.messageKey + '.error'));
369-
this.router.navigate([getItemEditRoute(this.item)]);
410+
void this.router.navigate([getItemEditRoute(this.item)]);
370411
}
371412
}
372413

src/app/item-page/full/full-item-page.component.spec.ts

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,7 +15,6 @@ import { BehaviorSubject, of as observableOf } from 'rxjs';
1515
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
1616
import { By } from '@angular/platform-browser';
1717
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
18-
import { AuthService } from '../../core/auth/auth.service';
1918
import { createPaginatedList } from '../../shared/testing/utils.test';
2019
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
2120
import { createRelationshipsObservable } from '../simple/item-types/shared/item.component.spec';
@@ -54,7 +53,6 @@ describe('FullItemPageComponent', () => {
5453
let comp: FullItemPageComponent;
5554
let fixture: ComponentFixture<FullItemPageComponent>;
5655

57-
let authService: AuthService;
5856
let routeStub: ActivatedRouteStub;
5957
let routeData;
6058
let authorizationDataService: AuthorizationDataService;
@@ -75,11 +73,6 @@ describe('FullItemPageComponent', () => {
7573
};
7674

7775
beforeEach(waitForAsync(() => {
78-
authService = jasmine.createSpyObj('authService', {
79-
isAuthenticated: observableOf(true),
80-
setRedirectUrl: {}
81-
});
82-
8376
routeData = {
8477
dso: createSuccessfulRemoteDataObject(mockItem),
8578
};
@@ -117,7 +110,6 @@ describe('FullItemPageComponent', () => {
117110
{ provide: ActivatedRoute, useValue: routeStub },
118111
{ provide: ItemDataService, useValue: {} },
119112
{ provide: MetadataService, useValue: metadataServiceStub },
120-
{ provide: AuthService, useValue: authService },
121113
{ provide: AuthorizationDataService, useValue: authorizationDataService },
122114
{ provide: ServerResponseService, useValue: serverResponseService },
123115
{ provide: SignpostingDataService, useValue: signpostingDataService },

src/app/item-page/full/full-item-page.component.ts

Lines changed: 1 addition & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@ import { Item } from '../../core/shared/item.model';
1313

1414
import { fadeInOut } from '../../shared/animations/fade';
1515
import { hasValue } from '../../shared/empty.util';
16-
import { AuthService } from '../../core/auth/auth.service';
1716
import { Location } from '@angular/common';
1817
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
1918
import { ServerResponseService } from '../../core/services/server-response.service';
@@ -49,15 +48,14 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit,
4948
protected route: ActivatedRoute,
5049
protected router: Router,
5150
protected items: ItemDataService,
52-
protected authService: AuthService,
5351
protected authorizationService: AuthorizationDataService,
5452
protected _location: Location,
5553
protected responseService: ServerResponseService,
5654
protected signpostingDataService: SignpostingDataService,
5755
protected linkHeadService: LinkHeadService,
5856
@Inject(PLATFORM_ID) protected platformId: string,
5957
) {
60-
super(route, router, items, authService, authorizationService, responseService, signpostingDataService, linkHeadService, platformId);
58+
super(route, router, items, authorizationService, responseService, signpostingDataService, linkHeadService, platformId);
6159
}
6260

6361
/*** AoT inheritance fix, will hopefully be resolved in the near future **/

src/app/item-page/item-page.resolver.spec.ts

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,14 +3,15 @@ import { createSuccessfulRemoteDataObject$ } from '../shared/remote-data.utils';
33
import { DSpaceObject } from '../core/shared/dspace-object.model';
44
import { MetadataValueFilter } from '../core/shared/metadata.models';
55
import { first } from 'rxjs/operators';
6-
import { Router } from '@angular/router';
6+
import { Router, RouterModule } from '@angular/router';
77
import { TestBed } from '@angular/core/testing';
8-
import { RouterTestingModule } from '@angular/router/testing';
8+
import { AuthServiceStub } from '../shared/testing/auth-service.stub';
9+
import { AuthService } from '../core/auth/auth.service';
910

1011
describe('ItemPageResolver', () => {
1112
beforeEach(() => {
1213
TestBed.configureTestingModule({
13-
imports: [RouterTestingModule.withRoutes([{
14+
imports: [RouterModule.forRoot([{
1415
path: 'entities/:entity-type/:id',
1516
component: {} as any
1617
}])]
@@ -21,7 +22,8 @@ describe('ItemPageResolver', () => {
2122
let resolver: ItemPageResolver;
2223
let itemService: any;
2324
let store: any;
24-
let router: any;
25+
let router: Router;
26+
let authService: AuthServiceStub;
2527

2628
const uuid = '1234-65487-12354-1235';
2729
let item: DSpaceObject;
@@ -41,7 +43,8 @@ describe('ItemPageResolver', () => {
4143
store = jasmine.createSpyObj('store', {
4244
dispatch: {},
4345
});
44-
resolver = new ItemPageResolver(itemService, store, router);
46+
authService = new AuthServiceStub();
47+
resolver = new ItemPageResolver(itemService, store, router, authService as unknown as AuthService);
4548
});
4649

4750
it('should redirect to the correct route for the entity type', (done) => {

src/app/item-page/item-page.resolver.ts

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@ import { map } from 'rxjs/operators';
99
import { hasValue } from '../shared/empty.util';
1010
import { getItemPageRoute } from './item-page-routing-paths';
1111
import { ItemResolver } from './item.resolver';
12+
import { redirectOn4xx } from '../core/shared/authorized.operators';
13+
import { AuthService } from '../core/auth/auth.service';
1214

1315
/**
1416
* This class represents a resolver that requests a specific item before the route is activated and will redirect to the
@@ -19,7 +21,8 @@ export class ItemPageResolver extends ItemResolver {
1921
constructor(
2022
protected itemService: ItemDataService,
2123
protected store: Store<any>,
22-
protected router: Router
24+
protected router: Router,
25+
protected authService: AuthService,
2326
) {
2427
super(itemService, store, router);
2528
}
@@ -33,6 +36,7 @@ export class ItemPageResolver extends ItemResolver {
3336
*/
3437
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<RemoteData<Item>> {
3538
return super.resolve(route, state).pipe(
39+
redirectOn4xx(this.router, this.authService),
3640
map((rd: RemoteData<Item>) => {
3741
if (rd.hasSucceeded && hasValue(rd.payload)) {
3842
const thisRoute = state.url;

0 commit comments

Comments
 (0)