mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge pull request #3060 from alexandrevryghem/w2p-113560_edit-item-add-relationships-one-by-one_contribute-main
Fixed creation & deletion of relationships not working correctly & added same entity type relationship support
This commit is contained in:
@@ -32,6 +32,7 @@ import {
|
|||||||
Observable,
|
Observable,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
|
delay,
|
||||||
distinctUntilChanged,
|
distinctUntilChanged,
|
||||||
take,
|
take,
|
||||||
withLatestFrom,
|
withLatestFrom,
|
||||||
@@ -136,7 +137,10 @@ export class AppComponent implements OnInit, AfterViewInit {
|
|||||||
}
|
}
|
||||||
|
|
||||||
ngAfterViewInit() {
|
ngAfterViewInit() {
|
||||||
this.router.events.subscribe((event) => {
|
this.router.events.pipe(
|
||||||
|
// delay(0) to prevent "Expression has changed after it was checked" errors
|
||||||
|
delay(0),
|
||||||
|
).subscribe((event) => {
|
||||||
if (event instanceof NavigationStart) {
|
if (event instanceof NavigationStart) {
|
||||||
distinctNext(this.isRouteLoading$, true);
|
distinctNext(this.isRouteLoading$, true);
|
||||||
} else if (
|
} else if (
|
||||||
|
@@ -61,6 +61,8 @@ export interface VirtualMetadataSource {
|
|||||||
|
|
||||||
export interface RelationshipIdentifiable extends Identifiable {
|
export interface RelationshipIdentifiable extends Identifiable {
|
||||||
nameVariant?: string;
|
nameVariant?: string;
|
||||||
|
originalItem: Item;
|
||||||
|
originalIsLeft: boolean
|
||||||
relatedItem: Item;
|
relatedItem: Item;
|
||||||
relationship: Relationship;
|
relationship: Relationship;
|
||||||
type: RelationshipType;
|
type: RelationshipType;
|
||||||
|
@@ -15,6 +15,7 @@ import {
|
|||||||
filter,
|
filter,
|
||||||
map,
|
map,
|
||||||
switchMap,
|
switchMap,
|
||||||
|
take,
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
@@ -212,8 +213,14 @@ export class ObjectUpdatesService {
|
|||||||
* @param url The page's URL for which the changes are saved
|
* @param url The page's URL for which the changes are saved
|
||||||
* @param field An updated field for the page's object
|
* @param field An updated field for the page's object
|
||||||
*/
|
*/
|
||||||
saveAddFieldUpdate(url: string, field: Identifiable) {
|
saveAddFieldUpdate(url: string, field: Identifiable): Observable<boolean> {
|
||||||
|
const update$: Observable<boolean> = this.getFieldUpdatesExclusive(url, [field]).pipe(
|
||||||
|
filter((fieldUpdates: FieldUpdates) => fieldUpdates[field.uuid].changeType === FieldChangeType.ADD),
|
||||||
|
take(1),
|
||||||
|
map(() => true),
|
||||||
|
);
|
||||||
this.saveFieldUpdate(url, field, FieldChangeType.ADD);
|
this.saveFieldUpdate(url, field, FieldChangeType.ADD);
|
||||||
|
return update$;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -221,8 +228,14 @@ export class ObjectUpdatesService {
|
|||||||
* @param url The page's URL for which the changes are saved
|
* @param url The page's URL for which the changes are saved
|
||||||
* @param field An updated field for the page's object
|
* @param field An updated field for the page's object
|
||||||
*/
|
*/
|
||||||
saveRemoveFieldUpdate(url: string, field: Identifiable) {
|
saveRemoveFieldUpdate(url: string, field: Identifiable): Observable<boolean> {
|
||||||
|
const update$: Observable<boolean> = this.getFieldUpdatesExclusive(url, [field]).pipe(
|
||||||
|
filter((fieldUpdates: FieldUpdates) => fieldUpdates[field.uuid].changeType === FieldChangeType.REMOVE),
|
||||||
|
take(1),
|
||||||
|
map(() => true),
|
||||||
|
);
|
||||||
this.saveFieldUpdate(url, field, FieldChangeType.REMOVE);
|
this.saveFieldUpdate(url, field, FieldChangeType.REMOVE);
|
||||||
|
return update$;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -155,8 +155,11 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
* @param id the ID of the relationship to delete
|
* @param id the ID of the relationship to delete
|
||||||
* @param copyVirtualMetadata whether to copy this relationship's virtual metadata to the related Items
|
* @param copyVirtualMetadata whether to copy this relationship's virtual metadata to the related Items
|
||||||
* accepted values: none, all, left, right, configured
|
* accepted values: none, all, left, right, configured
|
||||||
|
* @param shouldRefresh refresh the cache for the items in the relationship after creating
|
||||||
|
* it. Disable this if you want to add relationships in bulk, and
|
||||||
|
* want to refresh the cachemanually at the end
|
||||||
*/
|
*/
|
||||||
deleteRelationship(id: string, copyVirtualMetadata: string): Observable<RemoteData<NoContent>> {
|
deleteRelationship(id: string, copyVirtualMetadata: string, shouldRefresh = true): Observable<RemoteData<NoContent>> {
|
||||||
return this.getRelationshipEndpoint(id).pipe(
|
return this.getRelationshipEndpoint(id).pipe(
|
||||||
isNotEmptyOperator(),
|
isNotEmptyOperator(),
|
||||||
take(1),
|
take(1),
|
||||||
@@ -167,7 +170,11 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
sendRequest(this.requestService),
|
sendRequest(this.requestService),
|
||||||
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
|
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
tap(() => this.refreshRelationshipItemsInCacheByRelationship(id)),
|
tap(() => {
|
||||||
|
if (shouldRefresh) {
|
||||||
|
this.refreshRelationshipItemsInCacheByRelationship(id);
|
||||||
|
}
|
||||||
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,8 +185,11 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
* @param item2 The second item of the relationship
|
* @param item2 The second item of the relationship
|
||||||
* @param leftwardValue The leftward value of the relationship
|
* @param leftwardValue The leftward value of the relationship
|
||||||
* @param rightwardValue The rightward value of the relationship
|
* @param rightwardValue The rightward value of the relationship
|
||||||
|
* @param shouldRefresh refresh the cache for the items in the relationship after creating it.
|
||||||
|
* Disable this if you want to add relationships in bulk, and want to refresh
|
||||||
|
* the cache manually at the end
|
||||||
*/
|
*/
|
||||||
addRelationship(typeId: string, item1: Item, item2: Item, leftwardValue?: string, rightwardValue?: string): Observable<RemoteData<Relationship>> {
|
addRelationship(typeId: string, item1: Item, item2: Item, leftwardValue?: string, rightwardValue?: string, shouldRefresh = true): Observable<RemoteData<Relationship>> {
|
||||||
const options: HttpOptions = Object.create({});
|
const options: HttpOptions = Object.create({});
|
||||||
let headers = new HttpHeaders();
|
let headers = new HttpHeaders();
|
||||||
headers = headers.append('Content-Type', 'text/uri-list');
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
@@ -194,8 +204,12 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
sendRequest(this.requestService),
|
sendRequest(this.requestService),
|
||||||
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
|
switchMap((restRequest: RestRequest) => this.rdbService.buildFromRequestUUID(restRequest.uuid)),
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
tap(() => this.refreshRelationshipItemsInCache(item1)),
|
tap(() => {
|
||||||
tap(() => this.refreshRelationshipItemsInCache(item2)),
|
if (shouldRefresh) {
|
||||||
|
this.refreshRelationshipItemsInCache(item1);
|
||||||
|
this.refreshRelationshipItemsInCache(item2);
|
||||||
|
}
|
||||||
|
}),
|
||||||
) as Observable<RemoteData<Relationship>>;
|
) as Observable<RemoteData<Relationship>>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,7 +237,7 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
* Method to remove an item that's part of a relationship from the cache
|
* Method to remove an item that's part of a relationship from the cache
|
||||||
* @param item The item to remove from the cache
|
* @param item The item to remove from the cache
|
||||||
*/
|
*/
|
||||||
public refreshRelationshipItemsInCache(item) {
|
public refreshRelationshipItemsInCache(item: Item): void {
|
||||||
this.objectCache.remove(item._links.self.href);
|
this.objectCache.remove(item._links.self.href);
|
||||||
this.requestService.removeByHrefSubstring(item.uuid);
|
this.requestService.removeByHrefSubstring(item.uuid);
|
||||||
observableCombineLatest([
|
observableCombineLatest([
|
||||||
@@ -336,7 +350,19 @@ export class RelationshipDataService extends IdentifiableDataService<Relationshi
|
|||||||
} else {
|
} else {
|
||||||
findListOptions.searchParams = searchParams;
|
findListOptions.searchParams = searchParams;
|
||||||
}
|
}
|
||||||
return this.searchBy('byLabel', findListOptions, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow);
|
|
||||||
|
// always set reRequestOnStale to false here, so it doesn't happen automatically in BaseDataService
|
||||||
|
const result$ = this.searchBy('byLabel', findListOptions, useCachedVersionIfAvailable, false, ...linksToFollow);
|
||||||
|
|
||||||
|
// add this result as a dependency of the item, meaning that if the item is invalided, this
|
||||||
|
// result will be as well
|
||||||
|
this.addDependency(result$, item._links.self.href);
|
||||||
|
|
||||||
|
// do the reRequestOnStale call here, to ensure any re-requests also get added as dependencies
|
||||||
|
return result$.pipe(
|
||||||
|
this.reRequestStaleRemoteData(reRequestOnStale, () =>
|
||||||
|
this.getItemRelationshipsByLabel(item, label, options, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow)),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -55,6 +55,10 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
|||||||
*/
|
*/
|
||||||
updates$: Observable<FieldUpdates>;
|
updates$: Observable<FieldUpdates>;
|
||||||
|
|
||||||
|
hasChanges$: Observable<boolean>;
|
||||||
|
|
||||||
|
isReinstatable$: Observable<boolean>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Route to the item's page
|
* Route to the item's page
|
||||||
*/
|
*/
|
||||||
@@ -101,10 +105,9 @@ export class AbstractItemUpdateComponent extends AbstractTrackableComponent impl
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.discardTimeOut = environment.item.edit.undoTimeout;
|
this.discardTimeOut = environment.item.edit.undoTimeout;
|
||||||
this.url = this.router.url;
|
this.url = this.router.url.split('?')[0];
|
||||||
if (this.url.indexOf('?') > 0) {
|
this.hasChanges$ = this.hasChanges();
|
||||||
this.url = this.url.substr(0, this.url.indexOf('?'));
|
this.isReinstatable$ = this.isReinstatable();
|
||||||
}
|
|
||||||
this.hasChanges().pipe(first()).subscribe((hasChanges) => {
|
this.hasChanges().pipe(first()).subscribe((hasChanges) => {
|
||||||
if (!hasChanges) {
|
if (!hasChanges) {
|
||||||
this.initializeOriginalFields();
|
this.initializeOriginalFields();
|
||||||
|
@@ -6,21 +6,21 @@
|
|||||||
class="fas fa-upload"></i>
|
class="fas fa-upload"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.upload-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.upload-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
<button class="btn btn-warning" *ngIf="isReinstatable$ | async"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.reinstate-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.reinstate-button' | translate"
|
||||||
(click)="reinstate()"><i
|
(click)="reinstate()"><i
|
||||||
class="fas fa-undo-alt"></i>
|
class="fas fa-undo-alt"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" [disabled]="(hasChanges() | async) !== true || submitting"
|
<button class="btn btn-primary" [disabled]="(hasChanges$ | async) !== true || submitting"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
|
||||||
(click)="submit()"><i
|
(click)="submit()"><i
|
||||||
class="fas fa-save"></i>
|
class="fas fa-save"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.save-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.save-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" *ngIf="(isReinstatable() | async) !== true"
|
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
|
||||||
[disabled]="(hasChanges() | async) !== true || submitting"
|
[disabled]="(hasChanges$ | async) !== true || submitting"
|
||||||
(click)="discard()"><i
|
(click)="discard()"><i
|
||||||
class="fas fa-times"></i>
|
class="fas fa-times"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
||||||
@@ -52,21 +52,21 @@
|
|||||||
|
|
||||||
<div class="button-row bottom">
|
<div class="button-row bottom">
|
||||||
<div class="mt-4 float-right space-children-mr ml-gap">
|
<div class="mt-4 float-right space-children-mr ml-gap">
|
||||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
<button class="btn btn-warning" *ngIf="isReinstatable$ | async"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.reinstate-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.reinstate-button' | translate"
|
||||||
(click)="reinstate()"><i
|
(click)="reinstate()"><i
|
||||||
class="fas fa-undo-alt"></i>
|
class="fas fa-undo-alt"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.reinstate-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-primary" [disabled]="(hasChanges() | async) !== true || submitting"
|
<button class="btn btn-primary" [disabled]="(hasChanges$ | async) !== true || submitting"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.save-button' | translate"
|
||||||
(click)="submit()"><i
|
(click)="submit()"><i
|
||||||
class="fas fa-save"></i>
|
class="fas fa-save"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.save-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.save-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
<button class="btn btn-danger" *ngIf="(isReinstatable() | async) !== true"
|
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||||
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
|
[attr.aria-label]="'item.edit.bitstreams.discard-button' | translate"
|
||||||
[disabled]="(hasChanges() | async) !== true || submitting"
|
[disabled]="(hasChanges$ | async) !== true || submitting"
|
||||||
(click)="discard()"><i
|
(click)="discard()"><i
|
||||||
class="fas fa-times"></i>
|
class="fas fa-times"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.bitstreams.discard-button" | translate}}</span>
|
||||||
|
@@ -0,0 +1,301 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { v4 as uuidv4 } from 'uuid';
|
||||||
|
|
||||||
|
import { EntityTypeDataService } from '../../../core/data/entity-type-data.service';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
|
||||||
|
import { FieldUpdate } from '../../../core/data/object-updates/field-update.model';
|
||||||
|
import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model';
|
||||||
|
import {
|
||||||
|
DeleteRelationship,
|
||||||
|
RelationshipIdentifiable,
|
||||||
|
} from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
||||||
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import {
|
||||||
|
createFailedRemoteDataObject,
|
||||||
|
createSuccessfulRemoteDataObject,
|
||||||
|
createSuccessfulRemoteDataObject$,
|
||||||
|
} from '../../../shared/remote-data.utils';
|
||||||
|
import { EntityTypeDataServiceStub } from '../../../shared/testing/entity-type-data.service.stub';
|
||||||
|
import { ItemDataServiceStub } from '../../../shared/testing/item-data.service.stub';
|
||||||
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
|
import { ObjectUpdatesServiceStub } from '../../../shared/testing/object-updates.service.stub';
|
||||||
|
import { RelationshipDataServiceStub } from '../../../shared/testing/relationship-data.service.stub';
|
||||||
|
import { EditItemRelationshipsService } from './edit-item-relationships.service';
|
||||||
|
|
||||||
|
describe('EditItemRelationshipsService', () => {
|
||||||
|
let service: EditItemRelationshipsService;
|
||||||
|
|
||||||
|
let itemService: ItemDataServiceStub;
|
||||||
|
let objectUpdatesService: ObjectUpdatesServiceStub;
|
||||||
|
let notificationsService: NotificationsServiceStub;
|
||||||
|
let relationshipService: RelationshipDataServiceStub;
|
||||||
|
let entityTypeDataService: EntityTypeDataServiceStub;
|
||||||
|
|
||||||
|
let currentItem: Item;
|
||||||
|
|
||||||
|
let relationshipItem1: Item;
|
||||||
|
let relationshipIdentifiable1: RelationshipIdentifiable;
|
||||||
|
let relationship1: Relationship;
|
||||||
|
|
||||||
|
let relationshipItem2: Item;
|
||||||
|
let relationshipIdentifiable2: RelationshipIdentifiable;
|
||||||
|
let relationship2: Relationship;
|
||||||
|
|
||||||
|
let orgUnitType: ItemType;
|
||||||
|
let orgUnitToOrgUnitType: RelationshipType;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
itemService = new ItemDataServiceStub();
|
||||||
|
objectUpdatesService = new ObjectUpdatesServiceStub();
|
||||||
|
notificationsService = new NotificationsServiceStub();
|
||||||
|
relationshipService = new RelationshipDataServiceStub();
|
||||||
|
entityTypeDataService = new EntityTypeDataServiceStub();
|
||||||
|
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{ provide: ItemDataService, useValue: itemService },
|
||||||
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
|
{ provide: NotificationsService, useValue: notificationsService },
|
||||||
|
{ provide: RelationshipDataService, useValue: relationshipService },
|
||||||
|
{ provide: EntityTypeDataService, useValue: entityTypeDataService },
|
||||||
|
],
|
||||||
|
});
|
||||||
|
service = TestBed.inject(EditItemRelationshipsService);
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
currentItem = Object.assign(new Item(), {
|
||||||
|
uuid: uuidv4(),
|
||||||
|
metadata: {
|
||||||
|
'dspace.entity.type': 'OrgUnit',
|
||||||
|
},
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'selfLink1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
relationshipItem1 = Object.assign(new Item(), {
|
||||||
|
uuid: uuidv4(),
|
||||||
|
metadata: {
|
||||||
|
'dspace.entity.type': 'OrgUnit',
|
||||||
|
},
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'selfLink2',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
relationshipIdentifiable1 = {
|
||||||
|
originalItem: currentItem,
|
||||||
|
relatedItem: relationshipItem1,
|
||||||
|
type: orgUnitToOrgUnitType,
|
||||||
|
uuid: `1-${relationshipItem1.uuid}`,
|
||||||
|
} as RelationshipIdentifiable;
|
||||||
|
relationship1 = Object.assign(new Relationship(), {
|
||||||
|
_links: {
|
||||||
|
leftItem: currentItem._links.self,
|
||||||
|
rightItem: relationshipItem1._links.self,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
relationshipItem2 = Object.assign(new Item(), {
|
||||||
|
uuid: uuidv4(),
|
||||||
|
metadata: {
|
||||||
|
'dspace.entity.type': 'OrgUnit',
|
||||||
|
},
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'selfLink3',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
relationshipIdentifiable2 = {
|
||||||
|
originalItem: currentItem,
|
||||||
|
relatedItem: relationshipItem2,
|
||||||
|
type: orgUnitToOrgUnitType,
|
||||||
|
uuid: `1-${relationshipItem2.uuid}`,
|
||||||
|
} as RelationshipIdentifiable;
|
||||||
|
relationship2 = Object.assign(new Relationship(), {
|
||||||
|
_links: {
|
||||||
|
leftItem: currentItem._links.self,
|
||||||
|
rightItem: relationshipItem2._links.self,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
orgUnitType = Object.assign(new ItemType(), {
|
||||||
|
id: '2',
|
||||||
|
label: 'OrgUnit',
|
||||||
|
});
|
||||||
|
orgUnitToOrgUnitType = Object.assign(new RelationshipType(), {
|
||||||
|
id: '1',
|
||||||
|
leftMaxCardinality: null,
|
||||||
|
leftMinCardinality: 0,
|
||||||
|
leftType: createSuccessfulRemoteDataObject$(orgUnitType),
|
||||||
|
leftwardType: 'isOrgUnitOfOrgUnit',
|
||||||
|
rightMaxCardinality: null,
|
||||||
|
rightMinCardinality: 0,
|
||||||
|
rightType: createSuccessfulRemoteDataObject$(orgUnitType),
|
||||||
|
rightwardType: 'isOrgUnitOfOrgUnit',
|
||||||
|
uuid: 'relationshiptype-1',
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('submit', () => {
|
||||||
|
let fieldUpdateAddRelationship1: FieldUpdate;
|
||||||
|
let fieldUpdateRemoveRelationship2: FieldUpdate;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fieldUpdateAddRelationship1 = {
|
||||||
|
changeType: FieldChangeType.ADD,
|
||||||
|
field: relationshipIdentifiable1,
|
||||||
|
};
|
||||||
|
fieldUpdateRemoveRelationship2 = {
|
||||||
|
changeType: FieldChangeType.REMOVE,
|
||||||
|
field: relationshipIdentifiable2,
|
||||||
|
};
|
||||||
|
|
||||||
|
spyOn(service, 'addRelationship').withArgs(relationshipIdentifiable1).and.returnValue(createSuccessfulRemoteDataObject$(relationship1));
|
||||||
|
spyOn(service, 'deleteRelationship').withArgs(relationshipIdentifiable2 as DeleteRelationship).and.returnValue(createSuccessfulRemoteDataObject$({}));
|
||||||
|
spyOn(itemService, 'invalidateByHref').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should support performing multiple relationships manipulations in one submit() call', () => {
|
||||||
|
spyOn(objectUpdatesService, 'getFieldUpdates').and.returnValue(observableOf({
|
||||||
|
[`1-${relationshipItem1.uuid}`]: fieldUpdateAddRelationship1,
|
||||||
|
[`1-${relationshipItem2.uuid}`]: fieldUpdateRemoveRelationship2,
|
||||||
|
} as FieldUpdates));
|
||||||
|
service.submit(currentItem, `/entities/orgunit/${currentItem.uuid}/edit/relationships`);
|
||||||
|
|
||||||
|
expect(service.addRelationship).toHaveBeenCalledWith(relationshipIdentifiable1);
|
||||||
|
expect(service.deleteRelationship).toHaveBeenCalledWith(relationshipIdentifiable2 as DeleteRelationship);
|
||||||
|
|
||||||
|
expect(itemService.invalidateByHref).toHaveBeenCalledWith(currentItem.self);
|
||||||
|
expect(itemService.invalidateByHref).toHaveBeenCalledWith(relationshipItem1.self);
|
||||||
|
|
||||||
|
expect(notificationsService.success).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('deleteRelationship', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(relationshipService, 'deleteRelationship').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass "all" as copyVirtualMetadata when the user want to keep the data on both sides', () => {
|
||||||
|
service.deleteRelationship({
|
||||||
|
uuid: relationshipItem1.uuid,
|
||||||
|
keepLeftVirtualMetadata: true,
|
||||||
|
keepRightVirtualMetadata: true,
|
||||||
|
} as DeleteRelationship);
|
||||||
|
|
||||||
|
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'all', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass "left" as copyVirtualMetadata when the user only want to keep the data on the left side', () => {
|
||||||
|
service.deleteRelationship({
|
||||||
|
uuid: relationshipItem1.uuid,
|
||||||
|
keepLeftVirtualMetadata: true,
|
||||||
|
keepRightVirtualMetadata: false,
|
||||||
|
} as DeleteRelationship);
|
||||||
|
|
||||||
|
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'left', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass "right" as copyVirtualMetadata when the user only want to keep the data on the right side', () => {
|
||||||
|
service.deleteRelationship({
|
||||||
|
uuid: relationshipItem1.uuid,
|
||||||
|
keepLeftVirtualMetadata: false,
|
||||||
|
keepRightVirtualMetadata: true,
|
||||||
|
} as DeleteRelationship);
|
||||||
|
|
||||||
|
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'right', false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should pass "none" as copyVirtualMetadata when the user doesn\'t want to keep the virtual metadata', () => {
|
||||||
|
service.deleteRelationship({
|
||||||
|
uuid: relationshipItem1.uuid,
|
||||||
|
keepLeftVirtualMetadata: false,
|
||||||
|
keepRightVirtualMetadata: false,
|
||||||
|
} as DeleteRelationship);
|
||||||
|
|
||||||
|
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationshipItem1.uuid, 'none', false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('addRelationship', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(relationshipService, 'addRelationship').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the addRelationship from relationshipService correctly when original item is on the right', () => {
|
||||||
|
service.addRelationship({
|
||||||
|
originalItem: currentItem,
|
||||||
|
originalIsLeft: false,
|
||||||
|
relatedItem: relationshipItem1,
|
||||||
|
type: orgUnitToOrgUnitType,
|
||||||
|
uuid: `1-${relationshipItem1.uuid}`,
|
||||||
|
} as RelationshipIdentifiable);
|
||||||
|
expect(relationshipService.addRelationship).toHaveBeenCalledWith(orgUnitToOrgUnitType.id, relationshipItem1, currentItem, undefined, null, false);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should call the addRelationship from relationshipService correctly when original item is on the left', () => {
|
||||||
|
service.addRelationship({
|
||||||
|
originalItem: currentItem,
|
||||||
|
originalIsLeft: true,
|
||||||
|
relatedItem: relationshipItem1,
|
||||||
|
type: orgUnitToOrgUnitType,
|
||||||
|
uuid: `1-${relationshipItem1.uuid}`,
|
||||||
|
} as RelationshipIdentifiable);
|
||||||
|
|
||||||
|
expect(relationshipService.addRelationship).toHaveBeenCalledWith(orgUnitToOrgUnitType.id, currentItem, relationshipItem1, null, undefined, false);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('displayNotifications', () => {
|
||||||
|
it('should show one success notification when multiple requests succeeded', () => {
|
||||||
|
service.displayNotifications([
|
||||||
|
createSuccessfulRemoteDataObject({}),
|
||||||
|
createSuccessfulRemoteDataObject({}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(notificationsService.success).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show one success notification even when some requests failed', () => {
|
||||||
|
service.displayNotifications([
|
||||||
|
createSuccessfulRemoteDataObject({}),
|
||||||
|
createFailedRemoteDataObject('Request Failed'),
|
||||||
|
createSuccessfulRemoteDataObject({}),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(notificationsService.success).toHaveBeenCalledTimes(1);
|
||||||
|
expect(notificationsService.error).toHaveBeenCalledTimes(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a separate error notification for each failed request', () => {
|
||||||
|
service.displayNotifications([
|
||||||
|
createSuccessfulRemoteDataObject({}),
|
||||||
|
createFailedRemoteDataObject('Request Failed 1'),
|
||||||
|
createSuccessfulRemoteDataObject({}),
|
||||||
|
createFailedRemoteDataObject('Request Failed 2'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
expect(notificationsService.success).toHaveBeenCalledTimes(1);
|
||||||
|
expect(notificationsService.error).toHaveBeenCalledTimes(2);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,202 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
EMPTY,
|
||||||
|
Observable,
|
||||||
|
Subscription,
|
||||||
|
} from 'rxjs';
|
||||||
|
import {
|
||||||
|
concatMap,
|
||||||
|
map,
|
||||||
|
switchMap,
|
||||||
|
take,
|
||||||
|
toArray,
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { EntityTypeDataService } from '../../../core/data/entity-type-data.service';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
|
||||||
|
import { FieldUpdate } from '../../../core/data/object-updates/field-update.model';
|
||||||
|
import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model';
|
||||||
|
import {
|
||||||
|
DeleteRelationship,
|
||||||
|
RelationshipIdentifiable,
|
||||||
|
} from '../../../core/data/object-updates/object-updates.reducer';
|
||||||
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
|
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { NoContent } from '../../../core/shared/NoContent.model';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class EditItemRelationshipsService {
|
||||||
|
public notificationsPrefix = 'item.edit.relationships.notifications.';
|
||||||
|
|
||||||
|
public isSaving$: BehaviorSubject<boolean> = new BehaviorSubject(false);
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
public itemService: ItemDataService,
|
||||||
|
public objectUpdatesService: ObjectUpdatesService,
|
||||||
|
public notificationsService: NotificationsService,
|
||||||
|
protected modalService: NgbModal,
|
||||||
|
public relationshipService: RelationshipDataService,
|
||||||
|
public entityTypeService: EntityTypeDataService,
|
||||||
|
public translateService: TranslateService,
|
||||||
|
) { }
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Resolve the currently selected related items back to relationships and send a delete request for each of the relationships found
|
||||||
|
* Make sure the lists are refreshed afterwards and notifications are sent for success and errors
|
||||||
|
*/
|
||||||
|
public submit(item: Item, url: string): void {
|
||||||
|
this.isSaving$.next(true);
|
||||||
|
this.objectUpdatesService.getFieldUpdates(url, [], true).pipe(
|
||||||
|
map((fieldUpdates: FieldUpdates) =>
|
||||||
|
Object.values(fieldUpdates)
|
||||||
|
.filter((fieldUpdate: FieldUpdate) => hasValue(fieldUpdate))
|
||||||
|
.filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.ADD || fieldUpdate.changeType === FieldChangeType.REMOVE),
|
||||||
|
),
|
||||||
|
take(1),
|
||||||
|
// emit each update in the array separately
|
||||||
|
switchMap((updates) => updates),
|
||||||
|
// process each update one by one, while waiting for the previous to finish
|
||||||
|
concatMap((update: FieldUpdate) => {
|
||||||
|
if (update.changeType === FieldChangeType.REMOVE) {
|
||||||
|
return this.deleteRelationship(update.field as DeleteRelationship).pipe(take(1));
|
||||||
|
} else if (update.changeType === FieldChangeType.ADD) {
|
||||||
|
return this.addRelationship(update.field as RelationshipIdentifiable).pipe(
|
||||||
|
take(1),
|
||||||
|
switchMap((relationshipRD: RemoteData<Relationship>) => {
|
||||||
|
if (relationshipRD.hasSucceeded) {
|
||||||
|
// Set the newly related item to stale, so its relationships will update to include
|
||||||
|
// the new one. Only set the current item to stale at the very end so we only do it
|
||||||
|
// once
|
||||||
|
const { leftItem, rightItem } = relationshipRD.payload._links;
|
||||||
|
if (leftItem.href === item.self) {
|
||||||
|
return this.itemService.invalidateByHref(rightItem.href).pipe(
|
||||||
|
// when it's invalidated, emit the original relationshipRD for use in the pipe below
|
||||||
|
map(() => relationshipRD),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return this.itemService.invalidateByHref(leftItem.href).pipe(
|
||||||
|
// when it's invalidated, emit the original relationshipRD for use in the pipe below
|
||||||
|
map(() => relationshipRD),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return [relationshipRD];
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return EMPTY;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
toArray(),
|
||||||
|
switchMap((responses) => {
|
||||||
|
// once all relationships are made and all related items have been invalidated, invalidate
|
||||||
|
// the current item
|
||||||
|
return this.itemService.invalidateByHref(item.self).pipe(
|
||||||
|
map(() => responses),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
).subscribe((responses) => {
|
||||||
|
if (responses.length > 0) {
|
||||||
|
this.initializeOriginalFields(item, url);
|
||||||
|
this.displayNotifications(responses);
|
||||||
|
this.modalService.dismissAll();
|
||||||
|
this.isSaving$.next(false);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sends all initial values of this item to the object updates service
|
||||||
|
*/
|
||||||
|
public initializeOriginalFields(item: Item, url: string): Subscription {
|
||||||
|
return this.relationshipService.getRelatedItems(item).pipe(
|
||||||
|
take(1),
|
||||||
|
).subscribe((items: Item[]) => {
|
||||||
|
this.objectUpdatesService.initialize(url, items, item.lastModified);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRelationship(deleteRelationship: DeleteRelationship): Observable<RemoteData<NoContent>> {
|
||||||
|
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, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
addRelationship(addRelationship: RelationshipIdentifiable): Observable<RemoteData<Relationship>> {
|
||||||
|
let leftItem: Item;
|
||||||
|
let rightItem: Item;
|
||||||
|
let leftwardValue: string;
|
||||||
|
let rightwardValue: string;
|
||||||
|
if (addRelationship.originalIsLeft) {
|
||||||
|
leftItem = addRelationship.originalItem;
|
||||||
|
rightItem = addRelationship.relatedItem;
|
||||||
|
leftwardValue = null;
|
||||||
|
rightwardValue = addRelationship.nameVariant;
|
||||||
|
} else {
|
||||||
|
leftItem = addRelationship.relatedItem;
|
||||||
|
rightItem = addRelationship.originalItem;
|
||||||
|
leftwardValue = addRelationship.nameVariant;
|
||||||
|
rightwardValue = null;
|
||||||
|
}
|
||||||
|
return this.relationshipService.addRelationship(addRelationship.type.id, leftItem, rightItem, leftwardValue, rightwardValue, false);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display notifications
|
||||||
|
* - Error notification for each failed response with their message
|
||||||
|
* - Success notification in case there's at least one successful response
|
||||||
|
* @param responses
|
||||||
|
*/
|
||||||
|
displayNotifications(responses: RemoteData<NoContent>[]): void {
|
||||||
|
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
||||||
|
const successfulResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
||||||
|
|
||||||
|
failedResponses.forEach((response: RemoteData<NoContent>) => {
|
||||||
|
this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage);
|
||||||
|
});
|
||||||
|
if (successfulResponses.length > 0) {
|
||||||
|
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get translated notification title
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
getNotificationTitle(key: string): string {
|
||||||
|
return this.translateService.instant(this.notificationsPrefix + key + '.title');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get translated notification content
|
||||||
|
* @param key
|
||||||
|
*/
|
||||||
|
getNotificationContent(key: string): string {
|
||||||
|
return this.translateService.instant(this.notificationsPrefix + key + '.content');
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
@@ -1,5 +1,5 @@
|
|||||||
<h2 class="h4">
|
<h2 class="h4">
|
||||||
{{getRelationshipMessageKey$ | async | translate}}
|
{{relationshipMessageKey$ | async | translate}}
|
||||||
<button class="ml-2 btn btn-success" [disabled]="(hasChanges | async)" (click)="openLookup()">
|
<button class="ml-2 btn btn-success" [disabled]="(hasChanges | async)" (click)="openLookup()">
|
||||||
<i class="fas fa-plus"></i>
|
<i class="fas fa-plus"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.relationships.edit.buttons.add" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.relationships.edit.buttons.add" | translate}}</span>
|
||||||
|
@@ -10,19 +10,17 @@ import {
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import {
|
import {
|
||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
Router,
|
RouterModule,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { cold } from 'jasmine-marbles';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { AuthRequestService } from 'src/app/core/auth/auth-request.service';
|
|
||||||
import { CookieService } from 'src/app/core/services/cookie.service';
|
|
||||||
import { HardRedirectService } from 'src/app/core/services/hard-redirect.service';
|
|
||||||
import { ActivatedRouteStub } from 'src/app/shared/testing/active-router.stub';
|
|
||||||
import { AuthRequestServiceStub } from 'src/app/shared/testing/auth-request-service.stub';
|
|
||||||
|
|
||||||
import { APP_CONFIG } from '../../../../../config/app-config.interface';
|
import { APP_CONFIG } from '../../../../../config/app-config.interface';
|
||||||
|
import { environment } from '../../../../../environments/environment.test';
|
||||||
import { REQUEST } from '../../../../../express.tokens';
|
import { REQUEST } from '../../../../../express.tokens';
|
||||||
|
import { AuthRequestService } from '../../../../core/auth/auth-request.service';
|
||||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
|
||||||
import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model';
|
import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model';
|
||||||
@@ -31,6 +29,8 @@ import { RelationshipDataService } from '../../../../core/data/relationship-data
|
|||||||
import { RelationshipTypeDataService } from '../../../../core/data/relationship-type-data.service';
|
import { RelationshipTypeDataService } from '../../../../core/data/relationship-type-data.service';
|
||||||
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
import { GroupDataService } from '../../../../core/eperson/group-data.service';
|
||||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||||
|
import { CookieService } from '../../../../core/services/cookie.service';
|
||||||
|
import { HardRedirectService } from '../../../../core/services/hard-redirect.service';
|
||||||
import { LinkHeadService } from '../../../../core/services/link-head.service';
|
import { LinkHeadService } from '../../../../core/services/link-head.service';
|
||||||
import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model';
|
import { ConfigurationProperty } from '../../../../core/shared/configuration-property.model';
|
||||||
import { Item } from '../../../../core/shared/item.model';
|
import { Item } from '../../../../core/shared/item.model';
|
||||||
@@ -40,51 +40,55 @@ import { RelationshipType } from '../../../../core/shared/item-relationships/rel
|
|||||||
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../core/shared/search/search-configuration.service';
|
||||||
import { XSRFService } from '../../../../core/xsrf/xsrf.service';
|
import { XSRFService } from '../../../../core/xsrf/xsrf.service';
|
||||||
import { HostWindowService } from '../../../../shared/host-window.service';
|
import { HostWindowService } from '../../../../shared/host-window.service';
|
||||||
import { RouterMock } from '../../../../shared/mocks/router.mock';
|
|
||||||
import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service';
|
import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service';
|
||||||
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
||||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/remote-data.utils';
|
||||||
|
import { ActivatedRouteStub } from '../../../../shared/testing/active-router.stub';
|
||||||
|
import { AuthRequestServiceStub } from '../../../../shared/testing/auth-request-service.stub';
|
||||||
|
import { EditItemRelationshipsServiceStub } from '../../../../shared/testing/edit-item-relationships.service.stub';
|
||||||
import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub';
|
import { HostWindowServiceStub } from '../../../../shared/testing/host-window-service.stub';
|
||||||
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
import { PaginationServiceStub } from '../../../../shared/testing/pagination-service.stub';
|
||||||
import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub';
|
import { SearchConfigurationServiceStub } from '../../../../shared/testing/search-configuration-service.stub';
|
||||||
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../../shared/testing/utils.test';
|
||||||
|
import { EditItemRelationshipsService } from '../edit-item-relationships.service';
|
||||||
import { EditRelationshipListComponent } from './edit-relationship-list.component';
|
import { EditRelationshipListComponent } from './edit-relationship-list.component';
|
||||||
|
|
||||||
let comp: EditRelationshipListComponent;
|
|
||||||
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
|
||||||
let de: DebugElement;
|
|
||||||
|
|
||||||
let linkService;
|
|
||||||
let objectUpdatesService;
|
|
||||||
let relationshipService;
|
|
||||||
let selectableListService;
|
|
||||||
let paginationService;
|
|
||||||
let hostWindowService;
|
|
||||||
let hardRedirectService;
|
|
||||||
const relationshipTypeService = {};
|
|
||||||
|
|
||||||
const url = 'http://test-url.com/test-url';
|
|
||||||
|
|
||||||
let item;
|
|
||||||
let entityType;
|
|
||||||
let relatedEntityType;
|
|
||||||
let author1;
|
|
||||||
let author2;
|
|
||||||
let fieldUpdate1;
|
|
||||||
let fieldUpdate2;
|
|
||||||
let relationships;
|
|
||||||
let relationshipType;
|
|
||||||
let paginationOptions;
|
|
||||||
|
|
||||||
describe('EditRelationshipListComponent', () => {
|
describe('EditRelationshipListComponent', () => {
|
||||||
|
|
||||||
|
let comp: EditRelationshipListComponent;
|
||||||
|
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
||||||
|
let de: DebugElement;
|
||||||
|
|
||||||
|
let linkService;
|
||||||
|
let objectUpdatesService;
|
||||||
|
let relationshipService;
|
||||||
|
let selectableListService;
|
||||||
|
let paginationService: PaginationServiceStub;
|
||||||
|
let hostWindowService: HostWindowServiceStub;
|
||||||
|
let hardRedirectService;
|
||||||
|
const relationshipTypeService = {};
|
||||||
|
let editItemRelationshipsService: EditItemRelationshipsServiceStub;
|
||||||
|
|
||||||
|
const url = 'http://test-url.com/test-url';
|
||||||
|
|
||||||
|
let itemLeft: Item;
|
||||||
|
let entityTypeLeft: ItemType;
|
||||||
|
let entityTypeRight: ItemType;
|
||||||
|
let itemRight1: Item;
|
||||||
|
let itemRight2: Item;
|
||||||
|
let fieldUpdate1;
|
||||||
|
let fieldUpdate2;
|
||||||
|
let relationships: Relationship[];
|
||||||
|
let relationshipType: RelationshipType;
|
||||||
|
let paginationOptions: PaginationComponentOptions;
|
||||||
|
|
||||||
const resetComponent = () => {
|
const resetComponent = () => {
|
||||||
fixture = TestBed.createComponent(EditRelationshipListComponent);
|
fixture = TestBed.createComponent(EditRelationshipListComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
de = fixture.debugElement;
|
de = fixture.debugElement;
|
||||||
comp.item = item;
|
comp.item = itemLeft;
|
||||||
comp.itemType = entityType;
|
comp.itemType = entityTypeLeft;
|
||||||
comp.url = url;
|
comp.url = url;
|
||||||
comp.relationshipType = relationshipType;
|
comp.relationshipType = relationshipType;
|
||||||
comp.hasChanges = observableOf(false);
|
comp.hasChanges = observableOf(false);
|
||||||
@@ -101,29 +105,26 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
hardRedirectService = jasmine.createSpyObj('hardRedirectService', ['redirect']);
|
function init(leftType: string, rightType: string): void {
|
||||||
|
entityTypeLeft = Object.assign(new ItemType(), {
|
||||||
beforeEach(waitForAsync(() => {
|
id: leftType,
|
||||||
|
uuid: leftType,
|
||||||
entityType = Object.assign(new ItemType(), {
|
label: leftType,
|
||||||
id: 'Publication',
|
|
||||||
uuid: 'Publication',
|
|
||||||
label: 'Publication',
|
|
||||||
});
|
});
|
||||||
|
|
||||||
relatedEntityType = Object.assign(new ItemType(), {
|
entityTypeRight = Object.assign(new ItemType(), {
|
||||||
id: 'Author',
|
id: rightType,
|
||||||
uuid: 'Author',
|
uuid: rightType,
|
||||||
label: 'Author',
|
label: rightType,
|
||||||
});
|
});
|
||||||
|
|
||||||
relationshipType = Object.assign(new RelationshipType(), {
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
id: '1',
|
id: '1',
|
||||||
uuid: '1',
|
uuid: '1',
|
||||||
leftType: createSuccessfulRemoteDataObject$(entityType),
|
leftType: createSuccessfulRemoteDataObject$(entityTypeLeft),
|
||||||
rightType: createSuccessfulRemoteDataObject$(relatedEntityType),
|
rightType: createSuccessfulRemoteDataObject$(entityTypeRight),
|
||||||
leftwardType: 'isAuthorOfPublication',
|
leftwardType: `is${rightType}Of${leftType}`,
|
||||||
rightwardType: 'isPublicationOfAuthor',
|
rightwardType: `is${leftType}Of${rightType}`,
|
||||||
});
|
});
|
||||||
|
|
||||||
paginationOptions = Object.assign(new PaginationComponentOptions(), {
|
paginationOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
@@ -132,13 +133,13 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
currentPage: 1,
|
currentPage: 1,
|
||||||
});
|
});
|
||||||
|
|
||||||
author1 = Object.assign(new Item(), {
|
itemRight1 = Object.assign(new Item(), {
|
||||||
id: 'author1',
|
id: `${rightType}-1`,
|
||||||
uuid: 'author1',
|
uuid: `${rightType}-1`,
|
||||||
});
|
});
|
||||||
author2 = Object.assign(new Item(), {
|
itemRight2 = Object.assign(new Item(), {
|
||||||
id: 'author2',
|
id: `${rightType}-2`,
|
||||||
uuid: 'author2',
|
uuid: `${rightType}-2`,
|
||||||
});
|
});
|
||||||
|
|
||||||
relationships = [
|
relationships = [
|
||||||
@@ -147,25 +148,25 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
id: '2',
|
id: '2',
|
||||||
uuid: '2',
|
uuid: '2',
|
||||||
relationshipType: createSuccessfulRemoteDataObject$(relationshipType),
|
relationshipType: createSuccessfulRemoteDataObject$(relationshipType),
|
||||||
leftItem: createSuccessfulRemoteDataObject$(item),
|
leftItem: createSuccessfulRemoteDataObject$(itemLeft),
|
||||||
rightItem: createSuccessfulRemoteDataObject$(author1),
|
rightItem: createSuccessfulRemoteDataObject$(itemRight1),
|
||||||
}),
|
}),
|
||||||
Object.assign(new Relationship(), {
|
Object.assign(new Relationship(), {
|
||||||
self: url + '/3',
|
self: url + '/3',
|
||||||
id: '3',
|
id: '3',
|
||||||
uuid: '3',
|
uuid: '3',
|
||||||
relationshipType: createSuccessfulRemoteDataObject$(relationshipType),
|
relationshipType: createSuccessfulRemoteDataObject$(relationshipType),
|
||||||
leftItem: createSuccessfulRemoteDataObject$(item),
|
leftItem: createSuccessfulRemoteDataObject$(itemLeft),
|
||||||
rightItem: createSuccessfulRemoteDataObject$(author2),
|
rightItem: createSuccessfulRemoteDataObject$(itemRight2),
|
||||||
}),
|
}),
|
||||||
];
|
];
|
||||||
|
|
||||||
item = Object.assign(new Item(), {
|
itemLeft = Object.assign(new Item(), {
|
||||||
_links: {
|
_links: {
|
||||||
self: { href: 'fake-item-url/publication' },
|
self: { href: 'fake-item-url/publication' },
|
||||||
},
|
},
|
||||||
id: 'publication',
|
id: `1-${leftType}`,
|
||||||
uuid: 'publication',
|
uuid: `1-${leftType}`,
|
||||||
relationships: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)),
|
relationships: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)),
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -197,7 +198,7 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
|
|
||||||
relationshipService = jasmine.createSpyObj('relationshipService',
|
relationshipService = jasmine.createSpyObj('relationshipService',
|
||||||
{
|
{
|
||||||
getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList([author1, author2])),
|
getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList([itemRight1, itemRight2])),
|
||||||
getItemRelationshipsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)),
|
getItemRelationshipsByLabel: createSuccessfulRemoteDataObject$(createPaginatedList(relationships)),
|
||||||
isLeftItem: observableOf(true),
|
isLeftItem: observableOf(true),
|
||||||
},
|
},
|
||||||
@@ -233,14 +234,14 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
})),
|
})),
|
||||||
});
|
});
|
||||||
|
|
||||||
const environmentUseThumbs = {
|
editItemRelationshipsService = new EditItemRelationshipsServiceStub();
|
||||||
browseBy: {
|
|
||||||
showThumbnails: true,
|
|
||||||
},
|
|
||||||
};
|
|
||||||
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), EditRelationshipListComponent],
|
imports: [
|
||||||
|
EditRelationshipListComponent,
|
||||||
|
RouterModule.forRoot([]),
|
||||||
|
TranslateModule.forRoot(),
|
||||||
|
],
|
||||||
providers: [
|
providers: [
|
||||||
provideMockStore({ initialState }),
|
provideMockStore({ initialState }),
|
||||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
@@ -251,15 +252,15 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
{ provide: HostWindowService, useValue: hostWindowService },
|
{ provide: HostWindowService, useValue: hostWindowService },
|
||||||
{ provide: RelationshipTypeDataService, useValue: relationshipTypeService },
|
{ provide: RelationshipTypeDataService, useValue: relationshipTypeService },
|
||||||
{ provide: GroupDataService, useValue: groupDataService },
|
{ provide: GroupDataService, useValue: groupDataService },
|
||||||
{ provide: Router, useValue: new RouterMock() },
|
|
||||||
{ provide: LinkHeadService, useValue: linkHeadService },
|
{ provide: LinkHeadService, useValue: linkHeadService },
|
||||||
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
||||||
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
|
{ provide: SearchConfigurationService, useValue: new SearchConfigurationServiceStub() },
|
||||||
|
{ provide: EditItemRelationshipsService, useValue: editItemRelationshipsService },
|
||||||
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||||
{ provide: AuthRequestService, useValue: new AuthRequestServiceStub() },
|
{ provide: AuthRequestService, useValue: new AuthRequestServiceStub() },
|
||||||
{ provide: HardRedirectService, useValue: hardRedirectService },
|
{ provide: HardRedirectService, useValue: hardRedirectService },
|
||||||
{ provide: XSRFService, useValue: {} },
|
{ provide: XSRFService, useValue: {} },
|
||||||
{ provide: APP_CONFIG, useValue: environmentUseThumbs },
|
{ provide: APP_CONFIG, useValue: environment },
|
||||||
{ provide: REQUEST, useValue: {} },
|
{ provide: REQUEST, useValue: {} },
|
||||||
CookieService,
|
CookieService,
|
||||||
], schemas: [
|
], schemas: [
|
||||||
@@ -268,7 +269,10 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
|
|
||||||
resetComponent();
|
resetComponent();
|
||||||
}));
|
}
|
||||||
|
|
||||||
|
describe('Publication - Author relationship', () => {
|
||||||
|
beforeEach(waitForAsync(() => init('Publication', 'Author')));
|
||||||
|
|
||||||
describe('changeType is REMOVE', () => {
|
describe('changeType is REMOVE', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
@@ -316,8 +320,8 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
relationshipType = Object.assign(new RelationshipType(), {
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
id: '1',
|
id: '1',
|
||||||
uuid: '1',
|
uuid: '1',
|
||||||
leftType: createSuccessfulRemoteDataObject$(entityType), // publication
|
leftType: createSuccessfulRemoteDataObject$(entityTypeLeft), // publication
|
||||||
rightType: createSuccessfulRemoteDataObject$(relatedEntityType), // author
|
rightType: createSuccessfulRemoteDataObject$(entityTypeRight), // author
|
||||||
leftwardType: 'isAuthorOfPublication',
|
leftwardType: 'isAuthorOfPublication',
|
||||||
rightwardType: 'isPublicationOfAuthor',
|
rightwardType: 'isPublicationOfAuthor',
|
||||||
});
|
});
|
||||||
@@ -340,8 +344,8 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
relationshipType = Object.assign(new RelationshipType(), {
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
id: '1',
|
id: '1',
|
||||||
uuid: '1',
|
uuid: '1',
|
||||||
leftType: createSuccessfulRemoteDataObject$(relatedEntityType), // author
|
leftType: createSuccessfulRemoteDataObject$(entityTypeRight), // author
|
||||||
rightType: createSuccessfulRemoteDataObject$(entityType), // publication
|
rightType: createSuccessfulRemoteDataObject$(entityTypeLeft), // publication
|
||||||
leftwardType: 'isPublicationOfAuthor',
|
leftwardType: 'isPublicationOfAuthor',
|
||||||
rightwardType: 'isAuthorOfPublication',
|
rightwardType: 'isAuthorOfPublication',
|
||||||
});
|
});
|
||||||
@@ -377,5 +381,15 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('OrgUnit - OrgUnit relationship', () => {
|
||||||
|
beforeEach(waitForAsync(() => init('OrgUnit', 'OrgUnit')));
|
||||||
|
|
||||||
|
it('should emit the relatedEntityType$ even for same entity relationships', () => {
|
||||||
|
expect(comp.relatedEntityType$).toBeObservable(cold('(a|)', {
|
||||||
|
a: entityTypeRight,
|
||||||
|
}));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
@@ -21,11 +21,13 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
|
EMPTY,
|
||||||
from as observableFrom,
|
from as observableFrom,
|
||||||
Observable,
|
Observable,
|
||||||
Subscription,
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
|
concatMap,
|
||||||
defaultIfEmpty,
|
defaultIfEmpty,
|
||||||
map,
|
map,
|
||||||
mergeMap,
|
mergeMap,
|
||||||
@@ -48,7 +50,6 @@ import { RelationshipIdentifiable } from '../../../../core/data/object-updates/o
|
|||||||
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../../core/data/paginated-list.model';
|
||||||
import { RelationshipDataService } from '../../../../core/data/relationship-data.service';
|
import { RelationshipDataService } from '../../../../core/data/relationship-data.service';
|
||||||
import { RelationshipTypeDataService } from '../../../../core/data/relationship-type-data.service';
|
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../../core/pagination/pagination.service';
|
||||||
import { Collection } from '../../../../core/shared/collection.model';
|
import { Collection } from '../../../../core/shared/collection.model';
|
||||||
@@ -70,14 +71,15 @@ import {
|
|||||||
import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
import { DsDynamicLookupRelationModalComponent } from '../../../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component';
|
||||||
import { RelationshipOptions } from '../../../../shared/form/builder/models/relationship-options.model';
|
import { RelationshipOptions } from '../../../../shared/form/builder/models/relationship-options.model';
|
||||||
import { ThemedLoadingComponent } from '../../../../shared/loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../../../shared/loading/themed-loading.component';
|
||||||
|
import { ItemSearchResult } from '../../../../shared/object-collection/shared/item-search-result.model';
|
||||||
import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service';
|
import { SelectableListService } from '../../../../shared/object-list/selectable-list/selectable-list.service';
|
||||||
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
import { PaginationComponent } from '../../../../shared/pagination/pagination.component';
|
||||||
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../../shared/pagination/pagination-component-options.model';
|
||||||
import { SearchResult } from '../../../../shared/search/models/search-result.model';
|
|
||||||
import { FollowLinkConfig } from '../../../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../../../shared/utils/follow-link-config.model';
|
||||||
import { ObjectValuesPipe } from '../../../../shared/utils/object-values-pipe';
|
import { ObjectValuesPipe } from '../../../../shared/utils/object-values-pipe';
|
||||||
import { itemLinksToFollow } from '../../../../shared/utils/relation-query.utils';
|
import { itemLinksToFollow } from '../../../../shared/utils/relation-query.utils';
|
||||||
import { VarDirective } from '../../../../shared/utils/var.directive';
|
import { VarDirective } from '../../../../shared/utils/var.directive';
|
||||||
|
import { EditItemRelationshipsService } from '../edit-item-relationships.service';
|
||||||
import { EditRelationshipComponent } from '../edit-relationship/edit-relationship.component';
|
import { EditRelationshipComponent } from '../edit-relationship/edit-relationship.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -130,7 +132,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* The event emmiter to submit the new information
|
* The event emmiter to submit the new information
|
||||||
*/
|
*/
|
||||||
@Output() submit: EventEmitter<any> = new EventEmitter();
|
@Output() submitModal: EventEmitter<void> = new EventEmitter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Observable that emits the left and right item type of {@link relationshipType} simultaneously.
|
* Observable that emits the left and right item type of {@link relationshipType} simultaneously.
|
||||||
@@ -141,11 +143,14 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
* Observable that emits true if {@link itemType} is on the left-hand side of {@link relationshipType},
|
* Observable that emits true if {@link itemType} is on the left-hand side of {@link relationshipType},
|
||||||
* false if it is on the right-hand side and undefined in the rare case that it is on neither side.
|
* false if it is on the right-hand side and undefined in the rare case that it is on neither side.
|
||||||
*/
|
*/
|
||||||
private currentItemIsLeftItem$: Observable<boolean>;
|
private currentItemIsLeftItem$: BehaviorSubject<boolean> = new BehaviorSubject(undefined);
|
||||||
|
|
||||||
private relatedEntityType$: Observable<ItemType>;
|
relatedEntityType$: Observable<ItemType>;
|
||||||
|
|
||||||
getRelationshipMessageKey$: Observable<string>;
|
/**
|
||||||
|
* The translation key for the entity type
|
||||||
|
*/
|
||||||
|
relationshipMessageKey$: Observable<string>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list ID to save selected entities under
|
* The list ID to save selected entities under
|
||||||
@@ -202,15 +207,38 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
protected objectUpdatesService: ObjectUpdatesService,
|
protected objectUpdatesService: ObjectUpdatesService,
|
||||||
protected linkService: LinkService,
|
protected linkService: LinkService,
|
||||||
protected relationshipService: RelationshipDataService,
|
protected relationshipService: RelationshipDataService,
|
||||||
protected relationshipTypeService: RelationshipTypeDataService,
|
|
||||||
protected modalService: NgbModal,
|
protected modalService: NgbModal,
|
||||||
protected paginationService: PaginationService,
|
protected paginationService: PaginationService,
|
||||||
protected selectableListService: SelectableListService,
|
protected selectableListService: SelectableListService,
|
||||||
|
protected editItemRelationshipsService: EditItemRelationshipsService,
|
||||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
) {
|
) {
|
||||||
this.fetchThumbnail = this.appConfig.browseBy.showThumbnails;
|
this.fetchThumbnail = this.appConfig.browseBy.showThumbnails;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the i18n message key for this relationship type
|
||||||
|
*/
|
||||||
|
public getRelationshipMessageKey(): Observable<string> {
|
||||||
|
return observableCombineLatest([
|
||||||
|
this.getLabel(),
|
||||||
|
this.relatedEntityType$,
|
||||||
|
]).pipe(
|
||||||
|
map(([label, relatedEntityType]) => {
|
||||||
|
if (hasValue(label) && label.indexOf('is') > -1 && label.indexOf('Of') > -1) {
|
||||||
|
const relationshipLabel = `${label.substring(2, label.indexOf('Of'))}`;
|
||||||
|
if (relationshipLabel !== relatedEntityType.label) {
|
||||||
|
return `relationships.is${relationshipLabel}Of.${relatedEntityType.label}`;
|
||||||
|
} else {
|
||||||
|
return `relationships.is${relationshipLabel}Of`;
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return label;
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the relevant label for this relationship type
|
* Get the relevant label for this relationship type
|
||||||
*/
|
*/
|
||||||
@@ -240,7 +268,6 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
* Open the dynamic lookup modal to search for items to add as relationships
|
* Open the dynamic lookup modal to search for items to add as relationships
|
||||||
*/
|
*/
|
||||||
openLookup() {
|
openLookup() {
|
||||||
|
|
||||||
this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, {
|
this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, {
|
||||||
size: 'lg',
|
size: 'lg',
|
||||||
});
|
});
|
||||||
@@ -261,11 +288,11 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
modalComp.collection = collection;
|
modalComp.collection = collection;
|
||||||
});
|
});
|
||||||
|
|
||||||
modalComp.select = (...selectableObjects: SearchResult<Item>[]) => {
|
modalComp.select = (...selectableObjects: ItemSearchResult[]) => {
|
||||||
selectableObjects.forEach((searchResult) => {
|
selectableObjects.forEach((searchResult) => {
|
||||||
const relatedItem: Item = searchResult.indexableObject;
|
const relatedItem: Item = searchResult.indexableObject;
|
||||||
|
|
||||||
const foundIndex = modalComp.toRemove.findIndex( el => el.uuid === relatedItem.uuid);
|
const foundIndex = modalComp.toRemove.findIndex((itemSearchResult: ItemSearchResult) => itemSearchResult.indexableObject.uuid === relatedItem.uuid);
|
||||||
|
|
||||||
if (foundIndex !== -1) {
|
if (foundIndex !== -1) {
|
||||||
modalComp.toRemove.splice(foundIndex,1);
|
modalComp.toRemove.splice(foundIndex,1);
|
||||||
@@ -289,7 +316,7 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
modalComp.deselect = (...selectableObjects: SearchResult<Item>[]) => {
|
modalComp.deselect = (...selectableObjects: ItemSearchResult[]) => {
|
||||||
selectableObjects.forEach((searchResult) => {
|
selectableObjects.forEach((searchResult) => {
|
||||||
const relatedItem: Item = searchResult.indexableObject;
|
const relatedItem: Item = searchResult.indexableObject;
|
||||||
|
|
||||||
@@ -306,27 +333,30 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
|
|
||||||
|
|
||||||
modalComp.submitEv = () => {
|
modalComp.submitEv = () => {
|
||||||
|
modalComp.isPending = true;
|
||||||
const subscriptions = [];
|
const isLeft = this.currentItemIsLeftItem$.getValue();
|
||||||
|
const addOperations = modalComp.toAdd.map((searchResult: ItemSearchResult) => ({ type: 'add', searchResult }));
|
||||||
modalComp.toAdd.forEach((searchResult: SearchResult<Item>) => {
|
const removeOperations = modalComp.toRemove.map((searchResult: ItemSearchResult) => ({ type: 'remove', searchResult }));
|
||||||
const relatedItem = searchResult.indexableObject;
|
observableFrom([...addOperations, ...removeOperations]).pipe(
|
||||||
subscriptions.push(this.relationshipService.getNameVariant(this.listId, relatedItem.uuid).pipe(
|
concatMap(({ type, searchResult }: { type: string, searchResult: ItemSearchResult }) => {
|
||||||
map((nameVariant) => {
|
const relatedItem: Item = searchResult.indexableObject;
|
||||||
|
if (type === 'add') {
|
||||||
|
return this.relationshipService.getNameVariant(this.listId, relatedItem.uuid).pipe(
|
||||||
|
switchMap((nameVariant) => {
|
||||||
const update = {
|
const update = {
|
||||||
uuid: this.relationshipType.id + '-' + searchResult.indexableObject.uuid,
|
uuid: `${this.relationshipType.id}-${relatedItem.uuid}`,
|
||||||
nameVariant,
|
nameVariant,
|
||||||
type: this.relationshipType,
|
type: this.relationshipType,
|
||||||
|
originalIsLeft: isLeft,
|
||||||
|
originalItem: this.item,
|
||||||
relatedItem,
|
relatedItem,
|
||||||
} as RelationshipIdentifiable;
|
} as RelationshipIdentifiable;
|
||||||
this.objectUpdatesService.saveAddFieldUpdate(this.url, update);
|
return this.objectUpdatesService.saveAddFieldUpdate(this.url, update);
|
||||||
return update;
|
|
||||||
}),
|
}),
|
||||||
));
|
take(1),
|
||||||
});
|
);
|
||||||
|
} else if (type === 'remove') {
|
||||||
modalComp.toRemove.forEach( (searchResult) => {
|
return this.relationshipService.getNameVariant(this.listId, relatedItem.uuid).pipe(
|
||||||
subscriptions.push(this.relationshipService.getNameVariant(this.listId, searchResult.indexableObjectuuid).pipe(
|
|
||||||
switchMap((nameVariant) => {
|
switchMap((nameVariant) => {
|
||||||
return this.getRelationFromId(searchResult.indexableObject).pipe(
|
return this.getRelationFromId(searchResult.indexableObject).pipe(
|
||||||
map( (relationship: Relationship) => {
|
map( (relationship: Relationship) => {
|
||||||
@@ -334,23 +364,26 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
uuid: relationship.id,
|
uuid: relationship.id,
|
||||||
nameVariant,
|
nameVariant,
|
||||||
type: this.relationshipType,
|
type: this.relationshipType,
|
||||||
|
originalIsLeft: isLeft,
|
||||||
|
originalItem: this.item,
|
||||||
relationship,
|
relationship,
|
||||||
} as RelationshipIdentifiable;
|
} as RelationshipIdentifiable;
|
||||||
this.objectUpdatesService.saveRemoveFieldUpdate(this.url,update);
|
return this.objectUpdatesService.saveRemoveFieldUpdate(this.url,update);
|
||||||
return update;
|
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}),
|
}),
|
||||||
));
|
take(1),
|
||||||
});
|
);
|
||||||
|
} else {
|
||||||
observableCombineLatest(subscriptions).subscribe( (res) => {
|
return EMPTY;
|
||||||
// Wait until the states changes since there are multiple items
|
}
|
||||||
setTimeout( () => {
|
}),
|
||||||
this.submit.emit();
|
toArray(),
|
||||||
},1000);
|
).subscribe({
|
||||||
|
complete: () => {
|
||||||
modalComp.isPending = true;
|
this.editItemRelationshipsService.submit(this.item, this.url);
|
||||||
|
this.submitModal.emit();
|
||||||
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -384,92 +417,15 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
}
|
}
|
||||||
|
|
||||||
getRelationFromId(relatedItem) {
|
getRelationFromId(relatedItem) {
|
||||||
return this.currentItemIsLeftItem$.pipe(
|
const relationshipLabel = this.currentItemIsLeftItem$.getValue() ? this.relationshipType.leftwardType : this.relationshipType.rightwardType;
|
||||||
take(1),
|
return this.relationshipService.searchByItemsAndType( this.relationshipType.id, this.item.uuid, relationshipLabel ,[relatedItem.id] ).pipe(
|
||||||
switchMap( isLeft => {
|
|
||||||
let apiCall;
|
|
||||||
if (isLeft) {
|
|
||||||
apiCall = this.relationshipService.searchByItemsAndType( this.relationshipType.id, this.item.uuid, this.relationshipType.leftwardType ,[relatedItem.id] ).pipe(
|
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
);
|
|
||||||
} else {
|
|
||||||
apiCall = this.relationshipService.searchByItemsAndType( this.relationshipType.id, this.item.uuid, this.relationshipType.rightwardType ,[relatedItem.id] ).pipe(
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
return apiCall.pipe(
|
|
||||||
map( (res: PaginatedList<Relationship>) => res.page[0]),
|
map( (res: PaginatedList<Relationship>) => res.page[0]),
|
||||||
);
|
);
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the existing field updates regarding a relationship with a given item
|
|
||||||
* @param relatedItem The item for which to get the existing field updates
|
|
||||||
*/
|
|
||||||
private getFieldUpdatesForRelatedItem(relatedItem: Item): Observable<RelationshipIdentifiable[]> {
|
|
||||||
return this.updates$.pipe(
|
|
||||||
take(1),
|
|
||||||
map((updates) => Object.values(updates)
|
|
||||||
.map((update) => update.field as RelationshipIdentifiable)
|
|
||||||
.filter((field) => field.relationship),
|
|
||||||
),
|
|
||||||
mergeMap((identifiables) =>
|
|
||||||
observableCombineLatest(
|
|
||||||
identifiables.map((identifiable) => this.getRelatedItem(identifiable.relationship)),
|
|
||||||
).pipe(
|
|
||||||
defaultIfEmpty([]),
|
|
||||||
map((relatedItems) => {
|
|
||||||
return identifiables.filter( (identifiable, index) => {
|
|
||||||
return relatedItems[index].uuid === relatedItem.uuid;
|
|
||||||
});
|
|
||||||
},
|
|
||||||
),
|
|
||||||
),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the given item is related with the item we are editing relationships
|
|
||||||
* @param relatedItem The item for which to get the existing field updates
|
|
||||||
*/
|
|
||||||
private getIsRelatedItem(relatedItem: Item): Observable<boolean> {
|
|
||||||
|
|
||||||
return this.currentItemIsLeftItem$.pipe(
|
|
||||||
take(1),
|
|
||||||
map( isLeft => {
|
|
||||||
if (isLeft) {
|
|
||||||
const listOfRelatedItems = this.item.allMetadataValues( 'relation.' + this.relationshipType.leftwardType );
|
|
||||||
return !!listOfRelatedItems.find( (uuid) => uuid === relatedItem.uuid );
|
|
||||||
} else {
|
|
||||||
const listOfRelatedItems = this.item.allMetadataValues( 'relation.' + this.relationshipType.rightwardType );
|
|
||||||
return !!listOfRelatedItems.find( (uuid) => uuid === relatedItem.uuid );
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the related item for a given relationship
|
|
||||||
* @param relationship The relationship for which to get the related item
|
|
||||||
*/
|
|
||||||
private getRelatedItem(relationship: Relationship): Observable<Item> {
|
|
||||||
return this.relationshipService.isLeftItem(relationship, this.item).pipe(
|
|
||||||
switchMap((isLeftItem) => isLeftItem ? relationship.rightItem : relationship.leftItem),
|
|
||||||
getFirstSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
) as Observable<Item>;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
|
||||||
// store the left and right type of the relationship in a single observable
|
// store the left and right type of the relationship in a single observable
|
||||||
this.relationshipLeftAndRightType$ = observableCombineLatest([
|
this.relationshipLeftAndRightType$ = observableCombineLatest([
|
||||||
this.relationshipType.leftType,
|
this.relationshipType.leftType,
|
||||||
@@ -480,7 +436,13 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
))) as Observable<[ItemType, ItemType]>;
|
))) as Observable<[ItemType, ItemType]>;
|
||||||
|
|
||||||
this.relatedEntityType$ = this.relationshipLeftAndRightType$.pipe(
|
this.relatedEntityType$ = this.relationshipLeftAndRightType$.pipe(
|
||||||
map((relatedTypes: ItemType[]) => relatedTypes.find((relatedType) => relatedType.uuid !== this.itemType.uuid)),
|
map(([leftType, rightType]: [ItemType, ItemType]) => {
|
||||||
|
if (leftType.uuid !== this.itemType.uuid) {
|
||||||
|
return leftType;
|
||||||
|
} else {
|
||||||
|
return rightType;
|
||||||
|
}
|
||||||
|
}),
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -490,7 +452,9 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
(relatedEntityType) => this.listId = `edit-relationship-${this.itemType.id}-${relatedEntityType.id}`,
|
(relatedEntityType) => this.listId = `edit-relationship-${this.itemType.id}-${relatedEntityType.id}`,
|
||||||
);
|
);
|
||||||
|
|
||||||
this.currentItemIsLeftItem$ = this.relationshipLeftAndRightType$.pipe(
|
this.relationshipMessageKey$ = this.getRelationshipMessageKey();
|
||||||
|
|
||||||
|
this.subs.push(this.relationshipLeftAndRightType$.pipe(
|
||||||
map(([leftType, rightType]: [ItemType, ItemType]) => {
|
map(([leftType, rightType]: [ItemType, ItemType]) => {
|
||||||
if (leftType.id === this.itemType.id) {
|
if (leftType.id === this.itemType.id) {
|
||||||
return true;
|
return true;
|
||||||
@@ -504,25 +468,9 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
console.warn(`The item ${this.item.uuid} is not on the right or the left side of relationship type ${this.relationshipType.uuid}`);
|
console.warn(`The item ${this.item.uuid} is not on the right or the left side of relationship type ${this.relationshipType.uuid}`);
|
||||||
return undefined;
|
return undefined;
|
||||||
}),
|
}),
|
||||||
);
|
).subscribe((nextValue: boolean) => {
|
||||||
|
this.currentItemIsLeftItem$.next(nextValue);
|
||||||
this.getRelationshipMessageKey$ = observableCombineLatest(
|
}));
|
||||||
this.getLabel(),
|
|
||||||
this.relatedEntityType$,
|
|
||||||
).pipe(
|
|
||||||
map(([label, relatedEntityType]) => {
|
|
||||||
if (hasValue(label) && label.indexOf('is') > -1 && label.indexOf('Of') > -1) {
|
|
||||||
const relationshipLabel = `${label.substring(2, label.indexOf('Of'))}`;
|
|
||||||
if (relationshipLabel !== relatedEntityType.label) {
|
|
||||||
return `relationships.is${relationshipLabel}Of.${relatedEntityType.label}`;
|
|
||||||
} else {
|
|
||||||
return `relationships.is${relationshipLabel}Of`;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return label;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
|
|
||||||
|
|
||||||
// initialize the pagination options
|
// initialize the pagination options
|
||||||
@@ -547,19 +495,20 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
currentPagination$,
|
currentPagination$,
|
||||||
this.currentItemIsLeftItem$,
|
this.currentItemIsLeftItem$,
|
||||||
]).pipe(
|
]).pipe(
|
||||||
switchMap(([currentPagination, currentItemIsLeftItem]: [PaginationComponentOptions, boolean]) =>
|
switchMap(([currentPagination, currentItemIsLeftItem]: [PaginationComponentOptions, boolean]) => {
|
||||||
// get the relationships for the current item, relationshiptype and page
|
// get the relationships for the current item, relationshiptype and page
|
||||||
this.relationshipService.getItemRelationshipsByLabel(
|
return this.relationshipService.getItemRelationshipsByLabel(
|
||||||
this.item,
|
this.item,
|
||||||
currentItemIsLeftItem ? this.relationshipType.leftwardType : this.relationshipType.rightwardType,
|
currentItemIsLeftItem ? this.relationshipType.leftwardType : this.relationshipType.rightwardType,
|
||||||
{
|
{
|
||||||
elementsPerPage: currentPagination.pageSize,
|
elementsPerPage: currentPagination.pageSize,
|
||||||
currentPage: currentPagination.currentPage,
|
currentPage: currentPagination.currentPage,
|
||||||
},
|
},
|
||||||
false,
|
true,
|
||||||
true,
|
true,
|
||||||
...linksToFollow,
|
...linksToFollow,
|
||||||
)),
|
);
|
||||||
|
}),
|
||||||
).subscribe((rd: RemoteData<PaginatedList<Relationship>>) => {
|
).subscribe((rd: RemoteData<PaginatedList<Relationship>>) => {
|
||||||
this.relationshipsRd$.next(rd);
|
this.relationshipsRd$.next(rd);
|
||||||
}),
|
}),
|
||||||
@@ -595,6 +544,8 @@ export class EditRelationshipListComponent implements OnInit, OnDestroy {
|
|||||||
uuid: relationship.id,
|
uuid: relationship.id,
|
||||||
type: this.relationshipType,
|
type: this.relationshipType,
|
||||||
relationship,
|
relationship,
|
||||||
|
originalIsLeft: isLeftItem,
|
||||||
|
originalItem: this.item,
|
||||||
nameVariant,
|
nameVariant,
|
||||||
} as RelationshipIdentifiable;
|
} as RelationshipIdentifiable;
|
||||||
}),
|
}),
|
||||||
|
@@ -167,9 +167,9 @@ export class EditRelationshipComponent implements OnChanges {
|
|||||||
) as DeleteRelationship;
|
) as DeleteRelationship;
|
||||||
}),
|
}),
|
||||||
take(1),
|
take(1),
|
||||||
).subscribe((deleteRelationship: DeleteRelationship) =>
|
).subscribe((deleteRelationship: DeleteRelationship) => {
|
||||||
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, deleteRelationship),
|
this.objectUpdatesService.saveRemoveFieldUpdate(this.url, deleteRelationship);
|
||||||
);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
openVirtualMetadataModal(content: any) {
|
openVirtualMetadataModal(content: any) {
|
||||||
|
@@ -1,63 +1,55 @@
|
|||||||
<div class="item-relationships">
|
<div class="item-relationships">
|
||||||
<ng-container *ngVar="entityType$ | async as entityType">
|
<ng-container *ngIf="entityType$ | async as entityType; else noEntityType">
|
||||||
<ng-container *ngIf="entityType">
|
<div class="button-row top d-flex justify-content-end">
|
||||||
<div class="button-row top d-flex space-children-mr">
|
<ng-container *ngTemplateOutlet="buttons"></ng-container>
|
||||||
<button class="btn btn-danger ml-auto" *ngIf="(isReinstatable() | async) !== true"
|
|
||||||
[disabled]="(hasChanges() | async) !== true"
|
|
||||||
(click)="discard()"><i
|
|
||||||
class="fas fa-times"></i>
|
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-warning ml-auto" *ngIf="isReinstatable() | async"
|
|
||||||
(click)="reinstate()"><i
|
|
||||||
class="fas fa-undo-alt"></i>
|
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-primary" [disabled]="(hasChanges() | async) !== true"
|
|
||||||
(click)="submit()"><i
|
|
||||||
class="fas fa-save"></i>
|
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
<ng-container *ngVar="relationshipTypes$ | async as relationshipTypes">
|
<div *ngIf="relationshipTypes$ | async as relationshipTypes; else loading" class="mb-4">
|
||||||
<ng-container *ngIf="relationshipTypes">
|
<div *ngFor="let relationshipType of relationshipTypes; trackBy: trackById" class="mb-4">
|
||||||
<div *ngFor="let relationshipType of relationshipTypes" class="mb-4">
|
|
||||||
<ds-edit-relationship-list
|
<ds-edit-relationship-list
|
||||||
[url]="url"
|
[url]="url"
|
||||||
[item]="item"
|
[item]="item"
|
||||||
[itemType]="entityType$ | async"
|
[itemType]="entityType"
|
||||||
[relationshipType]="relationshipType"
|
[relationshipType]="relationshipType"
|
||||||
[hasChanges] = hasChanges()
|
[hasChanges]="hasChanges$"
|
||||||
(submit) = submit()
|
|
||||||
></ds-edit-relationship-list>
|
></ds-edit-relationship-list>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</div>
|
||||||
<ds-loading *ngIf="!relationshipTypes"></ds-loading>
|
|
||||||
</ng-container>
|
|
||||||
<div class="button-row bottom">
|
<div class="button-row bottom">
|
||||||
<div class="float-right space-children-mr ml-gap">
|
<div class="float-right ml-gap">
|
||||||
<button class="btn btn-danger" *ngIf="(isReinstatable() | async) !== true"
|
<ng-container *ngTemplateOutlet="buttons"></ng-container>
|
||||||
[disabled]="(hasChanges() | async) !== true"
|
|
||||||
(click)="discard()"><i
|
|
||||||
class="fas fa-times"></i>
|
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.discard-button" | translate}}</span>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-warning" *ngIf="isReinstatable() | async"
|
|
||||||
(click)="reinstate()"><i
|
|
||||||
class="fas fa-undo-alt"></i>
|
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.reinstate-button" | translate}}</span>
|
|
||||||
</button>
|
|
||||||
<button class="btn btn-primary" [disabled]="(hasChanges() | async) !== true"
|
|
||||||
(click)="submit()"><i
|
|
||||||
class="fas fa-save"></i>
|
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
|
||||||
</button>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
<div *ngIf="!entityType"
|
|
||||||
class="alert alert-info mt-2" role="alert">
|
|
||||||
{{ 'item.edit.relationships.no-entity-type' | translate }}
|
|
||||||
</div>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<ng-template #noEntityType>
|
||||||
|
<ds-alert [type]="AlertType.Info" class="d-block mt-2">
|
||||||
|
{{ 'item.edit.relationships.no-entity-type' | translate }}
|
||||||
|
</ds-alert>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #loading>
|
||||||
|
<ds-loading></ds-loading>
|
||||||
|
</ng-template>
|
||||||
|
|
||||||
|
<ng-template #buttons>
|
||||||
|
<div class="d-flex space-children-mr justify-content-end">
|
||||||
|
<button class="btn btn-danger" *ngIf="(isReinstatable$ | async) !== true"
|
||||||
|
[disabled]="(hasChanges$ | async) !== true"
|
||||||
|
(click)="discard()">
|
||||||
|
<i aria-hidden="true" class="fas fa-times"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{ 'item.edit.metadata.discard-button' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-warning" *ngIf="isReinstatable$ | async" (click)="reinstate()">
|
||||||
|
<i aria-hidden="true" class="fas fa-undo-alt"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{ 'item.edit.metadata.reinstate-button' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
<button class="btn btn-primary"
|
||||||
|
[disabled]="(hasChanges$ | async) !== true || (isSaving$ | async) === true"
|
||||||
|
(click)="submit()">
|
||||||
|
<span *ngIf="isSaving$ | async" aria-hidden="true" class="spinner-border spinner-border-sm" role="status"></span>
|
||||||
|
<i *ngIf="(isSaving$ | async) !== true" aria-hidden="true" class="fas fa-save"></i>
|
||||||
|
<span class="d-none d-sm-inline"> {{ 'item.edit.metadata.save-button' | translate }}</span>
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
@@ -13,12 +13,10 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { getTestScheduler } from 'jasmine-marbles';
|
|
||||||
import {
|
import {
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import { TestScheduler } from 'rxjs/testing';
|
|
||||||
|
|
||||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
@@ -33,6 +31,7 @@ import { Item } from '../../../core/shared/item.model';
|
|||||||
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
||||||
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
||||||
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import { AlertComponent } from '../../../shared/alert/alert.component';
|
||||||
import { getMockThemeService } from '../../../shared/mocks/theme-service.mock';
|
import { getMockThemeService } from '../../../shared/mocks/theme-service.mock';
|
||||||
import {
|
import {
|
||||||
INotification,
|
INotification,
|
||||||
@@ -44,6 +43,7 @@ import {
|
|||||||
createSuccessfulRemoteDataObject,
|
createSuccessfulRemoteDataObject,
|
||||||
createSuccessfulRemoteDataObject$,
|
createSuccessfulRemoteDataObject$,
|
||||||
} from '../../../shared/remote-data.utils';
|
} from '../../../shared/remote-data.utils';
|
||||||
|
import { ItemDataServiceStub } from '../../../shared/testing/item-data.service.stub';
|
||||||
import { relationshipTypes } from '../../../shared/testing/relationship-types.mock';
|
import { relationshipTypes } from '../../../shared/testing/relationship-types.mock';
|
||||||
import { RouterStub } from '../../../shared/testing/router.stub';
|
import { RouterStub } from '../../../shared/testing/router.stub';
|
||||||
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
@@ -72,12 +72,11 @@ const notificationsService = jasmine.createSpyObj('notificationsService',
|
|||||||
const router = new RouterStub();
|
const router = new RouterStub();
|
||||||
let relationshipTypeService;
|
let relationshipTypeService;
|
||||||
let routeStub;
|
let routeStub;
|
||||||
let itemService;
|
let itemService: ItemDataServiceStub;
|
||||||
|
|
||||||
const url = 'http://test-url.com/test-url';
|
const url = 'http://test-url.com/test-url';
|
||||||
router.url = url;
|
router.url = url;
|
||||||
|
|
||||||
let scheduler: TestScheduler;
|
|
||||||
let item;
|
let item;
|
||||||
let author1;
|
let author1;
|
||||||
let author2;
|
let author2;
|
||||||
@@ -157,10 +156,7 @@ describe('ItemRelationshipsComponent', () => {
|
|||||||
changeType: FieldChangeType.REMOVE,
|
changeType: FieldChangeType.REMOVE,
|
||||||
};
|
};
|
||||||
|
|
||||||
itemService = jasmine.createSpyObj('itemService', {
|
itemService = new ItemDataServiceStub();
|
||||||
findByHref: createSuccessfulRemoteDataObject$(item),
|
|
||||||
findById: createSuccessfulRemoteDataObject$(item),
|
|
||||||
});
|
|
||||||
routeStub = {
|
routeStub = {
|
||||||
data: observableOf({}),
|
data: observableOf({}),
|
||||||
parent: {
|
parent: {
|
||||||
@@ -228,7 +224,6 @@ describe('ItemRelationshipsComponent', () => {
|
|||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
scheduler = getTestScheduler();
|
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [TranslateModule.forRoot(), ItemRelationshipsComponent],
|
imports: [TranslateModule.forRoot(), ItemRelationshipsComponent],
|
||||||
providers: [
|
providers: [
|
||||||
@@ -247,10 +242,18 @@ describe('ItemRelationshipsComponent', () => {
|
|||||||
], schemas: [
|
], schemas: [
|
||||||
NO_ERRORS_SCHEMA,
|
NO_ERRORS_SCHEMA,
|
||||||
],
|
],
|
||||||
|
}).overrideComponent(ItemRelationshipsComponent, {
|
||||||
|
remove: {
|
||||||
|
imports: [
|
||||||
|
AlertComponent,
|
||||||
|
],
|
||||||
|
},
|
||||||
}).compileComponents();
|
}).compileComponents();
|
||||||
}));
|
}));
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
spyOn(itemService, 'findByHref').and.returnValue(item);
|
||||||
|
spyOn(itemService, 'findById').and.returnValue(item);
|
||||||
fixture = TestBed.createComponent(ItemRelationshipsComponent);
|
fixture = TestBed.createComponent(ItemRelationshipsComponent);
|
||||||
comp = fixture.componentInstance;
|
comp = fixture.componentInstance;
|
||||||
de = fixture.debugElement;
|
de = fixture.debugElement;
|
||||||
@@ -285,7 +288,7 @@ describe('ItemRelationshipsComponent', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('it should delete the correct relationship', () => {
|
it('it should delete the correct relationship', () => {
|
||||||
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid, 'left');
|
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid, 'left', false);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -2,6 +2,7 @@ import {
|
|||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
NgForOf,
|
NgForOf,
|
||||||
NgIf,
|
NgIf,
|
||||||
|
NgTemplateOutlet,
|
||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
ChangeDetectorRef,
|
ChangeDetectorRef,
|
||||||
@@ -17,49 +18,37 @@ import {
|
|||||||
TranslateService,
|
TranslateService,
|
||||||
} from '@ngx-translate/core';
|
} from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
combineLatest as observableCombineLatest,
|
BehaviorSubject,
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
|
||||||
zip as observableZip,
|
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
|
distinctUntilChanged,
|
||||||
map,
|
map,
|
||||||
startWith,
|
|
||||||
switchMap,
|
|
||||||
take,
|
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||||
import { EntityTypeDataService } from '../../../core/data/entity-type-data.service';
|
import { EntityTypeDataService } from '../../../core/data/entity-type-data.service';
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
import { FieldChangeType } from '../../../core/data/object-updates/field-change-type.model';
|
|
||||||
import { FieldUpdate } from '../../../core/data/object-updates/field-update.model';
|
|
||||||
import { FieldUpdates } from '../../../core/data/object-updates/field-updates.model';
|
|
||||||
import {
|
|
||||||
DeleteRelationship,
|
|
||||||
RelationshipIdentifiable,
|
|
||||||
} from '../../../core/data/object-updates/object-updates.reducer';
|
|
||||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
|
import { RelationshipDataService } from '../../../core/data/relationship-data.service';
|
||||||
import { RelationshipTypeDataService } from '../../../core/data/relationship-type-data.service';
|
import { RelationshipTypeDataService } from '../../../core/data/relationship-type-data.service';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
|
||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
|
||||||
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
import { ItemType } from '../../../core/shared/item-relationships/item-type.model';
|
||||||
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
|
|
||||||
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../../../core/shared/item-relationships/relationship-type.model';
|
||||||
import { NoContent } from '../../../core/shared/NoContent.model';
|
|
||||||
import {
|
import {
|
||||||
getFirstSucceededRemoteData,
|
getFirstSucceededRemoteData,
|
||||||
getRemoteDataPayload,
|
getRemoteDataPayload,
|
||||||
} from '../../../core/shared/operators';
|
} from '../../../core/shared/operators';
|
||||||
import { hasValue } from '../../../shared/empty.util';
|
import { AlertComponent } from '../../../shared/alert/alert.component';
|
||||||
|
import { AlertType } from '../../../shared/alert/alert-type';
|
||||||
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../../shared/loading/themed-loading.component';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
||||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
|
import { compareArraysUsingIds } from '../../simple/item-types/shared/item-relationships-utils';
|
||||||
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
|
||||||
|
import { EditItemRelationshipsService } from './edit-item-relationships.service';
|
||||||
import { EditRelationshipListComponent } from './edit-relationship-list/edit-relationship-list.component';
|
import { EditRelationshipListComponent } from './edit-relationship-list/edit-relationship-list.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -67,12 +56,14 @@ import { EditRelationshipListComponent } from './edit-relationship-list/edit-rel
|
|||||||
styleUrls: ['./item-relationships.component.scss'],
|
styleUrls: ['./item-relationships.component.scss'],
|
||||||
templateUrl: './item-relationships.component.html',
|
templateUrl: './item-relationships.component.html',
|
||||||
imports: [
|
imports: [
|
||||||
ThemedLoadingComponent,
|
AlertComponent,
|
||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
TranslateModule,
|
|
||||||
NgIf,
|
|
||||||
EditRelationshipListComponent,
|
EditRelationshipListComponent,
|
||||||
NgForOf,
|
NgForOf,
|
||||||
|
NgIf,
|
||||||
|
NgTemplateOutlet,
|
||||||
|
ThemedLoadingComponent,
|
||||||
|
TranslateModule,
|
||||||
VarDirective,
|
VarDirective,
|
||||||
],
|
],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
@@ -91,7 +82,13 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
|
|||||||
/**
|
/**
|
||||||
* The item's entity type as an observable
|
* The item's entity type as an observable
|
||||||
*/
|
*/
|
||||||
entityType$: Observable<ItemType>;
|
entityType$: BehaviorSubject<ItemType> = new BehaviorSubject(undefined);
|
||||||
|
|
||||||
|
get isSaving$(): BehaviorSubject<boolean> {
|
||||||
|
return this.editItemRelationshipsService.isSaving$;
|
||||||
|
}
|
||||||
|
|
||||||
|
readonly AlertType = AlertType;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public itemService: ItemDataService,
|
public itemService: ItemDataService,
|
||||||
@@ -107,6 +104,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
|
|||||||
protected relationshipTypeService: RelationshipTypeDataService,
|
protected relationshipTypeService: RelationshipTypeDataService,
|
||||||
public cdr: ChangeDetectorRef,
|
public cdr: ChangeDetectorRef,
|
||||||
protected modalService: NgbModal,
|
protected modalService: NgbModal,
|
||||||
|
protected editItemRelationshipsService: EditItemRelationshipsService,
|
||||||
) {
|
) {
|
||||||
super(itemService, objectUpdatesService, router, notificationsService, translateService, route);
|
super(itemService, objectUpdatesService, router, notificationsService, translateService, route);
|
||||||
}
|
}
|
||||||
@@ -118,18 +116,18 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
|
|||||||
|
|
||||||
const label = this.item.firstMetadataValue('dspace.entity.type');
|
const label = this.item.firstMetadataValue('dspace.entity.type');
|
||||||
if (label !== undefined) {
|
if (label !== undefined) {
|
||||||
this.relationshipTypes$ = this.relationshipTypeService.searchByEntityType(label, true, true, ...this.getRelationshipTypeFollowLinks())
|
this.relationshipTypes$ = this.relationshipTypeService.searchByEntityType(label, true, true, ...this.getRelationshipTypeFollowLinks()).pipe(
|
||||||
.pipe(
|
|
||||||
map((relationshipTypes: PaginatedList<RelationshipType>) => relationshipTypes.page),
|
map((relationshipTypes: PaginatedList<RelationshipType>) => relationshipTypes.page),
|
||||||
|
distinctUntilChanged(compareArraysUsingIds()),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.entityType$ = this.entityTypeService.getEntityTypeByLabel(label).pipe(
|
this.entityTypeService.getEntityTypeByLabel(label).pipe(
|
||||||
getFirstSucceededRemoteData(),
|
getFirstSucceededRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
);
|
).subscribe((type) => this.entityType$.next(type));
|
||||||
|
|
||||||
} else {
|
} else {
|
||||||
this.entityType$ = observableOf(undefined);
|
this.entityType$.next(undefined);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -145,127 +143,24 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent {
|
|||||||
* Make sure the lists are refreshed afterwards and notifications are sent for success and errors
|
* Make sure the lists are refreshed afterwards and notifications are sent for success and errors
|
||||||
*/
|
*/
|
||||||
public submit(): void {
|
public submit(): void {
|
||||||
|
this.editItemRelationshipsService.submit(this.item, this.url);
|
||||||
// Get all the relationships that should be removed
|
|
||||||
const removedRelationshipIDs$: Observable<DeleteRelationship[]> = this.relationshipService.getItemRelationshipsArray(this.item).pipe(
|
|
||||||
startWith([]),
|
|
||||||
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),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
const addRelatedItems$: Observable<RelationshipIdentifiable[]> = this.objectUpdatesService.getFieldUpdates(this.url, []).pipe(
|
|
||||||
map((fieldUpdates: FieldUpdates) =>
|
|
||||||
Object.values(fieldUpdates)
|
|
||||||
.filter((fieldUpdate: FieldUpdate) => hasValue(fieldUpdate))
|
|
||||||
.filter((fieldUpdate: FieldUpdate) => fieldUpdate.changeType === FieldChangeType.ADD)
|
|
||||||
.map((fieldUpdate: FieldUpdate) => fieldUpdate.field as RelationshipIdentifiable),
|
|
||||||
),
|
|
||||||
);
|
|
||||||
|
|
||||||
observableCombineLatest(
|
|
||||||
removedRelationshipIDs$,
|
|
||||||
addRelatedItems$,
|
|
||||||
).pipe(
|
|
||||||
take(1),
|
|
||||||
).subscribe(([removeRelationshipIDs, addRelatedItems]) => {
|
|
||||||
const actions = [
|
|
||||||
this.deleteRelationships(removeRelationshipIDs),
|
|
||||||
this.addRelationships(addRelatedItems),
|
|
||||||
];
|
|
||||||
actions.forEach((action) =>
|
|
||||||
action.subscribe((response) => {
|
|
||||||
if (response.length > 0) {
|
|
||||||
this.initializeOriginalFields();
|
|
||||||
this.cdr.detectChanges();
|
|
||||||
this.displayNotifications(response);
|
|
||||||
this.modalService.dismissAll();
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
deleteRelationships(deleteRelationshipIDs: DeleteRelationship[]): Observable<RemoteData<NoContent>[]> {
|
|
||||||
return observableZip(...deleteRelationshipIDs.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);
|
|
||||||
},
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
addRelationships(addRelatedItems: RelationshipIdentifiable[]): Observable<RemoteData<Relationship>[]> {
|
|
||||||
return observableZip(...addRelatedItems.map((addRelationship) =>
|
|
||||||
this.entityType$.pipe(
|
|
||||||
switchMap((entityType) => this.entityTypeService.isLeftType(addRelationship.type, entityType)),
|
|
||||||
switchMap((isLeftType) => {
|
|
||||||
let leftItem: Item;
|
|
||||||
let rightItem: Item;
|
|
||||||
let leftwardValue: string;
|
|
||||||
let rightwardValue: string;
|
|
||||||
if (isLeftType) {
|
|
||||||
leftItem = this.item;
|
|
||||||
rightItem = addRelationship.relatedItem;
|
|
||||||
leftwardValue = null;
|
|
||||||
rightwardValue = addRelationship.nameVariant;
|
|
||||||
} else {
|
|
||||||
leftItem = addRelationship.relatedItem;
|
|
||||||
rightItem = this.item;
|
|
||||||
leftwardValue = addRelationship.nameVariant;
|
|
||||||
rightwardValue = null;
|
|
||||||
}
|
|
||||||
return this.relationshipService.addRelationship(addRelationship.type.id, leftItem, rightItem, leftwardValue, rightwardValue);
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Display notifications
|
|
||||||
* - Error notification for each failed response with their message
|
|
||||||
* - Success notification in case there's at least one successful response
|
|
||||||
* @param responses
|
|
||||||
*/
|
|
||||||
displayNotifications(responses: RemoteData<NoContent>[]) {
|
|
||||||
const failedResponses = responses.filter((response: RemoteData<NoContent>) => response.hasFailed);
|
|
||||||
const successfulResponses = responses.filter((response: RemoteData<NoContent>) => response.hasSucceeded);
|
|
||||||
|
|
||||||
failedResponses.forEach((response: RemoteData<NoContent>) => {
|
|
||||||
this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage);
|
|
||||||
});
|
|
||||||
if (successfulResponses.length > 0) {
|
|
||||||
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
/**
|
||||||
* Sends all initial values of this item to the object updates service
|
* Sends all initial values of this item to the object updates service
|
||||||
*/
|
*/
|
||||||
public initializeOriginalFields() {
|
public initializeOriginalFields() {
|
||||||
return this.relationshipService.getRelatedItems(this.item).pipe(
|
return this.editItemRelationshipsService.initializeOriginalFields(this.item, this.url);
|
||||||
take(1),
|
|
||||||
).subscribe((items: Item[]) => {
|
|
||||||
this.objectUpdatesService.initialize(this.url, items, this.item.lastModified);
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method to prevent unnecessary for loop re-rendering
|
||||||
|
*/
|
||||||
|
trackById(index: number, relationshipType: RelationshipType): string {
|
||||||
|
return relationshipType.id;
|
||||||
|
}
|
||||||
|
|
||||||
getRelationshipTypeFollowLinks() {
|
getRelationshipTypeFollowLinks() {
|
||||||
return [
|
return [
|
||||||
followLink('leftType'),
|
followLink('leftType'),
|
||||||
|
@@ -5,9 +5,9 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<ng-container *ngFor="let item of items; trackBy: trackItem">
|
<ng-container *ngFor="let itemDTO of itemDTOs$ | async; trackBy: trackItemDTO">
|
||||||
<div *ngVar="(isSelectedVirtualMetadataItem(item) | async) as selected"
|
<div *ngVar="(itemDTO.isSelectedVirtualMetadataItem$ | async) as selected"
|
||||||
(click)="setSelectedVirtualMetadataItem(item, !selected)"
|
(click)="setSelectedVirtualMetadataItem(itemDTO.item, !selected)"
|
||||||
class="item d-flex flex-row">
|
class="item d-flex flex-row">
|
||||||
<div class="m-2">
|
<div class="m-2">
|
||||||
<label>
|
<label>
|
||||||
@@ -15,9 +15,9 @@
|
|||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-column">
|
<div class="flex-column">
|
||||||
<ds-listable-object-component-loader [object]="item">
|
<ds-listable-object-component-loader [object]="itemDTO.item">
|
||||||
</ds-listable-object-component-loader>
|
</ds-listable-object-component-loader>
|
||||||
<div *ngFor="let metadata of virtualMetadata.get(item.uuid)">
|
<div *ngFor="let metadata of virtualMetadata.get(itemDTO.item.uuid)">
|
||||||
<div class="font-weight-bold">
|
<div class="font-weight-bold">
|
||||||
{{metadata.metadataField}}
|
{{metadata.metadataField}}
|
||||||
</div>
|
</div>
|
||||||
@@ -31,7 +31,7 @@
|
|||||||
<div class="d-flex flex-row-reverse m-2">
|
<div class="d-flex flex-row-reverse m-2">
|
||||||
<button class="btn btn-primary save"
|
<button class="btn btn-primary save"
|
||||||
(click)="save.emit()">
|
(click)="save.emit()">
|
||||||
<i class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
<i aria-hidden="true" class="fas fa-save"></i> {{ 'item.edit.metadata.save-button' | translate }}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -77,6 +77,7 @@ describe('VirtualMetadataComponent', () => {
|
|||||||
comp.url = url;
|
comp.url = url;
|
||||||
comp.leftItem = item;
|
comp.leftItem = item;
|
||||||
comp.rightItem = relatedItem;
|
comp.rightItem = relatedItem;
|
||||||
|
comp.ngOnChanges();
|
||||||
comp.relationshipId = relationshipId;
|
comp.relationshipId = relationshipId;
|
||||||
|
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
|
@@ -8,11 +8,17 @@ import {
|
|||||||
EventEmitter,
|
EventEmitter,
|
||||||
Inject,
|
Inject,
|
||||||
Input,
|
Input,
|
||||||
|
OnChanges,
|
||||||
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
Output,
|
Output,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { Observable } from 'rxjs';
|
import {
|
||||||
|
BehaviorSubject,
|
||||||
|
Observable,
|
||||||
|
Subscription,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
import {
|
import {
|
||||||
APP_CONFIG,
|
APP_CONFIG,
|
||||||
@@ -21,9 +27,18 @@ import {
|
|||||||
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { MetadataValue } from '../../../core/shared/metadata.models';
|
import { MetadataValue } from '../../../core/shared/metadata.models';
|
||||||
|
import { hasValue } from '../../../shared/empty.util';
|
||||||
import { ListableObjectComponentLoaderComponent } from '../../../shared/object-collection/shared/listable-object/listable-object-component-loader.component';
|
import { ListableObjectComponentLoaderComponent } from '../../../shared/object-collection/shared/listable-object/listable-object-component-loader.component';
|
||||||
import { VarDirective } from '../../../shared/utils/var.directive';
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
|
|
||||||
|
interface ItemDTO {
|
||||||
|
|
||||||
|
item: Item;
|
||||||
|
|
||||||
|
isSelectedVirtualMetadataItem$: Observable<boolean>;
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-virtual-metadata',
|
selector: 'ds-virtual-metadata',
|
||||||
templateUrl: './virtual-metadata.component.html',
|
templateUrl: './virtual-metadata.component.html',
|
||||||
@@ -42,7 +57,7 @@ import { VarDirective } from '../../../shared/utils/var.directive';
|
|||||||
* The component is shown when a relationship is marked to be deleted.
|
* 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.
|
* Each item has a checkbox to indicate whether its virtual metadata should be saved as real metadata.
|
||||||
*/
|
*/
|
||||||
export class VirtualMetadataComponent implements OnInit {
|
export class VirtualMetadataComponent implements OnInit, OnChanges, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current url of this page
|
* The current url of this page
|
||||||
@@ -83,9 +98,9 @@ export class VirtualMetadataComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Get an array of the left and the right item of the relationship to be deleted.
|
* Get an array of the left and the right item of the relationship to be deleted.
|
||||||
*/
|
*/
|
||||||
get items() {
|
itemDTOs$: BehaviorSubject<ItemDTO[]> = new BehaviorSubject([]);
|
||||||
return [this.leftItem, this.rightItem];
|
|
||||||
}
|
subs: Subscription[] = [];
|
||||||
|
|
||||||
public virtualMetadata: Map<string, VirtualMetadata[]> = new Map<string, VirtualMetadata[]>();
|
public virtualMetadata: Map<string, VirtualMetadata[]> = new Map<string, VirtualMetadata[]>();
|
||||||
|
|
||||||
@@ -137,14 +152,33 @@ export class VirtualMetadataComponent implements OnInit {
|
|||||||
/**
|
/**
|
||||||
* Prevent unnecessary rerendering so fields don't lose focus
|
* Prevent unnecessary rerendering so fields don't lose focus
|
||||||
*/
|
*/
|
||||||
trackItem(index, item: Item) {
|
trackItemDTO(index, itemDTO: ItemDTO): string {
|
||||||
return item && item.uuid;
|
return itemDTO?.item?.uuid;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.items.forEach((item) => {
|
this.subs.push(this.itemDTOs$.subscribe((itemDTOs: ItemDTO[]) => {
|
||||||
this.virtualMetadata.set(item.uuid, this.getVirtualMetadata(item));
|
itemDTOs.forEach((itemDTO: ItemDTO) => this.virtualMetadata.set(itemDTO.item.uuid, this.getVirtualMetadata(itemDTO.item)));
|
||||||
});
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnChanges(): void {
|
||||||
|
if (hasValue(this.leftItem) && hasValue(this.rightItem)) {
|
||||||
|
this.itemDTOs$.next([
|
||||||
|
{
|
||||||
|
item: this.leftItem,
|
||||||
|
isSelectedVirtualMetadataItem$: this.isSelectedVirtualMetadataItem(this.leftItem),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
item: this.rightItem,
|
||||||
|
isSelectedVirtualMetadataItem$: this.isSelectedVirtualMetadataItem(this.rightItem),
|
||||||
|
},
|
||||||
|
]);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.subs.forEach((sub: Subscription) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -32,7 +32,7 @@
|
|||||||
</ng-template>
|
</ng-template>
|
||||||
</li>
|
</li>
|
||||||
<li ngbNavItem *ngFor="let source of (externalSourcesRD$ | async); let idx = index" role="presentation">
|
<li ngbNavItem *ngFor="let source of (externalSourcesRD$ | async); let idx = index" role="presentation">
|
||||||
<a ngbNavLink>{{'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + source.id | translate : { count: (totalExternal$ | async)[idx] } }}</a>
|
<a ngbNavLink>{{'submission.sections.describe.relationship-lookup.search-tab.tab-title.' + source.id | translate : { count: (totalExternal$ | async)?.[idx] } }}</a>
|
||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<ds-dynamic-lookup-relation-external-source-tab
|
<ds-dynamic-lookup-relation-external-source-tab
|
||||||
[label]="label"
|
[label]="label"
|
||||||
@@ -87,7 +87,7 @@
|
|||||||
(click)="submitEv()">
|
(click)="submitEv()">
|
||||||
<span *ngIf="isPending" class="spinner-border spinner-border-sm" role="status"
|
<span *ngIf="isPending" class="spinner-border spinner-border-sm" role="status"
|
||||||
aria-hidden="true"></span>
|
aria-hidden="true"></span>
|
||||||
<i class="fas fa-save"></i>
|
<i *ngIf="!isPending" class="fas fa-save"></i>
|
||||||
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
<span class="d-none d-sm-inline"> {{"item.edit.metadata.save-button" | translate}}</span>
|
||||||
</button>
|
</button>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -41,6 +41,7 @@ import { PaginatedList } from '../../../../../core/data/paginated-list.model';
|
|||||||
import { RelationshipDataService } from '../../../../../core/data/relationship-data.service';
|
import { RelationshipDataService } from '../../../../../core/data/relationship-data.service';
|
||||||
import { RelationshipTypeDataService } from '../../../../../core/data/relationship-type-data.service';
|
import { RelationshipTypeDataService } from '../../../../../core/data/relationship-type-data.service';
|
||||||
import { Context } from '../../../../../core/shared/context.model';
|
import { Context } from '../../../../../core/shared/context.model';
|
||||||
|
import { DSpaceObject } from '../../../../../core/shared/dspace-object.model';
|
||||||
import { ExternalSource } from '../../../../../core/shared/external-source.model';
|
import { ExternalSource } from '../../../../../core/shared/external-source.model';
|
||||||
import { Item } from '../../../../../core/shared/item.model';
|
import { Item } from '../../../../../core/shared/item.model';
|
||||||
import { RelationshipType } from '../../../../../core/shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
@@ -55,6 +56,7 @@ import {
|
|||||||
isNotEmpty,
|
isNotEmpty,
|
||||||
} from '../../../../empty.util';
|
} from '../../../../empty.util';
|
||||||
import { ThemedLoadingComponent } from '../../../../loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../../../loading/themed-loading.component';
|
||||||
|
import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model';
|
||||||
import { ListableObject } from '../../../../object-collection/shared/listable-object.model';
|
import { ListableObject } from '../../../../object-collection/shared/listable-object.model';
|
||||||
import { SelectableListState } from '../../../../object-list/selectable-list/selectable-list.reducer';
|
import { SelectableListState } from '../../../../object-list/selectable-list/selectable-list.reducer';
|
||||||
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
|
import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service';
|
||||||
@@ -193,12 +195,12 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
/**
|
/**
|
||||||
* Maintain the list of the related items to be added
|
* Maintain the list of the related items to be added
|
||||||
*/
|
*/
|
||||||
toAdd = [];
|
toAdd: ItemSearchResult[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Maintain the list of the related items to be removed
|
* Maintain the list of the related items to be removed
|
||||||
*/
|
*/
|
||||||
toRemove = [];
|
toRemove: ItemSearchResult[] = [];
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Disable buttons while the submit button is pressed
|
* Disable buttons while the submit button is pressed
|
||||||
@@ -282,7 +284,7 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
* Select (a list of) objects and add them to the store
|
* Select (a list of) objects and add them to the store
|
||||||
* @param selectableObjects
|
* @param selectableObjects
|
||||||
*/
|
*/
|
||||||
select(...selectableObjects: SearchResult<Item>[]) {
|
select(...selectableObjects: SearchResult<DSpaceObject>[]) {
|
||||||
this.zone.runOutsideAngular(
|
this.zone.runOutsideAngular(
|
||||||
() => {
|
() => {
|
||||||
const obs: Observable<any[]> = observableCombineLatest([...selectableObjects.map((sri: SearchResult<Item>) => {
|
const obs: Observable<any[]> = observableCombineLatest([...selectableObjects.map((sri: SearchResult<Item>) => {
|
||||||
@@ -325,11 +327,11 @@ export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy
|
|||||||
* Deselect (a list of) objects and remove them from the store
|
* Deselect (a list of) objects and remove them from the store
|
||||||
* @param selectableObjects
|
* @param selectableObjects
|
||||||
*/
|
*/
|
||||||
deselect(...selectableObjects: SearchResult<Item>[]) {
|
deselect(...selectableObjects: SearchResult<DSpaceObject>[]) {
|
||||||
this.zone.runOutsideAngular(
|
this.zone.runOutsideAngular(
|
||||||
() => selectableObjects.forEach((object) => {
|
() => selectableObjects.forEach((object) => {
|
||||||
this.subMap[object.indexableObject.uuid].unsubscribe();
|
this.subMap[object.indexableObject.uuid].unsubscribe();
|
||||||
this.store.dispatch(new RemoveRelationshipAction(this.item, object.indexableObject, this.relationshipOptions.relationshipType, this.submissionId));
|
this.store.dispatch(new RemoveRelationshipAction(this.item, object.indexableObject as Item, this.relationshipOptions.relationshipType, this.submissionId));
|
||||||
}),
|
}),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@@ -132,12 +132,12 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
|
|||||||
/**
|
/**
|
||||||
* Send an event to deselect an object from the list
|
* Send an event to deselect an object from the list
|
||||||
*/
|
*/
|
||||||
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
@Output() deselectObject: EventEmitter<SearchResult<DSpaceObject>> = new EventEmitter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an event to select an object from the list
|
* Send an event to select an object from the list
|
||||||
*/
|
*/
|
||||||
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
@Output() selectObject: EventEmitter<SearchResult<DSpaceObject>> = new EventEmitter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Search results
|
* Search results
|
||||||
@@ -214,7 +214,7 @@ export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDest
|
|||||||
this.selection$
|
this.selection$
|
||||||
.pipe(take(1))
|
.pipe(take(1))
|
||||||
.subscribe((selection: SearchResult<Item>[]) => {
|
.subscribe((selection: SearchResult<Item>[]) => {
|
||||||
const filteredPage = page.filter((pageItem) => selection.findIndex((selected) => selected.equals(pageItem)) < 0);
|
const filteredPage: SearchResult<DSpaceObject>[] = page.filter((pageItem: SearchResult<DSpaceObject>) => selection.findIndex((selected: SearchResult<Item>) => selected.equals(pageItem)) < 0);
|
||||||
this.selectObject.emit(...filteredPage);
|
this.selectObject.emit(...filteredPage);
|
||||||
});
|
});
|
||||||
this.selectableListService.select(this.listId, page);
|
this.selectableListService.select(this.listId, page);
|
||||||
|
@@ -51,9 +51,9 @@ export class ThemedDynamicLookupRelationSearchTabComponent extends ThemedCompone
|
|||||||
|
|
||||||
@Input() isEditRelationship: boolean;
|
@Input() isEditRelationship: boolean;
|
||||||
|
|
||||||
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter();
|
@Output() deselectObject: EventEmitter<SearchResult<DSpaceObject>> = new EventEmitter();
|
||||||
|
|
||||||
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter();
|
@Output() selectObject: EventEmitter<SearchResult<DSpaceObject>> = new EventEmitter();
|
||||||
|
|
||||||
@Output() resultFound: EventEmitter<SearchObjects<DSpaceObject>> = new EventEmitter();
|
@Output() resultFound: EventEmitter<SearchObjects<DSpaceObject>> = new EventEmitter();
|
||||||
|
|
||||||
|
@@ -24,6 +24,7 @@ import {
|
|||||||
import { RemoteData } from '../../../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../../../core/data/remote-data';
|
||||||
import { PaginationService } from '../../../../../../core/pagination/pagination.service';
|
import { PaginationService } from '../../../../../../core/pagination/pagination.service';
|
||||||
import { Context } from '../../../../../../core/shared/context.model';
|
import { Context } from '../../../../../../core/shared/context.model';
|
||||||
|
import { DSpaceObject } from '../../../../../../core/shared/dspace-object.model';
|
||||||
import { PageInfo } from '../../../../../../core/shared/page-info.model';
|
import { PageInfo } from '../../../../../../core/shared/page-info.model';
|
||||||
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
|
import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service';
|
||||||
import { SEARCH_CONFIG_SERVICE } from '../../../../../../my-dspace-page/my-dspace-configuration.service';
|
import { SEARCH_CONFIG_SERVICE } from '../../../../../../my-dspace-page/my-dspace-configuration.service';
|
||||||
@@ -33,6 +34,7 @@ import { PageSizeSelectorComponent } from '../../../../../page-size-selector/pag
|
|||||||
import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model';
|
||||||
import { createSuccessfulRemoteDataObject } from '../../../../../remote-data.utils';
|
import { createSuccessfulRemoteDataObject } from '../../../../../remote-data.utils';
|
||||||
import { PaginatedSearchOptions } from '../../../../../search/models/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../../../../search/models/paginated-search-options.model';
|
||||||
|
import { SearchResult } from '../../../../../search/models/search-result.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-dynamic-lookup-relation-selection-tab',
|
selector: 'ds-dynamic-lookup-relation-selection-tab',
|
||||||
@@ -91,12 +93,12 @@ export class DsDynamicLookupRelationSelectionTabComponent {
|
|||||||
/**
|
/**
|
||||||
* Send an event to deselect an object from the list
|
* Send an event to deselect an object from the list
|
||||||
*/
|
*/
|
||||||
@Output() deselectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
@Output() deselectObject: EventEmitter<SearchResult<DSpaceObject>> = new EventEmitter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Send an event to select an object from the list
|
* Send an event to select an object from the list
|
||||||
*/
|
*/
|
||||||
@Output() selectObject: EventEmitter<ListableObject> = new EventEmitter<ListableObject>();
|
@Output() selectObject: EventEmitter<SearchResult<DSpaceObject>> = new EventEmitter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The initial pagination to use
|
* The initial pagination to use
|
||||||
|
@@ -4,12 +4,19 @@ import {
|
|||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
import { CacheableObject } from '../../core/cache/cacheable-object.model';
|
import { CacheableObject } from '../../core/cache/cacheable-object.model';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||||
|
import { FollowLinkConfig } from '../utils/follow-link-config.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Stub class for {@link BaseDataService}
|
* Stub class for {@link BaseDataService}
|
||||||
*/
|
*/
|
||||||
export abstract class BaseDataServiceStub<T extends CacheableObject> {
|
export abstract class BaseDataServiceStub<T extends CacheableObject> {
|
||||||
|
|
||||||
|
findByHref(_href$: string | Observable<string>, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<T>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
invalidateByHref(_href: string): Observable<boolean> {
|
invalidateByHref(_href: string): Observable<boolean> {
|
||||||
return observableOf(true);
|
return observableOf(true);
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,48 @@
|
|||||||
|
/* eslint-disable no-empty, @typescript-eslint/no-empty-function */
|
||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
Subscription,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import {
|
||||||
|
DeleteRelationship,
|
||||||
|
RelationshipIdentifiable,
|
||||||
|
} from '../../core/data/object-updates/object-updates.reducer';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { Relationship } from '../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { NoContent } from '../../core/shared/NoContent.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub class of {@link EditItemRelationshipsService}
|
||||||
|
*/
|
||||||
|
export class EditItemRelationshipsServiceStub {
|
||||||
|
|
||||||
|
submit(_item: Item, _url: string): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
initializeOriginalFields(_item: Item, _url: string): Subscription {
|
||||||
|
return new Subscription();
|
||||||
|
}
|
||||||
|
|
||||||
|
deleteRelationship(_deleteRelationship: DeleteRelationship): Observable<RemoteData<NoContent>> {
|
||||||
|
return createSuccessfulRemoteDataObject$({});
|
||||||
|
}
|
||||||
|
|
||||||
|
addRelationship(_addRelationship: RelationshipIdentifiable): Observable<RemoteData<Relationship>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
displayNotifications(_responses: RemoteData<NoContent>[]): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotificationTitle(_key: string): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
getNotificationContent(_key: string): string {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
5
src/app/shared/testing/entity-type-data.service.stub.ts
Normal file
5
src/app/shared/testing/entity-type-data.service.stub.ts
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
/**
|
||||||
|
* Stub class of {@link EntityTypeDataService}
|
||||||
|
*/
|
||||||
|
export class EntityTypeDataServiceStub {
|
||||||
|
}
|
8
src/app/shared/testing/item-data.service.stub.ts
Normal file
8
src/app/shared/testing/item-data.service.stub.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { IdentifiableDataServiceStub } from './identifiable-data-service.stub';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub class of {@link ItemDataService}
|
||||||
|
*/
|
||||||
|
export class ItemDataServiceStub extends IdentifiableDataServiceStub<Item> {
|
||||||
|
}
|
24
src/app/shared/testing/object-updates.service.stub.ts
Normal file
24
src/app/shared/testing/object-updates.service.stub.ts
Normal file
@@ -0,0 +1,24 @@
|
|||||||
|
/* eslint-disable no-empty, @typescript-eslint/no-empty-function */
|
||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { FieldUpdates } from '../../core/data/object-updates/field-updates.model';
|
||||||
|
import { Identifiable } from '../../core/data/object-updates/identifiable.model';
|
||||||
|
import { PatchOperationService } from '../../core/data/object-updates/patch-operation-service/patch-operation.service';
|
||||||
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub class of {@link ObjectUpdatesService}
|
||||||
|
*/
|
||||||
|
export class ObjectUpdatesServiceStub {
|
||||||
|
|
||||||
|
initialize(_url: string, _fields: Identifiable[], _lastModified: Date, _patchOperationService?: GenericConstructor<PatchOperationService>): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
getFieldUpdates(_url: string, _initialFields: Identifiable[], _ignoreStates?: boolean): Observable<FieldUpdates> {
|
||||||
|
return observableOf({});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
86
src/app/shared/testing/relationship-data.service.stub.ts
Normal file
86
src/app/shared/testing/relationship-data.service.stub.ts
Normal file
@@ -0,0 +1,86 @@
|
|||||||
|
/* eslint-disable no-empty, @typescript-eslint/no-empty-function */
|
||||||
|
import {
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { FindListOptions } from '../../core/data/find-list-options.model';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { Item } from '../../core/shared/item.model';
|
||||||
|
import { Relationship } from '../../core/shared/item-relationships/relationship.model';
|
||||||
|
import { MetadataValue } from '../../core/shared/metadata.models';
|
||||||
|
import { MetadataRepresentation } from '../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
|
import { NoContent } from '../../core/shared/NoContent.model';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||||
|
import { FollowLinkConfig } from '../utils/follow-link-config.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Stub class of {@link RelationshipDataService}
|
||||||
|
*/
|
||||||
|
export class RelationshipDataServiceStub {
|
||||||
|
|
||||||
|
deleteRelationship(_id: string, _copyVirtualMetadata: string, _shouldRefresh = true): Observable<RemoteData<NoContent>> {
|
||||||
|
return createSuccessfulRemoteDataObject$({});
|
||||||
|
}
|
||||||
|
|
||||||
|
addRelationship(_typeId: string, _item1: Item, _item2: Item, _leftwardValue?: string, _rightwardValue?: string, _shouldRefresh = true): Observable<RemoteData<Relationship>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(new Relationship());
|
||||||
|
}
|
||||||
|
|
||||||
|
refreshRelationshipItemsInCache(_item: Item): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemRelationshipsArray(_item: Item, ..._linksToFollow: FollowLinkConfig<Relationship>[]): Observable<Relationship[]> {
|
||||||
|
return observableOf([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRelatedItems(_item: Item): Observable<Item[]> {
|
||||||
|
return observableOf([]);
|
||||||
|
}
|
||||||
|
|
||||||
|
getRelatedItemsByLabel(_item: Item, _label: string, _options?: FindListOptions): Observable<RemoteData<PaginatedList<Item>>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList<Item>());
|
||||||
|
}
|
||||||
|
|
||||||
|
getItemRelationshipsByLabel(_item: Item, _label: string, _options?: FindListOptions, _useCachedVersionIfAvailable = true, _reRequestOnStale = true, ..._linksToFollow: FollowLinkConfig<Relationship>[]): Observable<RemoteData<PaginatedList<Relationship>>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList<Relationship>());
|
||||||
|
}
|
||||||
|
|
||||||
|
getRelationshipByItemsAndLabel(_item1: Item, _item2: Item, _label: string, _options?: FindListOptions): Observable<Relationship> {
|
||||||
|
return observableOf(new Relationship());
|
||||||
|
}
|
||||||
|
|
||||||
|
setNameVariant(_listID: string, _itemID: string, _nameVariant: string): void {
|
||||||
|
}
|
||||||
|
|
||||||
|
getNameVariant(_listID: string, _itemID: string): Observable<string> {
|
||||||
|
return observableOf('');
|
||||||
|
}
|
||||||
|
|
||||||
|
updateNameVariant(_item1: Item, _item2: Item, _relationshipLabel: string, _nameVariant: string): Observable<RemoteData<Relationship>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(new Relationship());
|
||||||
|
}
|
||||||
|
|
||||||
|
isLeftItem(_relationship: Relationship, _item: Item): Observable<boolean> {
|
||||||
|
return observableOf(false);
|
||||||
|
}
|
||||||
|
|
||||||
|
update(_object: Relationship): Observable<RemoteData<Relationship>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(new Relationship());
|
||||||
|
}
|
||||||
|
|
||||||
|
searchByItemsAndType(_typeId: string, _itemUuid: string, _relationshipLabel: string, _arrayOfItemIds: string[]): Observable<RemoteData<PaginatedList<Relationship>>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList<Relationship>());
|
||||||
|
}
|
||||||
|
|
||||||
|
searchBy(_searchMethod: string, _options?: FindListOptions, _useCachedVersionIfAvailable?: boolean, _reRequestOnStale?: boolean, ..._linksToFollow: FollowLinkConfig<Relationship>[]): Observable<RemoteData<PaginatedList<Relationship>>> {
|
||||||
|
return createSuccessfulRemoteDataObject$(new PaginatedList<Relationship>());
|
||||||
|
}
|
||||||
|
|
||||||
|
resolveMetadataRepresentation(_metadatum: MetadataValue, _parentItem: DSpaceObject, _itemType: string): Observable<MetadataRepresentation> {
|
||||||
|
return observableOf({} as MetadataRepresentation);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -3668,6 +3668,8 @@
|
|||||||
|
|
||||||
"orgunit.page.ror": "ROR Identifier",
|
"orgunit.page.ror": "ROR Identifier",
|
||||||
|
|
||||||
|
"orgunit.search.results.head": "Organizational Unit Search Results",
|
||||||
|
|
||||||
"pagination.options.description": "Pagination options",
|
"pagination.options.description": "Pagination options",
|
||||||
|
|
||||||
"pagination.results-per-page": "Results Per Page",
|
"pagination.results-per-page": "Results Per Page",
|
||||||
|
Reference in New Issue
Block a user