diff --git a/src/app/+home-page/home-page-routing.module.ts b/src/app/+home-page/home-page-routing.module.ts index e68b633a6d..f70109e3fe 100644 --- a/src/app/+home-page/home-page-routing.module.ts +++ b/src/app/+home-page/home-page-routing.module.ts @@ -6,7 +6,7 @@ import { HomePageComponent } from './home-page.component'; @NgModule({ imports: [ RouterModule.forChild([ - { path: '', component: HomePageComponent, pathMatch: 'full' } + { path: '', component: HomePageComponent, pathMatch: 'full', data: { title: 'DSpace Angular :: Home' } } ]) ] }) diff --git a/src/app/+search-page/search-page-routing.module.ts b/src/app/+search-page/search-page-routing.module.ts index de2d64c6c9..a74a88dcfa 100644 --- a/src/app/+search-page/search-page-routing.module.ts +++ b/src/app/+search-page/search-page-routing.module.ts @@ -6,7 +6,7 @@ import { SearchPageComponent } from './search-page.component'; @NgModule({ imports: [ RouterModule.forChild([ - { path: '', component: SearchPageComponent } + { path: '', component: SearchPageComponent, data: { title: 'DSpace Angular :: Search' } } ]) ] }) diff --git a/src/app/core/cache/models/normalized-bitstream-format.model.ts b/src/app/core/cache/models/normalized-bitstream-format.model.ts index 2813794011..bb8b049a1c 100644 --- a/src/app/core/cache/models/normalized-bitstream-format.model.ts +++ b/src/app/core/cache/models/normalized-bitstream-format.model.ts @@ -1,13 +1,30 @@ -import { inheritSerialization } from 'cerialize'; +import { inheritSerialization, autoserialize } from 'cerialize'; +import { mapsTo } from '../builders/build-decorators'; + +import { BitstreamFormat } from '../../shared/bitstream-format.model'; import { NormalizedObject } from './normalized-object.model'; +@mapsTo(BitstreamFormat) @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; - } + @autoserialize + shortDescription: string; + + @autoserialize + description: string; + + @autoserialize + mimetype: string; + + @autoserialize + supportLevel: number; + + @autoserialize + internal: boolean; + + @autoserialize + extensions: string; } diff --git a/src/app/core/cache/models/normalized-bitstream.model.ts b/src/app/core/cache/models/normalized-bitstream.model.ts index ba5343e252..db8002a874 100644 --- a/src/app/core/cache/models/normalized-bitstream.model.ts +++ b/src/app/core/cache/models/normalized-bitstream.model.ts @@ -21,16 +21,11 @@ export class NormalizedBitstream extends NormalizedDSpaceObject { @autoserialize content: string; - /** - * The mime type of this Bitstream - */ - @autoserialize - mimetype: string; - /** * The format of this Bitstream */ @autoserialize + @relationship(ResourceType.BitstreamFormat, false) format: string; /** diff --git a/src/app/core/cache/models/normalized-object-factory.ts b/src/app/core/cache/models/normalized-object-factory.ts index e9f3b1c9e6..3c67b18b3e 100644 --- a/src/app/core/cache/models/normalized-object-factory.ts +++ b/src/app/core/cache/models/normalized-object-factory.ts @@ -15,11 +15,9 @@ export class NormalizedObjectFactory { 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.BitstreamFormat: { + return NormalizedBitstreamFormat + } case ResourceType.Bundle: { return NormalizedBundle } diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 818a398e72..90501c4308 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -2,7 +2,7 @@ import { ComponentFixture, TestBed, async, fakeAsync, inject, tick } from '@angu import { RouterTestingModule } from '@angular/router/testing'; import { Location, CommonModule } from '@angular/common'; -import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; +import { Component, DebugElement, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { By, Meta, MetaDefinition } from '@angular/platform-browser'; import { Router } from '@angular/router'; @@ -58,6 +58,10 @@ describe('MetadataService', () => { let router: Router; let fixture: ComponentFixture; + let tagStore: Map; + + let envConfig: GlobalConfig; + beforeEach(() => { store = new Store(undefined, undefined, undefined); @@ -74,7 +78,7 @@ describe('MetadataService', () => { StoreModule.forRoot({}), RouterTestingModule.withRoutes([ { path: 'items/:id', component: DummyItemComponent, pathMatch: 'full', data: { type: NormalizedItem } }, - { path: 'other', component: DummyItemComponent, pathMatch: 'full' } + { path: 'other', component: DummyItemComponent, pathMatch: 'full', data: { title: 'Dummy Title', description: 'This is a dummy component for testing!' } } ]) ], declarations: [ @@ -95,11 +99,14 @@ describe('MetadataService', () => { meta = TestBed.get(Meta); metadataService = TestBed.get(MetadataService); + envConfig = TestBed.get(GLOBAL_CONFIG); + router = TestBed.get(Router); location = TestBed.get(Location); fixture = TestBed.createComponent(TestComponent); - fixture.detectChanges(); + + tagStore = metadataService.getTagStore(); }); beforeEach(() => { @@ -112,40 +119,45 @@ describe('MetadataService', () => { spyOn(remoteDataBuildService, 'build').and.returnValue(MockItem); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); tick(); - const tagStore: Map = metadataService.getTagStore(); - expect(tagStore.get('citation_title').length).toEqual(1); expect(tagStore.get('citation_title')[0].content).toEqual('Test PowerPoint Document'); + expect(tagStore.get('citation_author')[0].content).toEqual('Doe, Jane'); + expect(tagStore.get('citation_date')[0].content).toEqual('1650-06-26T19:58:25Z'); + expect(tagStore.get('citation_issn')[0].content).toEqual('123456789'); + expect(tagStore.get('citation_language')[0].content).toEqual('en'); + expect(tagStore.get('citation_keywords')[0].content).toEqual('keyword1; keyword2; keyword3'); })); it('items page should set meta tags as published Thesis', fakeAsync(() => { spyOn(remoteDataBuildService, 'build').and.returnValue(mockPublisher(mockType(MockItem, 'Thesis'))); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); tick(); - const tagStore: Map = metadataService.getTagStore(); - expect(tagStore.get('citation_dissertation_name').length).toEqual(1); expect(tagStore.get('citation_dissertation_name')[0].content).toEqual('Test PowerPoint Document'); - expect(tagStore.get('citation_dissertation_institution').length).toEqual(1); expect(tagStore.get('citation_dissertation_institution')[0].content).toEqual('Mock Publisher'); + expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual([envConfig.ui.baseUrl, router.url].join('')); + expect(tagStore.get('citation_pdf_url')[0].content).toEqual('https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content'); })); it('items page should set meta tags as published Technical Report', fakeAsync(() => { spyOn(remoteDataBuildService, 'build').and.returnValue(mockPublisher(mockType(MockItem, 'Technical Report'))); router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); tick(); - const tagStore: Map = metadataService.getTagStore(); - expect(tagStore.get('citation_technical_report_institution').length).toEqual(1); expect(tagStore.get('citation_technical_report_institution')[0].content).toEqual('Mock Publisher'); })); - it('other navigation should clear meta tags', fakeAsync(() => { + it('other navigation should title and description', fakeAsync(() => { + spyOn(remoteDataBuildService, 'build').and.returnValue(MockItem); + router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']); + tick(); + expect(tagStore.size).toBeGreaterThan(0) router.navigate(['/other']); tick(); - const tagStore: Map = metadataService.getTagStore(); - expect(tagStore.size).toEqual(0); + expect(tagStore.size).toEqual(2); + expect(tagStore.get('title')[0].content).toEqual('Dummy Title'); + expect(tagStore.get('description')[0].content).toEqual('This is a dummy component for testing!'); })); const mockType = (mockItem: Item, type: string): Item => { - const typedMockItem = Object.assign({}, mockItem) as Item; + const typedMockItem = Object.assign(new Item(), mockItem) as Item; for (const metadatum of typedMockItem.metadata) { if (metadatum.key === 'dc.type') { metadatum.value = type; @@ -156,7 +168,7 @@ describe('MetadataService', () => { } const mockPublisher = (mockItem: Item): Item => { - const publishedMockItem = Object.assign({}, mockItem) as Item; + const publishedMockItem = Object.assign(new Item(), mockItem) as Item; publishedMockItem.metadata.push({ key: 'dc.publisher', language: 'en_US', diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index a7674ae7f2..cfc4744161 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -40,15 +40,17 @@ export class MetadataService { private meta: Meta, @Inject(GLOBAL_CONFIG) private envConfig: GlobalConfig ) { + // TODO: this.meta.addTags([ - { property: 'og:title', content: 'DSpace Angular Universal' } + { property: 'og:title', content: 'DSpace Angular Universal' }, + { property: 'og:description', content: 'The modern front-end for DSpace 7.' } ]); this.initialized = false; this.tagStore = new Map(); } public listenForRouteChange(): void { - const subscription = this.router.events + this.router.events .filter((event) => event instanceof NavigationEnd) .map(() => this.router.routerState.root) .map((route: ActivatedRoute) => { @@ -60,6 +62,7 @@ export class MetadataService { } private processRouteChange(routeInfo: any): void { + this.clearMetaTags(); if (routeInfo.params.value.id && routeInfo.data.value.type) { this.objectCacheService.getByUUID(routeInfo.params.value.id, routeInfo.data.value.type) .first().subscribe((normalizedObject: CacheableObject) => { @@ -70,13 +73,18 @@ export class MetadataService { this.currentObject.next(dspaceObject); }); } else { - this.clearMetaTags(); + if (routeInfo.data.value.title) { + this.addMetaTag('title', routeInfo.data.value.title); + } + if (routeInfo.data.value.description) { + this.addMetaTag('description', routeInfo.data.value.description); + } } } private initialize(dspaceObject: DSpaceObject): void { this.currentObject = new BehaviorSubject(dspaceObject); - const subscription = this.currentObject.asObservable().distinctUntilKeyChanged('uuid').subscribe(() => { + this.currentObject.asObservable().distinctUntilKeyChanged('uuid').subscribe(() => { this.setMetaTags(); }); this.initialized = true; @@ -91,7 +99,8 @@ export class MetadataService { private setMetaTags(): void { - this.clearMetaTags(); + this.setTitleTag(); + this.setDescriptionTag(); this.setCitationTitleTag(); this.setCitationAuthorTags(); @@ -131,6 +140,23 @@ export class MetadataService { } + /** + * Add to the + */ + private setTitleTag(): void { + const value = this.getMetaTagValue('dc.title'); + this.addMetaTag('title', value); + } + + /** + * Add to the + */ + private setDescriptionTag(): void { + // TODO: truncate abstract + const value = this.getMetaTagValue('dc.description.abstract'); + this.addMetaTag('desciption', value); + } + /** * Add to the */ @@ -175,7 +201,7 @@ export class MetadataService { * Add to the */ private setCitationLanguageTag(): void { - const value = this.getMetaTagValue('dc.language.iso'); + const value = this.getFirstMetaTagValue(['dc.language', 'dc.language.iso']); this.addMetaTag('citation_language', value); } @@ -229,12 +255,13 @@ export class MetadataService { const item = this.currentObject.value as Item; // NOTE: Observable resolves many times with same data // taking only two, fist one is empty array - const subscription = item.getFiles().take(2).subscribe((bitstreams: Bitstream[]) => { + item.getFiles().take(2).subscribe((bitstreams: Bitstream[]) => { for (const bitstream of bitstreams) { - if (bitstream.mimetype === 'application/pdf') { - this.addMetaTag('citation_abstract_html_url', bitstream.content); - break; - } + bitstream.format.payload.take(1).subscribe((format) => { + if (format.mimetype === 'application/pdf') { + this.addMetaTag('citation_pdf_url', bitstream.content); + } + }); } }); } diff --git a/src/app/core/shared/bitstream-format.model.ts b/src/app/core/shared/bitstream-format.model.ts new file mode 100644 index 0000000000..c0f6be29c9 --- /dev/null +++ b/src/app/core/shared/bitstream-format.model.ts @@ -0,0 +1,17 @@ +import { DSpaceObject } from './dspace-object.model'; + +export class BitstreamFormat extends DSpaceObject { + + shortDescription: string; + + description: string; + + mimetype: string; + + supportLevel: number; + + internal: boolean; + + extensions: string; + +} diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index 5e4ee929d4..0b77a7b032 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -1,6 +1,7 @@ import { DSpaceObject } from './dspace-object.model'; import { RemoteData } from '../data/remote-data'; import { Item } from './item.model'; +import { BitstreamFormat } from './bitstream-format.model'; export class Bitstream extends DSpaceObject { @@ -9,11 +10,6 @@ export class Bitstream extends DSpaceObject { */ sizeBytes: number; - /** - * The mime type of this Bitstream - */ - mimetype: string; - /** * The description of this Bitstream */ @@ -24,6 +20,11 @@ export class Bitstream extends DSpaceObject { */ bundleName: string; + /** + * An array of Bitstream Format of this Bitstream + */ + format: RemoteData; + /** * An array of Items that are direct parents of this Bitstream */ diff --git a/src/app/shared/mocks/mock-item.ts b/src/app/shared/mocks/mock-item.ts index 43ad178485..0331491aa0 100644 --- a/src/app/shared/mocks/mock-item.ts +++ b/src/app/shared/mocks/mock-item.ts @@ -2,6 +2,7 @@ import { Observable } from 'rxjs/Observable'; import { Item } from '../../core/shared/item.model'; +/* tslint:disable:no-shadowed-variable */ export const MockItem: Item = Object.assign(new Item(), { handle: '10673/6', lastModified: '2017-04-24T19:44:08.178+0000', @@ -33,7 +34,114 @@ export const MockItem: Item = Object.assign(new Item(), { observer.next({}); }), payload: Observable.create((observer) => { - observer.next([]); + observer.next([ + { + sizeBytes: 10201, + content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content', + format: { + self: { + _isScalar: true, + value: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10', + scheduler: null + }, + requestPending: Observable.create((observer) => { + observer.next(false); + }), + responsePending: Observable.create((observer) => { + observer.next(false); + }), + isSuccessFul: Observable.create((observer) => { + observer.next(true); + }), + errorMessage: Observable.create((observer) => { + observer.next(''); + }), + statusCode: Observable.create((observer) => { + observer.next(202); + }), + pageInfo: Observable.create((observer) => { + observer.next({}); + }), + payload: Observable.create((observer) => { + observer.next({ + shortDescription: 'Microsoft Word XML', + description: 'Microsoft Word XML', + mimetype: 'application/vnd.openxmlformats-officedocument.wordprocessingml.document', + supportLevel: 0, + internal: false, + extensions: null, + self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/10' + }); + }) + }, + bundleName: 'ORIGINAL', + self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713', + id: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', + uuid: 'cf9b0c8e-a1eb-4b65-afd0-567366448713', + type: 'bitstream', + name: 'test_word.docx', + metadata: [ + { + key: 'dc.title', + language: null, + value: 'test_word.docx' + } + ] + }, + { + sizeBytes: 31302, + content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content', + format: { + self: { + _isScalar: true, + value: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4', + scheduler: null + }, + requestPending: Observable.create((observer) => { + observer.next(false); + }), + responsePending: Observable.create((observer) => { + observer.next(false); + }), + isSuccessFul: Observable.create((observer) => { + observer.next(true); + }), + errorMessage: Observable.create((observer) => { + observer.next(''); + }), + statusCode: Observable.create((observer) => { + observer.next(202); + }), + pageInfo: Observable.create((observer) => { + observer.next({}); + }), + payload: Observable.create((observer) => { + observer.next({ + shortDescription: 'Adobe PDF', + description: 'Adobe Portable Document Format', + mimetype: 'application/pdf', + supportLevel: 0, + internal: false, + extensions: null, + self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/4' + }); + }) + }, + bundleName: 'ORIGINAL', + self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28', + id: '99b00f3c-1cc6-4689-8158-91965bee6b28', + uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28', + type: 'bitstream', + name: 'test_pdf.pdf', + metadata: [ + { + key: 'dc.title', + language: null, + value: 'test_pdf.pdf' + } + ] + } + ]); }) }, self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357', @@ -162,3 +270,4 @@ export const MockItem: Item = Object.assign(new Item(), { }) } }) +/* tslint:enable:no-shadowed-variable */