From ad4e8eeb8c5cab5aa657ad9a0c09ae9c00c67ea2 Mon Sep 17 00:00:00 2001 From: Art Lowel <art.lowel@atmire.com> Date: Wed, 11 Dec 2019 17:18:08 +0100 Subject: [PATCH] refactored items, bundles and bitstreams, test builders --- .../item-collection-mapper.component.ts | 12 +- .../collections/collections.component.html | 4 +- .../collections/collections.component.ts | 39 +++-- .../full-file-section.component.html | 6 +- .../full-file-section.component.ts | 44 ++++-- .../file-section/file-section.component.html | 4 +- .../file-section/file-section.component.ts | 13 +- .../publication/publication.component.html | 2 +- .../item-types/shared/item.component.ts | 14 ++ .../core/cache/builders/bitstream-builder.ts | 60 +++++++ .../core/cache/builders/build-decorators.ts | 5 +- .../builders/remote-data-build.service.ts | 85 ++++++---- .../models/normalized-bitstream.model.ts | 7 +- .../cache/models/normalized-bundle.model.ts | 4 +- .../models/normalized-collection.model.ts | 11 +- .../cache/models/normalized-item.model.ts | 8 +- .../data/base-response-parsing.service.ts | 38 ++--- src/app/core/data/bitstream-data.service.ts | 147 ++++++++++++++++++ .../data/bitstream-format-data.service.ts | 6 + src/app/core/data/bundle-data.service.ts | 53 +++++-- src/app/core/data/collection-data.service.ts | 6 + src/app/core/data/comcol-data.service.ts | 4 +- src/app/core/data/data.service.ts | 47 ++++-- src/app/core/data/item-data.service.ts | 5 +- src/app/core/data/relationship.service.ts | 4 +- src/app/core/data/resource-policy.service.ts | 2 +- src/app/core/metadata/metadata.service.ts | 46 +++--- src/app/core/shared/HALLink.model.ts | 5 + src/app/core/shared/bitstream.model.ts | 26 ++-- src/app/core/shared/bundle.model.ts | 33 +--- src/app/core/shared/collection.model.ts | 10 ++ src/app/core/shared/item.model.ts | 96 ++---------- src/app/core/shared/operators.ts | 82 ++++++++++ ...-search-result-grid-element.component.html | 4 +- ...-search-result-grid-element.component.html | 4 +- ...-search-result-grid-element.component.html | 4 +- .../journal-issue.component.html | 2 +- .../journal-volume.component.html | 2 +- .../item-pages/journal/journal.component.html | 2 +- ...-search-result-grid-element.component.html | 4 +- ...-search-result-grid-element.component.html | 4 +- ...-search-result-grid-element.component.html | 4 +- .../org-unit/org-unit.component.html | 2 +- .../item-pages/person/person.component.html | 2 +- .../item-pages/project/project.component.html | 2 +- .../comcol-page-logo.component.html | 2 +- .../item-detail-preview.component.ts | 34 +++- .../grid-thumbnail.component.ts | 4 +- ...-search-result-grid-element.component.html | 4 +- .../search-result-grid-element.component.ts | 23 ++- src/app/thumbnail/thumbnail.component.ts | 27 ++-- .../publication/publication.component.html | 2 +- .../journal-issue.component.html | 2 +- .../journal-volume.component.html | 2 +- .../item-pages/journal/journal.component.html | 2 +- .../org-unit/org-unit.component.html | 2 +- .../item-pages/person/person.component.html | 2 +- .../item-pages/project/project.component.html | 2 +- 58 files changed, 739 insertions(+), 333 deletions(-) create mode 100644 src/app/core/cache/builders/bitstream-builder.ts create mode 100644 src/app/core/data/bitstream-data.service.ts create mode 100644 src/app/core/shared/HALLink.model.ts diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 5494d5ab5f..b22949d7b5 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -1,12 +1,18 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; import { Collection } from '../../../core/shared/collection.model'; import { Item } from '../../../core/shared/item.model'; -import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators'; +import { + getFirstSucceededRemoteDataPayload, + getRemoteDataPayload, + getSucceededRemoteData, + toDSpaceObjectListRD +} from '../../../core/shared/operators'; import { ActivatedRoute, Router } from '@angular/router'; import { map, startWith, switchMap, take } from 'rxjs/operators'; import { ItemDataService } from '../../../core/data/item-data.service'; @@ -81,6 +87,7 @@ export class ItemCollectionMapperComponent implements OnInit { private searchService: SearchService, private notificationsService: NotificationsService, private itemDataService: ItemDataService, + private collectionDataService: CollectionDataService, private translateService: TranslateService) { } @@ -106,7 +113,8 @@ export class ItemCollectionMapperComponent implements OnInit { ); const owningCollectionRD$ = this.itemRD$.pipe( - switchMap((itemRD: RemoteData<Item>) => itemRD.payload.owningCollection) + getFirstSucceededRemoteDataPayload(), + switchMap((item: Item) => this.collectionDataService.findOwningCollectionFor(item)) ); const itemCollectionsAndOptions$ = observableCombineLatest( this.itemCollectionsRD$, diff --git a/src/app/+item-page/field-components/collections/collections.component.html b/src/app/+item-page/field-components/collections/collections.component.html index 6e5f9a350c..e0f963b5bc 100644 --- a/src/app/+item-page/field-components/collections/collections.component.html +++ b/src/app/+item-page/field-components/collections/collections.component.html @@ -1,6 +1,6 @@ -<ds-metadata-field-wrapper *ngIf="hasSucceeded() | async" [label]="label | translate"> +<ds-metadata-field-wrapper *ngIf="(this.collectionsRD$ | async)?.hasSucceeded" [label]="label | translate"> <div class="collections"> - <a *ngFor="let collection of (collections |Â async); let last=last;" [routerLink]="['/collections', collection.id]"> + <a *ngFor="let collection of (this.collectionsRD$ | async)?.payload?.page; let last=last;" [routerLink]="['/collections', collection.id]"> <span>{{collection?.name}}</span><span *ngIf="!last" [innerHTML]="separator"></span> </a> </div> diff --git a/src/app/+item-page/field-components/collections/collections.component.ts b/src/app/+item-page/field-components/collections/collections.component.ts index b33c5fd41b..0d50fcad83 100644 --- a/src/app/+item-page/field-components/collections/collections.component.ts +++ b/src/app/+item-page/field-components/collections/collections.component.ts @@ -1,12 +1,13 @@ - -import {map} from 'rxjs/operators'; import { Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; +import { map } from 'rxjs/operators'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { PaginatedList } from '../../../core/data/paginated-list'; +import { RemoteData } from '../../../core/data/remote-data'; import { Collection } from '../../../core/shared/collection.model'; import { Item } from '../../../core/shared/item.model'; -import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; -import { RemoteData } from '../../../core/data/remote-data'; +import { PageInfo } from '../../../core/shared/page-info.model'; /** * This component renders the parent collections section of the item @@ -25,9 +26,9 @@ export class CollectionsComponent implements OnInit { separator = '<br/>'; - collections: Observable<Collection[]>; + collectionsRD$: Observable<RemoteData<PaginatedList<Collection>>>; - constructor(private rdbs: RemoteDataBuildService) { + constructor(private cds: CollectionDataService) { } @@ -37,11 +38,25 @@ export class CollectionsComponent implements OnInit { // TODO: this should use parents, but the collections // for an Item aren't returned by the REST API yet, // only the owning collection - this.collections = this.item.owner.pipe(map((rd: RemoteData<Collection>) => [rd.payload])); + this.collectionsRD$ = this.cds.findOwningCollectionFor(this.item).pipe( + map((rd: RemoteData<Collection>) => { + if (rd.hasSucceeded) { + return new RemoteData( + false, + false, + true, + undefined, + new PaginatedList({ + elementsPerPage: 10, + totalPages: 1, + currentPage: 1, + totalElements: 1 + } as PageInfo, [rd.payload]) + ); + } else { + return rd as any; + } + }) + ); } - - hasSucceeded() { - return this.item.owner.pipe(map((rd: RemoteData<Collection>) => rd.hasSucceeded)); - } - } diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html index a68993cd16..b8ab9bdb41 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.html +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.html @@ -1,7 +1,7 @@ <ds-metadata-field-wrapper [label]="label | translate"> - <div class="file-section row" *ngFor="let file of (bitstreamsObs | async); let last=last;"> + <div class="file-section row" *ngFor="let file of (bitstreams$ | async); let last=last;"> <div class="col-3"> - <ds-thumbnail [thumbnail]="thumbnails.get(file.id) | async"></ds-thumbnail> + <ds-thumbnail [thumbnail]="(file.thumbnail | async)?.payload"></ds-thumbnail> </div> <div class="col-7"> <dl class="row"> @@ -21,7 +21,7 @@ </dl> </div> <div class="col-2"> - <a [href]="file.content" [download]="file.name"> + <a [href]="file._links.content.href" [download]="file.name"> {{"item.page.filesection.download" | translate}} </a> </div> diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts index 23d9ef05d0..0ce0c904a9 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts @@ -1,10 +1,13 @@ +import { Component, Injector, Input, OnInit } from '@angular/core'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { Component, Input, OnInit } from '@angular/core'; +import { map, startWith } from 'rxjs/operators'; +import { getBitstreamBuilder } from '../../../../core/cache/builders/bitstream-builder'; +import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Item } from '../../../../core/shared/item.model'; +import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators'; import { FileSectionComponent } from '../../../simple/field-components/file-section/file-section.component'; -import { map } from 'rxjs/operators'; /** * This component renders the file section of the item @@ -22,27 +25,42 @@ export class FullFileSectionComponent extends FileSectionComponent implements On label: string; - bitstreamsObs: Observable<Bitstream[]>; + bitstreams$: Observable<Bitstream[]>; - thumbnails: Map<string, Observable<Bitstream>> = new Map(); + constructor( + bitstreamDataService: BitstreamDataService, + private parentInjector: Injector + ) { + super(bitstreamDataService); + } ngOnInit(): void { super.ngOnInit(); } initialize(): void { - const originals = this.item.getFiles(); - const licenses = this.item.getBitstreamsByBundleName('LICENSE'); - this.bitstreamsObs = observableCombineLatest(originals, licenses).pipe(map(([o, l]) => [...o, ...l])); - this.bitstreamsObs.subscribe( - (files) => - files.forEach( + // TODO pagination + const originals$ = this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL', { elementsPerPage: Number.MAX_SAFE_INTEGER }).pipe( + getFirstSucceededRemoteListPayload(), + startWith([]) + ); + const licenses$ = this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'LICENSE', { elementsPerPage: Number.MAX_SAFE_INTEGER }).pipe( + getFirstSucceededRemoteListPayload(), + startWith([]) + ); + this.bitstreams$ = observableCombineLatest(originals$, licenses$).pipe( + map(([o, l]) => [...o, ...l]), + map((files: Bitstream[]) => + files.map( (original) => { - const thumbnail: Observable<Bitstream> = this.item.getThumbnailForOriginal(original); - this.thumbnails.set(original.id, thumbnail); + return getBitstreamBuilder(this.parentInjector, original) + .loadThumbnail(this.item) + .loadBitstreamFormat() + .build(); } ) - ) + ) + ); } } diff --git a/src/app/+item-page/simple/field-components/file-section/file-section.component.html b/src/app/+item-page/simple/field-components/file-section/file-section.component.html index 7063bac0be..6533322e03 100644 --- a/src/app/+item-page/simple/field-components/file-section/file-section.component.html +++ b/src/app/+item-page/simple/field-components/file-section/file-section.component.html @@ -1,7 +1,7 @@ -<ng-container *ngVar="(bitstreamsObs | async) as bitstreams"> +<ng-container *ngVar="(bitstreams$ | async) as bitstreams"> <ds-metadata-field-wrapper *ngIf="bitstreams?.length > 0" [label]="label | translate"> <div class="file-section"> - <a *ngFor="let file of bitstreams; let last=last;" [href]="file?.content" [download]="file?.name"> + <a *ngFor="let file of bitstreams; let last=last;" [href]="file?._links.content.href" [download]="file?.name"> <span>{{file?.name}}</span> <span>({{(file?.sizeBytes) |Â dsFileSize }})</span> <span *ngIf="!last" innerHTML="{{separator}}"></span> diff --git a/src/app/+item-page/simple/field-components/file-section/file-section.component.ts b/src/app/+item-page/simple/field-components/file-section/file-section.component.ts index 8c40d123bf..2e09c1cd49 100644 --- a/src/app/+item-page/simple/field-components/file-section/file-section.component.ts +++ b/src/app/+item-page/simple/field-components/file-section/file-section.component.ts @@ -1,8 +1,10 @@ import { Component, Input, OnInit } from '@angular/core'; import { Observable } from 'rxjs'; +import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Item } from '../../../../core/shared/item.model'; +import { getFirstSucceededRemoteListPayload } from '../../../../core/shared/operators'; /** * This component renders the file section of the item @@ -20,14 +22,21 @@ export class FileSectionComponent implements OnInit { separator = '<br/>'; - bitstreamsObs: Observable<Bitstream[]>; + bitstreams$: Observable<Bitstream[]>; + + constructor( + protected bitstreamDataService: BitstreamDataService + ) { + } ngOnInit(): void { this.initialize(); } initialize(): void { - this.bitstreamsObs = this.item.getFiles(); + this.bitstreams$ = this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL').pipe( + getFirstSucceededRemoteListPayload() + ); } } 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 c45e85668a..9eb704b9e9 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 @@ -4,7 +4,7 @@ <div class="row"> <div class="col-xs-12 col-md-4"> <ds-metadata-field-wrapper> - <ds-thumbnail [thumbnail]="object.getThumbnail() | async"></ds-thumbnail> + <ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail> </ds-metadata-field-wrapper> <ds-item-page-file-section [item]="object"></ds-item-page-file-section> <ds-item-page-date-field [item]="object"></ds-item-page-date-field> 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 64a96fdd52..abfcd24346 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,5 +1,9 @@ import { Component, Input } from '@angular/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; +import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Item } from '../../../../core/shared/item.model'; +import { getFirstSucceededRemoteDataPayload } from '../../../../core/shared/operators'; @Component({ selector: 'ds-item', @@ -10,4 +14,14 @@ import { Item } from '../../../../core/shared/item.model'; */ export class ItemComponent { @Input() object: Item; + + constructor(protected bitstreamDataService: BitstreamDataService) { + } + + // TODO refactor to return RemoteData, and thumbnail template to deal with loading + getThumbnail(): Observable<Bitstream> { + return this.bitstreamDataService.getThumbnailFor(this.object).pipe( + getFirstSucceededRemoteDataPayload() + ); + } } diff --git a/src/app/core/cache/builders/bitstream-builder.ts b/src/app/core/cache/builders/bitstream-builder.ts new file mode 100644 index 0000000000..4e50992aea --- /dev/null +++ b/src/app/core/cache/builders/bitstream-builder.ts @@ -0,0 +1,60 @@ +import { Injector } from '@angular/core'; +import { Observable } from 'rxjs/internal/Observable'; +import { BitstreamDataService } from '../../data/bitstream-data.service'; +import { BitstreamFormatDataService } from '../../data/bitstream-format-data.service'; +import { RemoteData } from '../../data/remote-data'; +import { BitstreamFormat } from '../../shared/bitstream-format.model'; +import { Bitstream } from '../../shared/bitstream.model'; +import { Item } from '../../shared/item.model'; + +export const getBitstreamBuilder = (parentInjector: Injector, bitstream: Bitstream) => { + const injector = Injector.create({ + providers:[ + { + provide: BitstreamBuilder, + useClass: BitstreamBuilder, + deps:[ + BitstreamDataService, + BitstreamFormatDataService, + ] + } + ], + parent: parentInjector + }); + return injector.get(BitstreamBuilder).initWithBitstream(bitstream); +}; + +export class BitstreamBuilder { + private bitstream: Bitstream; + private thumbnail: Observable<RemoteData<Bitstream>>; + private format: Observable<RemoteData<BitstreamFormat>>; + + constructor( + protected bitstreamDataService: BitstreamDataService, + protected bitstreamFormatDataService: BitstreamFormatDataService + ) { + } + + initWithBitstream(bitstream: Bitstream): BitstreamBuilder { + this.bitstream = bitstream; + return this; + } + + loadThumbnail(item: Item): BitstreamBuilder { + this.thumbnail = this.bitstreamDataService.getMatchingThumbnail(item, this.bitstream); + return this; + } + + loadBitstreamFormat(): BitstreamBuilder { + this.format = this.bitstreamFormatDataService.findByBitstream(this.bitstream); + return this; + } + + build(): Bitstream { + const bitstream = this.bitstream; + bitstream.thumbnail = this.thumbnail; + bitstream.format = this.format; + return bitstream; + } + +} diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts index 0bfb5f0321..19fb7c881f 100644 --- a/src/app/core/cache/builders/build-decorators.ts +++ b/src/app/core/cache/builders/build-decorators.ts @@ -53,7 +53,7 @@ export function getMapsToType(type: string | ResourceType) { return typeMap.get(type); } -export function relationship<T extends CacheableObject>(value: GenericConstructor<T>, isList: boolean = false): any { +export function relationship<T extends CacheableObject>(value: GenericConstructor<T>, isList: boolean = false, shouldAutoResolve: boolean = true): any { return function r(target: any, propertyKey: string, descriptor: PropertyDescriptor) { if (!target || !propertyKey) { return; @@ -66,7 +66,8 @@ export function relationship<T extends CacheableObject>(value: GenericConstructo relationshipMap.set(target.constructor, metaDataList); return Reflect.metadata(relationshipKey, { resourceType: (value as any).type.value, - isList + isList, + shouldAutoResolve }).apply(this, arguments); }; } diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 403a0273bc..5df3f20ca5 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -1,9 +1,21 @@ import { Injectable } from '@angular/core'; -import { combineLatest as observableCombineLatest, Observable, of as observableOf, race as observableRace } from 'rxjs'; +import { + combineLatest as observableCombineLatest, + Observable, + of as observableOf, + race as observableRace +} from 'rxjs'; import { distinctUntilChanged, flatMap, map, startWith, switchMap, tap } from 'rxjs/operators'; -import { hasValue, hasValueOperator, isEmpty, isNotEmpty, isNotUndefined } from '../../../shared/empty.util'; +import { + hasNoValue, + hasValue, + hasValueOperator, + isEmpty, + isNotEmpty, + isNotUndefined +} from '../../../shared/empty.util'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; import { RemoteDataError } from '../../data/remote-data-error'; @@ -157,46 +169,55 @@ export class RemoteDataBuildService { relationships.forEach((relationship: string) => { let result; if (hasValue(normalized[relationship])) { - const { resourceType, isList } = getRelationMetadata(normalized, relationship); + const { resourceType, isList, shouldAutoResolve } = getRelationMetadata(normalized, relationship); const objectList = normalized[relationship].page || normalized[relationship]; - if (typeof objectList !== 'string') { - objectList.forEach((href: string) => { - const request = new GetRequest(this.requestService.generateRequestId(), href); + if (shouldAutoResolve) { + if (typeof objectList !== 'string') { + objectList.forEach((href: string) => { + const request = new GetRequest(this.requestService.generateRequestId(), href); + if (!this.requestService.isCachedOrPending(request)) { + this.requestService.configure(request) + } + }); + + const rdArr = []; + objectList.forEach((href: string) => { + rdArr.push(this.buildSingle(href)); + }); + + if (isList) { + result = this.aggregate(rdArr); + } else if (rdArr.length === 1) { + result = rdArr[0]; + } + } else { + const request = new GetRequest(this.requestService.generateRequestId(), objectList); if (!this.requestService.isCachedOrPending(request)) { this.requestService.configure(request) } - }); - const rdArr = []; - objectList.forEach((href: string) => { - rdArr.push(this.buildSingle(href)); - }); - - if (isList) { - result = this.aggregate(rdArr); - } else if (rdArr.length === 1) { - result = rdArr[0]; - } - } else { - const request = new GetRequest(this.requestService.generateRequestId(), objectList); - if (!this.requestService.isCachedOrPending(request)) { - this.requestService.configure(request) + // The rest API can return a single URL to represent a list of resources (e.g. /items/:id/bitstreams) + // in that case only 1 href will be stored in the normalized obj (so the isArray above fails), + // but it should still be built as a list + if (isList) { + result = this.buildList(objectList); + } else { + result = this.buildSingle(objectList); + } } - // The rest API can return a single URL to represent a list of resources (e.g. /items/:id/bitstreams) - // in that case only 1 href will be stored in the normalized obj (so the isArray above fails), - // but it should still be built as a list - if (isList) { - result = this.buildList(objectList); + if (hasValue(normalized[relationship].page)) { + links[relationship] = this.toPaginatedList(result, normalized[relationship].pageInfo); } else { - result = this.buildSingle(objectList); + links[relationship] = result; } - } - - if (hasValue(normalized[relationship].page)) { - links[relationship] = this.toPaginatedList(result, normalized[relationship].pageInfo); } else { - links[relationship] = result; + if (hasNoValue(links._links)) { + links._links = {}; + } + links._links[relationship] = { + href: objectList + }; } } }); diff --git a/src/app/core/cache/models/normalized-bitstream.model.ts b/src/app/core/cache/models/normalized-bitstream.model.ts index a9e389fd41..028c1a24de 100644 --- a/src/app/core/cache/models/normalized-bitstream.model.ts +++ b/src/app/core/cache/models/normalized-bitstream.model.ts @@ -22,13 +22,14 @@ export class NormalizedBitstream extends NormalizedDSpaceObject<Bitstream> { * The relative path to this Bitstream's file */ @autoserialize + @relationship(Bitstream, false, false) content: string; /** * The format of this Bitstream */ @autoserialize - @relationship(BitstreamFormat, false) + @relationship(BitstreamFormat, false, false) format: string; /** @@ -41,14 +42,14 @@ export class NormalizedBitstream extends NormalizedDSpaceObject<Bitstream> { * An array of Bundles that are direct parents of this Bitstream */ @autoserialize - @relationship(Item, true) + @relationship(Item, true, false) parents: string[]; /** * The Bundle that owns this Bitstream */ @autoserialize - @relationship(Item, false) + @relationship(Item, false, false) owner: string; /** diff --git a/src/app/core/cache/models/normalized-bundle.model.ts b/src/app/core/cache/models/normalized-bundle.model.ts index 9582643efb..0eff125c87 100644 --- a/src/app/core/cache/models/normalized-bundle.model.ts +++ b/src/app/core/cache/models/normalized-bundle.model.ts @@ -22,7 +22,7 @@ export class NormalizedBundle extends NormalizedDSpaceObject<Bundle> { * The primary bitstream of this Bundle */ @autoserialize - @relationship(Bitstream, false) + @relationship(Bitstream, false, false) primaryBitstream: string; /** @@ -39,7 +39,7 @@ export class NormalizedBundle extends NormalizedDSpaceObject<Bundle> { * List of Bitstreams that are part of this Bundle */ @autoserialize - @relationship(Bitstream, true) + @relationship(Bitstream, true, false) bitstreams: string[]; } diff --git a/src/app/core/cache/models/normalized-collection.model.ts b/src/app/core/cache/models/normalized-collection.model.ts index 9b3419675a..f79ed024fc 100644 --- a/src/app/core/cache/models/normalized-collection.model.ts +++ b/src/app/core/cache/models/normalized-collection.model.ts @@ -30,42 +30,41 @@ export class NormalizedCollection extends NormalizedDSpaceObject<Collection> { * The Bitstream that represents the license of this Collection */ @autoserialize - @relationship(License, false) license: string; /** * The Bitstream that represents the default Access Conditions of this Collection */ @autoserialize - @relationship(ResourcePolicy, false) + @relationship(ResourcePolicy, false, false) defaultAccessConditions: string; /** * The Bitstream that represents the logo of this Collection */ @deserialize - @relationship(Bitstream, false) + @relationship(Bitstream, false, false) logo: string; /** * An array of Communities that are direct parents of this Collection */ @deserialize - @relationship(Community, true) + @relationship(Community, true, false) parents: string[]; /** * The Community that owns this Collection */ @deserialize - @relationship(Community, false) + @relationship(Community, false, false) owner: string; /** * List of Items that are part of (not necessarily owned by) this Collection */ @deserialize - @relationship(Item, true) + @relationship(Item, true, false) items: string[]; } diff --git a/src/app/core/cache/models/normalized-item.model.ts b/src/app/core/cache/models/normalized-item.model.ts index 9b7edf70c0..7e326d6c1d 100644 --- a/src/app/core/cache/models/normalized-item.model.ts +++ b/src/app/core/cache/models/normalized-item.model.ts @@ -48,25 +48,25 @@ export class NormalizedItem extends NormalizedDSpaceObject<Item> { * An array of Collections that are direct parents of this Item */ @deserialize - @relationship(Collection, true) + @relationship(Collection, true, false) parents: string[]; /** * The Collection that owns this Item */ @deserialize - @relationship(Collection, false) + @relationship(Collection, false, false) owningCollection: string; /** * List of Bitstreams that are owned by this Item */ @deserialize - @relationship(Bundle, true) + @relationship(Bundle, true, false) bundles: string[]; @deserialize - @relationship(Relationship, true) + @relationship(Relationship, true, false) relationships: string[]; } diff --git a/src/app/core/data/base-response-parsing.service.ts b/src/app/core/data/base-response-parsing.service.ts index ea2d71faa7..d1bc6e9e10 100644 --- a/src/app/core/data/base-response-parsing.service.ts +++ b/src/app/core/data/base-response-parsing.service.ts @@ -27,24 +27,26 @@ export abstract class BaseResponseParsingService { return this.processArray(data, request); } else if (isRestDataObject(data)) { const object = this.deserialize(data); - if (isNotEmpty(data._embedded)) { - Object - .keys(data._embedded) - .filter((property) => data._embedded.hasOwnProperty(property)) - .forEach((property) => { - const parsedObj = this.process<ObjectDomain>(data._embedded[property], request); - if (isNotEmpty(parsedObj)) { - if (isRestPaginatedList(data._embedded[property])) { - object[property] = parsedObj; - object[property].page = parsedObj.page.map((obj) => this.retrieveObjectOrUrl(obj)); - } else if (isRestDataObject(data._embedded[property])) { - object[property] = this.retrieveObjectOrUrl(parsedObj); - } else if (Array.isArray(parsedObj)) { - object[property] = parsedObj.map((obj) => this.retrieveObjectOrUrl(obj)) - } - } - }); - } + + // TODO remove + // if (isNotEmpty(data._embedded)) { + // Object + // .keys(data._embedded) + // .filter((property) => data._embedded.hasOwnProperty(property)) + // .forEach((property) => { + // const parsedObj = this.process<ObjectDomain>(data._embedded[property], request); + // if (isNotEmpty(parsedObj)) { + // if (isRestPaginatedList(data._embedded[property])) { + // object[property] = parsedObj; + // object[property].page = parsedObj.page.map((obj) => this.retrieveObjectOrUrl(obj)); + // } else if (isRestDataObject(data._embedded[property])) { + // object[property] = this.retrieveObjectOrUrl(parsedObj); + // } else if (Array.isArray(parsedObj)) { + // object[property] = parsedObj.map((obj) => this.retrieveObjectOrUrl(obj)) + // } + // } + // }); + // } this.cache(object, request); return object; diff --git a/src/app/core/data/bitstream-data.service.ts b/src/app/core/data/bitstream-data.service.ts new file mode 100644 index 0000000000..5664e0a442 --- /dev/null +++ b/src/app/core/data/bitstream-data.service.ts @@ -0,0 +1,147 @@ +import { HttpClient } from '@angular/common/http'; +import { Injectable } from '@angular/core'; +import { Store } from '@ngrx/store'; +import { Observable } from 'rxjs/internal/Observable'; +import { map, switchMap } from 'rxjs/operators'; +import { hasNoValue, hasValue } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { Bitstream } from '../shared/bitstream.model'; +import { Bundle } from '../shared/bundle.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { Item } from '../shared/item.model'; +import { BundleDataService } from './bundle-data.service'; +import { CommunityDataService } from './community-data.service'; +import { DataService } from './data.service'; +import { DSOChangeAnalyzer } from './dso-change-analyzer.service'; +import { PaginatedList } from './paginated-list'; +import { RemoteData } from './remote-data'; +import { RemoteDataError } from './remote-data-error'; +import { FindListOptions } from './request.models'; +import { RequestService } from './request.service'; + +@Injectable({ + providedIn: 'root' +}) +export class BitstreamDataService extends DataService<Bitstream> { + + protected linkPath = 'bitstreams'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store<CoreState>, + protected cds: CommunityDataService, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DSOChangeAnalyzer<Bitstream>, + protected bundleService: BundleDataService, + ) { + super(); + } + + getBrowseEndpoint(options: FindListOptions, linkPath?: string): Observable<string> { + // TODO needed? if not, perhaps remove it from datasevice? + return undefined; + } + + /** + * Retrieves the bitstreams in a given bundle + * + * @param bundle the bundle to retrieve bitstreams from + * @param options options for the find all request + */ + findAllByBundle(bundle: Bundle, options?: FindListOptions): Observable<RemoteData<PaginatedList<Bitstream>>> { + return this.findAllByHref(bundle._links.bitstreams.href, options); + } + + /** + * Retrieves the thumbnail for the given item + * @returns {Observable<RemoteData<Bitstream>>} the first bitstream in the THUMBNAIL bundle + */ + // TODO should be implemented rest side. Item should get a thumbnail link + public getThumbnailFor(item: Item): Observable<RemoteData<Bitstream>> { + return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe( + switchMap((bundleRD: RemoteData<Bundle>) => { + if (hasValue(bundleRD.payload)) { + return this.findAllByBundle(bundleRD.payload, { elementsPerPage: 1 }).pipe( + map((bitstreamRD: RemoteData<PaginatedList<Bitstream>>) => { + if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) { + return new RemoteData( + false, + false, + true, + undefined, + bitstreamRD.payload.page[0] + ); + } else { + return bitstreamRD as any; + } + }) + ); + } else { + return [bundleRD as any]; + } + }) + ); + } + + // TODO should be implemented rest side + public getMatchingThumbnail(item: Item, bitstreamInOriginal: Bitstream): Observable<RemoteData<Bitstream>> { + return this.bundleService.findByItemAndName(item, 'THUMBNAIL').pipe( + switchMap((bundleRD: RemoteData<Bundle>) => { + if (hasValue(bundleRD.payload)) { + return this.findAllByBundle(bundleRD.payload, { elementsPerPage: Number.MAX_SAFE_INTEGER }).pipe( + map((bitstreamRD: RemoteData<PaginatedList<Bitstream>>) => { + if (hasValue(bitstreamRD.payload) && hasValue(bitstreamRD.payload.page)) { + const matchingThumbnail = bitstreamRD.payload.page.find((thumbnail: Bitstream) => + thumbnail.name.startsWith(bitstreamInOriginal.name) + ); + if (hasValue(matchingThumbnail)) { + return new RemoteData( + false, + false, + true, + undefined, + matchingThumbnail + ); + } else { + return new RemoteData( + false, + false, + false, + new RemoteDataError(404, '404', 'No matching thumbnail found'), + undefined + ); + } + } else { + return bitstreamRD as any; + } + }) + ); + } else { + return [bundleRD as any]; + } + }) + ); + } + + public findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions): Observable<RemoteData<PaginatedList<Bitstream>>> { + return this.bundleService.findByItemAndName(item, bundleName).pipe( + switchMap((bundleRD: RemoteData<Bundle>) => { + if (hasValue(bundleRD.payload)) { + return this.findAllByBundle(bundleRD.payload, options); + } else { + return [bundleRD as any]; + } + }) + ); + } + +} diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts index c30330a0a3..c707c42075 100644 --- a/src/app/core/data/bitstream-format-data.service.ts +++ b/src/app/core/data/bitstream-format-data.service.ts @@ -1,6 +1,8 @@ import { Injectable } from '@angular/core'; +import { Bitstream } from '../shared/bitstream.model'; import { DataService } from './data.service'; import { BitstreamFormat } from '../shared/bitstream-format.model'; +import { RemoteData } from './remote-data'; import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; @@ -183,4 +185,8 @@ export class BitstreamFormatDataService extends DataService<BitstreamFormat> { map((request: RequestEntry) => request.response.isSuccessful) ); } + + findByBitstream(bitstream: Bitstream): Observable<RemoteData<BitstreamFormat>> { + return this.findByHref(bitstream._links.format.href); + } } diff --git a/src/app/core/data/bundle-data.service.ts b/src/app/core/data/bundle-data.service.ts index 280f727aad..f6f24a7d7f 100644 --- a/src/app/core/data/bundle-data.service.ts +++ b/src/app/core/data/bundle-data.service.ts @@ -1,23 +1,31 @@ +import { HttpClient } from '@angular/common/http'; import { Injectable } from '@angular/core'; -import { DataService } from './data.service'; -import { Bundle } from '../shared/bundle.model'; -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { Store } from '@ngrx/store'; -import { CoreState } from '../core.reducers'; +import { Observable } from 'rxjs/internal/Observable'; +import { map } from 'rxjs/operators'; +import { hasValue } from '../../shared/empty.util'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { CoreState } from '../core.reducers'; +import { Bundle } from '../shared/bundle.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { HttpClient } from '@angular/common/http'; +import { Item } from '../shared/item.model'; +import { BitstreamDataService } from './bitstream-data.service'; +import { DataService } from './data.service'; import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; +import { PaginatedList } from './paginated-list'; +import { RemoteData } from './remote-data'; import { FindListOptions } from './request.models'; -import { Observable } from 'rxjs/internal/Observable'; +import { RequestService } from './request.service'; /** * A service responsible for fetching/sending data from/to the REST API on the bundles endpoint */ -@Injectable() +@Injectable( + {providedIn: 'root'} +) export class BundleDataService extends DataService<Bundle> { protected linkPath = 'bundles'; protected forceBypassCache = false; @@ -43,4 +51,29 @@ export class BundleDataService extends DataService<Bundle> { getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> { return this.halService.getEndpoint(this.linkPath); } + + findAllByItem(item: Item, options?: FindListOptions): Observable<RemoteData<PaginatedList<Bundle>>> { + return this.findAllByHref(item._links.bundles.href, options); + } + + // TODO should be implemented rest side + findByItemAndName(item: Item, bundleName: string): Observable<RemoteData<Bundle>> { + return this.findAllByItem(item, { elementsPerPage: Number.MAX_SAFE_INTEGER }).pipe( + map((rd: RemoteData<PaginatedList<Bundle>>) => { + if (hasValue(rd.payload) && hasValue(rd.payload.page)) { + const matchingBundle = rd.payload.page.find((bundle: Bundle) => + bundle.name === bundleName); + return new RemoteData( + false, + false, + true, + undefined, + matchingBundle + ); + } else { + return rd as any; + } + }), + ); + } } diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index ed05c99e27..e6fb823c96 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -7,6 +7,7 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { ObjectCacheService } from '../cache/object-cache.service'; import { CoreState } from '../core.reducers'; import { Collection } from '../shared/collection.model'; +import { Item } from '../shared/item.model'; import { ComColDataService } from './comcol-data.service'; import { CommunityDataService } from './community-data.service'; import { RequestService } from './request.service'; @@ -240,4 +241,9 @@ export class CollectionDataService extends ComColDataService<Collection> { this.halService.getEndpoint('collections', `${communityEndpointHref}/${parentUUID}`)), ); } + + findOwningCollectionFor(item: Item): Observable<RemoteData<Collection>> { + return this.findByHref(item._links.owningCollection.href); + } + } diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 2ce0362a4e..915de4862f 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -81,7 +81,9 @@ export abstract class ComColDataService<T extends CacheableObject> extends DataS protected abstract getFindByParentHref(parentUUID: string): Observable<string>; public findByParent(parentUUID: string, options: FindListOptions = {}): Observable<RemoteData<PaginatedList<T>>> { - const href$ = this.buildHrefFromFindOptions(this.getFindByParentHref(parentUUID), [], options); + const href$ = this.getFindByParentHref(parentUUID).pipe( + map((href: string) => this.buildHrefFromFindOptions(href, options)) + ); return this.findList(href$, options); } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 82fdb82008..9a214ddd89 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -76,12 +76,12 @@ export abstract class DataService<T extends CacheableObject> { * Return an observable that emits created HREF */ protected getFindAllHref(options: FindListOptions = {}, linkPath?: string): Observable<string> { - let result: Observable<string>; + let result$: Observable<string>; const args = []; - result = this.getBrowseEndpoint(options, linkPath).pipe(distinctUntilChanged()); + result$ = this.getBrowseEndpoint(options, linkPath).pipe(distinctUntilChanged()); - return this.buildHrefFromFindOptions(result, args, options); + return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args))); } /** @@ -93,10 +93,10 @@ export abstract class DataService<T extends CacheableObject> { * Return an observable that emits created HREF */ protected getSearchByHref(searchMethod: string, options: FindListOptions = {}): Observable<string> { - let result: Observable<string>; + let result$: Observable<string>; const args = []; - result = this.getSearchEndpoint(searchMethod); + result$ = this.getSearchEndpoint(searchMethod); if (hasValue(options.searchParams)) { options.searchParams.forEach((param: SearchParam) => { @@ -104,37 +104,39 @@ export abstract class DataService<T extends CacheableObject> { }) } - return this.buildHrefFromFindOptions(result, args, options); + return result$.pipe(map((result: string) => this.buildHrefFromFindOptions(result, options, args))); } /** * Turn an options object into a query string and combine it with the given HREF * - * @param href$ The HREF to which the query string should be appended - * @param args Array with additional params to combine with query string + * @param href The HREF to which the query string should be appended * @param options The [[FindListOptions]] object + * @param extraArgs Array with additional params to combine with query string * @return {Observable<string>} * Return an observable that emits created HREF */ - protected buildHrefFromFindOptions(href$: Observable<string>, args: string[], options: FindListOptions): Observable<string> { + protected buildHrefFromFindOptions(href: string, options: FindListOptions, extraArgs: string[] = []): string { + + let args = [...extraArgs]; if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { /* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */ - args.push(`page=${options.currentPage - 1}`); + args = [...args, `page=${options.currentPage - 1}`]; } if (hasValue(options.elementsPerPage)) { - args.push(`size=${options.elementsPerPage}`); + args = [...args, `size=${options.elementsPerPage}`]; } if (hasValue(options.sort)) { - args.push(`sort=${options.sort.field},${options.sort.direction}`); + args = [...args, `sort=${options.sort.field},${options.sort.direction}`]; } if (hasValue(options.startsWith)) { - args.push(`startsWith=${options.startsWith}`); + args = [...args, `startsWith=${options.startsWith}`]; } if (isNotEmpty(args)) { - return href$.pipe(map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString())); + return new URLCombiner(href, `?${args.join('&')}`).toString(); } else { - return href$; + return href; } } @@ -183,8 +185,9 @@ export abstract class DataService<T extends CacheableObject> { return this.rdbService.buildSingle<T>(hrefObs); } - findByHref(href: string, options?: HttpOptions): Observable<RemoteData<T>> { - const request = new GetRequest(this.requestService.generateRequestId(), href, null, options); + findByHref(href: string, findListOptions: FindListOptions = {}, httpOptions?: HttpOptions): Observable<RemoteData<T>> { + const requestHref = this.buildHrefFromFindOptions(href, findListOptions, []); + const request = new GetRequest(this.requestService.generateRequestId(), requestHref, null, httpOptions); if (hasValue(this.responseMsToLive)) { request.responseMsToLive = this.responseMsToLive; } @@ -192,6 +195,16 @@ export abstract class DataService<T extends CacheableObject> { return this.rdbService.buildSingle<T>(href); } + findAllByHref(href: string, findListOptions: FindListOptions = {}, httpOptions?: HttpOptions): Observable<RemoteData<PaginatedList<T>>> { + const requestHref = this.buildHrefFromFindOptions(href, findListOptions, []); + const request = new GetRequest(this.requestService.generateRequestId(), requestHref, null, httpOptions); + if (hasValue(this.responseMsToLive)) { + request.responseMsToLive = this.responseMsToLive; + } + this.requestService.configure(request); + return this.rdbService.buildList<T>(requestHref); + } + /** * Return object search endpoint by given search method * diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index cd7e70dc32..4ac6cac5cc 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -29,7 +29,7 @@ import { configureRequest, filterSuccessfulResponses, getRequestFromRequestHref, - getResponseFromEntry + getResponseFromEntry, getSucceededRemoteData } from '../shared/operators'; import { RequestEntry } from './request.reducer'; import { GenericSuccessResponse, RestResponse } from '../cache/response.models'; @@ -53,7 +53,8 @@ export class ItemDataService extends DataService<Item> { protected halService: HALEndpointService, protected notificationsService: NotificationsService, protected http: HttpClient, - protected comparator: DSOChangeAnalyzer<Item>) { + protected comparator: DSOChangeAnalyzer<Item>, + ) { super(); } diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index 0448c18ec6..20e3628de2 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -178,11 +178,11 @@ export class RelationshipService extends DataService<Relationship> { } /** - * Get an item its relationships in the form of an array + * Get an item's relationships in the form of an array * @param item */ getItemRelationshipsArray(item: Item): Observable<Relationship[]> { - return item.relationships.pipe( + return this.findAllByHref(item._links.relationships.href).pipe( getSucceededRemoteData(), getRemoteDataPayload(), map((rels: PaginatedList<Relationship>) => rels.page), diff --git a/src/app/core/data/resource-policy.service.ts b/src/app/core/data/resource-policy.service.ts index 017e5cf5ee..79530dc7b1 100644 --- a/src/app/core/data/resource-policy.service.ts +++ b/src/app/core/data/resource-policy.service.ts @@ -61,6 +61,6 @@ export class ResourcePolicyService { } findByHref(href: string, options?: HttpOptions): Observable<RemoteData<ResourcePolicy>> { - return this.dataService.findByHref(href, options); + return this.dataService.findByHref(href, {}, options); } } diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 2b1cf4ffc1..06842638b7 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -1,29 +1,28 @@ -import { - catchError, - distinctUntilKeyChanged, - filter, - first, - map, - take -} from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; -import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Meta, MetaDefinition, Title } from '@angular/platform-browser'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { TranslateService } from '@ngx-translate/core'; import { BehaviorSubject, Observable } from 'rxjs'; +import { catchError, distinctUntilKeyChanged, filter, first, map, take } from 'rxjs/operators'; + +import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { BitstreamDataService } from '../data/bitstream-data.service'; +import { BitstreamFormatDataService } from '../data/bitstream-format-data.service'; import { RemoteData } from '../data/remote-data'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; import { Bitstream } from '../shared/bitstream.model'; -import { CacheableObject } from '../cache/object-cache.reducer'; import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; - -import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; -import { BitstreamFormat } from '../shared/bitstream-format.model'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { + getFirstSucceededRemoteDataPayload, + getFirstSucceededRemoteListPayload +} from '../shared/operators'; @Injectable() export class MetadataService { @@ -39,6 +38,8 @@ export class MetadataService { private translate: TranslateService, private meta: Meta, private title: Title, + private bitstreamDataService: BitstreamDataService, + private bitstreamFormatDataService: BitstreamFormatDataService, @Inject(GLOBAL_CONFIG) private envConfig: GlobalConfig ) { // TODO: determine what open graph meta tags are needed and whether @@ -266,8 +267,9 @@ export class MetadataService { private setCitationPdfUrlTag(): void { if (this.currentObject.value instanceof Item) { const item = this.currentObject.value as Item; - item.getFiles() + this.bitstreamDataService.findAllByItemAndBundleName(item, 'ORIGINAL') .pipe( + getFirstSucceededRemoteListPayload(), first((files) => isNotEmpty(files)), catchError((error) => { console.debug(error.message); @@ -275,17 +277,11 @@ export class MetadataService { })) .subscribe((bitstreams: Bitstream[]) => { for (const bitstream of bitstreams) { - bitstream.format.pipe( - first(), - catchError((error: Error) => { - console.debug(error.message); - return [] - }), - map((rd: RemoteData<BitstreamFormat>) => rd.payload), - filter((format: BitstreamFormat) => hasValue(format))) - .subscribe((format: BitstreamFormat) => { + this.bitstreamFormatDataService.findByBitstream(bitstream).pipe( + getFirstSucceededRemoteDataPayload() + ).subscribe((format: BitstreamFormat) => { if (format.mimetype === 'application/pdf') { - this.addMetaTag('citation_pdf_url', bitstream.content); + this.addMetaTag('citation_pdf_url', bitstream._links.content.href); } }); } diff --git a/src/app/core/shared/HALLink.model.ts b/src/app/core/shared/HALLink.model.ts new file mode 100644 index 0000000000..32c4c7c99f --- /dev/null +++ b/src/app/core/shared/HALLink.model.ts @@ -0,0 +1,5 @@ +export class HALLink { + href: string; + name?: string; + templated?: boolean +} diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index 887f7d0843..9f2eb15e52 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -1,8 +1,8 @@ -import { DSpaceObject } from './dspace-object.model'; +import { Observable } from 'rxjs'; import { RemoteData } from '../data/remote-data'; -import { Item } from './item.model'; import { BitstreamFormat } from './bitstream-format.model'; -import { Observable } from 'rxjs'; +import { DSpaceObject } from './dspace-object.model'; +import { HALLink } from './HALLink.model'; import { ResourceType } from './resource-type'; export class Bitstream extends DSpaceObject { @@ -24,22 +24,24 @@ export class Bitstream extends DSpaceObject { bundleName: string; /** - * An array of Bitstream Format of this Bitstream + * The Thumbnail for this Bitstream */ - format: Observable<RemoteData<BitstreamFormat>>; + thumbnail?: Observable<RemoteData<Bitstream>>; /** - * An array of Items that are direct parents of this Bitstream + * The Bitstream Format for this Bitstream */ - parents: Observable<RemoteData<Item[]>>; - - /** - * The Bundle that owns this Bitstream - */ - owner: Observable<RemoteData<Item>>; + format?: Observable<RemoteData<BitstreamFormat>>; /** * The URL to retrieve this Bitstream's file */ content: string; + + _links: { + self: HALLink; + bundle: HALLink; + content: HALLink; + format: HALLink; + } } diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index dade7d12be..58359e959c 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -1,10 +1,6 @@ import { DSpaceObject } from './dspace-object.model'; -import { Bitstream } from './bitstream.model'; -import { Item } from './item.model'; -import { RemoteData } from '../data/remote-data'; -import { Observable } from 'rxjs'; +import { HALLink } from './HALLink.model'; import { ResourceType } from './resource-type'; -import { PaginatedList } from '../data/paginated-list'; export class Bundle extends DSpaceObject { static type = new ResourceType('bundle'); @@ -14,24 +10,11 @@ export class Bundle extends DSpaceObject { */ name: string; - /** - * The primary bitstream of this Bundle - */ - primaryBitstream: Observable<RemoteData<Bitstream>>; - - /** - * An array of Items that are direct parents of this Bundle - */ - parents: Observable<RemoteData<Item[]>>; - - /** - * The Item that owns this Bundle - */ - owner: Observable<RemoteData<Item>>; - - /** - * List of Bitstreams that are part of this Bundle - */ - bitstreams: Observable<RemoteData<PaginatedList<Bitstream>>>; - + _links: { + self: HALLink; + primaryBitstream: HALLink; + parents: HALLink; + owner: HALLink; + bitstreams: HALLink; + } } diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index 642fe50736..b04ad95bc4 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -1,5 +1,6 @@ import { DSpaceObject } from './dspace-object.model'; import { Bitstream } from './bitstream.model'; +import { HALLink } from './HALLink.model'; import { Item } from './item.model'; import { RemoteData } from '../data/remote-data'; import { Observable } from 'rxjs'; @@ -82,4 +83,13 @@ export class Collection extends DSpaceObject { owner: Observable<RemoteData<Collection>>; items: Observable<RemoteData<Item[]>>; + + _links: { + license: HALLink; + harvester: HALLink; + mappedItems: HALLink; + defaultAccessConditions: HALLink; + logo: HALLink; + self: HALLink; + } } diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index bd304274ab..73853dc948 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -1,19 +1,11 @@ -import { map, startWith, filter, switchMap } from 'rxjs/operators'; -import { Observable } from 'rxjs'; +import { isEmpty } from '../../shared/empty.util'; +import { DEFAULT_ENTITY_TYPE } from '../../shared/metadata-representation/metadata-representation.decorator'; +import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; import { DSpaceObject } from './dspace-object.model'; -import { Collection } from './collection.model'; -import { RemoteData } from '../data/remote-data'; -import { Bitstream } from './bitstream.model'; -import { hasValueOperator, isNotEmpty, isEmpty } from '../../shared/empty.util'; -import { PaginatedList } from '../data/paginated-list'; -import { Relationship } from './item-relationships/relationship.model'; -import { ResourceType } from './resource-type'; -import { getAllSucceededRemoteData, getSucceededRemoteData } from './operators'; -import { Bundle } from './bundle.model'; import { GenericConstructor } from './generic-constructor'; -import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; -import { DEFAULT_ENTITY_TYPE } from '../../shared/metadata-representation/metadata-representation.decorator'; +import { HALLink } from './HALLink.model'; +import { ResourceType } from './resource-type'; /** * Class representing a DSpace Item @@ -46,77 +38,13 @@ export class Item extends DSpaceObject { */ isWithdrawn: boolean; - /** - * An array of Collections that are direct parents of this Item - */ - parents: Observable<RemoteData<Collection[]>>; - - /** - * The Collection that owns this Item - */ - owningCollection: Observable<RemoteData<Collection>>; - - get owner(): Observable<RemoteData<Collection>> { - return this.owningCollection; - } - - /** - * Bitstream bundles within this item - */ - bundles: Observable<RemoteData<PaginatedList<Bundle>>>; - - relationships: Observable<RemoteData<PaginatedList<Relationship>>>; - - /** - * Retrieves the thumbnail of this item - * @returns {Observable<Bitstream>} the primaryBitstream of the 'THUMBNAIL' bundle - */ - getThumbnail(): Observable<Bitstream> { - // TODO: currently this just picks the first thumbnail - // should be adjusted when we have a way to determine - // the primary thumbnail from rest - return this.getBitstreamsByBundleName('THUMBNAIL').pipe( - filter((thumbnails) => isNotEmpty(thumbnails)), - map((thumbnails) => thumbnails[0]),) - } - - /** - * Retrieves the thumbnail for the given original of this item - * @returns {Observable<Bitstream>} the primaryBitstream of the 'THUMBNAIL' bundle - */ - getThumbnailForOriginal(original: Bitstream): Observable<Bitstream> { - return this.getBitstreamsByBundleName('THUMBNAIL').pipe( - map((files) => { - return files.find((thumbnail) => thumbnail.name.startsWith(original.name)) - }),startWith(undefined),); - } - - /** - * Retrieves all files that should be displayed on the item page of this item - * @returns {Observable<Array<Observable<Bitstream>>>} an array of all Bitstreams in the 'ORIGINAL' bundle - */ - getFiles(): Observable<Bitstream[]> { - return this.getBitstreamsByBundleName('ORIGINAL'); - } - - /** - * Retrieves bitstreams by bundle name - * @param bundleName The name of the Bundle that should be returned - * @returns {Observable<Bitstream[]>} the bitstreams with the given bundleName - * TODO now that bitstreams can be paginated this should move to the server - * see https://github.com/DSpace/dspace-angular/issues/332 - */ - getBitstreamsByBundleName(bundleName: string): Observable<Bitstream[]> { - return this.bundles.pipe( - getSucceededRemoteData(), - map((rd: RemoteData<PaginatedList<Bundle>>) => rd.payload.page.find((bundle: Bundle) => bundle.name === bundleName)), - hasValueOperator(), - switchMap((bundle: Bundle) => bundle.bitstreams), - getAllSucceededRemoteData(), - map((rd: RemoteData<PaginatedList<Bitstream>>) => rd.payload.page), - startWith([]) - ); - } + _links: { + self: HALLink; + parents: HALLink; + owningCollection: HALLink; + bundles: HALLink; + relationships: HALLink; + }; /** * Method that returns as which type of object this object should be rendered diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index 308e4f8a2d..57a955b5fc 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -59,10 +59,92 @@ export const getRemoteDataPayload = () => <T>(source: Observable<RemoteData<T>>): Observable<T> => source.pipe(map((remoteData: RemoteData<T>) => remoteData.payload)); +export const getPaginatedListPayload = () => + <T>(source: Observable<PaginatedList<T>>): Observable<T[]> => + source.pipe(map((list: PaginatedList<T>) => list.page)); + export const getSucceededRemoteData = () => <T>(source: Observable<RemoteData<T>>): Observable<RemoteData<T>> => source.pipe(find((rd: RemoteData<T>) => rd.hasSucceeded)); +/** + * Get the first successful remotely retrieved object + * + * You usually don't want to use this, it is a code smell. + * Work with the RemoteData object instead, that way you can + * handle loading and errors correctly. + * + * These operators were created as a first step in refactoring + * out all the instances where this is used incorrectly. + */ +export const getFirstSucceededRemoteDataPayload = () => + <T>(source: Observable<RemoteData<T>>): Observable<T> => + source.pipe( + getSucceededRemoteData(), + getRemoteDataPayload() + ); + +/** + * Get the all successful remotely retrieved objects + * + * You usually don't want to use this, it is a code smell. + * Work with the RemoteData object instead, that way you can + * handle loading and errors correctly. + * + * These operators were created as a first step in refactoring + * out all the instances where this is used incorrectly. + */ +export const getAllSucceededRemoteDataPayload = () => + <T>(source: Observable<RemoteData<T>>): Observable<T> => + source.pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload() + ); + +/** + * Get the first successful remotely retrieved paginated list + * as an array + * + * You usually don't want to use this, it is a code smell. + * Work with the RemoteData object instead, that way you can + * handle loading and errors correctly. + * + * You also don't want to ignore pagination and simply use the + * page as an array. + * + * These operators were created as a first step in refactoring + * out all the instances where this is used incorrectly. + */ +export const getFirstSucceededRemoteListPayload = () => + <T>(source: Observable<RemoteData<PaginatedList<T>>>): Observable<T[]> => + source.pipe( + getSucceededRemoteData(), + getRemoteDataPayload(), + getPaginatedListPayload() + ); + +/** + * Get all successful remotely retrieved paginated lists + * as arrays + * + * You usually don't want to use this, it is a code smell. + * Work with the RemoteData object instead, that way you can + * handle loading and errors correctly. + * + * You also don't want to ignore pagination and simply use the + * page as an array. + * + * These operators were created as a first step in refactoring + * out all the instances where this is used incorrectly. + */ +export const getAllSucceededRemoteListPayload = () => + <T>(source: Observable<RemoteData<PaginatedList<T>>>): Observable<T[]> => + source.pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + getPaginatedListPayload() + ); + /** * Operator that checks if a remote data object contains a page not found error * When it does contain such an error, it will redirect the user to a page not found, without altering the current URL diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html index 18ff77bf23..af339109c6 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-issue/journal-issue-search-result-grid-element.component.html @@ -2,13 +2,13 @@ <div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'"> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> <div> - <ds-grid-thumbnail [thumbnail]="dso.getThumbnail() | async"> + <ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> </ds-grid-thumbnail> </div> </a> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <div> - <ds-grid-thumbnail [thumbnail]="dso.getThumbnail() | async"> + <ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> </ds-grid-thumbnail> </div> </span> diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html index 07e50eb6fb..2c7f24662a 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal-volume/journal-volume-search-result-grid-element.component.html @@ -2,13 +2,13 @@ <div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'"> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> <div> - <ds-grid-thumbnail [thumbnail]="dso.getThumbnail() | async"> + <ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> </ds-grid-thumbnail> </div> </a> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <div> - <ds-grid-thumbnail [thumbnail]="dso.getThumbnail() | async"> + <ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> </ds-grid-thumbnail> </div> </span> diff --git a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html index 394e5241e1..d6b9c4a62e 100644 --- a/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html +++ b/src/app/entity-groups/journal-entities/item-grid-elements/search-result-grid-elements/journal/journal-search-result-grid-element.component.html @@ -2,13 +2,13 @@ <div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'"> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> <div> - <ds-grid-thumbnail [thumbnail]="dso.getThumbnail() | async"> + <ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> </ds-grid-thumbnail> </div> </a> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <div> - <ds-grid-thumbnail [thumbnail]="dso.getThumbnail() | async"> + <ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> </ds-grid-thumbnail> </div> </span> 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 87312f8784..cdfa6293c4 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 @@ -4,7 +4,7 @@ <div class="row"> <div class="col-xs-12 col-md-4"> <ds-metadata-field-wrapper> - <ds-thumbnail [thumbnail]="object.getThumbnail() | async"></ds-thumbnail> + <ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail> </ds-metadata-field-wrapper> <ds-generic-item-page-field [item]="object" [fields]="['publicationvolume.volumeNumber']" 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 e77b24a98b..5df1997f0b 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 @@ -4,7 +4,7 @@ <div class="row"> <div class="col-xs-12 col-md-4"> <ds-metadata-field-wrapper> - <ds-thumbnail [thumbnail]="object.getThumbnail() | async"></ds-thumbnail> + <ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail> </ds-metadata-field-wrapper> <ds-generic-item-page-field [item]="object" [fields]="['publicationvolume.volumeNumber']" 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 e86ab35e0e..d8d32df04a 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 @@ -4,7 +4,7 @@ <div class="row"> <div class="col-xs-12 col-md-4"> <ds-metadata-field-wrapper> - <ds-thumbnail [thumbnail]="object.getThumbnail() | async"></ds-thumbnail> + <ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail> </ds-metadata-field-wrapper> <ds-generic-item-page-field class="item-page-fields" [item]="object" [fields]="['creativeworkseries.issn']" diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html index 5c42be2b24..0fb1ec02f8 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/org-unit/org-unit-search-result-grid-element.component.html @@ -2,13 +2,13 @@ <div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'"> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> <div> - <ds-grid-thumbnail [thumbnail]="dso.getThumbnail() | async"> + <ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> </ds-grid-thumbnail> </div> </a> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <div> - <ds-grid-thumbnail [thumbnail]="dso.getThumbnail() | async"> + <ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> </ds-grid-thumbnail> </div> </span> diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html index b7eed7c8b4..321ecd4a47 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/person/person-search-result-grid-element.component.html @@ -3,13 +3,13 @@ <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> <div> - <ds-grid-thumbnail [thumbnail]="dso.getThumbnail() | async"> + <ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> </ds-grid-thumbnail> </div> </a> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <div> - <ds-grid-thumbnail [thumbnail]="dso.getThumbnail() | async"> + <ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> </ds-grid-thumbnail> </div> </span> diff --git a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html index f3a0dea81f..c39de6bc2a 100644 --- a/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html +++ b/src/app/entity-groups/research-entities/item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component.html @@ -2,13 +2,13 @@ <div class="card" [@focusShadow]="(isCollapsed$ | async)?'blur':'focus'"> <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> <div> - <ds-grid-thumbnail [thumbnail]="dso.getThumbnail() | async"> + <ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> </ds-grid-thumbnail> </div> </a> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <div> - <ds-grid-thumbnail [thumbnail]="dso.getThumbnail() | async"> + <ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> </ds-grid-thumbnail> </div> </span> diff --git a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html index 1b23d567f5..784000b446 100644 --- a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html +++ b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html @@ -4,7 +4,7 @@ <div class="row"> <div class="col-xs-12 col-md-4"> <ds-metadata-field-wrapper> - <ds-thumbnail [thumbnail]="object.getThumbnail() | async" [defaultImage]="'assets/images/orgunit-placeholder.svg'"></ds-thumbnail> + <ds-thumbnail [thumbnail]="getThumbnail() | async" [defaultImage]="'assets/images/orgunit-placeholder.svg'"></ds-thumbnail> </ds-metadata-field-wrapper> <ds-generic-item-page-field [item]="object" [fields]="['organization.foundingDate']" 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 97a3cf416e..af71d5a1c6 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 @@ -4,7 +4,7 @@ <div class="row"> <div class="col-xs-12 col-md-4"> <ds-metadata-field-wrapper> - <ds-thumbnail [thumbnail]="this.object.getThumbnail() | async" [defaultImage]="'assets/images/person-placeholder.svg'"></ds-thumbnail> + <ds-thumbnail [thumbnail]="getThumbnail() | async" [defaultImage]="'assets/images/person-placeholder.svg'"></ds-thumbnail> </ds-metadata-field-wrapper> <ds-generic-item-page-field [item]="object" [fields]="['person.email']" 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 bb6ce21cd1..6c4a0bab01 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 @@ -4,7 +4,7 @@ <div class="row"> <div class="col-xs-12 col-md-4"> <ds-metadata-field-wrapper> - <ds-thumbnail [thumbnail]="object.getThumbnail() | async" [defaultImage]="'assets/images/project-placeholder.svg'"></ds-thumbnail> + <ds-thumbnail [thumbnail]="getThumbnail() | async" [defaultImage]="'assets/images/project-placeholder.svg'"></ds-thumbnail> </ds-metadata-field-wrapper> <!--<ds-generic-item-page-field [item]="object"--> <!--[fields]="['project.identifier.status']"--> diff --git a/src/app/shared/comcol-page-logo/comcol-page-logo.component.html b/src/app/shared/comcol-page-logo/comcol-page-logo.component.html index 4bd7369f06..057c358223 100644 --- a/src/app/shared/comcol-page-logo/comcol-page-logo.component.html +++ b/src/app/shared/comcol-page-logo/comcol-page-logo.component.html @@ -1,3 +1,3 @@ <div *ngIf="logo" class="dso-logo mb-3"> - <img [src]="logo.content" class="img-fluid" [attr.alt]="alternateText ? alternateText : null" (error)="errorHandler($event)"/> + <img [src]="logo._links.content.href" class="img-fluid" [attr.alt]="alternateText ? alternateText : null" (error)="errorHandler($event)"/> </div> diff --git a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts index 9a8f5ebeec..859a13c0ac 100644 --- a/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts +++ b/src/app/shared/object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview.component.ts @@ -1,9 +1,18 @@ import { Component, Input } from '@angular/core'; import { Observable } from 'rxjs'; -import { first } from 'rxjs/operators'; +import { first, map } from 'rxjs/operators'; +import { BitstreamDataService } from '../../../../core/data/bitstream-data.service'; +import { RemoteData } from '../../../../core/data/remote-data'; import { Item } from '../../../../core/shared/item.model'; +import { + getAllSucceededRemoteListPayload, + getFirstSucceededRemoteDataPayload, + getFirstSucceededRemoteListPayload, + getRemoteDataPayload, + getSucceededRemoteData +} from '../../../../core/shared/operators'; import { MyDspaceItemStatusType } from '../../../object-collection/shared/mydspace-item-status/my-dspace-item-status-type'; import { fadeInOut } from '../../../animations/fade'; import { Bitstream } from '../../../../core/shared/bitstream.model'; @@ -64,15 +73,16 @@ export class ItemDetailPreviewComponent { * @param {HALEndpointService} halService */ constructor(private fileService: FileService, - private halService: HALEndpointService) { + private halService: HALEndpointService, + private bitstreamDataService: BitstreamDataService) { } /** * Initialize all instance variables */ ngOnInit() { - this.thumbnail$ = this.item.getThumbnail(); - this.bitstreams$ = this.item.getFiles(); + this.thumbnail$ = this.getThumbnail(); + this.bitstreams$ = this.getFiles(); } /** @@ -86,4 +96,20 @@ export class ItemDetailPreviewComponent { this.fileService.downloadFile(fileUrl); }); } + + // TODO refactor this method to return RemoteData, and the template to deal with loading and errors + getThumbnail(): Observable<Bitstream> { + return this.bitstreamDataService.getThumbnailFor(this.item).pipe( + getFirstSucceededRemoteDataPayload() + ); + } + + // TODO refactor this method to return RemoteData, and the template to deal with loading and errors + getFiles(): Observable<Bitstream[]> { + return this.bitstreamDataService + .findAllByItemAndBundleName(this.item, 'ORIGINAL', { elementsPerPage: Number.MAX_SAFE_INTEGER }) + .pipe( + getFirstSucceededRemoteListPayload() + ); + } } diff --git a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts index 6ae0c2d37e..2c8c29ec85 100644 --- a/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts +++ b/src/app/shared/object-grid/grid-thumbnail/grid-thumbnail.component.ts @@ -30,8 +30,8 @@ export class GridThumbnailComponent implements OnInit { } ngOnInit(): void { - if (hasValue(this.thumbnail) && this.thumbnail.content) { - this.src = this.thumbnail.content; + if (hasValue(this.thumbnail) && this.thumbnail._links.content.href) { + this.src = this.thumbnail._links.content.href; } else { this.src = this.defaultImage } diff --git a/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.html b/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.html index a00e30cbcd..41c16c6eab 100644 --- a/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.html +++ b/src/app/shared/object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component.html @@ -3,13 +3,13 @@ <a *ngIf="linkType != linkTypes.None" [target]="(linkType == linkTypes.ExternalLink) ? '_blank' : '_self'" rel="noopener noreferrer" [routerLink]="['/items/' + dso.id]" class="card-img-top full-width"> <div> - <ds-grid-thumbnail [thumbnail]="this.dso.getThumbnail() | async"> + <ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> </ds-grid-thumbnail> </div> </a> <span *ngIf="linkType == linkTypes.None" class="card-img-top full-width"> <div> - <ds-grid-thumbnail [thumbnail]="dso.getThumbnail() | async"> + <ds-grid-thumbnail [thumbnail]="getThumbnail() | async"> </ds-grid-thumbnail> </div> </span> diff --git a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts index 8587e91302..dc05f78e40 100644 --- a/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts +++ b/src/app/shared/object-grid/search-result-grid-element/search-result-grid-element.component.ts @@ -1,12 +1,15 @@ -import { Component, Inject, OnInit } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { Observable } from 'rxjs'; import { SearchResult } from '../../search/search-result.model'; +import { BitstreamDataService } from '../../../core/data/bitstream-data.service'; +import { Bitstream } from '../../../core/shared/bitstream.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; -import { TruncatableService } from '../../truncatable/truncatable.service'; -import { Observable } from 'rxjs'; import { Metadata } from '../../../core/shared/metadata.utils'; +import { getFirstSucceededRemoteDataPayload } from '../../../core/shared/operators'; import { hasValue } from '../../empty.util'; +import { AbstractListableElementComponent } from '../../object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { TruncatableService } from '../../truncatable/truncatable.service'; @Component({ selector: 'ds-search-result-grid-element', @@ -24,7 +27,10 @@ export class SearchResultGridElementComponent<T extends SearchResult<K>, K exten */ isCollapsed$: Observable<boolean>; - public constructor(protected truncatableService: TruncatableService) { + public constructor( + protected truncatableService: TruncatableService, + protected bitstreamDataService: BitstreamDataService + ) { super(); if (hasValue(this.object)) { this.isCollapsed$ = this.isCollapsed(); @@ -63,4 +69,11 @@ export class SearchResultGridElementComponent<T extends SearchResult<K>, K exten private isCollapsed(): Observable<boolean> { return this.truncatableService.isCollapsed(this.dso.id); } + + // TODO refactor to return RemoteData, and thumbnail template to deal with loading + getThumbnail(): Observable<Bitstream> { + return this.bitstreamDataService.getThumbnailFor(this.dso as any).pipe( + getFirstSucceededRemoteDataPayload() + ); + } } diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index e31e907b47..73278c7c79 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -13,9 +13,22 @@ import { hasValue } from '../shared/empty.util'; styleUrls: ['./thumbnail.component.scss'], templateUrl: './thumbnail.component.html' }) -export class ThumbnailComponent implements OnInit { +export class ThumbnailComponent { - @Input() thumbnail: Bitstream; + private _thumbnail: Bitstream; + + get thumbnail(): Bitstream { + return this._thumbnail; + }; + + @Input() set thumbnail(t: Bitstream) { + this._thumbnail = t; + if (hasValue(this.thumbnail) && hasValue(this.thumbnail._links.content) && this.thumbnail._links.content.href) { + this.src = this.thumbnail._links.content.href; + } else { + this.src = this.defaultImage + } + } /** * The default 'holder.js' image @@ -23,16 +36,8 @@ export class ThumbnailComponent implements OnInit { @Input() defaultImage? = 'data:image/svg+xml;charset=UTF-8,%3Csvg%20xmlns%3D%22http%3A%2F%2Fwww.w3.org%2F2000%2Fsvg%22%20width%3D%2293%22%20height%3D%22120%22%20viewBox%3D%220%200%2093%20120%22%20preserveAspectRatio%3D%22none%22%3E%3C!--%0ASource%20URL%3A%20holder.js%2F93x120%3Ftext%3DNo%20Thumbnail%0ACreated%20with%20Holder.js%202.8.2.%0ALearn%20more%20at%20http%3A%2F%2Fholderjs.com%0A(c)%202012-2015%20Ivan%20Malopinsky%20-%20http%3A%2F%2Fimsky.co%0A--%3E%3Cdefs%3E%3Cstyle%20type%3D%22text%2Fcss%22%3E%3C!%5BCDATA%5B%23holder_1543e460b05%20text%20%7B%20fill%3A%23AAAAAA%3Bfont-weight%3Abold%3Bfont-family%3AArial%2C%20Helvetica%2C%20Open%20Sans%2C%20sans-serif%2C%20monospace%3Bfont-size%3A10pt%20%7D%20%5D%5D%3E%3C%2Fstyle%3E%3C%2Fdefs%3E%3Cg%20id%3D%22holder_1543e460b05%22%3E%3Crect%20width%3D%2293%22%20height%3D%22120%22%20fill%3D%22%23FFFFFF%22%2F%3E%3Cg%3E%3Ctext%20x%3D%2235.6171875%22%20y%3D%2257%22%3ENo%3C%2Ftext%3E%3Ctext%20x%3D%2210.8125%22%20y%3D%2272%22%3EThumbnail%3C%2Ftext%3E%3C%2Fg%3E%3C%2Fg%3E%3C%2Fsvg%3E'; src: string; + errorHandler(event) { event.currentTarget.src = this.defaultImage; } - - ngOnInit(): void { - if (hasValue(this.thumbnail) && this.thumbnail.content) { - this.src = this.thumbnail.content; - } else { - this.src = this.defaultImage - } - } - } 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 50b5fed9d3..adecd9e1af 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 @@ -4,7 +4,7 @@ a<div class="top-item-page"> <div class="col-12 col-md-2 d-flex flex-md-column justify-content-between"> <ds-metadata-field-wrapper> - <ds-thumbnail [thumbnail]="object.getThumbnail() | async"></ds-thumbnail> + <ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail> </ds-metadata-field-wrapper> <div> <a class="btn btn-secondary" diff --git a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html index 9fa80d9c3c..78a0eba13e 100644 --- a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html +++ b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-issue/journal-issue.component.html @@ -4,7 +4,7 @@ <div class="col-12 col-md-2 d-flex flex-md-column justify-content-between"> <ds-metadata-field-wrapper> - <ds-thumbnail [thumbnail]="object.getThumbnail() | async"></ds-thumbnail> + <ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail> </ds-metadata-field-wrapper> <div> <a class="btn btn-secondary" diff --git a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html index 907f70b941..3dca7dfbba 100644 --- a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html +++ b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html @@ -4,7 +4,7 @@ <div class="col-12 col-md-2 d-flex flex-md-column justify-content-between"> <ds-metadata-field-wrapper> - <ds-thumbnail [thumbnail]="object.getThumbnail() | async"></ds-thumbnail> + <ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail> </ds-metadata-field-wrapper> <div> <a class="btn btn-secondary" diff --git a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.html b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.html index 089511804e..ed542c7840 100644 --- a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.html +++ b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal/journal.component.html @@ -3,7 +3,7 @@ <div class="row"> <div class="col-12 col-md-2 d-flex flex-md-column justify-content-between"> <ds-metadata-field-wrapper> - <ds-thumbnail [thumbnail]="object.getThumbnail() | async"></ds-thumbnail> + <ds-thumbnail [thumbnail]="getThumbnail() | async"></ds-thumbnail> </ds-metadata-field-wrapper> <div> <a class="btn btn-secondary" diff --git a/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html b/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html index ee78d9c653..ae003e31a8 100644 --- a/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html +++ b/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html @@ -4,7 +4,7 @@ <div class="col-12 col-md-2 d-flex flex-md-column justify-content-between"> <ds-metadata-field-wrapper> - <ds-thumbnail [thumbnail]="object.getThumbnail() | async" + <ds-thumbnail [thumbnail]="getThumbnail() | async" [defaultImage]="'assets/images/orgunit-placeholder.svg'"></ds-thumbnail> </ds-metadata-field-wrapper> <div> diff --git a/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html b/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html index 1679f9354d..dbcb76a292 100644 --- a/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html +++ b/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html @@ -4,7 +4,7 @@ <div class="col-12 col-md-2 d-flex flex-md-column justify-content-between"> <ds-metadata-field-wrapper> - <ds-thumbnail [thumbnail]="object.getThumbnail() | async" + <ds-thumbnail [thumbnail]="getThumbnail() | async" [defaultImage]="'assets/images/person-placeholder.svg'"></ds-thumbnail> </ds-metadata-field-wrapper> <div> diff --git a/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html b/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html index 31ba79a158..b31353ef76 100644 --- a/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html +++ b/themes/mantis/app/entity-groups/research-entities/item-pages/project/project.component.html @@ -4,7 +4,7 @@ <div class="col-12 col-md-2 d-flex flex-md-column justify-content-between"> <ds-metadata-field-wrapper> - <ds-thumbnail [thumbnail]="object.getThumbnail() | async" + <ds-thumbnail [thumbnail]="getThumbnail() | async" [defaultImage]="'assets/images/project-placeholder.svg'"></ds-thumbnail> </ds-metadata-field-wrapper> <div> -- GitLab