From 9f3ee328580719006a08b3bfd8bcbedf35944d12 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 8 May 2024 17:48:33 +0200 Subject: [PATCH] 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); }