diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts index 9fcf16b62d..2635ba3f60 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts @@ -15,12 +15,12 @@ import { NotificationsService } from '../../../shared/notifications/notification import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; import { TranslateService } from '@ngx-translate/core'; import { RegistryService } from '../../../core/registry/registry.service'; -import { MetadatumViewModel } from '../../../core/shared/metadata.models'; +import { MetadataValue, MetadatumViewModel } from '../../../core/shared/metadata.models'; import { Metadata } from '../../../core/shared/metadata.utils'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { MetadataField } from '../../../core/metadata/metadata-field.model'; import { UpdateDataService } from '../../../core/data/update-data.service'; -import { hasNoValue } from '../../../shared/empty.util'; +import { hasNoValue, hasValue } from '../../../shared/empty.util'; import { AlertType } from '../../../shared/alert/aletr-type'; @Component({ @@ -115,13 +115,14 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { first(), switchMap((metadata: MetadatumViewModel[]) => { const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata: Metadata.toMetadataMap(metadata) }); - return this.updateService.update(updatedItem); + return this.updateService.update(updatedItem, ['relation.*', 'relationship.*']); }), tap(() => this.updateService.commitUpdates()), getSucceededRemoteData() ).subscribe( (rd: RemoteData) => { this.item = rd.payload; + this.checkAndFixMetadataUUIDs(); this.initializeOriginalFields(); this.updates$ = this.objectUpdatesService.getFieldUpdates(this.url, this.getMetadataAsListExcludingRelationships()); this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); @@ -143,7 +144,21 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { map((remoteData$) => remoteData$.payload.page.map((field: MetadataField) => field.toString()))); } + /** + * Get the object's metadata as a list and exclude relationship metadata + */ getMetadataAsListExcludingRelationships(): MetadatumViewModel[] { return this.item.metadataAsList.filter((metadata: MetadatumViewModel) => !metadata.key.startsWith('relation.') && !metadata.key.startsWith('relationship.')); } + + /** + * Check for empty metadata UUIDs and fix them (empty UUIDs would break the object-update service) + */ + checkAndFixMetadataUUIDs() { + const metadata = cloneDeep(this.item.metadata); + Object.keys(this.item.metadata).forEach((key: string) => { + metadata[key] = this.item.metadata[key].map((value) => hasValue(value.uuid) ? value : Object.assign(new MetadataValue(), value)); + }); + this.item.metadata = metadata; + } } diff --git a/src/app/core/cache/server-sync-buffer.effects.ts b/src/app/core/cache/server-sync-buffer.effects.ts index 3aa6ad312f..61f758e5a4 100644 --- a/src/app/core/cache/server-sync-buffer.effects.ts +++ b/src/app/core/cache/server-sync-buffer.effects.ts @@ -15,7 +15,7 @@ import { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/s import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer'; import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs'; import { RequestService } from '../data/request.service'; -import { PutRequest } from '../data/request.models'; +import { PatchRequest, PutRequest } from '../data/request.models'; import { ObjectCacheService } from './object-cache.service'; import { ApplyPatchObjectCacheAction } from './object-cache.actions'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; @@ -23,6 +23,8 @@ import { GenericConstructor } from '../shared/generic-constructor'; import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util'; import { Observable } from 'rxjs/internal/Observable'; import { RestRequestMethod } from '../data/rest-request-method'; +import { Operation } from 'fast-json-patch'; +import { ObjectCacheEntry } from './object-cache.reducer'; @Injectable() export class ServerSyncBufferEffects { @@ -96,15 +98,18 @@ export class ServerSyncBufferEffects { * @returns {Observable} ApplyPatchObjectCacheAction to be dispatched */ private applyPatch(href: string): Observable { - const patchObject = this.objectCache.getObjectBySelfLink(href).pipe(take(1)); + const patchObject = this.objectCache.getBySelfLink(href).pipe(take(1)); return patchObject.pipe( - map((object) => { - const serializedObject = new DSpaceRESTv2Serializer(object.constructor as GenericConstructor<{}>).serialize(object); - - this.requestService.configure(new PutRequest(this.requestService.generateRequestId(), href, serializedObject)); - - return new ApplyPatchObjectCacheAction(href) + map((entry: ObjectCacheEntry) => { + if (isNotEmpty(entry.patches)) { + const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations)); + const objectPatch = flatPatch.filter((op: Operation) => op.path.startsWith('/metadata')); + if (isNotEmpty(objectPatch)) { + this.requestService.configure(new PatchRequest(this.requestService.generateRequestId(), href, objectPatch)); + } + } + return new ApplyPatchObjectCacheAction(href); }) ) } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 04d1990eac..5a56fd5668 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -262,13 +262,24 @@ export abstract class DataService implements UpdateDa * The patch is derived from the differences between the given object and its version in the object cache * @param {DSpaceObject} object The given object */ - update(object: T): Observable> { + update(object: T, ignoreMetadataFields: string[] = []): Observable> { + const ignoreMetadataFieldsPrefix = ignoreMetadataFields.map((field) => field.indexOf('*') > -1 ? field.slice(0, field.indexOf('*')) : field); const oldVersion$ = this.findByHref(object.self); return oldVersion$.pipe( getSucceededRemoteData(), getRemoteDataPayload(), mergeMap((oldVersion: T) => { - const operations = this.comparator.diff(oldVersion, object); + // Fetch operations from difference between old version and new version + // Filter out any metadata operations for a field specified under ignoreMetadataFields + const operations = this.comparator.diff(oldVersion, object).filter((operation) => { + let ignoredFieldFound = false; + ignoreMetadataFieldsPrefix.forEach((fieldPrefix) => { + if (operation.path.indexOf('/metadata/' + fieldPrefix) > -1) { + ignoredFieldFound = true; + } + }); + return !ignoredFieldFound; + }); if (isNotEmpty(operations)) { this.objectCache.addPatch(object.self, operations); } diff --git a/src/app/core/data/dso-change-analyzer.service.ts b/src/app/core/data/dso-change-analyzer.service.ts index dd3487d3d0..ce3ed2452e 100644 --- a/src/app/core/data/dso-change-analyzer.service.ts +++ b/src/app/core/data/dso-change-analyzer.service.ts @@ -4,6 +4,8 @@ import { ChangeAnalyzer } from './change-analyzer'; import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model'; import { Injectable } from '@angular/core'; import { DSpaceObject } from '../shared/dspace-object.model'; +import { MetadataMap } from '../shared/metadata.models'; +import { cloneDeep } from 'lodash'; /** * A class to determine what differs between two @@ -22,6 +24,21 @@ export class DSOChangeAnalyzer implements ChangeAnalyzer * The second object to compare */ diff(object1: T | NormalizedDSpaceObject, object2: T | NormalizedDSpaceObject): Operation[] { - return compare(object1.metadata, object2.metadata).map((operation: Operation) => Object.assign({}, operation, { path: '/metadata' + operation.path })); + return compare(this.filterUUIDsFromMetadata(object1.metadata), this.filterUUIDsFromMetadata(object2.metadata)) + .map((operation: Operation) => Object.assign({}, operation, { path: '/metadata' + operation.path })); + } + + /** + * Filter the UUIDs out of a MetadataMap + * @param metadata + */ + filterUUIDsFromMetadata(metadata: MetadataMap): MetadataMap { + const result = cloneDeep(metadata); + for (const key of Object.keys(result)) { + for (const metadataValue of result[key]) { + metadataValue.uuid = undefined; + } + } + return result; } } diff --git a/src/app/core/data/item-template-data.service.ts b/src/app/core/data/item-template-data.service.ts index aaa664d7ec..e5b862b947 100644 --- a/src/app/core/data/item-template-data.service.ts +++ b/src/app/core/data/item-template-data.service.ts @@ -158,8 +158,8 @@ export class ItemTemplateDataService implements UpdateDataService { /** * Add a new patch to the object cache */ - update(object: Item): Observable> { - return this.dataService.update(object); + update(object: Item, ignoreMetadataFields: string[] = []): Observable> { + return this.dataService.update(object, ignoreMetadataFields); } /** diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index ca864f99de..f7546183ae 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -118,6 +118,8 @@ export class HeadRequest extends RestRequest { } export class PatchRequest extends RestRequest { + public responseMsToLive = 60 * 15 * 1000; + constructor( public uuid: string, public href: string, diff --git a/src/app/core/data/update-data.service.ts b/src/app/core/data/update-data.service.ts index 34835e14c1..a429be4319 100644 --- a/src/app/core/data/update-data.service.ts +++ b/src/app/core/data/update-data.service.ts @@ -6,6 +6,6 @@ import { RestRequestMethod } from './rest-request-method'; * Represents a data service to update a given object */ export interface UpdateDataService { - update(object: T): Observable>; + update(object: T, ignoreMetadataFields?: string[]): Observable>; commitUpdates(method?: RestRequestMethod); }