forked from hazza/dspace-angular
Merge pull request #1228 from atmire/w2p-79768_fix-issues-with-meta-tags
Fix issues with meta tags
This commit is contained in:
@@ -1,6 +1,11 @@
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { BehaviorSubject, combineLatest as observableCombineLatest, Observable, Subject } from 'rxjs';
|
||||
import {
|
||||
BehaviorSubject,
|
||||
combineLatest as observableCombineLatest,
|
||||
Observable,
|
||||
Subject
|
||||
} from 'rxjs';
|
||||
import { filter, map, mergeMap, startWith, switchMap, take } from 'rxjs/operators';
|
||||
import { PaginatedSearchOptions } from '../shared/search/paginated-search-options.model';
|
||||
import { SearchService } from '../core/shared/search/search.service';
|
||||
@@ -8,8 +13,6 @@ import { SortDirection, SortOptions } from '../core/cache/models/sort-options.mo
|
||||
import { CollectionDataService } from '../core/data/collection-data.service';
|
||||
import { PaginatedList } from '../core/data/paginated-list.model';
|
||||
import { RemoteData } from '../core/data/remote-data';
|
||||
|
||||
import { MetadataService } from '../core/metadata/metadata.service';
|
||||
import { Bitstream } from '../core/shared/bitstream.model';
|
||||
|
||||
import { Collection } from '../core/shared/collection.model';
|
||||
@@ -65,7 +68,6 @@ export class CollectionPageComponent implements OnInit {
|
||||
constructor(
|
||||
private collectionDataService: CollectionDataService,
|
||||
private searchService: SearchService,
|
||||
private metadata: MetadataService,
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private authService: AuthService,
|
||||
@@ -122,10 +124,6 @@ export class CollectionPageComponent implements OnInit {
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
map((collection) => getCollectionPageRoute(collection.id))
|
||||
);
|
||||
|
||||
this.route.queryParams.pipe(take(1)).subscribe((params) => {
|
||||
this.metadata.processRemoteData(this.collectionRD$);
|
||||
});
|
||||
}
|
||||
|
||||
isNotEmpty(object: any) {
|
||||
|
@@ -1,8 +1,8 @@
|
||||
import {filter, map} from 'rxjs/operators';
|
||||
import { filter, map } from 'rxjs/operators';
|
||||
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
|
||||
import { Observable , BehaviorSubject } from 'rxjs';
|
||||
import { Observable, BehaviorSubject } from 'rxjs';
|
||||
|
||||
import { ItemPageComponent } from '../simple/item-page.component';
|
||||
import { MetadataMap } from '../../core/shared/metadata.models';
|
||||
@@ -11,8 +11,6 @@ import { ItemDataService } from '../../core/data/item-data.service';
|
||||
import { RemoteData } from '../../core/data/remote-data';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
|
||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
||||
|
||||
import { fadeInOut } from '../../shared/animations/fade';
|
||||
import { hasValue } from '../../shared/empty.util';
|
||||
import { AuthService } from '../../core/auth/auth.service';
|
||||
@@ -35,8 +33,8 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit {
|
||||
|
||||
metadata$: Observable<MetadataMap>;
|
||||
|
||||
constructor(route: ActivatedRoute, router: Router, items: ItemDataService, metadataService: MetadataService, authService: AuthService) {
|
||||
super(route, router, items, metadataService, authService);
|
||||
constructor(route: ActivatedRoute, router: Router, items: ItemDataService, authService: AuthService) {
|
||||
super(route, router, items, authService);
|
||||
}
|
||||
|
||||
/*** AoT inheritance fix, will hopefully be resolved in the near future **/
|
||||
|
@@ -8,8 +8,6 @@ import { RemoteData } from '../../core/data/remote-data';
|
||||
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
|
||||
import { MetadataService } from '../../core/metadata/metadata.service';
|
||||
|
||||
import { fadeInOut } from '../../shared/animations/fade';
|
||||
import { getAllSucceededRemoteDataPayload, redirectOn4xx } from '../../core/shared/operators';
|
||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||
@@ -54,7 +52,6 @@ export class ItemPageComponent implements OnInit {
|
||||
private route: ActivatedRoute,
|
||||
private router: Router,
|
||||
private items: ItemDataService,
|
||||
private metadataService: MetadataService,
|
||||
private authService: AuthService,
|
||||
) { }
|
||||
|
||||
@@ -66,7 +63,6 @@ export class ItemPageComponent implements OnInit {
|
||||
map((data) => data.dso as RemoteData<Item>),
|
||||
redirectOn4xx(this.router, this.authService)
|
||||
);
|
||||
this.metadataService.processRemoteData(this.itemRD$);
|
||||
this.itemPageRoute$ = this.itemRD$.pipe(
|
||||
getAllSucceededRemoteDataPayload(),
|
||||
map((item) => getItemPageRoute(item))
|
||||
|
@@ -13,6 +13,7 @@ import {
|
||||
BitstreamFormatRegistryState
|
||||
} from '../+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
|
||||
import { historyReducer, HistoryState } from './history/history.reducer';
|
||||
import { metaTagReducer, MetaTagState } from './metadata/meta-tag.reducer';
|
||||
|
||||
export interface CoreState {
|
||||
'bitstreamFormats': BitstreamFormatRegistryState;
|
||||
@@ -24,6 +25,7 @@ export interface CoreState {
|
||||
'index': MetaIndexState;
|
||||
'auth': AuthState;
|
||||
'json/patch': JsonPatchOperationsState;
|
||||
'metaTag': MetaTagState;
|
||||
'route': RouteState;
|
||||
}
|
||||
|
||||
@@ -37,5 +39,6 @@ export const coreReducers: ActionReducerMap<CoreState> = {
|
||||
'index': indexReducer,
|
||||
'auth': authReducer,
|
||||
'json/patch': jsonPatchOperationsReducer,
|
||||
'metaTag': metaTagReducer,
|
||||
'route': routeReducer
|
||||
};
|
||||
|
23
src/app/core/metadata/meta-tag.actions.ts
Normal file
23
src/app/core/metadata/meta-tag.actions.ts
Normal file
@@ -0,0 +1,23 @@
|
||||
import { type } from '../../shared/ngrx/type';
|
||||
import { Action } from '@ngrx/store';
|
||||
|
||||
// tslint:disable:max-classes-per-file
|
||||
export const MetaTagTypes = {
|
||||
ADD: type('dspace/meta-tag/ADD'),
|
||||
CLEAR: type('dspace/meta-tag/CLEAR')
|
||||
};
|
||||
|
||||
export class AddMetaTagAction implements Action {
|
||||
type = MetaTagTypes.ADD;
|
||||
payload: string;
|
||||
|
||||
constructor(property: string) {
|
||||
this.payload = property;
|
||||
}
|
||||
}
|
||||
|
||||
export class ClearMetaTagAction implements Action {
|
||||
type = MetaTagTypes.CLEAR;
|
||||
}
|
||||
|
||||
export type MetaTagAction = AddMetaTagAction | ClearMetaTagAction;
|
50
src/app/core/metadata/meta-tag.reducer.spec.ts
Normal file
50
src/app/core/metadata/meta-tag.reducer.spec.ts
Normal file
@@ -0,0 +1,50 @@
|
||||
/**
|
||||
* The contents of this file are subject to the license and copyright
|
||||
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||
* tree and available online at
|
||||
*
|
||||
* http://www.dspace.org/license/
|
||||
*/
|
||||
|
||||
import { metaTagReducer } from './meta-tag.reducer';
|
||||
import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions';
|
||||
|
||||
const nullAction = { type: null };
|
||||
|
||||
describe('metaTagReducer', () => {
|
||||
it('should start with an empty array', () => {
|
||||
const state0 = metaTagReducer(undefined, nullAction);
|
||||
expect(state0.tagsInUse).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return the current state on invalid action', () => {
|
||||
const state0 = {
|
||||
tagsInUse: ['foo', 'bar'],
|
||||
};
|
||||
|
||||
const state1 = metaTagReducer(state0, nullAction);
|
||||
expect(state1).toEqual(state0);
|
||||
});
|
||||
|
||||
it('should add tags on AddMetaTagAction', () => {
|
||||
const state0 = {
|
||||
tagsInUse: ['foo'],
|
||||
};
|
||||
|
||||
const state1 = metaTagReducer(state0, new AddMetaTagAction('bar'));
|
||||
const state2 = metaTagReducer(state1, new AddMetaTagAction('baz'));
|
||||
|
||||
expect(state1.tagsInUse).toEqual(['foo', 'bar']);
|
||||
expect(state2.tagsInUse).toEqual(['foo', 'bar', 'baz']);
|
||||
});
|
||||
|
||||
it('should clear tags on ClearMetaTagAction', () => {
|
||||
const state0 = {
|
||||
tagsInUse: ['foo', 'bar'],
|
||||
};
|
||||
|
||||
const state1 = metaTagReducer(state0, new ClearMetaTagAction());
|
||||
|
||||
expect(state1.tagsInUse).toEqual([]);
|
||||
});
|
||||
});
|
38
src/app/core/metadata/meta-tag.reducer.ts
Normal file
38
src/app/core/metadata/meta-tag.reducer.ts
Normal file
@@ -0,0 +1,38 @@
|
||||
import {
|
||||
MetaTagAction,
|
||||
MetaTagTypes,
|
||||
AddMetaTagAction,
|
||||
ClearMetaTagAction,
|
||||
} from './meta-tag.actions';
|
||||
|
||||
export interface MetaTagState {
|
||||
tagsInUse: string[];
|
||||
}
|
||||
|
||||
const initialstate: MetaTagState = {
|
||||
tagsInUse: []
|
||||
};
|
||||
|
||||
export const metaTagReducer = (state: MetaTagState = initialstate, action: MetaTagAction): MetaTagState => {
|
||||
switch (action.type) {
|
||||
case MetaTagTypes.ADD: {
|
||||
return addMetaTag(state, action as AddMetaTagAction);
|
||||
}
|
||||
case MetaTagTypes.CLEAR: {
|
||||
return clearMetaTags(state, action as ClearMetaTagAction);
|
||||
}
|
||||
default: {
|
||||
return state;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const addMetaTag = (state: MetaTagState, action: AddMetaTagAction): MetaTagState => {
|
||||
return {
|
||||
tagsInUse: [...state.tagsInUse, action.payload]
|
||||
};
|
||||
};
|
||||
|
||||
const clearMetaTags = (state: MetaTagState, action: ClearMetaTagAction): MetaTagState => {
|
||||
return Object.assign({}, initialstate);
|
||||
};
|
@@ -1,82 +1,28 @@
|
||||
import { CommonModule, Location } from '@angular/common';
|
||||
import { HttpClient } from '@angular/common/http';
|
||||
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
|
||||
import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||
import { Meta, MetaDefinition, Title } from '@angular/platform-browser';
|
||||
import { ActivatedRoute, Router } from '@angular/router';
|
||||
import { RouterTestingModule } from '@angular/router/testing';
|
||||
import { fakeAsync, tick } from '@angular/core/testing';
|
||||
import { Meta, Title } from '@angular/platform-browser';
|
||||
import { NavigationEnd, Router } from '@angular/router';
|
||||
|
||||
import { Store, StoreModule } from '@ngrx/store';
|
||||
|
||||
import { TranslateLoader, TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||
import { EmptyError, Observable, of } from 'rxjs';
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
import { Observable, of } from 'rxjs';
|
||||
|
||||
import { RemoteData } from '../data/remote-data';
|
||||
import { Item } from '../shared/item.model';
|
||||
|
||||
import {
|
||||
ItemMock,
|
||||
MockBitstream1,
|
||||
MockBitstream2,
|
||||
MockBitstreamFormat1,
|
||||
MockBitstreamFormat2
|
||||
} from '../../shared/mocks/item.mock';
|
||||
import { TranslateLoaderMock } from '../../shared/mocks/translate-loader.mock';
|
||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||
import { AuthService } from '../auth/auth.service';
|
||||
import { BrowseService } from '../browse/browse.service';
|
||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||
|
||||
import { CoreState } from '../core.reducers';
|
||||
import { BitstreamDataService } from '../data/bitstream-data.service';
|
||||
import { BitstreamFormatDataService } from '../data/bitstream-format-data.service';
|
||||
import { CommunityDataService } from '../data/community-data.service';
|
||||
import { DefaultChangeAnalyzer } from '../data/default-change-analyzer.service';
|
||||
import { DSOChangeAnalyzer } from '../data/dso-change-analyzer.service';
|
||||
|
||||
import { ItemDataService } from '../data/item-data.service';
|
||||
import { buildPaginatedList, PaginatedList } from '../data/paginated-list.model';
|
||||
import { FindListOptions } from '../data/request.models';
|
||||
import { RequestService } from '../data/request.service';
|
||||
import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||
import { ItemMock, MockBitstream1, MockBitstream3 } from '../../shared/mocks/item.mock';
|
||||
import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../shared/remote-data.utils';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
import { Bitstream } from '../shared/bitstream.model';
|
||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||
import { MetadataValue } from '../shared/metadata.models';
|
||||
import { PageInfo } from '../shared/page-info.model';
|
||||
import { UUIDService } from '../shared/uuid.service';
|
||||
|
||||
import { MetadataService } from './metadata.service';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { storeModuleConfig } from '../../app.reducer';
|
||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { RootDataService } from '../data/root-data.service';
|
||||
import { Root } from '../data/root.model';
|
||||
|
||||
/* tslint:disable:max-classes-per-file */
|
||||
@Component({
|
||||
template: `
|
||||
<router-outlet></router-outlet>`
|
||||
})
|
||||
class TestComponent {
|
||||
constructor(private metadata: MetadataService) {
|
||||
metadata.listenForRouteChange();
|
||||
}
|
||||
}
|
||||
|
||||
@Component({ template: '' })
|
||||
class DummyItemComponent {
|
||||
constructor(private route: ActivatedRoute, private items: ItemDataService, private metadata: MetadataService) {
|
||||
this.route.params.subscribe((params) => {
|
||||
this.metadata.processRemoteData(this.items.findById(params.id));
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/* tslint:enable:max-classes-per-file */
|
||||
import { Bundle } from '../shared/bundle.model';
|
||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||
import { DSONameService } from '../breadcrumbs/dso-name.service';
|
||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||
import { getMockStore } from '@ngrx/store/testing';
|
||||
import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions';
|
||||
|
||||
describe('MetadataService', () => {
|
||||
let metadataService: MetadataService;
|
||||
@@ -85,188 +31,339 @@ describe('MetadataService', () => {
|
||||
|
||||
let title: Title;
|
||||
|
||||
let store: Store<CoreState>;
|
||||
let dsoNameService: DSONameService;
|
||||
|
||||
let objectCacheService: ObjectCacheService;
|
||||
let requestService: RequestService;
|
||||
let uuidService: UUIDService;
|
||||
let remoteDataBuildService: RemoteDataBuildService;
|
||||
let itemDataService: ItemDataService;
|
||||
let authService: AuthService;
|
||||
let bundleDataService;
|
||||
let bitstreamDataService;
|
||||
let rootService: RootDataService;
|
||||
let translateService: TranslateService;
|
||||
let hardRedirectService: HardRedirectService;
|
||||
|
||||
let location: Location;
|
||||
let router: Router;
|
||||
let fixture: ComponentFixture<TestComponent>;
|
||||
let store;
|
||||
|
||||
const initialState = { 'core': { metaTag: { tagsInUse: ['title', 'description'] }}};
|
||||
|
||||
let tagStore: Map<string, MetaDefinition[]>;
|
||||
|
||||
beforeEach(() => {
|
||||
rootService = jasmine.createSpyObj({
|
||||
findRoot: createSuccessfulRemoteDataObject$({ dspaceVersion: 'mock-dspace-version' })
|
||||
});
|
||||
bitstreamDataService = jasmine.createSpyObj({
|
||||
findAllByHref: createSuccessfulRemoteDataObject$(createPaginatedList([MockBitstream3]))
|
||||
});
|
||||
bundleDataService = jasmine.createSpyObj({
|
||||
findByItemAndName: mockBundleRD$([MockBitstream3])
|
||||
});
|
||||
translateService = getMockTranslateService();
|
||||
meta = jasmine.createSpyObj('meta', {
|
||||
addTag: {},
|
||||
removeTag: {}
|
||||
});
|
||||
title = jasmine.createSpyObj({
|
||||
setTitle: {}
|
||||
});
|
||||
dsoNameService = jasmine.createSpyObj({
|
||||
getName: ItemMock.firstMetadataValue('dc.title')
|
||||
});
|
||||
router = {
|
||||
url: '/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357',
|
||||
events: of(new NavigationEnd(1, '', '')),
|
||||
routerState: {
|
||||
root: {}
|
||||
}
|
||||
} as any as Router;
|
||||
hardRedirectService = jasmine.createSpyObj( {
|
||||
getRequestOrigin: 'https://request.org',
|
||||
});
|
||||
|
||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||
// @ts-ignore
|
||||
store = getMockStore({ initialState });
|
||||
spyOn(store, 'dispatch');
|
||||
|
||||
objectCacheService = new ObjectCacheService(store, undefined);
|
||||
uuidService = new UUIDService();
|
||||
requestService = new RequestService(objectCacheService, uuidService, store, undefined);
|
||||
remoteDataBuildService = new RemoteDataBuildService(objectCacheService, undefined, requestService);
|
||||
const mockBitstreamDataService = {
|
||||
findAllByItemAndBundleName(item: Item, bundleName: string, options?: FindListOptions, ...linksToFollow: FollowLinkConfig<Bitstream>[]): Observable<RemoteData<PaginatedList<Bitstream>>> {
|
||||
if (item.equals(ItemMock)) {
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), [MockBitstream1, MockBitstream2]));
|
||||
} else {
|
||||
return createSuccessfulRemoteDataObject$(buildPaginatedList(new PageInfo(), []));
|
||||
}
|
||||
},
|
||||
};
|
||||
const mockBitstreamFormatDataService = {
|
||||
findByBitstream(bitstream: Bitstream): Observable<RemoteData<BitstreamFormat>> {
|
||||
switch (bitstream) {
|
||||
case MockBitstream1:
|
||||
return createSuccessfulRemoteDataObject$(MockBitstreamFormat1);
|
||||
break;
|
||||
case MockBitstream2:
|
||||
return createSuccessfulRemoteDataObject$(MockBitstreamFormat2);
|
||||
break;
|
||||
default:
|
||||
return createSuccessfulRemoteDataObject$(new BitstreamFormat());
|
||||
}
|
||||
}
|
||||
};
|
||||
rootService = jasmine.createSpyObj('rootService', {
|
||||
findRoot: createSuccessfulRemoteDataObject$(Object.assign(new Root(), {
|
||||
dspaceVersion: 'mock-dspace-version'
|
||||
}))
|
||||
});
|
||||
|
||||
TestBed.configureTestingModule({
|
||||
imports: [
|
||||
CommonModule,
|
||||
StoreModule.forRoot({}, storeModuleConfig),
|
||||
TranslateModule.forRoot({
|
||||
loader: {
|
||||
provide: TranslateLoader,
|
||||
useClass: TranslateLoaderMock
|
||||
}
|
||||
}),
|
||||
RouterTestingModule.withRoutes([
|
||||
{ path: 'items/:id', component: DummyItemComponent, pathMatch: 'full' },
|
||||
{
|
||||
path: 'other',
|
||||
component: DummyItemComponent,
|
||||
pathMatch: 'full',
|
||||
data: { title: 'Dummy Title', description: 'This is a dummy item component for testing!' }
|
||||
}
|
||||
])
|
||||
],
|
||||
declarations: [
|
||||
TestComponent,
|
||||
DummyItemComponent
|
||||
],
|
||||
providers: [
|
||||
{ provide: ObjectCacheService, useValue: objectCacheService },
|
||||
{ provide: RequestService, useValue: requestService },
|
||||
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
|
||||
{ provide: HALEndpointService, useValue: {} },
|
||||
{ provide: AuthService, useValue: {} },
|
||||
{ provide: NotificationsService, useValue: {} },
|
||||
{ provide: HttpClient, useValue: {} },
|
||||
{ provide: DSOChangeAnalyzer, useValue: {} },
|
||||
{ provide: CommunityDataService, useValue: {} },
|
||||
{ provide: DefaultChangeAnalyzer, useValue: {} },
|
||||
{ provide: BitstreamFormatDataService, useValue: mockBitstreamFormatDataService },
|
||||
{ provide: BitstreamDataService, useValue: mockBitstreamDataService },
|
||||
{ provide: RootDataService, useValue: rootService },
|
||||
Meta,
|
||||
Title,
|
||||
// tslint:disable-next-line:no-empty
|
||||
{ provide: ItemDataService, useValue: { findById: () => {} } },
|
||||
BrowseService,
|
||||
MetadataService
|
||||
],
|
||||
schemas: [CUSTOM_ELEMENTS_SCHEMA]
|
||||
});
|
||||
meta = TestBed.inject(Meta);
|
||||
title = TestBed.inject(Title);
|
||||
itemDataService = TestBed.inject(ItemDataService);
|
||||
metadataService = TestBed.inject(MetadataService);
|
||||
authService = TestBed.inject(AuthService);
|
||||
translateService = TestBed.inject(TranslateService);
|
||||
|
||||
router = TestBed.inject(Router);
|
||||
location = TestBed.inject(Location);
|
||||
|
||||
fixture = TestBed.createComponent(TestComponent);
|
||||
|
||||
tagStore = metadataService.getTagStore();
|
||||
metadataService = new MetadataService(
|
||||
router,
|
||||
translateService,
|
||||
meta,
|
||||
title,
|
||||
dsoNameService,
|
||||
bundleDataService,
|
||||
bitstreamDataService,
|
||||
undefined,
|
||||
rootService,
|
||||
store,
|
||||
hardRedirectService
|
||||
);
|
||||
});
|
||||
|
||||
it('items page should set meta tags', fakeAsync(() => {
|
||||
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock));
|
||||
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
expect(title.getTitle()).toEqual('Test PowerPoint Document');
|
||||
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-26');
|
||||
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');
|
||||
expect(title.setTitle).toHaveBeenCalledWith('Test PowerPoint Document');
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'citation_title',
|
||||
content: 'Test PowerPoint Document'
|
||||
});
|
||||
expect(meta.addTag).toHaveBeenCalledWith({ property: 'citation_author', content: 'Doe, Jane' });
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'citation_publication_date',
|
||||
content: '1650-06-26'
|
||||
});
|
||||
expect(meta.addTag).toHaveBeenCalledWith({ property: 'citation_issn', content: '123456789' });
|
||||
expect(meta.addTag).toHaveBeenCalledWith({ property: 'citation_language', content: 'en' });
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'citation_keywords',
|
||||
content: 'keyword1; keyword2; keyword3'
|
||||
});
|
||||
}));
|
||||
|
||||
it('items page should set meta tags as published Thesis', fakeAsync(() => {
|
||||
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(ItemMock, 'Thesis'))));
|
||||
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))),
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
expect(tagStore.get('citation_dissertation_name')[0].content).toEqual('Test PowerPoint Document');
|
||||
expect(tagStore.get('citation_dissertation_institution')[0].content).toEqual('Mock Publisher');
|
||||
expect(tagStore.get('citation_abstract_html_url')[0].content).toEqual([environment.ui.baseUrl, router.url].join(''));
|
||||
expect(tagStore.get('citation_pdf_url')[0].content).toEqual('/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/download');
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'citation_dissertation_name',
|
||||
content: 'Test PowerPoint Document'
|
||||
});
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download'
|
||||
});
|
||||
}));
|
||||
|
||||
it('items page should set meta tags as published Technical Report', fakeAsync(() => {
|
||||
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(ItemMock, 'Technical Report'))));
|
||||
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))),
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
expect(tagStore.get('citation_technical_report_institution')[0].content).toEqual('Mock Publisher');
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'citation_technical_report_institution',
|
||||
content: 'Mock Publisher'
|
||||
});
|
||||
}));
|
||||
|
||||
it('other navigation should add title, description and Generator', fakeAsync(() => {
|
||||
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock));
|
||||
spyOn(translateService, 'get').and.returnValues(of('DSpace :: '), of('Dummy Title'), of('This is a dummy item component for testing!'));
|
||||
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
|
||||
it('other navigation should add title and description', fakeAsync(() => {
|
||||
(translateService.get as jasmine.Spy).and.returnValues(of('DSpace :: '), of('Dummy Title'), of('This is a dummy item component for testing!'));
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
title: 'Dummy Title',
|
||||
description: 'This is a dummy item component for testing!'
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
expect(tagStore.size).toBeGreaterThan(0);
|
||||
router.navigate(['/other']);
|
||||
tick();
|
||||
expect(tagStore.size).toEqual(3);
|
||||
expect(title.getTitle()).toEqual('DSpace :: Dummy Title');
|
||||
expect(tagStore.get('title')[0].content).toEqual('DSpace :: Dummy Title');
|
||||
expect(tagStore.get('description')[0].content).toEqual('This is a dummy item component for testing!');
|
||||
expect(tagStore.get('Generator')[0].content).toEqual('mock-dspace-version');
|
||||
expect(title.setTitle).toHaveBeenCalledWith('DSpace :: Dummy Title');
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'title',
|
||||
content: 'DSpace :: Dummy Title'
|
||||
});
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'description',
|
||||
content: 'This is a dummy item component for testing!'
|
||||
});
|
||||
}));
|
||||
|
||||
describe('when the item has no bitstreams', () => {
|
||||
|
||||
beforeEach(() => {
|
||||
// this.bitstreamDataService.findAllByItemAndBundleName(this.item, 'ORIGINAL')
|
||||
// spyOn(MockItem, 'getFiles').and.returnValue(observableOf([]));
|
||||
describe(`listenForRouteChange`, () => {
|
||||
it(`should call processRouteChange`, fakeAsync(() => {
|
||||
spyOn(metadataService as any, 'processRouteChange').and.callFake(() => undefined);
|
||||
metadataService.listenForRouteChange();
|
||||
tick();
|
||||
expect((metadataService as any).processRouteChange).toHaveBeenCalled();
|
||||
}));
|
||||
it(`should add Generator`, fakeAsync(() => {
|
||||
spyOn(metadataService as any, 'processRouteChange').and.callFake(() => undefined);
|
||||
metadataService.listenForRouteChange();
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'Generator',
|
||||
content: 'mock-dspace-version'
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
it('processRemoteData should not produce an EmptyError', fakeAsync(() => {
|
||||
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(ItemMock));
|
||||
spyOn(metadataService, 'processRemoteData').and.callThrough();
|
||||
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
|
||||
describe('citation_abstract_html_url', () => {
|
||||
it('should use dc.identifier.uri if available', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockUri(ItemMock, 'https://ddg.gg')),
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
expect(metadataService.processRemoteData).not.toThrow(new EmptyError());
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'citation_abstract_html_url',
|
||||
content: 'https://ddg.gg'
|
||||
});
|
||||
}));
|
||||
|
||||
it('should use current route as fallback', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockUri(ItemMock)),
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'citation_abstract_html_url',
|
||||
content: 'https://request.org/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357'
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
const mockRemoteData = (mockItem: Item): Observable<RemoteData<Item>> => {
|
||||
return createSuccessfulRemoteDataObject$(ItemMock);
|
||||
};
|
||||
describe('citation_*_institution / citation_publisher', () => {
|
||||
it('should use citation_dissertation_institution tag for dissertations', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Thesis'))),
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'citation_dissertation_institution',
|
||||
content: 'Mock Publisher'
|
||||
});
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_technical_report_institution' }));
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_publisher' }));
|
||||
}));
|
||||
|
||||
it('should use citation_tech_report_institution tag for tech reports', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Technical Report'))),
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_dissertation_institution' }));
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'citation_technical_report_institution',
|
||||
content: 'Mock Publisher'
|
||||
});
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_publisher' }));
|
||||
}));
|
||||
|
||||
it('should use citation_publisher for other item types', fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(mockPublisher(mockType(ItemMock, 'Some Other Type'))),
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_dissertation_institution' }));
|
||||
expect(meta.addTag).not.toHaveBeenCalledWith(jasmine.objectContaining({ property: 'citation_technical_report_institution' }));
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'citation_publisher',
|
||||
content: 'Mock Publisher'
|
||||
});
|
||||
}));
|
||||
});
|
||||
|
||||
describe('citation_pdf_url', () => {
|
||||
it('should link to primary Bitstream URL regardless of format', fakeAsync(() => {
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([], MockBitstream3));
|
||||
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download'
|
||||
});
|
||||
}));
|
||||
|
||||
describe('no primary Bitstream', () => {
|
||||
it('should link to first and only Bitstream regardless of format', fakeAsync(() => {
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$([MockBitstream3]));
|
||||
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/download'
|
||||
});
|
||||
}));
|
||||
|
||||
it('should link to first Bitstream with allowed format', fakeAsync(() => {
|
||||
const bitstreams = [MockBitstream3, MockBitstream3, MockBitstream1];
|
||||
(bundleDataService.findByItemAndName as jasmine.Spy).and.returnValue(mockBundleRD$(bitstreams));
|
||||
(bitstreamDataService.findAllByHref as jasmine.Spy).and.returnValues(
|
||||
...mockBitstreamPages$(bitstreams).map(bp => createSuccessfulRemoteDataObject$(bp)),
|
||||
);
|
||||
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
expect(meta.addTag).toHaveBeenCalledWith({
|
||||
property: 'citation_pdf_url',
|
||||
content: 'https://request.org/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/download'
|
||||
});
|
||||
}));
|
||||
});
|
||||
});
|
||||
|
||||
describe('tagstore', () => {
|
||||
beforeEach(fakeAsync(() => {
|
||||
(metadataService as any).processRouteChange({
|
||||
data: {
|
||||
value: {
|
||||
dso: createSuccessfulRemoteDataObject(ItemMock),
|
||||
}
|
||||
}
|
||||
});
|
||||
tick();
|
||||
}));
|
||||
|
||||
it('should remove previous tags on route change', fakeAsync(() => {
|
||||
expect(meta.removeTag).toHaveBeenCalledWith('property=\'title\'');
|
||||
expect(meta.removeTag).toHaveBeenCalledWith('property=\'description\'');
|
||||
}));
|
||||
|
||||
it('should clear all tags and add new ones on route change', () => {
|
||||
expect(store.dispatch.calls.argsFor(0)).toEqual([new ClearMetaTagAction()]);
|
||||
expect(store.dispatch.calls.argsFor(1)).toEqual([new AddMetaTagAction('title')]);
|
||||
expect(store.dispatch.calls.argsFor(2)).toEqual([new AddMetaTagAction('description')]);
|
||||
});
|
||||
});
|
||||
|
||||
const mockType = (mockItem: Item, type: string): Item => {
|
||||
const typedMockItem = Object.assign(new Item(), mockItem) as Item;
|
||||
@@ -285,4 +382,30 @@ describe('MetadataService', () => {
|
||||
return publishedMockItem;
|
||||
};
|
||||
|
||||
const mockUri = (mockItem: Item, uri?: string): Item => {
|
||||
const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
|
||||
publishedMockItem.metadata['dc.identifier.uri'] = [{ value: uri }] as MetadataValue[];
|
||||
return publishedMockItem;
|
||||
};
|
||||
|
||||
const mockBundleRD$ = (bitstreams: Bitstream[], primary?: Bitstream): Observable<RemoteData<Bundle>> => {
|
||||
return createSuccessfulRemoteDataObject$(
|
||||
Object.assign(new Bundle(), {
|
||||
name: 'ORIGINAL',
|
||||
bitstreams: createSuccessfulRemoteDataObject$(mockBitstreamPages$(bitstreams)[0]),
|
||||
primaryBitstream: createSuccessfulRemoteDataObject$(primary),
|
||||
})
|
||||
);
|
||||
};
|
||||
|
||||
const mockBitstreamPages$ = (bitstreams: Bitstream[]): PaginatedList<Bitstream>[] => {
|
||||
return bitstreams.map((bitstream, index) => Object.assign(createPaginatedList([bitstream]), {
|
||||
pageInfo: {
|
||||
totalElements: bitstreams.length, // announce multiple elements/pages
|
||||
},
|
||||
_links: index < bitstreams.length - 1
|
||||
? { next: { href: 'not empty' }} // fake link to the next bitstream page
|
||||
: { next: { href: undefined }}, // last page has no link
|
||||
}));
|
||||
};
|
||||
});
|
||||
|
@@ -5,12 +5,11 @@ import { ActivatedRoute, NavigationEnd, Router } from '@angular/router';
|
||||
|
||||
import { TranslateService } from '@ngx-translate/core';
|
||||
|
||||
import { BehaviorSubject, combineLatest, Observable } from 'rxjs';
|
||||
import { catchError, distinctUntilKeyChanged, filter, first, map, take } from 'rxjs/operators';
|
||||
import { BehaviorSubject, combineLatest, EMPTY, Observable, of as observableOf } from 'rxjs';
|
||||
import { expand, filter, map, switchMap, take } from 'rxjs/operators';
|
||||
|
||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||
import { hasNoValue, hasValue } from '../../shared/empty.util';
|
||||
import { DSONameService } from '../breadcrumbs/dso-name.service';
|
||||
import { CacheableObject } from '../cache/object-cache.reducer';
|
||||
import { BitstreamDataService } from '../data/bitstream-data.service';
|
||||
import { BitstreamFormatDataService } from '../data/bitstream-format-data.service';
|
||||
|
||||
@@ -19,22 +18,57 @@ import { BitstreamFormat } from '../shared/bitstream-format.model';
|
||||
import { Bitstream } from '../shared/bitstream.model';
|
||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||
import { Item } from '../shared/item.model';
|
||||
import {
|
||||
getFirstSucceededRemoteDataPayload,
|
||||
getFirstSucceededRemoteListPayload
|
||||
} from '../shared/operators';
|
||||
import { environment } from '../../../environments/environment';
|
||||
import { getFirstCompletedRemoteData, getFirstSucceededRemoteDataPayload } from '../shared/operators';
|
||||
import { RootDataService } from '../data/root-data.service';
|
||||
import { getBitstreamDownloadRoute } from '../../app-routing-paths';
|
||||
import { BundleDataService } from '../data/bundle-data.service';
|
||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||
import { Bundle } from '../shared/bundle.model';
|
||||
import { PaginatedList } from '../data/paginated-list.model';
|
||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||
import { HardRedirectService } from '../services/hard-redirect.service';
|
||||
import { MetaTagState } from './meta-tag.reducer';
|
||||
import { createSelector, select, Store } from '@ngrx/store';
|
||||
import { AddMetaTagAction, ClearMetaTagAction } from './meta-tag.actions';
|
||||
import { coreSelector } from '../core.selectors';
|
||||
import { CoreState } from '../core.reducers';
|
||||
|
||||
/**
|
||||
* The base selector function to select the metaTag section in the store
|
||||
*/
|
||||
const metaTagSelector = createSelector(
|
||||
coreSelector,
|
||||
(state: CoreState) => state.metaTag
|
||||
);
|
||||
|
||||
/**
|
||||
* Selector function to select the tags in use from the MetaTagState
|
||||
*/
|
||||
const tagsInUseSelector =
|
||||
createSelector(
|
||||
metaTagSelector,
|
||||
(state: MetaTagState) => state.tagsInUse,
|
||||
);
|
||||
|
||||
@Injectable()
|
||||
export class MetadataService {
|
||||
|
||||
private initialized: boolean;
|
||||
private currentObject: BehaviorSubject<DSpaceObject> = new BehaviorSubject<DSpaceObject>(undefined);
|
||||
|
||||
private tagStore: Map<string, MetaDefinition[]>;
|
||||
|
||||
private currentObject: BehaviorSubject<DSpaceObject>;
|
||||
/**
|
||||
* When generating the citation_pdf_url meta tag for Items with more than one Bitstream (and no primary Bitstream),
|
||||
* the first Bitstream to match one of the following MIME types is selected.
|
||||
* See {@linkcode getFirstAllowedFormatBitstreamLink}
|
||||
* @private
|
||||
*/
|
||||
private readonly CITATION_PDF_URL_MIMETYPES = [
|
||||
'application/pdf', // .pdf
|
||||
'application/postscript', // .ps
|
||||
'application/msword', // .doc
|
||||
'application/vnd.openxmlformats-officedocument.wordprocessingml.document', // .docx
|
||||
'application/rtf', // .rtf
|
||||
'application/epub+zip', // .epub
|
||||
];
|
||||
|
||||
constructor(
|
||||
private router: Router,
|
||||
@@ -42,21 +76,19 @@ export class MetadataService {
|
||||
private meta: Meta,
|
||||
private title: Title,
|
||||
private dsoNameService: DSONameService,
|
||||
private bundleDataService: BundleDataService,
|
||||
private bitstreamDataService: BitstreamDataService,
|
||||
private bitstreamFormatDataService: BitstreamFormatDataService,
|
||||
private rootService: RootDataService
|
||||
private rootService: RootDataService,
|
||||
private store: Store<CoreState>,
|
||||
private hardRedirectService: HardRedirectService,
|
||||
) {
|
||||
// TODO: determine what open graph meta tags are needed and whether
|
||||
// the differ per route. potentially add image based on DSpaceObject
|
||||
this.meta.addTags([
|
||||
{ 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<string, MetaDefinition[]>();
|
||||
}
|
||||
|
||||
public listenForRouteChange(): void {
|
||||
// This never changes, set it only once
|
||||
this.setGenerator();
|
||||
|
||||
this.router.events.pipe(
|
||||
filter((event) => event instanceof NavigationEnd),
|
||||
map(() => this.router.routerState.root),
|
||||
@@ -68,22 +100,9 @@ export class MetadataService {
|
||||
});
|
||||
}
|
||||
|
||||
public processRemoteData(remoteData: Observable<RemoteData<CacheableObject>>): void {
|
||||
remoteData.pipe(map((rd: RemoteData<CacheableObject>) => rd.payload),
|
||||
filter((co: CacheableObject) => hasValue(co)),
|
||||
take(1))
|
||||
.subscribe((dspaceObject: DSpaceObject) => {
|
||||
if (!this.initialized) {
|
||||
this.initialize(dspaceObject);
|
||||
}
|
||||
this.currentObject.next(dspaceObject);
|
||||
});
|
||||
}
|
||||
|
||||
private processRouteChange(routeInfo: any): void {
|
||||
if (routeInfo.params.value.id === undefined) {
|
||||
this.clearMetaTags();
|
||||
}
|
||||
|
||||
if (routeInfo.data.value.title) {
|
||||
const titlePrefix = this.translate.get('repository.title.prefix');
|
||||
const title = this.translate.get(routeInfo.data.value.title, routeInfo.data.value);
|
||||
@@ -98,15 +117,10 @@ export class MetadataService {
|
||||
});
|
||||
}
|
||||
|
||||
this.setGenerator();
|
||||
if (hasValue(routeInfo.data.value.dso) && hasValue(routeInfo.data.value.dso.payload)) {
|
||||
this.currentObject.next(routeInfo.data.value.dso.payload);
|
||||
this.setDSOMetaTags();
|
||||
}
|
||||
|
||||
private initialize(dspaceObject: DSpaceObject): void {
|
||||
this.currentObject = new BehaviorSubject<DSpaceObject>(dspaceObject);
|
||||
this.currentObject.asObservable().pipe(distinctUntilKeyChanged('uuid')).subscribe(() => {
|
||||
this.setMetaTags();
|
||||
});
|
||||
this.initialized = true;
|
||||
}
|
||||
|
||||
private getCurrentRoute(route: ActivatedRoute): ActivatedRoute {
|
||||
@@ -116,16 +130,14 @@ export class MetadataService {
|
||||
return route;
|
||||
}
|
||||
|
||||
private setMetaTags(): void {
|
||||
|
||||
this.clearMetaTags();
|
||||
private setDSOMetaTags(): void {
|
||||
|
||||
this.setTitleTag();
|
||||
this.setDescriptionTag();
|
||||
|
||||
this.setCitationTitleTag();
|
||||
this.setCitationAuthorTags();
|
||||
this.setCitationDateTag();
|
||||
this.setCitationPublicationDateTag();
|
||||
this.setCitationISSNTag();
|
||||
this.setCitationISBNTag();
|
||||
|
||||
@@ -134,14 +146,10 @@ export class MetadataService {
|
||||
|
||||
this.setCitationAbstractUrlTag();
|
||||
this.setCitationPdfUrlTag();
|
||||
this.setCitationPublisherTag();
|
||||
|
||||
if (this.isDissertation()) {
|
||||
this.setCitationDissertationNameTag();
|
||||
this.setCitationDissertationInstitutionTag();
|
||||
}
|
||||
|
||||
if (this.isTechReport()) {
|
||||
this.setCitationTechReportInstitutionTag();
|
||||
}
|
||||
|
||||
// this.setCitationJournalTitleTag();
|
||||
@@ -176,7 +184,7 @@ export class MetadataService {
|
||||
private setDescriptionTag(): void {
|
||||
// TODO: truncate abstract
|
||||
const value = this.getMetaTagValue('dc.description.abstract');
|
||||
this.addMetaTag('desciption', value);
|
||||
this.addMetaTag('description', value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -196,11 +204,11 @@ export class MetadataService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_date" ... > to the <head>
|
||||
* Add <meta name="citation_publication_date" ... > to the <head>
|
||||
*/
|
||||
private setCitationDateTag(): void {
|
||||
private setCitationPublicationDateTag(): void {
|
||||
const value = this.getFirstMetaTagValue(['dc.date.copyright', 'dc.date.issued', 'dc.date.available', 'dc.date.accessioned']);
|
||||
this.addMetaTag('citation_date', value);
|
||||
this.addMetaTag('citation_publication_date', value);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -236,19 +244,17 @@ export class MetadataService {
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_dissertation_institution" ... > to the <head>
|
||||
* Add dc.publisher to the <head>. The tag name depends on the item type.
|
||||
*/
|
||||
private setCitationDissertationInstitutionTag(): void {
|
||||
private setCitationPublisherTag(): void {
|
||||
const value = this.getMetaTagValue('dc.publisher');
|
||||
if (this.isDissertation()) {
|
||||
this.addMetaTag('citation_dissertation_institution', value);
|
||||
}
|
||||
|
||||
/**
|
||||
* Add <meta name="citation_technical_report_institution" ... > to the <head>
|
||||
*/
|
||||
private setCitationTechReportInstitutionTag(): void {
|
||||
const value = this.getMetaTagValue('dc.publisher');
|
||||
} else if (this.isTechReport()) {
|
||||
this.addMetaTag('citation_technical_report_institution', value);
|
||||
} else {
|
||||
this.addMetaTag('citation_publisher', value);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -264,8 +270,11 @@ export class MetadataService {
|
||||
*/
|
||||
private setCitationAbstractUrlTag(): void {
|
||||
if (this.currentObject.value instanceof Item) {
|
||||
const value = [environment.ui.baseUrl, this.router.url].join('');
|
||||
this.addMetaTag('citation_abstract_html_url', value);
|
||||
let url = this.getMetaTagValue('dc.identifier.uri');
|
||||
if (hasNoValue(url)) {
|
||||
url = new URLCombiner(this.hardRedirectService.getRequestOrigin(), this.router.url).toString();
|
||||
}
|
||||
this.addMetaTag('citation_abstract_html_url', url);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -275,27 +284,118 @@ export class MetadataService {
|
||||
private setCitationPdfUrlTag(): void {
|
||||
if (this.currentObject.value instanceof Item) {
|
||||
const item = this.currentObject.value as Item;
|
||||
this.bitstreamDataService.findAllByItemAndBundleName(item, 'ORIGINAL')
|
||||
.pipe(
|
||||
getFirstSucceededRemoteListPayload(),
|
||||
first((files) => isNotEmpty(files)),
|
||||
catchError((error) => {
|
||||
console.debug(error.message);
|
||||
return [];
|
||||
}))
|
||||
.subscribe((bitstreams: Bitstream[]) => {
|
||||
for (const bitstream of bitstreams) {
|
||||
this.bitstreamFormatDataService.findByBitstream(bitstream).pipe(
|
||||
getFirstSucceededRemoteDataPayload()
|
||||
).subscribe((format: BitstreamFormat) => {
|
||||
if (format.mimetype === 'application/pdf') {
|
||||
const bitstreamLink = getBitstreamDownloadRoute(bitstream);
|
||||
this.addMetaTag('citation_pdf_url', bitstreamLink);
|
||||
|
||||
// Retrieve the ORIGINAL bundle for the item
|
||||
this.bundleDataService.findByItemAndName(
|
||||
item,
|
||||
'ORIGINAL',
|
||||
true,
|
||||
true,
|
||||
followLink('primaryBitstream'),
|
||||
followLink('bitstreams', {}, followLink('format')),
|
||||
).pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
switchMap((bundle: Bundle) =>
|
||||
|
||||
// First try the primary bitstream
|
||||
bundle.primaryBitstream.pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((rd: RemoteData<Bitstream>) => {
|
||||
if (hasValue(rd.payload)) {
|
||||
return rd.payload;
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}),
|
||||
// return the bundle as well so we can use it again if there's no primary bitstream
|
||||
map((bitstream: Bitstream) => [bundle, bitstream])
|
||||
)
|
||||
),
|
||||
switchMap(([bundle, primaryBitstream]: [Bundle, Bitstream]) => {
|
||||
if (hasValue(primaryBitstream)) {
|
||||
// If there was a primary bitstream, emit its link
|
||||
return [getBitstreamDownloadRoute(primaryBitstream)];
|
||||
} else {
|
||||
// Otherwise consider the regular bitstreams in the bundle
|
||||
return bundle.bitstreams.pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
switchMap((bitstreamRd: RemoteData<PaginatedList<Bitstream>>) => {
|
||||
if (hasValue(bitstreamRd.payload) && bitstreamRd.payload.totalElements === 1) {
|
||||
// If there's only one bitstream in the bundle, emit its link
|
||||
return [getBitstreamDownloadRoute(bitstreamRd.payload.page[0])];
|
||||
} else {
|
||||
// Otherwise check all bitstreams to see if one matches the format whitelist
|
||||
return this.getFirstAllowedFormatBitstreamLink(bitstreamRd);
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}),
|
||||
take(1)
|
||||
).subscribe((link: string) => {
|
||||
// Use the found link to set the <meta> tag
|
||||
this.addMetaTag(
|
||||
'citation_pdf_url',
|
||||
new URLCombiner(this.hardRedirectService.getRequestOrigin(), link).toString()
|
||||
);
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* For Items with more than one Bitstream (and no primary Bitstream), link to the first Bitstream with a MIME type
|
||||
* included in {@linkcode CITATION_PDF_URL_MIMETYPES}
|
||||
* @param bitstreamRd
|
||||
* @private
|
||||
*/
|
||||
private getFirstAllowedFormatBitstreamLink(bitstreamRd: RemoteData<PaginatedList<Bitstream>>): Observable<string> {
|
||||
return observableOf(bitstreamRd.payload).pipe(
|
||||
// Because there can be more than one page of bitstreams, this expand operator
|
||||
// will retrieve them in turn. Due to the take(1) at the bottom, it will only
|
||||
// retrieve pages until a match is found
|
||||
expand((paginatedList: PaginatedList<Bitstream>) => {
|
||||
if (hasNoValue(paginatedList.next)) {
|
||||
// If there's no next page, stop.
|
||||
return EMPTY;
|
||||
} else {
|
||||
// Otherwise retrieve the next page
|
||||
return this.bitstreamDataService.findAllByHref(
|
||||
paginatedList.next,
|
||||
undefined,
|
||||
true,
|
||||
true,
|
||||
followLink('format')
|
||||
).pipe(
|
||||
getFirstCompletedRemoteData(),
|
||||
map((next: RemoteData<PaginatedList<Bitstream>>) => {
|
||||
if (hasValue(next.payload)) {
|
||||
return next.payload;
|
||||
} else {
|
||||
return EMPTY;
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
}),
|
||||
// Return the array of bitstreams inside each paginated list
|
||||
map((paginatedList: PaginatedList<Bitstream>) => paginatedList.page),
|
||||
// Emit the bitstreams in the list one at a time
|
||||
switchMap((bitstreams: Bitstream[]) => bitstreams),
|
||||
// Retrieve the format for each bitstream
|
||||
switchMap((bitstream: Bitstream) => bitstream.format.pipe(
|
||||
getFirstSucceededRemoteDataPayload(),
|
||||
// Keep the original bitstream, because it, not the format, is what we'll need
|
||||
// for the link at the end
|
||||
map((format: BitstreamFormat) => [bitstream, format])
|
||||
)),
|
||||
// Filter out only pairs with whitelisted formats
|
||||
filter(([, format]: [Bitstream, BitstreamFormat]) =>
|
||||
hasValue(format) && this.CITATION_PDF_URL_MIMETYPES.includes(format.mimetype)),
|
||||
// We only need 1
|
||||
take(1),
|
||||
// Emit the link of the match
|
||||
map(([bitstream, ]: [Bitstream, BitstreamFormat]) => getBitstreamDownloadRoute(bitstream))
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -303,7 +403,7 @@ export class MetadataService {
|
||||
*/
|
||||
private setGenerator(): void {
|
||||
this.rootService.findRoot().pipe(getFirstSucceededRemoteDataPayload()).subscribe((root) => {
|
||||
this.addMetaTag('Generator', root.dspaceVersion);
|
||||
this.meta.addTag({ property: 'Generator', content: root.dspaceVersion });
|
||||
});
|
||||
}
|
||||
|
||||
@@ -351,7 +451,7 @@ export class MetadataService {
|
||||
if (content) {
|
||||
const tag = { property, content } as MetaDefinition;
|
||||
this.meta.addTag(tag);
|
||||
this.storeTag(property, tag);
|
||||
this.storeTag(property);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -361,33 +461,21 @@ export class MetadataService {
|
||||
}
|
||||
}
|
||||
|
||||
private storeTag(key: string, tag: MetaDefinition): void {
|
||||
const tags: MetaDefinition[] = this.getTags(key);
|
||||
tags.push(tag);
|
||||
this.setTags(key, tags);
|
||||
}
|
||||
|
||||
private getTags(key: string): MetaDefinition[] {
|
||||
let tags: MetaDefinition[] = this.tagStore.get(key);
|
||||
if (tags === undefined) {
|
||||
tags = [];
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
private setTags(key: string, tags: MetaDefinition[]): void {
|
||||
this.tagStore.set(key, tags);
|
||||
private storeTag(key: string): void {
|
||||
this.store.dispatch(new AddMetaTagAction(key));
|
||||
}
|
||||
|
||||
public clearMetaTags() {
|
||||
this.tagStore.forEach((tags: MetaDefinition[], property: string) => {
|
||||
this.store.pipe(
|
||||
select(tagsInUseSelector),
|
||||
take(1)
|
||||
).subscribe((tagsInUse: string[]) => {
|
||||
for (const property of tagsInUse) {
|
||||
this.meta.removeTag('property=\'' + property + '\'');
|
||||
}
|
||||
this.store.dispatch(new ClearMetaTagAction());
|
||||
});
|
||||
this.tagStore.clear();
|
||||
}
|
||||
|
||||
public getTagStore(): Map<string, MetaDefinition[]> {
|
||||
return this.tagStore;
|
||||
}
|
||||
|
||||
}
|
||||
|
@@ -5,6 +5,7 @@ import { Bitstream } from '../../core/shared/bitstream.model';
|
||||
import { Item } from '../../core/shared/item.model';
|
||||
import { createSuccessfulRemoteDataObject$ } from '../remote-data.utils';
|
||||
import { createPaginatedList } from '../testing/utils.test';
|
||||
import { Bundle } from '../../core/shared/bundle.model';
|
||||
|
||||
export const MockBitstreamFormat1: BitstreamFormat = Object.assign(new BitstreamFormat(), {
|
||||
shortDescription: 'Microsoft Word XML',
|
||||
@@ -34,11 +35,25 @@ export const MockBitstreamFormat2: BitstreamFormat = Object.assign(new Bitstream
|
||||
}
|
||||
});
|
||||
|
||||
export const MockBitstreamFormat3: BitstreamFormat = Object.assign(new BitstreamFormat(), {
|
||||
shortDescription: 'Binary',
|
||||
description: 'Some scary unknown binary file',
|
||||
mimetype: 'application/octet-stream',
|
||||
supportLevel: 0,
|
||||
internal: false,
|
||||
extensions: null,
|
||||
_links:{
|
||||
self: {
|
||||
href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/17'
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
export const MockBitstream1: Bitstream = Object.assign(new Bitstream(),
|
||||
{
|
||||
sizeBytes: 10201,
|
||||
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/cf9b0c8e-a1eb-4b65-afd0-567366448713/content',
|
||||
format: observableOf(MockBitstreamFormat1),
|
||||
format: createSuccessfulRemoteDataObject$(MockBitstreamFormat1),
|
||||
bundleName: 'ORIGINAL',
|
||||
_links:{
|
||||
self: {
|
||||
@@ -61,7 +76,7 @@ export const MockBitstream1: Bitstream = Object.assign(new Bitstream(),
|
||||
export const MockBitstream2: Bitstream = Object.assign(new Bitstream(), {
|
||||
sizeBytes: 31302,
|
||||
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/99b00f3c-1cc6-4689-8158-91965bee6b28/content',
|
||||
format: observableOf(MockBitstreamFormat2),
|
||||
format: createSuccessfulRemoteDataObject$(MockBitstreamFormat2),
|
||||
bundleName: 'ORIGINAL',
|
||||
id: '99b00f3c-1cc6-4689-8158-91965bee6b28',
|
||||
uuid: '99b00f3c-1cc6-4689-8158-91965bee6b28',
|
||||
@@ -82,16 +97,33 @@ export const MockBitstream2: Bitstream = Object.assign(new Bitstream(), {
|
||||
}
|
||||
});
|
||||
|
||||
/* tslint:disable:no-shadowed-variable */
|
||||
export const ItemMock: Item = Object.assign(new Item(), {
|
||||
handle: '10673/6',
|
||||
lastModified: '2017-04-24T19:44:08.178+0000',
|
||||
isArchived: true,
|
||||
isDiscoverable: true,
|
||||
isWithdrawn: false,
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([
|
||||
export const MockBitstream3: Bitstream = Object.assign(new Bitstream(), {
|
||||
sizeBytes: 4975123,
|
||||
content: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/content',
|
||||
format: createSuccessfulRemoteDataObject$(MockBitstreamFormat3),
|
||||
bundleName: 'ORIGINAL',
|
||||
id: '4db100c1-e1f5-4055-9404-9bc3e2d15f29',
|
||||
uuid: '4db100c1-e1f5-4055-9404-9bc3e2d15f29',
|
||||
type: 'bitstream',
|
||||
_links: {
|
||||
self: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29' },
|
||||
content: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/4db100c1-e1f5-4055-9404-9bc3e2d15f29/content' },
|
||||
format: { href: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreamformats/17' },
|
||||
bundle: { href: '' }
|
||||
},
|
||||
metadata: {
|
||||
'dc.title': [
|
||||
{
|
||||
language: null,
|
||||
value: 'scary'
|
||||
}
|
||||
]
|
||||
}
|
||||
});
|
||||
|
||||
export const MockOriginalBundle: Bundle = Object.assign(new Bundle(), {
|
||||
name: 'ORIGINAL',
|
||||
primaryBitstream: createSuccessfulRemoteDataObject$(MockBitstream2),
|
||||
bitstreams: observableOf(Object.assign({
|
||||
_links: {
|
||||
self: {
|
||||
@@ -124,7 +156,18 @@ export const ItemMock: Item = Object.assign(new Item(), {
|
||||
]
|
||||
}
|
||||
}))
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
/* tslint:disable:no-shadowed-variable */
|
||||
export const ItemMock: Item = Object.assign(new Item(), {
|
||||
handle: '10673/6',
|
||||
lastModified: '2017-04-24T19:44:08.178+0000',
|
||||
isArchived: true,
|
||||
isDiscoverable: true,
|
||||
isWithdrawn: false,
|
||||
bundles: createSuccessfulRemoteDataObject$(createPaginatedList([
|
||||
MockOriginalBundle,
|
||||
])),
|
||||
_links:{
|
||||
self: {
|
||||
|
Reference in New Issue
Block a user