mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
taskid 66074 Keep virtual metadata on relationship delete
This commit is contained in:
@@ -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",
|
||||
}
|
||||
|
||||
|
@@ -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 {
|
||||
|
@@ -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>
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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>
|
||||
|
@@ -112,7 +112,7 @@ describe('EditRelationshipComponent', () => {
|
||||
|
||||
comp.url = url;
|
||||
comp.fieldUpdate = fieldUpdate1;
|
||||
comp.item = item;
|
||||
comp.editItem = item;
|
||||
|
||||
fixture.detectChanges();
|
||||
});
|
||||
|
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -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();
|
||||
|
@@ -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>
|
@@ -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();
|
||||
// });
|
||||
// });
|
||||
// });
|
@@ -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,
|
||||
}
|
@@ -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;
|
||||
|
@@ -79,7 +79,8 @@ describe('objectUpdatesReducer', () => {
|
||||
changeType: FieldChangeType.ADD
|
||||
}
|
||||
},
|
||||
lastModified: modDate
|
||||
lastModified: modDate,
|
||||
virtualMetadataSources: {},
|
||||
}
|
||||
};
|
||||
|
||||
@@ -213,6 +214,7 @@ describe('objectUpdatesReducer', () => {
|
||||
},
|
||||
},
|
||||
fieldUpdates: {},
|
||||
virtualMetadataSources: {},
|
||||
lastModified: modDate
|
||||
}
|
||||
};
|
||||
|
@@ -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
|
||||
|
@@ -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
|
||||
|
@@ -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', () => {
|
||||
|
@@ -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);
|
||||
});
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user