diff --git a/.travis.yml b/.travis.yml index e009520b68..119151e71d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -18,14 +18,16 @@ cache: directories: - node_modules +bundler_args: --retry 5 + before_install: - - yarn run global + - travis_retry yarn run global install: - - yarn install + - travis_retry yarn install before_script: - - yarn run build + - travis_wait yarn run build - export CHROME_BIN=chromium-browser - export DISPLAY=:99.0 - sh -e /etc/init.d/xvfb start diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 95358dc446..7489228eb5 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -4,7 +4,12 @@ "link.dspace": "DSpace software", "link.duraspace": "DuraSpace" }, - + "collection": { + "page": { + "news": "News", + "license": "License" + } + }, "item": { "page": { "author": "Author", @@ -12,24 +17,31 @@ "date": "Date", "uri": "URI", "files": "Files", - "collections": "Collections" + "collections": "Collections", + "filesection": { + "download": "Download", + "name": "Name:", + "format": "Format:", + "size": "Size:", + "description": "Description:" + }, + "link": { + "simple": "Simple item page", + "full": "Full item page" + } } }, - "nav": { "home": "Home" }, - "pagination": { "results-per-page": "Results Per Page", "showing": { - "label" : "Now showing items ", + "label": "Now showing items ", "detail": "{{ range }} of {{ total }}" } }, - "title": "DSpace", - "404": { "help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ", "page-not-found": "page not found", @@ -37,7 +49,6 @@ "home-page": "Take me to the home page" } }, - "home": { "top-level-communities": { "head": "Communities in DSpace", diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 42304c865e..95bfc73517 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -9,6 +9,7 @@ import { SharedModule } from './shared/shared.module'; import { AppRoutingModule } from './app-routing.module'; import { AppComponent } from './app.component'; import { HeaderComponent } from './header/header.component'; +import { CollectionPageModule } from './collection-page/collection-page.module'; import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; @@ -22,6 +23,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component'; SharedModule, HomeModule, ItemPageModule, + CollectionPageModule, CoreModule.forRoot(), AppRoutingModule ], diff --git a/src/app/collection-page/collection-page-routing.module.ts b/src/app/collection-page/collection-page-routing.module.ts new file mode 100644 index 0000000000..ac8c9b9cb5 --- /dev/null +++ b/src/app/collection-page/collection-page-routing.module.ts @@ -0,0 +1,13 @@ +import { NgModule } from '@angular/core'; +import { RouterModule } from '@angular/router'; + +import { CollectionPageComponent } from './collection-page.component'; + +@NgModule({ + imports: [ + RouterModule.forChild([ + { path: 'collections/:id', component: CollectionPageComponent } + ]) + ] +}) +export class CollectionPageRoutingModule { } diff --git a/src/app/collection-page/collection-page.component.html b/src/app/collection-page/collection-page.component.html new file mode 100644 index 0000000000..e9a4ab9916 --- /dev/null +++ b/src/app/collection-page/collection-page.component.html @@ -0,0 +1,16 @@ +
+ + + + + + + + + + +
diff --git a/src/app/item-page/item-page.component.scss b/src/app/collection-page/collection-page.component.scss similarity index 100% rename from src/app/item-page/item-page.component.scss rename to src/app/collection-page/collection-page.component.scss diff --git a/src/app/collection-page/collection-page.component.ts b/src/app/collection-page/collection-page.component.ts new file mode 100644 index 0000000000..42afe78881 --- /dev/null +++ b/src/app/collection-page/collection-page.component.ts @@ -0,0 +1,35 @@ +import { Component, OnInit } from '@angular/core'; +import { ActivatedRoute, Params } from '@angular/router'; + +import { Collection } from "../core/shared/collection.model"; +import { Bitstream } from "../core/shared/bitstream.model"; +import { RemoteData } from "../core/data/remote-data"; +import { CollectionDataService } from "../core/data/collection-data.service"; + +@Component({ + selector: 'ds-collection-page', + styleUrls: ['./collection-page.component.css'], + templateUrl: './collection-page.component.html', +}) +export class CollectionPageComponent implements OnInit { + collectionData: RemoteData; + logoData: RemoteData; + + constructor( + private collectionDataService: CollectionDataService, + private route: ActivatedRoute + ) { + this.universalInit(); + } + + ngOnInit(): void { + this.route.params.subscribe((params: Params) => { + this.collectionData = this.collectionDataService.findById(params['id']) + this.collectionData.payload + .subscribe(collection => this.logoData = collection.logo); + }); + } + + universalInit() { + } +} diff --git a/src/app/collection-page/collection-page.module.ts b/src/app/collection-page/collection-page.module.ts new file mode 100644 index 0000000000..9b204cb0bb --- /dev/null +++ b/src/app/collection-page/collection-page.module.ts @@ -0,0 +1,33 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; + +import { TranslateModule } from "@ngx-translate/core"; + +import { CollectionPageComponent } from './collection-page.component'; +import { FieldWrapperComponent } from './field-wrapper/field-wrapper.component'; +import { CollectionPageNameComponent } from './name/collection-page-name.component'; +import { CollectionPageLogoComponent } from './logo/collection-page-logo.component'; +import { CollectionPageIntroductoryTextComponent } from './introductory-text/collection-page-introductory-text.component'; +import { CollectionPageNewsComponent } from './news/collection-page-news.component'; +import { CollectionPageCopyrightComponent } from './copyright/collection-page-copyright.component'; +import { CollectionPageLicenseComponent } from './license/collection-page-license.component'; +import { CollectionPageRoutingModule } from './collection-page-routing.module'; + +@NgModule({ + imports: [ + CollectionPageRoutingModule, + CommonModule, + TranslateModule, + ], + declarations: [ + CollectionPageComponent, + FieldWrapperComponent, + CollectionPageNameComponent, + CollectionPageLogoComponent, + CollectionPageIntroductoryTextComponent, + CollectionPageNewsComponent, + CollectionPageCopyrightComponent, + CollectionPageLicenseComponent, + ] +}) +export class CollectionPageModule { } diff --git a/src/app/collection-page/copyright/collection-page-copyright.component.html b/src/app/collection-page/copyright/collection-page-copyright.component.html new file mode 100644 index 0000000000..db7b77d614 --- /dev/null +++ b/src/app/collection-page/copyright/collection-page-copyright.component.html @@ -0,0 +1,3 @@ + +

+
\ No newline at end of file diff --git a/src/app/collection-page/copyright/collection-page-copyright.component.scss b/src/app/collection-page/copyright/collection-page-copyright.component.scss new file mode 100644 index 0000000000..ad84b72f8c --- /dev/null +++ b/src/app/collection-page/copyright/collection-page-copyright.component.scss @@ -0,0 +1 @@ +@import '../../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/collection-page/copyright/collection-page-copyright.component.ts b/src/app/collection-page/copyright/collection-page-copyright.component.ts new file mode 100644 index 0000000000..e4bc21553f --- /dev/null +++ b/src/app/collection-page/copyright/collection-page-copyright.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; + + +@Component({ + selector: 'ds-collection-page-copyright', + styleUrls: ['./collection-page-copyright.component.css'], + templateUrl: './collection-page-copyright.component.html', +}) +export class CollectionPageCopyrightComponent { + @Input() copyrightText: String; +} diff --git a/src/app/collection-page/field-wrapper/field-wrapper.component.html b/src/app/collection-page/field-wrapper/field-wrapper.component.html new file mode 100644 index 0000000000..677ff2f918 --- /dev/null +++ b/src/app/collection-page/field-wrapper/field-wrapper.component.html @@ -0,0 +1,3 @@ +
+ +
\ No newline at end of file diff --git a/src/app/collection-page/field-wrapper/field-wrapper.component.scss b/src/app/collection-page/field-wrapper/field-wrapper.component.scss new file mode 100644 index 0000000000..ad84b72f8c --- /dev/null +++ b/src/app/collection-page/field-wrapper/field-wrapper.component.scss @@ -0,0 +1 @@ +@import '../../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/collection-page/field-wrapper/field-wrapper.component.ts b/src/app/collection-page/field-wrapper/field-wrapper.component.ts new file mode 100644 index 0000000000..c8420661ea --- /dev/null +++ b/src/app/collection-page/field-wrapper/field-wrapper.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; + + +@Component({ + selector: 'ds-field-wrapper', + styleUrls: ['./field-wrapper.component.css'], + templateUrl: './field-wrapper.component.html', +}) +export class FieldWrapperComponent { + @Input() name: String; +} diff --git a/src/app/collection-page/introductory-text/collection-page-introductory-text.component.html b/src/app/collection-page/introductory-text/collection-page-introductory-text.component.html new file mode 100644 index 0000000000..904a6d7f21 --- /dev/null +++ b/src/app/collection-page/introductory-text/collection-page-introductory-text.component.html @@ -0,0 +1,3 @@ + +

+
\ No newline at end of file diff --git a/src/app/collection-page/introductory-text/collection-page-introductory-text.component.scss b/src/app/collection-page/introductory-text/collection-page-introductory-text.component.scss new file mode 100644 index 0000000000..ad84b72f8c --- /dev/null +++ b/src/app/collection-page/introductory-text/collection-page-introductory-text.component.scss @@ -0,0 +1 @@ +@import '../../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/collection-page/introductory-text/collection-page-introductory-text.component.ts b/src/app/collection-page/introductory-text/collection-page-introductory-text.component.ts new file mode 100644 index 0000000000..f6526c9943 --- /dev/null +++ b/src/app/collection-page/introductory-text/collection-page-introductory-text.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; + + +@Component({ + selector: 'ds-collection-page-introductory-text', + styleUrls: ['./collection-page-introductory-text.component.css'], + templateUrl: './collection-page-introductory-text.component.html', +}) +export class CollectionPageIntroductoryTextComponent { + @Input() introductoryText: String; +} diff --git a/src/app/collection-page/license/collection-page-license.component.html b/src/app/collection-page/license/collection-page-license.component.html new file mode 100644 index 0000000000..af0a6e4261 --- /dev/null +++ b/src/app/collection-page/license/collection-page-license.component.html @@ -0,0 +1,4 @@ + +

{{ 'collection.page.license' | translate }}

+

{{ license }}

+
\ No newline at end of file diff --git a/src/app/collection-page/license/collection-page-license.component.scss b/src/app/collection-page/license/collection-page-license.component.scss new file mode 100644 index 0000000000..ad84b72f8c --- /dev/null +++ b/src/app/collection-page/license/collection-page-license.component.scss @@ -0,0 +1 @@ +@import '../../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/collection-page/license/collection-page-license.component.ts b/src/app/collection-page/license/collection-page-license.component.ts new file mode 100644 index 0000000000..8f269cca8c --- /dev/null +++ b/src/app/collection-page/license/collection-page-license.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; + + +@Component({ + selector: 'ds-collection-page-license', + styleUrls: ['./collection-page-license.component.css'], + templateUrl: './collection-page-license.component.html', +}) +export class CollectionPageLicenseComponent { + @Input() license: String; +} diff --git a/src/app/collection-page/logo/collection-page-logo.component.html b/src/app/collection-page/logo/collection-page-logo.component.html new file mode 100644 index 0000000000..1c331e2e25 --- /dev/null +++ b/src/app/collection-page/logo/collection-page-logo.component.html @@ -0,0 +1,3 @@ + \ No newline at end of file diff --git a/src/app/collection-page/logo/collection-page-logo.component.scss b/src/app/collection-page/logo/collection-page-logo.component.scss new file mode 100644 index 0000000000..ad84b72f8c --- /dev/null +++ b/src/app/collection-page/logo/collection-page-logo.component.scss @@ -0,0 +1 @@ +@import '../../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/collection-page/logo/collection-page-logo.component.ts b/src/app/collection-page/logo/collection-page-logo.component.ts new file mode 100644 index 0000000000..22c34422ea --- /dev/null +++ b/src/app/collection-page/logo/collection-page-logo.component.ts @@ -0,0 +1,13 @@ +import { Component, Input } from '@angular/core'; + +import { Bitstream } from "../../core/shared/bitstream.model"; + + +@Component({ + selector: 'ds-collection-page-logo', + styleUrls: ['./collection-page-logo.component.css'], + templateUrl: './collection-page-logo.component.html', +}) +export class CollectionPageLogoComponent { + @Input() logo: Bitstream; +} diff --git a/src/app/collection-page/name/collection-page-name.component.html b/src/app/collection-page/name/collection-page-name.component.html new file mode 100644 index 0000000000..21f1f65331 --- /dev/null +++ b/src/app/collection-page/name/collection-page-name.component.html @@ -0,0 +1 @@ +

{{ name }}

\ No newline at end of file diff --git a/src/app/collection-page/name/collection-page-name.component.scss b/src/app/collection-page/name/collection-page-name.component.scss new file mode 100644 index 0000000000..ad84b72f8c --- /dev/null +++ b/src/app/collection-page/name/collection-page-name.component.scss @@ -0,0 +1 @@ +@import '../../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/collection-page/name/collection-page-name.component.ts b/src/app/collection-page/name/collection-page-name.component.ts new file mode 100644 index 0000000000..30121fd01b --- /dev/null +++ b/src/app/collection-page/name/collection-page-name.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; + + +@Component({ + selector: 'ds-collection-page-name', + styleUrls: ['./collection-page-name.component.css'], + templateUrl: './collection-page-name.component.html', +}) +export class CollectionPageNameComponent { + @Input() name: String; +} diff --git a/src/app/collection-page/news/collection-page-news.component.html b/src/app/collection-page/news/collection-page-news.component.html new file mode 100644 index 0000000000..bd17c7ea77 --- /dev/null +++ b/src/app/collection-page/news/collection-page-news.component.html @@ -0,0 +1,4 @@ + +

{{ 'collection.page.news' | translate }}

+

+
\ No newline at end of file diff --git a/src/app/collection-page/news/collection-page-news.component.scss b/src/app/collection-page/news/collection-page-news.component.scss new file mode 100644 index 0000000000..ad84b72f8c --- /dev/null +++ b/src/app/collection-page/news/collection-page-news.component.scss @@ -0,0 +1 @@ +@import '../../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/collection-page/news/collection-page-news.component.ts b/src/app/collection-page/news/collection-page-news.component.ts new file mode 100644 index 0000000000..479c37a7c6 --- /dev/null +++ b/src/app/collection-page/news/collection-page-news.component.ts @@ -0,0 +1,11 @@ +import { Component, Input } from '@angular/core'; + + +@Component({ + selector: 'ds-collection-page-news', + styleUrls: ['./collection-page-news.component.css'], + templateUrl: './collection-page-news.component.html', +}) +export class CollectionPageNewsComponent { + @Input() sidebarText: String; +} 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 af47b62fe0..2096d47459 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -135,9 +135,12 @@ export class RemoteDataBuildService { }); }, 0); - links[relationship] = normalized[relationship].map((href: string) => { - return this.buildSingle(href, resourceConstructor); + let rdArr = []; + normalized[relationship].forEach((href: string) => { + rdArr.push(this.buildSingle(href, resourceConstructor)); }); + + links[relationship] = this.aggregate(rdArr); } else { // without the setTimeout, the actions inside requestService.configure @@ -154,4 +157,49 @@ export class RemoteDataBuildService { const domainModel = getMapsTo(normalized.constructor); return Object.assign(new domainModel(), normalized, links); } + + aggregate(input: RemoteData[]): RemoteData { + const requestPending = Observable.combineLatest( + ...input.map(rd => rd.isRequestPending), + ).map((...pendingArray) => pendingArray.every(e => e === true)) + .distinctUntilChanged(); + + const responsePending = Observable.combineLatest( + ...input.map(rd => rd.isResponsePending), + ).map((...pendingArray) => pendingArray.every(e => e === true)) + .distinctUntilChanged(); + + const isSuccessFul = Observable.combineLatest( + ...input.map(rd => rd.hasSucceeded), + ).map((...successArray) => successArray.every(e => e === true)) + .distinctUntilChanged(); + + const errorMessage = Observable.combineLatest( + ...input.map(rd => rd.errorMessage), + ).map((...errors) => errors + .map((e, idx) => { + if (hasValue(e)) { + return `[${idx}]: ${e}`; + } + }) + .filter(e => hasValue(e)) + .join(", ") + ); + + const payload = > Observable.combineLatest( + ...input.map(rd => rd.payload) + ); + + return new RemoteData( + // This is an aggregated object, it doesn't necessarily correspond + // to a single REST endpoint, so instead of a self link, use the + // current time in ms for a somewhat unique id + `${new Date().getTime()}`, + requestPending, + responsePending, + isSuccessFul, + errorMessage, + payload + ); + } } diff --git a/src/app/core/cache/models/normalized-bitstream.model.ts b/src/app/core/cache/models/normalized-bitstream.model.ts index 57b4f63346..c89cf5dc05 100644 --- a/src/app/core/cache/models/normalized-bitstream.model.ts +++ b/src/app/core/cache/models/normalized-bitstream.model.ts @@ -16,13 +16,20 @@ export class NormalizedBitstream extends NormalizedDSpaceObject { /** * The relative path to this Bitstream's file */ + @autoserialize url: string; /** * The mime type of this Bitstream */ + @autoserialize mimetype: string; + /** + * The format of this Bitstream + */ + format: string; + /** * The description of this Bitstream */ diff --git a/src/app/core/cache/models/normalized-collection.model.ts b/src/app/core/cache/models/normalized-collection.model.ts index e0f96ff805..86a1ba177a 100644 --- a/src/app/core/cache/models/normalized-collection.model.ts +++ b/src/app/core/cache/models/normalized-collection.model.ts @@ -17,6 +17,8 @@ export class NormalizedCollection extends NormalizedDSpaceObject { /** * The Bitstream that represents the logo of this Collection */ + @autoserialize + @relationship(NormalizedDSOType.NormalizedBitstream) logo: string; /** diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index 5325e395d8..06bdf41f30 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -27,7 +27,7 @@ export class Bitstream extends DSpaceObject { /** * An array of Bundles that are direct parents of this Bitstream */ - parents: Array>; + parents: RemoteData; /** * The Bundle that owns this Bitstream diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index 7c2f6b05d4..8f3a284fb6 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -12,13 +12,13 @@ export class Bundle extends DSpaceObject { /** * An array of Items that are direct parents of this Bundle */ - parents: Array>; + parents: RemoteData; /** * The Item that owns this Bundle */ owner: Item; - bitstreams: Array> + bitstreams: RemoteData } diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index 4287eff63c..30814726b8 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -58,13 +58,13 @@ export class Collection extends DSpaceObject { /** * An array of Collections that are direct parents of this Collection */ - parents: Array>; + parents: RemoteData; /** * The Collection that owns this Collection */ owner: Collection; - items: Array>; + items: RemoteData; } diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index 9639abd258..67c8211fd2 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -50,13 +50,13 @@ export class Community extends DSpaceObject { /** * An array of Communities that are direct parents of this Community */ - parents: Array>; + parents: RemoteData; /** * The Community that owns this Community */ owner: Community; - collections: Array>; + collections: RemoteData; } diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 9609782735..ca7c67207a 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -39,7 +39,7 @@ export abstract class DSpaceObject implements CacheableObject { /** * An array of DSpaceObjects that are direct parents of this DSpaceObject */ - parents: Array>; + parents: RemoteData; /** * The DSpaceObject that owns this DSpaceObject diff --git a/src/app/core/shared/item.model.spec.ts b/src/app/core/shared/item.model.spec.ts new file mode 100644 index 0000000000..e20bd6e592 --- /dev/null +++ b/src/app/core/shared/item.model.spec.ts @@ -0,0 +1,115 @@ +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"; + + +describe('Item', () => { + + + let item: Item; + const thumbnailBundleName = "THUMBNAIL"; + const originalBundleName = "ORIGINAL"; + const thumbnailPath = "thumbnail.jpg"; + const bitstream1Path = "document.pdf"; + const bitstream2Path = "otherfile.doc"; + + const nonExistingBundleName = "c1e568f7-d14e-496b-bdd7-07026998cc00"; + let remoteBundles; + let thumbnailBundle; + let originalBundle; + + beforeEach(() => { + const thumbnail = { + retrieve: thumbnailPath + }; + + const bitstreams = [{ + retrieve: bitstream1Path + }, { + retrieve: bitstream2Path + }]; + + const remoteDataThumbnail = createRemoteDataObject(thumbnail); + const remoteDataFiles = createRemoteDataObject(bitstreams); + + + // Create Bundles + + const bundles = + [ + { + name: thumbnailBundleName, + primaryBitstream: remoteDataThumbnail + }, + + { + name: originalBundleName, + bitstreams: remoteDataFiles + }]; + + remoteBundles = createRemoteDataObject(bundles); + + item = Object.assign(new Item(), { bundles: remoteBundles }); + + }); + + + 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 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()); + }); + + + describe("get thumbnail", () => { + beforeEach(() => { + spyOn(item, 'getBundle').and.returnValue(Observable.of(thumbnailBundle)); + }); + + it('should return the thumbnail (the primaryBitstream in the bundle "THUMBNAIL") of this item', () => { + let path: string = thumbnailPath; + let bitstream: Observable = item.getThumbnail(); + bitstream.map(b => expect(b.retrieve).toBe(path)); + }); + }); + + + describe("get files", () => { + beforeEach(() => { + spyOn(item, 'getBundle').and.returnValue(Observable.of(originalBundle)); + }); + + it('should return all files in the ORIGINAL bundle', () => { + let paths = [bitstream1Path, bitstream2Path]; + + let files: Observable = item.getFiles(); + let index = 0; + files.map(f => expect(f.length).toBe(2)); + files.subscribe( + array => array.forEach( + file => { + expect(file.retrieve).toBe(paths[index]); + index++; + } + ) + ) + }); + + }); + + +}); + +function createRemoteDataObject(object: Object) { + return new RemoteData("", Observable.of(false), Observable.of(false), Observable.of(true), Observable.of(undefined), Observable.of(object)); + +} \ 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 92a05263a4..a3e098eed9 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -4,6 +4,7 @@ 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"; export class Item extends DSpaceObject { @@ -30,51 +31,72 @@ export class Item extends DSpaceObject { /** * An array of Collections that are direct parents of this Item */ - parents: Array>; + parents: RemoteData; /** * The Collection that owns this Item */ owner: Collection; - bundles: Array>; + bundles: RemoteData; + + /** + * Retrieves the thumbnail of this item + * @returns {Observable} the primaryBitstream of the "THUMBNAIL" bundle + */ getThumbnail(): Observable { const bundle: Observable = this.getBundle("THUMBNAIL"); - return bundle.flatMap( - bundle => { - if (bundle != null) { - return bundle.primaryBitstream.payload; - } - else { - return Observable.of(undefined); - } - } - ); + return bundle + .filter(bundle => hasValue(bundle)) + .flatMap(bundle => bundle.primaryBitstream.payload) + .startWith(undefined); } - getFiles(): Observable>> { - const bundle: Observable = this.getBundle("ORIGINAL"); - return bundle.map(bundle => { - if (bundle != null) { - return bundle.bitstreams.map(bitstream => bitstream.payload) - } - }); + /** + * 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 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 Observable.combineLatest( - ...this.bundles.map(b => b.payload), - (...bundles: Array) => bundles) + return this.bundles.payload + .filter(bundles => hasValue(bundles)) .map(bundles => { return bundles.find((bundle: Bundle) => { return bundle.name === name }); - }); - } - - getCollections(): Array> { - return this.parents.map(collection => collection.payload.map(parent => parent)); + }) + .startWith(undefined); } } diff --git a/src/app/item-page/collections/collections.component.html b/src/app/item-page/collections/collections.component.html deleted file mode 100644 index bf764ae182..0000000000 --- a/src/app/item-page/collections/collections.component.html +++ /dev/null @@ -1,7 +0,0 @@ - - - diff --git a/src/app/item-page/field-components/collections/collections.component.html b/src/app/item-page/field-components/collections/collections.component.html new file mode 100644 index 0000000000..8f184817c2 --- /dev/null +++ b/src/app/item-page/field-components/collections/collections.component.html @@ -0,0 +1,7 @@ + + + diff --git a/src/app/item-page/collections/collections.component.ts b/src/app/item-page/field-components/collections/collections.component.ts similarity index 58% rename from src/app/item-page/collections/collections.component.ts rename to src/app/item-page/field-components/collections/collections.component.ts index 742ac247a1..199fb2b773 100644 --- a/src/app/item-page/collections/collections.component.ts +++ b/src/app/item-page/field-components/collections/collections.component.ts @@ -1,7 +1,12 @@ import { Component, Input, OnInit } from '@angular/core'; -import { Collection } from "../../core/shared/collection.model"; +import { Collection } from "../../../core/shared/collection.model"; import { Observable } from "rxjs"; -import { Item } from "../../core/shared/item.model"; +import { Item } from "../../../core/shared/item.model"; + +/** + * This component renders the parent collections section of the item + * inside a 'ds-metadata-field-wrapper' component. + */ @Component({ selector: 'ds-item-page-collections', @@ -15,7 +20,7 @@ export class CollectionsComponent implements OnInit { separator: string = "
" - collections: Array>; + collections: Observable; constructor() { this.universalInit(); @@ -26,7 +31,7 @@ export class CollectionsComponent implements OnInit { } ngOnInit(): void { - this.collections = this.item.getCollections(); + this.collections = this.item.parents.payload; } diff --git a/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.html b/src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html similarity index 100% rename from src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.html rename to src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.html diff --git a/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.scss b/src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.scss similarity index 61% rename from src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.scss rename to src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.scss index 749382bc9a..26f0f6fa65 100644 --- a/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.scss +++ b/src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.scss @@ -1,4 +1,4 @@ -@import '../../../styles/variables.scss'; +@import '../../../../styles/variables.scss'; :host { .simple-view-element { margin-bottom: 15px; diff --git a/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.ts b/src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts similarity index 73% rename from src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.ts rename to src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts index caae4bd5f1..2b40f33b5d 100644 --- a/src/app/item-page/metadata-field-wrapper/metadata-field-wrapper.component.ts +++ b/src/app/item-page/field-components/metadata-field-wrapper/metadata-field-wrapper.component.ts @@ -1,5 +1,10 @@ import { Component, Input } from '@angular/core'; +/** + * This component renders any content inside this wrapper. + * The wrapper prints a label before the content (if available) + */ + @Component({ selector: 'ds-metadata-field-wrapper', styleUrls: ['./metadata-field-wrapper.component.css'], diff --git a/src/app/item-page/metadata-uri-values/metadata-uri-values.component.html b/src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.html similarity index 100% rename from src/app/item-page/metadata-uri-values/metadata-uri-values.component.html rename to src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.html diff --git a/src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.scss b/src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.scss new file mode 100644 index 0000000000..96ce861942 --- /dev/null +++ b/src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.scss @@ -0,0 +1 @@ +@import '../../../../styles/variables.scss'; diff --git a/src/app/item-page/metadata-uri-values/metadata-uri-values.component.ts b/src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.ts similarity index 56% rename from src/app/item-page/metadata-uri-values/metadata-uri-values.component.ts rename to src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.ts index fa4a9ebfc5..398e6ed278 100644 --- a/src/app/item-page/metadata-uri-values/metadata-uri-values.component.ts +++ b/src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.ts @@ -1,6 +1,14 @@ import { Component, Input } from '@angular/core'; import { MetadataValuesComponent } from "../metadata-values/metadata-values.component"; +/** + * This component renders the configured 'values' into the ds-metadata-field-wrapper component as a link. + * It puts the given 'separator' between each two values + * and creates an 'a' tag for each value, + * using the 'linktext' as it's value (if it exists) + * and using the values as the 'href' attribute (and as value of the tag when no 'linktext' is defined) + */ + @Component({ selector: 'ds-metadata-uri-values', styleUrls: ['./metadata-uri-values.component.css'], diff --git a/src/app/item-page/metadata-values/metadata-values.component.html b/src/app/item-page/field-components/metadata-values/metadata-values.component.html similarity index 100% rename from src/app/item-page/metadata-values/metadata-values.component.html rename to src/app/item-page/field-components/metadata-values/metadata-values.component.html diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.scss b/src/app/item-page/field-components/metadata-values/metadata-values.component.scss new file mode 100644 index 0000000000..96ce861942 --- /dev/null +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.scss @@ -0,0 +1 @@ +@import '../../../../styles/variables.scss'; diff --git a/src/app/item-page/metadata-values/metadata-values.component.ts b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts similarity index 71% rename from src/app/item-page/metadata-values/metadata-values.component.ts rename to src/app/item-page/field-components/metadata-values/metadata-values.component.ts index 6ac07ad914..26e18adffd 100644 --- a/src/app/item-page/metadata-values/metadata-values.component.ts +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.ts @@ -1,5 +1,10 @@ import { Component, Input } from '@angular/core'; +/** + * This component renders the configured 'values' into the ds-metadata-field-wrapper component. + * It puts the given 'separator' between each two values. + */ + @Component({ selector: 'ds-metadata-values', styleUrls: ['./metadata-values.component.css'], 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 new file mode 100644 index 0000000000..d7e1712b7f --- /dev/null +++ b/src/app/item-page/full/field-components/file-section/full-file-section.component.html @@ -0,0 +1,29 @@ + +
+
+ +
+
+
+
{{"item.page.filesection.name" | translate}}
+
{{file.name}}
+ +
{{"item.page.filesection.size" | translate}}
+
{{(file.size) | dsFileSize }}
+ + +
{{"item.page.filesection.format" | translate}}
+
{{(file.mimetype)}}
+ + +
{{"item.page.filesection.description" | translate}}
+
{{file.findMetadata("dc.description")}}
+
+
+ +
+
diff --git a/src/app/item-page/full/field-components/file-section/full-file-section.component.scss b/src/app/item-page/full/field-components/file-section/full-file-section.component.scss new file mode 100644 index 0000000000..4597c711ff --- /dev/null +++ b/src/app/item-page/full/field-components/file-section/full-file-section.component.scss @@ -0,0 +1,7 @@ +@import '../../../../../styles/variables.scss'; +@import '../../../../../../node_modules/bootstrap/scss/_variables.scss'; +@media screen and (min-width: map-get($grid-breakpoints, md)) { + dt { + text-align: right; + } +} \ No newline at end of file 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 new file mode 100644 index 0000000000..3723bc5450 --- /dev/null +++ b/src/app/item-page/full/field-components/file-section/full-file-section.component.ts @@ -0,0 +1,52 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Bitstream } from "../../../../core/shared/bitstream.model"; +import { Item } from "../../../../core/shared/item.model"; +import { Observable } from "rxjs"; +import { FileSectionComponent } from "../../../simple/field-components/file-section/file-section.component"; +import { hasValue } from "../../../../shared/empty.util"; + +/** + * This component renders the file section of the item + * inside a 'ds-metadata-field-wrapper' component. + */ + +@Component({ + selector: 'ds-item-page-full-file-section', + styleUrls: ['./full-file-section.component.css'], + templateUrl: './full-file-section.component.html' +}) +export class FullFileSectionComponent extends FileSectionComponent implements OnInit { + + @Input() item: Item; + + label : string; + + files: Observable; + + + thumbnails: Map> = new Map(); + + + universalInit() { + } + + ngOnInit(): void { + super.ngOnInit(); + } + + initialize(): void { + const originals = this.item.getFiles("ORIGINAL"); + const licenses = this.item.getFiles("LICENSE"); + this.files = Observable.combineLatest(originals, licenses, (originals, licenses) => [...originals, ...licenses]); + this.files.subscribe( + files => + files.forEach( + original => { + const thumbnail: Observable = this.item.getThumbnailForOriginal(original); + this.thumbnails.set(original.id, thumbnail); + } + ) + ) + } + +} diff --git a/src/app/item-page/full/full-item-page.component.html b/src/app/item-page/full/full-item-page.component.html new file mode 100644 index 0000000000..b0b1f98037 --- /dev/null +++ b/src/app/item-page/full/full-item-page.component.html @@ -0,0 +1,25 @@ +
+ + + + + + + + + + + + +
{{metadatum.key}}{{metadatum.value}}{{metadatum.language}}
+ + + + + + +
diff --git a/src/app/item-page/full/full-item-page.component.scss b/src/app/item-page/full/full-item-page.component.scss new file mode 100644 index 0000000000..65b9262338 --- /dev/null +++ b/src/app/item-page/full/full-item-page.component.scss @@ -0,0 +1,7 @@ +@import '../../../styles/variables.scss'; +:host { + div.simple-view-link { + text-align: center; + margin: 20px; + } +} \ No newline at end of file diff --git a/src/app/item-page/full/full-item-page.component.ts b/src/app/item-page/full/full-item-page.component.ts new file mode 100644 index 0000000000..b19fd93677 --- /dev/null +++ b/src/app/item-page/full/full-item-page.component.ts @@ -0,0 +1,45 @@ +import { Component, OnInit } from '@angular/core'; +import { Observable } from "rxjs"; +import { ItemPageComponent } from "../simple/item-page.component"; +import { Metadatum } from "../../core/shared/metadatum.model"; +import { ItemDataService } from "../../core/data/item-data.service"; +import { ActivatedRoute } from "@angular/router"; +import { RemoteData } from "../../core/data/remote-data"; +import { Item } from "../../core/shared/item.model"; + +/** + * This component renders a simple item page. + * The route parameter 'id' is used to request the item it represents. + * All fields of the item that should be displayed, are defined in its template. + */ + +@Component({ + selector: 'ds-full-item-page', + styleUrls: ['./full-item-page.component.css'], + templateUrl: './full-item-page.component.html', +}) +export class FullItemPageComponent extends ItemPageComponent implements OnInit { + + item: RemoteData; + + metadata: Observable>; + + constructor(route: ActivatedRoute, items: ItemDataService) { + super(route, items); + } + + universalInit() { + + } + + /*** AoT inheritance fix, will hopefully be resolved in the near future **/ + ngOnInit(): void { + super.ngOnInit(); + } + + initialize(params) { + super.initialize(params); + this.metadata = this.item.payload.map(i => i.metadata); + } + +} diff --git a/src/app/item-page/item-page-routing.module.ts b/src/app/item-page/item-page-routing.module.ts index 64c0a607c5..2cd0c200ad 100644 --- a/src/app/item-page/item-page-routing.module.ts +++ b/src/app/item-page/item-page-routing.module.ts @@ -1,12 +1,14 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; -import { ItemPageComponent } from './item-page.component'; +import { ItemPageComponent } from './simple/item-page.component'; +import { FullItemPageComponent } from './full/full-item-page.component'; @NgModule({ imports: [ RouterModule.forChild([ { path: 'items/:id', pathMatch: 'full', component: ItemPageComponent }, + { path: 'items/:id/full', component: FullItemPageComponent }, ]) ] }) diff --git a/src/app/item-page/item-page.module.ts b/src/app/item-page/item-page.module.ts index ab0e2809f6..8b7bae4477 100644 --- a/src/app/item-page/item-page.module.ts +++ b/src/app/item-page/item-page.module.ts @@ -1,23 +1,26 @@ import { NgModule } from '@angular/core'; import { CommonModule } from '@angular/common'; -import { ItemPageComponent } from './item-page.component'; +import { ItemPageComponent } from './simple/item-page.component'; import { ItemPageRoutingModule } from './item-page-routing.module'; -import { MetadataValuesComponent } from './metadata-values/metadata-values.component'; -import { MetadataUriValuesComponent } from './metadata-uri-values/metadata-uri-values.component'; -import { MetadataFieldWrapperComponent } from './metadata-field-wrapper/metadata-field-wrapper.component'; -import { ItemPageAuthorFieldComponent } from './specific-field/author/item-page-author-field.component'; -import { ItemPageDateFieldComponent } from './specific-field/date/item-page-date-field.component'; -import { ItemPageAbstractFieldComponent } from './specific-field/abstract/item-page-abstract-field.component'; -import { ItemPageUriFieldComponent } from './specific-field/uri/item-page-uri-field.component'; -import { ItemPageTitleFieldComponent } from './specific-field/title/item-page-title-field.component'; -import { ItemPageSpecificFieldComponent } from './specific-field/item-page-specific-field.component'; +import { MetadataValuesComponent } from './field-components/metadata-values/metadata-values.component'; +import { MetadataUriValuesComponent } from './field-components/metadata-uri-values/metadata-uri-values.component'; +import { MetadataFieldWrapperComponent } from './field-components/metadata-field-wrapper/metadata-field-wrapper.component'; +import { ItemPageAuthorFieldComponent } from './simple/field-components/specific-field/author/item-page-author-field.component'; +import { ItemPageDateFieldComponent } from './simple/field-components/specific-field/date/item-page-date-field.component'; +import { ItemPageAbstractFieldComponent } from './simple/field-components/specific-field/abstract/item-page-abstract-field.component'; +import { ItemPageUriFieldComponent } from './simple/field-components/specific-field/uri/item-page-uri-field.component'; +import { ItemPageTitleFieldComponent } from './simple/field-components/specific-field/title/item-page-title-field.component'; +import { ItemPageSpecificFieldComponent } from './simple/field-components/specific-field/item-page-specific-field.component'; import { SharedModule } from './../shared/shared.module'; -import { FileSectionComponent } from "./file-section/file-section.component"; -import { CollectionsComponent } from "./collections/collections.component"; +import { FileSectionComponent } from "./simple/field-components/file-section/file-section.component"; +import { CollectionsComponent } from "./field-components/collections/collections.component"; +import { FullItemPageComponent } from "./full/full-item-page.component"; +import { FullFileSectionComponent } from "./full/field-components/file-section/full-file-section.component"; @NgModule({ declarations: [ ItemPageComponent, + FullItemPageComponent, MetadataValuesComponent, MetadataUriValuesComponent, MetadataFieldWrapperComponent, @@ -28,7 +31,8 @@ import { CollectionsComponent } from "./collections/collections.component"; ItemPageTitleFieldComponent, ItemPageSpecificFieldComponent, FileSectionComponent, - CollectionsComponent + CollectionsComponent, + FullFileSectionComponent ], imports: [ ItemPageRoutingModule, diff --git a/src/app/item-page/metadata-values/metadata-values.component.scss b/src/app/item-page/metadata-values/metadata-values.component.scss deleted file mode 100644 index 50be6f5ad0..0000000000 --- a/src/app/item-page/metadata-values/metadata-values.component.scss +++ /dev/null @@ -1 +0,0 @@ -@import '../../../styles/variables.scss'; diff --git a/src/app/item-page/file-section/file-section.component.html b/src/app/item-page/simple/field-components/file-section/file-section.component.html similarity index 63% rename from src/app/item-page/file-section/file-section.component.html rename to src/app/item-page/simple/field-components/file-section/file-section.component.html index 0cc15e090e..149a1b2017 100644 --- a/src/app/item-page/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/item-page/file-section/file-section.component.ts b/src/app/item-page/simple/field-components/file-section/file-section.component.ts similarity index 56% rename from src/app/item-page/file-section/file-section.component.ts rename to src/app/item-page/simple/field-components/file-section/file-section.component.ts index 08b01b8ea1..4cd1daf290 100644 --- a/src/app/item-page/file-section/file-section.component.ts +++ b/src/app/item-page/simple/field-components/file-section/file-section.component.ts @@ -1,8 +1,13 @@ import { Component, Input, OnInit } from '@angular/core'; -import { Bitstream } from "../../core/shared/bitstream.model"; -import { Item } from "../../core/shared/item.model"; +import { Bitstream } from "../../../../core/shared/bitstream.model"; +import { Item } from "../../../../core/shared/item.model"; import { Observable } from "rxjs"; +/** + * This component renders the file section of the item + * inside a 'ds-metadata-field-wrapper' component. + */ + @Component({ selector: 'ds-item-page-file-section', templateUrl: './file-section.component.html' @@ -13,21 +18,23 @@ export class FileSectionComponent implements OnInit { label : string = "item.page.files"; - separator: string = "
" + separator: string = "
"; - files: Observable>>; + files: Observable; constructor() { this.universalInit(); - } universalInit() { } ngOnInit(): void { + this.initialize(); + } + + initialize(): void { this.files = this.item.getFiles(); } - } diff --git a/src/app/item-page/specific-field/abstract/item-page-abstract-field.component.ts b/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts similarity index 80% rename from src/app/item-page/specific-field/abstract/item-page-abstract-field.component.ts rename to src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts index 3e88117654..f0e410d89e 100644 --- a/src/app/item-page/specific-field/abstract/item-page-abstract-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/abstract/item-page-abstract-field.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { Item } from "../../../core/shared/item.model"; +import { Component, Input } from '@angular/core'; +import { Item } from "../../../../../core/shared/item.model"; import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component"; @Component({ diff --git a/src/app/item-page/specific-field/author/item-page-author-field.component.ts b/src/app/item-page/simple/field-components/specific-field/author/item-page-author-field.component.ts similarity index 80% rename from src/app/item-page/specific-field/author/item-page-author-field.component.ts rename to src/app/item-page/simple/field-components/specific-field/author/item-page-author-field.component.ts index 2ff76d509a..5b85038e38 100644 --- a/src/app/item-page/specific-field/author/item-page-author-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/author/item-page-author-field.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { Item } from "../../../core/shared/item.model"; +import { Component, Input } from '@angular/core'; +import { Item } from "../../../../../core/shared/item.model"; import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component"; @Component({ diff --git a/src/app/item-page/specific-field/date/item-page-date-field.component.ts b/src/app/item-page/simple/field-components/specific-field/date/item-page-date-field.component.ts similarity index 79% rename from src/app/item-page/specific-field/date/item-page-date-field.component.ts rename to src/app/item-page/simple/field-components/specific-field/date/item-page-date-field.component.ts index 16b6aad1f0..2d7b3f7c41 100644 --- a/src/app/item-page/specific-field/date/item-page-date-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/date/item-page-date-field.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { Item } from "../../../core/shared/item.model"; +import { Component, Input } from '@angular/core'; +import { Item } from "../../../../../core/shared/item.model"; import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component"; @Component({ diff --git a/src/app/item-page/specific-field/item-page-specific-field.component.html b/src/app/item-page/simple/field-components/specific-field/item-page-specific-field.component.html similarity index 100% rename from src/app/item-page/specific-field/item-page-specific-field.component.html rename to src/app/item-page/simple/field-components/specific-field/item-page-specific-field.component.html diff --git a/src/app/item-page/simple/field-components/specific-field/item-page-specific-field.component.ts b/src/app/item-page/simple/field-components/specific-field/item-page-specific-field.component.ts new file mode 100644 index 0000000000..20c9528ebe --- /dev/null +++ b/src/app/item-page/simple/field-components/specific-field/item-page-specific-field.component.ts @@ -0,0 +1,40 @@ +import { Component, Input } from '@angular/core'; +import { Item } from "../../../../core/shared/item.model"; + +/** + * This component can be used to represent metadata on a simple item page. + * It expects one input parameter of type Item to which the metadata belongs. + * This class can be extended to print certain metadata. + */ + +@Component({ + templateUrl: './item-page-specific-field.component.html' +}) +export class ItemPageSpecificFieldComponent { + + @Input() item: Item; + + /** + * Fields (schema.element.qualifier) used to render their values. + */ + fields : string[]; + + /** + * Label i18n key for the rendered metadata + */ + label : string; + + /** + * Separator string between multiple values of the metadata fields defined + * @type {string} + */ + separator : string = "
"; + + constructor() { + this.universalInit(); + } + + universalInit() { + + } +} diff --git a/src/app/item-page/specific-field/title/item-page-title-field.component.html b/src/app/item-page/simple/field-components/specific-field/title/item-page-title-field.component.html similarity index 100% rename from src/app/item-page/specific-field/title/item-page-title-field.component.html rename to src/app/item-page/simple/field-components/specific-field/title/item-page-title-field.component.html diff --git a/src/app/item-page/specific-field/title/item-page-title-field.component.ts b/src/app/item-page/simple/field-components/specific-field/title/item-page-title-field.component.ts similarity index 77% rename from src/app/item-page/specific-field/title/item-page-title-field.component.ts rename to src/app/item-page/simple/field-components/specific-field/title/item-page-title-field.component.ts index 6dc2d159c8..4f926000a4 100644 --- a/src/app/item-page/specific-field/title/item-page-title-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/title/item-page-title-field.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { Item } from "../../../core/shared/item.model"; +import { Component, Input } from '@angular/core'; +import { Item } from "../../../../../core/shared/item.model"; import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component"; @Component({ diff --git a/src/app/item-page/specific-field/uri/item-page-uri-field.component.html b/src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.html similarity index 100% rename from src/app/item-page/specific-field/uri/item-page-uri-field.component.html rename to src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.html diff --git a/src/app/item-page/specific-field/uri/item-page-uri-field.component.ts b/src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.ts similarity index 78% rename from src/app/item-page/specific-field/uri/item-page-uri-field.component.ts rename to src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.ts index 4f7b0bb874..4879fa5aa6 100644 --- a/src/app/item-page/specific-field/uri/item-page-uri-field.component.ts +++ b/src/app/item-page/simple/field-components/specific-field/uri/item-page-uri-field.component.ts @@ -1,5 +1,5 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { Item } from "../../../core/shared/item.model"; +import { Component, Input } from '@angular/core'; +import { Item } from "../../../../../core/shared/item.model"; import { ItemPageSpecificFieldComponent } from "../item-page-specific-field.component"; @Component({ diff --git a/src/app/item-page/item-page.component.html b/src/app/item-page/simple/item-page.component.html similarity index 82% rename from src/app/item-page/item-page.component.html rename to src/app/item-page/simple/item-page.component.html index 47e5330ca5..9378c3839b 100644 --- a/src/app/item-page/item-page.component.html +++ b/src/app/item-page/simple/item-page.component.html @@ -8,12 +8,19 @@ + + diff --git a/src/app/item-page/metadata-uri-values/metadata-uri-values.component.scss b/src/app/item-page/simple/item-page.component.scss similarity index 100% rename from src/app/item-page/metadata-uri-values/metadata-uri-values.component.scss rename to src/app/item-page/simple/item-page.component.scss diff --git a/src/app/item-page/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts similarity index 50% rename from src/app/item-page/item-page.component.ts rename to src/app/item-page/simple/item-page.component.ts index c0dcafb3b0..53e57f3ae1 100644 --- a/src/app/item-page/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -1,10 +1,16 @@ import { Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { Item } from "../core/shared/item.model"; -import { ItemDataService } from "../core/data/item-data.service"; -import { RemoteData } from "../core/data/remote-data"; +import { Item } from "../../core/shared/item.model"; +import { ItemDataService } from "../../core/data/item-data.service"; +import { RemoteData } from "../../core/data/remote-data"; import { Observable } from "rxjs"; -import { Bitstream } from "../core/shared/bitstream.model"; +import { Bitstream } from "../../core/shared/bitstream.model"; + +/** + * This component renders a simple item page. + * The route parameter 'id' is used to request the item it represents. + * All fields of the item that should be displayed, are defined in its template. + */ @Component({ selector: 'ds-item-page', @@ -31,11 +37,16 @@ export class ItemPageComponent implements OnInit { ngOnInit(): void { this.sub = this.route.params.subscribe(params => { - this.id = +params['id']; - this.item = this.items.findById(params['id']); - this.thumbnail = this.item.payload.flatMap(i => i.getThumbnail()); + this.initialize(params); }); } + initialize(params) { + this.id = +params['id']; + this.item = this.items.findById(params['id']); + this.thumbnail = this.item.payload.flatMap(i => i.getThumbnail()); + } + + } diff --git a/src/app/item-page/specific-field/item-page-specific-field.component.ts b/src/app/item-page/specific-field/item-page-specific-field.component.ts deleted file mode 100644 index 4161505ccc..0000000000 --- a/src/app/item-page/specific-field/item-page-specific-field.component.ts +++ /dev/null @@ -1,24 +0,0 @@ -import { Component, OnInit, Input } from '@angular/core'; -import { Item } from "../../core/shared/item.model"; - -@Component({ - templateUrl: './item-page-specific-field.component.html' -}) -export class ItemPageSpecificFieldComponent { - - @Input() item: Item; - - fields : string[]; - - label : string; - - separator : string = "
"; - - constructor() { - this.universalInit(); - } - - universalInit() { - - } -} diff --git a/src/app/shared/utils/file-size-pipe.ts b/src/app/shared/utils/file-size-pipe.ts index fbd3ae081c..e00092271e 100644 --- a/src/app/shared/utils/file-size-pipe.ts +++ b/src/app/shared/utils/file-size-pipe.ts @@ -9,6 +9,7 @@ import { Pipe, PipeTransform } from '@angular/core'; * {{ 1024 | fileSize}} * formats to: 1 KB */ + @Pipe({name: 'dsFileSize'}) export class FileSizePipe implements PipeTransform { diff --git a/src/app/shared/utils/safe-url-pipe.ts b/src/app/shared/utils/safe-url-pipe.ts index e05e58764b..a32aa18ab8 100644 --- a/src/app/shared/utils/safe-url-pipe.ts +++ b/src/app/shared/utils/safe-url-pipe.ts @@ -1,6 +1,11 @@ import { Pipe, PipeTransform } from '@angular/core'; import { DomSanitizer } from '@angular/platform-browser'; +/** + * This pipe explicitly escapes the sanitization of a URL, + * only use this when you are sure the URL is indeed safe + */ + @Pipe({name: 'dsSafeUrl'}) export class SafeUrlPipe implements PipeTransform { constructor(private domSanitizer: DomSanitizer) {} diff --git a/src/app/thumbnail/thumbnail.component.spec.ts b/src/app/thumbnail/thumbnail.component.spec.ts new file mode 100644 index 0000000000..eb126287ba --- /dev/null +++ b/src/app/thumbnail/thumbnail.component.spec.ts @@ -0,0 +1,49 @@ +import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { By } from '@angular/platform-browser'; +import { DebugElement } from '@angular/core'; + +import { ThumbnailComponent } from "./thumbnail.component"; +import { Bitstream } from "../core/shared/bitstream.model"; +import { SafeUrlPipe } from "../shared/utils/safe-url-pipe"; + + +describe('ThumbnailComponent', () => { + let comp: ThumbnailComponent; + let fixture: ComponentFixture; + let de: DebugElement; + let el: HTMLElement; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ThumbnailComponent, SafeUrlPipe] + }) + .compileComponents(); + + })); + + + beforeEach(() => { + fixture = TestBed.createComponent(ThumbnailComponent); + + comp = fixture.componentInstance; // BannerComponent test instance + de = fixture.debugElement.query(By.css('div.thumbnail')); + el = de.nativeElement; + }); + + + it('should display image', () => { + comp.thumbnail = new Bitstream(); + comp.thumbnail.retrieve = "test.url"; + fixture.detectChanges(); + let image : HTMLElement = de.query(By.css('img')).nativeElement; + expect(image.getAttribute("src")).toBe(comp.thumbnail.retrieve); + }); + + it('should display placeholder', () => { + fixture.detectChanges(); + let image : HTMLElement = de.query(By.css('img')).nativeElement; + expect(image.getAttribute("src")).toBe(comp.holderSource); + }); + + +}); \ No newline at end of file diff --git a/src/app/thumbnail/thumbnail.component.ts b/src/app/thumbnail/thumbnail.component.ts index e8e226c004..61b9e31fbe 100644 --- a/src/app/thumbnail/thumbnail.component.ts +++ b/src/app/thumbnail/thumbnail.component.ts @@ -1,6 +1,12 @@ import { Component, Input, OnInit } from '@angular/core'; import { Bitstream } from "../core/shared/bitstream.model"; +/** + * This component renders a given Bitstream as a thumbnail. + * One input parameter of type Bitstream is expected. + * If no Bitstream is provided, a holderjs image will be rendered instead. + */ + @Component({ selector: 'ds-thumbnail', styleUrls: ['./thumbnail.component.css'], diff --git a/src/backend/bitstreams.ts b/src/backend/bitstreams.ts index 537cd5890b..da6a87e042 100644 --- a/src/backend/bitstreams.ts +++ b/src/backend/bitstreams.ts @@ -43,6 +43,40 @@ export const BITSTREAMS = { ], "format": "JPEG", "mimetype": "image/jpeg" - } + }, + { + "_links": { + "self": { "href": "/bitstreams/8934" }, + "bundle": { "href": "/bundles/99f78e5e-3677-43b0-aaef-cddaa1a49092" }, + "retrieve": { "href": "/rest/bitstreams/ba7d24f2-8fc7-4b8e-b7b6-6c32be1c12a6/retrieve" } + }, + "id": "8934", + "uuid": "ba7d24f2-8fc7-4b8e-b7b6-6c32be1c12a6", + "name": "license.txt", + "size": 41183, + "checksum": { + "value": "8ad416e8a39e645020e13e06f1427814", + "algorithm": "MD5" + }, + "metadata": [ + { "key": "dc.title", "value": "license.txt", "language": null }, + { "key": "dc.description", "value": "License", "language": "en" } + ], + "format": "Text", + "mimetype": "text/plain" + }, + { + "_links": { + "self": { "href": "/bitstreams/4688" }, + }, + "id": "4688", + "uuid": "1bb1be24-c934-41e3-a0fb-ca7a71ab0e71", + "type": "bitstream", + "name": "collection-5179-logo.png", + "size": 299832, + "url": "/bitstreams/1bb1be24-c934-41e3-a0fb-ca7a71ab0e71/retrieve", + "format": "PNG", + "mimetype": "image/png" + }, ] }; diff --git a/src/backend/bundles.ts b/src/backend/bundles.ts index 5370c1a5a3..b7411c6412 100644 --- a/src/backend/bundles.ts +++ b/src/backend/bundles.ts @@ -37,6 +37,24 @@ export const BUNDLES = { "metadata": [ { "key": "dc.title", "value": "THUMBNAIL", "language": "en" } ] + }, + { + "_links": { + "self": { "href": "/bundles/8475" }, + "items": [ + { "href": "/items/8871" } + ], + "bitstreams": [ + { "href": "/bitstreams/8934" }, + ], + "primaryBitstream": { "href": "/bitstreams/8934" } + }, + "id": "8475", + "uuid": "99f78e5e-3677-43b0-aaef-cddaa1a49092", + "name": "LICENSE", + "metadata": [ + { "key": "dc.title", "value": "LICENSE", "language": "en" } + ] } ] }; diff --git a/src/backend/collections.ts b/src/backend/collections.ts index ffc56b0140..5170f14c46 100644 --- a/src/backend/collections.ts +++ b/src/backend/collections.ts @@ -6,7 +6,8 @@ export const COLLECTIONS = { "items": [ { "href": "/items/8871" }, { "href": "/items/9978" } - ] + ], + "logo": { "href": "/bitstreams/4688" } }, "id": "5179", "uuid": "9e32a2e2-6b91-4236-a361-995ccdc14c60", diff --git a/src/backend/items.ts b/src/backend/items.ts index eb69821037..63913f8245 100644 --- a/src/backend/items.ts +++ b/src/backend/items.ts @@ -17,9 +17,12 @@ export const ITEMS = { { "href": "/bundles/2355" }, - // { - // "href": "/bundles/5687" - // } + { + "href": "/bundles/5687" + }, + { + "href": "/bundles/8475" + } ] }, "id": "8871", @@ -91,86 +94,7 @@ export const ITEMS = { "value": "(not specified)", "language": "en" } - ], - "_embedded": { - "parents": [ - { - "_links": { - "self": { "href": "/collections/5179" }, - "items": [ - { "href": "/items/8871" }, - { "href": "/items/9978" } - ] - }, - "id": "5179", - "uuid": "9e32a2e2-6b91-4236-a361-995ccdc14c60", - "type": "collection", - "name": "A Test Collection", - "handle": "123456789/5179", - }, - { - "_links": { - "self": { "href": "/collections/6547" }, - "items": [ - { "href": "/items/8871" }, - { "href": "/items/9978" } - ] - }, - "id": "6547", - "uuid": "598ce822-c357-46f3-ab70-63724d02d6ad", - "type": "collection", - "name": "Another Test Collection", - "handle": "123456789/6547", - } - ], - "bundles": [ - { - "_links": { - "self": { "href": "/bundles/2355" }, - "items": [ - { "href": "/items/8871" } - ], - "bitstreams": [ - { "href": "/bitstreams/3678" }, - ], - "primaryBitstream": { "href": "/bitstreams/3678" } - }, - "id": "2355", - "uuid": "35e0606d-5e18-4f9c-aa61-74fc751cc3f9", - "type": "bundle", - "name": "ORIGINAL", - "metadata": [ - { "key": "dc.title", "value": "ORIGINAL", "language": "en" } - ], - "_embedded": { - "bitstreams": [ - { - "_links": { - "self": { "href": "/bitstreams/3678" }, - "bundle": { "href": "/bundles/35e0606d-5e18-4f9c-aa61-74fc751cc3f9" }, - "retrieve": { "href": "/bitstreams/43c57c2b-206f-4645-8c8f-5f10c84b09fa/retrieve" } - }, - "id": "3678", - "uuid": "43c57c2b-206f-4645-8c8f-5f10c84b09fa", - "type": "bitstream", - "name": "do_open_access_CRL.pdf", - "size": 636626, - "checksum": { - "value": "063dfbbbac873aa3fca479b878eccff3", - "algorithm": "MD5" - }, - "metadata": [ - { "key": "dc.title", "value": "do_open_access_CRL.pdf", "language": null }, - { "key": "dc.description", "value": "Conference Paper", "language": "en" } - ], - "format": "Adobe PDF", - "mimetype": "application/pdf" - } - ] - } - } - ] - } + ] }, { "_links": { @@ -189,9 +113,9 @@ export const ITEMS = { { "href": "/bundles/2355" }, - // { - // "href": "/bundles/5687" - // } + { + "href": "/bundles/5687" + } ] }, "id": "9978", @@ -243,39 +167,7 @@ export const ITEMS = { "value": "(not specified)", "language": "en" } - ], - "_embedded": { - "parents": [ - { - "_links": { - "self": { "href": "/collections/5179" }, - "items": [ - { "href": "/items/8871" }, - { "href": "/items/9978" } - ] - }, - "id": "5179", - "uuid": "9e32a2e2-6b91-4236-a361-995ccdc14c60", - "type": "collection", - "name": "A Test Collection", - "handle": "123456789/5179", - }, - { - "_links": { - "self": { "href": "/collections/6547" }, - "items": [ - { "href": "/items/8871" }, - { "href": "/items/9978" } - ] - }, - "id": "6547", - "uuid": "598ce822-c357-46f3-ab70-63724d02d6ad", - "type": "collection", - "name": "Another Test Collection", - "handle": "123456789/6547", - } - ] - } + ] } ] }; diff --git a/src/server.routes.ts b/src/server.routes.ts index 704ea15df5..19ba0264aa 100644 --- a/src/server.routes.ts +++ b/src/server.routes.ts @@ -10,5 +10,5 @@ * ]; **/ export const routes: string[] = [ - 'home', 'items/:id' , '**' + 'home', 'items/:id' , 'collections/:id', '**' ]; diff --git a/src/server.ts b/src/server.ts index 13837821d0..42f8d9bd5e 100644 --- a/src/server.ts +++ b/src/server.ts @@ -82,22 +82,25 @@ function ngApp(req, res) { function onHandleError(parentZoneDelegate, currentZone, targetZone, error) { console.warn('Error in SSR, serving for direct CSR'); res.sendFile('index.html', { root: './src' }); - return false; } - Zone.current.fork({ name: 'CSR fallback', onHandleError }).run(() => { - res.render('index', { - req, - res, - // time: true, // use this to determine what part of your app is slow only in development - async: EnvConfig.universal.async, - preboot: EnvConfig.universal.preboot, - baseUrl: EnvConfig.ui.nameSpace, - requestUrl: req.originalUrl, - originUrl: EnvConfig.ui.baseUrl + if (EnvConfig.universal.preboot) { + Zone.current.fork({ name: 'CSR fallback', onHandleError }).run(() => { + res.render('index', { + req, + res, + // time: true, // use this to determine what part of your app is slow only in development + async: EnvConfig.universal.async, + preboot: EnvConfig.universal.preboot, + baseUrl: EnvConfig.ui.nameSpace, + requestUrl: req.originalUrl, + originUrl: EnvConfig.ui.baseUrl + }); }); - }); - + } + else { + res.sendFile('index.html', { root: './src' }); + } } /** diff --git a/yarn.lock b/yarn.lock index 17ed417d48..c1e67e8572 100644 --- a/yarn.lock +++ b/yarn.lock @@ -301,14 +301,10 @@ alphanum-sort@^1.0.1, alphanum-sort@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/alphanum-sort/-/alphanum-sort-1.0.2.tgz#97a1119649b211ad33691d9f9f486a8ec9fbe0a3" -amdefine@1.0.0: +amdefine@1.0.0, amdefine@>=0.0.4: version "1.0.0" resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.0.tgz#fd17474700cb5cc9c2b709f0be9d23ce3c198c33" -amdefine@>=0.0.4: - version "1.0.1" - resolved "https://registry.yarnpkg.com/amdefine/-/amdefine-1.0.1.tgz#4a5282ac164729e93619bcfd3ad151f817ce91f5" - angular2-express-engine@2.1.0-rc.1: version "2.1.0-rc.1" resolved "https://registry.yarnpkg.com/angular2-express-engine/-/angular2-express-engine-2.1.0-rc.1.tgz#79c8e481cde7ff1253b373cbf98de7c9fab4f215" @@ -3521,11 +3517,7 @@ lowercase-keys@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/lowercase-keys/-/lowercase-keys-1.0.0.tgz#4e3366b39e7f5457e35f1324bdf6f88d0bfc7306" -lru-cache@2: - version "2.7.3" - resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.7.3.tgz#6d4524e8b955f95d4f5b58851ce21dd72fb4e952" - -lru-cache@2.2.x: +lru-cache@2, lru-cache@2.2.x: version "2.2.4" resolved "https://registry.yarnpkg.com/lru-cache/-/lru-cache-2.2.4.tgz#6c658619becf14031d0d0b594b16042ce4dc063d" @@ -4659,14 +4651,10 @@ punycode@^1.2.4, punycode@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/punycode/-/punycode-1.4.1.tgz#c0d5a63b2718800ad8e1eb0fa5269c84dd41845e" -q@1.4.1: +q@1.4.1, q@^1.1.2, q@^1.4.1: version "1.4.1" resolved "https://registry.yarnpkg.com/q/-/q-1.4.1.tgz#55705bcd93c5f3673530c2c2cbc0c2b3addc286e" -q@^1.1.2, q@^1.4.1: - version "1.5.0" - resolved "https://registry.yarnpkg.com/q/-/q-1.5.0.tgz#dd01bac9d06d30e6f219aecb8253ee9ebdc308f1" - qjobs@^1.1.4: version "1.1.5" resolved "https://registry.yarnpkg.com/qjobs/-/qjobs-1.1.5.tgz#659de9f2cf8dcc27a1481276f205377272382e73" @@ -5063,13 +5051,13 @@ right-align@^0.1.1: dependencies: align-text "^0.1.1" -rimraf@2, rimraf@^2.2.8, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.6.0, rimraf@^2.6.1: +rimraf@2, rimraf@^2.2.8, rimraf@^2.4.4, rimraf@^2.5.1, rimraf@^2.5.2, rimraf@^2.6.0, rimraf@^2.6.1: version "2.6.1" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.6.1.tgz#c2338ec643df7a1b7fe5c54fa86f57428a55f33d" dependencies: glob "^7.0.5" -rimraf@2.5.4, rimraf@^2.4.4: +rimraf@2.5.4: version "2.5.4" resolved "https://registry.yarnpkg.com/rimraf/-/rimraf-2.5.4.tgz#96800093cbf1a0c86bd95b4625467535c29dfa04" dependencies: