From ae476baa6276732eba25c37181e0d145e863b20b Mon Sep 17 00:00:00 2001
From: Samuel <samuel@atmire.com>
Date: Tue, 26 Nov 2019 16:23:47 +0100
Subject: [PATCH] taskid 66074 Keep virtual metadata on relationship delete

---
 resources/i18n/en.json5                       |   1 +
 .../edit-item-page/edit-item-page.module.ts   |   2 +
 .../edit-relationship-list.component.html     |   1 +
 .../edit-relationship-list.component.ts       |  23 +--
 .../edit-relationship.component.html          |  15 +-
 .../edit-relationship.component.spec.ts       |   2 +-
 .../edit-relationship.component.ts            | 101 ++++++++--
 .../item-relationships.component.ts           |  50 +++--
 .../virtual-metadata.component.html           |  42 +++++
 .../virtual-metadata.component.spec.ts        | 172 ++++++++++++++++++
 .../virtual-metadata.component.ts             |  66 +++++++
 .../object-updates/object-updates.actions.ts  |  40 +++-
 .../object-updates.reducer.spec.ts            |   4 +-
 .../object-updates/object-updates.reducer.ts  |  73 +++++++-
 .../object-updates/object-updates.service.ts  |  63 ++++++-
 .../core/data/relationship.service.spec.ts    |   2 +-
 src/app/core/data/relationship.service.ts     |   9 +-
 17 files changed, 595 insertions(+), 71 deletions(-)
 create mode 100644 src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.html
 create mode 100644 src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.spec.ts
 create mode 100644 src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.ts

diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5
index 0a1d67c9c2..f104099d26 100644
--- a/resources/i18n/en.json5
+++ b/resources/i18n/en.json5
@@ -1686,5 +1686,6 @@
 
   "uploader.queue-length": "Queue length",
 
+  "virtual-metadata-modal.head": "Select the items for which you want to save the virtual metadata as real metadata",
 }
 
diff --git a/src/app/+item-page/edit-item-page/edit-item-page.module.ts b/src/app/+item-page/edit-item-page/edit-item-page.module.ts
index 77740f0c6c..6d86e7f35c 100644
--- a/src/app/+item-page/edit-item-page/edit-item-page.module.ts
+++ b/src/app/+item-page/edit-item-page/edit-item-page.module.ts
@@ -21,6 +21,7 @@ import { ItemRelationshipsComponent } from './item-relationships/item-relationsh
 import { EditRelationshipComponent } from './item-relationships/edit-relationship/edit-relationship.component';
 import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
 import { ItemMoveComponent } from './item-move/item-move.component';
+import {VirtualMetadataComponent} from "./virtual-metadata/virtual-metadata.component";
 
 /**
  * Module that contains all components related to the Edit Item page administrator functionality
@@ -51,6 +52,7 @@ import { ItemMoveComponent } from './item-move/item-move.component';
     EditRelationshipListComponent,
     ItemCollectionMapperComponent,
     ItemMoveComponent,
+    VirtualMetadataComponent,
   ]
 })
 export class EditItemPageModule {
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 ba5164e81a..8edcb6808e 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
@@ -7,6 +7,7 @@
            class="relationship-row d-block"
            [fieldUpdate]="updateValue || {}"
            [url]="url"
+           [editItem]="item"
            [ngClass]="{'alert alert-danger': updateValue.changeType === 2}">
       </div>
       <ds-loading *ngIf="updateValues.length == 0" message="{{'loading.items' | translate}}"></ds-loading>
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 3a145c99e0..e7d1a14200 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
@@ -4,10 +4,9 @@ import { Observable } from 'rxjs/internal/Observable';
 import { FieldUpdate, FieldUpdates } from '../../../../core/data/object-updates/object-updates.reducer';
 import { RelationshipService } from '../../../../core/data/relationship.service';
 import { Item } from '../../../../core/shared/item.model';
-import { map, switchMap } from 'rxjs/operators';
+import { map, switchMap} from 'rxjs/operators';
 import { hasValue } from '../../../../shared/empty.util';
-import { RemoteData } from '../../../../core/data/remote-data';
-import { PaginatedList } from '../../../../core/data/paginated-list';
+import {Relationship} from "../../../../core/shared/item-relationships/relationship.model";
 
 @Component({
   selector: 'ds-edit-relationship-list',
@@ -61,22 +60,17 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
     this.updates$ = this.getUpdatesByLabel(this.relationshipLabel);
   }
 
-  /**
-   * Transform the item's relationships of a specific type into related items
-   * @param label   The relationship type's label
-   */
-  public getRelatedItemsByLabel(label: string): Observable<RemoteData<PaginatedList<Item>>> {
-    return this.relationshipService.getRelatedItemsByLabel(this.item, label);
-  }
-
   /**
    * Get FieldUpdates for the relationships of a specific type
    * @param label   The relationship type's label
    */
   public getUpdatesByLabel(label: string): Observable<FieldUpdates> {
-    return this.getRelatedItemsByLabel(label).pipe(
-      switchMap((itemsRD) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, itemsRD.payload.page))
-    )
+    return this.relationshipService.getItemRelationshipsByLabel(this.item, label).pipe(
+      map(relationsRD => relationsRD.payload.page.map(relationship =>
+        Object.assign(new Relationship(), relationship, {uuid: relationship.id})
+      )),
+      switchMap((initialFields) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, initialFields)),
+    );
   }
 
   /**
@@ -97,5 +91,4 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
   trackUpdate(index, update: FieldUpdate) {
     return update && update.field ? update.field.uuid : undefined;
   }
-
 }
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.html b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.html
index 03040ce8e0..245bf04051 100644
--- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.html
+++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.html
@@ -1,10 +1,10 @@
-<div class="row" *ngIf="item">
+<div class="row" *ngIf="relatedItem$ | async">
   <div class="col-10 relationship">
-    <ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
+    <ds-listable-object-component-loader [object]="relatedItem$ | async" [viewMode]="viewMode"></ds-listable-object-component-loader>
   </div>
   <div class="col-2">
     <div class="btn-group relationship-action-buttons">
-      <button [disabled]="!canRemove()" (click)="remove()"
+      <button [disabled]="!canRemove()" (click)="openVirtualMetadataModal(virtualMetadataModal)"
               class="btn btn-outline-danger btn-sm"
               title="{{'item.edit.metadata.edit.buttons.remove' | translate}}">
         <i class="fas fa-trash-alt fa-fw"></i>
@@ -17,3 +17,12 @@
     </div>
   </div>
 </div>
+<ng-template #virtualMetadataModal>
+  <ds-virtual-metadata
+          [relationship]="relationship"
+          [url]="url"
+          (close)="closeVirtualMetadataModal()"
+          (save)="remove()"
+  >
+  </ds-virtual-metadata>
+</ng-template>
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts
index 54fce0a68e..00c2a33006 100644
--- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts
+++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts
@@ -112,7 +112,7 @@ describe('EditRelationshipComponent', () => {
 
     comp.url = url;
     comp.fieldUpdate = fieldUpdate1;
-    comp.item = item;
+    comp.editItem = item;
 
     fixture.detectChanges();
   });
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 302ebf68a7..7c1780da58 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
@@ -1,10 +1,15 @@
-import { Component, Input, OnChanges } from '@angular/core';
-import { FieldUpdate } from '../../../../core/data/object-updates/object-updates.reducer';
-import { cloneDeep } from 'lodash';
-import { Item } from '../../../../core/shared/item.model';
-import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
-import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions';
-import { ViewMode } from '../../../../core/shared/view-mode.model';
+import {Component, Input, OnChanges, OnInit} from '@angular/core';
+import {combineLatest as observableCombineLatest, Observable} from 'rxjs';
+import {filter, map, switchMap, take, tap} from 'rxjs/operators';
+import {FieldChangeType} from '../../../../core/data/object-updates/object-updates.actions';
+import {DeleteRelationship, FieldUpdate} from '../../../../core/data/object-updates/object-updates.reducer';
+import {ObjectUpdatesService} from '../../../../core/data/object-updates/object-updates.service';
+import {Relationship} from '../../../../core/shared/item-relationships/relationship.model';
+import {Item} from '../../../../core/shared/item.model';
+import {getRemoteDataPayload, getSucceededRemoteData} from '../../../../core/shared/operators';
+import {ViewMode} from '../../../../core/shared/view-mode.model';
+import {hasValue, isNotEmpty} from '../../../../shared/empty.util';
+import {NgbModal, NgbModalRef} from "@ng-bootstrap/ng-bootstrap";
 
 @Component({
   // tslint:disable-next-line:component-selector
@@ -23,38 +28,109 @@ export class EditRelationshipComponent implements OnChanges {
    */
   @Input() url: string;
 
+  /**
+   * The item being edited
+   */
+  @Input() editItem: Item;
+
+  /**
+   * The relationship being edited
+   */
+  get relationship(): Relationship {
+    return this.fieldUpdate.field as Relationship;
+  }
+
+  private leftItem$: Observable<Item>;
+  private rightItem$: Observable<Item>;
+
   /**
    * The related item of this relationship
    */
-  item: Item;
+  relatedItem$: Observable<Item>;
 
   /**
    * The view-mode we're currently on
    */
   viewMode = ViewMode.ListElement;
 
