diff --git a/resources/i18n/en.json b/resources/i18n/en.json index b9a2478e71..e4d09738a8 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -232,10 +232,10 @@ "item.edit.relationships.edit.buttons.remove": "Remove", "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.title": "Changed discarded", + "item.edit.relationships.notifications.discarded.title": "Changes discarded", "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.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.title": "Relationships saved", "item.edit.relationships.reinstate-button": "Undo", @@ -294,6 +294,8 @@ "item.page.link.full": "Full item page", "item.page.link.simple": "Simple item page", "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.uri": "URI", "item.select.confirm": "Confirm selected", diff --git a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts index 93217ec8f7..be657d71dc 100644 --- a/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts +++ b/src/app/+item-page/edit-item-page/item-metadata/item-metadata.component.ts @@ -17,8 +17,8 @@ import { TranslateService } from '@ngx-translate/core'; import { RegistryService } from '../../../core/registry/registry.service'; import { MetadatumViewModel } from '../../../core/shared/metadata.models'; 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 { MetadataField } from '../../../core/metadata/metadata-field.model'; @Component({ selector: 'ds-item-metadata', diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts index 6016f54445..744bd50a1b 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.spec.ts @@ -36,7 +36,6 @@ let relationshipType; describe('EditRelationshipListComponent', () => { beforeEach(async(() => { relationshipType = Object.assign(new RelationshipType(), { - type: ResourceType.RelationshipType, id: '1', uuid: '1', leftLabel: 'isAuthorOfPublication', @@ -98,7 +97,7 @@ describe('EditRelationshipListComponent', () => { relationshipService = jasmine.createSpyObj('relationshipService', { - getRelatedItemsByLabel: observableOf([author1, author2]), + getRelatedItemsByLabel: observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), [author1, author2]))), } ); diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts index 765d6484d4..3a145c99e0 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship-list/edit-relationship-list.component.ts @@ -4,8 +4,10 @@ import { Observable } from 'rxjs/internal/Observable'; import { FieldUpdate, FieldUpdates } from '../../../../core/data/object-updates/object-updates.reducer'; import { RelationshipService } from '../../../../core/data/relationship.service'; import { Item } from '../../../../core/shared/item.model'; -import { switchMap } from 'rxjs/operators'; +import { map, switchMap } from 'rxjs/operators'; import { hasValue } from '../../../../shared/empty.util'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { PaginatedList } from '../../../../core/data/paginated-list'; @Component({ 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 * @param label The relationship type's label */ - public getRelatedItemsByLabel(label: string): Observable { + public getRelatedItemsByLabel(label: string): Observable>> { return this.relationshipService.getRelatedItemsByLabel(this.item, label); } @@ -73,7 +75,7 @@ export class EditRelationshipListComponent implements OnInit, OnChanges { */ public getUpdatesByLabel(label: string): Observable { return this.getRelatedItemsByLabel(label).pipe( - switchMap((items: Item[]) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, items)) + switchMap((itemsRD) => this.objectUpdatesService.getFieldUpdatesExclusive(this.url, itemsRD.payload.page)) ) } diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts index e60a7f3a35..6830fa2097 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.spec.ts @@ -32,7 +32,6 @@ let el; describe('EditRelationshipComponent', () => { beforeEach(async(() => { relationshipType = Object.assign(new RelationshipType(), { - type: ResourceType.RelationshipType, id: '1', uuid: '1', leftLabel: 'isAuthorOfPublication', diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts index 1d3d59201b..b1a4e11371 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts @@ -66,7 +66,6 @@ describe('ItemRelationshipsComponent', () => { const date = new Date(); relationshipType = Object.assign(new RelationshipType(), { - type: ResourceType.RelationshipType, id: '1', uuid: '1', leftLabel: 'isAuthorOfPublication', @@ -227,10 +226,8 @@ describe('ItemRelationshipsComponent', () => { 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(objectCache.remove).toHaveBeenCalledWith(item.self); - expect(requestService.removeByHrefSubstring).toHaveBeenCalledWith(item.self); }); }); }); diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index d4c625dee0..42ebc5563e 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -140,10 +140,6 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl this.notificationsService.error(this.getNotificationTitle('failed'), response.errorMessage); }); 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')); } } diff --git a/src/app/+item-page/simple/item-types/publication/publication.component.html b/src/app/+item-page/simple/item-types/publication/publication.component.html index abf5225c27..1edc00f7eb 100644 --- a/src/app/+item-page/simple/item-types/publication/publication.component.html +++ b/src/app/+item-page/simple/item-types/publication/publication.component.html @@ -28,19 +28,24 @@
+ [parentItem]="item" + [itemType]="'Person'" + [metadataField]="'dc.contributor.author'" + [label]="'relationships.isAuthorOf' | translate"> diff --git a/src/app/+item-page/simple/item-types/publication/publication.component.ts b/src/app/+item-page/simple/item-types/publication/publication.component.ts index 11d1f7a836..f0986624da 100644 --- a/src/app/+item-page/simple/item-types/publication/publication.component.ts +++ b/src/app/+item-page/simple/item-types/publication/publication.component.ts @@ -1,12 +1,9 @@ -import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; -import { Item } from '../../../../core/shared/item.model'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; import { DEFAULT_ITEM_TYPE, ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator'; import { ItemComponent } from '../shared/item.component'; -import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model'; @rendersItemType('Publication', ItemViewMode.Detail) @rendersItemType(DEFAULT_ITEM_TYPE, ItemViewMode.Detail) @@ -16,36 +13,5 @@ import { MetadataRepresentation } from '../../../../core/shared/metadata-represe templateUrl: './publication.component.html', changeDetection: ChangeDetectionStrategy.OnPush, }) -export class PublicationComponent extends ItemComponent implements OnInit { - /** - * The authors related to this publication - */ - authors$: Observable; - - /** - * The projects related to this publication - */ - projects$: Observable; - - /** - * The organisation units related to this publication - */ - orgUnits$: Observable; - - /** - * The journal issues related to this publication - */ - journalIssues$: Observable; - - 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'); - } +export class PublicationComponent extends ItemComponent { } diff --git a/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts b/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts index 0f054f89ec..ab06f0cad5 100644 --- a/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts +++ b/src/app/+item-page/simple/item-types/shared/item-relationships-utils.ts @@ -1,23 +1,12 @@ -import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; -import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model'; -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 { getSucceededRemoteData } from '../../../../core/shared/operators'; +import { hasValue } from '../../../../shared/empty.util'; import { Observable } from 'rxjs/internal/Observable'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.model'; -import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; -import { distinctUntilChanged, filter, flatMap, map, startWith, switchMap } from 'rxjs/operators'; -import { - combineLatest as observableCombineLatest, - of as observableOf, - zip as observableZip -} from 'rxjs'; -import { ItemDataService } from '../../../../core/data/item-data.service'; +import { distinctUntilChanged, flatMap, map, switchMap } from 'rxjs/operators'; +import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs'; 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 { RemoteData } from '../../../../core/data/remote-data'; /** * Operator for comparing arrays using a mapping function @@ -28,7 +17,7 @@ import { PaginatedList } from '../../../../core/data/paginated-list'; */ export const compareArraysUsing = (mapFn: (t: T) => any) => (a: T[], b: T[]): boolean => { - if (!Array.isArray(a) || !Array.isArray(b)) { + if (!Array.isArray(a) || ! Array.isArray(b)) { 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 - * @param parentId 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 - * @param ids The ItemDataService to use for fetching Items from the Rest API + * Operator for turning a paginated list of relationships into a paginated list of the relevant items + * The result is wrapped in the original RemoteData and PaginatedList + * @param {string} thisId The item's id of which the relations belong to + * @returns {(source: Observable) => Observable} */ -export const relationsToRepresentations = (parentId: string, itemType: string, metadata: MetadataValue[], ids: ItemDataService) => - (source: Observable): Observable => +export const paginatedRelationsToItems = (thisId: string) => + (source: Observable>>): Observable>> => 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( - map(([leftItem, rightItem]) => { - if (leftItem.payload.id === parentId) { - return rightItem.payload; - } else if (rightItem.payload.id === parentId) { - return leftItem.payload; - } - }), - map((item: Item) => Object.assign(new ItemMetadataRepresentation(), item)) - ); + getSucceededRemoteData(), + switchMap((relationshipsRD: RemoteData>) => { + return observableZip( + ...relationshipsRD.payload.page.map((rel: Relationship) => observableCombineLatest(rel.leftItem, rel.rightItem)) + ).pipe( + map((arr) => + arr + .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; } - } 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 } )}) + ) ) - ) + }) ); \ No newline at end of file diff --git a/src/app/+item-page/simple/item-types/shared/item.component.spec.ts b/src/app/+item-page/simple/item-types/shared/item.component.spec.ts index 940b195400..a615bdd94b 100644 --- a/src/app/+item-page/simple/item-types/shared/item.component.spec.ts +++ b/src/app/+item-page/simple/item-types/shared/item.component.spec.ts @@ -12,18 +12,8 @@ import { TruncatePipe } from '../../../../shared/utils/truncate.pipe'; import { isNotEmpty } from '../../../../shared/empty.util'; import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model'; import { PaginatedList } from '../../../../core/data/paginated-list'; -import { RemoteData } from '../../../../core/data/remote-data'; import { Relationship } from '../../../../core/shared/item-relationships/relationship.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 { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; import { RelationshipService } from '../../../../core/data/relationship.service'; @@ -311,116 +301,4 @@ describe('ItemComponent', () => { }); }); - describe('when calling buildRepresentations', () => { - let comp: ItemComponent; - let fixture: ComponentFixture; - - 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; - - 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); - }); - }); - }) - }); diff --git a/src/app/+item-page/simple/item-types/shared/item.component.ts b/src/app/+item-page/simple/item-types/shared/item.component.ts index 5184b30359..9814191475 100644 --- a/src/app/+item-page/simple/item-types/shared/item.component.ts +++ b/src/app/+item-page/simple/item-types/shared/item.component.ts @@ -1,59 +1,6 @@ -import { Component, Inject, OnInit } 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 { Component, Inject } from '@angular/core'; 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 { 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): Observable => - 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({ 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 */ -export class ItemComponent implements OnInit { +export class ItemComponent { + constructor( - @Inject(ITEM) public item: Item, - protected relationshipService: RelationshipService + @Inject(ITEM) public item: Item ) {} - - 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 { - const metadata = this.item.findMetadataSortedByPlace(metadataField); - const relsCurrentPage$ = this.item.relationships.pipe( - getSucceededRemoteData(), - getRemoteDataPayload(), - map((pl: PaginatedList) => pl.page), - distinctUntilChanged(compareArraysUsingIds()) - ); - - return relsCurrentPage$.pipe( - relationsToRepresentations(this.item.id, itemType, metadata) - ); - } - } diff --git a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.html b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.html index 48eabf8451..e9d2364ca6 100644 --- a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.html +++ b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.html @@ -1,5 +1,11 @@ - - + + + diff --git a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts index f02625e8c7..120e846523 100644 --- a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts +++ b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.spec.ts @@ -2,23 +2,72 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { By } from '@angular/platform-browser'; import { MetadataRepresentationListComponent } from './metadata-representation-list.component'; -import { MetadatumRepresentation } from '../../../core/shared/metadata-representation/metadatum/metadatum-representation.model'; -import { ItemMetadataRepresentation } from '../../../core/shared/metadata-representation/item/item-metadata-representation.model'; +import { RelationshipService } from '../../../core/data/relationship.service'; +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 metadataRepresentation1 = new MetadatumRepresentation(itemType); -const metadataRepresentation2 = new ItemMetadataRepresentation(); -const representations = [metadataRepresentation1, metadataRepresentation2]; +const itemType = 'Person'; +const metadataField = 'dc.contributor.author'; +const parentItem: Item = Object.assign(new Item(), { + 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', () => { let comp: MetadataRepresentationListComponent; let fixture: ComponentFixture; + relationshipService = jasmine.createSpyObj('relationshipService', + { + findById: createSuccessfulRemoteDataObject$(relation) + } + ); + beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [], + imports: [TranslateModule.forRoot()], declarations: [MetadataRepresentationListComponent], - providers: [], + providers: [ + { provide: RelationshipService, useValue: relationshipService } + ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(MetadataRepresentationListComponent, { set: {changeDetection: ChangeDetectionStrategy.Default} @@ -28,13 +77,45 @@ describe('MetadataRepresentationListComponent', () => { beforeEach(async(() => { fixture = TestBed.createComponent(MetadataRepresentationListComponent); comp = fixture.componentInstance; - comp.representations = representations; + comp.parentItem = parentItem; + comp.itemType = itemType; + comp.metadataField = metadataField; 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')); - 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); + }); }); }); diff --git a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts index f0dc222bf1..4b5553e404 100644 --- a/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts +++ b/src/app/+item-page/simple/metadata-representation-list/metadata-representation-list.component.ts @@ -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 { 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({ selector: 'ds-metadata-representation-list', @@ -8,22 +19,123 @@ import { ItemViewMode } from '../../../shared/items/item-type-decorator'; }) /** * 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 */ @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; + /** * The view-mode we're currently on * @type {ElementViewMode} */ 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 { + 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) => + 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(); + } } diff --git a/src/app/+item-page/simple/related-items/related-items-component.ts b/src/app/+item-page/simple/related-items/related-items-component.ts index b735696b15..05ae5e4d49 100644 --- a/src/app/+item-page/simple/related-items/related-items-component.ts +++ b/src/app/+item-page/simple/related-items/related-items-component.ts @@ -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 { 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({ 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 - * 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) */ @Input() label: string; + /** + * The list of related items + */ + items$: Observable>>; + + /** + * Search options for displaying all elements in a list + */ + allOptions = Object.assign(new FindAllOptions(), { elementsPerPage: 9999 }); + /** * The view-mode we're currently on * @type {ElementViewMode} */ 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; + } } diff --git a/src/app/+item-page/simple/related-items/related-items.component.html b/src/app/+item-page/simple/related-items/related-items.component.html index 4b284ad63c..a4baee9825 100644 --- a/src/app/+item-page/simple/related-items/related-items.component.html +++ b/src/app/+item-page/simple/related-items/related-items.component.html @@ -1,5 +1,11 @@ - - + + + diff --git a/src/app/+item-page/simple/related-items/related-items.component.spec.ts b/src/app/+item-page/simple/related-items/related-items.component.spec.ts index 1896f46015..6637091f02 100644 --- a/src/app/+item-page/simple/related-items/related-items.component.spec.ts +++ b/src/app/+item-page/simple/related-items/related-items.component.spec.ts @@ -2,14 +2,19 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { RelatedItemsComponent } from './related-items-component'; import { Item } from '../../../core/shared/item.model'; -import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; import { PageInfo } from '../../../core/shared/page-info.model'; import { By } from '@angular/platform-browser'; import { createRelationshipsObservable } from '../item-types/shared/item.component.spec'; -import { of as observableOf } from 'rxjs'; 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(), { bitstreams: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])), metadata: [], @@ -21,16 +26,26 @@ const mockItem2: Item = Object.assign(new Item(), { relationships: createRelationshipsObservable() }); const mockItems = [mockItem1, mockItem2]; +const relationType = 'isItemOfItem'; +let relationshipService: RelationshipService; describe('RelatedItemsComponent', () => { let comp: RelatedItemsComponent; let fixture: ComponentFixture; beforeEach(async(() => { + relationshipService = jasmine.createSpyObj('relationshipService', + { + getRelatedItemsByLabel: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), mockItems)), + } + ); + TestBed.configureTestingModule({ - imports: [], + imports: [TranslateModule.forRoot()], declarations: [RelatedItemsComponent], - providers: [], + providers: [ + { provide: RelationshipService, useValue: relationshipService } + ], schemas: [NO_ERRORS_SCHEMA] }).overrideComponent(RelatedItemsComponent, { set: {changeDetection: ChangeDetectionStrategy.Default} @@ -40,7 +55,8 @@ describe('RelatedItemsComponent', () => { beforeEach(async(() => { fixture = TestBed.createComponent(RelatedItemsComponent); comp = fixture.componentInstance; - comp.items = mockItems; + comp.parentItem = parentItem; + comp.relationType = relationType; fixture.detectChanges(); })); @@ -49,4 +65,32 @@ describe('RelatedItemsComponent', () => { 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); + }); + }); + }); diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index fa19665bab..cc77f65c55 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -112,7 +112,7 @@ export class SearchPageComponent implements OnInit { ngOnInit(): void { this.searchOptions$ = this.getSearchOptions(); 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) => { this.resultsRD$.next(results); }); diff --git a/src/app/core/data/relationship.service.spec.ts b/src/app/core/data/relationship.service.spec.ts index ce2b169eef..bbd950ef5c 100644 --- a/src/app/core/data/relationship.service.spec.ts +++ b/src/app/core/data/relationship.service.spec.ts @@ -13,6 +13,8 @@ import { Item } from '../shared/item.model'; import { PaginatedList } from './paginated-list'; import { PageInfo } from '../shared/page-info.model'; import { DeleteRequest } from './request.models'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { Observable } from 'rxjs/internal/Observable'; describe('RelationshipService', () => { let service: RelationshipService; @@ -22,33 +24,33 @@ describe('RelationshipService', () => { const relationshipsEndpointURL = `${restEndpointURL}/relationships`; const halService: any = new HALEndpointServiceStub(restEndpointURL); const rdbService = getMockRemoteDataBuildService(); + const objectCache = Object.assign({ + /* tslint:disable:no-empty */ + remove: () => {} + /* tslint:enable:no-empty */ + }) as ObjectCacheService; const relationshipType = Object.assign(new RelationshipType(), { - type: ResourceType.RelationshipType, id: '1', uuid: '1', leftLabel: 'isAuthorOfPublication', rightLabel: 'isPublicationOfAuthor' }); - const relationships = [ - Object.assign(new Relationship(), { - self: relationshipsEndpointURL + '/2', - id: '2', - uuid: '2', - leftId: 'author1', - rightId: 'publication', - relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) - }), - Object.assign(new Relationship(), { - self: relationshipsEndpointURL + '/3', - id: '3', - uuid: '3', - leftId: 'author2', - rightId: 'publication', - relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) - }) - ]; + const relationship1 = Object.assign(new Relationship(), { + self: relationshipsEndpointURL + '/2', + id: '2', + uuid: '2', + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }); + const relationship2 = Object.assign(new Relationship(), { + self: relationshipsEndpointURL + '/3', + id: '3', + uuid: '3', + relationshipType: observableOf(new RemoteData(false, false, true, undefined, relationshipType)) + }); + + const relationships = [ relationship1, relationship2 ]; const item = Object.assign(new Item(), { self: 'fake-item-url/publication', @@ -65,6 +67,10 @@ describe('RelationshipService', () => { id: 'author2', uuid: 'author2' }); + relationship1.leftItem = getRemotedataObservable(relatedItem1); + relationship1.rightItem = getRemotedataObservable(item); + relationship2.leftItem = getRemotedataObservable(relatedItem2); + relationship2.rightItem = getRemotedataObservable(item); const relatedItems = [relatedItem1, relatedItem2]; const itemService = jasmine.createSpyObj('itemService', { @@ -73,10 +79,16 @@ describe('RelationshipService', () => { function initTestService() { return new RelationshipService( + itemService, requestService, - halService, rdbService, - itemService + null, + null, + halService, + objectCache, + null, + null, + null ); } @@ -93,13 +105,22 @@ describe('RelationshipService', () => { describe('deleteRelationship', () => { beforeEach(() => { + spyOn(service, 'findById').and.returnValue(getRemotedataObservable(relationship1)); + spyOn(objectCache, 'remove'); service.deleteRelationship(relationships[0].uuid).subscribe(); }); 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); }); + + 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', () => { @@ -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> { + return observableOf(new RemoteData(false, false, true, undefined, obj)); +} diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index a8e8874dfe..0766de5757 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -2,8 +2,8 @@ import { Injectable } from '@angular/core'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { hasNoValue, hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/empty.util'; -import { distinctUntilChanged, filter, map, mergeMap, skip, startWith, switchMap, take, tap } from 'rxjs/operators'; +import { hasValue, hasValueOperator, isNotEmptyOperator } from '../../shared/empty.util'; +import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators'; import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators'; import { DeleteRequest, FindAllOptions, PostRequest, RestRequest } from './request.models'; import { Observable } from 'rxjs/internal/Observable'; @@ -15,16 +15,17 @@ import { RemoteData } from './remote-data'; import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs'; import { PaginatedList } from './paginated-list'; import { ItemDataService } from './item-data.service'; -import { compareArraysUsingIds, 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 { compareArraysUsingIds, paginatedRelationsToItems, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { DataService } from './data.service'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { Store } from '@ngrx/store'; import { CoreState } from '../core.reducers'; 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 { SearchParam } from '../cache/models/search-param.model'; +import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; /** * The service handling all relationship requests @@ -61,6 +62,15 @@ export class RelationshipService extends DataService { ); } + /** + * Find a relationship by its UUID + * @param uuid + */ + findById(uuid: string): Observable> { + const href$ = this.getRelationshipEndpoint(uuid); + return this.rdbService.buildSingle(href$); + } + /** * Send a delete request for a relationship by ID * @param id @@ -183,41 +193,31 @@ export class RelationshipService extends DataService { * and return the items as an array * @param item * @param label + * @param options */ - getRelatedItemsByLabel(item: Item, label: string): Observable { - return this.getItemRelationshipsByLabel(item, label).pipe(relationsToItems(item.uuid)); + getRelatedItemsByLabel(item: Item, label: string, options?: FindAllOptions): Observable>> { + return this.getItemRelationshipsByLabel(item, label, options).pipe(paginatedRelationsToItems(item.uuid)); } - /** * Resolve a given item's relationships into related items, filtered by a relationship label * and return the items as an array * @param item * @param label + * @param options */ - getItemRelationshipsByLabel(item: Item, label: string): Observable { - return this.getItemRelationshipsArray(item).pipe( - switchMap((relationships: Relationship[]) => { - return observableCombineLatest( - ...relationships.map((relationship: Relationship) => { - return relationship.relationshipType.pipe( - getSucceededRemoteData(), - getRemoteDataPayload(), - map((relationshipType: RelationshipType) => { - if (label === relationshipType.rightLabel || label === relationshipType.leftLabel) { - return relationship; - } else { - return undefined; - } - }), - ) - }) - ) - }), - map((relationships: Relationship[]) => - relationships.filter((relationship: Relationship) => hasValue(relationship)) - ), - ); + getItemRelationshipsByLabel(item: Item, label: string, options?: FindAllOptions): Observable>> { + let findAllOptions = new FindAllOptions(); + if (options) { + findAllOptions = Object.assign(new FindAllOptions(), options); + } + const searchParams = [ new SearchParam('label', label), new SearchParam('dso', item.id) ]; + if (findAllOptions.searchParams) { + findAllOptions.searchParams = [...findAllOptions.searchParams, ...searchParams]; + } else { + findAllOptions.searchParams = searchParams; + } + return this.searchBy('byLabel', findAllOptions); } diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 39e6baf46e..875b3435ed 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -90,7 +90,7 @@ export const toDSpaceObjectListRD = () => source.pipe( filter((rd: RemoteData>>) => rd.hasSucceeded), map((rd: RemoteData>>) => { - const dsoPage: T[] = rd.payload.page.map((searchResult: SearchResult) => searchResult.indexableObject); + const dsoPage: T[] = rd.payload.page.filter((result) => hasValue(result)).map((searchResult: SearchResult) => searchResult.indexableObject); const payload = Object.assign(rd.payload, { page: dsoPage }) as PaginatedList; return Object.assign(rd, { payload: payload }); }) diff --git a/src/app/core/shared/search/search.service.ts b/src/app/core/shared/search/search.service.ts index 8058e1f6ca..e37c2de48b 100644 --- a/src/app/core/shared/search/search.service.ts +++ b/src/app/core/shared/search/search.service.ts @@ -1,13 +1,8 @@ import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs'; import { Injectable, OnDestroy } from '@angular/core'; import { NavigationExtras, Router } from '@angular/router'; -import { first, map, switchMap } from 'rxjs/operators'; -import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; -import { - FacetConfigSuccessResponse, - FacetValueSuccessResponse, - SearchSuccessResponse -} from '../../cache/response.models'; +import { first, map, switchMap, tap } from 'rxjs/operators'; +import { FacetConfigSuccessResponse, FacetValueSuccessResponse, SearchSuccessResponse } from '../../cache/response.models'; import { PaginatedList } from '../../data/paginated-list'; import { ResponseParsingService } from '../../data/parsing.service'; import { RemoteData } from '../../data/remote-data'; @@ -16,12 +11,6 @@ import { RequestService } from '../../data/request.service'; import { DSpaceObject } from '../dspace-object.model'; import { GenericConstructor } from '../generic-constructor'; import { HALEndpointService } from '../hal-endpoint.service'; -import { - configureRequest, - filterSuccessfulResponses, - getResponseFromEntry, - getSucceededRemoteData -} from '../operators'; import { URLCombiner } from '../../url-combiner/url-combiner'; import { hasValue, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; 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 { DSpaceObjectDataService } from '../../data/dspace-object-data.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 @@ -103,11 +94,18 @@ export class SearchService implements OnDestroy { * @returns {Observable>>>} Emits a paginated list with all search results found */ search(searchOptions?: PaginatedSearchOptions): Observable>>> { - const requestObs = this.halService.getEndpoint(this.searchLinkPath).pipe( + const hrefObs = this.halService.getEndpoint(this.searchLinkPath).pipe( map((url: string) => { 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 getResponseParserFn: () => GenericConstructor = () => { @@ -136,10 +134,11 @@ export class SearchService implements OnDestroy { map((sqr: SearchQueryResponse) => { return sqr.objects .filter((nsr: NormalizedSearchResult) => isNotUndefined(nsr.indexableObject)) - .map((nsr: NormalizedSearchResult) => { - return this.rdb.buildSingle(nsr.indexableObject); - }) + .map((nsr: NormalizedSearchResult) => new GetRequest(this.requestService.generateRequestId(), 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>>) => this.rdb.aggregate(input)), ); @@ -168,11 +167,20 @@ export class SearchService implements OnDestroy { const payloadObs = observableCombineLatest(tDomainListObs, pageInfoObs).pipe( 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); + } + }) + ); } /** diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html index eebd3e03c8..8db50e78c4 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html @@ -29,12 +29,14 @@
; - - /** - * The publications related to this journal issue - */ - publications$: Observable; - - ngOnInit(): void { - super.ngOnInit(); - this.volumes$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isJournalVolumeOfIssue'); - this.publications$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isPublicationOfJournalIssue'); - } } diff --git a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html index 83626c7ae7..150037eccb 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html @@ -17,11 +17,13 @@
; - - /** - * The journal issues related to this journal volume - */ - issues$: Observable; - - ngOnInit(): void { - super.ngOnInit(); - this.journals$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isJournalOfVolume'); - this.issues$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isIssueOfJournalVolume'); - } } diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html index a82d3c5df6..d22933a657 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.html @@ -21,7 +21,8 @@
; - - ngOnInit(): void { - super.ngOnInit(); - this.volumes$ = this.relationshipService.getRelatedItemsByLabel(this.item, 'isVolumeOfJournal'); - } } diff --git a/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.html b/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.html index 92ac3eac30..a3d2fedb10 100644 --- a/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.html +++ b/src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.html @@ -25,15 +25,18 @@
; - - /** - * The projects related to this organisation unit - */ - projects$: Observable; - - /** - * The publications related to this organisation unit - */ - publications$: Observable; - - 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'); - } +export class OrgunitComponent extends ItemComponent { } diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.html b/src/app/entity-groups/research-entities/item-pages/person/person.component.html index 04d7b9e062..3f0ca90368 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.html +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.html @@ -25,11 +25,13 @@
; - - /** - * The projects related to this person - */ - projects$: Observable; - - /** - * The organisation units related to this person - */ - orgUnits$: Observable; - - /** - * The applied fixed filter - */ - fixedFilter$: Observable; - - /** - * 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'); - } } diff --git a/src/app/entity-groups/research-entities/item-pages/project/project.component.html b/src/app/entity-groups/research-entities/item-pages/project/project.component.html index 4e9a130b8c..9ea0676f23 100644 --- a/src/app/entity-groups/research-entities/item-pages/project/project.component.html +++ b/src/app/entity-groups/research-entities/item-pages/project/project.component.html @@ -11,8 +11,10 @@ + [parentItem]="item" + [itemType]="'OrgUnit'" + [metadataField]="'project.contributor.other'" + [label]="'project.page.contributor' | translate">
; - - /** - * The people related to this project - */ - people$: Observable; - - /** - * The publications related to this project - */ - publications$: Observable; - - /** - * The organisation units related to this project - */ - orgUnits$: Observable; - - 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'); - } +export class ProjectComponent extends ItemComponent { } diff --git a/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html b/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html index 6b6f484183..a6886cf1ac 100644 --- a/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html +++ b/themes/mantis/app/+item-page/simple/item-types/publication/publication.component.html @@ -18,13 +18,14 @@
+ [parentItem]="item" + [itemType]="'Person'" + [metadataField]="'dc.contributor.author'" + [label]="'relationships.isAuthorOf' | translate"> - + @@ -57,26 +58,29 @@
-
+
-
- - +
+ +
-
- - +
+ +
-
- - +
+ +