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