-  constructor(private objectUpdatesService: ObjectUpdatesService) {
+  constructor(
+    private objectUpdatesService: ObjectUpdatesService,
+    private modalService: NgbModal,
+  ) {
   }
 
   /**
    * Sets the current relationship based on the fieldUpdate input field
    */
   ngOnChanges(): void {
-    this.item = cloneDeep(this.fieldUpdate.field) as Item;
+    this.leftItem$ = this.relationship.leftItem.pipe(
+      getSucceededRemoteData(),
+      getRemoteDataPayload(),
+      filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid))
+    );
+    this.rightItem$ = this.relationship.rightItem.pipe(
+      getSucceededRemoteData(),
+      getRemoteDataPayload(),
+      filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid))
+    );
+    this.relatedItem$ = observableCombineLatest(
+      this.leftItem$,
+      this.rightItem$,
+    ).pipe(
+      map((items: Item[]) =>
+        items.find((item) => item.uuid !== this.editItem.uuid)
+      )
+    );
   }
 
   /**
    * Sends a new remove update for this field to the object updates service
    */
   remove(): void {
-    this.objectUpdatesService.saveRemoveFieldUpdate(this.url, this.item);
+    this.closeVirtualMetadataModal();
+    observableCombineLatest(
+      this.leftItem$,
+      this.rightItem$,
+    ).pipe(
+      map((items: Item[]) =>
+        items.map(item => this.objectUpdatesService
+          .isSelectedVirtualMetadataItem(this.url, this.relationship.id, item.uuid))
+      ),
+      switchMap((selection$: Observable<boolean>[]) => observableCombineLatest(selection$)),
+      map((selection: boolean[]) => {
+        return Object.assign({},
+          this.fieldUpdate.field,
+          {
+            uuid: this.relationship.id,
+            keepLeftVirtualMetadata: selection[0] == true,
+            keepRightVirtualMetadata: selection[1] == true,
+          }
+        ) as DeleteRelationship
+      }),
+      take(1),
+    ).subscribe((deleteRelationship: DeleteRelationship) =>
+      this.objectUpdatesService.saveRemoveFieldUpdate(this.url, deleteRelationship)
+    );
+  }
+
+  /**
+   * Reference to NgbModal
+   */
+  public modalRef: NgbModalRef;
+
+  openVirtualMetadataModal(content: any) {
+    this.modalRef = this.modalService.open(content);
+  }
+
+  closeVirtualMetadataModal() {
+    this.modalRef.close();
   }
 
   /**
    * Cancels the current update for this field in the object updates service
    */
   undo(): void {
-    this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.item.uuid);
+    this.objectUpdatesService.removeSingleFieldUpdate(this.url, this.fieldUpdate.field.uuid);
   }
 
   /**
@@ -70,5 +146,4 @@ export class EditRelationshipComponent implements OnChanges {
   canUndo(): boolean {
     return this.fieldUpdate.changeType >= 0;
   }
-
 }
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 e8f34bc70e..92b138e962 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,8 +1,12 @@
 import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core';
 import { Item } from '../../../core/shared/item.model';
-import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer';
+import {
+  DeleteRelationship,
+  FieldUpdate,
+  FieldUpdates
+} from '../../../core/data/object-updates/object-updates.reducer';
 import { Observable } from 'rxjs/internal/Observable';
-import { filter, map, switchMap, take, tap } from 'rxjs/operators';
+import {filter, map, switchMap, take, tap} from 'rxjs/operators';
 import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs';
 import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
 import { ItemDataService } from '../../../core/data/item-data.service';
@@ -18,7 +22,7 @@ import { ErrorResponse, RestResponse } from '../../../core/cache/response.models
 import { isNotEmptyOperator } from '../../../shared/empty.util';
 import { RemoteData } from '../../../core/data/remote-data';
 import { ObjectCacheService } from '../../../core/cache/object-cache.service';
-import { getSucceededRemoteData } from '../../../core/shared/operators';
+import { getSucceededRemoteData} from '../../../core/shared/operators';
 import { RequestService } from '../../../core/data/request.service';
 import { Subscription } from 'rxjs/internal/Subscription';
 import { getRelationsByRelatedItemIds } from '../../simple/item-types/shared/item-relationships-utils';
@@ -104,22 +108,36 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
    * Make sure the lists are refreshed afterwards and notifications are sent for success and errors
    */
   public submit(): void {
-    // Get all IDs of related items of which their relationship with the current item is about to be removed
-    const removedItemIds$ = this.relationshipService.getRelatedItems(this.item).pipe(
-      switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, items) as Observable<FieldUpdates>),
-      map((fieldUpdates: FieldUpdates) => Object.values(fieldUpdates).filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE)),
-      map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field.uuid) as string[]),
-      isNotEmptyOperator()
-    );
     // Get all the relationships that should be removed
-    const removedRelationships$ = removedItemIds$.pipe(
-      getRelationsByRelatedItemIds(this.item, this.relationshipService)
+    const removedRelationshipIDs$ = 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)),
+      map((fieldUpdates: FieldUpdate[]) => fieldUpdates.map((fieldUpdate: FieldUpdate) => fieldUpdate.field) as DeleteRelationship[]),
+      isNotEmptyOperator(),
     );
