diff --git a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts index 5a905fc7ea..74594d2339 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/edit-in-place-field/edit-in-place-field.component.ts @@ -11,6 +11,7 @@ import { NgModel } from '@angular/forms'; import { MetadatumViewModel } from '../../../../core/shared/metadata.models'; import { MetadataField } from '../../../../core/metadata/metadata-field.model'; import { InputSuggestion } from '../../../../shared/input-suggestions/input-suggestions.model'; +import { METADATA_PATCH_OPERATION_SERVICE_TOKEN } from '../../../../core/data/object-updates/patch-operation-service/metadata-patch-operation.service'; @Component({ // tslint:disable-next-line:component-selector @@ -75,7 +76,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges { * Sends a new change update for this field to the object updates service */ update(ngModel?: NgModel) { - this.objectUpdatesService.saveChangeFieldUpdate(this.url, cloneDeep(this.metadata)); + this.objectUpdatesService.saveChangeFieldUpdate(this.url, cloneDeep(this.metadata), METADATA_PATCH_OPERATION_SERVICE_TOKEN); if (hasValue(ngModel)) { this.checkValidity(ngModel); } @@ -103,7 +104,7 @@ export class EditInPlaceFieldComponent implements OnInit, OnChanges { * Sends a new remove update for this field to the object updates service */ remove() { - this.objectUpdatesService.saveRemoveFieldUpdate(this.url, cloneDeep(this.metadata)); + this.objectUpdatesService.saveRemoveFieldUpdate(this.url, cloneDeep(this.metadata), METADATA_PATCH_OPERATION_SERVICE_TOKEN); } /** 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 52402f8b32..edeb692c43 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 @@ -17,8 +17,12 @@ 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, hasValue } from '../../../shared/empty.util'; +import { hasNoValue, hasValue, isNotEmpty } from '../../../shared/empty.util'; import { AlertType } from '../../../shared/alert/aletr-type'; +import { Operation } from 'fast-json-patch'; +import { METADATA_PATCH_OPERATION_SERVICE_TOKEN } from '../../../core/data/object-updates/patch-operation-service/metadata-patch-operation.service'; +import { DSOSuccessResponse } from '../../../core/cache/response.models'; +import { ObjectCacheService } from '../../../core/cache/object-cache.service'; @Component({ selector: 'ds-item-metadata', @@ -55,6 +59,7 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { public translateService: TranslateService, public route: ActivatedRoute, public metadataFieldService: RegistryService, + public objectCacheService: ObjectCacheService, ) { super(itemService, objectUpdatesService, router, notificationsService, translateService, route); } @@ -89,7 +94,7 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { * @param metadata The metadata to add, if no parameter is supplied, create a new Metadatum */ add(metadata: MetadatumViewModel = new MetadatumViewModel()) { - this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata); + this.objectUpdatesService.saveAddFieldUpdate(this.url, metadata, METADATA_PATCH_OPERATION_SERVICE_TOKEN); } /** @@ -106,15 +111,20 @@ export class ItemMetadataComponent extends AbstractItemUpdateComponent { public submit() { this.isValid().pipe(first()).subscribe((isValid) => { if (isValid) { - const metadata$: Observable = this.objectUpdatesService.getUpdatedFields(this.url, this.item.metadataAsList) as Observable; - metadata$.pipe( + this.objectUpdatesService.createPatch(this.url).pipe( first(), - switchMap((metadata: MetadatumViewModel[]) => { - const updatedItem: Item = Object.assign(cloneDeep(this.item), { metadata: Metadata.toMetadataMap(metadata) }); - return this.updateService.update(updatedItem); - }), - tap(() => this.updateService.commitUpdates()), - getSucceededRemoteData() + switchMap((patch: Operation[]) => { + return this.updateService.patch(this.item, patch).pipe( + switchMap((response: DSOSuccessResponse) => { + if (isNotEmpty(response.resourceSelfLinks)) { + const selfLink = response.resourceSelfLinks[0]; + this.objectCacheService.addPatch(selfLink, patch, false); + return this.itemService.findByHref(selfLink); + } + }), + getSucceededRemoteData() + ); + }) ).subscribe( (rd: RemoteData) => { this.item = rd.payload; diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index d82a1f31fe..598f168726 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -275,9 +275,11 @@ export class ObjectCacheService { * @param {Operation[]} patch * list of operations to perform */ - public addPatch(selfLink: string, patch: Operation[]) { + public addPatch(selfLink: string, patch: Operation[], addToSSD = true) { this.store.dispatch(new AddPatchObjectCacheAction(selfLink, patch)); - this.store.dispatch(new AddToSSBAction(selfLink, RestRequestMethod.PATCH)); + if (addToSSD) { + this.store.dispatch(new AddToSSBAction(selfLink, RestRequestMethod.PATCH)); + } } /** diff --git a/src/app/core/data/item-template-data.service.ts b/src/app/core/data/item-template-data.service.ts index a44d48e9bd..4f26f47eee 100644 --- a/src/app/core/data/item-template-data.service.ts +++ b/src/app/core/data/item-template-data.service.ts @@ -20,6 +20,7 @@ import { switchMap, map } from 'rxjs/operators'; import { BundleDataService } from './bundle-data.service'; import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model'; import { RestResponse } from '../cache/response.models'; +import { Operation } from 'fast-json-patch'; /* tslint:disable:max-classes-per-file */ /** @@ -165,6 +166,10 @@ export class ItemTemplateDataService implements UpdateDataService { return this.dataService.update(object); } + patch(dso: Item, operations: Operation[]): Observable { + return this.dataService.patch(dso, operations); + } + /** * Find an item template by collection ID * @param collectionID diff --git a/src/app/core/data/object-updates/object-updates.actions.ts b/src/app/core/data/object-updates/object-updates.actions.ts index f26be768b1..d98b8495d4 100644 --- a/src/app/core/data/object-updates/object-updates.actions.ts +++ b/src/app/core/data/object-updates/object-updates.actions.ts @@ -2,6 +2,8 @@ import {type} from '../../../shared/ngrx/type'; import {Action} from '@ngrx/store'; import {Identifiable} from './object-updates.reducer'; import {INotification} from '../../../shared/notifications/models/notification.model'; +import { InjectionToken } from '@angular/core'; +import { PatchOperationService } from './patch-operation-service/patch-operation.service'; /** * The list of ObjectUpdatesAction type definitions @@ -70,6 +72,7 @@ export class AddFieldUpdateAction implements Action { url: string, field: Identifiable, changeType: FieldChangeType, + patchOperationServiceToken?: InjectionToken> }; /** @@ -83,8 +86,9 @@ export class AddFieldUpdateAction implements Action { constructor( url: string, field: Identifiable, - changeType: FieldChangeType) { - this.payload = { url, field, changeType }; + changeType: FieldChangeType, + patchOperationServiceToken?: InjectionToken>) { + this.payload = { url, field, changeType, patchOperationServiceToken }; } } 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 b1626a5ff5..3a2723a10e 100644 --- a/src/app/core/data/object-updates/object-updates.reducer.ts +++ b/src/app/core/data/object-updates/object-updates.reducer.ts @@ -14,6 +14,8 @@ import { } from './object-updates.actions'; import { hasNoValue, hasValue } from '../../../shared/empty.util'; import {Relationship} from '../../shared/item-relationships/relationship.model'; +import { InjectionToken } from '@angular/core'; +import { PatchOperationService } from './patch-operation-service/patch-operation.service'; /** * Path where discarded objects are saved @@ -48,7 +50,8 @@ export interface Identifiable { */ export interface FieldUpdate { field: Identifiable, - changeType: FieldChangeType + changeType: FieldChangeType, + patchOperationServiceToken?: InjectionToken> } /** @@ -184,6 +187,7 @@ function addFieldUpdate(state: any, action: AddFieldUpdateAction) { const url: string = action.payload.url; const field: Identifiable = action.payload.field; const changeType: FieldChangeType = action.payload.changeType; + const patchOperationServiceToken: InjectionToken> = action.payload.patchOperationServiceToken; const pageState: ObjectUpdatesEntry = state[url] || {}; let states = pageState.fieldStates; @@ -194,7 +198,7 @@ function addFieldUpdate(state: any, action: AddFieldUpdateAction) { let fieldUpdate: any = pageState.fieldUpdates[field.uuid] || {}; const newChangeType = determineChangeType(fieldUpdate.changeType, changeType); - fieldUpdate = Object.assign({}, { field, changeType: newChangeType }); + fieldUpdate = Object.assign({}, { field, changeType: newChangeType, patchOperationServiceToken }); const fieldUpdates = Object.assign({}, pageState.fieldUpdates, { [field.uuid]: fieldUpdate }); diff --git a/src/app/core/data/object-updates/object-updates.service.ts b/src/app/core/data/object-updates/object-updates.service.ts index 84f0f06035..5889c6e499 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -1,4 +1,4 @@ -import { Injectable } from '@angular/core'; +import { Injectable, InjectionToken, Injector } from '@angular/core'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { CoreState } from '../../core.reducers'; import { coreSelector } from '../../core.selectors'; @@ -26,6 +26,8 @@ import { import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; import { hasNoValue, hasValue, isEmpty, isNotEmpty, isNotEmptyOperator } from '../../../shared/empty.util'; import { INotification } from '../../../shared/notifications/models/notification.model'; +import { Operation } from 'fast-json-patch'; +import { PatchOperationService } from './patch-operation-service/patch-operation.service'; function objectUpdatesStateSelector(): MemoizedSelector { return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']); @@ -48,7 +50,8 @@ function virtualMetadataSourceSelector(url: string, source: string): MemoizedSel */ @Injectable() export class ObjectUpdatesService { - constructor(private store: Store) { + constructor(private store: Store, + private injector: Injector) { } /** @@ -67,8 +70,8 @@ export class ObjectUpdatesService { * @param field An updated field for the page's object * @param changeType The last type of change applied to this field */ - private saveFieldUpdate(url: string, field: Identifiable, changeType: FieldChangeType) { - this.store.dispatch(new AddFieldUpdateAction(url, field, changeType)) + private saveFieldUpdate(url: string, field: Identifiable, changeType: FieldChangeType, patchOperationServiceToken?: InjectionToken>) { + this.store.dispatch(new AddFieldUpdateAction(url, field, changeType, patchOperationServiceToken)) } /** @@ -185,8 +188,8 @@ export class ObjectUpdatesService { * @param url The page's URL for which the changes are saved * @param field An updated field for the page's object */ - saveAddFieldUpdate(url: string, field: Identifiable) { - this.saveFieldUpdate(url, field, FieldChangeType.ADD); + saveAddFieldUpdate(url: string, field: Identifiable, patchOperationServiceToken?: InjectionToken>) { + this.saveFieldUpdate(url, field, FieldChangeType.ADD, patchOperationServiceToken); } /** @@ -194,8 +197,8 @@ export class ObjectUpdatesService { * @param url The page's URL for which the changes are saved * @param field An updated field for the page's object */ - saveRemoveFieldUpdate(url: string, field: Identifiable) { - this.saveFieldUpdate(url, field, FieldChangeType.REMOVE); + saveRemoveFieldUpdate(url: string, field: Identifiable, patchOperationServiceToken?: InjectionToken>) { + this.saveFieldUpdate(url, field, FieldChangeType.REMOVE, patchOperationServiceToken); } /** @@ -203,8 +206,8 @@ export class ObjectUpdatesService { * @param url The page's URL for which the changes are saved * @param field An updated field for the page's object */ - saveChangeFieldUpdate(url: string, field: Identifiable) { - this.saveFieldUpdate(url, field, FieldChangeType.UPDATE); + saveChangeFieldUpdate(url: string, field: Identifiable, patchOperationServiceToken?: InjectionToken>) { + this.saveFieldUpdate(url, field, FieldChangeType.UPDATE, patchOperationServiceToken); } /** @@ -339,4 +342,23 @@ export class ObjectUpdatesService { getLastModified(url: string): Observable { return this.getObjectEntry(url).pipe(map((entry: ObjectUpdatesEntry) => entry.lastModified)); } + + /** + * Create a patch from the current object-updates state + * @param url The URL of the page for which the patch should be created + */ + createPatch(url: string): Observable { + return this.getObjectEntry(url).pipe( + map((entry) => { + const patch = []; + Object.keys(entry.fieldUpdates).forEach((uuid) => { + const update = entry.fieldUpdates[uuid]; + if (hasValue(update.patchOperationServiceToken)) { + patch.push(this.injector.get(update.patchOperationServiceToken).fieldUpdateToPatchOperation(update)); + } + }); + return patch; + }) + ); + } } diff --git a/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts new file mode 100644 index 0000000000..cbc441c3da --- /dev/null +++ b/src/app/core/data/object-updates/patch-operation-service/metadata-patch-operation.service.ts @@ -0,0 +1,29 @@ +import { PatchOperationService } from './patch-operation.service'; +import { MetadataValue, MetadatumViewModel } from '../../../shared/metadata.models'; +import { FieldUpdate } from '../object-updates.reducer'; +import { Operation } from 'fast-json-patch'; +import { FieldChangeType } from '../object-updates.actions'; +import { InjectionToken } from '@angular/core'; + +export const METADATA_PATCH_OPERATION_SERVICE_TOKEN = new InjectionToken('MetadataPatchOperationService', { + providedIn: 'root', + factory: () => new MetadataPatchOperationService(), +}); + +export class MetadataPatchOperationService implements PatchOperationService { + fieldUpdateToPatchOperation(fieldUpdate: FieldUpdate): Operation { + const metadatum = fieldUpdate.field as MetadatumViewModel; + const path = `/metadata/${metadatum.key}`; + const val = { + value: metadatum.value, + language: metadatum.language + } + + switch (fieldUpdate.changeType) { + case FieldChangeType.ADD: return { op: 'add', path, value: [ val ] }; + case FieldChangeType.REMOVE: return { op: 'remove', path: `${path}/${metadatum.place}` }; + case FieldChangeType.UPDATE: return { op: 'replace', path: `${path}/${metadatum.place}`, value: val }; + default: return undefined; + } + } +} diff --git a/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts b/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts new file mode 100644 index 0000000000..7a88e41b41 --- /dev/null +++ b/src/app/core/data/object-updates/patch-operation-service/patch-operation.service.ts @@ -0,0 +1,6 @@ +import { FieldUpdate, Identifiable } from '../object-updates.reducer'; +import { Operation } from 'fast-json-patch'; + +export interface PatchOperationService { + fieldUpdateToPatchOperation(fieldUpdate: FieldUpdate): Operation; +} diff --git a/src/app/core/data/update-data.service.ts b/src/app/core/data/update-data.service.ts index 34835e14c1..f572c75982 100644 --- a/src/app/core/data/update-data.service.ts +++ b/src/app/core/data/update-data.service.ts @@ -1,11 +1,14 @@ import { Observable } from 'rxjs/internal/Observable'; import { RemoteData } from './remote-data'; import { RestRequestMethod } from './rest-request-method'; +import { Operation } from 'fast-json-patch'; +import { RestResponse } from '../cache/response.models'; /** * Represents a data service to update a given object */ export interface UpdateDataService { + patch(dso: T, operations: Operation[]): Observable; update(object: T): Observable>; commitUpdates(method?: RestRequestMethod); }