Merge branch 'w2p-64574_Item-page-entities' into w2p-62849_relationships-in-submission

This commit is contained in:
lotte
2019-08-28 14:28:06 +02:00
36 changed files with 563 additions and 608 deletions

View File

@@ -232,10 +232,10 @@
"item.edit.relationships.edit.buttons.remove": "Remove", "item.edit.relationships.edit.buttons.remove": "Remove",
"item.edit.relationships.edit.buttons.undo": "Undo changes", "item.edit.relationships.edit.buttons.undo": "Undo changes",
"item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", "item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button",
"item.edit.relationships.notifications.discarded.title": "Changed discarded", "item.edit.relationships.notifications.discarded.title": "Changes discarded",
"item.edit.relationships.notifications.failed.title": "Error deleting relationship", "item.edit.relationships.notifications.failed.title": "Error deleting relationship",
"item.edit.relationships.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", "item.edit.relationships.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts",
"item.edit.relationships.notifications.outdated.title": "Changed outdated", "item.edit.relationships.notifications.outdated.title": "Changes outdated",
"item.edit.relationships.notifications.saved.content": "Your changes to this item's relationships were saved.", "item.edit.relationships.notifications.saved.content": "Your changes to this item's relationships were saved.",
"item.edit.relationships.notifications.saved.title": "Relationships saved", "item.edit.relationships.notifications.saved.title": "Relationships saved",
"item.edit.relationships.reinstate-button": "Undo", "item.edit.relationships.reinstate-button": "Undo",
@@ -294,6 +294,8 @@
"item.page.link.full": "Full item page", "item.page.link.full": "Full item page",
"item.page.link.simple": "Simple item page", "item.page.link.simple": "Simple item page",
"item.page.person.search.title": "Articles by this author", "item.page.person.search.title": "Articles by this author",
"item.page.related-items.view-more": "View more",
"item.page.related-items.view-less": "View less",
"item.page.subject": "Keywords", "item.page.subject": "Keywords",
"item.page.uri": "URI", "item.page.uri": "URI",
"item.select.confirm": "Confirm selected", "item.select.confirm": "Confirm selected",

View File

@@ -17,8 +17,8 @@ import { TranslateService } from '@ngx-translate/core';
import { RegistryService } from '../../../core/registry/registry.service'; import { RegistryService } from '../../../core/registry/registry.service';
import { MetadatumViewModel } from '../../../core/shared/metadata.models'; import { MetadatumViewModel } from '../../../core/shared/metadata.models';
import { Metadata } from '../../../core/shared/metadata.utils'; import { Metadata } from '../../../core/shared/metadata.utils';
import { MetadataField } from '../../../core/metadata/metadata-field.model';
import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component';
import { MetadataField } from '../../../core/metadata/metadata-field.model';
@Component({ @Component({
selector: 'ds-item-metadata', selector: 'ds-item-metadata',

View File

@@ -36,7 +36,6 @@ let relationshipType;
describe('EditRelationshipListComponent', () => { describe('EditRelationshipListComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
relationshipType = Object.assign(new RelationshipType(), { relationshipType = Object.assign(new RelationshipType(), {
type: ResourceType.RelationshipType,
id: '1', id: '1',
uuid: '1', uuid: '1',
leftLabel: 'isAuthorOfPublication', leftLabel: 'isAuthorOfPublication',
@@ -98,7 +97,7 @@ describe('EditRelationshipListComponent', () => {
relationshipService = jasmine.createSpyObj('relationshipService', relationshipService = jasmine.createSpyObj('relationshipService',
{ {
getRelatedItemsByLabel: observableOf([author1, author2]), getRelatedItemsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [author1, author2]))),
} }
); );

View File

@@ -4,8 +4,10 @@ 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 { RelationshipService } from '../../../../core/data/relationship.service';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { switchMap } from 'rxjs/operators'; import { map, switchMap } from 'rxjs/operators';
import { hasValue } from '../../../../shared/empty.util'; import { hasValue } from '../../../../shared/empty.util';
import { RemoteData } from '../../../../core/data/remote-data';
import { PaginatedList } from '../../../../core/data/paginated-list';
@Component({ @Component({
selector: 'ds-edit-relationship-list', selector: 'ds-edit-relationship-list',
@@ -63,7 +65,7 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
* Transform the item's relationships of a specific type into related items * Transform the item's relationships of a specific type into related items
* @param label The relationship type's label * @param label The relationship type's label
*/ */
public getRelatedItemsByLabel(label: string): Observable<Item[]> { public getRelatedItemsByLabel(label: string): Observable<RemoteData<PaginatedList<Item>>> {
return this.relationshipService.getRelatedItemsByLabel(this.item, label); return this.relationshipService.getRelatedItemsByLabel(this.item, label);
} }
@@ -73,7 +75,7 @@ export class EditRelationshipListComponent implements OnInit, OnChanges {
*/ */
public getUpdatesByLabel(label: string): Observable<FieldUpdates> { public getUpdatesByLabel(label: string): Observable<FieldUpdates> {
return this.getRelatedItemsByLabel(label).pipe( return this.getRelatedItemsByLabel(label).pipe(
switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, items)) switchMap((itemsRD) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, itemsRD.payload.page))
) )
} }

View File

@@ -32,7 +32,6 @@ let el;
describe('EditRelationshipComponent', () => { describe('EditRelationshipComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
relationshipType = Object.assign(new RelationshipType(), { relationshipType = Object.assign(new RelationshipType(), {
type: ResourceType.RelationshipType,
id: '1', id: '1',
uuid: '1', uuid: '1',
leftLabel: 'isAuthorOfPublication', leftLabel: 'isAuthorOfPublication',

View File

@@ -66,7 +66,6 @@ describe('ItemRelationshipsComponent', () => {
const date = new Date(); const date = new Date();
relationshipType = Object.assign(new RelationshipType(), { relationshipType = Object.assign(new RelationshipType(), {
type: ResourceType.RelationshipType,
id: '1', id: '1',
uuid: '1', uuid: '1',
leftLabel: 'isAuthorOfPublication', leftLabel: 'isAuthorOfPublication',
@@ -227,10 +226,8 @@ describe('ItemRelationshipsComponent', () => {
comp.submit(); comp.submit();
}); });
it('it should delete the correct relationship and de-cache the current item', () => { it('it should delete the correct relationship', () => {
expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid); expect(relationshipService.deleteRelationship).toHaveBeenCalledWith(relationships[1].uuid);
expect(objectCache.remove).toHaveBeenCalledWith(item.self);
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self);
}); });
}); });
}); });

View File

@@ -140,10 +140,6 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl
this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage); this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage);
}); });
if (successfulResponses.length > 0) { if (successfulResponses.length > 0) {
// Remove the item's cache to make sure the lists are reloaded with the newest values
this.objectCache.remove(this.item.self);
this.requestService.removeByHrefSubstring(this.item.self);
// Send a notification that the removal was successful
this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved')); this.notificationsService.success(this.getNotificationTitle('saved'), this.getNotificationContent('saved'));
} }
} }

View File

@@ -28,19 +28,24 @@
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<ds-metadata-representation-list <ds-metadata-representation-list
[label]="'relationships.isAuthorOf' | translate" [parentItem]="item"
[representations]="authors$ | async"> [itemType]="'Person'"
[metadataField]="'dc.contributor.author'"
[label]="'relationships.isAuthorOf' | translate">
</ds-metadata-representation-list> </ds-metadata-representation-list>
<ds-related-items <ds-related-items
[items]="projects$ | async" [parentItem]="item"
[relationType]="'isProjectOfPublication'"
[label]="'relationships.isProjectOf' | translate"> [label]="'relationships.isProjectOf' | translate">
</ds-related-items> </ds-related-items>
<ds-related-items <ds-related-items
[items]="orgUnits$ | async" [parentItem]="item"
[relationType]="'isOrgUnitOfPublication'"
[label]="'relationships.isOrgUnitOf' | translate"> [label]="'relationships.isOrgUnitOf' | translate">
</ds-related-items> </ds-related-items>
<ds-related-items <ds-related-items
[items]="journalIssues$ | async" [parentItem]="item"
[relationType]="'isJournalIssueOfPublication'"
[label]="'relationships.isJournalIssueOf' | translate"> [label]="'relationships.isJournalIssueOf' | translate">
</ds-related-items> </ds-related-items>
<ds-item-page-abstract-field [item]="item"></ds-item-page-abstract-field> <ds-item-page-abstract-field [item]="item"></ds-item-page-abstract-field>

View File

@@ -1,12 +1,9 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Item } from '../../../../core/shared/item.model';
import { import {
DEFAULT_ITEM_TYPE, ItemViewMode, DEFAULT_ITEM_TYPE, ItemViewMode,
rendersItemType rendersItemType
} from '../../../../shared/items/item-type-decorator'; } from '../../../../shared/items/item-type-decorator';
import { ItemComponent } from '../shared/item.component'; import { ItemComponent } from '../shared/item.component';
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
@rendersItemType('Publication', ItemViewMode.Detail) @rendersItemType('Publication', ItemViewMode.Detail)
@rendersItemType(DEFAULT_ITEM_TYPE, ItemViewMode.Detail) @rendersItemType(DEFAULT_ITEM_TYPE, ItemViewMode.Detail)
@@ -16,36 +13,5 @@ import { MetadataRepresentation } from '../../../../core/shared/metadata-represe
templateUrl: './publication.component.html', templateUrl: './publication.component.html',
changeDetection: ChangeDetectionStrategy.OnPush, changeDetection: ChangeDetectionStrategy.OnPush,
}) })
export class PublicationComponent extends ItemComponent implements OnInit { export class PublicationComponent extends ItemComponent {
/**
* The authors related to this publication
*/
authors$: Observable<MetadataRepresentation[]>;
/**
* The projects related to this publication
*/
projects$: Observable<Item[]>;
/**
* The organisation units related to this publication
*/
orgUnits$: Observable<Item[]>;
/**
* The journal issues related to this publication
*/
journalIssues$: Observable<Item[]>;
ngOnInit(): void {
super.ngOnInit();
this.authors$ = this.buildRepresentations('Person', 'dc.contributor.author');
this.projects$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isProjectOfPublication');
this.orgUnits$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isOrgUnitOfPublication');
this.journalIssues$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isJournalIssueOfPublication');
}
} }