-    // Request a delete for every relationship found in the observable created above
-    removedRelationships$.pipe(
+    removedRelationshipIDs$.pipe(
       take(1),
-      map((removedRelationships: Relationship[]) => removedRelationships.map((rel: Relationship) => rel.id)),
-      switchMap((removedIds: string[]) => observableZip(...removedIds.map((uuid: string) => this.relationshipService.deleteRelationship(uuid))))
+      switchMap((deleteRelationships: DeleteRelationship[]) =>
+        observableZip(...deleteRelationships.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);
+          }
+        ))
+      ),
     ).subscribe((responses: RestResponse[]) => {
       this.displayNotifications(responses);
       this.reset();
diff --git a/src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.html b/src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.html
new file mode 100644
index 0000000000..aafdb00b47
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.html
@@ -0,0 +1,42 @@
+<div>
+    <div class="modal-header">{{'virtual-metadata-modal.head' | translate}}
+        <button type="button" class="close" (click)="close.emit()" aria-label="Close">
+            <span aria-hidden="true">×</span>
+        </button>
+    </div>
+    <div class="modal-body">
+        <div *ngFor="let item$ of [leftItem$, rightItem$]">
+            <div *ngVar="item$ | async as item">
+                <div *ngVar="(isSelectedVirtualMetadataItem(item) | async) as selected"
+                     (click)="setSelectedVirtualMetadataItem(item, !selected)"
+                     class="d-flex flex-row">
+                    <div class="m-2">
+                        <label>
+                            <input type="checkbox" [checked]="selected">
+                        </label>
+                    </div>
+                    <div class="flex-column">
+                        <ds-listable-object-component-loader
+                                [object]="item$ | async"></ds-listable-object-component-loader>
+                        <div *ngFor="let metadata of getVirtualMetadata(relationship, item$ | async)">
+                            <div>
+                                <div class="font-weight-bold">
+                                    {{metadata.metadataField}}
+                                </div>
+                                <div>
+                                    {{metadata.metadataValue.value}}
+                                </div>
+                            </div>
+                        </div>
+                    </div>
+                </div>
+            </div>
+        </div>
+        <div class="d-flex flex-row-reverse m-2">
+            <button class="btn btn-primary"
+                    (click)="save.emit()"><i
+                    class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
+            </button>
+        </div>
+    </div>
+</div>
diff --git a/src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.spec.ts b/src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.spec.ts
new file mode 100644
index 0000000000..7b37b238e8
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.spec.ts
@@ -0,0 +1,172 @@
+import { async, ComponentFixture, TestBed } from '@angular/core/testing';
+import { Item } from '../../../core/shared/item.model';
+import { RouterStub } from '../../../shared/testing/router-stub';
+import { CommonModule } from '@angular/common';
+import { RouterTestingModule } from '@angular/router/testing';
+import { TranslateModule } from '@ngx-translate/core';
+import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
+import { ActivatedRoute, Router } from '@angular/router';
+import { VirtualMetadataComponent } from './virtual-metadata.component';
+import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
+import { NotificationsService } from '../../../shared/notifications/notifications.service';
+import { SearchService } from '../../../+search-page/search-service/search.service';
+import { of as observableOf } from 'rxjs';
+import { FormsModule } from '@angular/forms';
+import { ItemDataService } from '../../../core/data/item-data.service';
+import { RemoteData } from '../../../core/data/remote-data';
+import { PaginatedList } from '../../../core/data/paginated-list';
+import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
+import { RestResponse } from '../../../core/cache/response.models';
+import { Collection } from '../../../core/shared/collection.model';
+
+// describe('ItemMoveComponent', () => {
+//   let comp: VirtualMetadataComponent;
+//   let fixture: ComponentFixture<VirtualMetadataComponent>;
+//
+//   const mockItem = Object.assign(new Item(), {
+//     id: 'fake-id',
+//     handle: 'fake/handle',
+//     lastModified: '2018'
+//   });
+//
+//   const itemPageUrl = `fake-url/${mockItem.id}`;
+//   const routerStub = Object.assign(new RouterStub(), {
+//     url: `${itemPageUrl}/edit`
+//   });
+//
+//   const mockItemDataService = jasmine.createSpyObj({
+//     moveToCollection: observableOf(new RestResponse(true, 200, 'Success'))
+//   });
+//
+//   const mockItemDataServiceFail = jasmine.createSpyObj({
+//     moveToCollection: observableOf(new RestResponse(false, 500, 'Internal server error'))
+//   });
+//
+//   const routeStub = {
+//     data: observableOf({
+//       item: new RemoteData(false, false, true, null, {
+//         id: 'item1'
+//       })
+//     })
+//   };
+//
+//   const collection1 = Object.assign(new Collection(),{
+//     uuid: 'collection-uuid-1',
+//     name: 'Test collection 1',
+//     self: 'self-link-1',
+//   });
+//
+//   const collection2 = Object.assign(new Collection(),{
+//     uuid: 'collection-uuid-2',
+//     name: 'Test collection 2',
+//     self: 'self-link-2',
+//   });
+//
+//   const mockSearchService = {
+//     search: () => {
+//       return observableOf(new RemoteData(false, false, true, null,
+//         new PaginatedList(null, [
+//           {
+//             indexableObject: collection1,
+//             hitHighlights: {}
+//           }, {
+//             indexableObject: collection2,
+//             hitHighlights: {}
+//           }
+//         ])));
+//     }
+//   };
+//
+//   const notificationsServiceStub = new NotificationsServiceStub();
+//
+//   describe('ItemMoveComponent success', () => {
+//     beforeEach(async(() => {
+//       TestBed.configureTestingModule({
+//         imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+//         declarations: [VirtualMetadataComponent],
+//         providers: [
+//           {provide: ActivatedRoute, useValue: routeStub},
+//           {provide: Router, useValue: routerStub},
+//           {provide: ItemDataService, useValue: mockItemDataService},
+//           {provide: NotificationsService, useValue: notificationsServiceStub},
+//           {provide: SearchService, useValue: mockSearchService},
+//         ], schemas: [
+//           CUSTOM_ELEMENTS_SCHEMA
+//         ]
+//       }).compileComponents();
+//     }));
+//
+//     beforeEach(() => {
+//       fixture = TestBed.createComponent(VirtualMetadataComponent);
+//       comp = fixture.componentInstance;
+//       fixture.detectChanges();
+//     });
+//     it('should load suggestions', () => {
+//       const expected = [
+//         collection1,
+//         collection2
+//       ];
+//
+//       comp.collectionSearchResults.subscribe((value) => {
+//           expect(value).toEqual(expected);
+//         }
+//       );
+//     });
+//     it('should get current url ', () => {
+//       expect(comp.getCurrentUrl()).toEqual('fake-url/fake-id/edit');
+//     });
+//     it('should on click select the correct collection name and id', () => {
+//       const data = collection1;
+//
+//       comp.onClick(data);
+//
+//       expect(comp.selectedCollectionName).toEqual('Test collection 1');
+//       expect(comp.selectedCollection).toEqual(collection1);
+//     });
+//     describe('moveCollection', () => {
+//       it('should call itemDataService.moveToCollection', () => {
+//         comp.itemId = 'item-id';
+//         comp.selectedCollectionName = 'selected-collection-id';
+//         comp.selectedCollection = collection1;
+//         comp.moveCollection();
+//
+//         expect(mockItemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1);
+//       });
+//       it('should call notificationsService success message on success', () => {
+//         comp.moveCollection();
+//
+//         expect(notificationsServiceStub.success).toHaveBeenCalled();
+//       });
+//     });
+//   });
+//
+//   describe('ItemMoveComponent fail', () => {
+//     beforeEach(async(() => {
+//       TestBed.configureTestingModule({
+//         imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
+//         declarations: [VirtualMetadataComponent],
+//         providers: [
+//           {provide: ActivatedRoute, useValue: routeStub},
+//           {provide: Router, useValue: routerStub},
+//           {provide: ItemDataService, useValue: mockItemDataServiceFail},
+//           {provide: NotificationsService, useValue: notificationsServiceStub},
+//           {provide: SearchService, useValue: mockSearchService},
+//         ], schemas: [
+//           CUSTOM_ELEMENTS_SCHEMA
+//         ]
+//       }).compileComponents();
+//     }));
+//
+//     beforeEach(() => {
+//       fixture = TestBed.createComponent(VirtualMetadataComponent);
+//       comp = fixture.componentInstance;
+//       fixture.detectChanges();
+//     });
+//
+//     it('should call notificationsService error message on fail', () => {
+//       comp.moveCollection();
+//
+//       expect(notificationsServiceStub.error).toHaveBeenCalled();
+//     });
+//   });
+// });
diff --git a/src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.ts b/src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.ts
new file mode 100644
index 0000000000..c3379750a2
--- /dev/null
+++ b/src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.ts
@@ -0,0 +1,66 @@
+import {Component, EventEmitter, Input, OnChanges, Output} from '@angular/core';
+import {ActivatedRoute} from '@angular/router';
+import {Observable} from 'rxjs';
+import {Item} from "../../../core/shared/item.model";
+import {Relationship} from "../../../core/shared/item-relationships/relationship.model";
+import {MetadataValue} from "../../../core/shared/metadata.models";
+import {getRemoteDataPayload, getSucceededRemoteData} from "../../../core/shared/operators";
+import {ObjectUpdatesService} from "../../../core/data/object-updates/object-updates.service";
+
+@Component({
+  selector: 'ds-virtual-metadata',
+  templateUrl: './virtual-metadata.component.html'
+})
+/**
+ * Component that handles the moving of an item to a different collection
+ */
+export class VirtualMetadataComponent implements OnChanges {
+
+  /**
+   * The current url of this page
+   */
+  @Input() url: string;
+
+  @Input() relationship: Relationship;
+
+  @Output() close = new EventEmitter();
+  @Output() save = new EventEmitter();
+
+  constructor(
+    protected route: ActivatedRoute,
+    protected objectUpdatesService: ObjectUpdatesService,
+  ) {
+  }
+
+  leftItem$: Observable<Item>;
+  rightItem$: Observable<Item>;
+
+  ngOnChanges(): void {
+    this.leftItem$ = this.relationship.leftItem.pipe(
+      getSucceededRemoteData(),
+      getRemoteDataPayload(),
+    );
+    this.rightItem$ = this.relationship.rightItem.pipe(
+      getSucceededRemoteData(),
+      getRemoteDataPayload(),
+    );
+  }
+
+  getVirtualMetadata(relationship: Relationship, relatedItem: Item): VirtualMetadata[] {
+
+    return this.objectUpdatesService.getVirtualMetadataList(relationship, relatedItem);
+  }
+
+  setSelectedVirtualMetadataItem(item: Item, selected: boolean) {
+    this.objectUpdatesService.setSelectedVirtualMetadataItem(this.url, this.relationship.id, item.uuid, selected);
+  }
+
+  isSelectedVirtualMetadataItem(item: Item): Observable<boolean> {
+    return this.objectUpdatesService.isSelectedVirtualMetadataItem(this.url, this.relationship.id, item.uuid);
+  }
+}
+
+export interface VirtualMetadata {
+  metadataField: string,
+  metadataValue: MetadataValue,
+}
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 6cd74b2626..17ad145eb6 100644
--- a/src/app/core/data/object-updates/object-updates.actions.ts
+++ b/src/app/core/data/object-updates/object-updates.actions.ts
@@ -1,7 +1,7 @@
-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 {type} from '../../../shared/ngrx/type';
+import {Action} from '@ngrx/store';
+import {Identifiable} from './object-updates.reducer';
+import {INotification} from '../../../shared/notifications/models/notification.model';
 
 /**
  * The list of ObjectUpdatesAction type definitions
@@ -11,6 +11,7 @@ export const ObjectUpdatesActionTypes = {
   SET_EDITABLE_FIELD: type('dspace/core/cache/object-updates/SET_EDITABLE_FIELD'),
   SET_VALID_FIELD: type('dspace/core/cache/object-updates/SET_VALID_FIELD'),
   ADD_FIELD: type('dspace/core/cache/object-updates/ADD_FIELD'),
+  SELECT_VIRTUAL_METADATA: type('dspace/core/cache/object-updates/SELECT_VIRTUAL_METADATA'),
   DISCARD: type('dspace/core/cache/object-updates/DISCARD'),
   REINSTATE: type('dspace/core/cache/object-updates/REINSTATE'),
   REMOVE: type('dspace/core/cache/object-updates/REMOVE'),
@@ -83,6 +84,34 @@ export class AddFieldUpdateAction implements Action {
   }
 }
 
+export class SelectVirtualMetadataAction implements Action {
+
+  type = ObjectUpdatesActionTypes.SELECT_VIRTUAL_METADATA;
+  payload: {
+    url: string,
+    source: string,
+    uuid: string,
+    select: boolean;
+  };
+
+  /**
+   * Create a new AddFieldUpdateAction
+   *
+   * @param url
+   *    the unique url of the page for which a field update is added
+   * @param field The identifiable field of which a new update is added
+   * @param changeType The update's change type
+   */
+  constructor(
+    url: string,
+    source: string,
+    uuid: string,
+    select: boolean,
+  ) {
+    this.payload = { url, source, uuid, select: select};
+  }
+}
+
 /**
  * An ngrx action to set the editable state of an existing field in the ObjectUpdates state for a certain page url
  */
@@ -242,4 +271,5 @@ export type ObjectUpdatesAction
   | DiscardObjectUpdatesAction
   | ReinstateObjectUpdatesAction
   | RemoveObjectUpdatesAction
-  | RemoveFieldUpdateAction;
+  | RemoveFieldUpdateAction
+  | SelectVirtualMetadataAction;
diff --git a/src/app/core/data/object-updates/object-updates.reducer.spec.ts b/src/app/core/data/object-updates/object-updates.reducer.spec.ts
index f5698b9b78..8d821c9926 100644
--- a/src/app/core/data/object-updates/object-updates.reducer.spec.ts
+++ b/src/app/core/data/object-updates/object-updates.reducer.spec.ts
@@ -79,7 +79,8 @@ describe('objectUpdatesReducer', () => {
           changeType: FieldChangeType.ADD
         }
       },
-      lastModified: modDate
+      lastModified: modDate,
+      virtualMetadataSources: {},
     }
   };
 
