diff --git a/config/environment.default.js b/config/environment.default.js index cddd204eea..b3f0756378 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -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": { diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 82a51f2ebd..9714640287 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -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 }}" diff --git a/src/app/collection-page/collection-page.component.html b/src/app/collection-page/collection-page.component.html index 64520d3e84..a8345ef23d 100644 --- a/src/app/collection-page/collection-page.component.html +++ b/src/app/collection-page/collection-page.component.html @@ -1,32 +1,44 @@ -
- - - - - - - - +
+ + + + + + + + - - - + + - - - + + - - - + + - + +
+
+
+

{{'collection.page.browse.recent.head' | translate}}

+ +
diff --git a/src/app/collection-page/collection-page.component.ts b/src/app/collection-page/collection-page.component.ts index a7148cbc74..f636f9dd51 100644 --- a/src/app/collection-page/collection-page.component.ts +++ b/src/app/collection-page/collection-page.component.ts @@ -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; + itemData: RemoteData; logoData: RemoteData; + 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.collectionData.payload - .subscribe(collection => this.logoData = collection.logo)); - }); + 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(); + } } diff --git a/src/app/collection-page/collection-page.module.ts b/src/app/collection-page/collection-page.module.ts index 0dfe33fd5a..95e4a52540 100644 --- a/src/app/collection-page/collection-page.module.ts +++ b/src/app/collection-page/collection-page.module.ts @@ -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, diff --git a/src/app/community-page/community-page.component.ts b/src/app/community-page/community-page.component.ts index e1434fb21f..d3bd8aeeeb 100644 --- a/src/app/community-page/community-page.component.ts +++ b/src/app/community-page/community-page.component.ts @@ -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,9 +34,11 @@ 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() { } -} \ No newline at end of file +} diff --git a/src/app/core/cache/builders/build-decorators.ts b/src/app/core/cache/builders/build-decorators.ts index f00d8d87e5..5cd9a740dc 100644 --- a/src/app/core/cache/builders/build-decorators.ts +++ b/src/app/core/cache/builders/build-decorators.ts @@ -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); }; diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 00e99940e1..f8b4694a89 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -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) => ( entry.response).errorMessage) .distinctUntilChanged(); - const payload = this.objectCache.getBySelfLink(href, normalizedType) - .map((normalized: TNormalized) => { - return this.build(normalized); + 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) => ( 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(href, normalizedType).startWith(undefined), + responseCacheObs + .filter((entry: ResponseCacheEntry) => entry.response.isSuccessful) + .map((entry: ResponseCacheEntry) => ( entry.response).resourceUUIDs) + .flatMap((resourceUUIDs: Array) => { + 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(normalized); }); + return new RemoteData( href, requestPending, responsePending, isSuccessFul, errorMessage, + statusCode, + pageInfo, payload ); } @@ -90,6 +127,15 @@ export class RemoteDataBuildService { .map((entry: ResponseCacheEntry) => ( 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) => ( entry.response).pageInfo) + .distinctUntilChanged(); + const payload = responseCacheObs .filter((entry: ResponseCacheEntry) => entry.response.isSuccessful) .map((entry: ResponseCacheEntry) => ( 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,7 +185,12 @@ export class RemoteDataBuildService { rdArr.push(this.buildSingle(href, resourceConstructor)); }); - links[relationship] = this.aggregate(rdArr); + if (rdArr.length === 1) { + links[relationship] = rdArr[0]; + } + else { + links[relationship] = this.aggregate(rdArr); + } } else { // without the setTimeout, the actions inside requestService.configure @@ -146,7 +199,14 @@ export class RemoteDataBuildService { this.requestService.configure(new Request(normalized[relationship])); },0); - links[relationship] = this.buildSingle(normalized[relationship], resourceConstructor); + // 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); + } } } }); @@ -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.combineLatest( ...input.map(rd => rd.payload) ); @@ -196,6 +270,8 @@ export class RemoteDataBuildService { responsePending, isSuccessFul, errorMessage, + statusCode, + pageInfo, payload ); } diff --git a/src/app/core/cache/models/normalized-bitstream-format.model.ts b/src/app/core/cache/models/normalized-bitstream-format.model.ts new file mode 100644 index 0000000000..188bd3d185 --- /dev/null +++ b/src/app/core/cache/models/normalized-bitstream-format.model.ts @@ -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; + } +} diff --git a/src/app/core/cache/models/normalized-bitstream.model.ts b/src/app/core/cache/models/normalized-bitstream.model.ts index c89cf5dc05..3b5c4fdc85 100644 --- a/src/app/core/cache/models/normalized-bitstream.model.ts +++ b/src/app/core/cache/models/normalized-bitstream.model.ts @@ -1,50 +1,60 @@ 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(?) - */ - @autoserialize - size: number; + /** + * The size of this bitstream in bytes + */ + @autoserialize + sizeBytes: number; - /** - * The relative path to this Bitstream's file - */ - @autoserialize - url: string; + /** + * The relative path to this Bitstream's file + */ + @autoserialize + retrieve: string; - /** - * The mime type of this Bitstream - */ - @autoserialize - mimetype: string; + /** + * The mime type of this Bitstream + */ + @autoserialize + mimetype: string; - /** - * The format of this Bitstream - */ - format: string; + /** + * The format of this Bitstream + */ + @autoserialize + format: string; - /** - * The description of this Bitstream - */ - description: string; + /** + * The description of this Bitstream + */ + @autoserialize + description: string; - /** - * An array of Bundles that are direct parents of this Bitstream - */ - parents: Array; + /** + * An array of Bundles that are direct parents of this Bitstream + */ + @autoserialize + @relationship(ResourceType.Item, true) + parents: Array; - /** - * The Bundle that owns this Bitstream - */ - owner: string; + /** + * The Bundle that owns this Bitstream + */ + @autoserialize + @relationship(ResourceType.Item, false) + owner: string; - @autoserialize - retrieve: string; + /** + * The name of the Bundle this Bitstream is part of + */ + @autoserialize + bundleName: string; } diff --git a/src/app/core/cache/models/normalized-bundle.model.ts b/src/app/core/cache/models/normalized-bundle.model.ts index 6333428227..839f09b247 100644 --- a/src/app/core/cache/models/normalized-bundle.model.ts +++ b/src/app/core/cache/models/normalized-bundle.model.ts @@ -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; } diff --git a/src/app/core/cache/models/normalized-collection.model.ts b/src/app/core/cache/models/normalized-collection.model.ts index 2114e4cc43..669c02e383 100644 --- a/src/app/core/cache/models/normalized-collection.model.ts +++ b/src/app/core/cache/models/normalized-collection.model.ts @@ -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; /** - * 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; } diff --git a/src/app/core/cache/models/normalized-community.model.ts b/src/app/core/cache/models/normalized-community.model.ts index c5005a25e0..1a6b0fea9e 100644 --- a/src/app/core/cache/models/normalized-community.model.ts +++ b/src/app/core/cache/models/normalized-community.model.ts @@ -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; /** * The Community that owns this Community */ + @autoserialize + @relationship(ResourceType.Community, false) owner: string; @autoserialize - @relationship(ResourceType.Collection) + @relationship(ResourceType.Collection, true) collections: Array; } diff --git a/src/app/core/cache/models/normalized-dspace-object.model.ts b/src/app/core/cache/models/normalized-dspace-object.model.ts index f4b879c01a..259bb5e097 100644 --- a/src/app/core/cache/models/normalized-dspace-object.model.ts +++ b/src/app/core/cache/models/normalized-dspace-object.model.ts @@ -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; diff --git a/src/app/core/cache/models/normalized-item.model.ts b/src/app/core/cache/models/normalized-item.model.ts index 15b4822cdf..a4e52110ff 100644 --- a/src/app/core/cache/models/normalized-item.model.ts +++ b/src/app/core/cache/models/normalized-item.model.ts @@ -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; /** * The Collection that owns this Item */ - owner: string; + @relationship(ResourceType.Collection, false) + owningCollection: string; @autoserialize - @relationship(ResourceType.Bundle) - bundles: Array; + @relationship(ResourceType.Bitstream, true) + bitstreams: Array; } diff --git a/src/app/core/cache/models/normalized-object-factory.ts b/src/app/core/cache/models/normalized-object-factory.ts index 45eaacbf5c..84bd2061cb 100644 --- a/src/app/core/cache/models/normalized-object-factory.ts +++ b/src/app/core/cache/models/normalized-object-factory.ts @@ -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 { + public static getConstructor(type: ResourceType): GenericConstructor { 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 } diff --git a/src/app/core/cache/models/normalized-object.model.ts b/src/app/core/cache/models/normalized-object.model.ts new file mode 100644 index 0000000000..055996ab47 --- /dev/null +++ b/src/app/core/cache/models/normalized-object.model.ts @@ -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; + +} diff --git a/src/app/core/cache/models/sort-options.model.ts b/src/app/core/cache/models/sort-options.model.ts index dff65c3d35..27d45139e5 100644 --- a/src/app/core/cache/models/sort-options.model.ts +++ b/src/app/core/cache/models/sort-options.model.ts @@ -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) { + + } } diff --git a/src/app/core/cache/object-cache.service.spec.ts b/src/app/core/cache/object-cache.service.spec.ts index 827e39ab7e..dd77cab110 100644 --- a/src/app/core/cache/object-cache.service.spec.ts +++ b/src/app/core/cache/object-cache.service.spec.ts @@ -20,6 +20,7 @@ describe("ObjectCacheService", () => { let store: Store; 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)); }); }); diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts index 4208b4cada..b4137e8a22 100644 --- a/src/app/core/cache/response-cache.models.ts +++ b/src/app/core/cache/response-cache.models.ts @@ -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) { - super(true); + constructor( + public resourceUUIDs: Array, + 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; } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index be8da28571..c0a5e9d622 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -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 diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 232345d2be..53530cbcb3 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -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 { - 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 + protected store: Store, + @Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig ) { - super(NormalizedCollection); + super(NormalizedCollection, EnvConfig); } } diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 6e63597407..996b17eab5 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -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 { - 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 + protected store: Store, + @Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig ) { - super(NormalizedCommunity); + super(NormalizedCommunity, EnvConfig); } } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 43db6cc4a2..05e7066290 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -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 { protected abstract objectCache: ObjectCacheService; @@ -17,30 +20,61 @@ export abstract class DataService protected abstract requestService: RequestService; protected abstract rdbService: RemoteDataBuildService; protected abstract store: Store; - protected abstract endpoint: string; + protected abstract resourceEndpoint: string; + protected abstract browseEndpoint: string; - constructor(private normalizedResourceType: GenericConstructor) { + constructor( + private normalizedResourceType: GenericConstructor, + 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; + } + + 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(scopeID?: string): RemoteData> { - const href = this.getFindAllHref(scopeID); - const request = new FindAllRequest(href, scopeID); + findAll(options: FindAllOptions = {}): RemoteData> { + const href = this.getFindAllHref(options); + const request = new FindAllRequest(href, options); this.requestService.configure(request); return this.rdbService.buildList(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 { diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index fc13999f37..88bb1506c8 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -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 { - 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 + protected store: Store, + @Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig ) { - super(NormalizedItem); + super(NormalizedItem, EnvConfig); } } diff --git a/src/app/core/data/remote-data.ts b/src/app/core/data/remote-data.ts index 7fa02bf25c..7f0cf06979 100644 --- a/src/app/core/data/remote-data.ts +++ b/src/app/core/data/remote-data.ts @@ -1,10 +1,11 @@ import { Observable } from "rxjs"; +import { PageInfo } from "../shared/page-info.model"; export enum RemoteDataState { - RequestPending, - ResponsePending, - Failed, - Success + RequestPending = "RequestPending", + ResponsePending = "ResponsePending", + Failed = "Failed", + Success = "Success" } /** @@ -17,6 +18,8 @@ export class RemoteData { private responsePending: Observable, private isSuccessFul: Observable, public errorMessage: Observable, + public statusCode: Observable, + public pageInfo: Observable, public payload: Observable ) { } diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index f010f2e59c..dcfa4df66f 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -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 + 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) => 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 { + 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; + } + else { + 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): Array { - let type: ResourceType; - const isArray = Array.isArray(obj); - - if (isArray && isEmpty(obj)) { - return []; - } - - if (isArray) { - type = obj[0]["type"]; - } - else { - type = obj["type"]; + 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); - } + let processed; + if (isNotEmpty(obj._embedded)) { + processed = this.process(obj._embedded, requestHref); + } + let normalizedObj = serializer.deserialize(obj); + + if (isNotEmpty(processed)) { + let linksOnly = {}; + Object.keys(processed).forEach(key => { + linksOnly[key] = processed[key].map((no: NormalizedObject) => no.self); }); - const normalizedObjArr = serializer.deserializeArray(obj); - normalizedObjArr.forEach(t => this.addToObjectCache(t, requestHref)); - return normalizedObjArr.map(t => t.uuid); - } - else { - if (isNotEmpty(obj._embedded)) { - this.processEmbedded(obj._embedded, requestHref); - } - const normalizedObj = serializer.deserialize(obj); - this.addToObjectCache(normalizedObj, requestHref); - return [normalizedObj.uuid]; + Object.assign(normalizedObj, linksOnly); } + this.addToObjectCache(normalizedObj, requestHref); + 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; + } + } + } diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index eb74f4b35d..f33db5ab72 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -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 { @@ -17,13 +17,22 @@ export class FindByIDRequest extends Request { } } +export class FindAllOptions { + scopeID?: string; + elementsPerPage?: number; + currentPage?: number; + sort?: SortOptions; +} + export class FindAllRequest extends Request { constructor( href: string, - public scopeID?: string, - public paginationOptions?: PaginationOptions, - public sortOptions?: SortOptions + public options?: FindAllOptions, ) { super(href); } } + +export class RequestError extends Error { + statusText: string; +} diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2-response.model.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2-response.model.ts index 80567bdf7c..01af2a2c2b 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2-response.model.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2-response.model.ts @@ -1,4 +1,8 @@ export interface DSpaceRESTV2Response { - _embedded?: any; - _links?: any; + payload: { + _embedded?: any; + _links?: any; + page?: any; + }, + statusCode: string } diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts index 67914c2a92..097717b5e2 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.spec.ts @@ -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); }); }); diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts index b9f1a0be14..0cd1f9edad 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.serializer.ts @@ -26,10 +26,8 @@ export class DSpaceRESTv2Serializer implements Serializer { * @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 implements Serializer { * @param models The array of models to serialize * @returns An object to send to the backend */ - serializeArray(models: Array): DSpaceRESTV2Response { - return { - "_embedded": Serialize(models, this.modelType) - }; + serializeArray(models: Array): any { + return Serialize(models, this.modelType); } /** diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts index d8a21c6c9d..400a5c851e 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts @@ -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} - * An Observablse containing the response from the server + * An Observable containing the response from the server */ - get(relativeURL: string, options?: RequestOptionsArgs): Observable { - return this.http.get(new RESTURLCombiner(this.EnvConfig, relativeURL).toString(), options) - .map(res => res.json()) + get(absoluteURL: string, options?: RequestOptionsArgs): Observable { + return this.http.get(absoluteURL, options) + .map(res => ({ payload: res.json(), statusCode: res.statusText })) .catch(err => { console.log('Error: ', err); return Observable.throw(err); diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index 06bdf41f30..8e7f6204a3 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -1,42 +1,42 @@ 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(?) - */ - size: number; + /** + * The size of this bitstream in bytes + */ + sizeBytes: number; - /** - * The relative path to this Bitstream's file - */ - url: string; + /** + * The mime type of this Bitstream + */ + mimetype: string; - /** - * The mime type of this Bitstream - */ - mimetype: string; + /** + * The description of this Bitstream + */ + description: string; - /** - * The description of this Bitstream - */ - description: string; + /** + * The name of the Bundle this Bitstream is part of + */ + bundleName: string; - /** - * An array of Bundles that are direct parents of this Bitstream - */ - parents: RemoteData; + /** + * An array of Items that are direct parents of this Bitstream + */ + parents: RemoteData; - /** - * The Bundle that owns this Bitstream - */ - owner: Bundle; + /** + * The Bundle that owns this Bitstream + */ + owner: RemoteData; - /** - * The Bundle that owns this Bitstream - */ - retrieve: string; + /** + * The URL to retrieve this Bitstream's file + */ + retrieve: string; } diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index 8f3a284fb6..9d8e3855b8 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -17,7 +17,7 @@ export class Bundle extends DSpaceObject { /** * The Item that owns this Bundle */ - owner: Item; + owner: RemoteData; bitstreams: RemoteData diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index 30814726b8..43d5ff523c 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -63,7 +63,7 @@ export class Collection extends DSpaceObject { /** * The Collection that owns this Collection */ - owner: Collection; + owner: RemoteData; items: RemoteData; diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index 67c8211fd2..bd82c2e05f 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -55,7 +55,7 @@ export class Community extends DSpaceObject { /** * The Community that owns this Community */ - owner: Community; + owner: RemoteData; collections: RemoteData; diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index ca7c67207a..d3c698ac2f 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -44,7 +44,7 @@ export abstract class DSpaceObject implements CacheableObject { /** * The DSpaceObject that owns this DSpaceObject */ - owner: DSpaceObject; + owner: RemoteData; /** * Find a metadata field by key and language diff --git a/src/app/core/shared/item.model.spec.ts b/src/app/core/shared/item.model.spec.ts index e20bd6e592..d10b7d3046 100644 --- a/src/app/core/shared/item.model.spec.ts +++ b/src/app/core/shared/item.model.spec.ts @@ -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 = 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 = 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 = 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 = 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 = 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 = 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 + ); -} \ No newline at end of file +} diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index a3e098eed9..e57c6aec4e 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -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; - bundles: RemoteData; + get owner(): RemoteData { + return this.owningCollection; + } + + bitstreams: RemoteData; /** @@ -46,57 +54,44 @@ export class Item extends DSpaceObject { * @returns {Observable} the primaryBitstream of the "THUMBNAIL" bundle */ getThumbnail(): Observable { - const bundle: Observable = 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]) } - /** - * Retrieves the thumbnail for the given original of this item - * @returns {Observable} the primaryBitstream of the "THUMBNAIL" bundle - */ - getThumbnailForOriginal(original: Bitstream): Observable { - const bundle: Observable = this.getBundle("THUMBNAIL"); - return bundle - .filter(bundle => hasValue(bundle)) - .flatMap(bundle => bundle - .bitstreams.payload.map(files => files - .find(thumbnail => thumbnail - .name.startsWith(original.name) - ) - ) - ) - .startWith(undefined);; + /** + * Retrieves the thumbnail for the given original of this item + * @returns {Observable} the primaryBitstream of the "THUMBNAIL" bundle + */ + getThumbnailForOriginal(original: Bitstream): Observable { + return this.getBitstreamsByBundleName("THUMBNAIL").map(files => files + .find(thumbnail => thumbnail + .name.startsWith(original.name) + ) + ).startWith(undefined); + } + + /** + * Retrieves all files that should be displayed on the item page of this item + * @returns {Observable>>} an array of all Bitstreams in the "ORIGINAL" bundle + */ + getFiles(): Observable { + return this.getBitstreamsByBundleName("ORIGINAL"); } - /** - * Retrieves all files that should be displayed on the item page of this item - * @returns {Observable>>} an array of all Bitstreams in the "ORIGINAL" bundle - */ - getFiles(name: String = "ORIGINAL"): Observable { - const bundle: Observable = this.getBundle(name); - return bundle - .filter(bundle => hasValue(bundle)) - .flatMap(bundle => bundle.bitstreams.payload) - .startWith([]); - } - - /** - * Retrieves the bundle of this item by its name - * @param name The name of the Bundle that should be returned - * @returns {Observable} the Bundle that belongs to this item with the given name - */ - getBundle(name: String): Observable { - return this.bundles.payload - .filter(bundles => hasValue(bundles)) - .map(bundles => { - return bundles.find((bundle: Bundle) => { - return bundle.name === name - }); - }) - .startWith(undefined); + /** + * Retrieves bitstreams by bundle name + * @param bundleName The name of the Bundle that should be returned + * @returns {Observable} the bitstreams with the given bundleName + */ + getBitstreamsByBundleName(bundleName: string): Observable { + return this.bitstreams.payload.startWith([]) + .map(bitstreams => bitstreams + .filter(bitstream => bitstream.bundleName === bundleName) + ); } } diff --git a/src/app/core/shared/page-info.model.ts b/src/app/core/shared/page-info.model.ts new file mode 100644 index 0000000000..b911c8b276 --- /dev/null +++ b/src/app/core/shared/page-info.model.ts @@ -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; +} diff --git a/src/app/core/shared/resource-type.ts b/src/app/core/shared/resource-type.ts index 2e180cba71..182397d87c 100644 --- a/src/app/core/shared/resource-type.ts +++ b/src/app/core/shared/resource-type.ts @@ -5,6 +5,7 @@ export enum ResourceType { Bundle = "bundle", Bitstream = "bitstream", + BitstreamFormat = "bitstreamformat", Item = "item", Collection = "collection", Community = "community" diff --git a/src/app/home/home.module.ts b/src/app/home/home.module.ts index cbb785c1c4..4128c91097 100644 --- a/src/app/home/home.module.ts +++ b/src/app/home/home.module.ts @@ -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: [ diff --git a/src/app/home/top-level-community-list/top-level-community-list.component.html b/src/app/home/top-level-community-list/top-level-community-list.component.html index 7fe291ba87..625cb5118d 100644 --- a/src/app/home/top-level-community-list/top-level-community-list.component.html +++ b/src/app/home/top-level-community-list/top-level-community-list.component.html @@ -1,12 +1,10 @@

{{'home.top-level-communities.head' | translate}}

{{'home.top-level-communities.help' | translate}}

- +
diff --git a/src/app/home/top-level-community-list/top-level-community-list.component.ts b/src/app/home/top-level-community-list/top-level-community-list.component.ts index 87f9fad517..c502c53591 100644 --- a/src/app/home/top-level-community-list/top-level-community-list.component.ts +++ b/src/app/home/top-level-community-list/top-level-community-list.component.ts @@ -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; + 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(); } } diff --git a/src/app/item-page/field-components/collections/collections.component.html b/src/app/item-page/field-components/collections/collections.component.html index 8f184817c2..bb7ab63341 100644 --- a/src/app/item-page/field-components/collections/collections.component.html +++ b/src/app/item-page/field-components/collections/collections.component.html @@ -1,6 +1,6 @@ diff --git a/src/app/item-page/field-components/collections/collections.component.ts b/src/app/item-page/field-components/collections/collections.component.ts index 199fb2b773..895fe79bf8 100644 --- a/src/app/item-page/field-components/collections/collections.component.ts +++ b/src/app/item-page/field-components/collections/collections.component.ts @@ -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 = "
" + separator: string = "
"; collections: Observable; - 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]); } diff --git a/src/app/item-page/full/field-components/file-section/full-file-section.component.html b/src/app/item-page/full/field-components/file-section/full-file-section.component.html index d7e1712b7f..9a83442dd9 100644 --- a/src/app/item-page/full/field-components/file-section/full-file-section.component.html +++ b/src/app/item-page/full/field-components/file-section/full-file-section.component.html @@ -9,7 +9,7 @@
{{file.name}}
{{"item.page.filesection.size" | translate}}
-
{{(file.size) | dsFileSize }}
+
{{(file.sizeBytes) | dsFileSize }}
{{"item.page.filesection.format" | translate}}
@@ -21,7 +21,7 @@ diff --git a/src/app/item-page/full/field-components/file-section/full-file-section.component.ts b/src/app/item-page/full/field-components/file-section/full-file-section.component.ts index 3723bc5450..17208e8b20 100644 --- a/src/app/item-page/full/field-components/file-section/full-file-section.component.ts +++ b/src/app/item-page/full/field-components/file-section/full-file-section.component.ts @@ -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 => diff --git a/src/app/item-page/simple/field-components/file-section/file-section.component.html b/src/app/item-page/simple/field-components/file-section/file-section.component.html index 149a1b2017..aeceeab53a 100644 --- a/src/app/item-page/simple/field-components/file-section/file-section.component.html +++ b/src/app/item-page/simple/field-components/file-section/file-section.component.html @@ -1,8 +1,8 @@ diff --git a/src/app/object-list/collection-list-element/collection-list-element.component.html b/src/app/object-list/collection-list-element/collection-list-element.component.html new file mode 100644 index 0000000000..c680743716 --- /dev/null +++ b/src/app/object-list/collection-list-element/collection-list-element.component.html @@ -0,0 +1,6 @@ + + {{collection.name}} + +
+ {{collection.shortDescription}} +
diff --git a/src/app/object-list/collection-list-element/collection-list-element.component.scss b/src/app/object-list/collection-list-element/collection-list-element.component.scss new file mode 100644 index 0000000000..ad84b72f8c --- /dev/null +++ b/src/app/object-list/collection-list-element/collection-list-element.component.scss @@ -0,0 +1 @@ +@import '../../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/object-list/collection-list-element/collection-list-element.component.ts b/src/app/object-list/collection-list-element/collection-list-element.component.ts new file mode 100644 index 0000000000..c199994d5c --- /dev/null +++ b/src/app/object-list/collection-list-element/collection-list-element.component.ts @@ -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() { + + } + +} diff --git a/src/app/object-list/community-list-element/community-list-element.component.html b/src/app/object-list/community-list-element/community-list-element.component.html new file mode 100644 index 0000000000..2c2f4fc35f --- /dev/null +++ b/src/app/object-list/community-list-element/community-list-element.component.html @@ -0,0 +1,6 @@ + + {{community.name}} + +
+ {{community.shortDescription}} +
diff --git a/src/app/object-list/community-list-element/community-list-element.component.scss b/src/app/object-list/community-list-element/community-list-element.component.scss new file mode 100644 index 0000000000..ad84b72f8c --- /dev/null +++ b/src/app/object-list/community-list-element/community-list-element.component.scss @@ -0,0 +1 @@ +@import '../../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/object-list/community-list-element/community-list-element.component.ts b/src/app/object-list/community-list-element/community-list-element.component.ts new file mode 100644 index 0000000000..36e8a06e0f --- /dev/null +++ b/src/app/object-list/community-list-element/community-list-element.component.ts @@ -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() { + + } + +} diff --git a/src/app/object-list/item-list-element/item-list-element.component.html b/src/app/object-list/item-list-element/item-list-element.component.html new file mode 100644 index 0000000000..517cae480f --- /dev/null +++ b/src/app/object-list/item-list-element/item-list-element.component.html @@ -0,0 +1,14 @@ + + {{item.findMetadata("dc.title")}} + +
+ + + {{authorMd.value}} + ; + + + ({{item.findMetadata("dc.publisher")}}, {{item.findMetadata("dc.date.issued")}}) + +
{{item.findMetadata("dc.description.abstract") | dsTruncate:[200] }}
+
diff --git a/src/app/object-list/item-list-element/item-list-element.component.scss b/src/app/object-list/item-list-element/item-list-element.component.scss new file mode 100644 index 0000000000..ad84b72f8c --- /dev/null +++ b/src/app/object-list/item-list-element/item-list-element.component.scss @@ -0,0 +1 @@ +@import '../../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/object-list/item-list-element/item-list-element.component.ts b/src/app/object-list/item-list-element/item-list-element.component.ts new file mode 100644 index 0000000000..3f09cc200c --- /dev/null +++ b/src/app/object-list/item-list-element/item-list-element.component.ts @@ -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() { + + } + +} diff --git a/src/app/object-list/object-list-element/object-list-element.component.html b/src/app/object-list/object-list-element/object-list-element.component.html new file mode 100644 index 0000000000..5feb69b3a6 --- /dev/null +++ b/src/app/object-list/object-list-element/object-list-element.component.html @@ -0,0 +1,5 @@ +
+ + + +
\ No newline at end of file diff --git a/src/app/object-list/object-list-element/object-list-element.component.scss b/src/app/object-list/object-list-element/object-list-element.component.scss new file mode 100644 index 0000000000..6bdc45f30f --- /dev/null +++ b/src/app/object-list/object-list-element/object-list-element.component.scss @@ -0,0 +1,7 @@ +@import '../../../styles/variables.scss'; +@import '../../../../node_modules/bootstrap/scss/variables'; + +:host { + display: block; + margin-bottom: $spacer-y; +} diff --git a/src/app/object-list/object-list-element/object-list-element.component.ts b/src/app/object-list/object-list-element/object-list-element.component.ts new file mode 100644 index 0000000000..8b53ca5d6a --- /dev/null +++ b/src/app/object-list/object-list-element/object-list-element.component.ts @@ -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() { + + } + +} diff --git a/src/app/object-list/object-list.component.html b/src/app/object-list/object-list.component.html new file mode 100644 index 0000000000..b0ca991a93 --- /dev/null +++ b/src/app/object-list/object-list.component.html @@ -0,0 +1,16 @@ + +
    +
  • + +
  • +