View File

@@ -1,23 +1,12 @@
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { getSucceededRemoteData } from '../../../../core/shared/operators';
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model'; import { hasValue } from '../../../../shared/empty.util';
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { MetadataValue } from '../../../../core/shared/metadata.models';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { hasNoValue, hasValue } from '../../../../shared/empty.util';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
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 { distinctUntilChanged, flatMap, map, switchMap } from 'rxjs/operators';
import { distinctUntilChanged, filter, flatMap, map, startWith, switchMap } from 'rxjs/operators'; import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs';
import {
combineLatest as observableCombineLatest,
of as observableOf,
zip as observableZip
} from 'rxjs';
import { ItemDataService } from '../../../../core/data/item-data.service';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { RelationshipService } from '../../../../core/data/relationship.service';
import { RemoteData } from '../../../../core/data/remote-data';
import { PaginatedList } from '../../../../core/data/paginated-list'; import { PaginatedList } from '../../../../core/data/paginated-list';
import { RemoteData } from '../../../../core/data/remote-data';
/** /**
* Operator for comparing arrays using a mapping function * Operator for comparing arrays using a mapping function
@@ -28,7 +17,7 @@ import { PaginatedList } from '../../../../core/data/paginated-list';
*/ */
export const compareArraysUsing = <T>(mapFn: (t: T) => any) => export const compareArraysUsing = <T>(mapFn: (t: T) => any) =>
(a: T[], b: T[]): boolean => { (a: T[], b: T[]): boolean => {
if (!Array.isArray(a) || !Array.isArray(b)) { if (!Array.isArray(a) || ! Array.isArray(b)) {
return false return false
} }
@@ -75,39 +64,35 @@ export const relationsToItems = (thisId: string) =>
); );
/** /**
* Operator for turning a list of relationships into a list of metadatarepresentations given the original metadata * Operator for turning a paginated list of relationships into a paginated list of the relevant items
* @param parentId The id of the parent item * The result is wrapped in the original RemoteData and PaginatedList
* @param itemType The type of relation this list resembles (for creating representations) * @param {string} thisId The item's id of which the relations belong to
* @param metadata The list of original Metadatum objects * @returns {(source: Observable<Relationship[]>) => Observable<Item[]>}
* @param ids The ItemDataService to use for fetching Items from the Rest API
*/ */
export const relationsToRepresentations = (parentId: string, itemType: string, metadata: MetadataValue[], ids: ItemDataService) => export const paginatedRelationsToItems = (thisId: string) =>
(source: Observable<Relationship[]>): Observable<MetadataRepresentation[]> => (source: Observable<RemoteData<PaginatedList<Relationship>>>): Observable<RemoteData<PaginatedList<Item>>> =>
source.pipe( source.pipe(
flatMap((rels: Relationship[]) => getSucceededRemoteData(),
observableZip( switchMap((relationshipsRD: RemoteData<PaginatedList<Relationship>>) => {
...metadata return observableZip(
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum)) ...relationshipsRD.payload.page.map((rel: Relationship) => observableCombineLatest(rel.leftItem, rel.rightItem))
.map((metadatum: MetadataValue) => { ).pipe(
if (metadatum.isVirtual) { map((arr) =>
const matchingRels = rels.filter((rel: Relationship) => ('' + rel.id) === metadatum.virtualValue); arr
if (matchingRels.length > 0) { .filter(([leftItem, rightItem]) => leftItem.hasSucceeded && rightItem.hasSucceeded)
const matchingRel = matchingRels[0]; .map(([leftItem, rightItem]) => {
return observableCombineLatest(matchingRel.leftItem, matchingRel.rightItem).pipe( if (leftItem.payload.id === thisId) {
map(([leftItem, rightItem]) => { return rightItem.payload;
if (leftItem.payload.id === parentId) { } else if (rightItem.payload.id === thisId) {
return rightItem.payload; return leftItem.payload;
} else if (rightItem.payload.id === parentId) {
return leftItem.payload;
}
}),
map((item: Item) => Object.assign(new ItemMetadataRepresentation(), item))
);
} }
} else { })
return observableOf(Object.assign(new MetadatumRepresentation(itemType), metadatum)); .filter((item: Item) => hasValue(item))
} ),
}) distinctUntilChanged(compareArraysUsingIds()),
map((relatedItems: Item[]) =>
Object.assign(relationshipsRD, { payload: Object.assign(relationshipsRD.payload, { page: relatedItems } )})
)
) )
) })
); );

View File

@@ -12,18 +12,8 @@ import { TruncatePipe } from '../../../../shared/utils/truncate.pipe';
import { isNotEmpty } from '../../../../shared/empty.util'; import { isNotEmpty } from '../../../../shared/empty.util';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import { PaginatedList } from '../../../../core/data/paginated-list'; import { PaginatedList } from '../../../../core/data/paginated-list';
import { RemoteData } from '../../../../core/data/remote-data';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { PageInfo } from '../../../../core/shared/page-info.model'; import { PageInfo } from '../../../../core/shared/page-info.model';
import { ItemComponent } from './item.component';
import { of as observableOf } from 'rxjs';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { VarDirective } from '../../../../shared/utils/var.directive';
import { Observable } from 'rxjs/internal/Observable';
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { MetadataMap, MetadataValue } from '../../../../core/shared/metadata.models';
import { compareArraysUsing, compareArraysUsingIds } from './item-relationships-utils'; import { compareArraysUsing, compareArraysUsingIds } from './item-relationships-utils';
import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils';
import { RelationshipService } from '../../../../core/data/relationship.service'; import { RelationshipService } from '../../../../core/data/relationship.service';
@@ -311,116 +301,4 @@ describe('ItemComponent', () => {
}); });
}); });
describe('when calling buildRepresentations', () => {
let comp: ItemComponent;
let fixture: ComponentFixture<ItemComponent>;
const metadataField = 'dc.contributor.author';
const relatedItem = Object.assign(new Item(), {
id: '2',
metadata: Object.assign(new MetadataMap(), {
'dc.title': [
{
language: 'en_US',
value: 'related item'
}
]
})
});
const mockItem = Object.assign(new Item(), {
id: '1',
uuid: '1',
metadata: new MetadataMap()
});
mockItem.relationships = createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [
Object.assign(new Relationship(), {
uuid: '123',
id: '123',
leftItem: createSuccessfulRemoteDataObject$(mockItem),
rightItem: createSuccessfulRemoteDataObject$(relatedItem),
relationshipType: createSuccessfulRemoteDataObject$(new RelationshipType())
})
]));
mockItem.metadata[metadataField] = [
{
value: 'Second value',
place: 1
},
{
value: 'Third value',
place: 2,
authority: 'virtual::123'
},
{
value: 'First value',
place: 0
},
{
value: 'Fourth value',
place: 3,
authority: '123'
}
] as MetadataValue[];
const mockItemDataService = Object.assign({
findById: (id) => {
if (id === relatedItem.id) {
return createSuccessfulRemoteDataObject$(relatedItem)
}
}
}) as ItemDataService;
let representations: Observable<MetadataRepresentation[]>;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
}), BrowserAnimationsModule],
declarations: [ItemComponent, VarDirective],
providers: [
{provide: ITEM, useValue: mockItem},
{provide: RelationshipService, useValue: {}}
],
schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(ItemComponent, {
set: {changeDetection: ChangeDetectionStrategy.Default}
}).compileComponents();
}));
beforeEach(async(() => {
fixture = TestBed.createComponent(ItemComponent);
comp = fixture.componentInstance;
fixture.detectChanges();
representations = comp.buildRepresentations('bogus', metadataField);
}));
it('should contain exactly 4 metadata-representations', () => {
representations.subscribe((reps: MetadataRepresentation[]) => {
expect(reps.length).toEqual(4);
});
});
it('should have all the representations in the correct order', () => {
representations.subscribe((reps: MetadataRepresentation[]) => {
expect(reps[0].getValue()).toEqual('First value');
expect(reps[1].getValue()).toEqual('Second value');
expect(reps[2].getValue()).toEqual('related item');
expect(reps[3].getValue()).toEqual('Fourth value');
});
});
it('should have created the correct MetadatumRepresentation and ItemMetadataRepresentation objects for the correct Metadata', () => {
representations.subscribe((reps: MetadataRepresentation[]) => {
expect(reps[0] instanceof MetadatumRepresentation).toEqual(true);
expect(reps[1] instanceof MetadatumRepresentation).toEqual(true);
expect(reps[2] instanceof ItemMetadataRepresentation).toEqual(true);
expect(reps[3] instanceof MetadatumRepresentation).toEqual(true);
});
});
})
}); });