@@ -213,6 +214,7 @@ describe('objectUpdatesReducer', () => {
           },
         },
         fieldUpdates: {},
+        virtualMetadataSources: {},
         lastModified: modDate
       }
     };
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 c0f10ff92a..41d1704797 100644
--- a/src/app/core/data/object-updates/object-updates.reducer.ts
+++ b/src/app/core/data/object-updates/object-updates.reducer.ts
@@ -7,9 +7,13 @@ import {
   ObjectUpdatesActionTypes,
   ReinstateObjectUpdatesAction,
   RemoveFieldUpdateAction,
-  RemoveObjectUpdatesAction, SetEditableFieldUpdateAction, SetValidFieldUpdateAction
+  RemoveObjectUpdatesAction,
+  SetEditableFieldUpdateAction,
+  SetValidFieldUpdateAction,
+  SelectVirtualMetadataAction,
 } from './object-updates.actions';
 import { hasNoValue, hasValue } from '../../../shared/empty.util';
+import {Relationship} from "../../shared/item-relationships/relationship.model";
 
 /**
  * Path where discarded objects are saved
@@ -42,7 +46,7 @@ export interface Identifiable {
 /**
  * The state of a single field update
  */
-export interface FieldUpdate {
+export interface  FieldUpdate {
   field: Identifiable,
   changeType: FieldChangeType
 }
@@ -54,12 +58,26 @@ export interface FieldUpdates {
   [uuid: string]: FieldUpdate;
 }
 
+export interface VirtualMetadataSources {
+  [source: string]: VirtualMetadataSource
+}
+
+export interface VirtualMetadataSource {
+  [uuid: string]: boolean,
+}
+
+export interface DeleteRelationship extends Relationship {
+  keepLeftVirtualMetadata: boolean,
+  keepRightVirtualMetadata: boolean,
+}
+
 /**
  * The updated state of a single page
  */
 export interface ObjectUpdatesEntry {
   fieldStates: FieldStates;
-  fieldUpdates: FieldUpdates
+  fieldUpdates: FieldUpdates;
+  virtualMetadataSources: VirtualMetadataSources;
   lastModified: Date;
 }
 
@@ -96,6 +114,9 @@ export function objectUpdatesReducer(state = initialState, action: ObjectUpdates
     case ObjectUpdatesActionTypes.ADD_FIELD: {
       return addFieldUpdate(state, action as AddFieldUpdateAction);
     }
+    case ObjectUpdatesActionTypes.SELECT_VIRTUAL_METADATA: {
+      return selectVirtualMetadata(state, action as SelectVirtualMetadataAction);
+    }
     case ObjectUpdatesActionTypes.DISCARD: {
       return discardObjectUpdates(state, action as DiscardObjectUpdatesAction);
     }
@@ -135,6 +156,7 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
     state[url],
     { fieldStates: fieldStates },
     { fieldUpdates: {} },
+    { virtualMetadataSources: {} },
     { lastModified: lastModifiedServer }
   );
   return Object.assign({}, state, { [url]: newPageState });
@@ -169,6 +191,51 @@ function addFieldUpdate(state: any, action: AddFieldUpdateAction) {
   return Object.assign({}, state, { [url]: newPageState });
 }
 
+/**
+ * Add a new update for a specific field to the store
+ * @param state The current state
+ * @param action The action to perform on the current state
+ */
+function selectVirtualMetadata(state: any, action: SelectVirtualMetadataAction) {
+
+  const url: string = action.payload.url;
+  const source: string = action.payload.source;
+  const uuid: string = action.payload.uuid;
+  const select: boolean = action.payload.select;
+
+  const pageState: ObjectUpdatesEntry = state[url] || {};
+
+  const virtualMetadataSource = Object.assign(
+    {},
+    pageState.virtualMetadataSources[source],
+    {
+      [uuid]: select,
+    },
+  );
+
+  const virtualMetadataSources = Object.assign(
+    {},
+    pageState.virtualMetadataSources,
+    {
+      [source]: virtualMetadataSource,
+    },
+  );
+
+  const newPageState = Object.assign(
+    {},
+    pageState,
+    {virtualMetadataSources: virtualMetadataSources},
+  );
+
+  return Object.assign(
+    {},
+    state,
+    {
+      [url]: newPageState,
+    }
+  );
+}
+
 /**
  * Discard all updates for a specific action's url in the store
  * @param state The current state
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 08745f9223..0e8b1c8d07 100644
--- a/src/app/core/data/object-updates/object-updates.service.ts
+++ b/src/app/core/data/object-updates/object-updates.service.ts
@@ -1,16 +1,17 @@
-import { Injectable } from '@angular/core';
-import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
-import { CoreState } from '../../core.reducers';
-import { coreSelector } from '../../core.selectors';
+import {Injectable} from '@angular/core';
+import {createSelector, MemoizedSelector, select, Store} from '@ngrx/store';
+import {CoreState} from '../../core.reducers';
+import {coreSelector} from '../../core.selectors';
 import {
   FieldState,
   FieldUpdates,
   Identifiable,
   OBJECT_UPDATES_TRASH_PATH,
   ObjectUpdatesEntry,
-  ObjectUpdatesState
+  ObjectUpdatesState,
+  VirtualMetadataSource
 } from './object-updates.reducer';
-import { Observable } from 'rxjs';
+import {Observable} from 'rxjs';
 import {
   AddFieldUpdateAction,
   DiscardObjectUpdatesAction,
@@ -18,12 +19,17 @@ import {
   InitializeFieldsAction,
   ReinstateObjectUpdatesAction,
   RemoveFieldUpdateAction,
+  SelectVirtualMetadataAction,
   SetEditableFieldUpdateAction,
   SetValidFieldUpdateAction
 } from './object-updates.actions';
-import { distinctUntilChanged, filter, map } from 'rxjs/operators';
-import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
-import { INotification } from '../../../shared/notifications/models/notification.model';
+import {distinctUntilChanged, filter, map} from 'rxjs/operators';
+import {hasNoValue, hasValue, isEmpty, isNotEmpty} from '../../../shared/empty.util';
+import {INotification} from '../../../shared/notifications/models/notification.model';
+import {Item} from "../../shared/item.model";
+import {Relationship} from "../../shared/item-relationships/relationship.model";
+import {MetadataValue} from "../../shared/metadata.models";
+import {VirtualMetadata} from "../../../+item-page/edit-item-page/virtual-metadata/virtual-metadata.component";
 
 function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> {
   return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']);
@@ -37,6 +43,10 @@ function filterByUrlAndUUIDFieldStateSelector(url: string, uuid: string): Memoiz
   return createSelector(filterByUrlObjectUpdatesStateSelector(url), (state: ObjectUpdatesEntry) => state.fieldStates[uuid]);
 }
 
+function virtualMetadataSourceSelector(url: string, source: string): MemoizedSelector<CoreState, VirtualMetadataSource> {
+  return createSelector(filterByUrlObjectUpdatesStateSelector(url), (state: ObjectUpdatesEntry) => state.virtualMetadataSources[source]);
+}
+
 /**
  * Service that dispatches and reads from the ObjectUpdates' state in the store
  */
@@ -195,6 +205,41 @@ export class ObjectUpdatesService {
     this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
   }
 
+  getVirtualMetadataList(relationship: Relationship, item: Item): VirtualMetadata[] {
+    return Object.entries(item.metadata)
+      .map(([key, value]) =>
+        value
+          .filter((metadata: MetadataValue) =>
+            metadata.authority && metadata.authority.endsWith(relationship.id))
+          .map((metadata: MetadataValue) => {
+            return {
+              metadataField: key,
+              metadataValue: metadata,
+            }
+          })
+      )
+      .reduce((previous, current) => previous.concat(current));
+  }
+
+  isSelectedVirtualMetadataItem(url: string, relationship: string, item: string): Observable<boolean> {
+
+    return this.store
+      .pipe(
+        select(virtualMetadataSourceSelector(url, relationship)),
+        map(virtualMetadataSource => virtualMetadataSource && virtualMetadataSource[item]),
+    );
+  }
+
+  /**
+   * Method to dispatch an AddFieldUpdateAction to the store
+   * @param url The page's URL for which the changes are saved
+   * @param field An updated field for the page's object
+   * @param changeType The last type of change applied to this field
+   */
+  setSelectedVirtualMetadataItem(url: string, relationship: string, item: string, selected: boolean) {
+    this.store.dispatch(new SelectVirtualMetadataAction(url, relationship, item, selected));
+  }
+
   /**
    * Dispatches a SetEditableFieldUpdateAction to the store to set a field's editable state
    * @param url The URL of the page on which the field resides
diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts
index 4091759386..7d51f167d3 100644
--- a/src/app/core/data/relationship.service.spec.ts
+++ b/src/app/core/data/relationship.service.spec.ts
@@ -109,7 +109,7 @@ describe('RelationshipService', () => {
     beforeEach(() => {
       spyOn(service, 'findById').and.returnValue(getRemotedataObservable(relationship1));
       spyOn(objectCache, 'remove');
-      service.deleteRelationship(relationships[0].uuid).subscribe();
+      service.deleteRelationship(relationships[0].uuid, 'none').subscribe();
     });
 
     it('should send a DeleteRequest', () => {
diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts
index c466bd15af..a4a24e8aaa 100644
--- a/src/app/core/data/relationship.service.ts
+++ b/src/app/core/data/relationship.service.ts
@@ -31,7 +31,7 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec
 import { Store } from '@ngrx/store';
 import { CoreState } from '../core.reducers';
 import { NotificationsService } from '../../shared/notifications/notifications.service';
-import { HttpClient } from '@angular/common/http';
+import {HttpClient} from '@angular/common/http';
 import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
 import { SearchParam } from '../cache/models/search-param.model';
 
@@ -83,11 +83,13 @@ export class RelationshipService extends DataService<Relationship> {
    * Send a delete request for a relationship by ID
    * @param uuid
    */
-  deleteRelationship(uuid: string): Observable<RestResponse> {
+  deleteRelationship(uuid: string, copyVirtualMetadata: string): Observable<RestResponse> {
     return this.getRelationshipEndpoint(uuid).pipe(
       isNotEmptyOperator(),
       distinctUntilChanged(),
-      map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)),
+      map((endpointURL: string) =>
+        new DeleteRequest(this.requestService.generateRequestId(), endpointURL + "?copyVirtualMetadata=" + copyVirtualMetadata)
+      ),
       configureRequest(this.requestService),
       switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
       getResponseFromEntry(),
@@ -269,5 +271,4 @@ export class RelationshipService extends DataService<Relationship> {
       this.requestService.removeByHrefSubstring(rightItem.payload.self);
     });
   }
-
 }
-- 
GitLab