taskid 66074 Keep virtual metadata on relationship delete

This commit is contained in:
Samuel
2019-11-26 16:23:47 +01:00
parent 3c0adf9b12
commit ae476baa62
17 changed files with 595 additions and 71 deletions

View File

@@ -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",
}

View File

@@ -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 {

View File

@@ -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>

View File

@@ -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;
}
}

View File

@@ -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>

View File

@@ -112,7 +112,7 @@ describe('EditRelationshipComponent', () => {
comp.url = url;
comp.fieldUpdate = fieldUpdate1;
comp.item = item;
comp.editItem = item;
fixture.detectChanges();
});

View File

@@ -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;
}
}

View File

@@ -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();

View File

@@ -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>

View File

@@ -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();
// });
// });
// });

View File

@@ -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,
}

View File

@@ -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;

View File

@@ -79,7 +79,8 @@ describe('objectUpdatesReducer', () => {
changeType: FieldChangeType.ADD
}
},
lastModified: modDate
lastModified: modDate,
virtualMetadataSources: {},
}
};
@@ -213,6 +214,7 @@ describe('objectUpdatesReducer', () => {
},
},
fieldUpdates: {},
virtualMetadataSources: {},
lastModified: modDate
}
};

View File

@@ -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

View File

@@ -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

View File

@@ -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', () => {

View File

@@ -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);
});
}
}