refactored items, bundles and bitstreams, test builders

This commit is contained in:
Art Lowel
2019-12-11 17:18:08 +01:00
parent 8af72cb1d3
commit ad4e8eeb8c
58 changed files with 746 additions and 340 deletions

View File

@@ -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$,

View File

@@ -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>

View File

@@ -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));
}
}

View File

@@ -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>

View File

@@ -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();
}
)
)
)
);
}
}

View File

@@ -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>

View File

@@ -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()
);
}
}

View File

@@ -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>

View File

@@ -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()
);
}
}

View File

@@ -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;
}
}

View File

@@ -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);
};
}

View File

@@ -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
};
}
}
});

View File

@@ -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;
/**

View File

@@ -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[];
}

View File

@@ -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[];
}

View File

@@ -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[];
}

View File

@@ -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;

View File

@@ -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];
}
})
);
}
}

View File

@@ -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);
}
}

View File

@@ -1,23 +1,31 @@
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 { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { HttpClient } from '@angular/common/http';
import { DefaultChangeAnalyzer } from './default-change-analyzer.service';
import { FindListOptions } from './request.models';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
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 { 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 { 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;
}
}),
);
}
}

View File

@@ -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);
}
}

View File

@@ -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);
}

View File

@@ -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
*

View File

@@ -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();
}

View File

@@ -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),

View File

@@ -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);
}
}

View File

@@ -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 { RemoteData } from '../data/remote-data';
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 { catchError, distinctUntilKeyChanged, filter, first, map, take } from 'rxjs/operators';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { BitstreamFormat } from '../shared/bitstream-format.model';
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 { DSpaceObject } from '../shared/dspace-object.model';
import { Item } from '../shared/item.model';
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);
}
});
}

View File

@@ -0,0 +1,5 @@
export class HALLink {
href: string;
name?: string;
templated?: boolean
}

View File

@@ -1,8 +1,8 @@
import { DSpaceObject } from './dspace-object.model';
import { RemoteData } from '../data/remote-data';
import { Item } from './item.model';
import { BitstreamFormat } from './bitstream-format.model';
import { Observable } from 'rxjs';
import { RemoteData } from '../data/remote-data';
import { BitstreamFormat } from './bitstream-format.model';
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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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

View File

@@ -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

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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']"

View File

@@ -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']"

View File

@@ -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']"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>

View File

@@ -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']"

View File

@@ -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']"

View File

@@ -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']"-->

View File

@@ -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>

View File

@@ -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()
);
}
}

View File

@@ -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
}

View File

@@ -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>

View File

@@ -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 { 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';
import { Observable } from 'rxjs';
import { Metadata } from '../../../core/shared/metadata.utils';
import { hasValue } from '../../empty.util';
@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()
);
}
}

View File

@@ -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
}
}
}

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

@@ -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>

View File

@@ -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>

View File

@@ -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>