1
0

fix issue where a submit emitted from the edit relationship modal wouldn't arrive in the edit relationships component

(cherry picked from commit 9f3ee32858)
This commit is contained in:
Art Lowel
2024-05-08 17:48:33 +02:00
committed by Alexandre Vryghem
parent 814c81b7b7
commit b8a4c83353
9 changed files with 302 additions and 235 deletions

View File

@@ -32,6 +32,7 @@ import {
Observable, Observable,
} from 'rxjs'; } from 'rxjs';
import { import {
delay,
distinctUntilChanged, distinctUntilChanged,
take, take,
withLatestFrom, withLatestFrom,
@@ -136,7 +137,10 @@ export class AppComponent implements OnInit, AfterViewInit {
} }
ngAfterViewInit() { 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) { if (event instanceof NavigationStart) {
distinctNext(this.isRouteLoading$, true); distinctNext(this.isRouteLoading$, true);
} else if ( } else if (

View File

@@ -61,6 +61,8 @@ export interface VirtualMetadataSource {
export interface RelationshipIdentifiable extends Identifiable { export interface RelationshipIdentifiable extends Identifiable {
nameVariant?: string; nameVariant?: string;
originalItem: Item;
originalIsLeft: boolean
relatedItem: Item; relatedItem: Item;
relationship: Relationship; relationship: Relationship;
type: RelationshipType; type: RelationshipType;

View File

@@ -350,7 +350,13 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
} else { } else {
findListOptions.searchParams = searchParams; findListOptions.searchParams = searchParams;
} }
return this.searchBy('byLabel', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); const result$ = this.searchBy('byLabel', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
// add this result as a dependency of the item, meaning that if the item is invalided, this
// result will be as well
this.addDependency(result$, item.self);
return result$;
} }
/** /**

View File

@@ -0,0 +1,16 @@
import { TestBed } from '@angular/core/testing';
import { EditItemRelationshipsService } from './edit-item-relationships.service';
describe('EditItemRelationshipsService', () => {
let service: EditItemRelationshipsService;
beforeEach(() => {
TestBed.configureTestingModule({});
service = TestBed.inject(EditItemRelationshipsService);
});
it('should be created', () => {
expect(service).toBeTruthy();
});
});

View File

@@ -0,0 +1,196 @@
import { Injectable } from '@angular/core';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateService } from '@ngx-translate/core';
import {
EMPTY,
Observable,
} from 'rxjs';
import {
concatMap,
map,
switchMap,
take,
toArray,
} from 'rxjs/operators';
import { EntityTypeDataService } from '../../../core/data/entity-type-data.service';
import { ItemDataService } from '../../../core/data/item-data.service';
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
import { FieldUpdate } from '../../../core/data/object-updates/field-update.model';
import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model';
import {
DeleteRelationship,
RelationshipIdentifiable,
} from '../../../core/data/object-updates/object-updates.reducer';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
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 { hasValue } from '../../../shared/empty.util';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
@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<Relationship>) => {
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<RemoteData<NoContent>> {
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<RemoteData<Relationship>> {
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<NoContent>[]) {
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
const successfulResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
failedResponses.forEach((response: RemoteData<NoContent>) => {
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');
}
}

View File

@@ -21,11 +21,13 @@ import { TranslateModule } from '@ngx-translate/core';
import { import {
BehaviorSubject, BehaviorSubject,
combineLatest as observableCombineLatest, combineLatest as observableCombineLatest,
EMPTY,
from as observableFrom, from as observableFrom,
Observable, Observable,
Subscription, Subscription,
} from 'rxjs'; } from 'rxjs';
import { import {
concatMap,
defaultIfEmpty, defaultIfEmpty,
map, map,
mergeMap, mergeMap,
@@ -78,6 +80,7 @@ import { FollowLinkConfig } from '../../../../shared/utils/follow-link-config.mo
import { ObjectValuesPipe } from '../../../../shared/utils/object-values-pipe'; import { ObjectValuesPipe } from '../../../../shared/utils/object-values-pipe';
import { itemLinksToFollow } from '../../../../shared/utils/relation-query.utils'; import { itemLinksToFollow } from '../../../../shared/utils/relation-query.utils';
import { VarDirective } from '../../../../shared/utils/var.directive'; import { VarDirective } from '../../../../shared/utils/var.directive';
import { EditItemRelationshipsService } from '../edit-item-relationships.service';
import { EditRelationshipComponent } from '../edit-relationship/edit-relationship.component'; import { EditRelationshipComponent } from '../edit-relationship/edit-relationship.component';
@Component({ @Component({
@@ -141,7 +144,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
* Observable that emits true if {@link itemType} is on the left-hand side of {@link relationshipType}, * 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. * false if it is on the right-hand side and undefined in the rare case that it is on neither side.
*/ */
private currentItemIsLeftItem$: Observable<boolean>; private currentItemIsLeftItem$: BehaviorSubject<boolean> = new BehaviorSubject(undefined);
private relatedEntityType$: Observable<ItemType>; private relatedEntityType$: Observable<ItemType>;
@@ -206,6 +209,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
protected modalService: NgbModal, protected modalService: NgbModal,
protected paginationService: PaginationService, protected paginationService: PaginationService,
protected selectableListService: SelectableListService, protected selectableListService: SelectableListService,
protected editItemRelationshipsService: EditItemRelationshipsService,
@Inject(APP_CONFIG) protected appConfig: AppConfig, @Inject(APP_CONFIG) protected appConfig: AppConfig,
) { ) {
this.fetchThumbnail = this.appConfig.browseBy.showThumbnails; this.fetchThumbnail = this.appConfig.browseBy.showThumbnails;
@@ -240,7 +244,6 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
* Open the dynamic lookup modal to search for items to add as relationships * Open the dynamic lookup modal to search for items to add as relationships
*/ */
openLookup() { openLookup() {
this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, { this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, {
size: 'lg', size: 'lg',
}); });
@@ -306,51 +309,59 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
modalComp.submitEv = () => { modalComp.submitEv = () => {
modalComp.isPending = true;
const subscriptions = []; const isLeft = this.currentItemIsLeftItem$.getValue();
const addOperations = modalComp.toAdd.map((searchResult: any) => ({ type: 'add', searchResult }));
modalComp.toAdd.forEach((searchResult: SearchResult<Item>) => { const removeOperations = modalComp.toRemove.map((searchResult: any) => ({ type: 'remove', searchResult }));
const relatedItem = searchResult.indexableObject; observableFrom([...addOperations, ...removeOperations]).pipe(
subscriptions.push(this.relationshipService.getNameVariant(this.listId, relatedItem.uuid).pipe( concatMap(({ type, searchResult }: { type: string, searchResult: any }) => {
map((nameVariant) => { if (type === 'add') {
const update = { const relatedItem = searchResult.indexableObject;
uuid: this.relationshipType.id + '-' + searchResult.indexableObject.uuid, return this.relationshipService.getNameVariant(this.listId, relatedItem.uuid).pipe(
nameVariant, map((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) => {
const update = { const update = {
uuid: relationship.id, uuid: this.relationshipType.id + '-' + searchResult.indexableObject.uuid,
nameVariant, nameVariant,
type: this.relationshipType, type: this.relationshipType,
relationship, originalIsLeft: isLeft,
originalItem: this.item,
relatedItem,
} as RelationshipIdentifiable; } as RelationshipIdentifiable;
this.objectUpdatesService.saveRemoveFieldUpdate(this.url,update); this.objectUpdatesService.saveAddFieldUpdate(this.url, update);
return update; return update;
}), }),
take(1),
); );
}), } else if (type === 'remove') {
)); return this.relationshipService.getNameVariant(this.listId, searchResult.indexableObjectuuid).pipe(
}); switchMap((nameVariant) => {
return this.getRelationFromId(searchResult.indexableObject).pipe(
observableCombineLatest(subscriptions).subscribe( (res) => { map( (relationship: Relationship) => {
// Wait until the states changes since there are multiple items const update = {
setTimeout( () => { 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(); this.submit.emit();
},1000); },
modalComp.isPending = true;
}); });
}; };
@@ -384,27 +395,12 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
} }
getRelationFromId(relatedItem) { getRelationFromId(relatedItem) {
return this.currentItemIsLeftItem$.pipe( const relationshipLabel = this.currentItemIsLeftItem$.getValue() ? this.relationshipType.leftwardType : this.relationshipType.rightwardType;
take(1), return this.relationshipService.searchByItemsAndType( this.relationshipType.id, this.item.uuid, relationshipLabel ,[relatedItem.id] ).pipe(
switchMap( isLeft => { getFirstSucceededRemoteData(),
let apiCall; getRemoteDataPayload(),
if (isLeft) { map( (res: PaginatedList<Relationship>) => res.page[0]),
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<Relationship>) => res.page[0]),
);
},
));
} }
@@ -469,7 +465,6 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
} }
ngOnInit(): void { ngOnInit(): void {
// store the left and right type of the relationship in a single observable // store the left and right type of the relationship in a single observable
this.relationshipLeftAndRightType$ = observableCombineLatest([ this.relationshipLeftAndRightType$ = observableCombineLatest([
this.relationshipType.leftType, this.relationshipType.leftType,
@@ -490,7 +485,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
(relatedEntityType) => this.listId = `edit-relationship-${this.itemType.id}-${relatedEntityType.id}`, (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]) => { map(([leftType, rightType]: [ItemType, ItemType]) => {
if (leftType.id === this.itemType.id) { if (leftType.id === this.itemType.id) {
return true; return true;
@@ -504,7 +499,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}`); console.warn(`The item ${this.item.uuid} is not on the right or the left side of relationship type ${this.relationshipType.uuid}`);
return undefined; return undefined;
}), }),
); ).subscribe((nextValue: boolean) => {
this.currentItemIsLeftItem$.next(nextValue);
}));
this.getRelationshipMessageKey$ = observableCombineLatest( this.getRelationshipMessageKey$ = observableCombineLatest(
this.getLabel(), this.getLabel(),
@@ -547,19 +544,20 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
currentPagination$, currentPagination$,
this.currentItemIsLeftItem$, this.currentItemIsLeftItem$,
]).pipe( ]).pipe(
switchMap(([currentPagination, currentItemIsLeftItem]: [PaginationComponentOptions, boolean]) => switchMap(([currentPagination, currentItemIsLeftItem]: [PaginationComponentOptions, boolean]) => {
// get the relationships for the current item, relationshiptype and page // get the relationships for the current item, relationshiptype and page
this.relationshipService.getItemRelationshipsByLabel( return this.relationshipService.getItemRelationshipsByLabel(
this.item, this.item,
currentItemIsLeftItem ? this.relationshipType.leftwardType : this.relationshipType.rightwardType, currentItemIsLeftItem ? this.relationshipType.leftwardType : this.relationshipType.rightwardType,
{ {
elementsPerPage: currentPagination.pageSize, elementsPerPage: currentPagination.pageSize,
currentPage: currentPagination.currentPage, currentPage: currentPagination.currentPage,
}, },
false, true,
true, true,
...linksToFollow, ...linksToFollow,
)), );
}),
).subscribe((rd: RemoteData<PaginatedList<Relationship>>) => { ).subscribe((rd: RemoteData<PaginatedList<Relationship>>) => {
this.relationshipsRd$.next(rd); this.relationshipsRd$.next(rd);
}), }),
@@ -595,6 +593,8 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
uuid: relationship.id, uuid: relationship.id,
type: this.relationshipType, type: this.relationshipType,
relationship, relationship,
originalIsLeft: isLeftItem,
originalItem: this.item,
nameVariant, nameVariant,
} as RelationshipIdentifiable; } as RelationshipIdentifiable;
}), }),

View File

@@ -167,9 +167,9 @@ export class EditRelationshipComponent implements OnChanges {
) as DeleteRelationship; ) as DeleteRelationship;
}), }),
take(1), take(1),
).subscribe((deleteRelationship: DeleteRelationship) => ).subscribe((deleteRelationship: DeleteRelationship) => {
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, deleteRelationship), this.objectUpdatesService.saveRemoveFieldUpdate(this.url, deleteRelationship);
); });
} }
openVirtualMetadataModal(content: any) { openVirtualMetadataModal(content: any) {

View File

@@ -27,8 +27,7 @@
[item]="item" [item]="item"
[itemType]="entityType$ | async" [itemType]="entityType$ | async"
[relationshipType]="relationshipType" [relationshipType]="relationshipType"
[hasChanges] = hasChanges() [hasChanges]="hasChanges()"
(submit) = submit()
></ds-edit-relationship-list> ></ds-edit-relationship-list>
</div> </div>
</ng-container> </ng-container>

View File

@@ -18,49 +18,30 @@ import {
} from '@ngx-translate/core'; } from '@ngx-translate/core';
import { import {
BehaviorSubject, BehaviorSubject,
combineLatest as observableCombineLatest,
EMPTY,
Observable, Observable,
} from 'rxjs'; } from 'rxjs';
import { import { map } from 'rxjs/operators';
concatMap,
map,
switchMap,
take,
toArray,
} from 'rxjs/operators';
import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { EntityTypeDataService } from '../../../core/data/entity-type-data.service'; import { EntityTypeDataService } from '../../../core/data/entity-type-data.service';
import { ItemDataService } from '../../../core/data/item-data.service'; import { ItemDataService } from '../../../core/data/item-data.service';
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
import { FieldUpdate } from '../../../core/data/object-updates/field-update.model';
import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model';
import {
DeleteRelationship,
RelationshipIdentifiable,
} from '../../../core/data/object-updates/object-updates.reducer';
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
import { PaginatedList } from '../../../core/data/paginated-list.model'; import { PaginatedList } from '../../../core/data/paginated-list.model';
import { RelationshipDataService } from '../../../core/data/relationship-data.service'; import { RelationshipDataService } from '../../../core/data/relationship-data.service';
import { RelationshipTypeDataService } from '../../../core/data/relationship-type-data.service'; import { RelationshipTypeDataService } from '../../../core/data/relationship-type-data.service';
import { RemoteData } from '../../../core/data/remote-data';
import { RequestService } from '../../../core/data/request.service'; import { RequestService } from '../../../core/data/request.service';
import { Item } from '../../../core/shared/item.model';
import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
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 { NoContent } from '../../../core/shared/NoContent.model';
import { import {
getFirstSucceededRemoteData, getFirstSucceededRemoteData,
getRemoteDataPayload, getRemoteDataPayload,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { hasValue } from '../../../shared/empty.util';
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { followLink } from '../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
import { VarDirective } from '../../../shared/utils/var.directive'; import { VarDirective } from '../../../shared/utils/var.directive';
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
import { EditItemRelationshipsService } from './edit-item-relationships.service';
import { EditRelationshipListComponent } from './edit-relationship-list/edit-relationship-list.component'; import { EditRelationshipListComponent } from './edit-relationship-list/edit-relationship-list.component';
@Component({ @Component({
@@ -108,6 +89,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
protected relationshipTypeService: RelationshipTypeDataService, protected relationshipTypeService: RelationshipTypeDataService,
public cdr: ChangeDetectorRef, public cdr: ChangeDetectorRef,
protected modalService: NgbModal, protected modalService: NgbModal,
protected editItemRelationshipsService: EditItemRelationshipsService,
) { ) {
super(itemService, objectUpdatesService, router, notificationsService, translateService, route); super(itemService, objectUpdatesService, router, notificationsService, translateService, route);
} }
@@ -146,152 +128,14 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
* Make sure the lists are refreshed afterwards and notifications are sent for success and errors * Make sure the lists are refreshed afterwards and notifications are sent for success and errors
*/ */
public submit(): void { public submit(): void {
this.editItemRelationshipsService.submit(this.item, this.url);
// Get all the relationships that should be removed
const removeUpdates$: Observable<FieldUpdate[]> = 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<FieldUpdates>;
}),
map((fieldUpdates: FieldUpdates) =>
Object.values(fieldUpdates)
.filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE),
),
take(1),
);
const addUpdates$: Observable<FieldUpdate[]> = 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<Relationship>) => {
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();
}
});
} }
deleteRelationship(deleteRelationship: DeleteRelationship): Observable<RemoteData<NoContent>> {
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<RemoteData<Relationship>> {
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<NoContent>[]) {
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
const successfulResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
failedResponses.forEach((response: RemoteData<NoContent>) => {
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 * Sends all initial values of this item to the object updates service
*/ */
public initializeOriginalFields() { public initializeOriginalFields() {
return this.relationshipService.getRelatedItems(this.item).pipe( return this.editItemRelationshipsService.initializeOriginalFields(this.item, this.url);
take(1),
).subscribe((items: Item[]) => {
this.objectUpdatesService.initialize(this.url, items, this.item.lastModified);
});
} }