+ +
diff --git a/src/app/object-list/object-list.component.scss b/src/app/object-list/object-list.component.scss new file mode 100644 index 0000000000..b14c7376e3 --- /dev/null +++ b/src/app/object-list/object-list.component.scss @@ -0,0 +1 @@ +@import '../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/shared/comcol-page-logo/comcol-page-logo.component.html b/src/app/shared/comcol-page-logo/comcol-page-logo.component.html index 81769c3c0f..069f748d7a 100644 --- a/src/app/shared/comcol-page-logo/comcol-page-logo.component.html +++ b/src/app/shared/comcol-page-logo/comcol-page-logo.component.html @@ -1,3 +1,3 @@ \ No newline at end of file + + diff --git a/src/app/shared/comcol-page-logo/comcol-page-logo.component.ts b/src/app/shared/comcol-page-logo/comcol-page-logo.component.ts index 87239e3a11..582d6e1741 100644 --- a/src/app/shared/comcol-page-logo/comcol-page-logo.component.ts +++ b/src/app/shared/comcol-page-logo/comcol-page-logo.component.ts @@ -12,4 +12,14 @@ export class ComcolPageLogoComponent { @Input() logo: Bitstream; @Input() alternateText: string; -} \ No newline at end of file + + /** + * 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; + } +} diff --git a/src/app/shared/object-list/object-list.component.ts b/src/app/shared/object-list/object-list.component.ts new file mode 100644 index 0000000000..7e404d0f51 --- /dev/null +++ b/src/app/shared/object-list/object-list.component.ts @@ -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; + @Input() config : PaginationComponentOptions; + @Input() sortConfig : SortOptions; + @Input() hideGear : boolean = false; + @Input() hidePagerWhenSinglePage : boolean = true; + pageInfo : Observable; + + /** + * An event fired when the page is changed. + * Event's payload equals to the newly selected page. + */ + @Output() pageChange: EventEmitter = new EventEmitter(); + + /** + * An event fired when the page wsize is changed. + * Event's payload equals to the newly selected page size. + */ + @Output() pageSizeChange: EventEmitter = new EventEmitter(); + + /** + * An event fired when the sort direction is changed. + * Event's payload equals to the newly selected sort direction. + */ + @Output() sortDirectionChange: EventEmitter = new EventEmitter(); + + /** + * An event fired when the sort field is changed. + * Event's payload equals to the newly selected sort field. + */ + @Output() sortFieldChange: EventEmitter = new EventEmitter(); + 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); + } + +} diff --git a/src/app/core/cache/models/pagination-options.model.ts b/src/app/shared/pagination/pagination-component-options.model.ts similarity index 86% rename from src/app/core/cache/models/pagination-options.model.ts rename to src/app/shared/pagination/pagination-component-options.model.ts index 211e9b392d..86310ece17 100644 --- a/src/app/core/cache/models/pagination-options.model.ts +++ b/src/app/shared/pagination/pagination-component-options.model.ts @@ -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. diff --git a/src/app/shared/pagination/pagination.component.html b/src/app/shared/pagination/pagination.component.html index adaab7fcd9..33c3dba1f3 100644 --- a/src/app/shared/pagination/pagination.component.html +++ b/src/app/shared/pagination/pagination.component.html @@ -1,4 +1,4 @@ -
+
{{ 'pagination.showing.label' | translate }} @@ -9,7 +9,9 @@
@@ -18,7 +20,7 @@ -