forked from hazza/dspace-angular
Merge pull request #117 from artlowel/dso-list-component
Live REST API & Dso list component
This commit is contained in:
@@ -2,10 +2,10 @@ module.exports = {
|
||||
// The REST API server settings.
|
||||
"rest": {
|
||||
"ssl": false,
|
||||
"address": "localhost",
|
||||
"port": 3000,
|
||||
"address": "dspace7.4science.it",
|
||||
"port": 80,
|
||||
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||
"nameSpace": "/api"
|
||||
"nameSpace": "/dspace-spring-rest/api"
|
||||
},
|
||||
// Angular2 UI server settings.
|
||||
"ui": {
|
||||
|
@@ -7,7 +7,12 @@
|
||||
"collection": {
|
||||
"page": {
|
||||
"news": "News",
|
||||
"license": "License"
|
||||
"license": "License",
|
||||
"browse": {
|
||||
"recent": {
|
||||
"head": "Recent Submissions"
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
"community": {
|
||||
@@ -45,6 +50,7 @@
|
||||
},
|
||||
"pagination": {
|
||||
"results-per-page": "Results Per Page",
|
||||
"sort-direction": "Sort Options",
|
||||
"showing": {
|
||||
"label": "Now showing items ",
|
||||
"detail": "{{ range }} of {{ total }}"
|
||||
|
@@ -1,4 +1,5 @@
|
||||
<div class="collection-page" *ngIf="collectionData.hasSucceeded | async">
|
||||
<div class="collection-page">
|
||||
<div *ngIf="collectionData.hasSucceeded | async">
|
||||
<!-- Collection Name -->
|
||||
<ds-comcol-page-header
|
||||
[name]="(collectionData.payload | async)?.name">
|
||||
@@ -30,3 +31,14 @@
|
||||
[title]="'collection.page.license'">
|
||||
</ds-comcol-page-content>
|
||||
</div>
|
||||
<br>
|
||||
<div *ngIf="itemData.hasSucceeded | async">
|
||||
<h2>{{'collection.page.browse.recent.head' | translate}}</h2>
|
||||
<ds-object-list [config]="config" [sortConfig]="sortConfig"
|
||||
[objects]="itemData" [hideGear]="true"
|
||||
(pageChange)="onPageChange($event)"
|
||||
(pageSizeChange)="onPageSizeChange($event)"
|
||||
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||
(sortFieldChange)="onSortDirectionChange($event)"></ds-object-list>
|
||||
</div>
|
||||
</div>
|
||||
|
@@ -1,4 +1,7 @@
|
||||
import { Component, OnDestroy, OnInit } from '@angular/core';
|
||||
import {
|
||||
ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy,
|
||||
OnInit
|
||||
} from '@angular/core';
|
||||
import { ActivatedRoute, Params } from '@angular/router';
|
||||
|
||||
import { Collection } from "../core/shared/collection.model";
|
||||
@@ -6,36 +9,93 @@ import { Bitstream } from "../core/shared/bitstream.model";
|
||||
import { RemoteData } from "../core/data/remote-data";
|
||||
import { CollectionDataService } from "../core/data/collection-data.service";
|
||||
import { Subscription } from "rxjs/Subscription";
|
||||
import { ItemDataService } from "../core/data/item-data.service";
|
||||
import { Item } from "../core/shared/item.model";
|
||||
import { SortOptions, SortDirection } from "../core/cache/models/sort-options.model";
|
||||
import { PaginationComponentOptions } from "../shared/pagination/pagination-component-options.model";
|
||||
import { Observable } from "rxjs/Observable";
|
||||
import { hasValue } from "../shared/empty.util";
|
||||
|
||||
@Component({
|
||||
selector: 'ds-collection-page',
|
||||
styleUrls: ['./collection-page.component.css'],
|
||||
templateUrl: './collection-page.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class CollectionPageComponent implements OnInit, OnDestroy {
|
||||
collectionData: RemoteData<Collection>;
|
||||
itemData: RemoteData<Item[]>;
|
||||
logoData: RemoteData<Bitstream>;
|
||||
config : PaginationComponentOptions;
|
||||
sortConfig : SortOptions;
|
||||
private subs: Subscription[] = [];
|
||||
private collectionId: string;
|
||||
|
||||
constructor(
|
||||
private collectionDataService: CollectionDataService,
|
||||
private itemDataService: ItemDataService,
|
||||
private ref: ChangeDetectorRef,
|
||||
private route: ActivatedRoute
|
||||
) {
|
||||
this.universalInit();
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.route.params.subscribe((params: Params) => {
|
||||
this.collectionData = this.collectionDataService.findById(params['id']);
|
||||
this.subs.push(this.route.params.map((params: Params) => params['id'] )
|
||||
.subscribe((id: string) => {
|
||||
this.collectionId = id;
|
||||
this.collectionData = this.collectionDataService.findById(this.collectionId);
|
||||
this.subs.push(this.collectionData.payload
|
||||
.subscribe(collection => this.logoData = collection.logo));
|
||||
});
|
||||
|
||||
this.config = new PaginationComponentOptions();
|
||||
this.config.id = "collection-browse";
|
||||
this.config.pageSizeOptions = [ 4 ];
|
||||
this.config.pageSize = 4;
|
||||
this.sortConfig = new SortOptions();
|
||||
|
||||
this.updateResults();
|
||||
}));
|
||||
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
this.subs
|
||||
.filter(sub => hasValue(sub))
|
||||
.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
universalInit() {
|
||||
}
|
||||
|
||||
onPageChange(currentPage: number): void {
|
||||
this.config.currentPage = currentPage;
|
||||
this.updateResults();
|
||||
}
|
||||
|
||||
onPageSizeChange(elementsPerPage: number): void {
|
||||
this.config.pageSize = elementsPerPage;
|
||||
this.updateResults();
|
||||
}
|
||||
|
||||
onSortDirectionChange(sortDirection: SortDirection): void {
|
||||
this.sortConfig = new SortOptions(this.sortConfig.field, sortDirection);
|
||||
this.updateResults();
|
||||
}
|
||||
|
||||
onSortFieldChange(field: string): void {
|
||||
this.sortConfig = new SortOptions(field, this.sortConfig.direction);
|
||||
this.updateResults();
|
||||
}
|
||||
|
||||
updateResults() {
|
||||
this.itemData = undefined;
|
||||
this.itemData = this.itemDataService.findAll({
|
||||
scopeID: this.collectionId,
|
||||
currentPage: this.config.currentPage,
|
||||
elementsPerPage: this.config.pageSize,
|
||||
sort: this.sortConfig
|
||||
});
|
||||
// this.ref.detectChanges();
|
||||
}
|
||||
}
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { NgModule } from '@angular/core';
|
||||
import { CommonModule } from '@angular/common';
|
||||
|
||||
import { TranslateModule } from "@ngx-translate/core";
|
||||
|
||||
import { SharedModule } from '../shared/shared.module';
|
||||
|
||||
import { CollectionPageComponent } from './collection-page.component';
|
||||
import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
||||
|
||||
@@ -11,8 +11,8 @@ import { CollectionPageRoutingModule } from './collection-page-routing.module';
|
||||
imports: [
|
||||
CollectionPageRoutingModule,
|
||||
CommonModule,
|
||||
TranslateModule,
|
||||
SharedModule,
|
||||
TranslateModule,
|
||||
],
|
||||
declarations: [
|
||||
CollectionPageComponent,
|
||||
|
@@ -6,6 +6,7 @@ import { Bitstream } from "../core/shared/bitstream.model";
|
||||
import { RemoteData } from "../core/data/remote-data";
|
||||
import { CommunityDataService } from "../core/data/community-data.service";
|
||||
import { Subscription } from "rxjs/Subscription";
|
||||
import { hasValue } from "../shared/empty.util";
|
||||
|
||||
@Component({
|
||||
selector: 'ds-community-page',
|
||||
@@ -33,7 +34,9 @@ export class CommunityPageComponent implements OnInit, OnDestroy {
|
||||
}
|
||||
|
||||
ngOnDestroy(): void {
|
||||
this.subs.forEach(sub => sub.unsubscribe());
|
||||
this.subs
|
||||
.filter(sub => hasValue(sub))
|
||||
.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
universalInit() {
|
||||
|
@@ -16,7 +16,7 @@ export const getMapsTo = function(target: any) {
|
||||
return Reflect.getOwnMetadata(mapsToMetadataKey, target);
|
||||
};
|
||||
|
||||
export const relationship = function(value: ResourceType): any {
|
||||
export const relationship = function(value: ResourceType, isList: boolean = false): any {
|
||||
return function (target: any, propertyKey: string, descriptor: PropertyDescriptor) {
|
||||
if (!target || !propertyKey) {
|
||||
return;
|
||||
@@ -28,11 +28,11 @@ export const relationship = function(value: ResourceType): any {
|
||||
}
|
||||
relationshipMap.set(target.constructor, metaDataList);
|
||||
|
||||
return Reflect.metadata(relationshipKey, value).apply(this, arguments);
|
||||
return Reflect.metadata(relationshipKey, { resourceType: value, isList }).apply(this, arguments);
|
||||
};
|
||||
};
|
||||
|
||||
export const getResourceType = function(target: any, propertyKey: string) {
|
||||
export const getRelationMetadata = function(target: any, propertyKey: string) {
|
||||
return Reflect.getMetadata(relationshipKey, target, propertyKey);
|
||||
};
|
||||
|
||||
|
@@ -12,7 +12,7 @@ import { ErrorResponse, SuccessResponse } from "../response-cache.models";
|
||||
import { Observable } from "rxjs/Observable";
|
||||
import { RemoteData } from "../../data/remote-data";
|
||||
import { GenericConstructor } from "../../shared/generic-constructor";
|
||||
import { getMapsTo, getResourceType, getRelationships } from "./build-decorators";
|
||||
import { getMapsTo, getRelationMetadata, getRelationships } from "./build-decorators";
|
||||
import { NormalizedObjectFactory } from "../models/normalized-object-factory";
|
||||
import { Request } from "../../data/request.models";
|
||||
|
||||
@@ -55,17 +55,54 @@ export class RemoteDataBuildService {
|
||||
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
|
||||
.distinctUntilChanged();
|
||||
|
||||
const payload = this.objectCache.getBySelfLink<TNormalized>(href, normalizedType)
|
||||
const statusCode = responseCacheObs
|
||||
.map((entry: ResponseCacheEntry) => entry.response.statusCode)
|
||||
.distinctUntilChanged();
|
||||
|
||||
const pageInfo = responseCacheObs
|
||||
.filter((entry: ResponseCacheEntry) => hasValue(entry.response) && hasValue(entry.response['pageInfo']))
|
||||
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).pageInfo)
|
||||
.distinctUntilChanged();
|
||||
|
||||
//always use self link if that is cached, only if it isn't, get it via the response.
|
||||
const payload =
|
||||
Observable.combineLatest(
|
||||
this.objectCache.getBySelfLink<TNormalized>(href, normalizedType).startWith(undefined),
|
||||
responseCacheObs
|
||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
||||
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
|
||||
.flatMap((resourceUUIDs: Array<string>) => {
|
||||
if (isNotEmpty(resourceUUIDs)) {
|
||||
return this.objectCache.get(resourceUUIDs[0], normalizedType);
|
||||
}
|
||||
else {
|
||||
return Observable.of(undefined);
|
||||
}
|
||||
})
|
||||
.distinctUntilChanged()
|
||||
.startWith(undefined),
|
||||
(fromSelfLink, fromResponse) => {
|
||||
if (hasValue(fromSelfLink)) {
|
||||
return fromSelfLink;
|
||||
}
|
||||
else {
|
||||
return fromResponse;
|
||||
}
|
||||
}
|
||||
).filter(normalized => hasValue(normalized))
|
||||
.map((normalized: TNormalized) => {
|
||||
return this.build<TNormalized, TDomain>(normalized);
|
||||
});
|
||||
|
||||
|
||||
return new RemoteData(
|
||||
href,
|
||||
requestPending,
|
||||
responsePending,
|
||||
isSuccessFul,
|
||||
errorMessage,
|
||||
statusCode,
|
||||
pageInfo,
|
||||
payload
|
||||
);
|
||||
}
|
||||
@@ -90,6 +127,15 @@ export class RemoteDataBuildService {
|
||||
.map((entry: ResponseCacheEntry) => (<ErrorResponse> entry.response).errorMessage)
|
||||
.distinctUntilChanged();
|
||||
|
||||
const statusCode = responseCacheObs
|
||||
.map((entry: ResponseCacheEntry) => entry.response.statusCode)
|
||||
.distinctUntilChanged();
|
||||
|
||||
const pageInfo = responseCacheObs
|
||||
.filter((entry: ResponseCacheEntry) => hasValue(entry.response) && hasValue(entry.response['pageInfo']))
|
||||
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).pageInfo)
|
||||
.distinctUntilChanged();
|
||||
|
||||
const payload = responseCacheObs
|
||||
.filter((entry: ResponseCacheEntry) => entry.response.isSuccessful)
|
||||
.map((entry: ResponseCacheEntry) => (<SuccessResponse> entry.response).resourceUUIDs)
|
||||
@@ -109,6 +155,8 @@ export class RemoteDataBuildService {
|
||||
responsePending,
|
||||
isSuccessFul,
|
||||
errorMessage,
|
||||
statusCode,
|
||||
pageInfo,
|
||||
payload
|
||||
);
|
||||
}
|
||||
@@ -121,7 +169,7 @@ export class RemoteDataBuildService {
|
||||
|
||||
relationships.forEach((relationship: string) => {
|
||||
if (hasValue(normalized[relationship])) {
|
||||
const resourceType = getResourceType(normalized, relationship);
|
||||
const { resourceType, isList } = getRelationMetadata(normalized, relationship);
|
||||
const resourceConstructor = NormalizedObjectFactory.getConstructor(resourceType);
|
||||
if (Array.isArray(normalized[relationship])) {
|
||||
// without the setTimeout, the actions inside requestService.configure
|
||||
@@ -137,8 +185,13 @@ export class RemoteDataBuildService {
|
||||
rdArr.push(this.buildSingle(href, resourceConstructor));
|
||||
});
|
||||
|
||||
if (rdArr.length === 1) {
|
||||
links[relationship] = rdArr[0];
|
||||
}
|
||||
else {
|
||||
links[relationship] = this.aggregate(rdArr);
|
||||
}
|
||||
}
|
||||
else {
|
||||
// without the setTimeout, the actions inside requestService.configure
|
||||
// are dispatched, but sometimes don't arrive. I'm unsure why atm.
|
||||
@@ -146,9 +199,16 @@ export class RemoteDataBuildService {
|
||||
this.requestService.configure(new Request(normalized[relationship]));
|
||||
},0);
|
||||
|
||||
// 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) {
|
||||
links[relationship] = this.buildList(normalized[relationship], resourceConstructor);
|
||||
} else {
|
||||
links[relationship] = this.buildSingle(normalized[relationship], resourceConstructor);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
const domainModel = getMapsTo(normalized.constructor);
|
||||
@@ -183,6 +243,20 @@ export class RemoteDataBuildService {
|
||||
.join(", ")
|
||||
);
|
||||
|
||||
const statusCode = Observable.combineLatest(
|
||||
...input.map(rd => rd.statusCode),
|
||||
).map((...statusCodes) => statusCodes
|
||||
.map((code, idx) => {
|
||||
if (hasValue(code)) {
|
||||
return `[${idx}]: ${code}`;
|
||||
}
|
||||
})
|
||||
.filter(c => hasValue(c))
|
||||
.join(", ")
|
||||
);
|
||||
|
||||
const pageInfo = Observable.of(undefined);
|
||||
|
||||
const payload = <Observable<T[]>> Observable.combineLatest(
|
||||
...input.map(rd => rd.payload)
|
||||
);
|
||||
@@ -196,6 +270,8 @@ export class RemoteDataBuildService {
|
||||
responsePending,
|
||||
isSuccessFul,
|
||||
errorMessage,
|
||||
statusCode,
|
||||
pageInfo,
|
||||
payload
|
||||
);
|
||||
}
|
||||
|
11
src/app/core/cache/models/normalized-bitstream-format.model.ts
vendored
Normal file
11
src/app/core/cache/models/normalized-bitstream-format.model.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
||||
import { NormalizedObject } from "./normalized-object.model";
|
||||
import { inheritSerialization } from "cerialize";
|
||||
|
||||
@inheritSerialization(NormalizedObject)
|
||||
export class NormalizedBitstreamFormat extends NormalizedObject {
|
||||
//TODO this class was created as a placeholder when we connected to the live rest api
|
||||
|
||||
get uuid(): string {
|
||||
return this.self;
|
||||
}
|
||||
}
|
@@ -1,23 +1,24 @@
|
||||
import { inheritSerialization, autoserialize } from "cerialize";
|
||||
import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
|
||||
import { Bitstream } from "../../shared/bitstream.model";
|
||||
import { mapsTo } from "../builders/build-decorators";
|
||||
import { mapsTo, relationship } from "../builders/build-decorators";
|
||||
import { ResourceType } from "../../shared/resource-type";
|
||||
|
||||
@mapsTo(Bitstream)
|
||||
@inheritSerialization(NormalizedDSpaceObject)
|
||||
export class NormalizedBitstream extends NormalizedDSpaceObject {
|
||||
|
||||
/**
|
||||
* The size of this bitstream in bytes(?)
|
||||
* The size of this bitstream in bytes
|
||||
*/
|
||||
@autoserialize
|
||||
size: number;
|
||||
sizeBytes: number;
|
||||
|
||||
/**
|
||||
* The relative path to this Bitstream's file
|
||||
*/
|
||||
@autoserialize
|
||||
url: string;
|
||||
retrieve: string;
|
||||
|
||||
/**
|
||||
* The mime type of this Bitstream
|
||||
@@ -28,23 +29,32 @@ export class NormalizedBitstream extends NormalizedDSpaceObject {
|
||||
/**
|
||||
* The format of this Bitstream
|
||||
*/
|
||||
@autoserialize
|
||||
format: string;
|
||||
|
||||
/**
|
||||
* The description of this Bitstream
|
||||
*/
|
||||
@autoserialize
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* An array of Bundles that are direct parents of this Bitstream
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Item, true)
|
||||
parents: Array<string>;
|
||||
|
||||
/**
|
||||
* The Bundle that owns this Bitstream
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Item, false)
|
||||
owner: string;
|
||||
|
||||
/**
|
||||
* The name of the Bundle this Bitstream is part of
|
||||
*/
|
||||
@autoserialize
|
||||
retrieve: string;
|
||||
bundleName: string;
|
||||
}
|
||||
|
@@ -11,7 +11,7 @@ export class NormalizedBundle extends NormalizedDSpaceObject {
|
||||
* The primary bitstream of this Bundle
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Bitstream)
|
||||
@relationship(ResourceType.Bitstream, false)
|
||||
primaryBitstream: string;
|
||||
|
||||
/**
|
||||
@@ -25,6 +25,6 @@ export class NormalizedBundle extends NormalizedDSpaceObject {
|
||||
owner: string;
|
||||
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Bitstream)
|
||||
@relationship(ResourceType.Bitstream, true)
|
||||
bitstreams: Array<string>;
|
||||
}
|
||||
|
@@ -18,21 +18,25 @@ export class NormalizedCollection extends NormalizedDSpaceObject {
|
||||
* The Bitstream that represents the logo of this Collection
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Bitstream)
|
||||
@relationship(ResourceType.Bitstream, false)
|
||||
logo: string;
|
||||
|
||||
/**
|
||||
* An array of Collections that are direct parents of this Collection
|
||||
* An array of Communities that are direct parents of this Collection
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Community, true)
|
||||
parents: Array<string>;
|
||||
|
||||
/**
|
||||
* The Collection that owns this Collection
|
||||
* The Community that owns this Collection
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Community, false)
|
||||
owner: string;
|
||||
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Item)
|
||||
@relationship(ResourceType.Item, true)
|
||||
items: Array<string>;
|
||||
|
||||
}
|
||||
|
@@ -18,21 +18,25 @@ export class NormalizedCommunity extends NormalizedDSpaceObject {
|
||||
* The Bitstream that represents the logo of this Community
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Bitstream)
|
||||
@relationship(ResourceType.Bitstream, false)
|
||||
logo: string;
|
||||
|
||||
/**
|
||||
* An array of Communities that are direct parents of this Community
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Community, true)
|
||||
parents: Array<string>;
|
||||
|
||||
/**
|
||||
* The Community that owns this Community
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Community, false)
|
||||
owner: string;
|
||||
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Collection)
|
||||
@relationship(ResourceType.Collection, true)
|
||||
collections: Array<string>;
|
||||
|
||||
}
|
||||
|
@@ -1,24 +1,36 @@
|
||||
import { autoserialize, autoserializeAs } from "cerialize";
|
||||
import { CacheableObject } from "../object-cache.reducer";
|
||||
import { autoserialize, autoserializeAs, inheritSerialization } from "cerialize";
|
||||
import { Metadatum } from "../../shared/metadatum.model";
|
||||
import { ResourceType } from "../../shared/resource-type";
|
||||
import { NormalizedObject } from "./normalized-object.model";
|
||||
|
||||
/**
|
||||
* An abstract model class for a DSpaceObject.
|
||||
*/
|
||||
export abstract class NormalizedDSpaceObject implements CacheableObject {
|
||||
export abstract class NormalizedDSpaceObject extends NormalizedObject{
|
||||
|
||||
/**
|
||||
* The link to the rest endpoint where this object can be found
|
||||
*
|
||||
* Repeated here to make the serialization work,
|
||||
* inheritSerialization doesn't seem to work for more than one level
|
||||
*/
|
||||
@autoserialize
|
||||
self: string;
|
||||
|
||||
/**
|
||||
* The human-readable identifier of this DSpaceObject
|
||||
*
|
||||
* Currently mapped to uuid but left in to leave room
|
||||
* for a shorter, more user friendly type of id
|
||||
*/
|
||||
@autoserialize
|
||||
@autoserializeAs(String, 'uuid')
|
||||
id: string;
|
||||
|
||||
/**
|
||||
* The universally unique identifier of this DSpaceObject
|
||||
*
|
||||
* Repeated here to make the serialization work,
|
||||
* inheritSerialization doesn't seem to work for more than one level
|
||||
*/
|
||||
@autoserialize
|
||||
uuid: string;
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { inheritSerialization, autoserialize } from "cerialize";
|
||||
import { inheritSerialization, autoserialize, autoserializeAs } from "cerialize";
|
||||
import { NormalizedDSpaceObject } from "./normalized-dspace-object.model";
|
||||
import { Item } from "../../shared/item.model";
|
||||
import { mapsTo, relationship } from "../builders/build-decorators";
|
||||
@@ -17,31 +17,41 @@ export class NormalizedItem extends NormalizedDSpaceObject {
|
||||
/**
|
||||
* The Date of the last modification of this Item
|
||||
*/
|
||||
@autoserialize
|
||||
lastModified: Date;
|
||||
|
||||
/**
|
||||
* A boolean representing if this Item is currently archived or not
|
||||
*/
|
||||
@autoserializeAs(Boolean, 'inArchive')
|
||||
isArchived: boolean;
|
||||
|
||||
/**
|
||||
* A boolean representing if this Item is currently discoverable or not
|
||||
*/
|
||||
@autoserializeAs(Boolean, 'discoverable')
|
||||
isDiscoverable: boolean;
|
||||
|
||||
/**
|
||||
* A boolean representing if this Item is currently withdrawn or not
|
||||
*/
|
||||
@autoserializeAs(Boolean, 'withdrawn')
|
||||
isWithdrawn: boolean;
|
||||
|
||||
/**
|
||||
* An array of Collections that are direct parents of this Item
|
||||
*/
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Collection)
|
||||
@relationship(ResourceType.Collection, true)
|
||||
parents: Array<string>;
|
||||
|
||||
/**
|
||||
* The Collection that owns this Item
|
||||
*/
|
||||
owner: string;
|
||||
@relationship(ResourceType.Collection, false)
|
||||
owningCollection: string;
|
||||
|
||||
@autoserialize
|
||||
@relationship(ResourceType.Bundle)
|
||||
bundles: Array<string>;
|
||||
@relationship(ResourceType.Bitstream, true)
|
||||
bitstreams: Array<string>;
|
||||
}
|
||||
|
@@ -6,13 +6,20 @@ import { NormalizedCollection } from "./normalized-collection.model";
|
||||
import { GenericConstructor } from "../../shared/generic-constructor";
|
||||
import { NormalizedCommunity } from "./normalized-community.model";
|
||||
import { ResourceType } from "../../shared/resource-type";
|
||||
import { NormalizedObject } from "./normalized-object.model";
|
||||
import { NormalizedBitstreamFormat } from "./normalized-bitstream-format.model";
|
||||
|
||||
export class NormalizedObjectFactory {
|
||||
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedDSpaceObject> {
|
||||
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject> {
|
||||
switch (type) {
|
||||
case ResourceType.Bitstream: {
|
||||
return NormalizedBitstream
|
||||
}
|
||||
// commented out for now, bitstreamformats aren't used in the UI yet
|
||||
// and slow things down noticeably
|
||||
// case ResourceType.BitstreamFormat: {
|
||||
// return NormalizedBitstreamFormat
|
||||
// }
|
||||
case ResourceType.Bundle: {
|
||||
return NormalizedBundle
|
||||
}
|
||||
|
20
src/app/core/cache/models/normalized-object.model.ts
vendored
Normal file
20
src/app/core/cache/models/normalized-object.model.ts
vendored
Normal file
@@ -0,0 +1,20 @@
|
||||
import { CacheableObject } from "../object-cache.reducer";
|
||||
import { autoserialize } from "cerialize";
|
||||
/**
|
||||
* An abstract model class for a NormalizedObject.
|
||||
*/
|
||||
export abstract class NormalizedObject implements CacheableObject {
|
||||
|
||||
/**
|
||||
* The link to the rest endpoint where this object can be found
|
||||
*/
|
||||
@autoserialize
|
||||
self: string;
|
||||
|
||||
/**
|
||||
* The universally unique identifier of this Object
|
||||
*/
|
||||
@autoserialize
|
||||
uuid: string;
|
||||
|
||||
}
|
@@ -4,6 +4,8 @@ export enum SortDirection {
|
||||
}
|
||||
|
||||
export class SortOptions {
|
||||
field: string = "id";
|
||||
direction: SortDirection = SortDirection.Ascending
|
||||
|
||||
constructor (public field: string = "name", public direction : SortDirection = SortDirection.Ascending) {
|
||||
|
||||
}
|
||||
}
|
||||
|
@@ -20,6 +20,7 @@ describe("ObjectCacheService", () => {
|
||||
let store: Store<ObjectCacheState>;
|
||||
|
||||
const uuid = '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
const requestHref = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||
const timestamp = new Date().getTime();
|
||||
const msToLive = 900000;
|
||||
const objectToCache = {
|
||||
@@ -44,8 +45,8 @@ describe("ObjectCacheService", () => {
|
||||
describe("add", () => {
|
||||
it("should dispatch an ADD action with the object to add, the time to live, and the current timestamp", () => {
|
||||
|
||||
service.add(objectToCache, msToLive);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive));
|
||||
service.add(objectToCache, msToLive, requestHref);
|
||||
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestHref));
|
||||
});
|
||||
});
|
||||
|
||||
|
20
src/app/core/cache/response-cache.models.ts
vendored
20
src/app/core/cache/response-cache.models.ts
vendored
@@ -1,18 +1,28 @@
|
||||
import { RequestError } from "../data/request.models";
|
||||
import { PageInfo } from "../shared/page-info.model";
|
||||
|
||||
export class Response {
|
||||
constructor(public isSuccessful: boolean) {}
|
||||
constructor(
|
||||
public isSuccessful: boolean,
|
||||
public statusCode: string
|
||||
) {}
|
||||
}
|
||||
|
||||
export class SuccessResponse extends Response {
|
||||
constructor(public resourceUUIDs: Array<String>) {
|
||||
super(true);
|
||||
constructor(
|
||||
public resourceUUIDs: Array<String>,
|
||||
public statusCode: string,
|
||||
public pageInfo?: PageInfo
|
||||
) {
|
||||
super(true, statusCode);
|
||||
}
|
||||
}
|
||||
|
||||
export class ErrorResponse extends Response {
|
||||
errorMessage: string;
|
||||
|
||||
constructor(error: Error) {
|
||||
super(false);
|
||||
constructor(error: RequestError) {
|
||||
super(false, error.statusText);
|
||||
console.error(error);
|
||||
this.errorMessage = error.message;
|
||||
}
|
||||
|
@@ -12,7 +12,7 @@ import { ItemDataService } from "./data/item-data.service";
|
||||
import { RequestService } from "./data/request.service";
|
||||
import { RemoteDataBuildService } from "./cache/builders/remote-data-build.service";
|
||||
import { CommunityDataService } from "./data/community-data.service";
|
||||
import { PaginationOptions } from "./cache/models/pagination-options.model";
|
||||
import { PaginationComponentOptions } from "../shared/pagination/pagination-component-options.model";
|
||||
|
||||
const IMPORTS = [
|
||||
CommonModule,
|
||||
@@ -33,7 +33,7 @@ const PROVIDERS = [
|
||||
ItemDataService,
|
||||
DSpaceRESTv2Service,
|
||||
ObjectCacheService,
|
||||
PaginationOptions,
|
||||
PaginationComponentOptions,
|
||||
ResponseCacheService,
|
||||
RequestService,
|
||||
RemoteDataBuildService
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Inject, Injectable } from "@angular/core";
|
||||
import { DataService } from "./data.service";
|
||||
import { Collection } from "../shared/collection.model";
|
||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
||||
@@ -8,19 +8,22 @@ import { NormalizedCollection } from "../cache/models/normalized-collection.mode
|
||||
import { CoreState } from "../core.reducers";
|
||||
import { RequestService } from "./request.service";
|
||||
import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from "../../../config";
|
||||
|
||||
@Injectable()
|
||||
export class CollectionDataService extends DataService<NormalizedCollection, Collection> {
|
||||
protected endpoint = '/collections';
|
||||
protected resourceEndpoint = '/core/collections';
|
||||
protected browseEndpoint = '/discover/browses/dateissued/collections';
|
||||
|
||||
constructor(
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>
|
||||
protected store: Store<CoreState>,
|
||||
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
|
||||
) {
|
||||
super(NormalizedCollection);
|
||||
super(NormalizedCollection, EnvConfig);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Inject, Injectable } from "@angular/core";
|
||||
import { DataService } from "./data.service";
|
||||
import { Community } from "../shared/community.model";
|
||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
||||
@@ -8,19 +8,22 @@ import { NormalizedCommunity } from "../cache/models/normalized-community.model"
|
||||
import { CoreState } from "../core.reducers";
|
||||
import { RequestService } from "./request.service";
|
||||
import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from "../../../config";
|
||||
|
||||
@Injectable()
|
||||
export class CommunityDataService extends DataService<NormalizedCommunity, Community> {
|
||||
protected endpoint = '/communities';
|
||||
protected resourceEndpoint = '/core/communities';
|
||||
protected browseEndpoint = '/discover/browses/dateissued/communities';
|
||||
|
||||
constructor(
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>
|
||||
protected store: Store<CoreState>,
|
||||
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
|
||||
) {
|
||||
super(NormalizedCommunity);
|
||||
super(NormalizedCommunity, EnvConfig);
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,15 +1,18 @@
|
||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
||||
import { ResponseCacheService } from "../cache/response-cache.service";
|
||||
import { CacheableObject } from "../cache/object-cache.reducer";
|
||||
import { hasValue } from "../../shared/empty.util";
|
||||
import { hasValue, isNotEmpty } from "../../shared/empty.util";
|
||||
import { RemoteData } from "./remote-data";
|
||||
import { FindAllRequest, FindByIDRequest, Request } from "./request.models";
|
||||
import { FindAllOptions, FindAllRequest, FindByIDRequest, Request } from "./request.models";
|
||||
import { Store } from "@ngrx/store";
|
||||
import { RequestConfigureAction, RequestExecuteAction } from "./request.actions";
|
||||
import { CoreState } from "../core.reducers";
|
||||
import { RequestService } from "./request.service";
|
||||
import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
|
||||
import { GenericConstructor } from "../shared/generic-constructor";
|
||||
import { Inject } from "@angular/core";
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from "../../../config";
|
||||
import { RESTURLCombiner } from "../url-combiner/rest-url-combiner";
|
||||
|
||||
export abstract class DataService<TNormalized extends CacheableObject, TDomain> {
|
||||
protected abstract objectCache: ObjectCacheService;
|
||||
@@ -17,30 +20,61 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
|
||||
protected abstract requestService: RequestService;
|
||||
protected abstract rdbService: RemoteDataBuildService;
|
||||
protected abstract store: Store<CoreState>;
|
||||
protected abstract endpoint: string;
|
||||
protected abstract resourceEndpoint: string;
|
||||
protected abstract browseEndpoint: string;
|
||||
|
||||
constructor(private normalizedResourceType: GenericConstructor<TNormalized>) {
|
||||
constructor(
|
||||
private normalizedResourceType: GenericConstructor<TNormalized>,
|
||||
protected EnvConfig: GlobalConfig
|
||||
) {
|
||||
|
||||
}
|
||||
|
||||
protected getFindAllHref(scopeID?): string {
|
||||
let result = this.endpoint;
|
||||
if (hasValue(scopeID)) {
|
||||
result += `?scope=${scopeID}`
|
||||
protected getFindAllHref(options: FindAllOptions = {}): string {
|
||||
let result;
|
||||
let args = [];
|
||||
|
||||
if (hasValue(options.scopeID)) {
|
||||
result = this.browseEndpoint;
|
||||
args.push(`scope=${options.scopeID}`);
|
||||
}
|
||||
return result;
|
||||
else {
|
||||
result = this.resourceEndpoint;
|
||||
}
|
||||
|
||||
findAll(scopeID?: string): RemoteData<Array<TDomain>> {
|
||||
const href = this.getFindAllHref(scopeID);
|
||||
const request = new FindAllRequest(href, scopeID);
|
||||
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}`);
|
||||
}
|
||||
|
||||
if (hasValue(options.elementsPerPage)) {
|
||||
args.push(`size=${options.elementsPerPage}`);
|
||||
}
|
||||
|
||||
if (hasValue(options.sort)) {
|
||||
let direction = 'asc';
|
||||
if (options.sort.direction === 1) {
|
||||
direction = 'desc';
|
||||
}
|
||||
args.push(`sort=${options.sort.field},${direction}`);
|
||||
}
|
||||
|
||||
if (isNotEmpty(args)) {
|
||||
result = `${result}?${args.join('&')}`;
|
||||
}
|
||||
return new RESTURLCombiner(this.EnvConfig, result).toString();
|
||||
}
|
||||
|
||||
findAll(options: FindAllOptions = {}): RemoteData<Array<TDomain>> {
|
||||
const href = this.getFindAllHref(options);
|
||||
const request = new FindAllRequest(href, options);
|
||||
this.requestService.configure(request);
|
||||
return this.rdbService.buildList<TNormalized, TDomain>(href, this.normalizedResourceType);
|
||||
// return this.rdbService.buildList(href);
|
||||
}
|
||||
|
||||
protected getFindByIDHref(resourceID): string {
|
||||
return `${this.endpoint}/${resourceID}`;
|
||||
return new RESTURLCombiner(this.EnvConfig, `${this.resourceEndpoint}/${resourceID}`).toString();
|
||||
}
|
||||
|
||||
findById(id: string): RemoteData<TDomain> {
|
||||
|
@@ -1,4 +1,4 @@
|
||||
import { Injectable } from "@angular/core";
|
||||
import { Inject, Injectable } from "@angular/core";
|
||||
import { DataService } from "./data.service";
|
||||
import { Item } from "../shared/item.model";
|
||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
||||
@@ -8,18 +8,21 @@ import { CoreState } from "../core.reducers";
|
||||
import { NormalizedItem } from "../cache/models/normalized-item.model";
|
||||
import { RequestService } from "./request.service";
|
||||
import { RemoteDataBuildService } from "../cache/builders/remote-data-build.service";
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from "../../../config";
|
||||
|
||||
@Injectable()
|
||||
export class ItemDataService extends DataService<NormalizedItem, Item> {
|
||||
protected endpoint = '/items';
|
||||
protected resourceEndpoint = '/core/items';
|
||||
protected browseEndpoint = '/discover/browses/dateissued/items';
|
||||
|
||||
constructor(
|
||||
protected objectCache: ObjectCacheService,
|
||||
protected responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService,
|
||||
protected rdbService: RemoteDataBuildService,
|
||||
protected store: Store<CoreState>
|
||||
protected store: Store<CoreState>,
|
||||
@Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig
|
||||
) {
|
||||
super(NormalizedItem);
|
||||
super(NormalizedItem, EnvConfig);
|
||||
}
|
||||
}
|
||||
|
@@ -1,10 +1,11 @@
|
||||
import { Observable } from "rxjs";
|
||||
import { PageInfo } from "../shared/page-info.model";
|
||||
|
||||
export enum RemoteDataState {
|
||||
RequestPending,
|
||||
ResponsePending,
|
||||
Failed,
|
||||
Success
|
||||
RequestPending = <any> "RequestPending",
|
||||
ResponsePending = <any> "ResponsePending",
|
||||
Failed = <any> "Failed",
|
||||
Success = <any> "Success"
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -17,6 +18,8 @@ export class RemoteData<T> {
|
||||
private responsePending: Observable<boolean>,
|
||||
private isSuccessFul: Observable<boolean>,
|
||||
public errorMessage: Observable<string>,
|
||||
public statusCode: Observable<string>,
|
||||
public pageInfo: Observable<PageInfo>,
|
||||
public payload: Observable<T>
|
||||
) {
|
||||
}
|
||||
|
@@ -1,6 +1,5 @@
|
||||
import { Injectable, Inject } from "@angular/core";
|
||||
import { Actions, Effect } from "@ngrx/effects";
|
||||
import { Store } from "@ngrx/store";
|
||||
import { DSpaceRESTv2Service } from "../dspace-rest-v2/dspace-rest-v2.service";
|
||||
import { ObjectCacheService } from "../cache/object-cache.service";
|
||||
import { DSpaceRESTV2Response } from "../dspace-rest-v2/dspace-rest-v2-response.model";
|
||||
@@ -10,7 +9,7 @@ import { Observable } from "rxjs";
|
||||
import { Response, SuccessResponse, ErrorResponse } from "../cache/response-cache.models";
|
||||
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from "../../shared/empty.util";
|
||||
import { GlobalConfig, GLOBAL_CONFIG } from "../../../config";
|
||||
import { RequestState, RequestEntry } from "./request.reducer";
|
||||
import { RequestEntry } from "./request.reducer";
|
||||
import {
|
||||
RequestActionTypes, RequestExecuteAction,
|
||||
RequestCompleteAction
|
||||
@@ -19,11 +18,31 @@ import { ResponseCacheService } from "../cache/response-cache.service";
|
||||
import { RequestService } from "./request.service";
|
||||
import { NormalizedObjectFactory } from "../cache/models/normalized-object-factory";
|
||||
import { ResourceType } from "../shared/resource-type";
|
||||
import { RequestError } from "./request.models";
|
||||
import { PageInfo } from "../shared/page-info.model";
|
||||
import { NormalizedObject } from "../cache/models/normalized-object.model";
|
||||
|
||||
function isObjectLevel(halObj: any) {
|
||||
return isNotEmpty(halObj._links) && hasValue(halObj._links.self);
|
||||
}
|
||||
|
||||
function isPaginatedResponse(halObj: any) {
|
||||
return isNotEmpty(halObj.page) && hasValue(halObj._embedded);
|
||||
}
|
||||
|
||||
function flattenSingleKeyObject(obj: any): any {
|
||||
const keys = Object.keys(obj);
|
||||
if (keys.length !== 1) {
|
||||
throw new Error(`Expected an object with a single key, got: ${JSON.stringify(obj)}`);
|
||||
}
|
||||
return obj[keys[0]];
|
||||
}
|
||||
|
||||
class ProcessRequestDTO {
|
||||
[key: string]: NormalizedObject[]
|
||||
}
|
||||
|
||||
|
||||
@Injectable()
|
||||
export class RequestEffects {
|
||||
|
||||
@@ -33,8 +52,7 @@ export class RequestEffects {
|
||||
private restApi: DSpaceRESTv2Service,
|
||||
private objectCache: ObjectCacheService,
|
||||
private responseCache: ResponseCacheService,
|
||||
protected requestService: RequestService,
|
||||
private store: Store<RequestState>
|
||||
protected requestService: RequestService
|
||||
) { }
|
||||
|
||||
@Effect() execute = this.actions$
|
||||
@@ -45,83 +63,102 @@ export class RequestEffects {
|
||||
})
|
||||
.flatMap((entry: RequestEntry) => {
|
||||
return this.restApi.get(entry.request.href)
|
||||
.map((data: DSpaceRESTV2Response) => this.processEmbedded(data._embedded, entry.request.href))
|
||||
.map((ids: Array<string>) => new SuccessResponse(ids))
|
||||
.do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
|
||||
.map((data: DSpaceRESTV2Response) => {
|
||||
const processRequestDTO = this.process(data.payload, entry.request.href);
|
||||
const uuids = flattenSingleKeyObject(processRequestDTO).map(no => no.uuid);
|
||||
return new SuccessResponse(uuids, data.statusCode, this.processPageInfo(data.payload.page))
|
||||
}).do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
|
||||
.map((response: Response) => new RequestCompleteAction(entry.request.href))
|
||||
.catch((error: Error) => Observable.of(new ErrorResponse(error))
|
||||
.catch((error: RequestError) => Observable.of(new ErrorResponse(error))
|
||||
.do((response: Response) => this.responseCache.add(entry.request.href, response, this.EnvConfig.cache.msToLive))
|
||||
.map((response: Response) => new RequestCompleteAction(entry.request.href)));
|
||||
});
|
||||
|
||||
protected processEmbedded(_embedded: any, requestHref): Array<string> {
|
||||
protected process(data: any, requestHref: string): ProcessRequestDTO {
|
||||
|
||||
if (isNotEmpty(_embedded)) {
|
||||
if (isObjectLevel(_embedded)) {
|
||||
return this.deserializeAndCache(_embedded, requestHref);
|
||||
if (isNotEmpty(data)) {
|
||||
if (isPaginatedResponse(data)) {
|
||||
return this.process(data._embedded, requestHref);
|
||||
}
|
||||
else if (isObjectLevel(data)) {
|
||||
return { "topLevel": this.deserializeAndCache(data, requestHref) };
|
||||
}
|
||||
else {
|
||||
let uuids = [];
|
||||
Object.keys(_embedded)
|
||||
.filter(property => _embedded.hasOwnProperty(property))
|
||||
.forEach(property => {
|
||||
uuids = [...uuids, ...this.deserializeAndCache(_embedded[property], requestHref)];
|
||||
let result = new ProcessRequestDTO();
|
||||
if (Array.isArray(data)) {
|
||||
result['topLevel'] = [];
|
||||
data.forEach(datum => {
|
||||
if (isPaginatedResponse(datum)) {
|
||||
const obj = this.process(datum, requestHref);
|
||||
result['topLevel'] = [...result['topLevel'], ...flattenSingleKeyObject(obj)];
|
||||
}
|
||||
else {
|
||||
result['topLevel'] = [...result['topLevel'], ...this.deserializeAndCache(datum, requestHref)];
|
||||
}
|
||||
});
|
||||
return uuids;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected deserializeAndCache(obj, requestHref): Array<string> {
|
||||
let type: ResourceType;
|
||||
const isArray = Array.isArray(obj);
|
||||
|
||||
if (isArray && isEmpty(obj)) {
|
||||
return [];
|
||||
}
|
||||
|
||||
if (isArray) {
|
||||
type = obj[0]["type"];
|
||||
}
|
||||
else {
|
||||
type = obj["type"];
|
||||
Object.keys(data)
|
||||
.filter(property => data.hasOwnProperty(property))
|
||||
.filter(property => hasValue(data[property]))
|
||||
.forEach(property => {
|
||||
if (isPaginatedResponse(data[property])) {
|
||||
const obj = this.process(data[property], requestHref);
|
||||
result[property] = flattenSingleKeyObject(obj);
|
||||
}
|
||||
else {
|
||||
result[property] = this.deserializeAndCache(data[property], requestHref);
|
||||
}
|
||||
});
|
||||
}
|
||||
return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
protected deserializeAndCache(obj, requestHref: string): NormalizedObject[] {
|
||||
if(Array.isArray(obj)) {
|
||||
let result = [];
|
||||
obj.forEach(o => result = [...result, ...this.deserializeAndCache(o, requestHref)])
|
||||
return result;
|
||||
}
|
||||
|
||||
let type: ResourceType = obj["type"];
|
||||
if (hasValue(type)) {
|
||||
const normObjConstructor = NormalizedObjectFactory.getConstructor(type);
|
||||
|
||||
if (hasValue(normObjConstructor)) {
|
||||
const serializer = new DSpaceRESTv2Serializer(normObjConstructor);
|
||||
|
||||
if (isArray) {
|
||||
obj.forEach(o => {
|
||||
if (isNotEmpty(o._embedded)) {
|
||||
this.processEmbedded(o._embedded, requestHref);
|
||||
}
|
||||
});
|
||||
const normalizedObjArr = serializer.deserializeArray(obj);
|
||||
normalizedObjArr.forEach(t => this.addToObjectCache(t, requestHref));
|
||||
return normalizedObjArr.map(t => t.uuid);
|
||||
}
|
||||
else {
|
||||
let processed;
|
||||
if (isNotEmpty(obj._embedded)) {
|
||||
this.processEmbedded(obj._embedded, requestHref);
|
||||
processed = this.process(obj._embedded, requestHref);
|
||||
}
|
||||
const normalizedObj = serializer.deserialize(obj);
|
||||
let normalizedObj = serializer.deserialize(obj);
|
||||
|
||||
if (isNotEmpty(processed)) {
|
||||
let linksOnly = {};
|
||||
Object.keys(processed).forEach(key => {
|
||||
linksOnly[key] = processed[key].map((no: NormalizedObject) => no.self);
|
||||
});
|
||||
Object.assign(normalizedObj, linksOnly);
|
||||
}
|
||||
|
||||
this.addToObjectCache(normalizedObj, requestHref);
|
||||
return [normalizedObj.uuid];
|
||||
}
|
||||
return [normalizedObj];
|
||||
|
||||
}
|
||||
else {
|
||||
//TODO move check to Validator?
|
||||
throw new Error(`The server returned an object with an unknown a known type: ${type}`);
|
||||
// throw new Error(`The server returned an object with an unknown a known type: ${type}`);
|
||||
return [];
|
||||
}
|
||||
|
||||
}
|
||||
else {
|
||||
//TODO move check to Validator
|
||||
throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`);
|
||||
// throw new Error(`The server returned an object without a type: ${JSON.stringify(obj)}`);
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -131,4 +168,14 @@ export class RequestEffects {
|
||||
}
|
||||
this.objectCache.add(co, this.EnvConfig.cache.msToLive, requestHref);
|
||||
}
|
||||
|
||||
protected processPageInfo(pageObj: any): PageInfo {
|
||||
if (isNotEmpty(pageObj)) {
|
||||
return new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
|
||||
}
|
||||
else {
|
||||
return undefined;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -1,5 +1,5 @@
|
||||
import { SortOptions } from "../cache/models/sort-options.model";
|
||||
import { PaginationOptions } from "../cache/models/pagination-options.model";
|
||||
import { PaginationComponentOptions } from "../../shared/pagination/pagination-component-options.model";
|
||||
import { GenericConstructor } from "../shared/generic-constructor";
|
||||
|
||||
export class Request<T> {
|
||||
@@ -17,13 +17,22 @@ export class FindByIDRequest<T> extends Request<T> {
|
||||
}
|
||||
}
|
||||
|
||||
export class FindAllOptions {
|
||||
scopeID?: string;
|
||||
elementsPerPage?: number;
|
||||
currentPage?: number;
|
||||
sort?: SortOptions;
|
||||
}
|
||||
|
||||
export class FindAllRequest<T> extends Request<T> {
|
||||
constructor(
|
||||
href: string,
|
||||
public scopeID?: string,
|
||||
public paginationOptions?: PaginationOptions,
|
||||
public sortOptions?: SortOptions
|
||||
public options?: FindAllOptions,
|
||||
) {
|
||||
super(href);
|
||||
}
|
||||
}
|
||||
|
||||
export class RequestError extends Error {
|
||||
statusText: string;
|
||||
}
|
||||
|
@@ -1,4 +1,8 @@
|
||||
export interface DSpaceRESTV2Response {
|
||||
payload: {
|
||||
_embedded?: any;
|
||||
_links?: any;
|
||||
page?: any;
|
||||
},
|
||||
statusCode: string
|
||||
}
|
||||
|
@@ -60,8 +60,8 @@ describe("DSpaceRESTv2Serializer", () => {
|
||||
it("should turn a model in to a valid document", () => {
|
||||
const serializer = new DSpaceRESTv2Serializer(TestModel);
|
||||
const doc = serializer.serialize(testModels[0]);
|
||||
expect(testModels[0].id).toBe(doc._embedded.id);
|
||||
expect(testModels[0].name).toBe(doc._embedded.name);
|
||||
expect(testModels[0].id).toBe(doc.id);
|
||||
expect(testModels[0].name).toBe(doc.name);
|
||||
});
|
||||
|
||||
});
|
||||
@@ -72,10 +72,10 @@ describe("DSpaceRESTv2Serializer", () => {
|
||||
const serializer = new DSpaceRESTv2Serializer(TestModel);
|
||||
const doc = serializer.serializeArray(testModels);
|
||||
|
||||
expect(testModels[0].id).toBe(doc._embedded[0].id);
|
||||
expect(testModels[0].name).toBe(doc._embedded[0].name);
|
||||
expect(testModels[1].id).toBe(doc._embedded[1].id);
|
||||
expect(testModels[1].name).toBe(doc._embedded[1].name);
|
||||
expect(testModels[0].id).toBe(doc[0].id);
|
||||
expect(testModels[0].name).toBe(doc[0].name);
|
||||
expect(testModels[1].id).toBe(doc[1].id);
|
||||
expect(testModels[1].name).toBe(doc[1].name);
|
||||
});
|
||||
|
||||
});
|
||||
|
@@ -26,10 +26,8 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
|
||||
* @param model The model to serialize
|
||||
* @returns An object to send to the backend
|
||||
*/
|
||||
serialize(model: T): DSpaceRESTV2Response {
|
||||
return {
|
||||
"_embedded": Serialize(model, this.modelType)
|
||||
};
|
||||
serialize(model: T): any {
|
||||
return Serialize(model, this.modelType);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -38,10 +36,8 @@ export class DSpaceRESTv2Serializer<T> implements Serializer<T> {
|
||||
* @param models The array of models to serialize
|
||||
* @returns An object to send to the backend
|
||||
*/
|
||||
serializeArray(models: Array<T>): DSpaceRESTV2Response {
|
||||
return {
|
||||
"_embedded": Serialize(models, this.modelType)
|
||||
};
|
||||
serializeArray(models: Array<T>): any {
|
||||
return Serialize(models, this.modelType);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@@ -4,6 +4,7 @@ import { Observable } from 'rxjs/Observable';
|
||||
import { RESTURLCombiner } from "../url-combiner/rest-url-combiner";
|
||||
|
||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||
import { DSpaceRESTV2Response } from "./dspace-rest-v2-response.model";
|
||||
|
||||
/**
|
||||
* Service to access DSpace's REST API
|
||||
@@ -17,16 +18,16 @@ export class DSpaceRESTv2Service {
|
||||
/**
|
||||
* Performs a request to the REST API with the `get` http method.
|
||||
*
|
||||
* @param relativeURL
|
||||
* A URL, relative to the basepath of the rest api
|
||||
* @param absoluteURL
|
||||
* A URL
|
||||
* @param options
|
||||
* A RequestOptionsArgs object, with options for the http call.
|
||||
* @return {Observable<string>}
|
||||
* An Observablse<string> containing the response from the server
|
||||
* An Observable<string> containing the response from the server
|
||||
*/
|
||||
get(relativeURL: string, options?: RequestOptionsArgs): Observable<string> {
|
||||
return this.http.get(new RESTURLCombiner(this.EnvConfig, relativeURL).toString(), options)
|
||||
.map(res => res.json())
|
||||
get(absoluteURL: string, options?: RequestOptionsArgs): Observable<DSpaceRESTV2Response> {
|
||||
return this.http.get(absoluteURL, options)
|
||||
.map(res => ({ payload: res.json(), statusCode: res.statusText }))
|
||||
.catch(err => {
|
||||
console.log('Error: ', err);
|
||||
return Observable.throw(err);
|
||||
|
@@ -1,18 +1,13 @@
|
||||
import { DSpaceObject } from "./dspace-object.model";
|
||||
import { Bundle } from "./bundle.model";
|
||||
import { RemoteData } from "../data/remote-data";
|
||||
import { Item } from "./item.model";
|
||||
|
||||
export class Bitstream extends DSpaceObject {
|
||||
|
||||
/**
|
||||
* The size of this bitstream in bytes(?)
|
||||
* The size of this bitstream in bytes
|
||||
*/
|
||||
size: number;
|
||||
|
||||
/**
|
||||
* The relative path to this Bitstream's file
|
||||
*/
|
||||
url: string;
|
||||
sizeBytes: number;
|
||||
|
||||
/**
|
||||
* The mime type of this Bitstream
|
||||
@@ -25,17 +20,22 @@ export class Bitstream extends DSpaceObject {
|
||||
description: string;
|
||||
|
||||
/**
|
||||
* An array of Bundles that are direct parents of this Bitstream
|
||||
* The name of the Bundle this Bitstream is part of
|
||||
*/
|
||||
parents: RemoteData<Bundle[]>;
|
||||
bundleName: string;
|
||||
|
||||
/**
|
||||
* An array of Items that are direct parents of this Bitstream
|
||||
*/
|
||||
parents: RemoteData<Item[]>;
|
||||
|
||||
/**
|
||||
* The Bundle that owns this Bitstream
|
||||
*/
|
||||
owner: Bundle;
|
||||
owner: RemoteData<Item>;
|
||||
|
||||
/**
|
||||
* The Bundle that owns this Bitstream
|
||||
* The URL to retrieve this Bitstream's file
|
||||
*/
|
||||
retrieve: string;
|
||||
|
||||
|
@@ -17,7 +17,7 @@ export class Bundle extends DSpaceObject {
|
||||
/**
|
||||
* The Item that owns this Bundle
|
||||
*/
|
||||
owner: Item;
|
||||
owner: RemoteData<Item>;
|
||||
|
||||
bitstreams: RemoteData<Bitstream[]>
|
||||
|
||||
|
@@ -63,7 +63,7 @@ export class Collection extends DSpaceObject {
|
||||
/**
|
||||
* The Collection that owns this Collection
|
||||
*/
|
||||
owner: Collection;
|
||||
owner: RemoteData<Collection>;
|
||||
|
||||
items: RemoteData<Item[]>;
|
||||
|
||||
|
@@ -55,7 +55,7 @@ export class Community extends DSpaceObject {
|
||||
/**
|
||||
* The Community that owns this Community
|
||||
*/
|
||||
owner: Community;
|
||||
owner: RemoteData<Community>;
|
||||
|
||||
collections: RemoteData<Collection[]>;
|
||||
|
||||
|
@@ -44,7 +44,7 @@ export abstract class DSpaceObject implements CacheableObject {
|
||||
/**
|
||||
* The DSpaceObject that owns this DSpaceObject
|
||||
*/
|
||||
owner: DSpaceObject;
|
||||
owner: RemoteData<DSpaceObject>;
|
||||
|
||||
/**
|
||||
* Find a metadata field by key and language
|
||||
|
@@ -1,9 +1,9 @@
|
||||
import { TestBed, async } from '@angular/core/testing';
|
||||
import { Item } from "./item.model";
|
||||
import { Bundle } from "./bundle.model";
|
||||
import { Observable } from "rxjs";
|
||||
import { RemoteData } from "../data/remote-data";
|
||||
import { Bitstream } from "./bitstream.model";
|
||||
import { isEmpty } from "../../shared/empty.util";
|
||||
import { PageInfo } from "./page-info.model";
|
||||
|
||||
|
||||
describe('Item', () => {
|
||||
@@ -17,23 +17,25 @@ describe('Item', () => {
|
||||
const bitstream2Path = "otherfile.doc";
|
||||
|
||||
const nonExistingBundleName = "c1e568f7-d14e-496b-bdd7-07026998cc00";
|
||||
let remoteBundles;
|
||||
let thumbnailBundle;
|
||||
let originalBundle;
|
||||
let bitstreams;
|
||||
let remoteDataThumbnail;
|
||||
let remoteDataFiles;
|
||||
let remoteDataAll;
|
||||
|
||||
beforeEach(() => {
|
||||
const thumbnail = {
|
||||
retrieve: thumbnailPath
|
||||
};
|
||||
|
||||
const bitstreams = [{
|
||||
bitstreams = [{
|
||||
retrieve: bitstream1Path
|
||||
}, {
|
||||
retrieve: bitstream2Path
|
||||
}];
|
||||
|
||||
const remoteDataThumbnail = createRemoteDataObject(thumbnail);
|
||||
const remoteDataFiles = createRemoteDataObject(bitstreams);
|
||||
remoteDataThumbnail = createRemoteDataObject(thumbnail);
|
||||
remoteDataFiles = createRemoteDataObject(bitstreams);
|
||||
remoteDataAll = createRemoteDataObject([...bitstreams, thumbnail]);
|
||||
|
||||
|
||||
// Create Bundles
|
||||
@@ -50,32 +52,30 @@ describe('Item', () => {
|
||||
bitstreams: remoteDataFiles
|
||||
}];
|
||||
|
||||
remoteBundles = createRemoteDataObject(bundles);
|
||||
|
||||
item = Object.assign(new Item(), { bundles: remoteBundles });
|
||||
item = Object.assign(new Item(), { bitstreams: remoteDataAll});
|
||||
|
||||
});
|
||||
|
||||
|
||||
it('should return the bundle with the given name of this item when the bundle exists', () => {
|
||||
let name: string = thumbnailBundleName;
|
||||
let bundle: Observable<Bundle> = item.getBundle(name);
|
||||
bundle.map(b => expect(b.name).toBe(name));
|
||||
it('should return the bitstreams related to this item with the specified bundle name', () => {
|
||||
const bitObs: Observable<Bitstream[]> = item.getBitstreamsByBundleName(thumbnailBundleName);
|
||||
bitObs.take(1).subscribe(bs =>
|
||||
expect(bs.every(b => b.name === thumbnailBundleName)).toBeTruthy());
|
||||
});
|
||||
|
||||
it('should return null when no bundle with this name exists for this item', () => {
|
||||
let name: string = nonExistingBundleName;
|
||||
let bundle: Observable<Bundle> = item.getBundle(name);
|
||||
bundle.map(b => expect(b).toBeUndefined());
|
||||
it('should return an empty array when no bitstreams with this bundleName exist for this item', () => {
|
||||
const bitstreams: Observable<Bitstream[]> = item.getBitstreamsByBundleName(nonExistingBundleName);
|
||||
bitstreams.take(1).subscribe(bs => expect(isEmpty(bs)).toBeTruthy());
|
||||
});
|
||||
|
||||
|
||||
describe("get thumbnail", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(item, 'getBundle').and.returnValue(Observable.of(thumbnailBundle));
|
||||
spyOn(item, 'getBitstreamsByBundleName').and.returnValue(Observable.of([remoteDataThumbnail]));
|
||||
});
|
||||
|
||||
it('should return the thumbnail (the primaryBitstream in the bundle "THUMBNAIL") of this item', () => {
|
||||
it('should return the thumbnail of this item', () => {
|
||||
let path: string = thumbnailPath;
|
||||
let bitstream: Observable<Bitstream> = item.getThumbnail();
|
||||
bitstream.map(b => expect(b.retrieve).toBe(path));
|
||||
@@ -85,10 +85,10 @@ describe('Item', () => {
|
||||
|
||||
describe("get files", () => {
|
||||
beforeEach(() => {
|
||||
spyOn(item, 'getBundle').and.returnValue(Observable.of(originalBundle));
|
||||
spyOn(item, 'getBitstreamsByBundleName').and.returnValue(Observable.of(bitstreams));
|
||||
});
|
||||
|
||||
it('should return all files in the ORIGINAL bundle', () => {
|
||||
it('should return all bitstreams with "ORIGINAL" as bundleName', () => {
|
||||
let paths = [bitstream1Path, bitstream2Path];
|
||||
|
||||
let files: Observable<Bitstream[]> = item.getFiles();
|
||||
@@ -110,6 +110,23 @@ describe('Item', () => {
|
||||
});
|
||||
|
||||
function createRemoteDataObject(object: Object) {
|
||||
return new RemoteData("", Observable.of(false), Observable.of(false), Observable.of(true), Observable.of(undefined), Observable.of(object));
|
||||
const self = "";
|
||||
const requestPending = Observable.of(false);
|
||||
const responsePending = Observable.of(false);
|
||||
const isSuccessful = Observable.of(true);
|
||||
const errorMessage = Observable.of(undefined);
|
||||
const statusCode = Observable.of("200");
|
||||
const pageInfo = Observable.of(new PageInfo());
|
||||
const payload = Observable.of(object);
|
||||
return new RemoteData(
|
||||
self,
|
||||
requestPending,
|
||||
responsePending,
|
||||
isSuccessful,
|
||||
errorMessage,
|
||||
statusCode,
|
||||
pageInfo,
|
||||
payload
|
||||
);
|
||||
|
||||
}
|
@@ -1,10 +1,9 @@
|
||||
import { DSpaceObject } from "./dspace-object.model";
|
||||
import { Collection } from "./collection.model";
|
||||
import { RemoteData } from "../data/remote-data";
|
||||
import { Bundle } from "./bundle.model";
|
||||
import { Bitstream } from "./bitstream.model";
|
||||
import { Observable } from "rxjs";
|
||||
import { hasValue } from "../../shared/empty.util";
|
||||
import { isNotEmpty } from "../../shared/empty.util";
|
||||
|
||||
export class Item extends DSpaceObject {
|
||||
|
||||
@@ -23,6 +22,11 @@ export class Item extends DSpaceObject {
|
||||
*/
|
||||
isArchived: boolean;
|
||||
|
||||
/**
|
||||
* A boolean representing if this Item is currently discoverable or not
|
||||
*/
|
||||
isDiscoverable: boolean;
|
||||
|
||||
/**
|
||||
* A boolean representing if this Item is currently withdrawn or not
|
||||
*/
|
||||
@@ -36,9 +40,13 @@ export class Item extends DSpaceObject {
|
||||
/**
|
||||
* The Collection that owns this Item
|
||||
*/
|
||||
owner: Collection;
|
||||
owningCollection: RemoteData<Collection>;
|
||||
|
||||
bundles: RemoteData<Bundle[]>;
|
||||
get owner(): RemoteData<Collection> {
|
||||
return this.owningCollection;
|
||||
}
|
||||
|
||||
bitstreams: RemoteData<Bitstream[]>;
|
||||
|
||||
|
||||
/**
|
||||
@@ -46,11 +54,12 @@ export class Item extends DSpaceObject {
|
||||
* @returns {Observable<Bitstream>} the primaryBitstream of the "THUMBNAIL" bundle
|
||||
*/
|
||||
getThumbnail(): Observable<Bitstream> {
|
||||
const bundle: Observable<Bundle> = this.getBundle("THUMBNAIL");
|
||||
return bundle
|
||||
.filter(bundle => hasValue(bundle))
|
||||
.flatMap(bundle => bundle.primaryBitstream.payload)
|
||||
.startWith(undefined);
|
||||
//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")
|
||||
.filter(thumbnails => isNotEmpty(thumbnails))
|
||||
.map(thumbnails => thumbnails[0])
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -58,45 +67,31 @@ export class Item extends DSpaceObject {
|
||||
* @returns {Observable<Bitstream>} the primaryBitstream of the "THUMBNAIL" bundle
|
||||
*/
|
||||
getThumbnailForOriginal(original: Bitstream): Observable<Bitstream> {
|
||||
const bundle: Observable<Bundle> = this.getBundle("THUMBNAIL");
|
||||
return bundle
|
||||
.filter(bundle => hasValue(bundle))
|
||||
.flatMap(bundle => bundle
|
||||
.bitstreams.payload.map(files => files
|
||||
return this.getBitstreamsByBundleName("THUMBNAIL").map(files => files
|
||||
.find(thumbnail => thumbnail
|
||||
.name.startsWith(original.name)
|
||||
)
|
||||
)
|
||||
)
|
||||
.startWith(undefined);;
|
||||
).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(name: String = "ORIGINAL"): Observable<Bitstream[]> {
|
||||
const bundle: Observable <Bundle> = this.getBundle(name);
|
||||
return bundle
|
||||
.filter(bundle => hasValue(bundle))
|
||||
.flatMap(bundle => bundle.bitstreams.payload)
|
||||
.startWith([]);
|
||||
getFiles(): Observable<Bitstream[]> {
|
||||
return this.getBitstreamsByBundleName("ORIGINAL");
|
||||
}
|
||||
|
||||
/**
|
||||
* Retrieves the bundle of this item by its name
|
||||
* @param name The name of the Bundle that should be returned
|
||||
* @returns {Observable<Bundle>} the Bundle that belongs to this item with the given name
|
||||
* 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
|
||||
*/
|
||||
getBundle(name: String): Observable<Bundle> {
|
||||
return this.bundles.payload
|
||||
.filter(bundles => hasValue(bundles))
|
||||
.map(bundles => {
|
||||
return bundles.find((bundle: Bundle) => {
|
||||
return bundle.name === name
|
||||
});
|
||||
})
|
||||
.startWith(undefined);
|
||||
getBitstreamsByBundleName(bundleName: string): Observable<Bitstream[]> {
|
||||
return this.bitstreams.payload.startWith([])
|
||||
.map(bitstreams => bitstreams
|
||||
.filter(bitstream => bitstream.bundleName === bundleName)
|
||||
);
|
||||
}
|
||||
|
||||
}
|
||||
|
30
src/app/core/shared/page-info.model.ts
Normal file
30
src/app/core/shared/page-info.model.ts
Normal file
@@ -0,0 +1,30 @@
|
||||
import { autoserialize, autoserializeAs } from "cerialize";
|
||||
|
||||
/**
|
||||
* Represents the state of a paginated response
|
||||
*/
|
||||
export class PageInfo {
|
||||
/**
|
||||
* The number of elements on a page
|
||||
*/
|
||||
@autoserializeAs(Number, 'size')
|
||||
elementsPerPage: number;
|
||||
|
||||
/**
|
||||
* The total number of elements in the entire set
|
||||
*/
|
||||
@autoserialize
|
||||
totalElements: number;
|
||||
|
||||
/**
|
||||
* The total number of pages
|
||||
*/
|
||||
@autoserialize
|
||||
totalPages: number;
|
||||
|
||||
/**
|
||||
* The number of the current page, zero-based
|
||||
*/
|
||||
@autoserializeAs(Number, 'number')
|
||||
currentPage: number;
|
||||
}
|
@@ -5,6 +5,7 @@
|
||||
export enum ResourceType {
|
||||
Bundle = <any> "bundle",
|
||||
Bitstream = <any> "bitstream",
|
||||
BitstreamFormat = <any> "bitstreamformat",
|
||||
Item = <any> "item",
|
||||
Collection = <any> "collection",
|
||||
Community = <any> "community"
|
||||
|
@@ -7,12 +7,14 @@ import { TopLevelCommunityListComponent } from "./top-level-community-list/top-l
|
||||
import { HomeNewsComponent } from "./home-news/home-news.component";
|
||||
import { RouterModule } from "@angular/router";
|
||||
import { TranslateModule } from "@ngx-translate/core";
|
||||
import { SharedModule } from "../shared/shared.module";
|
||||
|
||||
@NgModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
HomeRoutingModule,
|
||||
RouterModule,
|
||||
SharedModule,
|
||||
TranslateModule
|
||||
],
|
||||
declarations: [
|
||||
|
@@ -1,12 +1,10 @@
|
||||
<div *ngIf="topLevelCommunities.hasSucceeded | async">
|
||||
<h2>{{'home.top-level-communities.head' | translate}}</h2>
|
||||
<p class="lead">{{'home.top-level-communities.help' | translate}}</p>
|
||||
<ul>
|
||||
<li *ngFor="let community of (topLevelCommunities.payload | async)">
|
||||
<p>
|
||||
<span class="lead"><a [routerLink]="['/communities', community.id]">{{community.name}}</a></span><br>
|
||||
<span class="text-muted">{{community.shortDescription}}</span>
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<ds-object-list [config]="config" [sortConfig]="sortConfig"
|
||||
[objects]="topLevelCommunities" [hideGear]="true"
|
||||
(pageChange)="onPageChange($event)"
|
||||
(pageSizeChange)="onPageSizeChange($event)"
|
||||
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||
(sortFieldChange)="onSortDirectionChange($event)"></ds-object-list>
|
||||
</div>
|
||||
|
@@ -1,18 +1,24 @@
|
||||
import { Component, OnInit } from '@angular/core';
|
||||
import { CommunityDataService } from "../../core/data/community-data.service";
|
||||
import { Component, OnInit, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core';
|
||||
import { RemoteData } from "../../core/data/remote-data";
|
||||
import { CommunityDataService } from "../../core/data/community-data.service";
|
||||
import { Community } from "../../core/shared/community.model";
|
||||
import { PaginationComponentOptions } from "../../shared/pagination/pagination-component-options.model";
|
||||
import { SortOptions, SortDirection } from "../../core/cache/models/sort-options.model";
|
||||
|
||||
@Component({
|
||||
selector: 'ds-top-level-community-list',
|
||||
styleUrls: ['./top-level-community-list.component.css'],
|
||||
templateUrl: './top-level-community-list.component.html'
|
||||
templateUrl: './top-level-community-list.component.html',
|
||||
changeDetection: ChangeDetectionStrategy.OnPush
|
||||
})
|
||||
export class TopLevelCommunityListComponent implements OnInit {
|
||||
topLevelCommunities: RemoteData<Community[]>;
|
||||
config : PaginationComponentOptions;
|
||||
sortConfig : SortOptions;
|
||||
|
||||
constructor(
|
||||
private cds: CommunityDataService
|
||||
private cds: CommunityDataService,
|
||||
private ref: ChangeDetectorRef
|
||||
) {
|
||||
this.universalInit();
|
||||
}
|
||||
@@ -22,6 +28,38 @@ export class TopLevelCommunityListComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.topLevelCommunities = this.cds.findAll();
|
||||
this.config = new PaginationComponentOptions();
|
||||
this.config.id = "top-level-pagination";
|
||||
this.config.pageSizeOptions = [ 4 ];
|
||||
this.config.pageSize = 4;
|
||||
this.sortConfig = new SortOptions();
|
||||
|
||||
this.updateResults();
|
||||
}
|
||||
|
||||
onPageChange(currentPage: number): void {
|
||||
this.config.currentPage = currentPage;
|
||||
this.updateResults();
|
||||
}
|
||||
|
||||
onPageSizeChange(elementsPerPage: number): void {
|
||||
this.config.pageSize = elementsPerPage;
|
||||
this.updateResults();
|
||||
}
|
||||
|
||||
onSortDirectionChange(sortDirection: SortDirection): void {
|
||||
this.sortConfig = new SortOptions(this.sortConfig.field, sortDirection);
|
||||
this.updateResults();
|
||||
}
|
||||
|
||||
onSortFieldChange(field: string): void {
|
||||
this.sortConfig = new SortOptions(field, this.sortConfig.direction);
|
||||
this.updateResults();
|
||||
}
|
||||
|
||||
updateResults() {
|
||||
this.topLevelCommunities = undefined;
|
||||
this.topLevelCommunities = this.cds.findAll({ currentPage: this.config.currentPage, elementsPerPage: this.config.pageSize, sort: this.sortConfig });
|
||||
// this.ref.detectChanges();
|
||||
}
|
||||
}
|
||||
|
@@ -1,6 +1,6 @@
|
||||
<ds-metadata-field-wrapper [label]="label | translate">
|
||||
<div class="collections">
|
||||
<a *ngFor="let collection of (collections | async); let last=last;" [href]="collection?.self">
|
||||
<a *ngFor="let collection of (collections | async); let last=last;" [routerLink]="['/collections', collection.id]">
|
||||
<span>{{collection?.name}}</span><span *ngIf="!last" [innerHTML]="separator"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -2,6 +2,7 @@ import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Collection } from "../../../core/shared/collection.model";
|
||||
import { Observable } from "rxjs";
|
||||
import { Item } from "../../../core/shared/item.model";
|
||||
import { RemoteDataBuildService } from "../../../core/cache/builders/remote-data-build.service";
|
||||
|
||||
/**
|
||||
* This component renders the parent collections section of the item
|
||||
@@ -18,11 +19,13 @@ export class CollectionsComponent implements OnInit {
|
||||
|
||||
label : string = "item.page.collections";
|
||||
|
||||
separator: string = "<br/>"
|
||||
separator: string = "<br/>";
|
||||
|
||||
collections: Observable<Collection[]>;
|
||||
|
||||
constructor() {
|
||||
constructor(
|
||||
private rdbs: RemoteDataBuildService
|
||||
) {
|
||||
this.universalInit();
|
||||
|
||||
}
|
||||
@@ -31,7 +34,11 @@ export class CollectionsComponent implements OnInit {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.collections = this.item.parents.payload;
|
||||
// this.collections = this.item.parents.payload;
|
||||
//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.payload.map(c => [c]);
|
||||
}
|
||||
|
||||
|
||||
|
@@ -9,7 +9,7 @@
|
||||
<dd class="col-md-8">{{file.name}}</dd>
|
||||
|
||||
<dt class="col-md-4">{{"item.page.filesection.size" | translate}}</dt>
|
||||
<dd class="col-md-8">{{(file.size) | dsFileSize }}</dd>
|
||||
<dd class="col-md-8">{{(file.sizeBytes) | dsFileSize }}</dd>
|
||||
|
||||
|
||||
<dt class="col-md-4">{{"item.page.filesection.format" | translate}}</dt>
|
||||
@@ -21,7 +21,7 @@
|
||||
</dl>
|
||||
</div>
|
||||
<div class="col-2">
|
||||
<a [href]="file.retrieve">
|
||||
<a [href]="file.retrieve" [download]="file.name">
|
||||
{{"item.page.filesection.download" | translate}}
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -35,8 +35,8 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
|
||||
}
|
||||
|
||||
initialize(): void {
|
||||
const originals = this.item.getFiles("ORIGINAL");
|
||||
const licenses = this.item.getFiles("LICENSE");
|
||||
const originals = this.item.getFiles();
|
||||
const licenses = this.item.getBitstreamsByBundleName("LICENSE");
|
||||
this.files = Observable.combineLatest(originals, licenses, (originals, licenses) => [...originals, ...licenses]);
|
||||
this.files.subscribe(
|
||||
files =>
|
||||
|
@@ -1,8 +1,8 @@
|
||||
<ds-metadata-field-wrapper [label]="label | translate">
|
||||
<div class="file-section">
|
||||
<a *ngFor="let file of (files | async); let last=last;" [href]="file?.retrieve">
|
||||
<a *ngFor="let file of (files | async); let last=last;" [href]="file?.retrieve" [download]="file?.name">
|
||||
<span>{{file?.name}}</span>
|
||||
<span>({{(file?.size) | dsFileSize }})</span>
|
||||
<span>({{(file?.sizeBytes) | dsFileSize }})</span>
|
||||
<span *ngIf="!last" innerHTML="{{separator}}"></span>
|
||||
</a>
|
||||
</div>
|
||||
|
@@ -0,0 +1,6 @@
|
||||
<a [routerLink]="['/collections/' + collection.id]" class="lead">
|
||||
{{collection.name}}
|
||||
</a>
|
||||
<div *ngIf="collection.shortDescription" class="text-muted">
|
||||
{{collection.shortDescription}}
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
@import '../../../styles/variables.scss';
|
@@ -0,0 +1,23 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Collection } from "../../core/shared/collection.model";
|
||||
|
||||
@Component({
|
||||
selector: 'ds-collection-list-element',
|
||||
styleUrls: ['./collection-list-element.component.css'],
|
||||
templateUrl: './collection-list-element.component.html'
|
||||
})
|
||||
export class CollectionListElementComponent {
|
||||
|
||||
@Input() collection: Collection;
|
||||
|
||||
data: any = {};
|
||||
|
||||
constructor() {
|
||||
this.universalInit();
|
||||
}
|
||||
|
||||
universalInit() {
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,6 @@
|
||||
<a [routerLink]="['/communities/' + community.id]" class="lead">
|
||||
{{community.name}}
|
||||
</a>
|
||||
<div *ngIf="community.shortDescription" class="text-muted">
|
||||
{{community.shortDescription}}
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
@import '../../../styles/variables.scss';
|
@@ -0,0 +1,23 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Community } from "../../core/shared/community.model";
|
||||
|
||||
@Component({
|
||||
selector: 'ds-community-list-element',
|
||||
styleUrls: ['./community-list-element.component.css'],
|
||||
templateUrl: './community-list-element.component.html'
|
||||
})
|
||||
export class CommunityListElementComponent {
|
||||
|
||||
@Input() community: Community;
|
||||
|
||||
data: any = {};
|
||||
|
||||
constructor() {
|
||||
this.universalInit();
|
||||
}
|
||||
|
||||
universalInit() {
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,14 @@
|
||||
<a [routerLink]="['/items/' + item.id]" class="lead">
|
||||
{{item.findMetadata("dc.title")}}
|
||||
</a>
|
||||
<div>
|
||||
<span class="text-muted">
|
||||
<span *ngIf="item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']);" class="item-list-authors">
|
||||
<span *ngFor="let authorMd of item.filterMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*']); let last=last;">{{authorMd.value}}
|
||||
<span *ngIf="!last">; </span>
|
||||
</span>
|
||||
</span>
|
||||
(<span *ngIf="item.findMetadata('dc.publisher')" class="item-list-publisher">{{item.findMetadata("dc.publisher")}}, </span><span *ngIf="item.findMetadata('dc.date.issued')" class="item-list-date">{{item.findMetadata("dc.date.issued")}}</span>)
|
||||
</span>
|
||||
<div *ngIf="item.findMetadata('dc.description.abstract')" class="item-list-abstract">{{item.findMetadata("dc.description.abstract") | dsTruncate:[200] }}</div>
|
||||
</div>
|
@@ -0,0 +1 @@
|
||||
@import '../../../styles/variables.scss';
|
@@ -0,0 +1,22 @@
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Item } from "../../core/shared/item.model";
|
||||
|
||||
@Component({
|
||||
selector: 'ds-item-list-element',
|
||||
styleUrls: ['./item-list-element.component.css'],
|
||||
templateUrl: './item-list-element.component.html'
|
||||
})
|
||||
export class ItemListElementComponent {
|
||||
@Input() item: Item;
|
||||
|
||||
data: any = {};
|
||||
|
||||
constructor() {
|
||||
this.universalInit();
|
||||
}
|
||||
|
||||
universalInit() {
|
||||
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,5 @@
|
||||
<div [ngSwitch]="object.type">
|
||||
<ds-item-list-element *ngSwitchCase="type.Item" [item]="object"></ds-item-list-element>
|
||||
<ds-collection-list-element *ngSwitchCase="type.Collection" [collection]="object"></ds-collection-list-element>
|
||||
<ds-community-list-element *ngSwitchCase="type.Community" [community]="object"></ds-community-list-element>
|
||||
</div>
|
@@ -0,0 +1,7 @@
|
||||
@import '../../../styles/variables.scss';
|
||||
@import '../../../../node_modules/bootstrap/scss/variables';
|
||||
|
||||
:host {
|
||||
display: block;
|
||||
margin-bottom: $spacer-y;
|
||||
}
|
@@ -0,0 +1,26 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { DSpaceObject } from "../../core/shared/dspace-object.model";
|
||||
import { ResourceType } from "../../core/shared/resource-type";
|
||||
|
||||
@Component({
|
||||
selector: 'ds-object-list-element',
|
||||
styleUrls: ['./object-list-element.component.css'],
|
||||
templateUrl: './object-list-element.component.html'
|
||||
})
|
||||
export class ObjectListElementComponent {
|
||||
|
||||
public type = ResourceType;
|
||||
|
||||
@Input() object: DSpaceObject;
|
||||
|
||||
data: any = {};
|
||||
|
||||
constructor() {
|
||||
this.universalInit();
|
||||
}
|
||||
|
||||
universalInit() {
|
||||
|
||||
}
|
||||
|
||||
}
|
16
src/app/object-list/object-list.component.html
Normal file
16
src/app/object-list/object-list.component.html
Normal file
@@ -0,0 +1,16 @@
|
||||
<ds-pagination [paginationOptions]="config"
|
||||
[collectionSize]="(pageInfo | async)?.totalElements"
|
||||
[sortOptions]="sortConfig"
|
||||
[hideGear]="hideGear"
|
||||
[hidePagerWhenSinglePage]="hidePagerWhenSinglePage"
|
||||
(pageChange)="onPageChange($event)"
|
||||
(pageSizeChange)="onPageSizeChange($event)"
|
||||
(sortDirectionChange)="onSortDirectionChange($event)"
|
||||
(sortFieldChange)="onSortDirectionChange($event)">
|
||||
<ul *ngIf="objects.hasSucceeded | async"> <!--class="list-unstyled"-->
|
||||
<li *ngFor="let object of (objects.payload | async) | paginate: { itemsPerPage: (pageInfo | async)?.elementsPerPage, currentPage: (pageInfo | async)?.currentPage, totalItems: (pageInfo | async)?.totalElements }">
|
||||
<ds-object-list-element [object]="object"></ds-object-list-element>
|
||||
</li>
|
||||
</ul>
|
||||
|
||||
</ds-pagination>
|
1
src/app/object-list/object-list.component.scss
Normal file
1
src/app/object-list/object-list.component.scss
Normal file
@@ -0,0 +1 @@
|
||||
@import '../../styles/variables.scss';
|
@@ -1,3 +1,3 @@
|
||||
<div *ngIf="logo" class="dso-logo">
|
||||
<img [src]="logo.url" class="img-responsive" [attr.alt]="alternateText ? alternateText : null" />
|
||||
<img [src]="logo.retrieve" class="img-responsive" [attr.alt]="alternateText ? alternateText : null" (error)="errorHandler($event)"/>
|
||||
</div>
|
@@ -12,4 +12,14 @@ export class ComcolPageLogoComponent {
|
||||
@Input() logo: Bitstream;
|
||||
|
||||
@Input() alternateText: string;
|
||||
|
||||
/**
|
||||
* The default 'holder.js' image
|
||||
*/
|
||||
holderSource: string = "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%23EEEEEE%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";
|
||||
|
||||
|
||||
errorHandler(event) {
|
||||
event.currentTarget.src = this.holderSource;
|
||||
}
|
||||
}
|
82
src/app/shared/object-list/object-list.component.ts
Normal file
82
src/app/shared/object-list/object-list.component.ts
Normal file
@@ -0,0 +1,82 @@
|
||||
import {
|
||||
Component, Input, ViewEncapsulation, ChangeDetectionStrategy,
|
||||
OnInit, Output
|
||||
} from '@angular/core';
|
||||
import { RemoteData } from "../../core/data/remote-data";
|
||||
import { DSpaceObject } from "../../core/shared/dspace-object.model";
|
||||
import { PageInfo } from "../../core/shared/page-info.model";
|
||||
import { Observable } from "rxjs";
|
||||
import { PaginationComponentOptions } from "../pagination/pagination-component-options.model";
|
||||
import { EventEmitter } from "@angular/common/src/facade/async";
|
||||
import { SortOptions, SortDirection } from "../../core/cache/models/sort-options.model";
|
||||
|
||||
|
||||
@Component({
|
||||
changeDetection: ChangeDetectionStrategy.Default,
|
||||
encapsulation: ViewEncapsulation.Emulated,
|
||||
selector: 'ds-object-list',
|
||||
styleUrls: ['../../object-list/object-list.component.css'],
|
||||
templateUrl: '../../object-list/object-list.component.html'
|
||||
})
|
||||
export class ObjectListComponent implements OnInit {
|
||||
|
||||
@Input() objects: RemoteData<DSpaceObject[]>;
|
||||
@Input() config : PaginationComponentOptions;
|
||||
@Input() sortConfig : SortOptions;
|
||||
@Input() hideGear : boolean = false;
|
||||
@Input() hidePagerWhenSinglePage : boolean = true;
|
||||
pageInfo : Observable<PageInfo>;
|
||||
|
||||
/**
|
||||
* An event fired when the page is changed.
|
||||
* Event's payload equals to the newly selected page.
|
||||
*/
|
||||
@Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
|
||||
|
||||
/**
|
||||
* An event fired when the page wsize is changed.
|
||||
* Event's payload equals to the newly selected page size.
|
||||
*/
|
||||
@Output() pageSizeChange: EventEmitter<number> = new EventEmitter<number>();
|
||||
|
||||
/**
|
||||
* An event fired when the sort direction is changed.
|
||||
* Event's payload equals to the newly selected sort direction.
|
||||
*/
|
||||
@Output() sortDirectionChange: EventEmitter<SortDirection> = new EventEmitter<SortDirection>();
|
||||
|
||||
/**
|
||||
* An event fired when the sort field is changed.
|
||||
* Event's payload equals to the newly selected sort field.
|
||||
*/
|
||||
@Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>();
|
||||
data: any = {};
|
||||
|
||||
constructor() {
|
||||
this.universalInit();
|
||||
}
|
||||
|
||||
universalInit() {
|
||||
}
|
||||
|
||||
ngOnInit(): void {
|
||||
this.pageInfo = this.objects.pageInfo;
|
||||
}
|
||||
|
||||
onPageChange(event) {
|
||||
this.pageChange.emit(event);
|
||||
}
|
||||
|
||||
onPageSizeChange(event) {
|
||||
this.pageSizeChange.emit(event);
|
||||
}
|
||||
|
||||
onSortDirectionChange(event) {
|
||||
this.sortDirectionChange.emit(event);
|
||||
}
|
||||
|
||||
onSortFieldChange(event) {
|
||||
this.sortFieldChange.emit(event);
|
||||
}
|
||||
|
||||
}
|
@@ -1,6 +1,6 @@
|
||||
import { NgbPaginationConfig } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
export class PaginationOptions extends NgbPaginationConfig {
|
||||
export class PaginationComponentOptions extends NgbPaginationConfig {
|
||||
/**
|
||||
* ID for the pagination instance. Only useful if you wish to
|
||||
* have more than once instance at a time in a given component.
|
@@ -1,4 +1,4 @@
|
||||
<div class="pagination-masked clearfix top">
|
||||
<div *ngIf="!hideGear" class="pagination-masked clearfix top">
|
||||
<div class="row">
|
||||
<div class="col pagination-info">
|
||||
<span class="align-middle hidden-xs-down">{{ 'pagination.showing.label' | translate }}</span>
|
||||
@@ -10,6 +10,8 @@
|
||||
<div class="dropdown-menu dropdown-menu-right" id="paginationControlsDropdownMenu" aria-labelledby="paginationControls">
|
||||
<h6 class="dropdown-header">{{ 'pagination.results-per-page' | translate}}</h6>
|
||||
<button class="dropdown-item" style="padding-left: 20px" *ngFor="let item of pageSizeOptions" (click)="setPageSize(item)"><i class="fa fa-check {{(item != paginationOptions.pageSize) ? 'invisible' : ''}}" aria-hidden="true"></i> {{item}} </button>
|
||||
<h6 class="dropdown-header">{{ 'pagination.sort-direction' | translate}}</h6>
|
||||
<button class="dropdown-item" style="padding-left: 20px" *ngFor="let direction of (sortDirections | dsKeys)" (click)="setSortDirection(direction.key)"><i class="fa fa-check {{(direction.key != sortOptions.direction) ? 'invisible' : ''}}" aria-hidden="true"></i> {{direction.value}} </button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -18,7 +20,7 @@
|
||||
|
||||
<ng-content></ng-content>
|
||||
|
||||
<div class="pagination justify-content-center clearfix bottom">
|
||||
<div *ngIf="shouldShowBottomPager" class="pagination justify-content-center clearfix bottom">
|
||||
<ngb-pagination [boundaryLinks]="paginationOptions.boundaryLinks"
|
||||
[collectionSize]="collectionSize"
|
||||
[disabled]="paginationOptions.disabled"
|
||||
|
@@ -25,12 +25,14 @@ import { Ng2PaginationModule } from 'ng2-pagination';
|
||||
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||
|
||||
import { PaginationComponent } from './pagination.component';
|
||||
import { PaginationOptions } from '../../core/cache/models/pagination-options.model';
|
||||
import { PaginationComponentOptions } from './pagination-component-options.model';
|
||||
import { MockTranslateLoader } from "../testing/mock-translate-loader";
|
||||
|
||||
import { GLOBAL_CONFIG, EnvConfig } from '../../../config';
|
||||
import { ActivatedRouteStub, RouterStub } from "../testing/router-stubs";
|
||||
import { HostWindowService } from "../host-window.service";
|
||||
import { EnumKeysPipe } from "../utils/enum-keys-pipe";
|
||||
import { SortOptions } from "../../core/cache/models/sort-options.model";
|
||||
|
||||
|
||||
function createTestComponent<T>(html: string, type: {new (...args: any[]): T}): ComponentFixture<T> {
|
||||
@@ -138,7 +140,7 @@ describe('Pagination component', () => {
|
||||
RouterTestingModule.withRoutes([
|
||||
{path: 'home', component: TestComponent}
|
||||
])],
|
||||
declarations: [PaginationComponent, TestComponent], // declare the test component
|
||||
declarations: [PaginationComponent, TestComponent, EnumKeysPipe], // declare the test component
|
||||
providers: [
|
||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||
{ provide: GLOBAL_CONFIG, useValue: EnvConfig },
|
||||
@@ -156,6 +158,7 @@ describe('Pagination component', () => {
|
||||
html = `
|
||||
<ds-pagination #p="paginationComponent"
|
||||
[paginationOptions]="paginationOptions"
|
||||
[sortOptions]="sortOptions"
|
||||
[collectionSize]="collectionSize"
|
||||
(pageChange)="pageChanged($event)"
|
||||
(pageSizeChange)="pageSizeChanged($event)">
|
||||
@@ -247,12 +250,12 @@ describe('Pagination component', () => {
|
||||
|
||||
changePage(testFixture, 3);
|
||||
tick();
|
||||
expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 3, pageSize: 10 } });
|
||||
expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 3, pageSize: 10, sortDirection: 0, sortField: 'name' } });
|
||||
expect(paginationComponent.currentPage).toEqual(3);
|
||||
|
||||
changePageSize(testFixture, '20');
|
||||
tick();
|
||||
expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 3, pageSize: 20 } });
|
||||
expect(routerStub.navigate).toHaveBeenCalledWith([], { queryParams: { pageId: 'test', page: 3, pageSize: 20, sortDirection: 0, sortField: 'name' } });
|
||||
expect(paginationComponent.pageSize).toEqual(20);
|
||||
}));
|
||||
|
||||
@@ -307,7 +310,8 @@ class TestComponent {
|
||||
|
||||
collection: string[] = [];
|
||||
collectionSize: number;
|
||||
paginationOptions = new PaginationOptions();
|
||||
paginationOptions = new PaginationComponentOptions();
|
||||
sortOptions = new SortOptions();
|
||||
|
||||
constructor() {
|
||||
this.collection = Array.from(new Array(100), (x, i) => `item ${i + 1}`);
|
||||
|
@@ -9,6 +9,7 @@ import {
|
||||
ViewEncapsulation
|
||||
} from '@angular/core'
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { Subscription } from "rxjs/Subscription";
|
||||
import { isNumeric } from "rxjs/util/isNumeric";
|
||||
import 'rxjs/add/operator/switchMap';
|
||||
import { Observable } from "rxjs";
|
||||
@@ -17,7 +18,9 @@ import { DEFAULT_TEMPLATE, DEFAULT_STYLES } from 'ng2-pagination/dist/template';
|
||||
|
||||
import { HostWindowService } from "../host-window.service";
|
||||
import { HostWindowState } from "../host-window.reducer";
|
||||
import { PaginationOptions } from '../../core/cache/models/pagination-options.model';
|
||||
import { PaginationComponentOptions } from './pagination-component-options.model';
|
||||
import { SortDirection, SortOptions } from "../../core/cache/models/sort-options.model";
|
||||
import { hasValue } from "../empty.util";
|
||||
|
||||
/**
|
||||
* The default pagination controls component.
|
||||
@@ -39,7 +42,12 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
/**
|
||||
* Configuration for the NgbPagination component.
|
||||
*/
|
||||
@Input() paginationOptions: PaginationOptions;
|
||||
@Input() paginationOptions: PaginationComponentOptions;
|
||||
|
||||
/**
|
||||
* Sort configuration for this component.
|
||||
*/
|
||||
@Input() sortOptions: SortOptions;
|
||||
|
||||
/**
|
||||
* An event fired when the page is changed.
|
||||
@@ -48,11 +56,34 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
@Output() pageChange: EventEmitter<number> = new EventEmitter<number>();
|
||||
|
||||
/**
|
||||
* An event fired when the page size is changed.
|
||||
* An event fired when the page wsize is changed.
|
||||
* Event's payload equals to the newly selected page size.
|
||||
*/
|
||||
@Output() pageSizeChange: EventEmitter<number> = new EventEmitter<number>();
|
||||
|
||||
/**
|
||||
* An event fired when the sort direction is changed.
|
||||
* Event's payload equals to the newly selected sort direction.
|
||||
*/
|
||||
@Output() sortDirectionChange: EventEmitter<SortDirection> = new EventEmitter<SortDirection>();
|
||||
|
||||
/**
|
||||
* An event fired when the sort field is changed.
|
||||
* Event's payload equals to the newly selected sort field.
|
||||
*/
|
||||
@Output() sortFieldChange: EventEmitter<string> = new EventEmitter<string>();
|
||||
|
||||
|
||||
/**
|
||||
* Option for hiding the gear
|
||||
*/
|
||||
@Input() public hideGear: boolean = false;
|
||||
|
||||
/**
|
||||
* Option for hiding the pager when there is less than 2 pages
|
||||
*/
|
||||
@Input() public hidePagerWhenSinglePage: boolean = true;
|
||||
|
||||
/**
|
||||
* Current page.
|
||||
*/
|
||||
@@ -84,20 +115,36 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
*/
|
||||
public pageSize: number = 10;
|
||||
|
||||
/**
|
||||
* Declare SortDirection enumeration to use it in the template
|
||||
*/
|
||||
public sortDirections = SortDirection
|
||||
|
||||
/**
|
||||
* A number array that represents options for a context pagination limit.
|
||||
*/
|
||||
private pageSizeOptions: Array<number>;
|
||||
|
||||
/**
|
||||
* Direction in which to sort: ascending or descending
|
||||
*/
|
||||
public sortDirection: SortDirection = SortDirection.Ascending;
|
||||
|
||||
/**
|
||||
* Name of the field that's used to sort by
|
||||
*/
|
||||
public sortField: string = "id";
|
||||
|
||||
/**
|
||||
* Local variable, which can be used in the template to access the paginate controls ngbDropdown methods and properties
|
||||
*/
|
||||
public paginationControls;
|
||||
|
||||
/**
|
||||
* Subscriber to observable.
|
||||
* Array to track all subscriptions and unsubscribe them onDestroy
|
||||
* @type {Array}
|
||||
*/
|
||||
private routeSubscription: any;
|
||||
private subs: Subscription[] = [];
|
||||
|
||||
/**
|
||||
* An object that represents pagination details of the current viewed page
|
||||
@@ -107,36 +154,34 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
total: null
|
||||
};
|
||||
|
||||
/**
|
||||
* Subscriber to observable.
|
||||
*/
|
||||
private stateSubscription: any;
|
||||
|
||||
/**
|
||||
* Method provided by Angular. Invoked after the constructor.
|
||||
*/
|
||||
ngOnInit() {
|
||||
this.stateSubscription = this.hostWindowService.isXs()
|
||||
this.subs.push(this.hostWindowService.isXs()
|
||||
.subscribe((status: boolean) => {
|
||||
this.isXs = status;
|
||||
});
|
||||
}));
|
||||
this.checkConfig(this.paginationOptions);
|
||||
this.id = this.paginationOptions.id || null;
|
||||
this.currentPage = this.paginationOptions.currentPage;
|
||||
this.pageSize = this.paginationOptions.pageSize;
|
||||
this.pageSizeOptions = this.paginationOptions.pageSizeOptions;
|
||||
|
||||
this.routeSubscription = this.route.queryParams
|
||||
.map(queryParams => queryParams)
|
||||
this.sortDirection = this.sortOptions.direction;
|
||||
this.sortField = this.sortOptions.field;
|
||||
this.subs.push(this.route.queryParams
|
||||
.filter(queryParams => hasValue(queryParams))
|
||||
.subscribe(queryParams => {
|
||||
this.currentQueryParams = queryParams;
|
||||
if (this.id == queryParams['pageId']
|
||||
&& (this.paginationOptions.currentPage != queryParams['page']
|
||||
|| this.paginationOptions.pageSize != queryParams['pageSize'])
|
||||
|| this.paginationOptions.pageSize != queryParams['pageSize']
|
||||
|| this.sortOptions.direction != queryParams['sortDirection']
|
||||
|| this.sortOptions.field != queryParams['sortField'] )
|
||||
) {
|
||||
this.validateParams(queryParams['page'], queryParams['pageSize']);
|
||||
this.validateParams(queryParams['page'], queryParams['pageSize'], queryParams['sortDirection'], queryParams['sortField']);
|
||||
}
|
||||
});
|
||||
}));
|
||||
this.setShowingDetail();
|
||||
}
|
||||
|
||||
@@ -144,8 +189,9 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
* Method provided by Angular. Invoked when the instance is destroyed.
|
||||
*/
|
||||
ngOnDestroy() {
|
||||
this.stateSubscription.unsubscribe();
|
||||
this.routeSubscription.unsubscribe();
|
||||
this.subs
|
||||
.filter(sub => hasValue(sub))
|
||||
.forEach(sub => sub.unsubscribe());
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -154,11 +200,9 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
* @param router
|
||||
* Router is a singleton service provided by Angular.
|
||||
*/
|
||||
constructor(
|
||||
private route: ActivatedRoute,
|
||||
constructor(private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
public hostWindowService: HostWindowService
|
||||
){
|
||||
public hostWindowService: HostWindowService) {
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -168,8 +212,8 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
* The page being navigated to.
|
||||
*/
|
||||
public doPageChange(page: number) {
|
||||
this.router.navigate([], { queryParams: Object.assign({}, this.currentQueryParams, { pageId: this.id, page: page, pageSize: this.pageSize }) });
|
||||
this.currentPage = page;
|
||||
this.updateRoute();
|
||||
this.setShowingDetail();
|
||||
this.pageChange.emit(page);
|
||||
}
|
||||
@@ -181,12 +225,53 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
* The new page size.
|
||||
*/
|
||||
public setPageSize(pageSize: number) {
|
||||
this.router.navigate([], { queryParams: Object.assign({}, this.currentQueryParams, { pageId: this.id, page: this.currentPage, pageSize: pageSize }) });
|
||||
this.pageSize = pageSize;
|
||||
this.updateRoute();
|
||||
this.setShowingDetail();
|
||||
this.pageSizeChange.emit(pageSize);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to set set new sort direction and update route parameters
|
||||
*
|
||||
* @param sortDirection
|
||||
* The new sort direction.
|
||||
*/
|
||||
public setSortDirection(sortDirection: SortDirection) {
|
||||
this.sortDirection = sortDirection;
|
||||
this.updateRoute();
|
||||
this.setShowingDetail();
|
||||
this.sortDirectionChange.emit(sortDirection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to set set new sort field and update route parameters
|
||||
*
|
||||
* @param sortField
|
||||
* The new sort field.
|
||||
*/
|
||||
public setSortField(field: string) {
|
||||
this.sortField = field;
|
||||
this.updateRoute();
|
||||
this.setShowingDetail();
|
||||
this.sortFieldChange.emit(field);
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to update the route parameters
|
||||
*/
|
||||
private updateRoute() {
|
||||
this.router.navigate([], {
|
||||
queryParams: Object.assign({}, this.currentQueryParams, {
|
||||
pageId: this.id,
|
||||
page: this.currentPage,
|
||||
pageSize: this.pageSize,
|
||||
sortDirection: this.sortDirection,
|
||||
sortField: this.sortField
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Method to set pagination details of the current viewed page.
|
||||
*/
|
||||
@@ -215,18 +300,31 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
* @param pageSize
|
||||
* The page size to validate
|
||||
*/
|
||||
private validateParams(page: any, pageSize: any) {
|
||||
private validateParams(page: any, pageSize: any, sortDirection: any, sortField: any) {
|
||||
let filteredPageSize = this.pageSizeOptions.find(x => x == pageSize);
|
||||
if (!isNumeric(page) || !filteredPageSize) {
|
||||
let filteredPage = isNumeric(page) ? page : this.currentPage;
|
||||
filteredPageSize = (filteredPageSize) ? filteredPageSize : this.pageSize;
|
||||
this.router.navigate([{ pageId: this.id, page: filteredPage, pageSize: filteredPageSize }]);
|
||||
this.router.navigate([], {
|
||||
queryParams: {
|
||||
pageId: this.id,
|
||||
page: filteredPage,
|
||||
pageSize: filteredPageSize,
|
||||
sortDirection: sortDirection,
|
||||
sortField: sortField
|
||||
}
|
||||
}
|
||||
);
|
||||
} else {
|
||||
// (+) converts string to a number
|
||||
this.currentPage = +page;
|
||||
this.pageSize = +pageSize;
|
||||
this.sortDirection = +sortDirection;
|
||||
this.sortField = sortField;
|
||||
this.pageChange.emit(this.currentPage);
|
||||
this.pageSizeChange.emit(this.pageSize);
|
||||
this.sortDirectionChange.emit(this.sortDirection);
|
||||
this.sortFieldChange.emit(this.sortField);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -238,10 +336,20 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
||||
*/
|
||||
private checkConfig(paginateOptions: any) {
|
||||
let required = ['id', 'currentPage', 'pageSize', 'pageSizeOptions'];
|
||||
let missing = required.filter(function (prop) { return !(prop in paginateOptions); });
|
||||
let missing = required.filter(function (prop) {
|
||||
return !(prop in paginateOptions);
|
||||
});
|
||||
if (0 < missing.length) {
|
||||
throw new Error("Paginate: Argument is missing the following required properties: " + missing.join(', '));
|
||||
}
|
||||
}
|
||||
|
||||
get hasMultiplePages(): boolean {
|
||||
return this.collectionSize > this.pageSize;
|
||||
}
|
||||
|
||||
get shouldShowBottomPager(): boolean {
|
||||
return this.hasMultiplePages || !this.hidePagerWhenSinglePage
|
||||
}
|
||||
}
|
||||
|
||||
|
@@ -17,6 +17,13 @@ import { NativeWindowFactory, NativeWindowService } from "./window.service";
|
||||
import { ComcolPageContentComponent } from "./comcol-page-content/comcol-page-content.component";
|
||||
import { ComcolPageHeaderComponent } from "./comcol-page-header/comcol-page-header.component";
|
||||
import { ComcolPageLogoComponent } from "./comcol-page-logo/comcol-page-logo.component";
|
||||
import { EnumKeysPipe } from "./utils/enum-keys-pipe";
|
||||
import { ObjectListComponent } from "./object-list/object-list.component";
|
||||
import { ObjectListElementComponent } from "../object-list/object-list-element/object-list-element.component";
|
||||
import { ItemListElementComponent } from "../object-list/item-list-element/item-list-element.component";
|
||||
import { CommunityListElementComponent } from "../object-list/community-list-element/community-list-element.component";
|
||||
import { CollectionListElementComponent } from "../object-list/collection-list-element/collection-list-element.component";
|
||||
import { TruncatePipe } from "./utils/truncate.pipe";
|
||||
|
||||
const MODULES = [
|
||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||
@@ -31,7 +38,9 @@ const MODULES = [
|
||||
|
||||
const PIPES = [
|
||||
FileSizePipe,
|
||||
SafeUrlPipe
|
||||
SafeUrlPipe,
|
||||
EnumKeysPipe,
|
||||
TruncatePipe
|
||||
// put pipes here
|
||||
];
|
||||
|
||||
@@ -41,7 +50,12 @@ const COMPONENTS = [
|
||||
ThumbnailComponent,
|
||||
ComcolPageContentComponent,
|
||||
ComcolPageHeaderComponent,
|
||||
ComcolPageLogoComponent
|
||||
ComcolPageLogoComponent,
|
||||
ObjectListComponent,
|
||||
ObjectListElementComponent,
|
||||
ItemListElementComponent,
|
||||
CollectionListElementComponent,
|
||||
CommunityListElementComponent
|
||||
];
|
||||
|
||||
const PROVIDERS = [
|
||||
|
13
src/app/shared/utils/enum-keys-pipe.ts
Normal file
13
src/app/shared/utils/enum-keys-pipe.ts
Normal file
@@ -0,0 +1,13 @@
|
||||
import { Pipe, PipeTransform } from "@angular/core";
|
||||
@Pipe({ name: 'dsKeys' })
|
||||
export class EnumKeysPipe implements PipeTransform {
|
||||
transform(value, args: string[]): any {
|
||||
let keys = [];
|
||||
for (var enumMember in value) {
|
||||
if (!isNaN(parseInt(enumMember, 10))) {
|
||||
keys.push({ key: +enumMember, value: value[enumMember] });
|
||||
}
|
||||
}
|
||||
return keys;
|
||||
}
|
||||
}
|
26
src/app/shared/utils/truncate.pipe.ts
Normal file
26
src/app/shared/utils/truncate.pipe.ts
Normal file
@@ -0,0 +1,26 @@
|
||||
import { Pipe, PipeTransform } from '@angular/core'
|
||||
import { hasValue } from "../empty.util";
|
||||
|
||||
/**
|
||||
* Pipe to truncate a value in Angular. (Take a substring, starting at 0)
|
||||
* Default value: 10
|
||||
*/
|
||||
@Pipe({
|
||||
name: 'dsTruncate'
|
||||
})
|
||||
export class TruncatePipe implements PipeTransform {
|
||||
|
||||
/**
|
||||
*
|
||||
*/
|
||||
transform(value: string, args: Array<string>) : string {
|
||||
if (hasValue(value)) {
|
||||
let limit = (args && args.length > 0) ? parseInt(args[0], 10) : 10; // 10 as default truncate value
|
||||
return value.length > limit ? value.substring(0, limit) + "..." : value;
|
||||
}
|
||||
else {
|
||||
return value;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -1,4 +1,4 @@
|
||||
<div class="thumbnail">
|
||||
<img *ngIf="thumbnail" [src]="thumbnail.retrieve"/>
|
||||
<img *ngIf="thumbnail" [src]="thumbnail.retrieve" (error)="errorHandler($event)"/>
|
||||
<img *ngIf="!thumbnail" [src]="holderSource | dsSafeUrl"/>
|
||||
</div>
|
@@ -1,4 +1,4 @@
|
||||
import { Component, Input, OnInit } from '@angular/core';
|
||||
import { Component, Input } from '@angular/core';
|
||||
import { Bitstream } from "../core/shared/bitstream.model";
|
||||
|
||||
/**
|
||||
@@ -28,7 +28,10 @@ export class ThumbnailComponent {
|
||||
}
|
||||
|
||||
universalInit() {
|
||||
}
|
||||
|
||||
errorHandler(event) {
|
||||
event.currentTarget.src = this.holderSource;
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user