mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-08 18:44:14 +00:00
PR-530 Keep virtual metadata on relationship delete - community feedback
This commit is contained in:
@@ -1,16 +1,15 @@
|
|||||||
<ng-container *ngVar="(updates$ | async) as updates">
|
<h5>{{getRelationshipMessageKey() | async | translate}}</h5>
|
||||||
<div *ngIf="updates">
|
<ng-container *ngVar="updates$ | async as updates">
|
||||||
<h5>{{getRelationshipMessageKey(relationshipLabel) | translate}}</h5>
|
<ng-container *ngIf="updates">
|
||||||
<ng-container *ngVar="(updates | dsObjectValues) as updateValues">
|
<ng-container *ngVar="updates | dsObjectValues as updateValues">
|
||||||
<div *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
<ds-edit-relationship *ngFor="let updateValue of updateValues; trackBy: trackUpdate"
|
||||||
ds-edit-relationship
|
|
||||||
class="relationship-row d-block"
|
class="relationship-row d-block"
|
||||||
[fieldUpdate]="updateValue || {}"
|
[fieldUpdate]="updateValue"
|
||||||
[url]="url"
|
[url]="url"
|
||||||
[editItem]="item"
|
[editItem]="item"
|
||||||
[ngClass]="{'alert alert-danger': updateValue.changeType === 2}">
|
[ngClass]="{'alert alert-danger': updateValue?.changeType === 2}">
|
||||||
</div>
|
</ds-edit-relationship>
|
||||||
<ds-loading *ngIf="updateValues.length == 0" message="{{'loading.items' | translate}}"></ds-loading>
|
|
||||||
</ng-container>
|
</ng-container>
|
||||||
</div>
|
</ng-container>
|
||||||
|
<div *ngIf="!updates">no relationships</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import {EditRelationshipListComponent} from './edit-relationship-list.component';
|
import {EditRelationshipListComponent} from './edit-relationship-list.component';
|
||||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import {RelationshipType} from '../../../../core/shared/item-relationships/relationship-type.model';
|
import {RelationshipType} from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
import { ResourceType } from '../../../../core/shared/resource-type';
|
|
||||||
import {Relationship} from '../../../../core/shared/item-relationships/relationship.model';
|
import {Relationship} from '../../../../core/shared/item-relationships/relationship.model';
|
||||||
import {of as observableOf} from 'rxjs/internal/observable/of';
|
import {of as observableOf} from 'rxjs/internal/observable/of';
|
||||||
import {RemoteData} from '../../../../core/data/remote-data';
|
import {RemoteData} from '../../../../core/data/remote-data';
|
||||||
@@ -12,16 +11,16 @@ import { FieldChangeType } from '../../../../core/data/object-updates/object-upd
|
|||||||
import {SharedModule} from '../../../../shared/shared.module';
|
import {SharedModule} from '../../../../shared/shared.module';
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import {ObjectUpdatesService} from '../../../../core/data/object-updates/object-updates.service';
|
import {ObjectUpdatesService} from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import { RelationshipService } from '../../../../core/data/relationship.service';
|
|
||||||
import {DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';
|
import {DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';
|
||||||
import {By} from '@angular/platform-browser';
|
import {By} from '@angular/platform-browser';
|
||||||
|
import {ItemType} from '../../../../core/shared/item-relationships/item-type.model';
|
||||||
|
|
||||||
let comp: EditRelationshipListComponent;
|
let comp: EditRelationshipListComponent;
|
||||||
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
let fixture: ComponentFixture<EditRelationshipListComponent>;
|
||||||
let de: DebugElement;
|
let de: DebugElement;
|
||||||
|
|
||||||
let objectUpdatesService;
|
let objectUpdatesService;
|
||||||
let relationshipService;
|
let entityTypeService;
|
||||||
|
|
||||||
const url = 'http://test-url.com/test-url';
|
const url = 'http://test-url.com/test-url';
|
||||||
|
|
||||||
@@ -30,42 +29,65 @@ let author1;
|
|||||||
let author2;
|
let author2;
|
||||||
let fieldUpdate1;
|
let fieldUpdate1;
|
||||||
let fieldUpdate2;
|
let fieldUpdate2;
|
||||||
let relationships;
|
let relationship1;
|
||||||
|
let relationship2;
|
||||||
let relationshipType;
|
let relationshipType;
|
||||||
|
let entityType;
|
||||||
|
let relatedEntityType;
|
||||||
|
|
||||||
describe('EditRelationshipListComponent', () => {
|
describe('EditRelationshipListComponent', () => {
|
||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
|
|
||||||
relationshipType = Object.assign(new RelationshipType(), {
|
relationshipType = Object.assign(new RelationshipType(), {
|
||||||
id: '1',
|
id: '1',
|
||||||
uuid: '1',
|
uuid: '1',
|
||||||
leftwardType: 'isAuthorOfPublication',
|
leftwardType: 'isAuthorOfPublication',
|
||||||
rightwardType: 'isPublicationOfAuthor'
|
rightwardType: 'isPublicationOfAuthor',
|
||||||
|
leftType: observableOf(new RemoteData(false, false, true, undefined, entityType)),
|
||||||
|
rightType: observableOf(new RemoteData(false, false, true, undefined, relatedEntityType)),
|
||||||
});
|
});
|
||||||
|
|
||||||
relationships = [
|
relationship1 = Object.assign(new Relationship(), {
|
||||||
Object.assign(new Relationship(), {
|
|
||||||
self: url + '/2',
|
self: url + '/2',
|
||||||
id: '2',
|
id: '2',
|
||||||
uuid: '2',
|
uuid: '2',
|
||||||
leftId: 'author1',
|
leftId: 'author1',
|
||||||
rightId: 'publication',
|
rightId: 'publication',
|
||||||
|
leftItem: observableOf(new RemoteData(false, false, true, undefined, item)),
|
||||||
|
rightItem: observableOf(new RemoteData(false, false, true, undefined, author1)),
|
||||||
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
||||||
}),
|
});
|
||||||
Object.assign(new Relationship(), {
|
|
||||||
|
relationship2 = Object.assign(new Relationship(), {
|
||||||
self: url + '/3',
|
self: url + '/3',
|
||||||
id: '3',
|
id: '3',
|
||||||
uuid: '3',
|
uuid: '3',
|
||||||
leftId: 'author2',
|
leftId: 'author2',
|
||||||
rightId: 'publication',
|
rightId: 'publication',
|
||||||
|
leftItem: observableOf(new RemoteData(false, false, true, undefined, item)),
|
||||||
|
rightItem: observableOf(new RemoteData(false, false, true, undefined, author2)),
|
||||||
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
|
||||||
})
|
});
|
||||||
];
|
|
||||||
|
|
||||||
item = Object.assign(new Item(), {
|
item = Object.assign(new Item(), {
|
||||||
self: 'fake-item-url/publication',
|
self: 'fake-item-url/publication',
|
||||||
id: 'publication',
|
id: 'publication',
|
||||||
uuid: 'publication',
|
uuid: 'publication',
|
||||||
relationships: observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(new PageInfo(), relationships)))
|
relationships: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
undefined,
|
||||||
|
new PaginatedList(new PageInfo(), [relationship1, relationship2])
|
||||||
|
))
|
||||||
|
});
|
||||||
|
|
||||||
|
entityType = Object.assign(new ItemType(), {
|
||||||
|
id: 'entityType',
|
||||||
|
});
|
||||||
|
|
||||||
|
relatedEntityType = Object.assign(new ItemType(), {
|
||||||
|
id: 'relatedEntityType',
|
||||||
});
|
});
|
||||||
|
|
||||||
author1 = Object.assign(new Item(), {
|
author1 = Object.assign(new Item(), {
|
||||||
@@ -88,17 +110,29 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
|
|
||||||
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService',
|
||||||
{
|
{
|
||||||
getFieldUpdatesExclusive: observableOf({
|
getFieldUpdates: observableOf({
|
||||||
[author1.uuid]: fieldUpdate1,
|
[author1.uuid]: fieldUpdate1,
|
||||||
[author2.uuid]: fieldUpdate2
|
[author2.uuid]: fieldUpdate2
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
relationshipService = jasmine.createSpyObj('relationshipService',
|
entityTypeService = jasmine.createSpyObj('entityTypeService',
|
||||||
{
|
{
|
||||||
getRelatedItemsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [author1, author2]))),
|
getEntityTypeByLabel: observableOf(new RemoteData(
|
||||||
getItemRelationshipsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), relationships))),
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
entityType,
|
||||||
|
)),
|
||||||
|
getEntityTypeRelationships: observableOf(new RemoteData(
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
true,
|
||||||
|
null,
|
||||||
|
new PaginatedList(new PageInfo(), [relationshipType]),
|
||||||
|
)),
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -107,7 +141,6 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
declarations: [EditRelationshipListComponent],
|
declarations: [EditRelationshipListComponent],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
{ provide: ObjectUpdatesService, useValue: objectUpdatesService },
|
||||||
{ provide: RelationshipService, useValue: relationshipService }
|
|
||||||
], schemas: [
|
], schemas: [
|
||||||
NO_ERRORS_SCHEMA
|
NO_ERRORS_SCHEMA
|
||||||
]
|
]
|
||||||
@@ -120,7 +153,7 @@ describe('EditRelationshipListComponent', () => {
|
|||||||
de = fixture.debugElement;
|
de = fixture.debugElement;
|
||||||
comp.item = item;
|
comp.item = item;
|
||||||
comp.url = url;
|
comp.url = url;
|
||||||
comp.relationshipLabel = relationshipType.leftwardType;
|
comp.relationshipType = relationshipType;
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -1,12 +1,15 @@
|
|||||||
import { Component, Input, OnChanges, OnInit, SimpleChanges } from '@angular/core';
|
import {Component, Input, OnInit} from '@angular/core';
|
||||||
import {ObjectUpdatesService} from '../../../../core/data/object-updates/object-updates.service';
|
import {ObjectUpdatesService} from '../../../../core/data/object-updates/object-updates.service';
|
||||||
import {Observable} from 'rxjs/internal/Observable';
|
import {Observable} from 'rxjs/internal/Observable';
|
||||||
import {FieldUpdate, FieldUpdates} from '../../../../core/data/object-updates/object-updates.reducer';
|
import {FieldUpdate, FieldUpdates} from '../../../../core/data/object-updates/object-updates.reducer';
|
||||||
import { RelationshipService } from '../../../../core/data/relationship.service';
|
|
||||||
import {Item} from '../../../../core/shared/item.model';
|
import {Item} from '../../../../core/shared/item.model';
|
||||||
import {map, switchMap} from 'rxjs/operators';
|
import {map, switchMap} from 'rxjs/operators';
|
||||||
import {hasValue} from '../../../../shared/empty.util';
|
import {hasValue} from '../../../../shared/empty.util';
|
||||||
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 {getRemoteDataPayload, getSucceededRemoteData} from '../../../../core/shared/operators';
|
||||||
|
import {combineLatest as observableCombineLatest, combineLatest} from 'rxjs';
|
||||||
|
import {ItemType} from '../../../../core/shared/item-relationships/item-type.model';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-edit-relationship-list',
|
selector: 'ds-edit-relationship-list',
|
||||||
@@ -17,12 +20,15 @@ import {Relationship} from '../../../../core/shared/item-relationships/relations
|
|||||||
* A component creating a list of editable relationships of a certain type
|
* A component creating a list of editable relationships of a certain type
|
||||||
* The relationships are rendered as a list of related items
|
* The relationships are rendered as a list of related items
|
||||||
*/
|
*/
|
||||||
export class EditRelationshipListComponent implements OnInit, OnChanges {
|
export class EditRelationshipListComponent implements OnInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The item to display related items for
|
* The item to display related items for
|
||||||
*/
|
*/
|
||||||
@Input() item: Item;
|
@Input() item: Item;
|
||||||
|
|
||||||
|
@Input() itemType: ItemType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The URL to the current page
|
* The URL to the current page
|
||||||
* Used to fetch updates for the current item from the store
|
* Used to fetch updates for the current item from the store
|
||||||
@@ -32,7 +38,7 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
|
|||||||
/**
|
/**
|
||||||
* The label of the relationship-type we're rendering a list for
|
* The label of the relationship-type we're rendering a list for
|
||||||
*/
|
*/
|
||||||
@Input() relationshipLabel: string;
|
@Input() relationshipType: RelationshipType;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The FieldUpdates for the relationships in question
|
* The FieldUpdates for the relationships in question
|
||||||
@@ -41,48 +47,42 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
|
|||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected objectUpdatesService: ObjectUpdatesService,
|
protected objectUpdatesService: ObjectUpdatesService,
|
||||||
protected relationshipService: RelationshipService
|
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
|
||||||
this.initUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnChanges(changes: SimpleChanges): void {
|
|
||||||
this.initUpdates();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize the FieldUpdates using the related items
|
* Get the i18n message key for this relationship type
|
||||||
*/
|
*/
|
||||||
initUpdates() {
|
public getRelationshipMessageKey(): Observable<string> {
|
||||||
this.updates$ = this.getUpdatesByLabel(this.relationshipLabel);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
return this.getLabel().pipe(
|
||||||
* Get FieldUpdates for the relationships of a specific type
|
map((label) => {
|
||||||
* @param label The relationship type's label
|
|
||||||
*/
|
|
||||||
public getUpdatesByLabel(label: string): Observable<FieldUpdates> {
|
|
||||||
return this.relationshipService.getItemRelationshipsByLabel(this.item, label).pipe(
|
|
||||||
map((relationsRD) => relationsRD.payload.page.map((relationship) =>
|
|
||||||
Object.assign(new Relationship(), relationship, {uuid: relationship.id})
|
|
||||||
)),
|
|
||||||
switchMap((initialFields) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, initialFields)),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the i18n message key for a relationship
|
|
||||||
* @param label The relationship type's label
|
|
||||||
*/
|
|
||||||
public getRelationshipMessageKey(label: string): string {
|
|
||||||
if (hasValue(label) && label.indexOf('Of') > -1) {
|
if (hasValue(label) && label.indexOf('Of') > -1) {
|
||||||
return `relationships.${label.substring(0, label.indexOf('Of') + 2)}`
|
return `relationships.${label.substring(0, label.indexOf('Of') + 2)}`
|
||||||
} else {
|
} else {
|
||||||
return label;
|
return label;
|
||||||
}
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the relevant label for this relationship type
|
||||||
|
*/
|
||||||
|
private getLabel(): Observable<string> {
|
||||||
|
|
||||||
|
return combineLatest([
|
||||||
|
this.relationshipType.leftType,
|
||||||
|
this.relationshipType.rightType,
|
||||||
|
].map((itemTypeRD) => itemTypeRD.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
))).pipe(
|
||||||
|
map((itemTypes) => [
|
||||||
|
this.relationshipType.leftwardType,
|
||||||
|
this.relationshipType.rightwardType,
|
||||||
|
][itemTypes.findIndex((itemType) => itemType.id === this.itemType.id)]),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -91,4 +91,27 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
|
|||||||
trackUpdate(index, update: FieldUpdate) {
|
trackUpdate(index, update: FieldUpdate) {
|
||||||
return update && update.field ? update.field.uuid : undefined;
|
return update && update.field ? update.field.uuid : undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.updates$ = this.item.relationships.pipe(
|
||||||
|
map((relationships) => relationships.payload.page.filter((relationship) => relationship)),
|
||||||
|
switchMap((itemRelationships) =>
|
||||||
|
observableCombineLatest(
|
||||||
|
itemRelationships
|
||||||
|
.map((relationship) => relationship.relationshipType.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
))
|
||||||
|
).pipe(
|
||||||
|
map((relationshipTypes) => itemRelationships.filter(
|
||||||
|
(relationship, index) => relationshipTypes[index].id === this.relationshipType.id)
|
||||||
|
),
|
||||||
|
map((relationships) => relationships.map((relationship) =>
|
||||||
|
Object.assign(new Relationship(), relationship, {uuid: relationship.id})
|
||||||
|
)),
|
||||||
|
)
|
||||||
|
),
|
||||||
|
switchMap((initialFields) => this.objectUpdatesService.getFieldUpdates(this.url, initialFields)),
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -19,7 +19,9 @@
|
|||||||
</div>
|
</div>
|
||||||
<ng-template #virtualMetadataModal>
|
<ng-template #virtualMetadataModal>
|
||||||
<ds-virtual-metadata
|
<ds-virtual-metadata
|
||||||
[relationship]="relationship"
|
[relationshipId]="relationship.id"
|
||||||
|
[leftItem]="leftItem$ | async"
|
||||||
|
[rightItem]="rightItem$ | async"
|
||||||
[url]="url"
|
[url]="url"
|
||||||
(close)="closeVirtualMetadataModal()"
|
(close)="closeVirtualMetadataModal()"
|
||||||
(save)="remove()"
|
(save)="remove()"
|
||||||
|
@@ -13,7 +13,7 @@ import {NgbModal, NgbModalRef} from '@ng-bootstrap/ng-bootstrap';
|
|||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
// tslint:disable-next-line:component-selector
|
// tslint:disable-next-line:component-selector
|
||||||
selector: '[ds-edit-relationship]',
|
selector: 'ds-edit-relationship',
|
||||||
styleUrls: ['./edit-relationship.component.scss'],
|
styleUrls: ['./edit-relationship.component.scss'],
|
||||||
templateUrl: './edit-relationship.component.html',
|
templateUrl: './edit-relationship.component.html',
|
||||||
})
|
})
|
||||||
|
@@ -17,8 +17,13 @@
|
|||||||
<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>
|
||||||
</div>
|
</div>
|
||||||
<div *ngFor="let label of relationLabels$ | async" class="mb-4">
|
<div *ngFor="let relationshipType of relationshipTypes$ | async" class="mb-4">
|
||||||
<ds-edit-relationship-list [item]="item" [url]="url" [relationshipLabel]="label" ></ds-edit-relationship-list>
|
<ds-edit-relationship-list
|
||||||
|
[url]="url"
|
||||||
|
[item]="item"
|
||||||
|
[itemType]="entityType$ | async"
|
||||||
|
[relationshipType]="relationshipType"
|
||||||
|
></ds-edit-relationship-list>
|
||||||
</div>
|
</div>
|
||||||
<div class="button-row bottom">
|
<div class="button-row bottom">
|
||||||
<div class="float-right">
|
<div class="float-right">
|
||||||
|
@@ -22,10 +22,12 @@ import { ErrorResponse, RestResponse } from '../../../core/cache/response.models
|
|||||||
import { isNotEmptyOperator } from '../../../shared/empty.util';
|
import { isNotEmptyOperator } from '../../../shared/empty.util';
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
|
||||||
import { getSucceededRemoteData} from '../../../core/shared/operators';
|
import {getRemoteDataPayload, getSucceededRemoteData} from '../../../core/shared/operators';
|
||||||
import { RequestService } from '../../../core/data/request.service';
|
import { RequestService } from '../../../core/data/request.service';
|
||||||
import { Subscription } from 'rxjs/internal/Subscription';
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
import { getRelationsByRelatedItemIds } from '../../simple/item-types/shared/item-relationships-utils';
|
import {RelationshipType} from '../../../core/shared/item-relationships/relationship-type.model';
|
||||||
|
import {ItemType} from '../../../core/shared/item-relationships/item-type.model';
|
||||||
|
import {EntityTypeService} from '../../../core/data/entity-type.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-relationships',
|
selector: 'ds-item-relationships',
|
||||||
@@ -40,13 +42,14 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
/**
|
/**
|
||||||
* The labels of all different relations within this item
|
* The labels of all different relations within this item
|
||||||
*/
|
*/
|
||||||
relationLabels$: Observable<string[]>;
|
relationshipTypes$: Observable<RelationshipType[]>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A subscription that checks when the item is deleted in cache and reloads the item by sending a new request
|
* A subscription that checks when the item is deleted in cache and reloads the item by sending a new request
|
||||||
* This is used to update the item in cache after relationships are deleted
|
* This is used to update the item in cache after relationships are deleted
|
||||||
*/
|
*/
|
||||||
itemUpdateSubscription: Subscription;
|
itemUpdateSubscription: Subscription;
|
||||||
|
entityType$: Observable<ItemType>;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected itemService: ItemDataService,
|
protected itemService: ItemDataService,
|
||||||
@@ -59,7 +62,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
protected relationshipService: RelationshipService,
|
protected relationshipService: RelationshipService,
|
||||||
protected objectCache: ObjectCacheService,
|
protected objectCache: ObjectCacheService,
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected cdRef: ChangeDetectorRef
|
protected entityTypeService: EntityTypeService,
|
||||||
) {
|
) {
|
||||||
super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route);
|
super(itemService, objectUpdatesService, router, notificationsService, translateService, EnvConfig, route);
|
||||||
}
|
}
|
||||||
@@ -69,21 +72,13 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
*/
|
*/
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
this.relationLabels$ = this.relationshipService.getItemRelationshipLabels(this.item);
|
|
||||||
this.initializeItemUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Update the item (and view) when it's removed in the request cache
|
|
||||||
*/
|
|
||||||
public initializeItemUpdate(): void {
|
|
||||||
this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe(
|
this.itemUpdateSubscription = this.requestService.hasByHrefObservable(this.item.self).pipe(
|
||||||
filter((exists: boolean) => !exists),
|
filter((exists: boolean) => !exists),
|
||||||
switchMap(() => this.itemService.findById(this.item.uuid)),
|
switchMap(() => this.itemService.findById(this.item.uuid)),
|
||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
).subscribe((itemRD: RemoteData<Item>) => {
|
).subscribe((itemRD: RemoteData<Item>) => {
|
||||||
this.item = itemRD.payload;
|
this.item = itemRD.payload;
|
||||||
this.cdRef.detectChanges();
|
this.initializeUpdates();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,8 +86,22 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
* Initialize the values and updates of the current item's relationship fields
|
* Initialize the values and updates of the current item's relationship fields
|
||||||
*/
|
*/
|
||||||
public initializeUpdates(): void {
|
public initializeUpdates(): void {
|
||||||
this.updates$ = this.relationshipService.getRelatedItems(this.item).pipe(
|
|
||||||
switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdates(this.url, items))
|
this.entityType$ = this.entityTypeService.getEntityTypeByLabel(
|
||||||
|
this.item.firstMetadataValue('relationship.type')
|
||||||
|
).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
);
|
||||||
|
|
||||||
|
this.relationshipTypes$ = this.entityType$.pipe(
|
||||||
|
switchMap((entityType) =>
|
||||||
|
this.entityTypeService.getEntityTypeRelationships(entityType.id).pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((relationshipTypes) => relationshipTypes.page),
|
||||||
|
)
|
||||||
|
),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -142,8 +151,9 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
))
|
))
|
||||||
),
|
),
|
||||||
).subscribe((responses: RestResponse[]) => {
|
).subscribe((responses: RestResponse[]) => {
|
||||||
|
this.itemUpdateSubscription.add(() => {
|
||||||
this.displayNotifications(responses);
|
this.displayNotifications(responses);
|
||||||
this.reset();
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -165,22 +175,12 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Re-initialize fields and subscriptions
|
|
||||||
*/
|
|
||||||
reset() {
|
|
||||||
this.initializeOriginalFields();
|
|
||||||
this.initializeUpdates();
|
|
||||||
this.initializeItemUpdate();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Sends all initial values of this item to the object updates service
|
* Sends all initial values of this item to the object updates service
|
||||||
*/
|
*/
|
||||||
public initializeOriginalFields() {
|
public initializeOriginalFields() {
|
||||||
this.relationshipService.getRelatedItems(this.item).pipe(take(1)).subscribe((items: Item[]) => {
|
const initialFields = [];
|
||||||
this.objectUpdatesService.initialize(this.url, items, this.item.lastModified);
|
this.objectUpdatesService.initialize(this.url, initialFields, this.item.lastModified);
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -189,5 +189,4 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
|
|||||||
ngOnDestroy(): void {
|
ngOnDestroy(): void {
|
||||||
this.itemUpdateSubscription.unsubscribe();
|
this.itemUpdateSubscription.unsubscribe();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -5,21 +5,19 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="modal-body">
|
<div class="modal-body">
|
||||||
<div *ngFor="let item$ of [leftItem$, rightItem$]">
|
<ng-container *ngFor="let item of items; trackBy: trackItem">
|
||||||
<div *ngVar="item$ | async as item">
|
|
||||||
<div *ngVar="(isSelectedVirtualMetadataItem(item) | async) as selected"
|
<div *ngVar="(isSelectedVirtualMetadataItem(item) | async) as selected"
|
||||||
(click)="setSelectedVirtualMetadataItem(item, !selected)"
|
(click)="setSelectedVirtualMetadataItem(item, !selected)"
|
||||||
class="d-flex flex-row">
|
class="d-flex flex-row">
|
||||||
<div class="m-2">
|
<div class="m-2">
|
||||||
<label>
|
<label>
|
||||||
<input type="checkbox" [checked]="selected">
|
<input class="select" type="checkbox">
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
<div class="flex-column">
|
<div class="flex-column">
|
||||||
<ds-listable-object-component-loader
|
<ds-listable-object-component-loader [object]="item">
|
||||||
[object]="item$ | async"></ds-listable-object-component-loader>
|
</ds-listable-object-component-loader>
|
||||||
<div *ngFor="let metadata of getVirtualMetadata(relationship, item$ | async)">
|
<div *ngFor="let metadata of virtualMetadata.get(item.uuid)">
|
||||||
<div>
|
|
||||||
<div class="font-weight-bold">
|
<div class="font-weight-bold">
|
||||||
{{metadata.metadataField}}
|
{{metadata.metadataField}}
|
||||||
</div>
|
</div>
|
||||||
@@ -29,13 +27,11 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</ng-container>
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<div class="d-flex flex-row-reverse m-2">
|
<div class="d-flex flex-row-reverse m-2">
|
||||||
<button class="btn btn-primary"
|
<button class="btn btn-primary save"
|
||||||
(click)="save.emit()"><i
|
(click)="save.emit()">
|
||||||
class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
<i class="fas fa-save"></i> {{"item.edit.metadata.save-button" | translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,172 +1,102 @@
|
|||||||
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
import {async, ComponentFixture, TestBed} from '@angular/core/testing';
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import {of as observableOf} from 'rxjs/internal/observable/of';
|
||||||
import { RouterStub } from '../../../shared/testing/router-stub';
|
|
||||||
import { CommonModule } from '@angular/common';
|
|
||||||
import { RouterTestingModule } from '@angular/router/testing';
|
|
||||||
import {TranslateModule} from '@ngx-translate/core';
|
import {TranslateModule} from '@ngx-translate/core';
|
||||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
import {DebugElement, NO_ERRORS_SCHEMA} from '@angular/core';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import {By} from '@angular/platform-browser';
|
||||||
import {VirtualMetadataComponent} from './virtual-metadata.component';
|
import {VirtualMetadataComponent} from './virtual-metadata.component';
|
||||||
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
import {Item} from '../../../core/shared/item.model';
|
||||||
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
||||||
import { SearchService } from '../../../+search-page/search-service/search.service';
|
import {VarDirective} from '../../../shared/utils/var.directive';
|
||||||
import { of as observableOf } from 'rxjs';
|
|
||||||
import { FormsModule } from '@angular/forms';
|
|
||||||
import { ItemDataService } from '../../../core/data/item-data.service';
|
|
||||||
import { RemoteData } from '../../../core/data/remote-data';
|
|
||||||
import { PaginatedList } from '../../../core/data/paginated-list';
|
|
||||||
import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
|
||||||
import { RestResponse } from '../../../core/cache/response.models';
|
|
||||||
import { Collection } from '../../../core/shared/collection.model';
|
|
||||||
|
|
||||||
// describe('ItemMoveComponent', () => {
|
fdescribe('VirtualMetadataComponent', () => {
|
||||||
// let comp: VirtualMetadataComponent;
|
|
||||||
// let fixture: ComponentFixture<VirtualMetadataComponent>;
|
let comp: VirtualMetadataComponent;
|
||||||
//
|
let fixture: ComponentFixture<VirtualMetadataComponent>;
|
||||||
// const mockItem = Object.assign(new Item(), {
|
let de: DebugElement;
|
||||||
// id: 'fake-id',
|
|
||||||
// handle: 'fake/handle',
|
let objectUpdatesService;
|
||||||
// lastModified: '2018'
|
|
||||||
// });
|
const url = 'http://test-url.com/test-url';
|
||||||
//
|
|
||||||
// const itemPageUrl = `fake-url/${mockItem.id}`;
|
let item;
|
||||||
// const routerStub = Object.assign(new RouterStub(), {
|
let relatedItem;
|
||||||
// url: `${itemPageUrl}/edit`
|
let relationshipId;
|
||||||
// });
|
|
||||||
//
|
beforeEach(() => {
|
||||||
// const mockItemDataService = jasmine.createSpyObj({
|
|
||||||
// moveToCollection: observableOf(new RestResponse(true, 200, 'Success'))
|
relationshipId = 'relationship id';
|
||||||
// });
|
|
||||||
//
|
item = Object.assign(new Item(), {
|
||||||
// const mockItemDataServiceFail = jasmine.createSpyObj({
|
uuid: 'publication',
|
||||||
// moveToCollection: observableOf(new RestResponse(false, 500, 'Internal server error'))
|
metadata: [],
|
||||||
// });
|
});
|
||||||
//
|
|
||||||
// const routeStub = {
|
relatedItem = Object.assign(new Item(), {
|
||||||
// data: observableOf({
|
uuid: 'relatedItem',
|
||||||
// item: new RemoteData(false, false, true, null, {
|
metadata: [],
|
||||||
// id: 'item1'
|
});
|
||||||
// })
|
|
||||||
// })
|
objectUpdatesService = jasmine.createSpyObj('objectUpdatesService', {
|
||||||
// };
|
isSelectedVirtualMetadata: observableOf(false),
|
||||||
//
|
setSelectedVirtualMetadata: null,
|
||||||
// const collection1 = Object.assign(new Collection(),{
|
});
|
||||||
// uuid: 'collection-uuid-1',
|
|
||||||
// name: 'Test collection 1',
|
TestBed.configureTestingModule({
|
||||||
// self: 'self-link-1',
|
imports: [TranslateModule.forRoot()],
|
||||||
// });
|
declarations: [VirtualMetadataComponent, VarDirective],
|
||||||
//
|
providers: [
|
||||||
// const collection2 = Object.assign(new Collection(),{
|
{provide: ObjectUpdatesService, useValue: objectUpdatesService},
|
||||||
// uuid: 'collection-uuid-2',
|
], schemas: [
|
||||||
// name: 'Test collection 2',
|
NO_ERRORS_SCHEMA
|
||||||
// self: 'self-link-2',
|
]
|
||||||
// });
|
}).compileComponents();
|
||||||
//
|
fixture = TestBed.createComponent(VirtualMetadataComponent);
|
||||||
// const mockSearchService = {
|
comp = fixture.componentInstance;
|
||||||
// search: () => {
|
de = fixture.debugElement;
|
||||||
// return observableOf(new RemoteData(false, false, true, null,
|
|
||||||
// new PaginatedList(null, [
|
comp.url = url;
|
||||||
// {
|
comp.leftItem = item;
|
||||||
// indexableObject: collection1,
|
comp.rightItem = relatedItem;
|
||||||
// hitHighlights: {}
|
comp.relationshipId = relationshipId;
|
||||||
// }, {
|
|
||||||
// indexableObject: collection2,
|
fixture.detectChanges();
|
||||||
// hitHighlights: {}
|
});
|
||||||
// }
|
|
||||||
// ])));
|
describe('when clicking the save button', () => {
|
||||||
// }
|
it('should emit a save event', () => {
|
||||||
// };
|
|
||||||
//
|
spyOn(comp.save, 'emit');
|
||||||
// const notificationsServiceStub = new NotificationsServiceStub();
|
fixture.debugElement
|
||||||
//
|
.query(By.css('button.save'))
|
||||||
// describe('ItemMoveComponent success', () => {
|
.triggerEventHandler('click', null);
|
||||||
// beforeEach(async(() => {
|
expect(comp.save.emit).toHaveBeenCalled();
|
||||||
// TestBed.configureTestingModule({
|
});
|
||||||
// imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
});
|
||||||
// declarations: [VirtualMetadataComponent],
|
|
||||||
// providers: [
|
describe('when clicking the close button', () => {
|
||||||
// {provide: ActivatedRoute, useValue: routeStub},
|
it('should emit a close event', () => {
|
||||||
// {provide: Router, useValue: routerStub},
|
|
||||||
// {provide: ItemDataService, useValue: mockItemDataService},
|
spyOn(comp.close, 'emit');
|
||||||
// {provide: NotificationsService, useValue: notificationsServiceStub},
|
fixture.debugElement
|
||||||
// {provide: SearchService, useValue: mockSearchService},
|
.query(By.css('button.close'))
|
||||||
// ], schemas: [
|
.triggerEventHandler('click', null);
|
||||||
// CUSTOM_ELEMENTS_SCHEMA
|
expect(comp.close.emit).toHaveBeenCalled();
|
||||||
// ]
|
});
|
||||||
// }).compileComponents();
|
});
|
||||||
// }));
|
|
||||||
//
|
describe('when selecting an item', () => {
|
||||||
// beforeEach(() => {
|
it('should call the updates service setSelectedVirtualMetadata method', () => {
|
||||||
// fixture = TestBed.createComponent(VirtualMetadataComponent);
|
fixture.debugElement
|
||||||
// comp = fixture.componentInstance;
|
.query(By.css('input.select'))
|
||||||
// fixture.detectChanges();
|
.triggerEventHandler('click', null);
|
||||||
// });
|
fixture.whenStable().then(() =>
|
||||||
// it('should load suggestions', () => {
|
expect(objectUpdatesService.setSelectedVirtualMetadata).toHaveBeenCalledWith(
|
||||||
// const expected = [
|
url,
|
||||||
// collection1,
|
relationshipId,
|
||||||
// collection2
|
item.uuid,
|
||||||
// ];
|
true
|
||||||
//
|
)
|
||||||
// comp.collectionSearchResults.subscribe((value) => {
|
);
|
||||||
// expect(value).toEqual(expected);
|
});
|
||||||
// }
|
})
|
||||||
// );
|
});
|
||||||
// });
|
|
||||||
// it('should get current url ', () => {
|
|
||||||
// expect(comp.getCurrentUrl()).toEqual('fake-url/fake-id/edit');
|
|
||||||
// });
|
|
||||||
// it('should on click select the correct collection name and id', () => {
|
|
||||||
// const data = collection1;
|
|
||||||
//
|
|
||||||
// comp.onClick(data);
|
|
||||||
//
|
|
||||||
// expect(comp.selectedCollectionName).toEqual('Test collection 1');
|
|
||||||
// expect(comp.selectedCollection).toEqual(collection1);
|
|
||||||
// });
|
|
||||||
// describe('moveCollection', () => {
|
|
||||||
// it('should call itemDataService.moveToCollection', () => {
|
|
||||||
// comp.itemId = 'item-id';
|
|
||||||
// comp.selectedCollectionName = 'selected-collection-id';
|
|
||||||
// comp.selectedCollection = collection1;
|
|
||||||
// comp.moveCollection();
|
|
||||||
//
|
|
||||||
// expect(mockItemDataService.moveToCollection).toHaveBeenCalledWith('item-id', collection1);
|
|
||||||
// });
|
|
||||||
// it('should call notificationsService success message on success', () => {
|
|
||||||
// comp.moveCollection();
|
|
||||||
//
|
|
||||||
// expect(notificationsServiceStub.success).toHaveBeenCalled();
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// describe('ItemMoveComponent fail', () => {
|
|
||||||
// beforeEach(async(() => {
|
|
||||||
// TestBed.configureTestingModule({
|
|
||||||
// imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
|
||||||
// declarations: [VirtualMetadataComponent],
|
|
||||||
// providers: [
|
|
||||||
// {provide: ActivatedRoute, useValue: routeStub},
|
|
||||||
// {provide: Router, useValue: routerStub},
|
|
||||||
// {provide: ItemDataService, useValue: mockItemDataServiceFail},
|
|
||||||
// {provide: NotificationsService, useValue: notificationsServiceStub},
|
|
||||||
// {provide: SearchService, useValue: mockSearchService},
|
|
||||||
// ], schemas: [
|
|
||||||
// CUSTOM_ELEMENTS_SCHEMA
|
|
||||||
// ]
|
|
||||||
// }).compileComponents();
|
|
||||||
// }));
|
|
||||||
//
|
|
||||||
// beforeEach(() => {
|
|
||||||
// fixture = TestBed.createComponent(VirtualMetadataComponent);
|
|
||||||
// comp = fixture.componentInstance;
|
|
||||||
// fixture.detectChanges();
|
|
||||||
// });
|
|
||||||
//
|
|
||||||
// it('should call notificationsService error message on fail', () => {
|
|
||||||
// comp.moveCollection();
|
|
||||||
//
|
|
||||||
// expect(notificationsServiceStub.error).toHaveBeenCalled();
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
// });
|
|
||||||
|
@@ -1,10 +1,7 @@
|
|||||||
import {Component, EventEmitter, Input, OnChanges, Output} from '@angular/core';
|
import {Component, EventEmitter, Input, OnInit, Output} from '@angular/core';
|
||||||
import {ActivatedRoute} from '@angular/router';
|
|
||||||
import {Observable} from 'rxjs';
|
import {Observable} from 'rxjs';
|
||||||
import {Item} from '../../../core/shared/item.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 {MetadataValue} from '../../../core/shared/metadata.models';
|
||||||
import {getRemoteDataPayload, getSucceededRemoteData} from '../../../core/shared/operators';
|
|
||||||
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
import {ObjectUpdatesService} from '../../../core/data/object-updates/object-updates.service';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -12,47 +9,67 @@ import {ObjectUpdatesService} from '../../../core/data/object-updates/object-upd
|
|||||||
templateUrl: './virtual-metadata.component.html'
|
templateUrl: './virtual-metadata.component.html'
|
||||||
})
|
})
|
||||||
/**
|
/**
|
||||||
* Component that handles the moving of an item to a different collection
|
* Component that lists both items of a relationship, along with their virtual metadata of the relationship.
|
||||||
|
* The component is shown when a relationship is marked to be deleted.
|
||||||
|
* Each item has a checkbox to indicate whether its virtual metadata should be saved as real metadata.
|
||||||
*/
|
*/
|
||||||
export class VirtualMetadataComponent implements OnChanges {
|
export class VirtualMetadataComponent implements OnInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The current url of this page
|
* The current url of this page
|
||||||
*/
|
*/
|
||||||
@Input() url: string;
|
@Input() url: string;
|
||||||
|
|
||||||
@Input() relationship: Relationship;
|
/**
|
||||||
|
* The id of the relationship to be deleted.
|
||||||
|
*/
|
||||||
|
@Input() relationshipId: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The left item of the relationship to be deleted.
|
||||||
|
*/
|
||||||
|
@Input() leftItem: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The right item of the relationship to be deleted.
|
||||||
|
*/
|
||||||
|
@Input() rightItem: Item;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when the close button is pressed.
|
||||||
|
*/
|
||||||
@Output() close = new EventEmitter();
|
@Output() close = new EventEmitter();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Emits when the save button is pressed.
|
||||||
|
*/
|
||||||
@Output() save = new EventEmitter();
|
@Output() save = new EventEmitter();
|
||||||
|
|
||||||
leftItem$: Observable<Item>;
|
/**
|
||||||
rightItem$: Observable<Item>;
|
* Get an array of the left and the right item of the relationship to be deleted.
|
||||||
|
*/
|
||||||
|
get items() {
|
||||||
|
return [this.leftItem, this.rightItem];
|
||||||
|
}
|
||||||
|
|
||||||
|
private virtualMetadata: Map<string, VirtualMetadata[]> = new Map<string, VirtualMetadata[]>();
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected route: ActivatedRoute,
|
|
||||||
protected objectUpdatesService: ObjectUpdatesService,
|
protected objectUpdatesService: ObjectUpdatesService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnChanges(): void {
|
/**
|
||||||
this.leftItem$ = this.relationship.leftItem.pipe(
|
* Get the virtual metadata of a given item corresponding to this relationship.
|
||||||
getSucceededRemoteData(),
|
* @param item the item to get the virtual metadata for
|
||||||
getRemoteDataPayload(),
|
*/
|
||||||
);
|
getVirtualMetadata(item: Item): VirtualMetadata[] {
|
||||||
this.rightItem$ = this.relationship.rightItem.pipe(
|
|
||||||
getSucceededRemoteData(),
|
|
||||||
getRemoteDataPayload(),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
getVirtualMetadata(relationship: Relationship, relatedItem: Item): VirtualMetadata[] {
|
return Object.entries(item.metadata)
|
||||||
|
|
||||||
return Object.entries(relatedItem.metadata)
|
|
||||||
.map(([key, value]) =>
|
.map(([key, value]) =>
|
||||||
value
|
value
|
||||||
.filter((metadata: MetadataValue) =>
|
.filter((metadata: MetadataValue) =>
|
||||||
metadata.authority && metadata.authority.endsWith(relationship.id))
|
!key.startsWith('relation') && metadata.authority && metadata.authority.endsWith(this.relationshipId))
|
||||||
.map((metadata: MetadataValue) => {
|
.map((metadata: MetadataValue) => {
|
||||||
return {
|
return {
|
||||||
metadataField: key,
|
metadataField: key,
|
||||||
@@ -60,18 +77,43 @@ export class VirtualMetadataComponent implements OnChanges {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
)
|
)
|
||||||
.reduce((previous, current) => previous.concat(current));
|
.reduce((previous, current) => previous.concat(current), []);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Select/deselect the virtual metadata of an item to be saved as real metadata.
|
||||||
|
* @param item the item for which (not) to save the virtual metadata as real metadata
|
||||||
|
* @param selected whether or not to save the virtual metadata as real metadata
|
||||||
|
*/
|
||||||
setSelectedVirtualMetadataItem(item: Item, selected: boolean) {
|
setSelectedVirtualMetadataItem(item: Item, selected: boolean) {
|
||||||
this.objectUpdatesService.setSelectedVirtualMetadata(this.url, this.relationship.id, item.uuid, selected);
|
this.objectUpdatesService.setSelectedVirtualMetadata(this.url, this.relationshipId, item.uuid, selected);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the virtual metadata of a given item is selected to be saved as real metadata
|
||||||
|
* @param item the item for which to check whether the virtual metadata is selected to be saved as real metadata
|
||||||
|
*/
|
||||||
isSelectedVirtualMetadataItem(item: Item): Observable<boolean> {
|
isSelectedVirtualMetadataItem(item: Item): Observable<boolean> {
|
||||||
return this.objectUpdatesService.isSelectedVirtualMetadata(this.url, this.relationship.id, item.uuid);
|
return this.objectUpdatesService.isSelectedVirtualMetadata(this.url, this.relationshipId, item.uuid);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Prevent unnecessary rerendering so fields don't lose focus
|
||||||
|
*/
|
||||||
|
trackItem(index, item: Item) {
|
||||||
|
return item && item.uuid;
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.items.forEach((item) => {
|
||||||
|
this.virtualMetadata.set(item.uuid, this.getVirtualMetadata(item));
|
||||||
|
});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Represents a virtual metadata entry.
|
||||||
|
*/
|
||||||
export interface VirtualMetadata {
|
export interface VirtualMetadata {
|
||||||
metadataField: string,
|
metadataField: string,
|
||||||
metadataValue: MetadataValue,
|
metadataValue: MetadataValue,
|
||||||
|
@@ -121,6 +121,7 @@ import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model';
|
|||||||
import { BrowseDefinition } from './shared/browse-definition.model';
|
import { BrowseDefinition } from './shared/browse-definition.model';
|
||||||
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
||||||
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
||||||
|
import {EntityTypeService} from './data/entity-type.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -211,6 +212,7 @@ const PROVIDERS = [
|
|||||||
TaskResponseParsingService,
|
TaskResponseParsingService,
|
||||||
ClaimedTaskDataService,
|
ClaimedTaskDataService,
|
||||||
PoolTaskDataService,
|
PoolTaskDataService,
|
||||||
|
EntityTypeService,
|
||||||
// register AuthInterceptor as HttpInterceptor
|
// register AuthInterceptor as HttpInterceptor
|
||||||
{
|
{
|
||||||
provide: HTTP_INTERCEPTORS,
|
provide: HTTP_INTERCEPTORS,
|
||||||
|
103
src/app/core/data/entity-type.service.ts
Normal file
103
src/app/core/data/entity-type.service.ts
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
import { DataService } from './data.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { CoreState } from '../core.reducers';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { HttpClient } from '@angular/common/http';
|
||||||
|
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
|
||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { GetRequest } from './request.models';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import {switchMap, take, tap} from 'rxjs/operators';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import {RelationshipType} from '../shared/item-relationships/relationship-type.model';
|
||||||
|
import {PaginatedList} from './paginated-list';
|
||||||
|
import {ItemType} from '../shared/item-relationships/item-type.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service handling all ItemType requests
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class EntityTypeService extends DataService<ItemType> {
|
||||||
|
|
||||||
|
protected linkPath = 'entitytypes';
|
||||||
|
protected forceBypassCache = false;
|
||||||
|
|
||||||
|
constructor(protected requestService: RequestService,
|
||||||
|
protected rdbService: RemoteDataBuildService,
|
||||||
|
protected dataBuildService: NormalizedObjectBuildService,
|
||||||
|
protected store: Store<CoreState>,
|
||||||
|
protected halService: HALEndpointService,
|
||||||
|
protected objectCache: ObjectCacheService,
|
||||||
|
protected notificationsService: NotificationsService,
|
||||||
|
protected http: HttpClient,
|
||||||
|
protected comparator: DefaultChangeAnalyzer<ItemType>) {
|
||||||
|
super();
|
||||||
|
}
|
||||||
|
|
||||||
|
getBrowseEndpoint(options, linkPath?: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the endpoint for the item type's allowed relationship types
|
||||||
|
* @param entityTypeId
|
||||||
|
*/
|
||||||
|
getRelationshipTypesEndpoint(entityTypeId: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
switchMap((href) => this.halService.getEndpoint('relationshiptypes', `${href}/${entityTypeId}`))
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the allowed relationship types for an entity type
|
||||||
|
* @param entityTypeId
|
||||||
|
*/
|
||||||
|
getEntityTypeRelationships(entityTypeId: string): Observable<RemoteData<PaginatedList<RelationshipType>>> {
|
||||||
|
|
||||||
|
const href$ = this.getRelationshipTypesEndpoint(entityTypeId);
|
||||||
|
|
||||||
|
href$.pipe(take(1)).subscribe((href) => {
|
||||||
|
const request = new GetRequest(this.requestService.generateRequestId(), href);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.rdbService.buildList(href$);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get an entity type by their label
|
||||||
|
* @param label
|
||||||
|
*/
|
||||||
|
getEntityTypeByLabel(label: string): Observable<RemoteData<ItemType>> {
|
||||||
|
|
||||||
|
// TODO: Remove mock data once REST API supports this
|
||||||
|
/*
|
||||||
|
href$.pipe(take(1)).subscribe((href) => {
|
||||||
|
const request = new GetRequest(this.requestService.generateRequestId(), href);
|
||||||
|
this.requestService.configure(request);
|
||||||
|
});
|
||||||
|
|
||||||
|
return this.rdbService.buildSingle<EntityType>(href$);
|
||||||
|
*/
|
||||||
|
|
||||||
|
// Mock:
|
||||||
|
const index = [
|
||||||
|
'Publication',
|
||||||
|
'Person',
|
||||||
|
'Project',
|
||||||
|
'OrgUnit',
|
||||||
|
'Journal',
|
||||||
|
'JournalVolume',
|
||||||
|
'JournalIssue',
|
||||||
|
'DataPackage',
|
||||||
|
'DataFile',
|
||||||
|
].indexOf(label);
|
||||||
|
|
||||||
|
return this.findById((index + 1) + '');
|
||||||
|
}
|
||||||
|
}
|
@@ -84,6 +84,9 @@ export class AddFieldUpdateAction implements Action {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to select/deselect virtual metadata in the ObjectUpdates state for a certain page url
|
||||||
|
*/
|
||||||
export class SelectVirtualMetadataAction implements Action {
|
export class SelectVirtualMetadataAction implements Action {
|
||||||
|
|
||||||
type = ObjectUpdatesActionTypes.SELECT_VIRTUAL_METADATA;
|
type = ObjectUpdatesActionTypes.SELECT_VIRTUAL_METADATA;
|
||||||
@@ -95,12 +98,16 @@ export class SelectVirtualMetadataAction implements Action {
|
|||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create a new AddFieldUpdateAction
|
* Create a new SelectVirtualMetadataAction
|
||||||
*
|
*
|
||||||
* @param url
|
* @param url
|
||||||
* the unique url of the page for which a field update is added
|
* the unique url of the page for which a field update is added
|
||||||
* @param field The identifiable field of which a new update is added
|
* @param source
|
||||||
* @param changeType The update's change type
|
* the id of the relationship which adds the virtual metadata
|
||||||
|
* @param uuid
|
||||||
|
* the id of the item which has the virtual metadata
|
||||||
|
* @param select
|
||||||
|
* whether to select or deselect the virtual metadata to be saved as real metadata
|
||||||
*/
|
*/
|
||||||
constructor(
|
constructor(
|
||||||
url: string,
|
url: string,
|
||||||
|
@@ -23,13 +23,9 @@ import {
|
|||||||
SetEditableFieldUpdateAction,
|
SetEditableFieldUpdateAction,
|
||||||
SetValidFieldUpdateAction
|
SetValidFieldUpdateAction
|
||||||
} from './object-updates.actions';
|
} from './object-updates.actions';
|
||||||
import {distinctUntilChanged, filter, map} from 'rxjs/operators';
|
import {distinctUntilChanged, filter, map, switchMap} from 'rxjs/operators';
|
||||||
import {hasNoValue, hasValue, isEmpty, isNotEmpty} from '../../../shared/empty.util';
|
import {hasNoValue, hasValue, isEmpty, isNotEmpty} from '../../../shared/empty.util';
|
||||||
import {INotification} from '../../../shared/notifications/models/notification.model';
|
import {INotification} from '../../../shared/notifications/models/notification.model';
|
||||||
import {Item} from '../../shared/item.model';
|
|
||||||
import {Relationship} from '../../shared/item-relationships/relationship.model';
|
|
||||||
import {MetadataValue} from '../../shared/metadata.models';
|
|
||||||
import {VirtualMetadata} from '../../../+item-page/edit-item-page/virtual-metadata/virtual-metadata.component';
|
|
||||||
|
|
||||||
function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> {
|
function objectUpdatesStateSelector(): MemoizedSelector<CoreState, ObjectUpdatesState> {
|
||||||
return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']);
|
return createSelector(coreSelector, (state: CoreState) => state['cache/object-updates']);
|
||||||
@@ -101,18 +97,22 @@ export class ObjectUpdatesService {
|
|||||||
*/
|
*/
|
||||||
getFieldUpdates(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
|
getFieldUpdates(url: string, initialFields: Identifiable[]): Observable<FieldUpdates> {
|
||||||
const objectUpdates = this.getObjectEntry(url);
|
const objectUpdates = this.getObjectEntry(url);
|
||||||
return objectUpdates.pipe(map((objectEntry) => {
|
return objectUpdates.pipe(
|
||||||
|
switchMap((objectEntry) => {
|
||||||
const fieldUpdates: FieldUpdates = {};
|
const fieldUpdates: FieldUpdates = {};
|
||||||
Object.keys(objectEntry.fieldStates).forEach((uuid) => {
|
Object.keys(objectEntry.fieldStates).forEach((uuid) => {
|
||||||
let fieldUpdate = objectEntry.fieldUpdates[uuid];
|
fieldUpdates[uuid] = objectEntry.fieldUpdates[uuid];
|
||||||
if (isEmpty(fieldUpdate)) {
|
});
|
||||||
const identifiable = initialFields.find((object: Identifiable) => object.uuid === uuid);
|
return this.getFieldUpdatesExclusive(url, initialFields).pipe(
|
||||||
fieldUpdate = { field: identifiable, changeType: undefined };
|
map((fieldUpdatesExclusive) => {
|
||||||
}
|
Object.keys(fieldUpdatesExclusive).forEach((uuid) => {
|
||||||
fieldUpdates[uuid] = fieldUpdate;
|
fieldUpdates[uuid] = fieldUpdatesExclusive[uuid];
|
||||||
});
|
});
|
||||||
return fieldUpdates;
|
return fieldUpdates;
|
||||||
}))
|
})
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -204,6 +204,15 @@ export class ObjectUpdatesService {
|
|||||||
saveChangeFieldUpdate(url: string, field: Identifiable) {
|
saveChangeFieldUpdate(url: string, field: Identifiable) {
|
||||||
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
|
this.saveFieldUpdate(url, field, FieldChangeType.UPDATE);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether the virtual metadata of a given item is selected to be saved as real metadata
|
||||||
|
* @param url The URL of the page on which the field resides
|
||||||
|
* @param relationship The id of the relationship for which to check whether the virtual metadata is selected to be
|
||||||
|
* saved as real metadata
|
||||||
|
* @param item The id of the item for which to check whether the virtual metadata is selected to be
|
||||||
|
* saved as real metadata
|
||||||
|
*/
|
||||||
isSelectedVirtualMetadata(url: string, relationship: string, item: string): Observable<boolean> {
|
isSelectedVirtualMetadata(url: string, relationship: string, item: string): Observable<boolean> {
|
||||||
|
|
||||||
return this.store
|
return this.store
|
||||||
|
@@ -6,8 +6,8 @@ import { hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/emp
|
|||||||
import {distinctUntilChanged, filter, flatMap, map, switchMap, take, tap} from 'rxjs/operators';
|
import {distinctUntilChanged, filter, flatMap, map, switchMap, take, tap} from 'rxjs/operators';
|
||||||
import {
|
import {
|
||||||
configureRequest,
|
configureRequest,
|
||||||
filterSuccessfulResponses,
|
getRemoteDataPayload,
|
||||||
getRemoteDataPayload, getResponseFromEntry,
|
getResponseFromEntry,
|
||||||
getSucceededRemoteData
|
getSucceededRemoteData
|
||||||
} from '../shared/operators';
|
} from '../shared/operators';
|
||||||
import {DeleteRequest, FindAllOptions, RestRequest} from './request.models';
|
import {DeleteRequest, FindAllOptions, RestRequest} from './request.models';
|
||||||
@@ -22,7 +22,8 @@ import { zip as observableZip } from 'rxjs';
|
|||||||
import {PaginatedList} from './paginated-list';
|
import {PaginatedList} from './paginated-list';
|
||||||
import {ItemDataService} from './item-data.service';
|
import {ItemDataService} from './item-data.service';
|
||||||
import {
|
import {
|
||||||
compareArraysUsingIds, filterRelationsByTypeLabel, paginatedRelationsToItems,
|
compareArraysUsingIds,
|
||||||
|
paginatedRelationsToItems,
|
||||||
relationsToItems
|
relationsToItems
|
||||||
} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
} from '../../+item-page/simple/item-types/shared/item-relationships-utils';
|
||||||
import {ObjectCacheService} from '../cache/object-cache.service';
|
import {ObjectCacheService} from '../cache/object-cache.service';
|
||||||
@@ -93,19 +94,12 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
configureRequest(this.requestService),
|
configureRequest(this.requestService),
|
||||||
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
|
switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)),
|
||||||
getResponseFromEntry(),
|
getResponseFromEntry(),
|
||||||
tap(() => this.clearRelatedCache(uuid))
|
take(1),
|
||||||
);
|
switchMap((response) =>
|
||||||
}
|
this.clearRelatedCache(uuid).pipe(
|
||||||
|
map(() => response),
|
||||||
/**
|
)
|
||||||
* Get a combined observable containing an array of all relationships in an item, as well as an array of the relationships their types
|
),
|
||||||
* This is used for easier access of a relationship's type because they exist as observables
|
|
||||||
* @param item
|
|
||||||
*/
|
|
||||||
getItemResolvedRelsAndTypes(item: Item): Observable<[Relationship[], RelationshipType[]]> {
|
|
||||||
return observableCombineLatest(
|
|
||||||
this.getItemRelationshipsArray(item),
|
|
||||||
this.getItemRelationshipTypesArray(item)
|
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -252,23 +246,33 @@ export class RelationshipService extends DataService<Relationship> {
|
|||||||
} else {
|
} else {
|
||||||
findAllOptions.searchParams = searchParams;
|
findAllOptions.searchParams = searchParams;
|
||||||
}
|
}
|
||||||
return this.searchBy('byLabel', findAllOptions);
|
return this.searchBy('byLabel', findAllOptions).pipe(
|
||||||
|
tap((relationshipsRD) => relationshipsRD.payload.page.forEach(
|
||||||
|
(relationship) => console.log('relationship: ' + relationship.id))
|
||||||
|
),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear object and request caches of the items related to a relationship (left and right items)
|
* Clear object and request caches of the items related to a relationship (left and right items)
|
||||||
* @param uuid
|
* @param uuid
|
||||||
*/
|
*/
|
||||||
clearRelatedCache(uuid: string) {
|
clearRelatedCache(uuid: string): Observable<void> {
|
||||||
this.findById(uuid).pipe(
|
return this.findById(uuid).pipe(
|
||||||
getSucceededRemoteData(),
|
getSucceededRemoteData(),
|
||||||
flatMap((rd: RemoteData<Relationship>) => observableCombineLatest(rd.payload.leftItem.pipe(getSucceededRemoteData()), rd.payload.rightItem.pipe(getSucceededRemoteData()))),
|
switchMap((rd: RemoteData<Relationship>) =>
|
||||||
take(1)
|
observableCombineLatest(
|
||||||
).subscribe(([leftItem, rightItem]) => {
|
rd.payload.leftItem.pipe(getSucceededRemoteData()),
|
||||||
|
rd.payload.rightItem.pipe(getSucceededRemoteData())
|
||||||
|
)
|
||||||
|
),
|
||||||
|
take(1),
|
||||||
|
map(([leftItem, rightItem]) => {
|
||||||
this.objectCache.remove(leftItem.payload.self);
|
this.objectCache.remove(leftItem.payload.self);
|
||||||
this.objectCache.remove(rightItem.payload.self);
|
this.objectCache.remove(rightItem.payload.self);
|
||||||
this.requestService.removeByHrefSubstring(leftItem.payload.self);
|
this.requestService.removeByHrefSubstring(leftItem.payload.self);
|
||||||
this.requestService.removeByHrefSubstring(rightItem.payload.self);
|
this.requestService.removeByHrefSubstring(rightItem.payload.self);
|
||||||
});
|
}),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -43,8 +43,8 @@ export class HALEndpointService {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
public getEndpoint(linkPath: string): Observable<string> {
|
public getEndpoint(linkPath: string, startHref?: string): Observable<string> {
|
||||||
return this.getEndpointAt(this.getRootHref(), ...linkPath.split('/'));
|
return this.getEndpointAt(startHref || this.getRootHref(), ...linkPath.split('/'));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
Reference in New Issue
Block a user