diff --git a/e2e/app.e2e-spec.ts b/e2e/app.e2e-spec.ts index 6856a6f01b..995340941f 100644 --- a/e2e/app.e2e-spec.ts +++ b/e2e/app.e2e-spec.ts @@ -13,7 +13,7 @@ describe('protractor App', () => { }); it('should contain a news section', () => { - page.navigateTo(); - expect(page.getHomePageNewsText()).toBeDefined(); + page.navigateTo() + .then(() => expect(page.getHomePageNewsText()).toBeDefined()); }); }); diff --git a/e2e/app.po.ts b/e2e/app.po.ts index c76bef118f..2ee9a86201 100644 --- a/e2e/app.po.ts +++ b/e2e/app.po.ts @@ -11,6 +11,6 @@ export class ProtractorPage { } getHomePageNewsText() { - return element(by.xpath('//ds-home-news')).getText(); + return element(by.css('ds-home-news')).getText(); } } diff --git a/e2e/search-page/search-page.e2e-spec.ts b/e2e/search-page/search-page.e2e-spec.ts index cb9c92a87b..e2ab6de824 100644 --- a/e2e/search-page/search-page.e2e-spec.ts +++ b/e2e/search-page/search-page.e2e-spec.ts @@ -11,33 +11,36 @@ describe('protractor SearchPage', () => { it('should contain query value when navigating to page with query parameter', () => { const queryString = 'Interesting query string'; - page.navigateToSearchWithQueryParameter(queryString); - page.getCurrentQuery().then((query: string) => { - expect(query).toEqual(queryString); - }); + page.navigateToSearchWithQueryParameter(queryString) + .then(() => page.getCurrentQuery()) + .then((query: string) => { + expect(query).toEqual(queryString); + }); }); it('should have right scope selected when navigating to page with scope parameter', () => { - const scope: promise.Promise = page.getRandomScopeOption(); - scope.then((scopeString: string) => { - page.navigateToSearchWithScopeParameter(scopeString); - page.getCurrentScope().then((s: string) => { - expect(s).toEqual(scopeString); + page.navigateToSearch() + .then(() => page.getRandomScopeOption()) + .then((scopeString: string) => { + page.navigateToSearchWithScopeParameter(scopeString); + page.getCurrentScope().then((s: string) => { + expect(s).toEqual(scopeString); + }); }); - }); }); it('should redirect to the correct url when scope was set and submit button was triggered', () => { - const scope: promise.Promise = page.getRandomScopeOption(); - scope.then((scopeString: string) => { - page.setCurrentScope(scopeString); - page.submitSearchForm(); - browser.wait(() => { - return browser.getCurrentUrl().then((url: string) => { - return url.indexOf('scope=' + encodeURI(scopeString)) !== -1; + page.navigateToSearch() + .then(() => page.getRandomScopeOption()) + .then((scopeString: string) => { + page.setCurrentScope(scopeString); + page.submitSearchForm(); + browser.wait(() => { + return browser.getCurrentUrl().then((url: string) => { + return url.indexOf('scope=' + encodeURI(scopeString)) !== -1; + }); }); }); - }); }); it('should redirect to the correct url when query was set and submit button was triggered', () => { diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 39d30859bc..34d7daafac 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -1976,5 +1976,6 @@ "uploader.queue-length": "Queue length", + "virtual-metadata.delete-relationship.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..71924cf6c8 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..1a7cc2e2df 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 @@ -1,15 +1,15 @@ - -
-
{{getRelationshipMessageKey(relationshipLabel) | translate}}
- -
-
- +
{{getRelationshipMessageKey() | async | translate}}
+ + + + + + -
+
no relationships
diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index 54cb2837a2..cede48e6ee 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -1,27 +1,26 @@ -import { EditRelationshipListComponent } from './edit-relationship-list.component'; -import { async, ComponentFixture, TestBed } from '@angular/core/testing'; -import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; -import { ResourceType } from '../../../../core/shared/resource-type'; -import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; -import { of as observableOf } from 'rxjs/internal/observable/of'; -import { RemoteData } from '../../../../core/data/remote-data'; -import { Item } from '../../../../core/shared/item.model'; -import { PaginatedList } from '../../../../core/data/paginated-list'; -import { PageInfo } from '../../../../core/shared/page-info.model'; -import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; -import { SharedModule } from '../../../../shared/shared.module'; -import { TranslateModule } from '@ngx-translate/core'; -import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; -import { RelationshipService } from '../../../../core/data/relationship.service'; -import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; -import { By } from '@angular/platform-browser'; +import {EditRelationshipListComponent} from './edit-relationship-list.component'; +import {async, ComponentFixture, TestBed} from '@angular/core/testing'; +import {RelationshipType} from '../../../../core/shared/item-relationships/relationship-type.model'; +import {Relationship} from '../../../../core/shared/item-relationships/relationship.model'; +import {of as observableOf} from 'rxjs/internal/observable/of'; +import {RemoteData} from '../../../../core/data/remote-data'; +import {Item} from '../../../../core/shared/item.model'; +import {PaginatedList} from '../../../../core/data/paginated-list'; +import {PageInfo} from '../../../../core/shared/page-info.model'; +import {FieldChangeType} from '../../../../core/data/object-updates/object-updates.actions'; +import {SharedModule} from '../../../../shared/shared.module'; +import {TranslateModule} from '@ngx-translate/core'; +import {ObjectUpdatesService} from '../../../../core/data/object-updates/object-updates.service'; +import {DebugElement, NO_ERRORS_SCHEMA} from '@angular/core'; +import {By} from '@angular/platform-browser'; +import {ItemType} from '../../../../core/shared/item-relationships/item-type.model'; let comp: EditRelationshipListComponent; let fixture: ComponentFixture; let de: DebugElement; let objectUpdatesService; -let relationshipService; +let entityTypeService; const url = 'http://test-url.com/test-url'; @@ -30,42 +29,66 @@ let author1; let author2; let fieldUpdate1; let fieldUpdate2; -let relationships; +let relationship1; +let relationship2; let relationshipType; +let entityType; +let relatedEntityType; describe('EditRelationshipListComponent', () => { - beforeEach(async(() => { + + beforeEach(() => { + + entityType = Object.assign(new ItemType(), { + id: 'entityType', + }); + + relatedEntityType = Object.assign(new ItemType(), { + id: 'relatedEntityType', + }); + relationshipType = Object.assign(new RelationshipType(), { id: '1', uuid: '1', leftwardType: 'isAuthorOfPublication', - rightwardType: 'isPublicationOfAuthor' + rightwardType: 'isPublicationOfAuthor', + leftType: observableOf(new RemoteData(false, false, true, undefined, entityType)), + rightType: observableOf(new RemoteData(false, false, true, undefined, relatedEntityType)), }); - relationships = [ - Object.assign(new Relationship(), { - self: url + '/2', - id: '2', - uuid: '2', - leftId: 'author1', - rightId: 'publication', - relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) - }), - Object.assign(new Relationship(), { - self: url + '/3', - id: '3', - uuid: '3', - leftId: 'author2', - rightId: 'publication', - relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) - }) - ]; + relationship1 = Object.assign(new Relationship(), { + self: url + '/2', + id: '2', + uuid: '2', + leftId: 'author1', + rightId: 'publication', + leftItem: observableOf(new RemoteData(false, false, true, undefined, item)), + rightItem: observableOf(new RemoteData(false, false, true, undefined, author1)), + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }); + + relationship2 = Object.assign(new Relationship(), { + self: url + '/3', + id: '3', + uuid: '3', + leftId: 'author2', + rightId: 'publication', + leftItem: observableOf(new RemoteData(false, false, true, undefined, item)), + rightItem: observableOf(new RemoteData(false, false, true, undefined, author2)), + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }); item = Object.assign(new Item(), { self: 'fake-item-url/publication', id: 'publication', uuid: 'publication', - relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))) + relationships: observableOf(new RemoteData( + false, + false, + true, + undefined, + new PaginatedList(new PageInfo(), [relationship1, relationship2]) + )) }); author1 = Object.assign(new Item(), { @@ -88,16 +111,29 @@ describe('EditRelationshipListComponent', () => { objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', { - getFieldUpdatesExclusive: observableOf({ + getFieldUpdates: observableOf({ [author1.uuid]: fieldUpdate1, [author2.uuid]: fieldUpdate2 }) } ); - relationshipService = jasmine.createSpyObj('relationshipService', + entityTypeService = jasmine.createSpyObj('entityTypeService', { - getRelatedItemsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [author1, author2]))), + getEntityTypeByLabel: observableOf(new RemoteData( + false, + false, + true, + null, + entityType, + )), + getEntityTypeRelationships: observableOf(new RemoteData( + false, + false, + true, + null, + new PaginatedList(new PageInfo(), [relationshipType]), + )), } ); @@ -106,29 +142,27 @@ describe('EditRelationshipListComponent', () => { declarations: [EditRelationshipListComponent], providers: [ { provide: ObjectUpdatesService, useValue: objectUpdatesService }, - { provide: RelationshipService, useValue: relationshipService } ], schemas: [ NO_ERRORS_SCHEMA ] }).compileComponents(); - })); - beforeEach(() => { fixture = TestBed.createComponent(EditRelationshipListComponent); comp = fixture.componentInstance; de = fixture.debugElement; + comp.item = item; + comp.itemType = entityType; comp.url = url; - comp.relationshipLabel = relationshipType.leftwardType; + comp.relationshipType = relationshipType; + fixture.detectChanges(); }); describe('changeType is REMOVE', () => { - beforeEach(() => { - fieldUpdate1.changeType = FieldChangeType.REMOVE; - fixture.detectChanges(); - }); it('the div should have class alert-danger', () => { + + fieldUpdate1.changeType = FieldChangeType.REMOVE; const element = de.queryAll(By.css('.relationship-row'))[1].nativeElement; expect(element.classList).toContain('alert-danger'); }); 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..73e3e1f875 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 @@ -1,13 +1,15 @@ -import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core'; +import { Component, Input, OnInit } from '@angular/core'; import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service'; 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 { hasValue } from '../../../../shared/empty.util'; -import { RemoteData } from '../../../../core/data/remote-data'; -import { PaginatedList } from '../../../../core/data/paginated-list'; +import {FieldUpdate, FieldUpdates} from '../../../../core/data/object-updates/object-updates.reducer'; +import {Item} from '../../../../core/shared/item.model'; +import {map, switchMap} from 'rxjs/operators'; +import {hasValue} from '../../../../shared/empty.util'; +import {Relationship} from '../../../../core/shared/item-relationships/relationship.model'; +import {RelationshipType} from '../../../../core/shared/item-relationships/relationship-type.model'; +import {getRemoteDataPayload, getSucceededRemoteData} from '../../../../core/shared/operators'; +import {combineLatest as observableCombineLatest, combineLatest} from 'rxjs'; +import {ItemType} from '../../../../core/shared/item-relationships/item-type.model'; @Component({ selector: 'ds-edit-relationship-list', @@ -18,12 +20,15 @@ import { PaginatedList } from '../../../../core/data/paginated-list'; * A component creating a list of editable relationships of a certain type * The relationships are rendered as a list of related items */ -export class EditRelationshipListComponent implements OnInit, OnChanges { +export class EditRelationshipListComponent implements OnInit { + /** * The item to display related items for */ @Input() item: Item; + @Input() itemType: ItemType; + /** * The URL to the current page * Used to fetch updates for the current item from the store @@ -33,7 +38,7 @@ export class EditRelationshipListComponent implements OnInit, OnChanges { /** * The label of the relationship-type we're rendering a list for */ - @Input() relationshipLabel: string; + @Input() relationshipType: RelationshipType; /** * The FieldUpdates for the relationships in question @@ -42,53 +47,42 @@ export class EditRelationshipListComponent implements OnInit, OnChanges { constructor( protected objectUpdatesService: ObjectUpdatesService, - protected relationshipService: RelationshipService ) { } - ngOnInit(): void { - this.initUpdates(); - } + /** + * Get the i18n message key for this relationship type + */ + public getRelationshipMessageKey(): Observable { - ngOnChanges(changes: SimpleChanges): void { - this.initUpdates(); + return this.getLabel().pipe( + map((label) => { + if (hasValue(label) && label.indexOf('Of') > -1) { + return `relationships.${label.substring(0, label.indexOf('Of') + 2)}` + } else { + return label; + } + }), + ); } /** - * Initialize the FieldUpdates using the related items + * Get the relevant label for this relationship type */ - initUpdates() { - this.updates$ = this.getUpdatesByLabel(this.relationshipLabel); - } + private getLabel(): Observable { - /** - * Transform the item's relationships of a specific type into related items - * @param label The relationship type's label - */ - public getRelatedItemsByLabel(label: string): Observable>> { - 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 { - return this.getRelatedItemsByLabel(label).pipe( - switchMap((itemsRD) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, itemsRD.payload.page)) - ) - } - - /** - * Get the i18n message key for a relationship - * @param label The relationship type's label - */ - public getRelationshipMessageKey(label: string): string { - if (hasValue(label) && label.indexOf('Of') > -1) { - return `relationships.${label.substring(0, label.indexOf('Of') + 2)}` - } else { - return label; - } + return combineLatest([ + this.relationshipType.leftType, + this.relationshipType.rightType, + ].map((itemTypeRD) => itemTypeRD.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + ))).pipe( + map((itemTypes) => [ + this.relationshipType.leftwardType, + this.relationshipType.rightwardType, + ][itemTypes.findIndex((itemType) => itemType.id === this.itemType.id)]), + ); } /** @@ -98,4 +92,26 @@ export class EditRelationshipListComponent implements OnInit, OnChanges { return update && update.field ? update.field.uuid : undefined; } + ngOnInit(): void { + this.updates$ = this.item.relationships.pipe( + map((relationships) => relationships.payload.page.filter((relationship) => relationship)), + switchMap((itemRelationships) => + observableCombineLatest( + itemRelationships + .map((relationship) => relationship.relationshipType.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + )) + ).pipe( + map((relationshipTypes) => itemRelationships.filter( + (relationship, index) => relationshipTypes[index].id === this.relationshipType.id) + ), + map((relationships) => relationships.map((relationship) => + Object.assign(new Relationship(), relationship, {uuid: relationship.id}) + )), + ) + ), + switchMap((initialFields) => this.objectUpdatesService.getFieldUpdates(this.url, initialFields)), + ); + } } 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..7e61e8958f 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 @@ -
+
- +
-
+ + + + 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..b3c3e773b2 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 @@ -11,11 +11,13 @@ import { Item } from '../../../../core/shared/item.model'; import { PaginatedList } from '../../../../core/data/paginated-list'; import { PageInfo } from '../../../../core/shared/page-info.model'; import { FieldChangeType } from '../../../../core/data/object-updates/object-updates.actions'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; -let objectUpdatesService: ObjectUpdatesService; +let objectUpdatesService; const url = 'http://test-url.com/test-url'; let item; +let relatedItem; let author1; let author2; let fieldUpdate1; @@ -29,7 +31,9 @@ let de; let el; describe('EditRelationshipComponent', () => { + beforeEach(async(() => { + relationshipType = Object.assign(new RelationshipType(), { id: '1', uuid: '1', @@ -37,6 +41,17 @@ describe('EditRelationshipComponent', () => { rightwardType: 'isPublicationOfAuthor' }); + item = Object.assign(new Item(), { + self: 'fake-item-url/publication', + id: 'publication', + uuid: 'publication', + relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))) + }); + + relatedItem = Object.assign(new Item(), { + uuid: 'related item id', + }); + relationships = [ Object.assign(new Relationship(), { self: url + '/2', @@ -44,7 +59,9 @@ describe('EditRelationshipComponent', () => { uuid: '2', leftId: 'author1', rightId: 'publication', - relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)), + leftItem: observableOf(new RemoteData(false, false, true, undefined, relatedItem)), + rightItem: observableOf(new RemoteData(false, false, true, undefined, item)), }), Object.assign(new Relationship(), { self: url + '/3', @@ -56,13 +73,6 @@ describe('EditRelationshipComponent', () => { }) ]; - item = Object.assign(new Item(), { - self: 'fake-item-url/publication', - id: 'publication', - uuid: 'publication', - relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships))) - }); - author1 = Object.assign(new Item(), { id: 'author1', uuid: 'author1' @@ -73,38 +83,44 @@ describe('EditRelationshipComponent', () => { }); fieldUpdate1 = { - field: author1, + field: relationships[0], changeType: undefined }; fieldUpdate2 = { - field: author2, + field: relationships[1], changeType: FieldChangeType.REMOVE }; - objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', - { - saveChangeFieldUpdate: {}, - saveRemoveFieldUpdate: {}, - setEditableFieldUpdate: {}, - setValidFieldUpdate: {}, - removeSingleFieldUpdate: {}, - isEditable: observableOf(false), // should always return something --> its in ngOnInit - isValid: observableOf(true) // should always return something --> its in ngOnInit - } - ); + const itemSelection = {}; + itemSelection[relatedItem.uuid] = false; + itemSelection[item.uuid] = true; + + objectUpdatesService = { + isSelectedVirtualMetadata: () => null, + removeSingleFieldUpdate: jasmine.createSpy('removeSingleFieldUpdate'), + saveRemoveFieldUpdate: jasmine.createSpy('saveRemoveFieldUpdate'), + }; + + spyOn(objectUpdatesService, 'isSelectedVirtualMetadata').and.callFake((a, b, uuid) => observableOf(itemSelection[uuid])); TestBed.configureTestingModule({ imports: [TranslateModule.forRoot()], declarations: [EditRelationshipComponent], providers: [ - { provide: ObjectUpdatesService, useValue: objectUpdatesService } - ], schemas: [ + { provide: ObjectUpdatesService, useValue: objectUpdatesService }, + { provide: NgbModal, useValue: { + open: () => {/*comment*/ + } + }, + }, + ], schemas: [ NO_ERRORS_SCHEMA ] }).compileComponents(); })); beforeEach(() => { + fixture = TestBed.createComponent(EditRelationshipComponent); comp = fixture.componentInstance; de = fixture.debugElement; @@ -112,7 +128,8 @@ describe('EditRelationshipComponent', () => { comp.url = url; comp.fieldUpdate = fieldUpdate1; - comp.item = item; + comp.editItem = item; + comp.relatedItem$ = observableOf(relatedItem); fixture.detectChanges(); }); @@ -156,23 +173,30 @@ describe('EditRelationshipComponent', () => { }); describe('remove', () => { + beforeEach(() => { + spyOn(comp, 'closeVirtualMetadataModal'); + comp.ngOnChanges(); comp.remove(); }); - it('should call saveRemoveFieldUpdate with the correct arguments', () => { - expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith(url, item); + it('should close the virtual metadata modal and call saveRemoveFieldUpdate with the correct arguments', () => { + expect(comp.closeVirtualMetadataModal).toHaveBeenCalled(); + expect(objectUpdatesService.saveRemoveFieldUpdate).toHaveBeenCalledWith( + url, + Object.assign({}, fieldUpdate1.field, { + keepLeftVirtualMetadata: false, + keepRightVirtualMetadata: true, + }), + ); }); }); describe('undo', () => { - beforeEach(() => { - comp.undo(); - }); it('should call removeSingleFieldUpdate with the correct arguments', () => { - expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, item.uuid); + comp.undo(); + expect(objectUpdatesService.removeSingleFieldUpdate).toHaveBeenCalledWith(url, relationships[0].uuid); }); }); - }); 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 ee9d2cda27..d1ee99c3a7 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,14 +1,19 @@ -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 { 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 - selector: '[ds-edit-relationship]', + selector: 'ds-edit-relationship', styleUrls: ['./edit-relationship.component.scss'], templateUrl: './edit-relationship.component.html', }) @@ -23,38 +28,108 @@ 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; + private rightItem$: Observable; + /** * The related item of this relationship */ - item: Item; + relatedItem$: Observable; /** * The view-mode we're currently on */ viewMode = ViewMode.ListElement; - constructor(private objectUpdatesService: ObjectUpdatesService) { + /** + * Reference to NgbModal + */ + public modalRef: NgbModalRef; + + 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 + .isSelectedVirtualMetadata(this.url, this.relationship.id, item.uuid)) + ), + switchMap((selection$) => observableCombineLatest(selection$)), + map((selection: boolean[]) => { + return Object.assign({}, + this.fieldUpdate.field, + { + keepLeftVirtualMetadata: selection[0] === true, + keepRightVirtualMetadata: selection[1] === true, + } + ) as DeleteRelationship + }), + take(1), + ).subscribe((deleteRelationship: DeleteRelationship) => + this.objectUpdatesService.saveRemoveFieldUpdate(this.url, deleteRelationship) + ); + } + + 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); } /** diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html index 4bd0b3df2c..384a469f24 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.html @@ -17,8 +17,13 @@  {{"item.edit.metadata.save-button" | translate}}
-
- +
+
diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts index 37745ec96a..aa812354b6 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts @@ -13,9 +13,8 @@ import { ActivatedRoute, Router } from '@angular/router'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { GLOBAL_CONFIG } from '../../../../config'; import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; -import { ResourceType } from '../../../core/shared/resource-type'; import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; -import { of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; +import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs'; import { RemoteData } from '../../../core/data/remote-data'; import { Item } from '../../../core/shared/item.model'; import { PaginatedList } from '../../../core/data/paginated-list'; @@ -26,6 +25,8 @@ import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { getTestScheduler } from 'jasmine-marbles'; import { RestResponse } from '../../../core/cache/response.models'; import { RequestService } from '../../../core/data/request.service'; +import { EntityTypeService } from '../../../core/data/entity-type.service'; +import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; let comp: any; let fixture: ComponentFixture; @@ -34,6 +35,7 @@ let el: HTMLElement; let objectUpdatesService; let relationshipService; let requestService; +let entityTypeService; let objectCache; const infoNotification: INotification = new Notification('id', NotificationType.Info, 'info'); const warningNotification: INotification = new Notification('id', NotificationType.Warning, 'warning'); @@ -58,6 +60,7 @@ let author1; let author2; let fieldUpdate1; let fieldUpdate2; +let entityType; let relationships; let relationshipType; @@ -95,6 +98,10 @@ describe('ItemRelationshipsComponent', () => { lastModified: date }); + entityType = Object.assign(new ItemType(), { + id: 'entityType', + }); + author1 = Object.assign(new Item(), { id: 'author1', uuid: 'author1' @@ -110,11 +117,14 @@ describe('ItemRelationshipsComponent', () => { relationships[1].rightItem = observableOf(new RemoteData(false, false, true, undefined, item)); fieldUpdate1 = { - field: author1, + field: relationships[0], changeType: undefined }; fieldUpdate2 = { - field: author2, + field: Object.assign( + relationships[1], + {keepLeftVirtualMetadata: true, keepRightVirtualMetadata: false} + ), changeType: FieldChangeType.REMOVE }; @@ -130,12 +140,12 @@ describe('ItemRelationshipsComponent', () => { objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', { getFieldUpdates: observableOf({ - [author1.uuid]: fieldUpdate1, - [author2.uuid]: fieldUpdate2 + [relationships[0].uuid]: fieldUpdate1, + [relationships[1].uuid]: fieldUpdate2 }), getFieldUpdatesExclusive: observableOf({ - [author1.uuid]: fieldUpdate1, - [author2.uuid]: fieldUpdate2 + [relationships[0].uuid]: fieldUpdate1, + [relationships[1].uuid]: fieldUpdate2 }), saveAddFieldUpdate: {}, discardFieldUpdates: {}, @@ -173,6 +183,25 @@ describe('ItemRelationshipsComponent', () => { remove: undefined }); + entityTypeService = jasmine.createSpyObj('entityTypeService', + { + getEntityTypeByLabel: observableOf(new RemoteData( + false, + false, + true, + null, + entityType, + )), + getEntityTypeRelationships: observableOf(new RemoteData( + false, + false, + true, + null, + new PaginatedList(new PageInfo(), [relationshipType]), + )), + } + ); + scheduler = getTestScheduler(); TestBed.configureTestingModule({ imports: [SharedModule, TranslateModule.forRoot()], @@ -185,6 +214,7 @@ describe('ItemRelationshipsComponent', () => { { provide: NotificationsService, useValue: notificationsService }, { provide: GLOBAL_CONFIG, useValue: { item: { edit: { undoTimeout: 10 } } } as any }, { provide: RelationshipService, useValue: relationshipService }, + { provide: EntityTypeService, useValue: entityTypeService }, { provide: ObjectCacheService, useValue: objectCache }, { provide: RequestService, useValue: requestService }, ChangeDetectorRef @@ -229,7 +259,7 @@ describe('ItemRelationshipsComponent', () => { }); it('it should delete the correct relationship', () => { - expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid); + expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid, 'left'); }); }); }); 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 42ebc5563e..3b0fe5f89b 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,8 @@ 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, flatMap, map, switchMap, take, tap } from 'rxjs/operators'; +import { filter, map, switchMap, take } from 'rxjs/operators'; import { zip as observableZip } from 'rxjs'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { ItemDataService } from '../../../core/data/item-data.service'; @@ -12,15 +12,18 @@ import { NotificationsService } from '../../../shared/notifications/notification import { TranslateService } from '@ngx-translate/core'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../../config'; import { RelationshipService } from '../../../core/data/relationship.service'; -import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; -import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; 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 { getRemoteDataPayload, getSucceededRemoteData } from '../../../core/shared/operators'; import { RequestService } from '../../../core/data/request.service'; import { Subscription } from 'rxjs/internal/Subscription'; +import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model'; +import { ItemType } from '../../../core/shared/item-relationships/item-type.model'; +import { EntityTypeService } from '../../../core/data/entity-type.service'; +import { isNotEmptyOperator } from '../../../shared/empty.util'; +import { FieldChangeType } from '../../../core/data/object-updates/object-updates.actions'; +import { Relationship } from '../../../core/shared/item-relationships/relationship.model'; @Component({ selector: 'ds-item-relationships', @@ -35,13 +38,14 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl /** * The labels of all different relations within this item */ - relationLabels$: Observable; + relationshipTypes$: Observable; /** * A subscription that checks when the item is deleted in cache and reloads the item by sending a new request * This is used to update the item in cache after relationships are deleted */ itemUpdateSubscription: Subscription; + entityType$: Observable; constructor( protected itemService: ItemDataService, @@ -54,7 +58,8 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl protected relationshipService: RelationshipService, protected objectCache: ObjectCacheService, protected requestService: RequestService, - protected cdRef: ChangeDetectorRef + protected entityTypeService: EntityTypeService, + protected cdr: ChangeDetectorRef, ) { super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route); } @@ -64,21 +69,14 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl */ ngOnInit(): void { super.ngOnInit(); - this.relationLabels$ = this.relationshipService.getRelationshipTypeLabelsByItem(this.item); - this.initializeItemUpdate(); - } - - /** - * Update the item (and view) when it's removed in the request cache - */ - public initializeItemUpdate(): void { this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe( filter((exists: boolean) => !exists), switchMap(() => this.itemService.findById(this.item.uuid)), getSucceededRemoteData(), ).subscribe((itemRD: RemoteData) => { this.item = itemRD.payload; - this.cdRef.detectChanges(); + this.cdr.detectChanges(); + this.initializeUpdates(); }); } @@ -86,8 +84,22 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl * Initialize the values and updates of the current item's relationship fields */ public initializeUpdates(): void { - this.updates$ = this.relationshipService.getRelatedItems(this.item).pipe( - switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdates(this.url, items)) + + this.entityType$ = this.entityTypeService.getEntityTypeByLabel( + this.item.firstMetadataValue('relationship.type') + ).pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + ); + + this.relationshipTypes$ = this.entityType$.pipe( + switchMap((entityType) => + this.entityTypeService.getEntityTypeRelationships(entityType.id).pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + map((relationshipTypes) => relationshipTypes.page), + ) + ), ); } @@ -103,26 +115,41 @@ 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), - 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( - flatMap((uuids) => this.relationshipService.getRelationshipsByRelatedItemIds(this.item, uuids)) - ); - // const removedRelationships$ = removedItemIds$.pipe(flatMap((uuids: string[]) => this.relationshipService.getRelationshipsByRelatedItemIds(this.item, uuids))); - // Request a delete for every relationship found in the observable created above - removedRelationships$.pipe( + 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 + }), + map((fieldUpdates: FieldUpdates) => + Object.values(fieldUpdates) + .filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.REMOVE) + .map((fieldUpdate: FieldUpdate) => fieldUpdate.field as DeleteRelationship) + ), + isNotEmptyOperator(), 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(); + this.itemUpdateSubscription.add(() => { + this.displayNotifications(responses); + }); }); } @@ -144,22 +171,12 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl } } - /** - * Re-initialize fields and subscriptions - */ - reset() { - this.initializeOriginalFields(); - this.initializeUpdates(); - this.initializeItemUpdate(); - } - /** * Sends all initial values of this item to the object updates service */ public initializeOriginalFields() { - this.relationshipService.getRelatedItems(this.item).pipe(take(1)).subscribe((items: Item[]) => { - this.objectUpdatesService.initialize(this.url, items, this.item.lastModified); - }); + const initialFields = []; + this.objectUpdatesService.initialize(this.url, initialFields, this.item.lastModified); } /** @@ -168,5 +185,4 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl ngOnDestroy(): void { this.itemUpdateSubscription.unsubscribe(); } - } 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..c103d83c71 --- /dev/null +++ b/src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.html @@ -0,0 +1,38 @@ +
+ + +
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..f2732d081a --- /dev/null +++ b/src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.spec.ts @@ -0,0 +1,102 @@ +import {ComponentFixture, TestBed} from '@angular/core/testing'; +import {of as observableOf} from 'rxjs/internal/observable/of'; +import {TranslateModule} from '@ngx-translate/core'; +import {DebugElement, NO_ERRORS_SCHEMA} from '@angular/core'; +import {By} from '@angular/platform-browser'; +import {VirtualMetadataComponent} from './virtual-metadata.component'; +import {Item} from '../../../core/shared/item.model'; +import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service'; +import {VarDirective} from '../../../shared/utils/var.directive'; + +describe('VirtualMetadataComponent', () => { + + let comp: VirtualMetadataComponent; + let fixture: ComponentFixture; + let de: DebugElement; + + let objectUpdatesService; + + const url = 'http://test-url.com/test-url'; + + let item; + let relatedItem; + let relationshipId; + + beforeEach(() => { + + relationshipId = 'relationship id'; + + item = Object.assign(new Item(), { + uuid: 'publication', + metadata: [], + }); + + relatedItem = Object.assign(new Item(), { + uuid: 'relatedItem', + metadata: [], + }); + + objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', { + isSelectedVirtualMetadata: observableOf(false), + setSelectedVirtualMetadata: null, + }); + + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot()], + declarations: [VirtualMetadataComponent, VarDirective], + providers: [ + {provide: ObjectUpdatesService, useValue: objectUpdatesService}, + ], schemas: [ + NO_ERRORS_SCHEMA + ] + }).compileComponents(); + + fixture = TestBed.createComponent(VirtualMetadataComponent); + comp = fixture.componentInstance; + de = fixture.debugElement; + + comp.url = url; + comp.leftItem = item; + comp.rightItem = relatedItem; + comp.relationshipId = relationshipId; + + fixture.detectChanges(); + }); + + describe('when clicking the save button', () => { + it('should emit a save event', () => { + + spyOn(comp.save, 'emit'); + fixture.debugElement + .query(By.css('button.save')) + .triggerEventHandler('click', null); + expect(comp.save.emit).toHaveBeenCalled(); + }); + }); + + describe('when clicking the close button', () => { + it('should emit a close event', () => { + + spyOn(comp.close, 'emit'); + fixture.debugElement + .query(By.css('button.close')) + .triggerEventHandler('click', null); + expect(comp.close.emit).toHaveBeenCalled(); + }); + }); + + describe('when selecting an item', () => { + it('should call the updates service setSelectedVirtualMetadata method', () => { + + fixture.debugElement + .query(By.css('div.item')) + .triggerEventHandler('click', null); + expect(objectUpdatesService.setSelectedVirtualMetadata).toHaveBeenCalledWith( + url, + relationshipId, + item.uuid, + true + ); + }); + }) +}); 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..cac46724f0 --- /dev/null +++ b/src/app/+item-page/edit-item-page/virtual-metadata/virtual-metadata.component.ts @@ -0,0 +1,120 @@ +import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core'; +import {Observable} from 'rxjs'; +import {Item} from '../../../core/shared/item.model'; +import {MetadataValue} from '../../../core/shared/metadata.models'; +import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service'; + +@Component({ + selector: 'ds-virtual-metadata', + templateUrl: './virtual-metadata.component.html' +}) +/** + * Component that lists both items of a relationship, along with their virtual metadata of the relationship. + * The component is shown when a relationship is marked to be deleted. + * Each item has a checkbox to indicate whether its virtual metadata should be saved as real metadata. + */ +export class VirtualMetadataComponent implements OnInit { + + /** + * The current url of this page + */ + @Input() url: string; + + /** + * The id of the relationship to be deleted. + */ + @Input() relationshipId: string; + + /** + * The left item of the relationship to be deleted. + */ + @Input() leftItem: Item; + + /** + * The right item of the relationship to be deleted. + */ + @Input() rightItem: Item; + + /** + * Emits when the close button is pressed. + */ + @Output() close = new EventEmitter(); + + /** + * Emits when the save button is pressed. + */ + @Output() save = new EventEmitter(); + + /** + * Get an array of the left and the right item of the relationship to be deleted. + */ + get items() { + return [this.leftItem, this.rightItem]; + } + + private virtualMetadata: Map = new Map(); + + constructor( + protected objectUpdatesService: ObjectUpdatesService, + ) { + } + + /** + * Get the virtual metadata of a given item corresponding to this relationship. + * @param item the item to get the virtual metadata for + */ + getVirtualMetadata(item: Item): VirtualMetadata[] { + + return Object.entries(item.metadata) + .map(([key, value]) => + value + .filter((metadata: MetadataValue) => + !key.startsWith('relation') && metadata.authority && metadata.authority.endsWith(this.relationshipId)) + .map((metadata: MetadataValue) => { + return { + metadataField: key, + metadataValue: metadata, + } + }) + ) + .reduce((previous, current) => previous.concat(current), []); + } + + /** + * Select/deselect the virtual metadata of an item to be saved as real metadata. + * @param item the item for which (not) to save the virtual metadata as real metadata + * @param selected whether or not to save the virtual metadata as real metadata + */ + setSelectedVirtualMetadataItem(item: Item, selected: boolean) { + this.objectUpdatesService.setSelectedVirtualMetadata(this.url, this.relationshipId, item.uuid, selected); + } + + /** + * Check whether the virtual metadata of a given item is selected to be saved as real metadata + * @param item the item for which to check whether the virtual metadata is selected to be saved as real metadata + */ + isSelectedVirtualMetadataItem(item: Item): Observable { + return this.objectUpdatesService.isSelectedVirtualMetadata(this.url, this.relationshipId, item.uuid); + } + + /** + * Prevent unnecessary rerendering so fields don't lose focus + */ + trackItem(index, item: Item) { + return item && item.uuid; + } + + ngOnInit(): void { + this.items.forEach((item) => { + this.virtualMetadata.set(item.uuid, this.getVirtualMetadata(item)); + }); + } +} + +/** + * Represents a virtual metadata entry. + */ +export interface VirtualMetadata { + metadataField: string, + metadataValue: MetadataValue, +} diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 811ecacd38..1621c4081d 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -122,6 +122,7 @@ import { BrowseDefinition } from './shared/browse-definition.model'; import { ContentSourceResponseParsingService } from './data/content-source-response-parsing.service'; import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service'; import { ObjectSelectService } from '../shared/object-select/object-select.service'; +import {EntityTypeService} from './data/entity-type.service'; import { SiteDataService } from './data/site-data.service'; import { NormalizedSite } from './cache/models/normalized-site.model'; @@ -245,6 +246,7 @@ const PROVIDERS = [ TaskResponseParsingService, ClaimedTaskDataService, PoolTaskDataService, + EntityTypeService, ContentSourceResponseParsingService, SearchService, SidebarService, diff --git a/src/app/core/data/entity-type.service.ts b/src/app/core/data/entity-type.service.ts new file mode 100644 index 0000000000..583601d898 --- /dev/null +++ b/src/app/core/data/entity-type.service.ts @@ -0,0 +1,103 @@ +import { DataService } from './data.service'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { Injectable } from '@angular/core'; +import { GetRequest } from './request.models'; +import { Observable } from 'rxjs/internal/Observable'; +import {switchMap, take, tap} from 'rxjs/operators'; +import { RemoteData } from './remote-data'; +import {RelationshipType} from '../shared/item-relationships/relationship-type.model'; +import {PaginatedList} from './paginated-list'; +import {ItemType} from '../shared/item-relationships/item-type.model'; + +/** + * Service handling all ItemType requests + */ +@Injectable() +export class EntityTypeService extends DataService { + + protected linkPath = 'entitytypes'; + protected forceBypassCache = false; + + constructor(protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, + protected halService: HALEndpointService, + protected objectCache: ObjectCacheService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } + + getBrowseEndpoint(options, linkPath?: string): Observable { + return this.halService.getEndpoint(this.linkPath); + } + + /** + * Get the endpoint for the item type's allowed relationship types + * @param entityTypeId + */ + getRelationshipTypesEndpoint(entityTypeId: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + switchMap((href) => this.halService.getEndpoint('relationshiptypes', `${href}/${entityTypeId}`)) + ); + } + + /** + * Get the allowed relationship types for an entity type + * @param entityTypeId + */ + getEntityTypeRelationships(entityTypeId: string): Observable>> { + + const href$ = this.getRelationshipTypesEndpoint(entityTypeId); + + href$.pipe(take(1)).subscribe((href) => { + const request = new GetRequest(this.requestService.generateRequestId(), href); + this.requestService.configure(request); + }); + + return this.rdbService.buildList(href$); + } + + /** + * Get an entity type by their label + * @param label + */ + getEntityTypeByLabel(label: string): Observable> { + + // TODO: Remove mock data once REST API supports this + /* + href$.pipe(take(1)).subscribe((href) => { + const request = new GetRequest(this.requestService.generateRequestId(), href); + this.requestService.configure(request); + }); + + return this.rdbService.buildSingle(href$); + */ + + // Mock: + const index = [ + 'Publication', + 'Person', + 'Project', + 'OrgUnit', + 'Journal', + 'JournalVolume', + 'JournalIssue', + 'DataPackage', + 'DataFile', + ].indexOf(label); + + return this.findById((index + 1) + ''); + } +} diff --git a/src/app/core/data/facet-value-response-parsing.service.ts b/src/app/core/data/facet-value-response-parsing.service.ts index 70585bc3d9..7fedc17545 100644 --- a/src/app/core/data/facet-value-response-parsing.service.ts +++ b/src/app/core/data/facet-value-response-parsing.service.ts @@ -4,7 +4,7 @@ import { ResponseParsingService } from './parsing.service'; import { RestRequest } from './request.models'; import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; -import { FacetValue } from '../../shared/search/facet-value.model'; +import {FacetValue} from '../../shared/search/facet-value.model'; import { BaseResponseParsingService } from './base-response-parsing.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { GLOBAL_CONFIG } from '../../../config'; 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..a3a95369fd 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,41 @@ export class AddFieldUpdateAction implements Action { } } +/** + * An ngrx action to select/deselect virtual metadata in the ObjectUpdates state for a certain page url + */ +export class SelectVirtualMetadataAction implements Action { + + type = ObjectUpdatesActionTypes.SELECT_VIRTUAL_METADATA; + payload: { + url: string, + source: string, + uuid: string, + select: boolean; + }; + + /** + * Create a new SelectVirtualMetadataAction + * + * @param url + * the unique url of the page for which a field update is added + * @param source + * the id of the relationship which adds the virtual metadata + * @param uuid + * the id of the item which has the virtual metadata + * @param select + * whether to select or deselect the virtual metadata to be saved as real metadata + */ + 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 +278,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..faae4732bc 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 @@ -5,10 +5,11 @@ import { FieldChangeType, InitializeFieldsAction, ReinstateObjectUpdatesAction, - RemoveFieldUpdateAction, RemoveObjectUpdatesAction, + RemoveFieldUpdateAction, RemoveObjectUpdatesAction, SelectVirtualMetadataAction, SetEditableFieldUpdateAction, SetValidFieldUpdateAction } from './object-updates.actions'; import { OBJECT_UPDATES_TRASH_PATH, objectUpdatesReducer } from './object-updates.reducer'; +import {Relationship} from '../../shared/item-relationships/relationship.model'; class NullAction extends RemoveFieldUpdateAction { type = null; @@ -44,6 +45,7 @@ const identifiable3 = { language: null, value: 'Unchanged value' }; +const relationship: Relationship = Object.assign(new Relationship(), {uuid: 'test relationship uuid'}); const modDate = new Date(2010, 2, 11); const uuid = identifiable1.uuid; @@ -79,7 +81,10 @@ describe('objectUpdatesReducer', () => { changeType: FieldChangeType.ADD } }, - lastModified: modDate + lastModified: modDate, + virtualMetadataSources: { + [relationship.uuid]: {[identifiable1.uuid]: true} + }, } }; @@ -102,7 +107,10 @@ describe('objectUpdatesReducer', () => { isValid: true }, }, - lastModified: modDate + lastModified: modDate, + virtualMetadataSources: { + [relationship.uuid]: {[identifiable1.uuid]: true} + }, }, [url + OBJECT_UPDATES_TRASH_PATH]: { fieldStates: { @@ -133,7 +141,10 @@ describe('objectUpdatesReducer', () => { changeType: FieldChangeType.ADD } }, - lastModified: modDate + lastModified: modDate, + virtualMetadataSources: { + [relationship.uuid]: {[identifiable1.uuid]: true} + }, } }; @@ -195,6 +206,12 @@ describe('objectUpdatesReducer', () => { objectUpdatesReducer(testState, action); }); + it('should perform the SELECT_VIRTUAL_METADATA action without affecting the previous state', () => { + const action = new SelectVirtualMetadataAction(url, relationship.uuid, identifiable1.uuid, true); + // testState has already been frozen above + objectUpdatesReducer(testState, action); + }); + it('should initialize all fields when the INITIALIZE action is dispatched, based on the payload', () => { const action = new InitializeFieldsAction(url, [identifiable1, identifiable3], modDate); @@ -213,6 +230,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..cffd41856d 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,36 @@ export interface FieldUpdates { [uuid: string]: FieldUpdate; } +/** + * The states of all virtual metadata selections available for a single page, mapped by the relationship uuid + */ +export interface VirtualMetadataSources { + [source: string]: VirtualMetadataSource +} + +/** + * The selection of virtual metadata for a relationship, mapped by the uuid of either the item or the relationship type + */ +export interface VirtualMetadataSource { + [uuid: string]: boolean, +} + +/** + * A fieldupdate interface which represents a relationship selected to be deleted, + * along with a selection of the virtual metadata to keep + */ +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 +124,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 +166,7 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) { state[url], { fieldStates: fieldStates }, { fieldUpdates: {} }, + { virtualMetadataSources: {} }, { lastModified: lastModifiedServer } ); return Object.assign({}, state, { [url]: newPageState }); @@ -169,6 +201,51 @@ function addFieldUpdate(state: any, action: AddFieldUpdateAction) { return Object.assign({}, state, { [url]: newPageState }); } +/** + * Update the selected virtual metadata in 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.spec.ts b/src/app/core/data/object-updates/object-updates.service.spec.ts index e9fc4652b0..730ee5ad43 100644 --- a/src/app/core/data/object-updates/object-updates.service.spec.ts +++ b/src/app/core/data/object-updates/object-updates.service.spec.ts @@ -4,13 +4,14 @@ import { ObjectUpdatesService } from './object-updates.service'; import { DiscardObjectUpdatesAction, FieldChangeType, - InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, + InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, SelectVirtualMetadataAction, SetEditableFieldUpdateAction } from './object-updates.actions'; import { of as observableOf } from 'rxjs'; import { Notification } from '../../../shared/notifications/models/notification.model'; import { NotificationType } from '../../../shared/notifications/models/notification-type'; import { OBJECT_UPDATES_TRASH_PATH } from './object-updates.reducer'; +import {Relationship} from '../../shared/item-relationships/relationship.model'; describe('ObjectUpdatesService', () => { let service: ObjectUpdatesService; @@ -22,6 +23,7 @@ describe('ObjectUpdatesService', () => { const identifiable2 = { uuid: '26cbb5ce-5786-4e57-a394-b9fcf8eaf241' }; const identifiable3 = { uuid: 'c5d2c2f7-d757-48bf-84cc-8c9229c8407e' }; const identifiables = [identifiable1, identifiable2]; + const relationship: Relationship = Object.assign(new Relationship(), {uuid: 'test relationship uuid'}); const fieldUpdates = { [identifiable1.uuid]: { field: identifiable1Updated, changeType: FieldChangeType.UPDATE }, @@ -38,11 +40,11 @@ describe('ObjectUpdatesService', () => { }; const objectEntry = { - fieldStates, fieldUpdates, lastModified: modDate + fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {} }; store = new Store(undefined, undefined, undefined); spyOn(store, 'dispatch'); - service = new ObjectUpdatesService(store); + service = (new ObjectUpdatesService(store)); spyOn(service as any, 'getObjectEntry').and.returnValue(observableOf(objectEntry)); spyOn(service as any, 'getFieldState').and.callFake((uuid) => { @@ -251,4 +253,10 @@ describe('ObjectUpdatesService', () => { }); }); + describe('setSelectedVirtualMetadata', () => { + it('should dispatch a SELECT_VIRTUAL_METADATA action with the correct URL, relationship, identifiable and boolean', () => { + service.setSelectedVirtualMetadata(url, relationship.uuid, identifiable1.uuid, true); + expect(store.dispatch).toHaveBeenCalledWith(new SelectVirtualMetadataAction(url, relationship.uuid, identifiable1.uuid, true)); + }); + }); }); 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 3bbb3960b6..367b73ee30 100644 --- a/src/app/core/data/object-updates/object-updates.service.ts +++ b/src/app/core/data/object-updates/object-updates.service.ts @@ -8,7 +8,8 @@ import { Identifiable, OBJECT_UPDATES_TRASH_PATH, ObjectUpdatesEntry, - ObjectUpdatesState + ObjectUpdatesState, + VirtualMetadataSource } from './object-updates.reducer'; import { Observable } from 'rxjs'; import { @@ -18,10 +19,11 @@ import { InitializeFieldsAction, ReinstateObjectUpdatesAction, RemoveFieldUpdateAction, + SelectVirtualMetadataAction, SetEditableFieldUpdateAction, SetValidFieldUpdateAction } from './object-updates.actions'; -import { distinctUntilChanged, filter, map } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, switchMap } from 'rxjs/operators'; import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { INotification } from '../../../shared/notifications/models/notification.model'; @@ -37,6 +39,10 @@ function filterByUrlAndUUIDFieldStateSelector(url: string, uuid: string): Memoiz return createSelector(filterByUrlObjectUpdatesStateSelector(url), (state: ObjectUpdatesEntry) => state.fieldStates[uuid]); } +function virtualMetadataSourceSelector(url: string, source: string): MemoizedSelector { + return createSelector(filterByUrlObjectUpdatesStateSelector(url), (state: ObjectUpdatesEntry) => state.virtualMetadataSources[source]); +} + /** * Service that dispatches and reads from the ObjectUpdates' state in the store */ @@ -91,20 +97,24 @@ export class ObjectUpdatesService { */ getFieldUpdates(url: string, initialFields: Identifiable[]): Observable { const objectUpdates = this.getObjectEntry(url); - return objectUpdates.pipe(map((objectEntry) => { - const fieldUpdates: FieldUpdates = {}; - if (hasValue(objectEntry)) { - Object.keys(objectEntry.fieldStates).forEach((uuid) => { - let fieldUpdate = objectEntry.fieldUpdates[uuid]; - if (isEmpty(fieldUpdate)) { - const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid); - fieldUpdate = {field: identifiable, changeType: undefined}; - } - fieldUpdates[uuid] = fieldUpdate; - }); - } - return fieldUpdates; - })) + return objectUpdates.pipe( + switchMap((objectEntry) => { + const fieldUpdates: FieldUpdates = {}; + if (hasValue(objectEntry)) { + Object.keys(objectEntry.fieldStates).forEach((uuid) => { + fieldUpdates[uuid] = objectEntry.fieldUpdates[uuid]; + }); + } + return this.getFieldUpdatesExclusive(url, initialFields).pipe( + map((fieldUpdatesExclusive) => { + Object.keys(fieldUpdatesExclusive).forEach((uuid) => { + fieldUpdates[uuid] = fieldUpdatesExclusive[uuid]; + }); + return fieldUpdates; + }) + ); + }), + ); } /** @@ -197,6 +207,34 @@ export class ObjectUpdatesService { this.saveFieldUpdate(url, field, FieldChangeType.UPDATE); } + /** + * Check whether the virtual metadata of a given item is selected to be saved as real metadata + * @param url The URL of the page on which the field resides + * @param relationship The id of the relationship for which to check whether the virtual metadata is selected to be + * saved as real metadata + * @param item The id of the item for which to check whether the virtual metadata is selected to be + * saved as real metadata + */ + isSelectedVirtualMetadata(url: string, relationship: string, item: string): Observable { + + return this.store + .pipe( + select(virtualMetadataSourceSelector(url, relationship)), + map((virtualMetadataSource) => virtualMetadataSource && virtualMetadataSource[item]), + ); + } + + /** + * Method to dispatch a SelectVirtualMetadataAction to the store + * @param url The page's URL for which the changes are saved + * @param relationship the relationship for which virtual metadata is selected + * @param uuid the selection identifier, can either be the item uuid or the relationship type uuid + * @param selected whether or not to select the virtual metadata to be saved + */ + setSelectedVirtualMetadata(url: string, relationship: string, uuid: string, selected: boolean) { + this.store.dispatch(new SelectVirtualMetadataAction(url, relationship, uuid, 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 9287935f59..99442da58d 100644 --- a/src/app/core/data/relationship.service.spec.ts +++ b/src/app/core/data/relationship.service.spec.ts @@ -54,10 +54,12 @@ describe('RelationshipService', () => { }); const relatedItem1 = Object.assign(new Item(), { + self: 'fake-item-url/author1', id: 'author1', uuid: 'author1' }); const relatedItem2 = Object.assign(new Item(), { + self: 'fake-item-url/author2', id: 'author2', uuid: 'author2' }); @@ -112,19 +114,19 @@ describe('RelationshipService', () => { beforeEach(() => { spyOn(service, 'findById').and.returnValue(getRemotedataObservable(relationship1)); spyOn(objectCache, 'remove'); - service.deleteRelationship(relationships[0].uuid).subscribe(); + service.deleteRelationship(relationships[0].uuid, 'right').subscribe(); }); it('should send a DeleteRequest', () => { - const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationship1.uuid); + const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationship1.uuid + '?copyVirtualMetadata=right'); expect(requestService.configure).toHaveBeenCalledWith(expected); }); - it('should clear the related items their cache', () => { + it('should clear the cache of the related items', () => { expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1.self); expect(objectCache.remove).toHaveBeenCalledWith(item.self); - expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.uuid); - expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.uuid); + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.self); + expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self); }); }); diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index d6993ebcee..db8f401687 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -81,15 +81,22 @@ export class RelationshipService extends DataService { * Send a delete request for a relationship by ID * @param id */ - deleteRelationship(id: string): Observable { + deleteRelationship(id: string, copyVirtualMetadata: string): Observable { return this.getRelationshipEndpoint(id).pipe( isNotEmptyOperator(), take(1), - map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)), + distinctUntilChanged(), + map((endpointURL: string) => + new DeleteRequest(this.requestService.generateRequestId(), endpointURL + '?copyVirtualMetadata=' + copyVirtualMetadata) + ), configureRequest(this.requestService), switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)), getResponseFromEntry(), - tap(() => this.removeRelationshipItemsFromCacheByRelationship(id)) + switchMap((response) => + this.clearRelatedCache(id).pipe( + map(() => response), + ) + ), ); } @@ -417,4 +424,26 @@ export class RelationshipService extends DataService { return update$; } + /** + * Clear object and request caches of the items related to a relationship (left and right items) + * @param uuid The uuid of the relationship for which to clear the related items from the cache + */ + clearRelatedCache(uuid: string): Observable { + return this.findById(uuid).pipe( + getSucceededRemoteData(), + switchMap((rd: RemoteData) => + observableCombineLatest( + rd.payload.leftItem.pipe(getSucceededRemoteData()), + rd.payload.rightItem.pipe(getSucceededRemoteData()) + ) + ), + take(1), + map(([leftItem, rightItem]) => { + this.objectCache.remove(leftItem.payload.self); + this.objectCache.remove(rightItem.payload.self); + this.requestService.removeByHrefSubstring(leftItem.payload.self); + this.requestService.removeByHrefSubstring(rightItem.payload.self); + }), + ); + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts index e26abf94c1..201d50e511 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts @@ -128,7 +128,7 @@ export class RelationshipEffects { this.relationshipService.getRelationshipByItemsAndLabel(item1, item2, relationshipType).pipe( take(1), hasValueOperator(), - mergeMap((relationship: Relationship) => this.relationshipService.deleteRelationship(relationship.id)), + mergeMap((relationship: Relationship) => this.relationshipService.deleteRelationship(relationship.id, 'none')), take(1) ).subscribe(); }