View File

@@ -1,59 +1,6 @@
import { Component, Inject, OnInit } from '@angular/core'; import { Component, Inject } from '@angular/core';
import { Observable , zip as observableZip, combineLatest as observableCombineLatest } from 'rxjs';
import { distinctUntilChanged, filter, flatMap, map } from 'rxjs/operators';
import { ItemDataService } from '../../../../core/data/item-data.service';
import { PaginatedList } from '../../../../core/data/paginated-list';
import { RemoteData } from '../../../../core/data/remote-data';
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
import { Relationship } from '../../../../core/shared/item-relationships/relationship.model';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component'; import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { of } from 'rxjs/internal/observable/of';
import { MetadataValue } from '../../../../core/shared/metadata.models';
import { compareArraysUsingIds } from './item-relationships-utils';
import { RelationshipService } from '../../../../core/data/relationship.service';
/**
* Operator for turning a list of relationships into a list of metadatarepresentations given the original metadata
* @param thisId The id of the parent item
* @param itemType The type of relation this list resembles (for creating representations)
* @param metadata The list of original Metadatum objects
*/
export const relationsToRepresentations = (thisId: string, itemType: string, metadata: MetadataValue[]) =>
(source: Observable<Relationship[]>): Observable<MetadataRepresentation[]> =>
source.pipe(
flatMap((rels: Relationship[]) =>
observableZip(
...metadata
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
.map((metadatum: MetadataValue) => {
if (metadatum.isVirtual) {
const matchingRels = rels.filter((rel: Relationship) => ('' + rel.id) === metadatum.virtualValue);
if (matchingRels.length > 0) {
const matchingRel = matchingRels[0];
return observableCombineLatest(matchingRel.leftItem, matchingRel.rightItem).pipe(
filter(([leftItem, rightItem]) => leftItem.hasSucceeded && rightItem.hasSucceeded),
map(([leftItem, rightItem]) => {
if (leftItem.payload.id === thisId) {
return rightItem.payload;
} else if (rightItem.payload.id === thisId) {
return leftItem.payload;
}
}),
map((item: Item) => Object.assign(new ItemMetadataRepresentation(), item))
);
}
} else {
return of(Object.assign(new MetadatumRepresentation(itemType), metadatum));
}
})
)
)
);
@Component({ @Component({
selector: 'ds-item', selector: 'ds-item',
@@ -62,33 +9,9 @@ export const relationsToRepresentations = (thisId: string, itemType: string, met
/** /**
* A generic component for displaying metadata and relations of an item * A generic component for displaying metadata and relations of an item
*/ */
export class ItemComponent implements OnInit { export class ItemComponent {
constructor( constructor(
@Inject(ITEM) public item: Item, @Inject(ITEM) public item: Item
protected relationshipService: RelationshipService
) {} ) {}
ngOnInit(): void {
}
/**
* Build a list of MetadataRepresentations for the current item. This combines all metadata and relationships of a
* certain type.
* @param itemType The type of item we're building representations of. Used for matching templates.
* @param metadataField The metadata field that resembles the item type.
*/
buildRepresentations(itemType: string, metadataField: string): Observable<MetadataRepresentation[]> {
const metadata = this.item.findMetadataSortedByPlace(metadataField);
const relsCurrentPage$ = this.item.relationships.pipe(
getSucceededRemoteData(),
getRemoteDataPayload(),
map((pl: PaginatedList<Relationship>) => pl.page),
distinctUntilChanged(compareArraysUsingIds())
);
return relsCurrentPage$.pipe(
relationsToRepresentations(this.item.id, itemType, metadata)
);
}
} }

View File

@@ -1,5 +1,11 @@
<ds-metadata-field-wrapper *ngIf="representations && representations.length > 0" [label]="label"> <ds-metadata-field-wrapper *ngIf="representations$ && (representations$ | async)?.length > 0" [label]="label">
<ds-item-type-switcher *ngFor="let rep of representations" <ds-item-type-switcher *ngFor="let rep of (representations$ | async)"
[object]="rep" [viewMode]="viewMode"> [object]="rep" [viewMode]="viewMode">
</ds-item-type-switcher> </ds-item-type-switcher>
<div *ngIf="(representations$ | async)?.length < total" class="mt-2">
<a [routerLink]="" (click)="viewMore()">{{'item.page.related-items.view-more' | translate}}</a>
</div>
<div *ngIf="limit > originalLimit" class="mt-2">
<a [routerLink]="" (click)="viewLess()">{{'item.page.related-items.view-less' | translate}}</a>
</div>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>

View File

@@ -2,23 +2,72 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { MetadataRepresentationListComponent } from './metadata-representation-list.component'; import { MetadataRepresentationListComponent } from './metadata-representation-list.component';
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; import { RelationshipService } from '../../../core/data/relationship.service';
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; import { Item } from '../../../core/shared/item.model';
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { TranslateModule } from '@ngx-translate/core';
const itemType = 'type'; const itemType = 'Person';
const metadataRepresentation1 = new MetadatumRepresentation(itemType); const metadataField = 'dc.contributor.author';
const metadataRepresentation2 = new ItemMetadataRepresentation(); const parentItem: Item = Object.assign(new Item(), {
const representations = [metadataRepresentation1, metadataRepresentation2]; id: 'parent-item',
metadata: {
'dc.contributor.author': [
{
language: null,
value: 'Related Author with authority',
authority: 'virtual::related-author',
place: 2
},
{
language: null,
value: 'Author without authority',
place: 1
}
],
'dc.title': [
{
language: null,
value: 'Parent Item'
}
]
}
});
const relatedAuthor: Item = Object.assign(new Item(), {
id: 'related-author',
metadata: {
'dc.title': [
{
language: null,
value: 'Related Author'
}
]
}
});
const relation: Relationship = Object.assign(new Relationship(), {
leftItem: createSuccessfulRemoteDataObject$(parentItem),
rightItem: createSuccessfulRemoteDataObject$(relatedAuthor)
});
let relationshipService: RelationshipService;
describe('MetadataRepresentationListComponent', () => { describe('MetadataRepresentationListComponent', () => {
let comp: MetadataRepresentationListComponent; let comp: MetadataRepresentationListComponent;
let fixture: ComponentFixture<MetadataRepresentationListComponent>; let fixture: ComponentFixture<MetadataRepresentationListComponent>;
relationshipService = jasmine.createSpyObj('relationshipService',
{
findById: createSuccessfulRemoteDataObject$(relation)
}
);
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [TranslateModule.forRoot()],
declarations: [MetadataRepresentationListComponent], declarations: [MetadataRepresentationListComponent],
providers: [], providers: [
{ provide: RelationshipService, useValue: relationshipService }
],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(MetadataRepresentationListComponent, { }).overrideComponent(MetadataRepresentationListComponent, {
set: {changeDetection: ChangeDetectionStrategy.Default} set: {changeDetection: ChangeDetectionStrategy.Default}
@@ -28,13 +77,45 @@ describe('MetadataRepresentationListComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
fixture = TestBed.createComponent(MetadataRepresentationListComponent); fixture = TestBed.createComponent(MetadataRepresentationListComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.representations = representations; comp.parentItem = parentItem;
comp.itemType = itemType;
comp.metadataField = metadataField;
fixture.detectChanges(); fixture.detectChanges();
})); }));
it(`should load ${representations.length} item-type-switcher components`, () => { it('should load 2 item-type-switcher components', () => {
const fields = fixture.debugElement.queryAll(By.css('ds-item-type-switcher')); const fields = fixture.debugElement.queryAll(By.css('ds-item-type-switcher'));
expect(fields.length).toBe(representations.length); expect(fields.length).toBe(2);
});
it('should initialize the original limit', () => {
expect(comp.originalLimit).toEqual(comp.limit);
});
describe('when viewMore is called', () => {
beforeEach(() => {
comp.viewMore();
});
it('should set the limit to a high number in order to retrieve all metadata representations', () => {
expect(comp.limit).toBeGreaterThanOrEqual(999);
});
});
describe('when viewLess is called', () => {
let originalLimit;
beforeEach(() => {
// Store the original value of limit
originalLimit = comp.limit;
// Set limit to a random number
comp.limit = 458;
comp.viewLess();
});
it('should reset the limit to the original value', () => {
expect(comp.limit).toEqual(originalLimit);
});
}); });
}); });

View File

@@ -1,6 +1,17 @@
import { Component, Input } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model'; import { MetadataRepresentation } from '../../../core/shared/metadata-representation/metadata-representation.model';
import { ItemViewMode } from '../../../shared/items/item-type-decorator'; import { ItemViewMode } from '../../../shared/items/item-type-decorator';
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../../core/data/remote-data';
import { RelationshipService } from '../../../core/data/relationship.service';
import { Item } from '../../../core/shared/item.model';
import { zip as observableZip, combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
import { MetadataValue } from '../../../core/shared/metadata.models';
import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
import { filter, map, switchMap } from 'rxjs/operators';
import { getSucceededRemoteData } from '../../../core/shared/operators';
import { Relationship } from '../../../core/shared/item-relationships/relationship.model';
import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model';
@Component({ @Component({
selector: 'ds-metadata-representation-list', selector: 'ds-metadata-representation-list',
@@ -8,22 +19,123 @@ import { ItemViewMode } from '../../../shared/items/item-type-decorator';
}) })
/** /**
* This component is used for displaying metadata * This component is used for displaying metadata
* It expects a list of MetadataRepresentation objects and a label to put on top of the list * It expects an item and a metadataField to fetch metadata
* It expects an itemType to resolve the metadata to a an item
* It expects a label to put on top of the list
*/ */
export class MetadataRepresentationListComponent { export class MetadataRepresentationListComponent implements OnInit {
/** /**
* A list of metadata-representations to display * The parent of the list of related items to display
*/ */
@Input() representations: MetadataRepresentation[]; @Input() parentItem: Item;
/**
* The type of item to create a representation of
*/
@Input() itemType: string;
/**
* The metadata field to use for fetching metadata from the item
*/
@Input() metadataField: string;
/** /**
* An i18n label to use as a title for the list * An i18n label to use as a title for the list
*/ */
@Input() label: string; @Input() label: string;
/**
* The max amount of representations to display
* Defaults to 10
* The default can optionally be overridden by providing the limit as input to the component
*/
@Input() limit = 10;
/**
* A list of metadata-representations to display
*/
representations$: Observable<MetadataRepresentation[]>;
/** /**
* The view-mode we're currently on * The view-mode we're currently on
* @type {ElementViewMode} * @type {ElementViewMode}
*/ */
viewMode = ItemViewMode.Metadata; viewMode = ItemViewMode.Metadata;
/**
* The originally provided limit
* Used for resetting the limit to the original value when collapsing the list
*/
originalLimit: number;
/**
* The total amount of metadata values available
*/
total: number;
constructor(public relationshipService: RelationshipService) {
}
ngOnInit(): void {
this.originalLimit = this.limit;
this.setRepresentations();
}
/**
* Initialize the metadata representations
*/
setRepresentations() {
const metadata = this.parentItem.findMetadataSortedByPlace(this.metadataField);
this.total = metadata.length;
this.representations$ = this.resolveMetadataRepresentations(metadata);
}
/**
* Resolve a list of metadata values to a list of metadata representations
* @param metadata
*/
resolveMetadataRepresentations(metadata: MetadataValue[]): Observable<MetadataRepresentation[]> {
return observableZip(
...metadata
.slice(0, this.limit)
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
.map((metadatum: MetadataValue) => {
if (metadatum.isVirtual) {
return this.relationshipService.findById(metadatum.virtualValue).pipe(
getSucceededRemoteData(),
switchMap((relRD: RemoteData<Relationship>) =>
observableCombineLatest(relRD.payload.leftItem, relRD.payload.rightItem).pipe(
filter(([leftItem, rightItem]) => leftItem.hasSucceeded && rightItem.hasSucceeded),
map(([leftItem, rightItem]) => {
if (leftItem.payload.id === this.parentItem.id) {
return rightItem.payload;
} else if (rightItem.payload.id === this.parentItem.id) {
return leftItem.payload;
}
}),
map((item: Item) => Object.assign(new ItemMetadataRepresentation(), item))
)
));
} else {
return observableOf(Object.assign(new MetadatumRepresentation(this.itemType), metadatum));
}
})
);
}
/**
* Expand the list to display all metadata representations
*/
viewMore() {
this.limit = 9999;
this.setRepresentations();
}
/**
* Collapse the list to display the originally displayed metadata representations
*/
viewLess() {
this.limit = this.originalLimit;
this.setRepresentations();
}
} }

View File

@@ -1,6 +1,11 @@
import { Component, Input } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { ItemViewMode } from '../../../shared/items/item-type-decorator'; import { ItemViewMode } from '../../../shared/items/item-type-decorator';
import { Observable } from 'rxjs/internal/Observable';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list';
import { RelationshipService } from '../../../core/data/relationship.service';
import { FindAllOptions } from '../../../core/data/request.models';
@Component({ @Component({
selector: 'ds-related-items', selector: 'ds-related-items',
@@ -9,22 +14,72 @@ import { ItemViewMode } from '../../../shared/items/item-type-decorator';
}) })
/** /**
* This component is used for displaying relations between items * This component is used for displaying relations between items
* It expects a list of items to display and a label to put on top * It expects a parent item and relationship type, as well as a label to display on top
*/ */
export class RelatedItemsComponent { export class RelatedItemsComponent implements OnInit {
/** /**
* A list of items to display * The parent of the list of related items to display
*/ */
@Input() items: Item[]; @Input() parentItem: Item;
/**
* The label of the relationship type to display
* Used in sending a search request to the REST API
*/
@Input() relationType: string;
/**
* Default options to start a search request with
* Optional input, should you wish a different page size (or other options)
*/
@Input() options = Object.assign(new FindAllOptions(), { elementsPerPage: 5 });
/** /**
* An i18n label to use as a title for the list (usually describes the relation) * An i18n label to use as a title for the list (usually describes the relation)
*/ */
@Input() label: string; @Input() label: string;
/**
* The list of related items
*/
items$: Observable<RemoteData<PaginatedList<Item>>>;
/**
* Search options for displaying all elements in a list
*/
allOptions = Object.assign(new FindAllOptions(), { elementsPerPage: 9999 });
/** /**
* The view-mode we're currently on * The view-mode we're currently on
* @type {ElementViewMode} * @type {ElementViewMode}
*/ */
viewMode = ItemViewMode.Summary; viewMode = ItemViewMode.Summary;
/**
* Whether or not the list is currently expanded to show all related items
*/
showingAll = false;
constructor(public relationshipService: RelationshipService) {
}
ngOnInit(): void {
this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.options);
}
/**
* Expand the list to display all related items
*/
viewMore() {
this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.allOptions);
this.showingAll = true;
}
/**
* Collapse the list to display the originally displayed items
*/
viewLess() {
this.items$ = this.relationshipService.getRelatedItemsByLabel(this.parentItem, this.relationType, this.options);
this.showingAll = false;
}
} }

View File

@@ -1,5 +1,11 @@
<ds-metadata-field-wrapper *ngIf="items && items.length > 0" [label]="label"> <ds-metadata-field-wrapper *ngIf="(items$ | async)?.payload?.page?.length > 0" [label]="label">
<ds-item-type-switcher *ngFor="let item of items" <ds-item-type-switcher *ngFor="let item of (items$ | async)?.payload?.page"
[object]="item" [viewMode]="viewMode"> [object]="item" [viewMode]="viewMode">
</ds-item-type-switcher> </ds-item-type-switcher>
<div *ngIf="(items$ | async)?.payload?.page?.length < (items$ | async)?.payload?.totalElements" class="mt-2" id="view-more">
<a [routerLink]="" (click)="viewMore()">{{'item.page.related-items.view-more' | translate}}</a>
</div>
<div *ngIf="showingAll" class="mt-2" id="view-less">
<a [routerLink]="" (click)="viewLess()">{{'item.page.related-items.view-less' | translate}}</a>
</div>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>

View File

@@ -2,14 +2,19 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { RelatedItemsComponent } from './related-items-component'; import { RelatedItemsComponent } from './related-items-component';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { PageInfo } from '../../../core/shared/page-info.model'; import { PageInfo } from '../../../core/shared/page-info.model';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { createRelationshipsObservable } from '../item-types/shared/item.component.spec'; import { createRelationshipsObservable } from '../item-types/shared/item.component.spec';
import { of as observableOf } from 'rxjs';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { RelationshipService } from '../../../core/data/relationship.service';
import { TranslateModule } from '@ngx-translate/core';
const parentItem: Item = Object.assign(new Item(), {
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
metadata: [],
relationships: createRelationshipsObservable()
});
const mockItem1: Item = Object.assign(new Item(), { const mockItem1: Item = Object.assign(new Item(), {
bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])),
metadata: [], metadata: [],
@@ -21,16 +26,26 @@ const mockItem2: Item = Object.assign(new Item(), {
relationships: createRelationshipsObservable() relationships: createRelationshipsObservable()
}); });
const mockItems = [mockItem1, mockItem2]; const mockItems = [mockItem1, mockItem2];
const relationType = 'isItemOfItem';
let relationshipService: RelationshipService;
describe('RelatedItemsComponent', () => { describe('RelatedItemsComponent', () => {
let comp: RelatedItemsComponent; let comp: RelatedItemsComponent;
let fixture: ComponentFixture<RelatedItemsComponent>; let fixture: ComponentFixture<RelatedItemsComponent>;
beforeEach(async(() => { beforeEach(async(() => {
relationshipService = jasmine.createSpyObj('relationshipService',
{
getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockItems)),
}
);
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [], imports: [TranslateModule.forRoot()],
declarations: [RelatedItemsComponent], declarations: [RelatedItemsComponent],
providers: [], providers: [
{ provide: RelationshipService, useValue: relationshipService }
],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(RelatedItemsComponent, { }).overrideComponent(RelatedItemsComponent, {
set: {changeDetection: ChangeDetectionStrategy.Default} set: {changeDetection: ChangeDetectionStrategy.Default}
@@ -40,7 +55,8 @@ describe('RelatedItemsComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {
fixture = TestBed.createComponent(RelatedItemsComponent); fixture = TestBed.createComponent(RelatedItemsComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.items = mockItems; comp.parentItem = parentItem;
comp.relationType = relationType;
fixture.detectChanges(); fixture.detectChanges();
})); }));
@@ -49,4 +65,32 @@ describe('RelatedItemsComponent', () => {
expect(fields.length).toBe(mockItems.length); expect(fields.length).toBe(mockItems.length);
}); });
describe('when viewMore is called', () => {
beforeEach(() => {
comp.viewMore();
});
it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments', () => {
expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, comp.allOptions);
});
it('should set showingAll to true', () => {
expect(comp.showingAll).toEqual(true);
});
});
describe('when viewLess is called', () => {
beforeEach(() => {
comp.viewLess();
});
it('should call relationship-service\'s getRelatedItemsByLabel with the correct arguments', () => {
expect(relationshipService.getRelatedItemsByLabel).toHaveBeenCalledWith(parentItem, relationType, comp.options);
});
it('should set showingAll to false', () => {
expect(comp.showingAll).toEqual(false);
});
});
}); });

