From 8e59b7d0b0798ed7f7f7fdc952dbcfe1c9f773b6 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 11 Apr 2024 17:51:59 +0200 Subject: [PATCH 1/8] also add relationships one by one on the edit item relationships tab --- .../core/data/relationship-data.service.ts | 24 ++- .../item-relationships.component.ts | 174 +++++++++++------- 2 files changed, 124 insertions(+), 74 deletions(-) diff --git a/src/app/core/data/relationship-data.service.ts b/src/app/core/data/relationship-data.service.ts index 46a51a2d01..f9c2c7f16a 100644 --- a/src/app/core/data/relationship-data.service.ts +++ b/src/app/core/data/relationship-data.service.ts @@ -114,8 +114,11 @@ export class RelationshipDataService extends IdentifiableDataService> { + deleteRelationship(id: string, copyVirtualMetadata: string, shouldRefresh = true): Observable> { return this.getRelationshipEndpoint(id).pipe( isNotEmptyOperator(), take(1), @@ -126,7 +129,11 @@ export class RelationshipDataService extends IdentifiableDataService this.rdbService.buildFromRequestUUID(restRequest.uuid)), getFirstCompletedRemoteData(), - tap(() => this.refreshRelationshipItemsInCacheByRelationship(id)), + tap(() => { + if (shouldRefresh) { + this.refreshRelationshipItemsInCacheByRelationship(id); + } + }), ); } @@ -137,8 +144,11 @@ export class RelationshipDataService extends IdentifiableDataService> { + addRelationship(typeId: string, item1: Item, item2: Item, leftwardValue?: string, rightwardValue?: string, shouldRefresh = true): Observable> { const options: HttpOptions = Object.create({}); let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'text/uri-list'); @@ -153,8 +163,12 @@ export class RelationshipDataService extends IdentifiableDataService this.rdbService.buildFromRequestUUID(restRequest.uuid)), getFirstCompletedRemoteData(), - tap(() => this.refreshRelationshipItemsInCache(item1)), - tap(() => this.refreshRelationshipItemsInCache(item2)), + tap(() => { + if (shouldRefresh) { + this.refreshRelationshipItemsInCache(item1); + this.refreshRelationshipItemsInCache(item2); + } + }), ) as Observable>; } diff --git a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts index 8f7c43e79f..433e0a9493 100644 --- a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -4,8 +4,14 @@ import { DeleteRelationship, RelationshipIdentifiable, } from '../../../core/data/object-updates/object-updates.reducer'; -import { map, startWith, switchMap, take } from 'rxjs/operators'; -import { combineLatest as observableCombineLatest, of as observableOf, zip as observableZip, Observable } from 'rxjs'; +import { map, startWith, switchMap, take, concatMap, toArray, tap } from 'rxjs/operators'; +import { + combineLatest as observableCombineLatest, + of as observableOf, + zip as observableZip, + Observable, + BehaviorSubject, EMPTY +} from 'rxjs'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { ItemDataService } from '../../../core/data/item-data.service'; @@ -30,6 +36,7 @@ import { FieldChangeType } from '../../../core/data/object-updates/field-change- import { RelationshipTypeDataService } from '../../../core/data/relationship-type-data.service'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { HALLink } from '../../../core/shared/hal-link.model'; @Component({ selector: 'ds-item-relationships', @@ -50,7 +57,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { /** * The item's entity type as an observable */ - entityType$: Observable; + entityType$: BehaviorSubject = new BehaviorSubject(undefined); constructor( public itemService: ItemDataService, @@ -82,13 +89,13 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { map((relationshipTypes: PaginatedList) => relationshipTypes.page) ); - this.entityType$ = this.entityTypeService.getEntityTypeByLabel(label).pipe( + this.entityTypeService.getEntityTypeByLabel(label).pipe( getFirstSucceededRemoteData(), getRemoteDataPayload(), - ); + ).subscribe((type) => this.entityType$.next(type)); } else { - this.entityType$ = observableOf(undefined); + this.entityType$.next(undefined); } } @@ -106,8 +113,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { public submit(): void { // Get all the relationships that should be removed - const removedRelationshipIDs$: Observable = this.relationshipService.getItemRelationshipsArray(this.item).pipe( - startWith([]), + const removeUpdates$: Observable = this.relationshipService.getItemRelationshipsArray(this.item).pipe( map((relationships: Relationship[]) => relationships.map((relationship) => Object.assign(new Relationship(), relationship, { uuid: relationship.id }) )), @@ -117,83 +123,113 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { map((fieldUpdates: FieldUpdates) => Object.values(fieldUpdates) .filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE) - .map((fieldUpdate: FieldUpdate) => fieldUpdate.field as DeleteRelationship) ), + take(1) ); - const addRelatedItems$: Observable = this.objectUpdatesService.getFieldUpdates(this.url, []).pipe( + const addUpdates$: Observable = this.objectUpdatesService.getFieldUpdates(this.url, []).pipe( map((fieldUpdates: FieldUpdates) => Object.values(fieldUpdates) .filter((fieldUpdate: FieldUpdate) => hasValue(fieldUpdate)) .filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.ADD) - .map((fieldUpdate: FieldUpdate) => fieldUpdate.field as RelationshipIdentifiable) ), + take(1) ); - observableCombineLatest( - removedRelationshipIDs$, - addRelatedItems$, - ).pipe( + observableCombineLatest([ + removeUpdates$, + addUpdates$, + ]).pipe( take(1), - ).subscribe(([removeRelationshipIDs, addRelatedItems]) => { - const actions = [ - this.deleteRelationships(removeRelationshipIDs), - this.addRelationships(addRelatedItems), - ]; - actions.forEach((action) => - action.subscribe((response) => { - if (response.length > 0) { - this.initializeOriginalFields(); - this.cdr.detectChanges(); - this.displayNotifications(response); - this.modalService.dismissAll(); - } - }) - ); + switchMap(([removeUpdates, addUpdates]) => [...removeUpdates, ...addUpdates]), + concatMap((update: FieldUpdate) => { + if (update.changeType === FieldChangeType.REMOVE) { + return this.deleteRelationship(update.field as DeleteRelationship).pipe(take(1)) + } else if (update.changeType === FieldChangeType.ADD) { + return this.addRelationship(update.field as RelationshipIdentifiable).pipe( + take(1), + switchMap((relationshipRD: RemoteData) => { + if (relationshipRD.hasSucceeded) { + // Set the newly related item to stale, so its relationships will update to include + // the new one. Only set the current item to stale at the very end so we only do it + // once + const { leftItem, rightItem } = relationshipRD.payload._links; + if (leftItem.href === this.item.self) { + return this.itemService.invalidateByHref(rightItem.href).pipe( + // when it's invalidated, emit the original relationshipRD for use in the pipe below + map(() => relationshipRD) + ); + } else { + return this.itemService.invalidateByHref(leftItem.href).pipe( + // when it's invalidated, emit the original relationshipRD for use in the pipe below + map(() => relationshipRD) + ); + } + } + else { + return [relationshipRD]; + } + }) + ) + } else { + return EMPTY; + } + }), + toArray(), + switchMap((responses) => { + // once all relationships are made and all related items have been invalidated, invalidate + // the current item + return this.itemService.invalidateByHref(this.item.self).pipe( + map(() => responses) + ); + }) + ).subscribe((responses) => { + if (responses.length > 0) { + this.initializeOriginalFields(); + this.displayNotifications(responses); + this.modalService.dismissAll(); + } }); } - deleteRelationships(deleteRelationshipIDs: DeleteRelationship[]): Observable[]> { - return observableZip(...deleteRelationshipIDs.map((deleteRelationship) => { - let copyVirtualMetadata: string; - if (deleteRelationship.keepLeftVirtualMetadata && deleteRelationship.keepRightVirtualMetadata) { - copyVirtualMetadata = 'all'; - } else if (deleteRelationship.keepLeftVirtualMetadata) { - copyVirtualMetadata = 'left'; - } else if (deleteRelationship.keepRightVirtualMetadata) { - copyVirtualMetadata = 'right'; - } else { - copyVirtualMetadata = 'none'; - } - return this.relationshipService.deleteRelationship(deleteRelationship.uuid, copyVirtualMetadata); - } - )); + deleteRelationship(deleteRelationship: DeleteRelationship): Observable> { + let copyVirtualMetadata: string; + if (deleteRelationship.keepLeftVirtualMetadata && deleteRelationship.keepRightVirtualMetadata) { + copyVirtualMetadata = 'all'; + } else if (deleteRelationship.keepLeftVirtualMetadata) { + copyVirtualMetadata = 'left'; + } else if (deleteRelationship.keepRightVirtualMetadata) { + copyVirtualMetadata = 'right'; + } else { + copyVirtualMetadata = 'none'; + } + + return this.relationshipService.deleteRelationship(deleteRelationship.uuid, copyVirtualMetadata, false); } - addRelationships(addRelatedItems: RelationshipIdentifiable[]): Observable[]> { - return observableZip(...addRelatedItems.map((addRelationship) => - this.entityType$.pipe( - switchMap((entityType) => this.entityTypeService.isLeftType(addRelationship.type, entityType)), - switchMap((isLeftType) => { - let leftItem: Item; - let rightItem: Item; - let leftwardValue: string; - let rightwardValue: string; - if (isLeftType) { - leftItem = this.item; - rightItem = addRelationship.relatedItem; - leftwardValue = null; - rightwardValue = addRelationship.nameVariant; - } else { - leftItem = addRelationship.relatedItem; - rightItem = this.item; - leftwardValue = addRelationship.nameVariant; - rightwardValue = null; - } - return this.relationshipService.addRelationship(addRelationship.type.id, leftItem, rightItem, leftwardValue, rightwardValue); - }), - ) - )); + addRelationship(addRelationship: RelationshipIdentifiable): Observable> { + return this.entityType$.pipe( + switchMap((entityType) => this.entityTypeService.isLeftType(addRelationship.type, entityType)), + switchMap((isLeftType) => { + let leftItem: Item; + let rightItem: Item; + let leftwardValue: string; + let rightwardValue: string; + if (isLeftType) { + leftItem = this.item; + rightItem = addRelationship.relatedItem; + leftwardValue = null; + rightwardValue = addRelationship.nameVariant; + } else { + leftItem = addRelationship.relatedItem; + rightItem = this.item; + leftwardValue = addRelationship.nameVariant; + rightwardValue = null; + } + return this.relationshipService.addRelationship(addRelationship.type.id, leftItem, rightItem, leftwardValue, rightwardValue, false); + }), + ) + } /** From e4b098e64dd98af3d87c8f9f2f28f9c3ea3c0336 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 25 Apr 2024 10:56:54 +0200 Subject: [PATCH 2/8] update test --- .../item-relationships.component.spec.ts | 2 +- .../item-relationships.component.ts | 14 +++++--------- 2 files changed, 6 insertions(+), 10 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts index aa711c8ea3..b7bd0f789f 100644 --- a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts @@ -266,7 +266,7 @@ describe('ItemRelationshipsComponent', () => { }); it('it should delete the correct relationship', () => { - expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid, 'left'); + expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid, 'left', false); }); }); diff --git a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts index 433e0a9493..033f22a40b 100644 --- a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -4,11 +4,9 @@ import { DeleteRelationship, RelationshipIdentifiable, } from '../../../core/data/object-updates/object-updates.reducer'; -import { map, startWith, switchMap, take, concatMap, toArray, tap } from 'rxjs/operators'; +import { map, switchMap, take, concatMap, toArray } from 'rxjs/operators'; import { combineLatest as observableCombineLatest, - of as observableOf, - zip as observableZip, Observable, BehaviorSubject, EMPTY } from 'rxjs'; @@ -36,7 +34,6 @@ import { FieldChangeType } from '../../../core/data/object-updates/field-change- import { RelationshipTypeDataService } from '../../../core/data/relationship-type-data.service'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -import { HALLink } from '../../../core/shared/hal-link.model'; @Component({ selector: 'ds-item-relationships', @@ -144,7 +141,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { switchMap(([removeUpdates, addUpdates]) => [...removeUpdates, ...addUpdates]), concatMap((update: FieldUpdate) => { if (update.changeType === FieldChangeType.REMOVE) { - return this.deleteRelationship(update.field as DeleteRelationship).pipe(take(1)) + return this.deleteRelationship(update.field as DeleteRelationship).pipe(take(1)); } else if (update.changeType === FieldChangeType.ADD) { return this.addRelationship(update.field as RelationshipIdentifiable).pipe( take(1), @@ -165,12 +162,11 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { map(() => relationshipRD) ); } - } - else { + } else { return [relationshipRD]; } }) - ) + ); } else { return EMPTY; } @@ -228,7 +224,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { } return this.relationshipService.addRelationship(addRelationship.type.id, leftItem, rightItem, leftwardValue, rightwardValue, false); }), - ) + ); } From 9f3ee328580719006a08b3bfd8bcbedf35944d12 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 8 May 2024 17:48:33 +0200 Subject: [PATCH 3/8] fix issue where a submit emitted from the edit relationship modal wouldn't arrive in the edit relationships component --- src/app/app.component.ts | 7 +- .../object-updates/object-updates.reducer.ts | 2 + .../core/data/relationship-data.service.ts | 8 +- .../edit-item-relationships.service.spec.ts | 16 ++ .../edit-item-relationships.service.ts | 186 ++++++++++++++++++ .../edit-relationship-list.component.ts | 167 +++++++++------- .../edit-relationship.component.ts | 10 +- .../item-relationships.component.html | 3 +- .../item-relationships.component.ts | 146 +------------- 9 files changed, 320 insertions(+), 225 deletions(-) create mode 100644 src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.spec.ts create mode 100644 src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts diff --git a/src/app/app.component.ts b/src/app/app.component.ts index ba7b738227..b77d53c81d 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,4 +1,4 @@ -import { distinctUntilChanged, take, withLatestFrom } from 'rxjs/operators'; +import { distinctUntilChanged, take, withLatestFrom, delay } from 'rxjs/operators'; import { DOCUMENT, isPlatformBrowser } from '@angular/common'; import { AfterViewInit, @@ -114,7 +114,10 @@ export class AppComponent implements OnInit, AfterViewInit { } ngAfterViewInit() { - this.router.events.subscribe((event) => { + this.router.events.pipe( + // delay(0) to prevent "Expression has changed after it was checked" errors + delay(0) + ).subscribe((event) => { if (event instanceof NavigationStart) { distinctNext(this.isRouteLoading$, true); } else if ( diff --git a/src/app/core/data/object-updates/object-updates.reducer.ts b/src/app/core/data/object-updates/object-updates.reducer.ts index 14bacc52db..da18a28d3f 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.ts @@ -58,6 +58,8 @@ export interface VirtualMetadataSource { export interface RelationshipIdentifiable extends Identifiable { nameVariant?: string; + originalItem: Item; + originalIsLeft: boolean relatedItem: Item; relationship: Relationship; type: RelationshipType; diff --git a/src/app/core/data/relationship-data.service.ts b/src/app/core/data/relationship-data.service.ts index f9c2c7f16a..8d8f62b14e 100644 --- a/src/app/core/data/relationship-data.service.ts +++ b/src/app/core/data/relationship-data.service.ts @@ -309,7 +309,13 @@ export class RelationshipDataService extends IdentifiableDataService { + let service: EditItemRelationshipsService; + + beforeEach(() => { + TestBed.configureTestingModule({}); + service = TestBed.inject(EditItemRelationshipsService); + }); + + it('should be created', () => { + expect(service).toBeTruthy(); + }); +}); diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts b/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts new file mode 100644 index 0000000000..cf29552cad --- /dev/null +++ b/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts @@ -0,0 +1,186 @@ +import { Injectable } from '@angular/core'; +import { take, map, switchMap, concatMap, toArray } from 'rxjs/operators'; +import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model'; +import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; +import { hasValue } from '../../../shared/empty.util'; +import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; +import { + DeleteRelationship, + RelationshipIdentifiable +} from '../../../core/data/object-updates/object-updates.reducer'; +import { RemoteData } from '../../../core/data/remote-data'; +import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; +import { EMPTY, Observable } from 'rxjs'; +import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; +import { ItemDataService } from '../../../core/data/item-data.service'; +import { Item } from '../../../core/shared/item.model'; +import { NoContent } from '../../../core/shared/NoContent.model'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { RelationshipDataService } from '../../../core/data/relationship-data.service'; +import { EntityTypeDataService } from '../../../core/data/entity-type-data.service'; +import { TranslateService } from '@ngx-translate/core'; + +@Injectable({ + providedIn: 'root' +}) +export class EditItemRelationshipsService { + public notificationsPrefix = 'static-pages.form.notification'; + + constructor( + public itemService: ItemDataService, + public objectUpdatesService: ObjectUpdatesService, + public notificationsService: NotificationsService, + protected modalService: NgbModal, + public relationshipService: RelationshipDataService, + public entityTypeService: EntityTypeDataService, + public translateService: TranslateService, + ) { } + + + /** + * Resolve the currently selected related items back to relationships and send a delete request for each of the relationships found + * Make sure the lists are refreshed afterwards and notifications are sent for success and errors + */ + public submit(item: Item, url: string): void { + this.objectUpdatesService.getFieldUpdates(url, [], true).pipe( + map((fieldUpdates: FieldUpdates) => + Object.values(fieldUpdates) + .filter((fieldUpdate: FieldUpdate) => hasValue(fieldUpdate)) + .filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.ADD || fieldUpdate.changeType === FieldChangeType.REMOVE) + ), + take(1), + // emit each update in the array separately + switchMap((updates) => updates), + // process each update one by one, while waiting for the previous to finish + concatMap((update: FieldUpdate) => { + if (update.changeType === FieldChangeType.REMOVE) { + return this.deleteRelationship(update.field as DeleteRelationship).pipe(take(1)); + } else if (update.changeType === FieldChangeType.ADD) { + return this.addRelationship(update.field as RelationshipIdentifiable).pipe( + take(1), + switchMap((relationshipRD: RemoteData) => { + if (relationshipRD.hasSucceeded) { + // Set the newly related item to stale, so its relationships will update to include + // the new one. Only set the current item to stale at the very end so we only do it + // once + const { leftItem, rightItem } = relationshipRD.payload._links; + if (leftItem.href === item.self) { + return this.itemService.invalidateByHref(rightItem.href).pipe( + // when it's invalidated, emit the original relationshipRD for use in the pipe below + map(() => relationshipRD) + ); + } else { + return this.itemService.invalidateByHref(leftItem.href).pipe( + // when it's invalidated, emit the original relationshipRD for use in the pipe below + map(() => relationshipRD) + ); + } + } else { + return [relationshipRD]; + } + }) + ); + } else { + return EMPTY; + } + }), + toArray(), + switchMap((responses) => { + // once all relationships are made and all related items have been invalidated, invalidate + // the current item + return this.itemService.invalidateByHref(item.self).pipe( + map(() => responses) + ); + }) + ).subscribe((responses) => { + if (responses.length > 0) { + this.initializeOriginalFields(item, url); + this.displayNotifications(responses); + this.modalService.dismissAll(); + } + }); + } + + /** + * Sends all initial values of this item to the object updates service + */ + public initializeOriginalFields(item: Item, url: string) { + return this.relationshipService.getRelatedItems(item).pipe( + take(1), + ).subscribe((items: Item[]) => { + this.objectUpdatesService.initialize(url, items, item.lastModified); + }); + } + + deleteRelationship(deleteRelationship: DeleteRelationship): Observable> { + let copyVirtualMetadata: string; + if (deleteRelationship.keepLeftVirtualMetadata && deleteRelationship.keepRightVirtualMetadata) { + copyVirtualMetadata = 'all'; + } else if (deleteRelationship.keepLeftVirtualMetadata) { + copyVirtualMetadata = 'left'; + } else if (deleteRelationship.keepRightVirtualMetadata) { + copyVirtualMetadata = 'right'; + } else { + copyVirtualMetadata = 'none'; + } + + return this.relationshipService.deleteRelationship(deleteRelationship.uuid, copyVirtualMetadata, false); + } + + addRelationship(addRelationship: RelationshipIdentifiable): Observable> { + let leftItem: Item; + let rightItem: Item; + let leftwardValue: string; + let rightwardValue: string; + if (addRelationship.originalIsLeft) { + leftItem = addRelationship.originalItem; + rightItem = addRelationship.relatedItem; + leftwardValue = null; + rightwardValue = addRelationship.nameVariant; + } else { + leftItem = addRelationship.relatedItem; + rightItem = addRelationship.originalItem; + leftwardValue = addRelationship.nameVariant; + rightwardValue = null; + } + return this.relationshipService.addRelationship(addRelationship.type.id, leftItem, rightItem, leftwardValue, rightwardValue, false); + } + + /** + * Display notifications + * - Error notification for each failed response with their message + * - Success notification in case there's at least one successful response + * @param responses + */ + displayNotifications(responses: RemoteData[]) { + const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); + const successfulResponses = responses.filter((response: RemoteData) => response.hasSucceeded); + + failedResponses.forEach((response: RemoteData) => { + this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage); + }); + if (successfulResponses.length > 0) { + this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); + } + } + + + + /** + * Get translated notification title + * @param key + */ + getNotificationTitle(key: string) { + return this.translateService.instant(this.notificationsPrefix + key + '.title'); + } + + /** + * Get translated notification content + * @param key + */ + getNotificationContent(key: string) { + return this.translateService.instant(this.notificationsPrefix + key + '.content'); + + } +} diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts index b8542f5806..b66fa67dd8 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -5,6 +5,7 @@ import { ObjectUpdatesService } from '../../../../core/data/object-updates/objec import { BehaviorSubject, combineLatest as observableCombineLatest, + EMPTY, from as observableFrom, Observable, Subscription @@ -14,10 +15,22 @@ import { } from '../../../../core/data/object-updates/object-updates.reducer'; import { RelationshipDataService } from '../../../../core/data/relationship-data.service'; import { Item } from '../../../../core/shared/item.model'; -import { defaultIfEmpty, map, mergeMap, startWith, switchMap, take, tap, toArray } from 'rxjs/operators'; +import { + defaultIfEmpty, + map, + mergeMap, + startWith, + switchMap, + take, + tap, + toArray, + concatMap +} from 'rxjs/operators'; import { hasNoValue, hasValue, hasValueOperator } from '../../../../shared/empty.util'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; -import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; +import { + RelationshipType +} from '../../../../core/shared/item-relationships/relationship-type.model'; import { getAllSucceededRemoteData, getFirstSucceededRemoteData, @@ -25,15 +38,23 @@ import { getRemoteDataPayload, } from '../../../../core/shared/operators'; import { ItemType } from '../../../../core/shared/item-relationships/item-type.model'; -import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component'; -import { RelationshipOptions } from '../../../../shared/form/builder/models/relationship-options.model'; -import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service'; +import { + DsDynamicLookupRelationModalComponent +} from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component'; +import { + RelationshipOptions +} from '../../../../shared/form/builder/models/relationship-options.model'; +import { + SelectableListService +} from '../../../../shared/object-list/selectable-list/selectable-list.service'; import { SearchResult } from '../../../../shared/search/models/search-result.model'; import { FollowLinkConfig } from '../../../../shared/utils/follow-link-config.model'; import { PaginatedList } from '../../../../core/data/paginated-list.model'; import { RemoteData } from '../../../../core/data/remote-data'; import { Collection } from '../../../../core/shared/collection.model'; -import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; +import { + PaginationComponentOptions +} from '../../../../shared/pagination/pagination-component-options.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; import { RelationshipTypeDataService } from '../../../../core/data/relationship-type-data.service'; import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; @@ -41,6 +62,7 @@ import { FieldUpdates } from '../../../../core/data/object-updates/field-updates import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; import { APP_CONFIG, AppConfig } from '../../../../../config/app-config.interface'; import { itemLinksToFollow } from '../../../../shared/utils/relation-query.utils'; +import { EditItemRelationshipsService } from '../edit-item-relationships.service'; @Component({ selector: 'ds-edit-relationship-list', @@ -90,7 +112,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { * Observable that emits true if {@link itemType} is on the left-hand side of {@link relationshipType}, * false if it is on the right-hand side and undefined in the rare case that it is on neither side. */ - private currentItemIsLeftItem$: Observable; + private currentItemIsLeftItem$: BehaviorSubject = new BehaviorSubject(undefined); private relatedEntityType$: Observable; @@ -153,6 +175,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { protected modalService: NgbModal, protected paginationService: PaginationService, protected selectableListService: SelectableListService, + protected editItemRelationshipsService: EditItemRelationshipsService, @Inject(APP_CONFIG) protected appConfig: AppConfig ) { this.fetchThumbnail = this.appConfig.browseBy.showThumbnails; @@ -211,7 +234,6 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { * Open the dynamic lookup modal to search for items to add as relationships */ openLookup() { - this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, { size: 'lg' }); @@ -277,51 +299,59 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { modalComp.submitEv = () => { - - const subscriptions = []; - - modalComp.toAdd.forEach((searchResult: SearchResult) => { - const relatedItem = searchResult.indexableObject; - subscriptions.push(this.relationshipService.getNameVariant(this.listId, relatedItem.uuid).pipe( - map((nameVariant) => { - const update = { - uuid: this.relationshipType.id + '-' + searchResult.indexableObject.uuid, - nameVariant, - type: this.relationshipType, - relatedItem, - } as RelationshipIdentifiable; - this.objectUpdatesService.saveAddFieldUpdate(this.url, update); - return update; - }) - )); - }); - - modalComp.toRemove.forEach( (searchResult) => { - subscriptions.push(this.relationshipService.getNameVariant(this.listId, searchResult.indexableObjectuuid).pipe( - switchMap((nameVariant) => { - return this.getRelationFromId(searchResult.indexableObject).pipe( - map( (relationship: Relationship) => { + modalComp.isPending = true; + const isLeft = this.currentItemIsLeftItem$.getValue(); + const addOperations = modalComp.toAdd.map((searchResult: any) => ({ type: 'add', searchResult })); + const removeOperations = modalComp.toRemove.map((searchResult: any) => ({ type: 'remove', searchResult })); + observableFrom([...addOperations, ...removeOperations]).pipe( + concatMap(({ type, searchResult }: { type: string, searchResult: any }) => { + if (type === 'add') { + const relatedItem = searchResult.indexableObject; + return this.relationshipService.getNameVariant(this.listId, relatedItem.uuid).pipe( + map((nameVariant) => { const update = { - uuid: relationship.id, + uuid: this.relationshipType.id + '-' + searchResult.indexableObject.uuid, nameVariant, type: this.relationshipType, - relationship, + originalIsLeft: isLeft, + originalItem: this.item, + relatedItem, } as RelationshipIdentifiable; - this.objectUpdatesService.saveRemoveFieldUpdate(this.url,update); + this.objectUpdatesService.saveAddFieldUpdate(this.url, update); return update; - }) + }), + take(1) ); - }) - )); - }); - - observableCombineLatest(subscriptions).subscribe( (res) => { - // Wait until the states changes since there are multiple items - setTimeout( () => { + } else if (type === 'remove') { + return this.relationshipService.getNameVariant(this.listId, searchResult.indexableObjectuuid).pipe( + switchMap((nameVariant) => { + return this.getRelationFromId(searchResult.indexableObject).pipe( + map( (relationship: Relationship) => { + const update = { + uuid: relationship.id, + nameVariant, + type: this.relationshipType, + originalIsLeft: isLeft, + originalItem: this.item, + relationship, + } as RelationshipIdentifiable; + this.objectUpdatesService.saveRemoveFieldUpdate(this.url,update); + return update; + }) + ); + }), + take(1) + ) + } else { + return EMPTY; + } + }), + toArray(), + ).subscribe({ + complete: () => { + this.editItemRelationshipsService.submit(this.item, this.url) this.submit.emit(); - },1000); - - modalComp.isPending = true; + } }); }; @@ -355,27 +385,12 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { } getRelationFromId(relatedItem) { - return this.currentItemIsLeftItem$.pipe( - take(1), - switchMap( isLeft => { - let apiCall; - if (isLeft) { - apiCall = this.relationshipService.searchByItemsAndType( this.relationshipType.id, this.item.uuid, this.relationshipType.leftwardType ,[relatedItem.id] ).pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - ); - } else { - apiCall = this.relationshipService.searchByItemsAndType( this.relationshipType.id, this.item.uuid, this.relationshipType.rightwardType ,[relatedItem.id] ).pipe( - getFirstSucceededRemoteData(), - getRemoteDataPayload(), - ); - } - - return apiCall.pipe( - map( (res: PaginatedList) => res.page[0]) - ); - } - )); + const relationshipLabel = this.currentItemIsLeftItem$.getValue() ? this.relationshipType.leftwardType : this.relationshipType.rightwardType; + return this.relationshipService.searchByItemsAndType( this.relationshipType.id, this.item.uuid, relationshipLabel ,[relatedItem.id] ).pipe( + getFirstSucceededRemoteData(), + getRemoteDataPayload(), + map( (res: PaginatedList) => res.page[0]) + ); } @@ -440,7 +455,6 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { } ngOnInit(): void { - // store the left and right type of the relationship in a single observable this.relationshipLeftAndRightType$ = observableCombineLatest([ this.relationshipType.leftType, @@ -461,7 +475,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { (relatedEntityType) => this.listId = `edit-relationship-${this.itemType.id}-${relatedEntityType.id}` ); - this.currentItemIsLeftItem$ = this.relationshipLeftAndRightType$.pipe( + this.subs.push(this.relationshipLeftAndRightType$.pipe( map(([leftType, rightType]: [ItemType, ItemType]) => { if (leftType.id === this.itemType.id) { return true; @@ -475,7 +489,9 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { console.warn(`The item ${this.item.uuid} is not on the right or the left side of relationship type ${this.relationshipType.uuid}`); return undefined; }) - ); + ).subscribe((nextValue: boolean) => { + this.currentItemIsLeftItem$.next(nextValue); + })); // initialize the pagination options @@ -500,19 +516,20 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { currentPagination$, this.currentItemIsLeftItem$, ]).pipe( - switchMap(([currentPagination, currentItemIsLeftItem]: [PaginationComponentOptions, boolean]) => + switchMap(([currentPagination, currentItemIsLeftItem]: [PaginationComponentOptions, boolean]) => { // get the relationships for the current item, relationshiptype and page - this.relationshipService.getItemRelationshipsByLabel( + return this.relationshipService.getItemRelationshipsByLabel( this.item, currentItemIsLeftItem ? this.relationshipType.leftwardType : this.relationshipType.rightwardType, { elementsPerPage: currentPagination.pageSize, currentPage: currentPagination.currentPage }, - false, + true, true, ...linksToFollow - )), + ); + }), ).subscribe((rd: RemoteData>) => { this.relationshipsRd$.next(rd); }) @@ -548,6 +565,8 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { uuid: relationship.id, type: this.relationshipType, relationship, + originalIsLeft: isLeftItem, + originalItem: this.item, nameVariant, } as RelationshipIdentifiable; }), diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts index 742cc7181c..cf0c610f8f 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts @@ -108,10 +108,10 @@ export class EditRelationshipComponent implements OnChanges { */ remove(): void { this.closeVirtualMetadataModal(); - observableCombineLatest( + observableCombineLatest([ this.leftItem$, this.rightItem$, - ).pipe( + ]).pipe( map((items: Item[]) => items.map((item) => this.objectUpdatesService .isSelectedVirtualMetadata(this.url, this.relationship.id, item.uuid)) @@ -127,9 +127,9 @@ export class EditRelationshipComponent implements OnChanges { ) as DeleteRelationship; }), take(1), - ).subscribe((deleteRelationship: DeleteRelationship) => - this.objectUpdatesService.saveRemoveFieldUpdate(this.url, deleteRelationship) - ); + ).subscribe((deleteRelationship: DeleteRelationship) => { + this.objectUpdatesService.saveRemoveFieldUpdate(this.url, deleteRelationship); + }); } openVirtualMetadataModal(content: any) { diff --git a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.html b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.html index c1505deb58..a253e2108f 100644 --- a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.html +++ b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.html @@ -27,8 +27,7 @@ [item]="item" [itemType]="entityType$ | async" [relationshipType]="relationshipType" - [hasChanges] = hasChanges() - (submit) = submit() + [hasChanges]="hasChanges()" > diff --git a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts index 033f22a40b..2fac6d8371 100644 --- a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -4,7 +4,7 @@ import { DeleteRelationship, RelationshipIdentifiable, } from '../../../core/data/object-updates/object-updates.reducer'; -import { map, switchMap, take, concatMap, toArray } from 'rxjs/operators'; +import { map, switchMap, take, concatMap, toArray, tap } from 'rxjs/operators'; import { combineLatest as observableCombineLatest, Observable, @@ -34,6 +34,7 @@ import { FieldChangeType } from '../../../core/data/object-updates/field-change- import { RelationshipTypeDataService } from '../../../core/data/relationship-type-data.service'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { EditItemRelationshipsService } from './edit-item-relationships.service'; @Component({ selector: 'ds-item-relationships', @@ -70,6 +71,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { protected relationshipTypeService: RelationshipTypeDataService, public cdr: ChangeDetectorRef, protected modalService: NgbModal, + protected editItemRelationshipsService: EditItemRelationshipsService, ) { super(itemService, objectUpdatesService, router, notificationsService, translateService, route); } @@ -108,152 +110,14 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { * Make sure the lists are refreshed afterwards and notifications are sent for success and errors */ public submit(): void { - - // Get all the relationships that should be removed - const removeUpdates$: Observable = this.relationshipService.getItemRelationshipsArray(this.item).pipe( - map((relationships: Relationship[]) => relationships.map((relationship) => - Object.assign(new Relationship(), relationship, { uuid: relationship.id }) - )), - switchMap((relationships: Relationship[]) => { - return this.objectUpdatesService.getFieldUpdatesExclusive(this.url, relationships) as Observable; - }), - map((fieldUpdates: FieldUpdates) => - Object.values(fieldUpdates) - .filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE) - ), - take(1) - ); - - const addUpdates$: Observable = this.objectUpdatesService.getFieldUpdates(this.url, []).pipe( - map((fieldUpdates: FieldUpdates) => - Object.values(fieldUpdates) - .filter((fieldUpdate: FieldUpdate) => hasValue(fieldUpdate)) - .filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.ADD) - ), - take(1) - ); - - observableCombineLatest([ - removeUpdates$, - addUpdates$, - ]).pipe( - take(1), - switchMap(([removeUpdates, addUpdates]) => [...removeUpdates, ...addUpdates]), - concatMap((update: FieldUpdate) => { - if (update.changeType === FieldChangeType.REMOVE) { - return this.deleteRelationship(update.field as DeleteRelationship).pipe(take(1)); - } else if (update.changeType === FieldChangeType.ADD) { - return this.addRelationship(update.field as RelationshipIdentifiable).pipe( - take(1), - switchMap((relationshipRD: RemoteData) => { - if (relationshipRD.hasSucceeded) { - // Set the newly related item to stale, so its relationships will update to include - // the new one. Only set the current item to stale at the very end so we only do it - // once - const { leftItem, rightItem } = relationshipRD.payload._links; - if (leftItem.href === this.item.self) { - return this.itemService.invalidateByHref(rightItem.href).pipe( - // when it's invalidated, emit the original relationshipRD for use in the pipe below - map(() => relationshipRD) - ); - } else { - return this.itemService.invalidateByHref(leftItem.href).pipe( - // when it's invalidated, emit the original relationshipRD for use in the pipe below - map(() => relationshipRD) - ); - } - } else { - return [relationshipRD]; - } - }) - ); - } else { - return EMPTY; - } - }), - toArray(), - switchMap((responses) => { - // once all relationships are made and all related items have been invalidated, invalidate - // the current item - return this.itemService.invalidateByHref(this.item.self).pipe( - map(() => responses) - ); - }) - ).subscribe((responses) => { - if (responses.length > 0) { - this.initializeOriginalFields(); - this.displayNotifications(responses); - this.modalService.dismissAll(); - } - }); + this.editItemRelationshipsService.submit(this.item, this.url); } - deleteRelationship(deleteRelationship: DeleteRelationship): Observable> { - let copyVirtualMetadata: string; - if (deleteRelationship.keepLeftVirtualMetadata && deleteRelationship.keepRightVirtualMetadata) { - copyVirtualMetadata = 'all'; - } else if (deleteRelationship.keepLeftVirtualMetadata) { - copyVirtualMetadata = 'left'; - } else if (deleteRelationship.keepRightVirtualMetadata) { - copyVirtualMetadata = 'right'; - } else { - copyVirtualMetadata = 'none'; - } - - return this.relationshipService.deleteRelationship(deleteRelationship.uuid, copyVirtualMetadata, false); - } - - addRelationship(addRelationship: RelationshipIdentifiable): Observable> { - return this.entityType$.pipe( - switchMap((entityType) => this.entityTypeService.isLeftType(addRelationship.type, entityType)), - switchMap((isLeftType) => { - let leftItem: Item; - let rightItem: Item; - let leftwardValue: string; - let rightwardValue: string; - if (isLeftType) { - leftItem = this.item; - rightItem = addRelationship.relatedItem; - leftwardValue = null; - rightwardValue = addRelationship.nameVariant; - } else { - leftItem = addRelationship.relatedItem; - rightItem = this.item; - leftwardValue = addRelationship.nameVariant; - rightwardValue = null; - } - return this.relationshipService.addRelationship(addRelationship.type.id, leftItem, rightItem, leftwardValue, rightwardValue, false); - }), - ); - - } - - /** - * Display notifications - * - Error notification for each failed response with their message - * - Success notification in case there's at least one successful response - * @param responses - */ - displayNotifications(responses: RemoteData[]) { - const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); - const successfulResponses = responses.filter((response: RemoteData) => response.hasSucceeded); - - failedResponses.forEach((response: RemoteData) => { - this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage); - }); - if (successfulResponses.length > 0) { - this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); - } - } /** * Sends all initial values of this item to the object updates service */ public initializeOriginalFields() { - return this.relationshipService.getRelatedItems(this.item).pipe( - take(1), - ).subscribe((items: Item[]) => { - this.objectUpdatesService.initialize(this.url, items, this.item.lastModified); - }); + return this.editItemRelationshipsService.initializeOriginalFields(this.item, this.url); } From 1c50dbf4c6199aecc22d8339174d95dfc4992785 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 9 May 2024 13:40:03 +0200 Subject: [PATCH 4/8] Merge remote-tracking branch 'alexandre/fix-create-relationship-not-working-between-same-types_contribute-7.6' into w2p-113560_edit-item-add-relationships-one-by-one --- .../edit-relationship-list.component.html | 2 +- .../edit-relationship-list.component.ts | 96 +++++-------------- .../item-relationships.component.html | 84 ++++++++-------- .../item-relationships.component.ts | 7 ++ ...dynamic-lookup-relation-modal.component.ts | 5 +- 5 files changed, 76 insertions(+), 118 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html index 7cdc903f24..372a4916df 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.html @@ -1,5 +1,5 @@
- {{getRelationshipMessageKey() | async | translate}} + {{relationshipMessageKey$ | async | translate}} + + + +
+
+ +
+
+ + + +
+
+ -
- - -
- -
-
- -
-
-
- - - -
-
- +
diff --git a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts index 3f84c97971..4ab5def74b 100644 --- a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -57,6 +57,10 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { */ entityType$: BehaviorSubject = new BehaviorSubject(undefined); + get isSaving$(): BehaviorSubject { + return this.editItemRelationshipsService.isSaving$; + } + constructor( public itemService: ItemDataService, public objectUpdatesService: ObjectUpdatesService, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html index b72a8722ae..25c3535836 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html @@ -86,7 +86,7 @@ (click)="submitEv()"> - +  {{"item.edit.metadata.save-button" | translate}} From 4541788a34500b7d23fed6d1016172eb63c5a8fe Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 9 May 2024 14:55:37 +0200 Subject: [PATCH 6/8] fix issue where relationship lists wouldn't update automatically from the second submit onwards --- src/app/core/data/relationship-data.service.ts | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/src/app/core/data/relationship-data.service.ts b/src/app/core/data/relationship-data.service.ts index 8d8f62b14e..b002d4c9ce 100644 --- a/src/app/core/data/relationship-data.service.ts +++ b/src/app/core/data/relationship-data.service.ts @@ -309,13 +309,19 @@ export class RelationshipDataService extends IdentifiableDataService + this.getItemRelationshipsByLabel(item, label, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)), + ); } /** From a658bf4531e97737e0ace703b042a8e4951212cc Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 9 May 2024 15:25:37 +0200 Subject: [PATCH 7/8] decrease the number of unnecessary rerenders by checking whether the relationship types have changed --- .../edit-relationship-list.component.ts | 4 ++-- .../item-relationships.component.ts | 24 +++++++------------ 2 files changed, 11 insertions(+), 17 deletions(-) diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts index 97fdb0fc69..54c870cfae 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -347,7 +347,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { ); }), take(1) - ) + ); } else { return EMPTY; } @@ -355,7 +355,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { toArray(), ).subscribe({ complete: () => { - this.editItemRelationshipsService.submit(this.item, this.url) + this.editItemRelationshipsService.submit(this.item, this.url); this.submitModal.emit(); } }); diff --git a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts index 4ab5def74b..758978f8d8 100644 --- a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -1,14 +1,13 @@ import { ChangeDetectorRef, Component } from '@angular/core'; -import { Item } from '../../../core/shared/item.model'; + + import { - DeleteRelationship, - RelationshipIdentifiable, -} from '../../../core/data/object-updates/object-updates.reducer'; -import { map, switchMap, take, concatMap, toArray, tap } from 'rxjs/operators'; + map, + distinctUntilChanged +} from 'rxjs/operators'; import { - combineLatest as observableCombineLatest, Observable, - BehaviorSubject, EMPTY + BehaviorSubject } from 'rxjs'; import { followLink } from '../../../shared/utils/follow-link-config.model'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; @@ -18,23 +17,17 @@ import { ActivatedRoute, Router } from '@angular/router'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { TranslateService } from '@ngx-translate/core'; import { RelationshipDataService } from '../../../core/data/relationship-data.service'; -import { RemoteData } from '../../../core/data/remote-data'; import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { getFirstSucceededRemoteData, getRemoteDataPayload } from '../../../core/shared/operators'; import { RequestService } from '../../../core/data/request.service'; import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; import { EntityTypeDataService } from '../../../core/data/entity-type-data.service'; -import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; -import { NoContent } from '../../../core/shared/NoContent.model'; -import { hasValue } from '../../../shared/empty.util'; -import { FieldUpdate } from '../../../core/data/object-updates/field-update.model'; -import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model'; -import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model'; import { RelationshipTypeDataService } from '../../../core/data/relationship-type-data.service'; import { PaginatedList } from '../../../core/data/paginated-list.model'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { EditItemRelationshipsService } from './edit-item-relationships.service'; +import { compareArraysUsingIds } from '../../simple/item-types/shared/item-relationships-utils'; @Component({ selector: 'ds-item-relationships', @@ -89,7 +82,8 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent { if (label !== undefined) { this.relationshipTypes$ = this.relationshipTypeService.searchByEntityType(label, true, true, ...this.getRelationshipTypeFollowLinks()) .pipe( - map((relationshipTypes: PaginatedList) => relationshipTypes.page) + map((relationshipTypes: PaginatedList) => relationshipTypes.page), + distinctUntilChanged(compareArraysUsingIds()) ); this.entityTypeService.getEntityTypeByLabel(label).pipe( From 479adf6519dcf88b539d525fa3703671a6fe99a3 Mon Sep 17 00:00:00 2001 From: Alexandre Vryghem Date: Tue, 14 May 2024 17:46:43 +0200 Subject: [PATCH 8/8] 115046: Fixed failing tests & added new test to cover added code --- .../core/data/relationship-data.service.ts | 2 +- .../edit-item-relationships.service.spec.ts | 293 +++++++++++++++- .../edit-item-relationships.service.ts | 10 +- .../edit-relationship-list.component.spec.ts | 331 +++++++++--------- .../edit-relationship-list.component.ts | 4 +- .../item-relationships.component.spec.ts | 10 +- .../shared/testing/base-data-service.stub.ts | 7 + .../edit-item-relationships.service.stub.ts | 48 +++ .../testing/entity-type-data.service.stub.ts | 5 + .../shared/testing/item-data.service.stub.ts | 8 + .../testing/object-updates.service.stub.ts | 24 ++ .../testing/relationship-data.service.stub.ts | 86 +++++ 12 files changed, 655 insertions(+), 173 deletions(-) create mode 100644 src/app/shared/testing/edit-item-relationships.service.stub.ts create mode 100644 src/app/shared/testing/entity-type-data.service.stub.ts create mode 100644 src/app/shared/testing/item-data.service.stub.ts create mode 100644 src/app/shared/testing/object-updates.service.stub.ts create mode 100644 src/app/shared/testing/relationship-data.service.stub.ts diff --git a/src/app/core/data/relationship-data.service.ts b/src/app/core/data/relationship-data.service.ts index b002d4c9ce..a7ab4eb31c 100644 --- a/src/app/core/data/relationship-data.service.ts +++ b/src/app/core/data/relationship-data.service.ts @@ -196,7 +196,7 @@ export class RelationshipDataService extends IdentifiableDataService { let service: EditItemRelationshipsService; + let itemService: ItemDataServiceStub; + let objectUpdatesService: ObjectUpdatesServiceStub; + let notificationsService: NotificationsServiceStub; + let relationshipService: RelationshipDataServiceStub; + let entityTypeDataService: EntityTypeDataServiceStub; + + let currentItem: Item; + + let relationshipItem1: Item; + let relationshipIdentifiable1: RelationshipIdentifiable; + let relationship1: Relationship; + + let relationshipItem2: Item; + let relationshipIdentifiable2: RelationshipIdentifiable; + let relationship2: Relationship; + + let orgUnitType: ItemType; + let orgUnitToOrgUnitType: RelationshipType; + beforeEach(() => { - TestBed.configureTestingModule({}); + itemService = new ItemDataServiceStub(); + objectUpdatesService = new ObjectUpdatesServiceStub(); + notificationsService = new NotificationsServiceStub(); + relationshipService = new RelationshipDataServiceStub(); + entityTypeDataService = new EntityTypeDataServiceStub(); + + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + ], + providers: [ + { provide: ItemDataService, useValue: itemService }, + { provide: ObjectUpdatesService, useValue: objectUpdatesService }, + { provide: NotificationsService, useValue: notificationsService }, + { provide: RelationshipDataService, useValue: relationshipService }, + { provide: EntityTypeDataService, useValue: entityTypeDataService }, + ], + }); service = TestBed.inject(EditItemRelationshipsService); }); - it('should be created', () => { - expect(service).toBeTruthy(); + beforeEach(() => { + currentItem = Object.assign(new Item(), { + uuid: uuidv4(), + metadata: { + 'dspace.entity.type': 'OrgUnit', + }, + _links: { + self: { + href: 'selfLink1', + }, + }, + }); + + relationshipItem1 = Object.assign(new Item(), { + uuid: uuidv4(), + metadata: { + 'dspace.entity.type': 'OrgUnit', + }, + _links: { + self: { + href: 'selfLink2', + }, + }, + }); + relationshipIdentifiable1 = { + originalItem: currentItem, + relatedItem: relationshipItem1, + type: orgUnitToOrgUnitType, + uuid: `1-${relationshipItem1.uuid}`, + } as RelationshipIdentifiable; + relationship1 = Object.assign(new Relationship(), { + _links: { + leftItem: currentItem._links.self, + rightItem: relationshipItem1._links.self, + }, + }); + + relationshipItem2 = Object.assign(new Item(), { + uuid: uuidv4(), + metadata: { + 'dspace.entity.type': 'OrgUnit', + }, + _links: { + self: { + href: 'selfLink3', + }, + }, + }); + relationshipIdentifiable2 = { + originalItem: currentItem, + relatedItem: relationshipItem2, + type: orgUnitToOrgUnitType, + uuid: `1-${relationshipItem2.uuid}`, + } as RelationshipIdentifiable; + relationship2 = Object.assign(new Relationship(), { + _links: { + leftItem: currentItem._links.self, + rightItem: relationshipItem2._links.self, + }, + }); + + orgUnitType = Object.assign(new ItemType(), { + id: '2', + label: 'OrgUnit', + }); + orgUnitToOrgUnitType = Object.assign(new RelationshipType(), { + id: '1', + leftMaxCardinality: null, + leftMinCardinality: 0, + leftType: createSuccessfulRemoteDataObject$(orgUnitType), + leftwardType: 'isOrgUnitOfOrgUnit', + rightMaxCardinality: null, + rightMinCardinality: 0, + rightType: createSuccessfulRemoteDataObject$(orgUnitType), + rightwardType: 'isOrgUnitOfOrgUnit', + uuid: 'relationshiptype-1', + }); + }); + + describe('submit', () => { + let fieldUpdateAddRelationship1: FieldUpdate; + let fieldUpdateRemoveRelationship2: FieldUpdate; + + beforeEach(() => { + fieldUpdateAddRelationship1 = { + changeType: FieldChangeType.ADD, + field: relationshipIdentifiable1, + }; + fieldUpdateRemoveRelationship2 = { + changeType: FieldChangeType.REMOVE, + field: relationshipIdentifiable2, + }; + + spyOn(service, 'addRelationship').withArgs(relationshipIdentifiable1).and.returnValue(createSuccessfulRemoteDataObject$(relationship1)); + spyOn(service, 'deleteRelationship').withArgs(relationshipIdentifiable2 as DeleteRelationship).and.returnValue(createSuccessfulRemoteDataObject$({})); + spyOn(itemService, 'invalidateByHref').and.callThrough(); + }); + + it('should support performing multiple relationships manipulations in one submit() call', () => { + spyOn(objectUpdatesService, 'getFieldUpdates').and.returnValue(observableOf({ + [`1-${relationshipItem1.uuid}`]: fieldUpdateAddRelationship1, + [`1-${relationshipItem2.uuid}`]: fieldUpdateRemoveRelationship2, + } as FieldUpdates)); + service.submit(currentItem, `/entities/orgunit/${currentItem.uuid}/edit/relationships`); + + expect(service.addRelationship).toHaveBeenCalledWith(relationshipIdentifiable1); + expect(service.deleteRelationship).toHaveBeenCalledWith(relationshipIdentifiable2 as DeleteRelationship); + + expect(itemService.invalidateByHref).toHaveBeenCalledWith(currentItem.self); + expect(itemService.invalidateByHref).toHaveBeenCalledWith(relationshipItem1.self); + // TODO currently this isn't done yet + // expect(itemService.invalidateByHref).toHaveBeenCalledWith(relationshipItem2.self); + + expect(notificationsService.success).toHaveBeenCalledTimes(1); + }); + }); + + describe('deleteRelationship', () => { + beforeEach(() => { + spyOn(relationshipService, 'deleteRelationship').and.callThrough(); + }); + + it('should pass "all" as copyVirtualMetadata when the user want to keep the data on both sides', () => { + service.deleteRelationship({ + uuid: relationshipItem1.uuid, + keepLeftVirtualMetadata: true, + keepRightVirtualMetadata: true, + } as DeleteRelationship); + + expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'all', false); + }); + + it('should pass "left" as copyVirtualMetadata when the user only want to keep the data on the left side', () => { + service.deleteRelationship({ + uuid: relationshipItem1.uuid, + keepLeftVirtualMetadata: true, + keepRightVirtualMetadata: false, + } as DeleteRelationship); + + expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'left', false); + }); + + it('should pass "right" as copyVirtualMetadata when the user only want to keep the data on the right side', () => { + service.deleteRelationship({ + uuid: relationshipItem1.uuid, + keepLeftVirtualMetadata: false, + keepRightVirtualMetadata: true, + } as DeleteRelationship); + + expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'right', false); + }); + + it('should pass "none" as copyVirtualMetadata when the user doesn\'t want to keep the virtual metadata', () => { + service.deleteRelationship({ + uuid: relationshipItem1.uuid, + keepLeftVirtualMetadata: false, + keepRightVirtualMetadata: false, + } as DeleteRelationship); + + expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'none', false); + }); + }); + + describe('addRelationship', () => { + beforeEach(() => { + spyOn(relationshipService, 'addRelationship').and.callThrough(); + }); + + it('should call the addRelationship from relationshipService correctly when original item is on the right', () => { + service.addRelationship({ + originalItem: currentItem, + originalIsLeft: false, + relatedItem: relationshipItem1, + type: orgUnitToOrgUnitType, + uuid: `1-${relationshipItem1.uuid}`, + } as RelationshipIdentifiable); + expect(relationshipService.addRelationship).toHaveBeenCalledWith(orgUnitToOrgUnitType.id, relationshipItem1, currentItem, undefined, null, false); + }); + + it('should call the addRelationship from relationshipService correctly when original item is on the left', () => { + service.addRelationship({ + originalItem: currentItem, + originalIsLeft: true, + relatedItem: relationshipItem1, + type: orgUnitToOrgUnitType, + uuid: `1-${relationshipItem1.uuid}`, + } as RelationshipIdentifiable); + + expect(relationshipService.addRelationship).toHaveBeenCalledWith(orgUnitToOrgUnitType.id, currentItem, relationshipItem1, null, undefined, false); + }); + }); + + describe('displayNotifications', () => { + it('should show one success notification when multiple requests succeeded', () => { + service.displayNotifications([ + createSuccessfulRemoteDataObject({}), + createSuccessfulRemoteDataObject({}), + ]); + + expect(notificationsService.success).toHaveBeenCalledTimes(1); + }); + + it('should show one success notification even when some requests failed', () => { + service.displayNotifications([ + createSuccessfulRemoteDataObject({}), + createFailedRemoteDataObject('Request Failed'), + createSuccessfulRemoteDataObject({}), + ]); + + expect(notificationsService.success).toHaveBeenCalledTimes(1); + expect(notificationsService.error).toHaveBeenCalledTimes(1); + }); + + it('should show a separate error notification for each failed request', () => { + service.displayNotifications([ + createSuccessfulRemoteDataObject({}), + createFailedRemoteDataObject('Request Failed 1'), + createSuccessfulRemoteDataObject({}), + createFailedRemoteDataObject('Request Failed 2'), + ]); + + expect(notificationsService.success).toHaveBeenCalledTimes(1); + expect(notificationsService.error).toHaveBeenCalledTimes(2); + }); }); }); diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts b/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts index 4a5e37c0e9..2cecd878b7 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-item-relationships.service.ts @@ -10,7 +10,7 @@ import { } from '../../../core/data/object-updates/object-updates.reducer'; import { RemoteData } from '../../../core/data/remote-data'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; -import { EMPTY, Observable, BehaviorSubject } from 'rxjs'; +import { EMPTY, Observable, BehaviorSubject, Subscription } from 'rxjs'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { ItemDataService } from '../../../core/data/item-data.service'; import { Item } from '../../../core/shared/item.model'; @@ -109,7 +109,7 @@ export class EditItemRelationshipsService { /** * Sends all initial values of this item to the object updates service */ - public initializeOriginalFields(item: Item, url: string) { + public initializeOriginalFields(item: Item, url: string): Subscription { return this.relationshipService.getRelatedItems(item).pipe( take(1), ).subscribe((items: Item[]) => { @@ -157,7 +157,7 @@ export class EditItemRelationshipsService { * - Success notification in case there's at least one successful response * @param responses */ - displayNotifications(responses: RemoteData[]) { + displayNotifications(responses: RemoteData[]): void { const failedResponses = responses.filter((response: RemoteData) => response.hasFailed); const successfulResponses = responses.filter((response: RemoteData) => response.hasSucceeded); @@ -175,7 +175,7 @@ export class EditItemRelationshipsService { * Get translated notification title * @param key */ - getNotificationTitle(key: string) { + getNotificationTitle(key: string): string { return this.translateService.instant(this.notificationsPrefix + key + '.title'); } @@ -183,7 +183,7 @@ export class EditItemRelationshipsService { * Get translated notification content * @param key */ - getNotificationContent(key: string) { + getNotificationContent(key: string): string { return this.translateService.instant(this.notificationsPrefix + key + '.content'); } diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index 4cd663f0fb..3a627232a4 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -32,67 +32,70 @@ import { ConfigurationProperty } from '../../../../core/shared/configuration-pro import { Router } from '@angular/router'; import { RouterMock } from '../../../../shared/mocks/router.mock'; import { APP_CONFIG } from '../../../../../config/app-config.interface'; - -let comp: EditRelationshipListComponent; -let fixture: ComponentFixture; -let de: DebugElement; - -let linkService; -let objectUpdatesService; -let relationshipService; -let selectableListService; -let paginationService; -let hostWindowService; -const relationshipTypeService = {}; - -const url = 'http://test-url.com/test-url'; - -let item; -let entityType; -let relatedEntityType; -let author1; -let author2; -let fieldUpdate1; -let fieldUpdate2; -let relationships; -let relationshipType; -let paginationOptions; +import { EditItemRelationshipsServiceStub } from '../../../../shared/testing/edit-item-relationships.service.stub'; +import { EditItemRelationshipsService } from '../edit-item-relationships.service'; +import { cold } from 'jasmine-marbles'; describe('EditRelationshipListComponent', () => { + let comp: EditRelationshipListComponent; + let fixture: ComponentFixture; + let de: DebugElement; + + let linkService; + let objectUpdatesService; + let relationshipService; + let selectableListService; + let paginationService: PaginationServiceStub; + let hostWindowService: HostWindowServiceStub; + const relationshipTypeService = {}; + let editItemRelationshipsService: EditItemRelationshipsServiceStub; + + const url = 'http://test-url.com/test-url'; + + let itemLeft: Item; + let entityTypeLeft: ItemType; + let entityTypeRight: ItemType; + let itemRight1: Item; + let itemRight2: Item; + let fieldUpdate1; + let fieldUpdate2; + let relationships: Relationship[]; + let relationshipType: RelationshipType; + let paginationOptions: PaginationComponentOptions; + const resetComponent = () => { fixture = TestBed.createComponent(EditRelationshipListComponent); comp = fixture.componentInstance; de = fixture.debugElement; - comp.item = item; - comp.itemType = entityType; + comp.item = itemLeft; + comp.itemType = entityTypeLeft; comp.url = url; comp.relationshipType = relationshipType; comp.hasChanges = observableOf(false); fixture.detectChanges(); }; - beforeEach(waitForAsync(() => { - - entityType = Object.assign(new ItemType(), { - id: 'Publication', - uuid: 'Publication', - label: 'Publication', + function init(leftType: string, rightType: string): void { + entityTypeLeft = Object.assign(new ItemType(), { + id: leftType, + uuid: leftType, + label: leftType, }); - relatedEntityType = Object.assign(new ItemType(), { - id: 'Author', - uuid: 'Author', - label: 'Author', + entityTypeRight = Object.assign(new ItemType(), { + id: rightType, + uuid: rightType, + label: rightType, }); relationshipType = Object.assign(new RelationshipType(), { id: '1', uuid: '1', - leftType: createSuccessfulRemoteDataObject$(entityType), - rightType: createSuccessfulRemoteDataObject$(relatedEntityType), - leftwardType: 'isAuthorOfPublication', - rightwardType: 'isPublicationOfAuthor', + leftType: createSuccessfulRemoteDataObject$(entityTypeLeft), + rightType: createSuccessfulRemoteDataObject$(entityTypeRight), + leftwardType: `is${rightType}Of${leftType}`, + rightwardType: `is${leftType}Of${rightType}`, }); paginationOptions = Object.assign(new PaginationComponentOptions(), { @@ -101,13 +104,13 @@ describe('EditRelationshipListComponent', () => { currentPage: 1, }); - author1 = Object.assign(new Item(), { - id: 'author1', - uuid: 'author1' + itemRight1 = Object.assign(new Item(), { + id: `${rightType}-1`, + uuid: `${rightType}-1`, }); - author2 = Object.assign(new Item(), { - id: 'author2', - uuid: 'author2' + itemRight2 = Object.assign(new Item(), { + id: `${rightType}-2`, + uuid: `${rightType}-2`, }); relationships = [ @@ -116,25 +119,25 @@ describe('EditRelationshipListComponent', () => { id: '2', uuid: '2', relationshipType: createSuccessfulRemoteDataObject$(relationshipType), - leftItem: createSuccessfulRemoteDataObject$(item), - rightItem: createSuccessfulRemoteDataObject$(author1), + leftItem: createSuccessfulRemoteDataObject$(itemLeft), + rightItem: createSuccessfulRemoteDataObject$(itemRight1), }), Object.assign(new Relationship(), { self: url + '/3', id: '3', uuid: '3', relationshipType: createSuccessfulRemoteDataObject$(relationshipType), - leftItem: createSuccessfulRemoteDataObject$(item), - rightItem: createSuccessfulRemoteDataObject$(author2), + leftItem: createSuccessfulRemoteDataObject$(itemLeft), + rightItem: createSuccessfulRemoteDataObject$(itemRight2), }) ]; - item = Object.assign(new Item(), { + itemLeft = Object.assign(new Item(), { _links: { self: { href: 'fake-item-url/publication' } }, - id: 'publication', - uuid: 'publication', + id: `1-${leftType}`, + uuid: `1-${leftType}`, relationships: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)) }); @@ -166,7 +169,7 @@ describe('EditRelationshipListComponent', () => { relationshipService = jasmine.createSpyObj('relationshipService', { - getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList([author1, author2])), + getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList([itemRight1, itemRight2])), getItemRelationshipsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)), isLeftItem: observableOf(true), } @@ -202,6 +205,8 @@ describe('EditRelationshipListComponent', () => { })) }); + editItemRelationshipsService = new EditItemRelationshipsServiceStub(); + const environmentUseThumbs = { browseBy: { showThumbnails: true @@ -224,6 +229,7 @@ describe('EditRelationshipListComponent', () => { { provide: LinkHeadService, useValue: linkHeadService }, { provide: ConfigurationDataService, useValue: configurationDataService }, { provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() }, + { provide: EditItemRelationshipsService, useValue: editItemRelationshipsService }, { provide: APP_CONFIG, useValue: environmentUseThumbs } ], schemas: [ NO_ERRORS_SCHEMA @@ -231,114 +237,127 @@ describe('EditRelationshipListComponent', () => { }).compileComponents(); resetComponent(); - })); + } - describe('changeType is REMOVE', () => { - beforeEach(() => { - fieldUpdate1.changeType = FieldChangeType.REMOVE; - fixture.detectChanges(); - }); - it('the div should have class alert-danger', () => { - const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement; - expect(element.classList).toContain('alert-danger'); - }); - }); + describe('Publication - Author relationship', () => { + beforeEach(waitForAsync(() => init('Publication', 'Author'))); - describe('pagination component', () => { - let paginationComp: PaginationComponent; - - beforeEach(() => { - paginationComp = de.query(By.css('ds-pagination')).componentInstance; - }); - - it('should receive the correct pagination config', () => { - expect(paginationComp.paginationOptions).toEqual(paginationOptions); - }); - - it('should receive correct collection size', () => { - expect(paginationComp.collectionSize).toEqual(relationships.length); - }); - - }); - - describe('relationshipService.getItemRelationshipsByLabel', () => { - it('should receive the correct pagination info', () => { - expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1); - - const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args; - const findListOptions = callArgs[2]; - const linksToFollow = callArgs[5]; - expect(findListOptions.elementsPerPage).toEqual(paginationOptions.pageSize); - expect(findListOptions.currentPage).toEqual(paginationOptions.currentPage); - expect(linksToFollow.linksToFollow[0].name).toEqual('thumbnail'); - - }); - - describe('when the publication is on the left side of the relationship', () => { + describe('changeType is REMOVE', () => { beforeEach(() => { - relationshipType = Object.assign(new RelationshipType(), { - id: '1', - uuid: '1', - leftType: createSuccessfulRemoteDataObject$(entityType), // publication - rightType: createSuccessfulRemoteDataObject$(relatedEntityType), // author - leftwardType: 'isAuthorOfPublication', - rightwardType: 'isPublicationOfAuthor', - }); - relationshipService.getItemRelationshipsByLabel.calls.reset(); - resetComponent(); - }); - - it('should fetch isAuthorOfPublication', () => { - expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1); - - const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args; - const label = callArgs[1]; - - expect(label).toEqual('isAuthorOfPublication'); - }); - }); - - describe('when the publication is on the right side of the relationship', () => { - beforeEach(() => { - relationshipType = Object.assign(new RelationshipType(), { - id: '1', - uuid: '1', - leftType: createSuccessfulRemoteDataObject$(relatedEntityType), // author - rightType: createSuccessfulRemoteDataObject$(entityType), // publication - leftwardType: 'isPublicationOfAuthor', - rightwardType: 'isAuthorOfPublication', - }); - relationshipService.getItemRelationshipsByLabel.calls.reset(); - resetComponent(); - }); - - it('should fetch isAuthorOfPublication', () => { - expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1); - - const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args; - const label = callArgs[1]; - - expect(label).toEqual('isAuthorOfPublication'); - }); - }); - - - - describe('changes managment for add buttons', () => { - - it('should show enabled add buttons', () => { - const element = de.query(By.css('.btn-success')); - expect(element.nativeElement?.disabled).toBeFalse(); - }); - - it('after hash changes changed', () => { - comp.hasChanges = observableOf(true); + fieldUpdate1.changeType = FieldChangeType.REMOVE; fixture.detectChanges(); - const element = de.query(By.css('.btn-success')); - expect(element.nativeElement?.disabled).toBeTrue(); + }); + it('the div should have class alert-danger', () => { + const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement; + expect(element.classList).toContain('alert-danger'); }); }); + describe('pagination component', () => { + let paginationComp: PaginationComponent; + + beforeEach(() => { + paginationComp = de.query(By.css('ds-pagination')).componentInstance; + }); + + it('should receive the correct pagination config', () => { + expect(paginationComp.paginationOptions).toEqual(paginationOptions); + }); + + it('should receive correct collection size', () => { + expect(paginationComp.collectionSize).toEqual(relationships.length); + }); + + }); + + describe('relationshipService.getItemRelationshipsByLabel', () => { + it('should receive the correct pagination info', () => { + expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1); + + const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args; + const findListOptions = callArgs[2]; + const linksToFollow = callArgs[5]; + expect(findListOptions.elementsPerPage).toEqual(paginationOptions.pageSize); + expect(findListOptions.currentPage).toEqual(paginationOptions.currentPage); + expect(linksToFollow.linksToFollow[0].name).toEqual('thumbnail'); + + }); + + describe('when the publication is on the left side of the relationship', () => { + beforeEach(() => { + relationshipType = Object.assign(new RelationshipType(), { + id: '1', + uuid: '1', + leftType: createSuccessfulRemoteDataObject$(entityTypeLeft), // publication + rightType: createSuccessfulRemoteDataObject$(entityTypeRight), // author + leftwardType: 'isAuthorOfPublication', + rightwardType: 'isPublicationOfAuthor', + }); + relationshipService.getItemRelationshipsByLabel.calls.reset(); + resetComponent(); + }); + + it('should fetch isAuthorOfPublication', () => { + expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1); + + const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args; + const label = callArgs[1]; + + expect(label).toEqual('isAuthorOfPublication'); + }); + }); + + describe('when the publication is on the right side of the relationship', () => { + beforeEach(() => { + relationshipType = Object.assign(new RelationshipType(), { + id: '1', + uuid: '1', + leftType: createSuccessfulRemoteDataObject$(entityTypeRight), // author + rightType: createSuccessfulRemoteDataObject$(entityTypeLeft), // publication + leftwardType: 'isPublicationOfAuthor', + rightwardType: 'isAuthorOfPublication', + }); + relationshipService.getItemRelationshipsByLabel.calls.reset(); + resetComponent(); + }); + + it('should fetch isAuthorOfPublication', () => { + expect(relationshipService.getItemRelationshipsByLabel).toHaveBeenCalledTimes(1); + + const callArgs = relationshipService.getItemRelationshipsByLabel.calls.mostRecent().args; + const label = callArgs[1]; + + expect(label).toEqual('isAuthorOfPublication'); + }); + }); + + + + describe('changes managment for add buttons', () => { + + it('should show enabled add buttons', () => { + const element = de.query(By.css('.btn-success')); + expect(element.nativeElement?.disabled).toBeFalse(); + }); + + it('after hash changes changed', () => { + comp.hasChanges = observableOf(true); + fixture.detectChanges(); + const element = de.query(By.css('.btn-success')); + expect(element.nativeElement?.disabled).toBeTrue(); + }); + }); + + }); }); + describe('OrgUnit - OrgUnit relationship', () => { + beforeEach(waitForAsync(() => init('OrgUnit', 'OrgUnit'))); + + it('should emit the relatedEntityType$ even for same entity relationships', () => { + expect(comp.relatedEntityType$).toBeObservable(cold('(a|)', { + a: entityTypeRight, + })); + }); + }); }); diff --git a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts index 54c870cfae..57f8116b9c 100644 --- a/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts +++ b/src/app/item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -58,7 +58,6 @@ import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model'; import { PaginationService } from '../../../../core/pagination/pagination.service'; -import { RelationshipTypeDataService } from '../../../../core/data/relationship-type-data.service'; import { FieldUpdate } from '../../../../core/data/object-updates/field-update.model'; import { FieldUpdates } from '../../../../core/data/object-updates/field-updates.model'; import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model'; @@ -116,7 +115,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { */ private currentItemIsLeftItem$: BehaviorSubject = new BehaviorSubject(undefined); - private relatedEntityType$: Observable; + relatedEntityType$: Observable; /** * The translation key for the entity type @@ -178,7 +177,6 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy { protected objectUpdatesService: ObjectUpdatesService, protected linkService: LinkService, protected relationshipService: RelationshipDataService, - protected relationshipTypeService: RelationshipTypeDataService, protected modalService: NgbModal, protected paginationService: PaginationService, protected selectableListService: SelectableListService, diff --git a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts index b7bd0f789f..24dc86cc3d 100644 --- a/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts @@ -29,6 +29,7 @@ import { RelationshipTypeDataService } from '../../../core/data/relationship-typ import { relationshipTypes } from '../../../shared/testing/relationship-types.mock'; import { ThemeService } from '../../../shared/theme-support/theme.service'; import { getMockThemeService } from '../../../shared/mocks/theme-service.mock'; +import { ItemDataServiceStub } from '../../../shared/testing/item-data.service.stub'; let comp: any; let fixture: ComponentFixture; @@ -52,7 +53,7 @@ const notificationsService = jasmine.createSpyObj('notificationsService', const router = new RouterStub(); let relationshipTypeService; let routeStub; -let itemService; +let itemService: ItemDataServiceStub; const url = 'http://test-url.com/test-url'; router.url = url; @@ -137,10 +138,7 @@ describe('ItemRelationshipsComponent', () => { changeType: FieldChangeType.REMOVE }; - itemService = jasmine.createSpyObj('itemService', { - findByHref: createSuccessfulRemoteDataObject$(item), - findById: createSuccessfulRemoteDataObject$(item) - }); + itemService = new ItemDataServiceStub(); routeStub = { data: observableOf({}), parent: { @@ -232,6 +230,8 @@ describe('ItemRelationshipsComponent', () => { })); beforeEach(() => { + spyOn(itemService, 'findByHref').and.returnValue(item); + spyOn(itemService, 'findById').and.returnValue(item); fixture = TestBed.createComponent(ItemRelationshipsComponent); comp = fixture.componentInstance; de = fixture.debugElement; diff --git a/src/app/shared/testing/base-data-service.stub.ts b/src/app/shared/testing/base-data-service.stub.ts index f4b6f25923..b8b71845d6 100644 --- a/src/app/shared/testing/base-data-service.stub.ts +++ b/src/app/shared/testing/base-data-service.stub.ts @@ -1,11 +1,18 @@ import { Observable, of as observableOf } from 'rxjs'; import { CacheableObject } from '../../core/cache/cacheable-object.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; +import { FollowLinkConfig } from '../utils/follow-link-config.model'; /** * Stub class for {@link BaseDataService} */ export abstract class BaseDataServiceStub { + findByHref(_href$: string | Observable, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig[]): Observable> { + return createSuccessfulRemoteDataObject$(undefined); + } + invalidateByHref(_href: string): Observable { return observableOf(true); } diff --git a/src/app/shared/testing/edit-item-relationships.service.stub.ts b/src/app/shared/testing/edit-item-relationships.service.stub.ts new file mode 100644 index 0000000000..1d29569793 --- /dev/null +++ b/src/app/shared/testing/edit-item-relationships.service.stub.ts @@ -0,0 +1,48 @@ +/* eslint-disable no-empty, @typescript-eslint/no-empty-function */ +import { + Observable, + Subscription, +} from 'rxjs'; + +import { + DeleteRelationship, + RelationshipIdentifiable, +} from '../../core/data/object-updates/object-updates.reducer'; +import { RemoteData } from '../../core/data/remote-data'; +import { Item } from '../../core/shared/item.model'; +import { Relationship } from '../../core/shared/item-relationships/relationship.model'; +import { NoContent } from '../../core/shared/NoContent.model'; +import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; + +/** + * Stub class of {@link EditItemRelationshipsService} + */ +export class EditItemRelationshipsServiceStub { + + submit(_item: Item, _url: string): void { + } + + initializeOriginalFields(_item: Item, _url: string): Subscription { + return new Subscription(); + } + + deleteRelationship(_deleteRelationship: DeleteRelationship): Observable> { + return createSuccessfulRemoteDataObject$({}); + } + + addRelationship(_addRelationship: RelationshipIdentifiable): Observable> { + return createSuccessfulRemoteDataObject$(undefined); + } + + displayNotifications(_responses: RemoteData[]): void { + } + + getNotificationTitle(_key: string): string { + return ''; + } + + getNotificationContent(_key: string): string { + return ''; + } + +} diff --git a/src/app/shared/testing/entity-type-data.service.stub.ts b/src/app/shared/testing/entity-type-data.service.stub.ts new file mode 100644 index 0000000000..367eeb5f74 --- /dev/null +++ b/src/app/shared/testing/entity-type-data.service.stub.ts @@ -0,0 +1,5 @@ +/** + * Stub class of {@link EntityTypeDataService} + */ +export class EntityTypeDataServiceStub { +} diff --git a/src/app/shared/testing/item-data.service.stub.ts b/src/app/shared/testing/item-data.service.stub.ts new file mode 100644 index 0000000000..eed5d4bb11 --- /dev/null +++ b/src/app/shared/testing/item-data.service.stub.ts @@ -0,0 +1,8 @@ +import { Item } from '../../core/shared/item.model'; +import { IdentifiableDataServiceStub } from './identifiable-data-service.stub'; + +/** + * Stub class of {@link ItemDataService} + */ +export class ItemDataServiceStub extends IdentifiableDataServiceStub { +} diff --git a/src/app/shared/testing/object-updates.service.stub.ts b/src/app/shared/testing/object-updates.service.stub.ts new file mode 100644 index 0000000000..c66cc3aa0d --- /dev/null +++ b/src/app/shared/testing/object-updates.service.stub.ts @@ -0,0 +1,24 @@ +/* eslint-disable no-empty, @typescript-eslint/no-empty-function */ +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { FieldUpdates } from '../../core/data/object-updates/field-updates.model'; +import { Identifiable } from '../../core/data/object-updates/identifiable.model'; +import { PatchOperationService } from '../../core/data/object-updates/patch-operation-service/patch-operation.service'; +import { GenericConstructor } from '../../core/shared/generic-constructor'; + +/** + * Stub class of {@link ObjectUpdatesService} + */ +export class ObjectUpdatesServiceStub { + + initialize(_url: string, _fields: Identifiable[], _lastModified: Date, _patchOperationService?: GenericConstructor): void { + } + + getFieldUpdates(_url: string, _initialFields: Identifiable[], _ignoreStates?: boolean): Observable { + return observableOf({}); + } + +} diff --git a/src/app/shared/testing/relationship-data.service.stub.ts b/src/app/shared/testing/relationship-data.service.stub.ts new file mode 100644 index 0000000000..f0463b3e6c --- /dev/null +++ b/src/app/shared/testing/relationship-data.service.stub.ts @@ -0,0 +1,86 @@ +/* eslint-disable no-empty, @typescript-eslint/no-empty-function */ +import { + Observable, + of as observableOf, +} from 'rxjs'; + +import { FindListOptions } from '../../core/data/find-list-options.model'; +import { PaginatedList } from '../../core/data/paginated-list.model'; +import { RemoteData } from '../../core/data/remote-data'; +import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { Item } from '../../core/shared/item.model'; +import { Relationship } from '../../core/shared/item-relationships/relationship.model'; +import { MetadataValue } from '../../core/shared/metadata.models'; +import { MetadataRepresentation } from '../../core/shared/metadata-representation/metadata-representation.model'; +import { NoContent } from '../../core/shared/NoContent.model'; +import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils'; +import { FollowLinkConfig } from '../utils/follow-link-config.model'; + +/** + * Stub class of {@link RelationshipDataService} + */ +export class RelationshipDataServiceStub { + + deleteRelationship(_id: string, _copyVirtualMetadata: string, _shouldRefresh = true): Observable> { + return createSuccessfulRemoteDataObject$({}); + } + + addRelationship(_typeId: string, _item1: Item, _item2: Item, _leftwardValue?: string, _rightwardValue?: string, _shouldRefresh = true): Observable> { + return createSuccessfulRemoteDataObject$(new Relationship()); + } + + refreshRelationshipItemsInCache(_item: Item): void { + } + + getItemRelationshipsArray(_item: Item, ..._linksToFollow: FollowLinkConfig[]): Observable { + return observableOf([]); + } + + getRelatedItems(_item: Item): Observable { + return observableOf([]); + } + + getRelatedItemsByLabel(_item: Item, _label: string, _options?: FindListOptions): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList()); + } + + getItemRelationshipsByLabel(_item: Item, _label: string, _options?: FindListOptions, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig[]): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList()); + } + + getRelationshipByItemsAndLabel(_item1: Item, _item2: Item, _label: string, _options?: FindListOptions): Observable { + return observableOf(new Relationship()); + } + + setNameVariant(_listID: string, _itemID: string, _nameVariant: string): void { + } + + getNameVariant(_listID: string, _itemID: string): Observable { + return observableOf(''); + } + + updateNameVariant(_item1: Item, _item2: Item, _relationshipLabel: string, _nameVariant: string): Observable> { + return createSuccessfulRemoteDataObject$(new Relationship()); + } + + isLeftItem(_relationship: Relationship, _item: Item): Observable { + return observableOf(false); + } + + update(_object: Relationship): Observable> { + return createSuccessfulRemoteDataObject$(new Relationship()); + } + + searchByItemsAndType(_typeId: string, _itemUuid: string, _relationshipLabel: string, _arrayOfItemIds: string[]): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList()); + } + + searchBy(_searchMethod: string, _options?: FindListOptions, _useCachedVersionIfAvailable?: boolean, _reRequestOnStale?: boolean, ..._linksToFollow: FollowLinkConfig[]): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList()); + } + + resolveMetadataRepresentation(_metadatum: MetadataValue, _parentItem: DSpaceObject, _itemType: string): Observable { + return observableOf({} as MetadataRepresentation); + } + +}