mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 10:04:11 +00:00
Merge branch 'master' into w2p-68346_Bundles-in-edit-item-Updates
Conflicts: src/app/+item-page/edit-item-page/edit-item-page.module.ts src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts src/app/core/core.module.ts src/app/core/data/data.service.ts src/app/core/data/object-updates/object-updates.actions.ts src/app/core/data/object-updates/object-updates.reducer.spec.ts src/app/core/data/object-updates/object-updates.reducer.ts src/app/core/data/object-updates/object-updates.service.spec.ts src/app/core/data/object-updates/object-updates.service.ts
This commit is contained in:
@@ -22,6 +22,11 @@ before_install:
|
||||
- sudo mv docker-compose /usr/local/bin
|
||||
|
||||
install:
|
||||
# update chrome
|
||||
- wget -q -O - https://dl-ssl.google.com/linux/linux_signing_key.pub | sudo apt-key add -
|
||||
- sudo sh -c 'echo "deb [arch=amd64] http://dl.google.com/linux/chrome/deb/ stable main" >> /etc/apt/sources.list.d/google-chrome.list'
|
||||
- sudo apt-get update
|
||||
- sudo apt-get install google-chrome-stable
|
||||
# Start up DSpace 7 using the entities database dump
|
||||
- docker-compose -f ./docker/docker-compose-travis.yml up -d
|
||||
# Use the dspace-cli image to populate the assetstore. Trigger a discovery and oai update
|
||||
|
@@ -13,7 +13,7 @@ describe('protractor App', () => {
|
||||
});
|
||||
|
||||
it('should contain a news section', () => {
|
||||
page.navigateTo();
|
||||
expect<any>(page.getHomePageNewsText()).toBeDefined();
|
||||
page.navigateTo()
|
||||
.then(() => expect<any>(page.getHomePageNewsText()).toBeDefined());
|
||||
});
|
||||
});
|
||||
|
@@ -11,6 +11,6 @@ export class ProtractorPage {
|
||||
}
|
||||
|
||||
getHomePageNewsText() {
|
||||
return element(by.xpath('//ds-home-news')).getText();
|
||||
return element(by.css('ds-home-news')).getText();
|
||||
}
|
||||
}
|
||||
|
@@ -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<string>(query).toEqual(queryString);
|
||||
});
|
||||
page.navigateToSearchWithQueryParameter(queryString)
|
||||
.then(() => page.getCurrentQuery())
|
||||
.then((query: string) => {
|
||||
expect<string>(query).toEqual(queryString);
|
||||
});
|
||||
});
|
||||
|
||||
it('should have right scope selected when navigating to page with scope parameter', () => {
|
||||
const scope: promise.Promise<string> = page.getRandomScopeOption();
|
||||
scope.then((scopeString: string) => {
|
||||
page.navigateToSearchWithScopeParameter(scopeString);
|
||||
page.getCurrentScope().then((s: string) => {
|
||||
expect<string>(s).toEqual(scopeString);
|
||||
page.navigateToSearch()
|
||||
.then(() => page.getRandomScopeOption())
|
||||
.then((scopeString: string) => {
|
||||
page.navigateToSearchWithScopeParameter(scopeString);
|
||||
page.getCurrentScope().then((s: string) => {
|
||||
expect<string>(s).toEqual(scopeString);
|
||||
});
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
it('should redirect to the correct url when scope was set and submit button was triggered', () => {
|
||||
const scope: promise.Promise<string> = 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', () => {
|
||||
|
@@ -608,6 +608,8 @@
|
||||
|
||||
"error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.",
|
||||
|
||||
"error.validation.filerequired": "The file upload is mandatory",
|
||||
|
||||
|
||||
|
||||
"footer.copyright": "copyright © 2002-{{ year }}",
|
||||
@@ -1580,6 +1582,8 @@
|
||||
|
||||
"relationships.isVolumeOf": "Journal Volumes",
|
||||
|
||||
"relationships.isContributorOf": "Contributors",
|
||||
|
||||
|
||||
|
||||
"search.description": "",
|
||||
@@ -2089,11 +2093,16 @@
|
||||
|
||||
"uploader.drag-message": "Drag & Drop your files here",
|
||||
|
||||
"uploader.or": ", or ",
|
||||
"uploader.or": ", or",
|
||||
|
||||
"uploader.processing": "Processing",
|
||||
|
||||
"uploader.queue-length": "Queue length",
|
||||
|
||||
"virtual-metadata.delete-item.info": "Select the types for which you want to save the virtual metadata as real metadata",
|
||||
|
||||
"virtual-metadata.delete-item.modal-head": "The virtual metadata of this relation",
|
||||
|
||||
"virtual-metadata.delete-relationship.modal-head": "Select the items for which you want to save the virtual metadata as real metadata",
|
||||
}
|
||||
|
||||
|
@@ -28,6 +28,7 @@ import { BundleDataService } from '../../core/data/bundle-data.service';
|
||||
import { DragDropModule } from '@angular/cdk/drag-drop';
|
||||
import { ItemEditBitstreamDragHandleComponent } from './item-bitstreams/item-edit-bitstream-drag-handle/item-edit-bitstream-drag-handle.component';
|
||||
import { PaginatedDragAndDropBitstreamListComponent } from './item-bitstreams/item-edit-bitstream-bundle/paginated-drag-and-drop-bitstream-list/paginated-drag-and-drop-bitstream-list.component';
|
||||
import { VirtualMetadataComponent } from './virtual-metadata/virtual-metadata.component';
|
||||
|
||||
/**
|
||||
* Module that contains all components related to the Edit Item page administrator functionality
|
||||
@@ -64,7 +65,8 @@ import { PaginatedDragAndDropBitstreamListComponent } from './item-bitstreams/it
|
||||
EditRelationshipListComponent,
|
||||
ItemCollectionMapperComponent,
|
||||
ItemMoveComponent,
|
||||
ItemEditBitstreamDragHandleComponent
|
||||
ItemEditBitstreamDragHandleComponent,
|
||||
VirtualMetadataComponent,
|
||||
],
|
||||
providers: [
|
||||
BundleDataService
|
||||
|
@@ -0,0 +1,98 @@
|
||||
<div class="container">
|
||||
<div class="row">
|
||||
<div class="col-12">
|
||||
|
||||
<h2>{{headerMessage | translate: {id: item.handle} }}</h2>
|
||||
<p>{{descriptionMessage | translate}}</p>
|
||||
<ds-modify-item-overview [item]="item"></ds-modify-item-overview>
|
||||
|
||||
<ng-container *ngVar="(types$ | async) as types">
|
||||
|
||||
<div *ngIf="types && types.length > 0" class="mb-4">
|
||||
|
||||
{{'virtual-metadata.delete-item.info' | translate}}
|
||||
|
||||
<div *ngFor="let type of types" class="mb-4">
|
||||
|
||||
<div *ngVar="(isSelected(type) | async) as selected"
|
||||
class="d-flex flex-row">
|
||||
|
||||
<div class="m-2" (click)="setSelected(type, !selected)">
|
||||
<label>
|
||||
<input type="checkbox" [checked]="selected">
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<div class="flex-column flex-grow-1">
|
||||
<h5 (click)="setSelected(type, !selected)">
|
||||
{{getRelationshipMessageKey(getLabel(type) | async) | translate}}
|
||||
</h5>
|
||||
<div *ngFor="let relationship of (getRelationships(type) | async)"
|
||||
class="d-flex flex-row">
|
||||
<ng-container *ngVar="(getRelatedItem(relationship) | async) as relatedItem">
|
||||
|
||||
<ds-listable-object-component-loader
|
||||
*ngIf="relatedItem"
|
||||
[object]="relatedItem"
|
||||
[viewMode]="viewMode">
|
||||
</ds-listable-object-component-loader>
|
||||
<div class="ml-auto">
|
||||
<div class="btn-group">
|
||||
<button class="btn btn-outline-info btn-sm"
|
||||
(click)="openVirtualMetadataModal(virtualMetadataModal)">
|
||||
<i class="fas fa-info fa-fw"></i>
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<ng-template #virtualMetadataModal>
|
||||
<div>
|
||||
<div class="modal-header">
|
||||
{{'virtual-metadata.delete-item.modal-head' | translate}}
|
||||
<button type="button" class="close"
|
||||
(click)="closeVirtualMetadataModal()" aria-label="Close">
|
||||
<span aria-hidden="true">×</span>
|
||||
</button>
|
||||
</div>
|
||||
<div class="modal-body">
|
||||
<ds-listable-object-component-loader
|
||||
*ngIf="relatedItem"
|
||||
[object]="relatedItem"
|
||||
[viewMode]="viewMode">
|
||||
</ds-listable-object-component-loader>
|
||||
<div *ngFor="let metadata of (getVirtualMetadata(relationship) | async)">
|
||||
<div>
|
||||
<div class="font-weight-bold">
|
||||
{{metadata.metadataField}}
|
||||
</div>
|
||||
<div>
|
||||
{{metadata.metadataValue.value}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-template>
|
||||
|
||||
</ng-container>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
|
||||
</ng-container>
|
||||
|
||||
<button (click)="performAction()"
|
||||
class="btn btn-outline-secondary perform-action">{{confirmMessage | translate}}
|
||||
</button>
|
||||
<button [routerLink]="['/items/', item.id, 'edit']" class="btn btn-outline-secondary cancel">
|
||||
{{cancelMessage| translate}}
|
||||
</button>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -1,44 +1,132 @@
|
||||
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||
import { Item } from '../../../core/shared/item.model';
|
||||
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||
import { CommonModule } from '@angular/common';
|
||||
import { FormsModule } from '@angular/forms';
|
||||
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 { ItemDataService } from '../../../core/data/item-data.service';
|
||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { By } from '@angular/platform-browser';
|
||||
import { ItemDeleteComponent } from './item-delete.component';
|
||||
import { getItemEditPath } from '../../item-page-routing.module';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import { createSuccessfulRemoteDataObject } from '../../../shared/testing/utils';
|
||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
||||
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||
import {Item} from '../../../core/shared/item.model';
|
||||
import {RouterStub} from '../../../shared/testing/router-stub';
|
||||
import {of as observableOf} from 'rxjs';
|
||||
import {NotificationsServiceStub} from '../../../shared/testing/notifications-service-stub';
|
||||
import {CommonModule} from '@angular/common';
|
||||
import {FormsModule} from '@angular/forms';
|
||||
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 {ItemDataService} from '../../../core/data/item-data.service';
|
||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||
import {CUSTOM_ELEMENTS_SCHEMA} from '@angular/core';
|
||||
import {By} from '@angular/platform-browser';
|
||||
import {ItemDeleteComponent} from './item-delete.component';
|
||||
import {getItemEditPath} from '../../item-page-routing.module';
|
||||
import {createSuccessfulRemoteDataObject} from '../../../shared/testing/utils';
|
||||
import {VarDirective} from '../../../shared/utils/var.directive';
|
||||
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
||||
import {RelationshipService} from '../../../core/data/relationship.service';
|
||||
import {RelationshipType} from '../../../core/shared/item-relationships/relationship-type.model';
|
||||
import {RemoteData} from '../../../core/data/remote-data';
|
||||
import {PaginatedList} from '../../../core/data/paginated-list';
|
||||
import {PageInfo} from '../../../core/shared/page-info.model';
|
||||
import {EntityTypeService} from '../../../core/data/entity-type.service';
|
||||
|
||||
let comp: ItemDeleteComponent;
|
||||
let fixture: ComponentFixture<ItemDeleteComponent>;
|
||||
|
||||
let mockItem;
|
||||
let itemType;
|
||||
let type1;
|
||||
let type2;
|
||||
let types;
|
||||
let relationships;
|
||||
let itemPageUrl;
|
||||
let routerStub;
|
||||
let mockItemDataService: ItemDataService;
|
||||
let routeStub;
|
||||
let objectUpdatesServiceStub;
|
||||
let relationshipService;
|
||||
let entityTypeService;
|
||||
let notificationsServiceStub;
|
||||
let typesSelection;
|
||||
|
||||
describe('ItemDeleteComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
|
||||
mockItem = Object.assign(new Item(), {
|
||||
id: 'fake-id',
|
||||
uuid: 'fake-uuid',
|
||||
handle: 'fake/handle',
|
||||
lastModified: '2018',
|
||||
isWithdrawn: true
|
||||
});
|
||||
|
||||
itemType = Object.assign(new ItemType(), {
|
||||
id: 'itemType',
|
||||
uuid: 'itemType',
|
||||
});
|
||||
|
||||
type1 = Object.assign(new RelationshipType(), {
|
||||
id: '1',
|
||||
uuid: 'type-1',
|
||||
});
|
||||
|
||||
type2 = Object.assign(new RelationshipType(), {
|
||||
id: '2',
|
||||
uuid: 'type-2',
|
||||
});
|
||||
|
||||
types = [type1, type2];
|
||||
|
||||
relationships = [
|
||||
Object.assign(new Relationship(), {
|
||||
id: '1',
|
||||
uuid: 'relationship-1',
|
||||
relationshipType: observableOf(new RemoteData(
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
type1
|
||||
)),
|
||||
leftItem: observableOf(new RemoteData(
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
mockItem,
|
||||
)),
|
||||
rightItem: observableOf(new RemoteData(
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
Object.assign(new Item(), {})
|
||||
)),
|
||||
}),
|
||||
Object.assign(new Relationship(), {
|
||||
id: '2',
|
||||
uuid: 'relationship-2',
|
||||
relationshipType: observableOf(new RemoteData(
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
type2
|
||||
)),
|
||||
leftItem: observableOf(new RemoteData(
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
mockItem,
|
||||
)),
|
||||
rightItem: observableOf(new RemoteData(
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
Object.assign(new Item(), {})
|
||||
)),
|
||||
}),
|
||||
];
|
||||
|
||||
itemPageUrl = `fake-url/${mockItem.id}`;
|
||||
routerStub = Object.assign(new RouterStub(), {
|
||||
url: `${itemPageUrl}/edit`
|
||||
@@ -54,16 +142,56 @@ describe('ItemDeleteComponent', () => {
|
||||
})
|
||||
};
|
||||
|
||||
typesSelection = {
|
||||
type1: false,
|
||||
type2: true,
|
||||
};
|
||||
|
||||
entityTypeService = jasmine.createSpyObj('entityTypeService',
|
||||
{
|
||||
getEntityTypeByLabel: observableOf(new RemoteData(
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
itemType,
|
||||
)),
|
||||
getEntityTypeRelationships: observableOf(new RemoteData(
|
||||
false,
|
||||
false,
|
||||
true,
|
||||
null,
|
||||
new PaginatedList(new PageInfo(), types),
|
||||
)),
|
||||
}
|
||||
);
|
||||
|
||||
objectUpdatesServiceStub = {
|
||||
initialize: () => {
|
||||
// do nothing
|
||||
},
|
||||
isSelectedVirtualMetadata: (type) => observableOf(typesSelection[type]),
|
||||
};
|
||||
|
||||
relationshipService = jasmine.createSpyObj('relationshipService',
|
||||
{
|
||||
getItemRelationshipsArray: observableOf(relationships),
|
||||
}
|
||||
);
|
||||
|
||||
notificationsServiceStub = new NotificationsServiceStub();
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||
declarations: [ItemDeleteComponent],
|
||||
declarations: [ItemDeleteComponent, VarDirective],
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: routeStub },
|
||||
{ provide: Router, useValue: routerStub },
|
||||
{ provide: ItemDataService, useValue: mockItemDataService },
|
||||
{ provide: NotificationsService, useValue: notificationsServiceStub },
|
||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesServiceStub },
|
||||
{ provide: RelationshipService, useValue: relationshipService },
|
||||
{ provide: EntityTypeService, useValue: entityTypeService },
|
||||
], schemas: [
|
||||
CUSTOM_ELEMENTS_SCHEMA
|
||||
]
|
||||
@@ -91,7 +219,8 @@ describe('ItemDeleteComponent', () => {
|
||||
it('should call delete function from the ItemDataService', () => {
|
||||
spyOn(comp, 'notify');
|
||||
comp.performAction();
|
||||
expect(mockItemDataService.delete).toHaveBeenCalledWith(mockItem);
|
||||
expect(mockItemDataService.delete)
|
||||
.toHaveBeenCalledWith(mockItem, types.filter((type) => typesSelection[type]).map((type) => type.id));
|
||||
expect(comp.notify).toHaveBeenCalled();
|
||||
});
|
||||
});
|
||||
|
@@ -1,29 +1,323 @@
|
||||
import { Component } from '@angular/core';
|
||||
import { first } from 'rxjs/operators';
|
||||
import { AbstractSimpleItemActionComponent } from '../simple-item-action/abstract-simple-item-action.component';
|
||||
import { getItemEditPath } from '../../item-page-routing.module';
|
||||
import { RestResponse } from '../../../core/cache/response.models';
|
||||
import {Component, Input, OnInit} from '@angular/core';
|
||||
import {filter, first, map, switchMap, take} from 'rxjs/operators';
|
||||
import {AbstractSimpleItemActionComponent} from '../simple-item-action/abstract-simple-item-action.component';
|
||||
import {getItemEditPath} from '../../item-page-routing.module';
|
||||
import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap';
|
||||
import {combineLatest as observableCombineLatest, combineLatest, Observable} from 'rxjs';
|
||||
import {RelationshipType} from '../../../core/shared/item-relationships/relationship-type.model';
|
||||
import {VirtualMetadata} from '../virtual-metadata/virtual-metadata.component';
|
||||
import {Relationship} from '../../../core/shared/item-relationships/relationship.model';
|
||||
import {getRemoteDataPayload, getSucceededRemoteData} from '../../../core/shared/operators';
|
||||
import {hasValue, isNotEmpty} from '../../../shared/empty.util';
|
||||
import {Item} from '../../../core/shared/item.model';
|
||||
import {MetadataValue} from '../../../core/shared/metadata.models';
|
||||
import {ViewMode} from '../../../core/shared/view-mode.model';
|
||||
import {ActivatedRoute, Router} from '@angular/router';
|
||||
import {NotificationsService} from '../../../shared/notifications/notifications.service';
|
||||
import {ItemDataService} from '../../../core/data/item-data.service';
|
||||
import {TranslateService} from '@ngx-translate/core';
|
||||
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
||||
import {RelationshipService} from '../../../core/data/relationship.service';
|
||||
import {EntityTypeService} from '../../../core/data/entity-type.service';
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-delete',
|
||||
templateUrl: '../simple-item-action/abstract-simple-item-action.component.html'
|
||||
templateUrl: '../item-delete/item-delete.component.html'
|
||||
})
|
||||
/**
|
||||
* Component responsible for rendering the item delete page
|
||||
*/
|
||||
export class ItemDeleteComponent extends AbstractSimpleItemActionComponent {
|
||||
export class ItemDeleteComponent
|
||||
extends AbstractSimpleItemActionComponent
|
||||
implements OnInit {
|
||||
|
||||
/**
|
||||
* The current url of this page
|
||||
*/
|
||||
@Input() url: string;
|
||||
|
||||
protected messageKey = 'delete';
|
||||
|
||||
/**
|
||||
* Perform the delete action to the item
|
||||
* The view-mode we're currently on
|
||||
*/
|
||||
viewMode = ViewMode.ListElement;
|
||||
|
||||
/**
|
||||
* A list of the relationship types for which this item has relations as an observable.
|
||||
* The list doesn't contain duplicates.
|
||||
*/
|
||||
types$: Observable<RelationshipType[]>;
|
||||
|
||||
/**
|
||||
* A map which stores the relationships of this item for each type as observable lists
|
||||
*/
|
||||
relationships$: Map<RelationshipType, Observable<Relationship[]>>
|
||||
= new Map<RelationshipType, Observable<Relationship[]>>();
|
||||
|
||||
/**
|
||||
* A map which stores the related item of each relationship of this item as an observable
|
||||
*/
|
||||
relatedItems$: Map<Relationship, Observable<Item>> = new Map<Relationship, Observable<Item>>();
|
||||
|
||||
/**
|
||||
* A map which stores the virtual metadata (of the related) item corresponding to each relationship of this item
|
||||
* as an observable list
|
||||
*/
|
||||
virtualMetadata$: Map<Relationship, Observable<VirtualMetadata[]>> = new Map<Relationship, Observable<VirtualMetadata[]>>();
|
||||
|
||||
/**
|
||||
* Reference to NgbModal
|
||||
*/
|
||||
public modalRef: NgbModalRef;
|
||||
|
||||
constructor(protected route: ActivatedRoute,
|
||||
protected router: Router,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected itemDataService: ItemDataService,
|
||||
protected translateService: TranslateService,
|
||||
protected modalService: NgbModal,
|
||||
protected objectUpdatesService: ObjectUpdatesService,
|
||||
protected relationshipService: RelationshipService,
|
||||
protected entityTypeService: EntityTypeService,
|
||||
) {
|
||||
super(
|
||||
route,
|
||||
router,
|
||||
notificationsService,
|
||||
itemDataService,
|
||||
translateService,
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Set up and initialize all fields
|
||||
*/
|
||||
ngOnInit() {
|
||||
|
||||
super.ngOnInit();
|
||||
this.url = this.router.url;
|
||||
|
||||
this.types$ = this.entityTypeService.getEntityTypeByLabel(
|
||||
this.item.firstMetadataValue('relationship.type')
|
||||
).pipe(
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
switchMap((entityType) => this.entityTypeService.getEntityTypeRelationships(entityType.id)),
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
map((relationshipTypes) => relationshipTypes.page),
|
||||
switchMap((types) =>
|
||||
combineLatest(types.map((type) => this.getRelationships(type))).pipe(
|
||||
map((relationships) =>
|
||||
types.reduce<RelationshipType[]>((includedTypes, type, index) => {
|
||||
if (!includedTypes.some((includedType) => includedType.id === type.id)
|
||||
&& !(relationships[index].length === 0)) {
|
||||
return [...includedTypes, type];
|
||||
} else {
|
||||
return includedTypes;
|
||||
}
|
||||
}, [])
|
||||
),
|
||||
)
|
||||
),
|
||||
);
|
||||
|
||||
this.types$.pipe(
|
||||
take(1),
|
||||
).subscribe((types) =>
|
||||
this.objectUpdatesService.initialize(this.url, types, this.item.lastModified)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open the modal which lists the virtual metadata of a relation
|
||||
* @param content the html content of the modal
|
||||
*/
|
||||
openVirtualMetadataModal(content: any) {
|
||||
this.modalRef = this.modalService.open(content);
|
||||
}
|
||||
|
||||
/**
|
||||
* Close the modal which lists the virtual metadata of a relation
|
||||
*/
|
||||
closeVirtualMetadataModal() {
|
||||
this.modalRef.close();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the i18n message key for a relationship
|
||||
* @param label The relationship type's label
|
||||
*/
|
||||
getRelationshipMessageKey(label: string): string {
|
||||
if (hasValue(label) && label.indexOf('Of') > -1) {
|
||||
return `relationships.${label.substring(0, label.indexOf('Of') + 2)}`
|
||||
} else {
|
||||
return label;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationship type label relevant for this item as an observable
|
||||
* @param relationshipType the relationship type to get the label for
|
||||
*/
|
||||
getLabel(relationshipType: RelationshipType): Observable<string> {
|
||||
|
||||
return this.getRelationships(relationshipType).pipe(
|
||||
switchMap((relationships) =>
|
||||
this.isLeftItem(relationships[0]).pipe(
|
||||
map((isLeftItem) => isLeftItem ? relationshipType.leftwardType : relationshipType.rightwardType),
|
||||
)
|
||||
),
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the relationships of this item with a given type as an observable
|
||||
* @param relationshipType the relationship type to filter the item's relationships on
|
||||
*/
|
||||
getRelationships(relationshipType: RelationshipType): Observable<Relationship[]> {
|
||||
|
||||
if (!this.relationships$.has(relationshipType)) {
|
||||
this.relationships$.set(
|
||||
relationshipType,
|
||||
this.relationshipService.getItemRelationshipsArray(this.item).pipe(
|
||||
// filter on type
|
||||
switchMap((relationships) =>
|
||||
observableCombineLatest(
|
||||
relationships.map((relationship) => this.getRelationshipType(relationship))
|
||||
).pipe(
|
||||
map((types) => relationships.filter(
|
||||
(relationship, index) => relationshipType.id === types[index].id
|
||||
)),
|
||||
)
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return this.relationships$.get(relationshipType);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the type of a given relationship as an observable
|
||||
* @param relationship the relationship to get the type for
|
||||
*/
|
||||
private getRelationshipType(relationship: Relationship): Observable<RelationshipType> {
|
||||
|
||||
return relationship.relationshipType.pipe(
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
filter((relationshipType: RelationshipType) => hasValue(relationshipType) && isNotEmpty(relationshipType.uuid))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the item this item is related to through a given relationship as an observable
|
||||
* @param relationship the relationship to get the other item for
|
||||
*/
|
||||
getRelatedItem(relationship: Relationship): Observable<Item> {
|
||||
|
||||
if (!this.relatedItems$.has(relationship)) {
|
||||
|
||||
this.relatedItems$.set(
|
||||
relationship,
|
||||
this.isLeftItem(relationship).pipe(
|
||||
switchMap((isLeftItem) => isLeftItem ? relationship.rightItem : relationship.leftItem),
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
return this.relatedItems$.get(relationship);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the virtual metadata for a given relationship of the related item.
|
||||
* @param relationship the relationship to get the virtual metadata for
|
||||
*/
|
||||
getVirtualMetadata(relationship: Relationship): Observable<VirtualMetadata[]> {
|
||||
|
||||
if (!this.virtualMetadata$.has(relationship)) {
|
||||
|
||||
this.virtualMetadata$.set(
|
||||
relationship,
|
||||
this.getRelatedItem(relationship).pipe(
|
||||
map((relatedItem) =>
|
||||
Object.entries(relatedItem.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))
|
||||
),
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
return this.virtualMetadata$.get(relationship);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether this item is the left item of a given relationship, as an observable boolean
|
||||
* @param relationship the relationship for which to check whether this item is the left item
|
||||
*/
|
||||
private isLeftItem(relationship: Relationship): Observable<boolean> {
|
||||
|
||||
return relationship.leftItem.pipe(
|
||||
getSucceededRemoteData(),
|
||||
getRemoteDataPayload(),
|
||||
filter((item: Item) => hasValue(item) && isNotEmpty(item.uuid)),
|
||||
map((leftItem) => leftItem.uuid === this.item.uuid)
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check whether a given relationship type is selected to save the corresponding virtual metadata
|
||||
* @param type the relationship type for which to check whether it is selected
|
||||
*/
|
||||
isSelected(type: RelationshipType): Observable<boolean> {
|
||||
return this.objectUpdatesService.isSelectedVirtualMetadata(this.url, this.item.uuid, type.uuid);
|
||||
}
|
||||
|
||||
/**
|
||||
* Select/deselect a given relationship type to save the corresponding virtual metadata
|
||||
* @param type the relationship type to select/deselect
|
||||
* @param selected whether the type should be selected
|
||||
*/
|
||||
setSelected(type: RelationshipType, selected: boolean): void {
|
||||
this.objectUpdatesService.setSelectedVirtualMetadata(this.url, this.item.uuid, type.uuid, selected);
|
||||
}
|
||||
|
||||
/**
|
||||
* Perform the delete operation
|
||||
*/
|
||||
performAction() {
|
||||
this.itemDataService.delete(this.item).pipe(first()).subscribe(
|
||||
(succeeded: boolean) => {
|
||||
this.notify(succeeded);
|
||||
}
|
||||
);
|
||||
|
||||
this.types$.pipe(
|
||||
switchMap((types) =>
|
||||
combineLatest(
|
||||
types.map((type) => this.isSelected(type))
|
||||
).pipe(
|
||||
map((selection) => types.filter(
|
||||
(type, index) => selection[index]
|
||||
)),
|
||||
map((selectedTypes) => selectedTypes.map((type) => type.id)),
|
||||
)
|
||||
),
|
||||
).subscribe((types) => {
|
||||
this.itemDataService.delete(this.item, types).pipe(first()).subscribe(
|
||||
(succeeded: boolean) => {
|
||||
this.notify(succeeded);
|
||||
}
|
||||
);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -1,15 +1,15 @@
|
||||
<ng-container *ngVar="(updates$ | async) as updates">
|
||||
<div *ngIf="updates">
|
||||
<h5>{{getRelationshipMessageKey(relationshipLabel) | translate}}</h5>
|
||||
<ng-container *ngVar="(updates | dsObjectValues) as updateValues">
|
||||
<div *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
||||
ds-edit-relationship
|
||||
class="relationship-row d-block"
|
||||
[fieldUpdate]="updateValue || {}"
|
||||
[url]="url"
|
||||
[ngClass]="{'alert alert-danger': updateValue.changeType === 2}">
|
||||
</div>
|
||||
<ds-loading *ngIf="updateValues.length == 0" message="{{'loading.items' | translate}}"></ds-loading>
|
||||
<h5>{{getRelationshipMessageKey() | async | translate}}</h5>
|
||||
<ng-container *ngVar="updates$ | async as updates">
|
||||
<ng-container *ngIf="updates">
|
||||
<ng-container *ngVar="updates | dsObjectValues as updateValues">
|
||||
<ds-edit-relationship *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
||||
class="relationship-row d-block"
|
||||
[fieldUpdate]="updateValue"
|
||||
[url]="url"
|
||||
[editItem]="item"
|
||||
[ngClass]="{'alert alert-danger': updateValue?.changeType === 2}">
|
||||
</ds-edit-relationship>
|
||||
</ng-container>
|
||||
</ng-container>
|
||||
</div>
|
||||
<div *ngIf="!updates">no relationships</div>
|
||||
</ng-container>
|
||||
|
@@ -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<EditRelationshipListComponent>;
|
||||
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');
|
||||
});
|
||||
|
@@ -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<string> {
|
||||
|
||||
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<string> {
|
||||
|
||||
/**
|
||||
* 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))
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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,14 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<ng-template #virtualMetadataModal>
|
||||
<ds-virtual-metadata
|
||||
[relationshipId]="relationship.id"
|
||||
[leftItem]="leftItem$ | async"
|
||||
[rightItem]="rightItem$ | async"
|
||||
[url]="url"
|
||||
(close)="closeVirtualMetadataModal()"
|
||||
(save)="remove()"
|
||||
>
|
||||
</ds-virtual-metadata>
|
||||
</ng-template>
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -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<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) {
|
||||
/**
|
||||
* 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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -17,8 +17,13 @@
|
||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||
</button>
|
||||
</div>
|
||||
<div *ngFor="let label of relationLabels$ | async" class="mb-4">
|
||||
<ds-edit-relationship-list [item]="item" [url]="url" [relationshipLabel]="label" ></ds-edit-relationship-list>
|
||||
<div *ngFor="let relationshipType of relationshipTypes$ | async" class="mb-4">
|
||||
<ds-edit-relationship-list
|
||||
[url]="url"
|
||||
[item]="item"
|
||||
[itemType]="entityType$ | async"
|
||||
[relationshipType]="relationshipType"
|
||||
></ds-edit-relationship-list>
|
||||
</div>
|
||||
<div class="button-row bottom">
|
||||
<div class="float-right">
|
||||
|
@@ -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<ItemRelationshipsComponent>;
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -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<string[]>;
|
||||
relationshipTypes$: Observable<RelationshipType[]>;
|
||||
|
||||
/**
|
||||
* 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<ItemType>;
|
||||
|
||||
constructor(
|
||||
public itemService: ItemDataService,
|
||||
@@ -54,7 +58,8 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
||||
public relationshipService: RelationshipService,
|
||||
public objectCache: ObjectCacheService,
|
||||
public requestService: RequestService,
|
||||
public cdRef: ChangeDetectorRef
|
||||
public entityTypeService: EntityTypeService,
|
||||
public 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<Item>) => {
|
||||
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<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(
|
||||
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<FieldUpdates>
|
||||
}),
|
||||
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();
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -0,0 +1,38 @@
|
||||
<div>
|
||||
<div class="modal-header">{{'virtual-metadata.delete-relationship.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">
|
||||
<ng-container *ngFor="let item of items; trackBy: trackItem">
|
||||
<div *ngVar="(isSelectedVirtualMetadataItem(item) | async) as selected"
|
||||
(click)="setSelectedVirtualMetadataItem(item, !selected)"
|
||||
class="item d-flex flex-row">
|
||||
<div class="m-2">
|
||||
<label>
|
||||
<input class="select" type="checkbox" [checked]="selected">
|
||||
</label>
|
||||
</div>
|
||||
<div class="flex-column">
|
||||
<ds-listable-object-component-loader [object]="item">
|
||||
</ds-listable-object-component-loader>
|
||||
<div *ngFor="let metadata of virtualMetadata.get(item.uuid)">
|
||||
<div class="font-weight-bold">
|
||||
{{metadata.metadataField}}
|
||||
</div>
|
||||
<div>
|
||||
{{metadata.metadataValue.value}}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</ng-container>
|
||||
<div class="d-flex flex-row-reverse m-2">
|
||||
<button class="btn btn-primary save"
|
||||
(click)="save.emit()">
|
||||
<i class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
@@ -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<VirtualMetadataComponent>;
|
||||
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
|
||||
);
|
||||
});
|
||||
})
|
||||
});
|
@@ -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<string, VirtualMetadata[]> = new Map<string, VirtualMetadata[]>();
|
||||
|
||||
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<boolean> {
|
||||
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,
|
||||
}
|
@@ -11,6 +11,7 @@ let fixture: ComponentFixture<ItemPageUriFieldComponent>;
|
||||
|
||||
const mockField = 'dc.identifier.uri';
|
||||
const mockValue = 'test value';
|
||||
const mockLabel = 'test label';
|
||||
|
||||
describe('ItemPageUriFieldComponent', () => {
|
||||
beforeEach(async(() => {
|
||||
@@ -32,6 +33,8 @@ describe('ItemPageUriFieldComponent', () => {
|
||||
fixture = TestBed.createComponent(ItemPageUriFieldComponent);
|
||||
comp = fixture.componentInstance;
|
||||
comp.item = mockItemWithMetadataFieldAndValue(mockField, mockValue);
|
||||
comp.fields = [mockField];
|
||||
comp.label = mockLabel;
|
||||
fixture.detectChanges();
|
||||
}));
|
||||
|
||||
|
@@ -8,7 +8,8 @@ import { ItemPageFieldComponent } from '../item-page-field.component';
|
||||
templateUrl: './item-page-uri-field.component.html'
|
||||
})
|
||||
/**
|
||||
* This component is used for displaying the uri (dc.identifier.uri) metadata of an item
|
||||
* This component can be used to represent any uri on a simple item page.
|
||||
* It expects 4 parameters: The item, a separator, the metadata keys and an i18n key
|
||||
*/
|
||||
export class ItemPageUriFieldComponent extends ItemPageFieldComponent {
|
||||
|
||||
@@ -21,19 +22,16 @@ export class ItemPageUriFieldComponent extends ItemPageFieldComponent {
|
||||
* Separator string between multiple values of the metadata fields defined
|
||||
* @type {string}
|
||||
*/
|
||||
separator: string;
|
||||
@Input() separator: string;
|
||||
|
||||
/**
|
||||
* Fields (schema.element.qualifier) used to render their values.
|
||||
* In this component, we want to display values for metadata 'dc.identifier.uri'
|
||||
*/
|
||||
fields: string[] = [
|
||||
'dc.identifier.uri'
|
||||
];
|
||||
@Input() fields: string[];
|
||||
|
||||
/**
|
||||
* Label i18n key for the rendered metadata
|
||||
*/
|
||||
label = 'item.page.uri';
|
||||
@Input() label: string;
|
||||
|
||||
}
|
||||
|
@@ -63,7 +63,10 @@
|
||||
[fields]="['dc.identifier.citation']"
|
||||
[label]="'item.page.citation'">
|
||||
</ds-generic-item-page-field>
|
||||
<ds-item-page-uri-field [item]="object"></ds-item-page-uri-field>
|
||||
<ds-item-page-uri-field [item]="object"
|
||||
[fields]="['dc.identifier.uri']"
|
||||
[label]="'item.page.uri'">
|
||||
</ds-item-page-uri-field>
|
||||
<ds-item-page-collections [item]="object"></ds-item-page-collections>
|
||||
<div>
|
||||
<a class="btn btn-outline-primary" [routerLink]="['/items/' + object.id + '/full']">
|
||||
|
@@ -124,6 +124,7 @@ import { ContentSourceResponseParsingService } from './data/content-source-respo
|
||||
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
||||
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
||||
import { ArrayMoveChangeAnalyzer } from './data/array-move-change-analyzer.service';
|
||||
import { EntityTypeService } from './data/entity-type.service';
|
||||
import { SiteDataService } from './data/site-data.service';
|
||||
import { NormalizedSite } from './cache/models/normalized-site.model';
|
||||
|
||||
@@ -249,6 +250,7 @@ const PROVIDERS = [
|
||||
ClaimedTaskDataService,
|
||||
PoolTaskDataService,
|
||||
BitstreamDataService,
|
||||
EntityTypeService,
|
||||
ContentSourceResponseParsingService,
|
||||
SearchService,
|
||||
SidebarService,
|
||||
|
@@ -345,10 +345,12 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
/**
|
||||
* Delete an existing DSpace Object on the server
|
||||
* @param dso The DSpace Object to be removed
|
||||
* Return an observable that emits true when the deletion was successful, false when it failed
|
||||
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||
* metadata should be saved as real metadata
|
||||
* @return an observable that emits true when the deletion was successful, false when it failed
|
||||
*/
|
||||
delete(dso: T): Observable<boolean> {
|
||||
const requestId = this.deleteAndReturnRequestId(dso);
|
||||
delete(dso: T, copyVirtualMetadata?: string[]): Observable<boolean> {
|
||||
const requestId = this.deleteAndReturnRequestId(dso, copyVirtualMetadata);
|
||||
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
find((request: RequestEntry) => request.completed),
|
||||
@@ -359,10 +361,12 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
/**
|
||||
* Delete an existing DSpace Object on the server
|
||||
* @param dso The DSpace Object to be removed
|
||||
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||
* metadata should be saved as real metadata
|
||||
* Return an observable of the completed response
|
||||
*/
|
||||
deleteAndReturnResponse(dso: T): Observable<RestResponse> {
|
||||
const requestId = this.deleteAndReturnRequestId(dso);
|
||||
deleteAndReturnResponse(dso: T, copyVirtualMetadata?: string[]): Observable<RestResponse> {
|
||||
const requestId = this.deleteAndReturnRequestId(dso, copyVirtualMetadata);
|
||||
|
||||
return this.requestService.getByUUID(requestId).pipe(
|
||||
hasValueOperator(),
|
||||
@@ -374,9 +378,11 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
/**
|
||||
* Delete an existing DSpace Object on the server
|
||||
* @param dso The DSpace Object to be removed
|
||||
* @param copyVirtualMetadata (optional parameter) the identifiers of the relationship types for which the virtual
|
||||
* metadata should be saved as real metadata
|
||||
* Return the delete request's ID
|
||||
*/
|
||||
private deleteAndReturnRequestId(dso: T): string {
|
||||
private deleteAndReturnRequestId(dso: T, copyVirtualMetadata?: string[]): string {
|
||||
const requestId = this.requestService.generateRequestId();
|
||||
|
||||
const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
|
||||
@@ -385,6 +391,13 @@ export abstract class DataService<T extends CacheableObject> {
|
||||
hrefObs.pipe(
|
||||
find((href: string) => hasValue(href)),
|
||||
map((href: string) => {
|
||||
if (copyVirtualMetadata) {
|
||||
copyVirtualMetadata.forEach((id) =>
|
||||
href += (href.includes('?') ? '&' : '?')
|
||||
+ 'copyVirtualMetadata='
|
||||
+ id
|
||||
);
|
||||
}
|
||||
const request = new DeleteByIDRequest(requestId, href, dso.uuid);
|
||||
this.requestService.configure(request);
|
||||
})
|
||||
|
103
src/app/core/data/entity-type.service.ts
Normal file
103
src/app/core/data/entity-type.service.ts
Normal file
@@ -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<ItemType> {
|
||||
|
||||
protected linkPath = 'entitytypes';
|
||||
protected forceBypassCache = false;
|
||||
|
||||
constructor(protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected dataBuildService: NormalizedObjectBuildService,
|
||||
protected store: Store<CoreState>,
|
||||
protected halService: HALEndpointService,
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected notificationsService: NotificationsService,
|
||||
protected http: HttpClient,
|
||||
protected comparator: DefaultChangeAnalyzer<ItemType>) {
|
||||
super();
|
||||
}
|
||||
|
||||
getBrowseEndpoint(options, linkPath?: string): Observable<string> {
|
||||
return this.halService.getEndpoint(this.linkPath);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the endpoint for the item type's allowed relationship types
|
||||
* @param entityTypeId
|
||||
*/
|
||||
getRelationshipTypesEndpoint(entityTypeId: string): Observable<string> {
|
||||
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<RemoteData<PaginatedList<RelationshipType>>> {
|
||||
|
||||
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<RemoteData<ItemType>> {
|
||||
|
||||
// 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<EntityType>(href$);
|
||||
*/
|
||||
|
||||
// Mock:
|
||||
const index = [
|
||||
'Publication',
|
||||
'Person',
|
||||
'Project',
|
||||
'OrgUnit',
|
||||
'Journal',
|
||||
'JournalVolume',
|
||||
'JournalIssue',
|
||||
'DataPackage',
|
||||
'DataFile',
|
||||
].indexOf(label);
|
||||
|
||||
return this.findById((index + 1) + '');
|
||||
}
|
||||
}
|
@@ -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';
|
||||
|
@@ -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
|
||||
@@ -12,6 +12,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'),
|
||||
@@ -126,6 +127,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
|
||||
*/
|
||||
@@ -334,4 +370,5 @@ export type ObjectUpdatesAction
|
||||
| RemoveObjectUpdatesAction
|
||||
| RemoveFieldUpdateAction
|
||||
| MoveFieldUpdateAction
|
||||
| AddPageToCustomOrderAction;
|
||||
| AddPageToCustomOrderAction
|
||||
| SelectVirtualMetadataAction;
|
||||
|
@@ -5,10 +5,11 @@ import {
|
||||
FieldChangeType,
|
||||
InitializeFieldsAction, MoveFieldUpdateAction,
|
||||
ReinstateObjectUpdatesAction, RemoveAllObjectUpdatesAction,
|
||||
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;
|
||||
@@ -80,6 +82,9 @@ describe('objectUpdatesReducer', () => {
|
||||
}
|
||||
},
|
||||
lastModified: modDate,
|
||||
virtualMetadataSources: {
|
||||
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||
},
|
||||
customOrder: {
|
||||
initialOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
@@ -113,6 +118,9 @@ describe('objectUpdatesReducer', () => {
|
||||
},
|
||||
},
|
||||
lastModified: modDate,
|
||||
virtualMetadataSources: {
|
||||
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||
},
|
||||
customOrder: {
|
||||
initialOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
@@ -154,6 +162,9 @@ describe('objectUpdatesReducer', () => {
|
||||
}
|
||||
},
|
||||
lastModified: modDate,
|
||||
virtualMetadataSources: {
|
||||
[relationship.uuid]: {[identifiable1.uuid]: true}
|
||||
},
|
||||
customOrder: {
|
||||
initialOrderPages: [
|
||||
{ order: [identifiable1.uuid, identifiable2.uuid, identifiable3.uuid] }
|
||||
@@ -225,6 +236,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, [identifiable1.uuid, identifiable3.uuid], 10, 0);
|
||||
|
||||
@@ -243,6 +260,7 @@ describe('objectUpdatesReducer', () => {
|
||||
},
|
||||
},
|
||||
fieldUpdates: {},
|
||||
virtualMetadataSources: {},
|
||||
lastModified: modDate,
|
||||
customOrder: {
|
||||
initialOrderPages: [
|
||||
|
@@ -7,11 +7,15 @@ import {
|
||||
ObjectUpdatesActionTypes,
|
||||
ReinstateObjectUpdatesAction,
|
||||
RemoveFieldUpdateAction,
|
||||
RemoveObjectUpdatesAction, SetEditableFieldUpdateAction, SetValidFieldUpdateAction
|
||||
RemoveObjectUpdatesAction,
|
||||
SetEditableFieldUpdateAction,
|
||||
SetValidFieldUpdateAction,
|
||||
SelectVirtualMetadataAction,
|
||||
} from './object-updates.actions';
|
||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../../shared/empty.util';
|
||||
import { moveItemInArray, transferArrayItem } from '@angular/cdk/drag-drop';
|
||||
import { from } from 'rxjs/internal/observable/from';
|
||||
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
||||
|
||||
/**
|
||||
* Path where discarded objects are saved
|
||||
@@ -56,6 +60,29 @@ 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,
|
||||
}
|
||||
|
||||
/**
|
||||
* A custom order given to the list of objects
|
||||
*/
|
||||
@@ -75,7 +102,8 @@ export interface OrderPage {
|
||||
*/
|
||||
export interface ObjectUpdatesEntry {
|
||||
fieldStates: FieldStates;
|
||||
fieldUpdates: FieldUpdates
|
||||
fieldUpdates: FieldUpdates;
|
||||
virtualMetadataSources: VirtualMetadataSources;
|
||||
lastModified: Date;
|
||||
customOrder: CustomOrder
|
||||
}
|
||||
@@ -116,6 +144,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);
|
||||
}
|
||||
@@ -165,6 +196,7 @@ function initializeFieldsUpdate(state: any, action: InitializeFieldsAction) {
|
||||
state[url],
|
||||
{ fieldStates: fieldStates },
|
||||
{ fieldUpdates: {} },
|
||||
{ virtualMetadataSources: {} },
|
||||
{ lastModified: lastModifiedServer },
|
||||
{ customOrder: {
|
||||
initialOrderPages: initialOrderPages,
|
||||
@@ -227,6 +259,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
|
||||
|
@@ -5,13 +5,14 @@ import {
|
||||
AddPageToCustomOrderAction,
|
||||
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;
|
||||
@@ -23,6 +24,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 },
|
||||
@@ -39,7 +41,7 @@ describe('ObjectUpdatesService', () => {
|
||||
};
|
||||
|
||||
const objectEntry = {
|
||||
fieldStates, fieldUpdates, lastModified: modDate
|
||||
fieldStates, fieldUpdates, lastModified: modDate, virtualMetadataSources: {}
|
||||
};
|
||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||
spyOn(store, 'dispatch');
|
||||
@@ -275,4 +277,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));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
@@ -8,7 +8,8 @@ import {
|
||||
Identifiable,
|
||||
OBJECT_UPDATES_TRASH_PATH,
|
||||
ObjectUpdatesEntry,
|
||||
ObjectUpdatesState, OrderPage
|
||||
ObjectUpdatesState, OrderPage,
|
||||
VirtualMetadataSource
|
||||
} from './object-updates.reducer';
|
||||
import { Observable } from 'rxjs';
|
||||
import {
|
||||
@@ -19,10 +20,11 @@ import {
|
||||
MoveFieldUpdateAction,
|
||||
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';
|
||||
import { ArrayMoveChangeAnalyzer } from '../array-move-change-analyzer.service';
|
||||
@@ -41,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
|
||||
*/
|
||||
@@ -119,20 +125,24 @@ export class ObjectUpdatesService {
|
||||
*/
|
||||
getFieldUpdates(url: string, initialFields: Identifiable[], ignoreStates?: boolean): Observable<FieldUpdates> {
|
||||
const objectUpdates = this.getObjectEntry(url);
|
||||
return objectUpdates.pipe(map((objectEntry) => {
|
||||
const fieldUpdates: FieldUpdates = {};
|
||||
if (hasValue(objectEntry)) {
|
||||
Object.keys(ignoreStates ? objectEntry.fieldUpdates : 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(ignoreStates ? objectEntry.fieldUpdates : 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;
|
||||
})
|
||||
);
|
||||
}),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -263,6 +273,34 @@ export class ObjectUpdatesService {
|
||||
this.store.dispatch(new MoveFieldUpdateAction(url, from, to, fromPage, toPage, field));
|
||||
}
|
||||
|
||||
/**
|
||||
* 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<boolean> {
|
||||
|
||||
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
|
||||
|
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1,35 +1,47 @@
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { Injectable } from '@angular/core';
|
||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
|
||||
import { compareArraysUsingIds, paginatedRelationsToItems, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||
import { AppState, keySelector } from '../../app.reducer';
|
||||
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { ReorderableRelationship } from '../../shared/form/builder/ds-dynamic-form-ui/existing-metadata-list-element/existing-metadata-list-element.component';
|
||||
import { RemoveNameVariantAction, SetNameVariantAction } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions';
|
||||
import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
|
||||
import { SearchParam } from '../cache/models/search-param.model';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||
import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
|
||||
import {
|
||||
configureRequest,
|
||||
getRemoteDataPayload,
|
||||
getResponseFromEntry,
|
||||
getSucceededRemoteData
|
||||
} from '../shared/operators';
|
||||
import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { RestResponse } from '../cache/response.models';
|
||||
import { Item } from '../shared/item.model';
|
||||
import { Relationship } from '../shared/item-relationships/relationship.model';
|
||||
import { RelationshipType } from '../shared/item-relationships/relationship-type.model';
|
||||
import { RemoteData, RemoteDataState } from './remote-data';
|
||||
import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs';
|
||||
import { PaginatedList } from './paginated-list';
|
||||
import { ItemDataService } from './item-data.service';
|
||||
import { Relationship } from '../shared/item-relationships/relationship.model';
|
||||
import { Item } from '../shared/item.model';
|
||||
import {
|
||||
compareArraysUsingIds,
|
||||
paginatedRelationsToItems,
|
||||
relationsToItems
|
||||
} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
import { DataService } from './data.service';
|
||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||
import { MemoizedSelector, select, Store } from '@ngrx/store';
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||
import { RequestService } from './request.service';
|
||||
import { Observable } from 'rxjs/internal/Observable';
|
||||
import { SearchParam } from '../cache/models/search-param.model';
|
||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||
import { AppState, keySelector } from '../../app.reducer';
|
||||
import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer';
|
||||
import {
|
||||
RemoveNameVariantAction,
|
||||
SetNameVariantAction
|
||||
} from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions';
|
||||
|
||||
const relationshipListsStateSelector = (state: AppState) => state.relationshipLists;
|
||||
|
||||
@@ -81,15 +93,22 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
* Send a delete request for a relationship by ID
|
||||
* @param id
|
||||
*/
|
||||
deleteRelationship(id: string): Observable<RestResponse> {
|
||||
deleteRelationship(id: string, copyVirtualMetadata: string): Observable<RestResponse> {
|
||||
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 +436,26 @@ export class RelationshipService extends DataService<Relationship> {
|
||||
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<void> {
|
||||
return this.findById(uuid).pipe(
|
||||
getSucceededRemoteData(),
|
||||
switchMap((rd: RemoteData<Relationship>) =>
|
||||
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);
|
||||
}),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
@@ -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();
|
||||
}
|
||||
|
@@ -1325,7 +1325,7 @@ export const mockUploadConfigResponse = {
|
||||
},
|
||||
self: 'https://rest.api/dspace-spring-rest/api/config/submissionforms/bitstream-metadata'
|
||||
},
|
||||
required: false,
|
||||
required: true,
|
||||
maxSize: 536870912,
|
||||
name: 'upload',
|
||||
type: 'submissionupload',
|
||||
@@ -1336,6 +1336,10 @@ export const mockUploadConfigResponse = {
|
||||
self: 'https://rest.api/dspace-spring-rest/api/config/submissionuploads/upload'
|
||||
};
|
||||
|
||||
// Clone the object and change one property
|
||||
export const mockUploadConfigResponseNotRequired = JSON.parse(JSON.stringify(mockUploadConfigResponse));
|
||||
mockUploadConfigResponseNotRequired.required = false;
|
||||
|
||||
export const mockAccessConditionOptions = [
|
||||
{
|
||||
name: 'openaccess',
|
||||
|
@@ -5,6 +5,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
||||
import { of as observableOf } from 'rxjs';
|
||||
|
||||
import { createSuccessfulRemoteDataObject$, createTestComponent } from '../../../shared/testing/utils';
|
||||
import { SubmissionObjectState } from '../../objects/submission-objects.reducer';
|
||||
import { SubmissionService } from '../../submission.service';
|
||||
import { SubmissionServiceStub } from '../../../shared/testing/submission-service-stub';
|
||||
import { SectionsService } from '../sections.service';
|
||||
@@ -18,7 +19,7 @@ import {
|
||||
mockSubmissionId,
|
||||
mockSubmissionState,
|
||||
mockUploadConfigResponse,
|
||||
mockUploadFiles
|
||||
mockUploadConfigResponseNotRequired, mockUploadFiles,
|
||||
} from '../../../shared/mocks/mock-submission';
|
||||
import { BrowserModule } from '@angular/platform-browser';
|
||||
import { CommonModule } from '@angular/common';
|
||||
@@ -31,7 +32,6 @@ import { cold, hot } from 'jasmine-marbles';
|
||||
import { Collection } from '../../../core/shared/collection.model';
|
||||
import { ResourcePolicy } from '../../../core/shared/resource-policy.model';
|
||||
import { ResourcePolicyService } from '../../../core/data/resource-policy.service';
|
||||
import { RemoteData } from '../../../core/data/remote-data';
|
||||
import { ConfigData } from '../../../core/config/config-data';
|
||||
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||
import { Group } from '../../../core/eperson/models/group.model';
|
||||
@@ -65,17 +65,7 @@ function getMockResourcePolicyService(): ResourcePolicyService {
|
||||
});
|
||||
}
|
||||
|
||||
const sectionObject: SectionDataObject = {
|
||||
config: 'https://dspace7.4science.it/or2018/api/config/submissionforms/upload',
|
||||
mandatory: true,
|
||||
data: {
|
||||
files: []
|
||||
},
|
||||
errors: [],
|
||||
header: 'submit.progressbar.describe.upload',
|
||||
id: 'upload',
|
||||
sectionType: SectionsType.Upload
|
||||
};
|
||||
let sectionObject: SectionDataObject;
|
||||
|
||||
describe('SubmissionSectionUploadComponent test suite', () => {
|
||||
|
||||
@@ -90,30 +80,48 @@ describe('SubmissionSectionUploadComponent test suite', () => {
|
||||
let uploadsConfigService: any;
|
||||
let bitstreamService: any;
|
||||
|
||||
const submissionId = mockSubmissionId;
|
||||
const collectionId = mockSubmissionCollectionId;
|
||||
const submissionState = Object.assign({}, mockSubmissionState[mockSubmissionId]);
|
||||
const mockCollection = Object.assign(new Collection(), {
|
||||
name: 'Community 1-Collection 1',
|
||||
id: collectionId,
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Community 1-Collection 1'
|
||||
}],
|
||||
_links: {
|
||||
defaultAccessConditions: collectionId + '/defaultAccessConditions'
|
||||
}
|
||||
});
|
||||
const mockDefaultAccessCondition = Object.assign(new ResourcePolicy(), {
|
||||
name: null,
|
||||
groupUUID: '11cc35e5-a11d-4b64-b5b9-0052a5d15509',
|
||||
id: 20,
|
||||
uuid: 'resource-policy-20'
|
||||
});
|
||||
let submissionId: string;
|
||||
let collectionId: string;
|
||||
let submissionState: SubmissionObjectState;
|
||||
let mockCollection: Collection;
|
||||
let mockDefaultAccessCondition: ResourcePolicy;
|
||||
|
||||
beforeEach(async(() => {
|
||||
sectionObject = {
|
||||
config: 'https://dspace7.4science.it/or2018/api/config/submissionforms/upload',
|
||||
mandatory: true,
|
||||
data: {
|
||||
files: []
|
||||
},
|
||||
errors: [],
|
||||
header: 'submit.progressbar.describe.upload',
|
||||
id: 'upload',
|
||||
sectionType: SectionsType.Upload
|
||||
};
|
||||
submissionId = mockSubmissionId;
|
||||
collectionId = mockSubmissionCollectionId;
|
||||
submissionState = Object.assign({}, mockSubmissionState[mockSubmissionId]) as any;
|
||||
mockCollection = Object.assign(new Collection(), {
|
||||
name: 'Community 1-Collection 1',
|
||||
id: collectionId,
|
||||
metadata: [
|
||||
{
|
||||
key: 'dc.title',
|
||||
language: 'en_US',
|
||||
value: 'Community 1-Collection 1'
|
||||
}],
|
||||
_links: {
|
||||
defaultAccessConditions: collectionId + '/defaultAccessConditions'
|
||||
}
|
||||
});
|
||||
|
||||
mockDefaultAccessCondition = Object.assign(new ResourcePolicy(), {
|
||||
name: null,
|
||||
groupUUID: '11cc35e5-a11d-4b64-b5b9-0052a5d15509',
|
||||
id: 20,
|
||||
uuid: 'resource-policy-20'
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
BrowserModule,
|
||||
@@ -206,7 +214,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
|
||||
|
||||
comp.onSectionInit();
|
||||
|
||||
const expectedGroupsMap = new Map([
|
||||
const expectedGroupsMap = new Map([
|
||||
[mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
|
||||
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
|
||||
]);
|
||||
@@ -215,6 +223,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
|
||||
expect(comp.collectionName).toBe(mockCollection.name);
|
||||
expect(comp.availableAccessConditionOptions.length).toBe(4);
|
||||
expect(comp.availableAccessConditionOptions).toEqual(mockUploadConfigResponse.accessConditionOptions as any);
|
||||
expect(comp.required$.getValue()).toBe(true);
|
||||
expect(compAsAny.subs.length).toBe(2);
|
||||
expect(compAsAny.availableGroups.size).toBe(2);
|
||||
expect(compAsAny.availableGroups).toEqual(expectedGroupsMap);
|
||||
@@ -245,7 +254,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
|
||||
|
||||
comp.onSectionInit();
|
||||
|
||||
const expectedGroupsMap = new Map([
|
||||
const expectedGroupsMap = new Map([
|
||||
[mockUploadConfigResponse.accessConditionOptions[1].name, [mockGroup as any]],
|
||||
[mockUploadConfigResponse.accessConditionOptions[2].name, [mockGroup as any]],
|
||||
]);
|
||||
@@ -254,6 +263,7 @@ describe('SubmissionSectionUploadComponent test suite', () => {
|
||||
expect(comp.collectionName).toBe(mockCollection.name);
|
||||
expect(comp.availableAccessConditionOptions.length).toBe(4);
|
||||
expect(comp.availableAccessConditionOptions).toEqual(mockUploadConfigResponse.accessConditionOptions as any);
|
||||
expect(comp.required$.getValue()).toBe(true);
|
||||
expect(compAsAny.subs.length).toBe(2);
|
||||
expect(compAsAny.availableGroups.size).toBe(2);
|
||||
expect(compAsAny.availableGroups).toEqual(expectedGroupsMap);
|
||||
@@ -263,17 +273,67 @@ describe('SubmissionSectionUploadComponent test suite', () => {
|
||||
|
||||
});
|
||||
|
||||
it('should the properly section status', () => {
|
||||
bitstreamService.getUploadedFileList.and.returnValue(hot('-a-b', {
|
||||
it('should properly read the section status when required is true', () => {
|
||||
submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState));
|
||||
|
||||
collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection));
|
||||
|
||||
resourcePolicyService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(mockDefaultAccessCondition));
|
||||
|
||||
uploadsConfigService.getConfigByHref.and.returnValue(observableOf(
|
||||
new ConfigData(new PageInfo(), mockUploadConfigResponse as any)
|
||||
));
|
||||
|
||||
groupService.findById.and.returnValues(
|
||||
createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)),
|
||||
createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup))
|
||||
);
|
||||
|
||||
bitstreamService.getUploadedFileList.and.returnValue(cold('-a-b', {
|
||||
a: [],
|
||||
b: mockUploadFiles
|
||||
}));
|
||||
|
||||
comp.onSectionInit();
|
||||
|
||||
expect(comp.required$.getValue()).toBe(true);
|
||||
|
||||
expect(compAsAny.getSectionStatus()).toBeObservable(cold('-c-d', {
|
||||
c: false,
|
||||
d: true
|
||||
}));
|
||||
});
|
||||
|
||||
it('should properly read the section status when required is false', () => {
|
||||
submissionServiceStub.getSubmissionObject.and.returnValue(observableOf(submissionState));
|
||||
|
||||
collectionDataService.findById.and.returnValue(createSuccessfulRemoteDataObject$(mockCollection));
|
||||
|
||||
resourcePolicyService.findByHref.and.returnValue(createSuccessfulRemoteDataObject$(mockDefaultAccessCondition));
|
||||
|
||||
uploadsConfigService.getConfigByHref.and.returnValue(observableOf(
|
||||
new ConfigData(new PageInfo(), mockUploadConfigResponseNotRequired as any)
|
||||
));
|
||||
|
||||
groupService.findById.and.returnValues(
|
||||
createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup)),
|
||||
createSuccessfulRemoteDataObject$(Object.assign(new Group(), mockGroup))
|
||||
);
|
||||
|
||||
bitstreamService.getUploadedFileList.and.returnValue(cold('-a-b', {
|
||||
a: [],
|
||||
b: mockUploadFiles
|
||||
}));
|
||||
|
||||
comp.onSectionInit();
|
||||
|
||||
expect(comp.required$.getValue()).toBe(false);
|
||||
|
||||
expect(compAsAny.getSectionStatus()).toBeObservable(cold('-c-d', {
|
||||
c: true,
|
||||
d: true
|
||||
}));
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
|
@@ -1,6 +1,6 @@
|
||||
import { ChangeDetectorRef, Component, Inject } from '@angular/core';
|
||||
|
||||
import { combineLatest, Observable, Subscription } from 'rxjs';
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subscription} from 'rxjs';
|
||||
import { distinctUntilChanged, filter, find, flatMap, map, reduce, take, tap } from 'rxjs/operators';
|
||||
|
||||
import { SectionModelComponent } from '../models/section.model';
|
||||
@@ -104,6 +104,12 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
|
||||
*/
|
||||
protected availableGroups: Map<string, Group[]>; // Groups for any policy
|
||||
|
||||
/**
|
||||
* Is the upload required
|
||||
* @type {boolean}
|
||||
*/
|
||||
public required$ = new BehaviorSubject<boolean>(true);
|
||||
|
||||
/**
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
@@ -172,6 +178,7 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
|
||||
}),
|
||||
flatMap(() => config$),
|
||||
flatMap((config: SubmissionUploadsModel) => {
|
||||
this.required$.next(config.required);
|
||||
this.availableAccessConditionOptions = isNotEmpty(config.accessConditionOptions) ? config.accessConditionOptions : [];
|
||||
|
||||
this.collectionPolicyType = this.availableAccessConditionOptions.length > 0
|
||||
@@ -221,7 +228,7 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
|
||||
}),
|
||||
|
||||
// retrieve submission's bitstreams from state
|
||||
combineLatest(this.configMetadataForm$,
|
||||
observableCombineLatest(this.configMetadataForm$,
|
||||
this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id)).pipe(
|
||||
filter(([configMetadataForm, fileList]: [SubmissionFormsModel, any[]]) => {
|
||||
return isNotEmpty(configMetadataForm) && isNotUndefined(fileList)
|
||||
@@ -273,8 +280,13 @@ export class SubmissionSectionUploadComponent extends SectionModelComponent {
|
||||
* the section status
|
||||
*/
|
||||
protected getSectionStatus(): Observable<boolean> {
|
||||
return this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id).pipe(
|
||||
map((fileList: any[]) => (isNotUndefined(fileList) && fileList.length > 0)));
|
||||
// if not mandatory, always true
|
||||
// if mandatory, at least one file is required
|
||||
return observableCombineLatest(this.required$,
|
||||
this.bitstreamService.getUploadedFileList(this.submissionId, this.sectionData.id),
|
||||
(required,fileList: any[]) => {
|
||||
return (!required || (isNotUndefined(fileList) && fileList.length > 0));
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -50,7 +50,10 @@ a<div class="top-item-page">
|
||||
[fields]="['dc.identifier.citation']"
|
||||
[label]="'item.page.citation'">
|
||||
</ds-generic-item-page-field>
|
||||
<ds-item-page-uri-field [item]="object"></ds-item-page-uri-field>
|
||||
<<ds-item-page-uri-field [item]="object"
|
||||
[fields]="['dc.identifier.uri']"
|
||||
[label]="'item.page.uri'">
|
||||
</ds-item-page-uri-field>
|
||||
<ds-item-page-collections [item]="object"></ds-item-page-collections>
|
||||
</div>
|
||||
</div>
|
||||
|
Reference in New Issue
Block a user