View File

@@ -112,7 +112,7 @@ export class SearchPageComponent implements OnInit {
ngOnInit(): void { ngOnInit(): void {
this.searchOptions$ = this.getSearchOptions(); this.searchOptions$ = this.getSearchOptions();
this.sub = this.searchOptions$.pipe( this.sub = this.searchOptions$.pipe(
switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(observableOf(undefined))))) switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData(), startWith(undefined))))
.subscribe((results) => { .subscribe((results) => {
this.resultsRD$.next(results); this.resultsRD$.next(results);
}); });

View File

@@ -13,6 +13,8 @@ import { Item } from '../shared/item.model';
import { PaginatedList } from './paginated-list'; import { PaginatedList } from './paginated-list';
import { PageInfo } from '../shared/page-info.model'; import { PageInfo } from '../shared/page-info.model';
import { DeleteRequest } from './request.models'; import { DeleteRequest } from './request.models';
import { ObjectCacheService } from '../cache/object-cache.service';
import { Observable } from 'rxjs/internal/Observable';
describe('RelationshipService', () => { describe('RelationshipService', () => {
let service: RelationshipService; let service: RelationshipService;
@@ -22,33 +24,33 @@ describe('RelationshipService', () => {
const relationshipsEndpointURL = `${restEndpointURL}/relationships`; const relationshipsEndpointURL = `${restEndpointURL}/relationships`;
const halService: any = new HALEndpointServiceStub(restEndpointURL); const halService: any = new HALEndpointServiceStub(restEndpointURL);
const rdbService = getMockRemoteDataBuildService(); const rdbService = getMockRemoteDataBuildService();
const objectCache = Object.assign({
/* tslint:disable:no-empty */
remove: () => {}
/* tslint:enable:no-empty */
}) as ObjectCacheService;
const relationshipType = Object.assign(new RelationshipType(), { const relationshipType = Object.assign(new RelationshipType(), {
type: ResourceType.RelationshipType,
id: '1', id: '1',
uuid: '1', uuid: '1',
leftLabel: 'isAuthorOfPublication', leftLabel: 'isAuthorOfPublication',
rightLabel: 'isPublicationOfAuthor' rightLabel: 'isPublicationOfAuthor'
}); });
const relationships = [ const relationship1 = Object.assign(new Relationship(), {
Object.assign(new Relationship(), { self: relationshipsEndpointURL + '/2',
self: relationshipsEndpointURL + '/2', id: '2',
id: '2', uuid: '2',
uuid: '2', relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
leftId: 'author1', });
rightId: 'publication', const relationship2 = Object.assign(new Relationship(), {
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) self: relationshipsEndpointURL + '/3',
}), id: '3',
Object.assign(new Relationship(), { uuid: '3',
self: relationshipsEndpointURL + '/3', relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
id: '3', });
uuid: '3',
leftId: 'author2', const relationships = [ relationship1, relationship2 ];
rightId: 'publication',
relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType))
})
];
const item = Object.assign(new Item(), { const item = Object.assign(new Item(), {
self: 'fake-item-url/publication', self: 'fake-item-url/publication',
@@ -65,6 +67,10 @@ describe('RelationshipService', () => {
id: 'author2', id: 'author2',
uuid: 'author2' uuid: 'author2'
}); });
relationship1.leftItem = getRemotedataObservable(relatedItem1);
relationship1.rightItem = getRemotedataObservable(item);
relationship2.leftItem = getRemotedataObservable(relatedItem2);
relationship2.rightItem = getRemotedataObservable(item);
const relatedItems = [relatedItem1, relatedItem2]; const relatedItems = [relatedItem1, relatedItem2];
const itemService = jasmine.createSpyObj('itemService', { const itemService = jasmine.createSpyObj('itemService', {
@@ -73,10 +79,16 @@ describe('RelationshipService', () => {
function initTestService() { function initTestService() {
return new RelationshipService( return new RelationshipService(
itemService,
requestService, requestService,
halService,
rdbService, rdbService,
itemService null,
null,
halService,
objectCache,
null,
null,
null
); );
} }
@@ -93,13 +105,22 @@ describe('RelationshipService', () => {
describe('deleteRelationship', () => { describe('deleteRelationship', () => {
beforeEach(() => { beforeEach(() => {
spyOn(service, 'findById').and.returnValue(getRemotedataObservable(relationship1));
spyOn(objectCache, 'remove');
service.deleteRelationship(relationships[0].uuid).subscribe(); service.deleteRelationship(relationships[0].uuid).subscribe();
}); });
it('should send a DeleteRequest', () => { it('should send a DeleteRequest', () => {
const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationships[0].uuid); const expected = new DeleteRequest(requestService.generateRequestId(), relationshipsEndpointURL + '/' + relationship1.uuid);
expect(requestService.configure).toHaveBeenCalledWith(expected, undefined); expect(requestService.configure).toHaveBeenCalledWith(expected, undefined);
}); });
it('should clear the related items their cache', () => {
expect(objectCache.remove).toHaveBeenCalledWith(relatedItem1.self);
expect(objectCache.remove).toHaveBeenCalledWith(item.self);
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(relatedItem1.self);
expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self);
});
}); });
describe('getItemRelationshipsArray', () => { describe('getItemRelationshipsArray', () => {
@@ -126,12 +147,8 @@ describe('RelationshipService', () => {
}); });
}); });
describe('getRelatedItemsByLabel', () => {
it('should return the related items by label', () => {
service.getRelatedItemsByLabel(item, relationshipType.rightLabel).subscribe((result) => {
expect(result).toEqual(relatedItems);
});
});
})
}); });
function getRemotedataObservable(obj: any): Observable<RemoteData<any>> {
return observableOf(new RemoteData(false, false, true, undefined, obj));
}

View File

@@ -2,8 +2,8 @@ import { Injectable } from '@angular/core';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { hasNoValue, hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/empty.util'; import { hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/empty.util';
import { distinctUntilChanged, filter, map, mergeMap, skip, startWith, switchMap, take, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators'; import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators';
import { DeleteRequest, FindAllOptions, PostRequest, RestRequest } from './request.models'; import { DeleteRequest, FindAllOptions, PostRequest, RestRequest } from './request.models';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
@@ -15,16 +15,17 @@ import { RemoteData } from './remote-data';
import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs'; import { combineLatest, combineLatest as observableCombineLatest } 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 { compareArraysUsingIds, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils'; import { compareArraysUsingIds, paginatedRelationsToItems, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { HttpClient, HttpHeaders } from '@angular/common/http';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { DataService } from './data.service';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { DataService } from './data.service'; import { HttpClient, HttpHeaders } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { SearchParam } from '../cache/models/search-param.model';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
/** /**
* The service handling all relationship requests * The service handling all relationship requests
@@ -61,6 +62,15 @@ export class RelationshipService extends DataService<Relationship> {
); );
} }
/**
* Find a relationship by its UUID
* @param uuid
*/
findById(uuid: string): Observable<RemoteData<Relationship>> {
const href$ = this.getRelationshipEndpoint(uuid);
return this.rdbService.buildSingle<Relationship>(href$);
}
/** /**
* Send a delete request for a relationship by ID * Send a delete request for a relationship by ID
* @param id * @param id
@@ -183,41 +193,31 @@ export class RelationshipService extends DataService<Relationship> {
* and return the items as an array * and return the items as an array
* @param item * @param item
* @param label * @param label
* @param options
*/ */
getRelatedItemsByLabel(item: Item, label: string): Observable<Item[]> { getRelatedItemsByLabel(item: Item, label: string, options?: FindAllOptions): Observable<RemoteData<PaginatedList<Item>>> {
return this.getItemRelationshipsByLabel(item, label).pipe(relationsToItems(item.uuid)); return this.getItemRelationshipsByLabel(item, label, options).pipe(paginatedRelationsToItems(item.uuid));
} }
/** /**
* Resolve a given item's relationships into related items, filtered by a relationship label * Resolve a given item's relationships into related items, filtered by a relationship label
* and return the items as an array * and return the items as an array
* @param item * @param item
* @param label * @param label
* @param options
*/ */
getItemRelationshipsByLabel(item: Item, label: string): Observable<Relationship[]> { getItemRelationshipsByLabel(item: Item, label: string, options?: FindAllOptions): Observable<RemoteData<PaginatedList<Relationship>>> {
return this.getItemRelationshipsArray(item).pipe( let findAllOptions = new FindAllOptions();
switchMap((relationships: Relationship[]) => { if (options) {
return observableCombineLatest( findAllOptions = Object.assign(new FindAllOptions(), options);
...relationships.map((relationship: Relationship) => { }
return relationship.relationshipType.pipe( const searchParams = [ new SearchParam('label', label), new SearchParam('dso', item.id) ];
getSucceededRemoteData(), if (findAllOptions.searchParams) {
getRemoteDataPayload(), findAllOptions.searchParams = [...findAllOptions.searchParams, ...searchParams];
map((relationshipType: RelationshipType) => { } else {
if (label === relationshipType.rightLabel || label === relationshipType.leftLabel) { findAllOptions.searchParams = searchParams;
return relationship; }
} else { return this.searchBy('byLabel', findAllOptions);
return undefined;
}
}),
)
})
)
}),
map((relationships: Relationship[]) =>
relationships.filter((relationship: Relationship) => hasValue(relationship))
),
);
} }

View File

@@ -90,7 +90,7 @@ export const toDSpaceObjectListRD = () =>
source.pipe( source.pipe(
filter((rd: RemoteData<PaginatedList<SearchResult<T>>>) => rd.hasSucceeded), filter((rd: RemoteData<PaginatedList<SearchResult<T>>>) => rd.hasSucceeded),
map((rd: RemoteData<PaginatedList<SearchResult<T>>>) => { map((rd: RemoteData<PaginatedList<SearchResult<T>>>) => {
const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult<T>) => searchResult.indexableObject); const dsoPage: T[] = rd.payload.page.filter((result) => hasValue(result)).map((searchResult: SearchResult<T>) => searchResult.indexableObject);
const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList<T>; const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList<T>;
return Object.assign(rd, { payload: payload }); return Object.assign(rd, { payload: payload });
}) })

View File

@@ -1,13 +1,8 @@
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
import { Injectable, OnDestroy } from '@angular/core'; import { Injectable, OnDestroy } from '@angular/core';
import { NavigationExtras, Router } from '@angular/router'; import { NavigationExtras, Router } from '@angular/router';
import { first, map, switchMap } from 'rxjs/operators'; import { first, map, switchMap, tap } from 'rxjs/operators';
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { FacetConfigSuccessResponse, FacetValueSuccessResponse, SearchSuccessResponse } from '../../cache/response.models';
import {
FacetConfigSuccessResponse,
FacetValueSuccessResponse,
SearchSuccessResponse
} from '../../cache/response.models';
import { PaginatedList } from '../../data/paginated-list'; import { PaginatedList } from '../../data/paginated-list';
import { ResponseParsingService } from '../../data/parsing.service'; import { ResponseParsingService } from '../../data/parsing.service';
import { RemoteData } from '../../data/remote-data'; import { RemoteData } from '../../data/remote-data';
@@ -16,12 +11,6 @@ import { RequestService } from '../../data/request.service';
import { DSpaceObject } from '../dspace-object.model'; import { DSpaceObject } from '../dspace-object.model';
import { GenericConstructor } from '../generic-constructor'; import { GenericConstructor } from '../generic-constructor';
import { HALEndpointService } from '../hal-endpoint.service'; import { HALEndpointService } from '../hal-endpoint.service';
import {
configureRequest,
filterSuccessfulResponses,
getResponseFromEntry,
getSucceededRemoteData
} from '../operators';
import { URLCombiner } from '../../url-combiner/url-combiner'; import { URLCombiner } from '../../url-combiner/url-combiner';
import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util';
import { NormalizedSearchResult } from '../../../shared/search/normalized-search-result.model'; import { NormalizedSearchResult } from '../../../shared/search/normalized-search-result.model';
@@ -42,6 +31,8 @@ import { CommunityDataService } from '../../data/community-data.service';
import { ViewMode } from '../view-mode.model'; import { ViewMode } from '../view-mode.model';
import { DSpaceObjectDataService } from '../../data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../data/dspace-object-data.service';
import { RouteService } from '../../../shared/services/route.service'; import { RouteService } from '../../../shared/services/route.service';
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
import { configureRequest, filterSuccessfulResponses, getResponseFromEntry, getSucceededRemoteData } from '../operators';
/** /**
* Service that performs all general actions that have to do with the search page * Service that performs all general actions that have to do with the search page
@@ -103,11 +94,18 @@ export class SearchService implements OnDestroy {
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found * @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
*/ */
search(searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> { search(searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
const requestObs = this.halService.getEndpoint(this.searchLinkPath).pipe( const hrefObs = this.halService.getEndpoint(this.searchLinkPath).pipe(
map((url: string) => { map((url: string) => {
if (hasValue(searchOptions)) { if (hasValue(searchOptions)) {
url = (searchOptions as PaginatedSearchOptions).toRestUrl(url); return (searchOptions as PaginatedSearchOptions).toRestUrl(url);
} else {
return url;
} }
})
);
const requestObs = hrefObs.pipe(
map((url: string) => {
const request = new this.request(this.requestService.generateRequestId(), url); const request = new this.request(this.requestService.generateRequestId(), url);
const getResponseParserFn: () => GenericConstructor<ResponseParsingService> = () => { const getResponseParserFn: () => GenericConstructor<ResponseParsingService> = () => {
@@ -136,10 +134,11 @@ export class SearchService implements OnDestroy {
map((sqr: SearchQueryResponse) => { map((sqr: SearchQueryResponse) => {
return sqr.objects return sqr.objects
.filter((nsr: NormalizedSearchResult) => isNotUndefined(nsr.indexableObject)) .filter((nsr: NormalizedSearchResult) => isNotUndefined(nsr.indexableObject))
.map((nsr: NormalizedSearchResult) => { .map((nsr: NormalizedSearchResult) => new GetRequest(this.requestService.generateRequestId(), nsr.indexableObject))
return this.rdb.buildSingle(nsr.indexableObject);
})
}), }),
// Send a request for each item to ensure fresh cache
tap((reqs: RestRequest[]) => reqs.forEach((req: RestRequest) => this.requestService.configure(req))),
map((reqs: RestRequest[]) => reqs.map((req: RestRequest) => this.rdb.buildSingle(req.href))),
switchMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input)), switchMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input)),
); );
@@ -168,11 +167,20 @@ export class SearchService implements OnDestroy {
const payloadObs = observableCombineLatest(tDomainListObs, pageInfoObs).pipe( const payloadObs = observableCombineLatest(tDomainListObs, pageInfoObs).pipe(
map(([tDomainList, pageInfo]) => { map(([tDomainList, pageInfo]) => {
return new PaginatedList(pageInfo, tDomainList); return new PaginatedList(pageInfo, tDomainList.filter((obj) => hasValue(obj)));
}) })
); );
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs); return observableCombineLatest(hrefObs, tDomainListObs, requestEntryObs).pipe(
switchMap(([href, tDomainList, requestEntry]) => {
if (tDomainList.indexOf(undefined) > -1 && requestEntry && requestEntry.completed) {
this.requestService.removeByHrefSubstring(href);
return this.search(searchOptions)
} else {
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
}
})
);
} }
/** /**

View File

@@ -29,12 +29,14 @@
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<ds-related-items <ds-related-items
[items]="volumes$ | async" [parentItem]="item"
[relationType]="'isJournalVolumeOfIssue'"
[label]="'relationships.isSingleVolumeOf' | translate"> [label]="'relationships.isSingleVolumeOf' | translate">
</ds-related-items> </ds-related-items>
<ds-related-items <ds-related-items
class="mb-1 mt-1" class="mb-1 mt-1"
[items]="publications$ | async" [parentItem]="item"
[relationType]="'isPublicationOfJournalIssue'"
[label]="'relationships.isPublicationOfJournalIssue' | translate"> [label]="'relationships.isPublicationOfJournalIssue' | translate">
</ds-related-items> </ds-related-items>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="item"

View File

@@ -1,6 +1,4 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Item } from '../../../../core/shared/item.model';
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component';
@@ -14,19 +12,4 @@ import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/i
* The component for displaying metadata and relations of an item of the type Journal Issue * The component for displaying metadata and relations of an item of the type Journal Issue
*/ */
export class JournalIssueComponent extends ItemComponent { export class JournalIssueComponent extends ItemComponent {
/**
* The volumes related to this journal issue
*/
volumes$: Observable<Item[]>;
/**
* The publications related to this journal issue
*/
publications$: Observable<Item[]>;
ngOnInit(): void {
super.ngOnInit();
this.volumes$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isJournalVolumeOfIssue');
this.publications$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isPublicationOfJournalIssue');
}
} }

View File

@@ -17,11 +17,13 @@
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<ds-related-items <ds-related-items
[items]="journals$ | async" [parentItem]="item"
[relationType]="'isJournalOfVolume'"
[label]="'relationships.isSingleJournalOf' | translate"> [label]="'relationships.isSingleJournalOf' | translate">
</ds-related-items> </ds-related-items>
<ds-related-items <ds-related-items
[items]="issues$ | async" [parentItem]="item"
[relationType]="'isIssueOfJournalVolume'"
[label]="'relationships.isIssueOf' | translate"> [label]="'relationships.isIssueOf' | translate">
</ds-related-items> </ds-related-items>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="item"

View File

@@ -1,6 +1,4 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Item } from '../../../../core/shared/item.model';
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component';
@@ -14,19 +12,4 @@ import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/i
* The component for displaying metadata and relations of an item of the type Journal Volume * The component for displaying metadata and relations of an item of the type Journal Volume
*/ */
export class JournalVolumeComponent extends ItemComponent { export class JournalVolumeComponent extends ItemComponent {
/**
* The journals related to this journal volume
*/
journals$: Observable<Item[]>;
/**
* The journal issues related to this journal volume
*/
issues$: Observable<Item[]>;
ngOnInit(): void {
super.ngOnInit();
this.journals$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isJournalOfVolume');
this.issues$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isIssueOfJournalVolume');
}
} }

View File

@@ -21,7 +21,8 @@
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<ds-related-items <ds-related-items
[items]="volumes$ | async" [parentItem]="item"
[relationType]="'isVolumeOfJournal'"
[label]="'relationships.isVolumeOf' | translate"> [label]="'relationships.isVolumeOf' | translate">
</ds-related-items> </ds-related-items>
<ds-generic-item-page-field class="item-page-fields" [item]="item" <ds-generic-item-page-field class="item-page-fields" [item]="item"

View File

@@ -1,6 +1,4 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Item } from '../../../../core/shared/item.model';
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component';
@@ -14,13 +12,4 @@ import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/i
* The component for displaying metadata and relations of an item of the type Journal * The component for displaying metadata and relations of an item of the type Journal
*/ */
export class JournalComponent extends ItemComponent { export class JournalComponent extends ItemComponent {
/**
* The volumes related to this journal
*/
volumes$: Observable<Item[]>;
ngOnInit(): void {
super.ngOnInit();
this.volumes$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isVolumeOfJournal');
}
} }

View File

@@ -25,15 +25,18 @@
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<ds-related-items <ds-related-items
[items]="people$ | async" [parentItem]="item"
[relationType]="'isPersonOfOrgUnit'"
[label]="'relationships.isPersonOf' | translate"> [label]="'relationships.isPersonOf' | translate">
</ds-related-items> </ds-related-items>
<ds-related-items <ds-related-items
[items]="projects$ | async" [parentItem]="item"
[relationType]="'isProjectOfOrgUnit'"
[label]="'relationships.isProjectOf' | translate"> [label]="'relationships.isProjectOf' | translate">
</ds-related-items> </ds-related-items>
<ds-related-items <ds-related-items
[items]="publications$ | async" [parentItem]="item"
[relationType]="'isPublicationOfOrgUnit'"
[label]="'relationships.isPublicationOf' | translate"> [label]="'relationships.isPublicationOf' | translate">
</ds-related-items> </ds-related-items>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="item"

View File

@@ -1,6 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Item } from '../../../../core/shared/item.model';
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component';
@@ -13,26 +11,5 @@ import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/i
/** /**
* The component for displaying metadata and relations of an item of the type Organisation Unit * The component for displaying metadata and relations of an item of the type Organisation Unit
*/ */
export class OrgunitComponent extends ItemComponent implements OnInit { export class OrgunitComponent extends ItemComponent {
/**
* The people related to this organisation unit
*/
people$: Observable<Item[]>;
/**
* The projects related to this organisation unit
*/
projects$: Observable<Item[]>;
/**
* The publications related to this organisation unit
*/
publications$: Observable<Item[]>;
ngOnInit(): void {
super.ngOnInit();
this.people$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isPersonOfOrgUnit');
this.projects$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isProjectOfOrgUnit');
this.publications$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isPublicationOfOrgUnit');
}
} }

View File

@@ -25,11 +25,13 @@
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<ds-related-items <ds-related-items
[items]="projects$ | async" [parentItem]="item"
[relationType]="'isProjectOfPerson'"
[label]="'relationships.isProjectOf' | translate"> [label]="'relationships.isProjectOf' | translate">
</ds-related-items> </ds-related-items>
<ds-related-items <ds-related-items
[items]="orgUnits$ | async" [parentItem]="item"
[relationType]="'isOrgUnitOfPerson'"
[label]="'relationships.isOrgUnitOf' | translate"> [label]="'relationships.isOrgUnitOf' | translate">
</ds-related-items> </ds-related-items>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="item"

View File

@@ -1,11 +1,6 @@
import { Component, Inject } from '@angular/core'; import { Component } from '@angular/core';
import { Observable, of as observableOf } from 'rxjs';
import { Item } from '../../../../core/shared/item.model';
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component';
import { getQueryByRelations } from '../../../../shared/utils/relation-query.utils';
import { RelationshipService } from '../../../../core/data/relationship.service';
@rendersItemType('Person', ItemViewMode.Detail) @rendersItemType('Person', ItemViewMode.Detail)
@Component({ @Component({
@@ -17,45 +12,4 @@ import { RelationshipService } from '../../../../core/data/relationship.service'
* The component for displaying metadata and relations of an item of the type Person * The component for displaying metadata and relations of an item of the type Person
*/ */
export class PersonComponent extends ItemComponent { export class PersonComponent extends ItemComponent {
/**
* The publications related to this person
*/
publications$: Observable<Item[]>;
/**
* The projects related to this person
*/
projects$: Observable<Item[]>;
/**
* The organisation units related to this person
*/
orgUnits$: Observable<Item[]>;
/**
* The applied fixed filter
*/
fixedFilter$: Observable<string>;
/**
* The query used for applying the fixed filter
*/
fixedFilterQuery: string;
constructor(
@Inject(ITEM) public item: Item,
protected relationshipService: RelationshipService
) {
super(item, relationshipService);
}
ngOnInit(): void {
super.ngOnInit();
this.publications$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isPublicationOfAuthor');
this.projects$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isProjectOfPerson');
this.orgUnits$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isOrgUnitOfPerson');
this.fixedFilterQuery = getQueryByRelations('isAuthorOfPublication', this.item.id);
this.fixedFilter$ = observableOf('publication');
}
} }

View File

@@ -11,8 +11,10 @@
<!--[label]="'project.page.status'">--> <!--[label]="'project.page.status'">-->
<!--</ds-generic-item-page-field>--> <!--</ds-generic-item-page-field>-->
<ds-metadata-representation-list <ds-metadata-representation-list
[label]="'project.page.contributor' | translate" [parentItem]="item"
[representations]="contributors$ | async"> [itemType]="'OrgUnit'"
[metadataField]="'project.contributor.other'"
[label]="'project.page.contributor' | translate">
</ds-metadata-representation-list> </ds-metadata-representation-list>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="item"
[fields]="['project.identifier.funder']" [fields]="['project.identifier.funder']"
@@ -29,15 +31,18 @@
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<ds-related-items <ds-related-items
[items]="people$ | async" [parentItem]="item"
[relationType]="'isPersonOfProject'"
[label]="'relationships.isPersonOf' | translate"> [label]="'relationships.isPersonOf' | translate">
</ds-related-items> </ds-related-items>
<ds-related-items <ds-related-items
[items]="publications$ | async" [parentItem]="item"
[relationType]="'isPublicationOfProject'"
[label]="'relationships.isPublicationOf' | translate"> [label]="'relationships.isPublicationOf' | translate">
</ds-related-items> </ds-related-items>
<ds-related-items <ds-related-items
[items]="orgUnits$ | async" [parentItem]="item"
[relationType]="'isOrgUnitOfProject'"
[label]="'relationships.isOrgUnitOf' | translate"> [label]="'relationships.isOrgUnitOf' | translate">
</ds-related-items> </ds-related-items>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="item"

View File

@@ -1,7 +1,4 @@
import { Component, OnInit } from '@angular/core'; import { Component } from '@angular/core';
import { Observable } from 'rxjs';
import { Item } from '../../../../core/shared/item.model';
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component';
@@ -14,33 +11,5 @@ import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/i
/** /**
* The component for displaying metadata and relations of an item of the type Project * The component for displaying metadata and relations of an item of the type Project
*/ */
export class ProjectComponent extends ItemComponent implements OnInit { export class ProjectComponent extends ItemComponent {
/**
* The contributors related to this project
*/
contributors$: Observable<MetadataRepresentation[]>;
/**
* The people related to this project
*/
people$: Observable<Item[]>;
/**
* The publications related to this project
*/
publications$: Observable<Item[]>;
/**
* The organisation units related to this project
*/
orgUnits$: Observable<Item[]>;
ngOnInit(): void {
super.ngOnInit();
this.contributors$ = this.buildRepresentations('OrgUnit', 'project.contributor.other');
this.people$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isPersonOfProject');
this.publications$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isPublicationOfProject');
this.orgUnits$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isOrgUnitOfProject');
}
} }

View File

@@ -18,13 +18,14 @@
<div class="row"> <div class="row">
<div class="col-12 col-md-6"> <div class="col-12 col-md-6">
<ds-metadata-representation-list <ds-metadata-representation-list
[label]="'relationships.isAuthorOf' | translate" [parentItem]="item"
[representations]="authors$ | async"> [itemType]="'Person'"
[metadataField]="'dc.contributor.author'"
[label]="'relationships.isAuthorOf' | translate">
</ds-metadata-representation-list> </ds-metadata-representation-list>
<ds-item-page-file-section [item]="item"></ds-item-page-file-section> <ds-item-page-file-section [item]="item"></ds-item-page-file-section>
<ds-item-page-date-field [item]="item"></ds-item-page-date-field> <ds-item-page-date-field [item]="item"></ds-item-page-date-field>
<ds-item-page-author-field *ngIf="!(authors$ | async)" <ds-item-page-author-field [item]="item"></ds-item-page-author-field>
[item]="item"></ds-item-page-author-field>
<ds-generic-item-page-field [item]="item" <ds-generic-item-page-field [item]="item"
[fields]="['journal.title']" [fields]="['journal.title']"
[label]="'publication.page.journal-title'"> [label]="'publication.page.journal-title'">
@@ -57,26 +58,29 @@
</div> </div>
</div> </div>
</div> </div>
<div class="relationships-item-page" *ngIf="(projects$ | async) || (orgUnits$ | async) || (journalIssues$ | async)"> <div class="relationships-item-page">
<div class="container"> <div class="container">
<div class="row"> <div class="row">
<div class="col-12 col-md-4" *ngIf="projects$ | async"> <div class="col-12 col-md-4">
<ds-related-items <ds-related-items
[items]="projects$ | async" [parentItem]="item"
[label]="'relationships.isProjectOf' | translate"> [relationType]="'isProjectOfPublication'"
</ds-related-items> [label]="'relationships.isProjectOf' | translate">
</ds-related-items>
</div> </div>
<div class="col-12 col-md-4" *ngIf="orgUnits$ | async"> <div class="col-12 col-md-4">
<ds-related-items <ds-related-items
[items]="orgUnits$ | async" [parentItem]="item"
[label]="'relationships.isOrgUnitOf' | translate"> [relationType]="'isOrgUnitOfPublication'"
</ds-related-items> [label]="'relationships.isOrgUnitOf' | translate">
</ds-related-items>
</div> </div>
<div class="col-12 col-md-4" *ngIf="journalIssues$ | async"> <div class="col-12 col-md-4">
<ds-related-items <ds-related-items
[items]="journalIssues$ | async" [parentItem]="item"
[label]="'relationships.isJournalIssueOf' | translate"> [relationType]="'isJournalIssueOfPublication'"
</ds-related-items> [label]="'relationships.isJournalIssueOf' | translate">
</ds-related-items>
</div> </div>
</div> </div>
</div> </div>