Merge branch 'master' into w2p-44988_search-sidebar

Conflicts:
	resources/i18n/en.json
	src/app/+search-page/search-page.component.html
This commit is contained in:
Lotte Hofstede
2017-10-26 13:30:30 +02:00
57 changed files with 1658 additions and 405 deletions

View File

@@ -10,4 +10,6 @@ import { CollectionPageComponent } from './collection-page.component';
])
]
})
export class CollectionPageRoutingModule { }
export class CollectionPageRoutingModule {
}

View File

@@ -7,6 +7,8 @@ import {
} from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { PageInfo } from '../core/shared/page-info.model';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { Collection } from '../core/shared/collection.model';
@@ -18,8 +20,8 @@ import { Item } from '../core/shared/item.model';
import { SortOptions, SortDirection } from '../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { hasValue, isNotEmpty, isUndefined } from '../shared/empty.util';
import { PageInfo } from '../core/shared/page-info.model';
import { Observable } from 'rxjs/Observable';
import { MetadataService } from '../core/metadata/metadata.service';
import { fadeIn, fadeInOut } from '../shared/animations/fade';
@@ -41,9 +43,12 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
private subs: Subscription[] = [];
private collectionId: string;
constructor(private collectionDataService: CollectionDataService,
private itemDataService: ItemDataService,
private route: ActivatedRoute) {
constructor(
private collectionDataService: CollectionDataService,
private itemDataService: ItemDataService,
private metadata: MetadataService,
private route: ActivatedRoute
) {
this.paginationConfig = new PaginationComponentOptions();
this.paginationConfig.id = 'collection-page-pagination';
this.paginationConfig.pageSizeOptions = [4];
@@ -57,12 +62,13 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
Observable.combineLatest(
this.route.params,
this.route.queryParams,
(params, queryParams,) => {
(params, queryParams, ) => {
return Object.assign({}, params, queryParams);
})
.subscribe((params) => {
this.collectionId = params.id;
this.collectionData = this.collectionDataService.findById(this.collectionId);
this.metadata.processRemoteData(this.collectionData);
this.subs.push(this.collectionData.payload.subscribe((collection) => this.logoData = collection.logo));
const page = +params.page || this.paginationConfig.currentPage;

View File

@@ -10,4 +10,6 @@ import { CommunityPageComponent } from './community-page.component';
])
]
})
export class CommunityPageRoutingModule { }
export class CommunityPageRoutingModule {
}

View File

@@ -9,6 +9,8 @@ import { RemoteData } from '../core/data/remote-data';
import { CommunityDataService } from '../core/data/community-data.service';
import { hasValue } from '../shared/empty.util';
import { MetadataService } from '../core/metadata/metadata.service';
import { fadeInOut } from '../shared/animations/fade';
@Component({
@@ -24,6 +26,7 @@ export class CommunityPageComponent implements OnInit, OnDestroy {
constructor(
private communityDataService: CommunityDataService,
private metadata: MetadataService,
private route: ActivatedRoute
) {
@@ -32,15 +35,13 @@ export class CommunityPageComponent implements OnInit, OnDestroy {
ngOnInit(): void {
this.route.params.subscribe((params: Params) => {
this.communityData = this.communityDataService.findById(params.id);
this.subs.push(this.communityData.payload
.subscribe((community) => this.logoData = community.logo));
this.metadata.processRemoteData(this.communityData);
this.subs.push(this.communityData.payload.subscribe((community) => this.logoData = community.logo));
});
}
ngOnDestroy(): void {
this.subs
.filter((sub) => hasValue(sub))
.forEach((sub) => sub.unsubscribe());
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -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: 'home.title' } }
])
]
})

View File

@@ -1,15 +1,17 @@
import { Component, OnInit } from '@angular/core';
import { animate, state, transition, trigger, style, keyframes } from '@angular/animations';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
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';
import { MetadataService } from '../../core/metadata/metadata.service';
import { fadeInOut } from '../../shared/animations/fade';
/**
@@ -30,8 +32,8 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit {
metadata: Observable<Metadatum[]>;
constructor(route: ActivatedRoute, items: ItemDataService) {
super(route, items);
constructor(route: ActivatedRoute, items: ItemDataService, metadataService: MetadataService) {
super(route, items, metadataService);
}
/*** AoT inheritance fix, will hopefully be resolved in the near future **/

View File

@@ -8,6 +8,8 @@ import { ItemDataService } from '../../core/data/item-data.service';
import { RemoteData } from '../../core/data/remote-data';
import { Bitstream } from '../../core/shared/bitstream.model';
import { MetadataService } from '../../core/metadata/metadata.service';
import { fadeInOut } from '../../shared/animations/fade';
/**
@@ -31,7 +33,11 @@ export class ItemPageComponent implements OnInit {
thumbnail: Observable<Bitstream>;
constructor(private route: ActivatedRoute, private items: ItemDataService) {
constructor(
private route: ActivatedRoute,
private items: ItemDataService,
private metadataService: MetadataService
) {
}
@@ -44,6 +50,7 @@ export class ItemPageComponent implements OnInit {
initialize(params) {
this.id = +params.id;
this.item = this.items.findById(params.id);
this.metadataService.processRemoteData(this.item);
this.thumbnail = this.item.payload.flatMap((i) => i.getThumbnail());
}

View File

@@ -1,7 +1,13 @@
import { SortOptions } from '../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
export enum ViewMode {
List = 'list',
Grid = 'grid'
}
export class SearchOptions {
pagination?: PaginationComponentOptions;
sort?: SortOptions;
view?: ViewMode = ViewMode.List;
}

View File

@@ -6,7 +6,7 @@ import { SearchPageComponent } from './search-page.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: '', component: SearchPageComponent }
{ path: '', component: SearchPageComponent, data: { title: 'search.title' } }
])
]
})

View File

@@ -3,6 +3,8 @@
<ds-layout-controls class="col-md-3 d-none d-md-block sidebar-md-fixed"
[isList]="isListView"
(toggleList)="setListView($event)"></ds-layout-controls>
<ds-view-mode-switch></ds-view-mode-switch>
<ds-search-form id="search-form" class="col-12 col-md-9 ml-md-auto"
[query]="query"
[scope]="scopeObject?.payload | async"
@@ -17,6 +19,8 @@
(toggleSidebar)="setSidebarActive($event)"></ds-search-sidebar>
<div id="search-content" class="col-12 col-md-9 ml-md-auto">
<div class="d-block d-md-none search-controls clearfix">
<ds-view-mode-switch></ds-view-mode-switch>
<ds-layout-controls [isList]="isListView"
(toggleList)="setListView($event)"></ds-layout-controls>
<button (click)="setSidebarActive(true)" aria-controls="#search-body"
@@ -30,4 +34,4 @@
</div>
</div>
</div>
</div>
</div>

View File

@@ -104,7 +104,7 @@ describe('SearchPageComponent', () => {
(comp as any).updateSearchResults({});
expect(comp.results as any).toBe(mockResults);
});
});
});
});

View File

@@ -6,6 +6,7 @@ import { SearchResult } from './search-result.model';
import { DSpaceObject } from '../core/shared/dspace-object.model';
import { SortOptions } from '../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { ViewModeSwitchComponent } from '../shared/view-mode-switch/view-mode-switch.component';
import { SearchOptions } from './search-options.model';
import { CommunityDataService } from '../core/data/community-data.service';
import { isNotEmpty } from '../shared/empty.util';

View File

@@ -0,0 +1,56 @@
import { TestBed, fakeAsync, tick } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { Router } from '@angular/router';
import { SearchService } from './search.service';
import { ItemDataService } from './../../core/data/item-data.service';
import { ViewMode } from '../../+search-page/search-options.model';
@Component({ template: '' })
class DummyComponent { }
describe('SearchService', () => {
let searchService: SearchService;
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
RouterTestingModule.withRoutes([
{ path: 'search', component: DummyComponent, pathMatch: 'full' },
])
],
declarations: [
DummyComponent
],
providers: [
{ provide: ItemDataService, useValue: {} },
SearchService
],
});
searchService = TestBed.get(SearchService);
});
it('should return list view mode by default', () => {
searchService.getViewMode().subscribe((viewMode) => {
expect(viewMode).toBe(ViewMode.List);
});
});
it('should return the view mode set through setViewMode', fakeAsync(() => {
searchService.setViewMode(ViewMode.Grid)
tick();
let viewMode = ViewMode.List;
searchService.getViewMode().subscribe((mode) => viewMode = mode);
expect(viewMode).toBe(ViewMode.Grid);
searchService.setViewMode(ViewMode.List)
tick();
searchService.getViewMode().subscribe((mode) => viewMode = mode);
expect(viewMode).toBe(ViewMode.List);
}));
});

View File

@@ -13,6 +13,8 @@ import { ItemSearchResult } from '../../object-list/search-result-list-element/i
import { SearchFilterConfig } from './search-filter-config.model';
import { FilterType } from './filter-type.model';
import { FacetValue } from './facet-value.model';
import { ViewMode } from '../../+search-page/search-options.model';
import { Router, NavigationExtras, ActivatedRoute } from '@angular/router';
function shuffle(array: any[]) {
let i = 0;
@@ -76,7 +78,10 @@ export class SearchService {
})
];
constructor(private itemDataService: ItemDataService) {
constructor(
private itemDataService: ItemDataService,
private route: ActivatedRoute,
private router: Router) {
}
@@ -192,4 +197,23 @@ export class SearchService {
Observable.of(values)
);
}
getViewMode(): Observable<ViewMode> {
return this.route.queryParams.map((params) => {
if (isNotEmpty(params.view) && hasValue(params.view)) {
return params.view;
} else {
return ViewMode.List;
}
});
}
setViewMode(viewMode: ViewMode) {
const navigationExtras: NavigationExtras = {
queryParams: {view: viewMode},
queryParamsHandling: 'merge'
};
this.router.navigate(['/search'], navigationExtras);
}
}

View File

@@ -1,4 +1,3 @@
// ... test imports
import {
async,
ComponentFixture,
@@ -23,14 +22,18 @@ import { AppComponent } from './app.component';
import { HostWindowState } from './shared/host-window.reducer';
import { HostWindowResizeAction } from './shared/host-window.actions';
import { MockTranslateLoader } from './shared/testing/mock-translate-loader';
import { BrowserTransferStateModule } from '../modules/transfer-state/browser-transfer-state.module';
import { BrowserTransferStoreModule } from '../modules/transfer-store/browser-transfer-store.module';
import { MetadataService } from './core/metadata/metadata.service';
import { GLOBAL_CONFIG, ENV_CONFIG } from '../config';
import { NativeWindowRef, NativeWindowService } from './shared/window.service';
import { MockTranslateLoader } from './shared/mocks/mock-translate-loader';
import { MockMetadataService } from './shared/mocks/mock-metadata-service';
let comp: AppComponent;
let fixture: ComponentFixture<AppComponent>;
let de: DebugElement;
@@ -57,6 +60,7 @@ describe('App component', () => {
providers: [
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
{ provide: NativeWindowService, useValue: new NativeWindowRef() },
{ provide: MetadataService, useValue: new MockMetadataService() },
AppComponent
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]

View File

@@ -12,12 +12,10 @@ import { TranslateService } from '@ngx-translate/core';
import { Store } from '@ngrx/store';
import { TransferState } from '../modules/transfer-state/transfer-state';
import { HostWindowState } from './shared/host-window.reducer';
import { HostWindowResizeAction } from './shared/host-window.actions';
import { NativeWindowRef, NativeWindowService } from './shared/window.service';
import { MetadataService } from './core/metadata/metadata.service';
import { GLOBAL_CONFIG, GlobalConfig } from '../config';
@@ -35,13 +33,16 @@ export class AppComponent implements OnInit {
@Inject(NativeWindowService) private _window: NativeWindowRef,
private translate: TranslateService,
private cache: TransferState,
private store: Store<HostWindowState>
private store: Store<HostWindowState>,
private metadata: MetadataService
) {
// this language will be used as a fallback when a translation isn't found in the current language
translate.setDefaultLang('en');
// the lang to use, if the lang isn't available, it will use the current loader to get them
translate.use('en');
metadata.listenForRouteChange();
if (config.debug) {
console.info(config);
}

View File

@@ -1,17 +1,17 @@
import { ActionReducerMap } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store';
import { headerReducer, HeaderState } from './header/header.reducer';
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
export interface AppState {
router: fromRouter.RouterReducerState;
hostWindow: HostWindowState;
header: HeaderState;
}
export const appReducers: ActionReducerMap<AppState> = {
router: fromRouter.routerReducer,
hostWindow: hostWindowReducer,
header: headerReducer
};
import { ActionReducerMap } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store';
import { headerReducer, HeaderState } from './header/header.reducer';
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
export interface AppState {
router: fromRouter.RouterReducerState;
hostWindow: HostWindowState;
header: HeaderState;
}
export const appReducers: ActionReducerMap<AppState> = {
router: fromRouter.routerReducer,
hostWindow: hostWindowReducer,
header: headerReducer
};

View File

@@ -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;
}

View File

@@ -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;
/**

View File

@@ -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
}

View File

@@ -1,29 +1,35 @@
import { NgModule, Optional, SkipSelf, ModuleWithProviders } from '@angular/core';
import {
NgModule,
Optional,
SkipSelf,
ModuleWithProviders
} from '@angular/core';
import { CommonModule } from '@angular/common';
import { isNotEmpty } from '../shared/empty.util';
import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service';
import { ObjectCacheService } from './cache/object-cache.service';
import { ResponseCacheService } from './cache/response-cache.service';
import { CollectionDataService } from './data/collection-data.service';
import { ItemDataService } from './data/item-data.service';
import { RequestService } from './data/request.service';
import { RemoteDataBuildService } from './cache/builders/remote-data-build.service';
import { CommunityDataService } from './data/community-data.service';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { coreEffects } from './core.effects';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { coreEffects } from './core.effects';
import { coreReducers } from './core.reducers';
import { DSOResponseParsingService } from './data/dso-response-parsing.service';
import { RootResponseParsingService } from './data/root-response-parsing.service';
import { isNotEmpty } from '../shared/empty.util';
import { ApiService } from '../shared/api.service';
import { CollectionDataService } from './data/collection-data.service';
import { CommunityDataService } from './data/community-data.service';
import { DSOResponseParsingService } from './data/dso-response-parsing.service';
import { DSpaceRESTv2Service } from './dspace-rest-v2/dspace-rest-v2.service';
import { HostWindowService } from '../shared/host-window.service';
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
import { ItemDataService } from './data/item-data.service';
import { MetadataService } from './metadata/metadata.service';
import { ObjectCacheService } from './cache/object-cache.service';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { RemoteDataBuildService } from './cache/builders/remote-data-build.service';
import { RequestService } from './data/request.service';
import { ResponseCacheService } from './cache/response-cache.service';
import { RootResponseParsingService } from './data/root-response-parsing.service';
import { ServerResponseService } from '../shared/server-response.service';
import { NativeWindowFactory, NativeWindowService } from '../shared/window.service';
const IMPORTS = [
CommonModule,
@@ -47,6 +53,7 @@ const PROVIDERS = [
DSpaceRESTv2Service,
HostWindowService,
ItemDataService,
MetadataService,
ObjectCacheService,
PaginationComponentOptions,
RemoteDataBuildService,

View File

@@ -3,7 +3,10 @@ import { CacheableObject } from '../cache/object-cache.reducer';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { RemoteData } from './remote-data';
import {
FindAllOptions, FindAllRequest, FindByIDRequest, RestRequest,
FindAllOptions,
FindAllRequest,
FindByIDRequest,
RestRequest,
RootEndpointRequest
} from './request.models';
import { Store } from '@ngrx/store';
@@ -51,7 +54,7 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
}
public isEnabledOnRestApi(): Observable<boolean> {
return this.getEndpointMap()
return this.getEndpointMap()
.map((map: EndpointMap) => isNotEmpty(map[this.linkName]))
.startWith(undefined)
.distinctUntilChanged();

View File

@@ -0,0 +1,226 @@
import { ComponentFixture, TestBed, async, fakeAsync, inject, tick } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing';
import { Location, CommonModule } from '@angular/common';
import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core';
import { By, Meta, MetaDefinition, Title } from '@angular/platform-browser';
import { ActivatedRoute, Router } from '@angular/router';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { Store, StoreModule } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { MetadataService } from './metadata.service';
import { CoreState } from '../core.reducers';
import { GlobalConfig } from '../../../config/global-config.interface';
import { ENV_CONFIG, GLOBAL_CONFIG } from '../../../config';
import { ItemDataService } from '../data/item-data.service';
import { ObjectCacheService } from '../cache/object-cache.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { RequestService } from '../data/request.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model';
import { MockItem } from '../../shared/mocks/mock-item';
import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader';
/* 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 */
describe('MetadataService', () => {
let metadataService: MetadataService;
let meta: Meta;
let title: Title;
let store: Store<CoreState>;
let objectCacheService: ObjectCacheService;
let responseCacheService: ResponseCacheService;
let requestService: RequestService;
let remoteDataBuildService: RemoteDataBuildService;
let itemDataService: ItemDataService;
let location: Location;
let router: Router;
let fixture: ComponentFixture<TestComponent>;
let tagStore: Map<string, MetaDefinition[]>;
let envConfig: GlobalConfig;
beforeEach(() => {
store = new Store<CoreState>(undefined, undefined, undefined);
spyOn(store, 'dispatch');
objectCacheService = new ObjectCacheService(store);
responseCacheService = new ResponseCacheService(store);
requestService = new RequestService(objectCacheService, responseCacheService, store);
remoteDataBuildService = new RemoteDataBuildService(objectCacheService, responseCacheService, requestService);
TestBed.configureTestingModule({
imports: [
CommonModule,
StoreModule.forRoot({}),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
}),
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: ResponseCacheService, useValue: responseCacheService },
{ provide: RequestService, useValue: requestService },
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
Meta,
Title,
ItemDataService,
MetadataService
],
schemas: [CUSTOM_ELEMENTS_SCHEMA]
});
meta = TestBed.get(Meta);
title = TestBed.get(Title);
itemDataService = TestBed.get(ItemDataService);
metadataService = TestBed.get(MetadataService);
envConfig = TestBed.get(GLOBAL_CONFIG);
router = TestBed.get(Router);
location = TestBed.get(Location);
fixture = TestBed.createComponent(TestComponent);
tagStore = metadataService.getTagStore();
});
it('items page should set meta tags', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(MockItem));
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
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-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(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(MockItem, 'Thesis'))));
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
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([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(itemDataService, 'findById').and.returnValue(mockRemoteData(mockPublisher(mockType(MockItem, 'Technical Report'))));
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
tick();
expect(tagStore.get('citation_technical_report_institution')[0].content).toEqual('Mock Publisher');
}));
it('other navigation should title and description', fakeAsync(() => {
spyOn(itemDataService, 'findById').and.returnValue(mockRemoteData(MockItem));
router.navigate(['/items/0ec7ff22-f211-40ab-a69e-c819b0b1f357']);
tick();
expect(tagStore.size).toBeGreaterThan(0)
router.navigate(['/other']);
tick();
expect(tagStore.size).toEqual(2);
expect(title.getTitle()).toEqual('Dummy Title');
expect(tagStore.get('title')[0].content).toEqual('Dummy Title');
expect(tagStore.get('description')[0].content).toEqual('This is a dummy item component for testing!');
}));
const mockRemoteData = (mockItem: Item): RemoteData<Item> => {
return new RemoteData<Item>(
Observable.create((observer) => {
observer.next('');
}),
Observable.create((observer) => {
observer.next(false);
}),
Observable.create((observer) => {
observer.next(false);
}),
Observable.create((observer) => {
observer.next(true);
}),
Observable.create((observer) => {
observer.next('');
}),
Observable.create((observer) => {
observer.next(200);
}),
Observable.create((observer) => {
observer.next({});
}),
Observable.create((observer) => {
observer.next(MockItem);
})
);
}
const mockType = (mockItem: Item, type: string): Item => {
const typedMockItem = Object.assign(new Item(), mockItem) as Item;
for (const metadatum of typedMockItem.metadata) {
if (metadatum.key === 'dc.type') {
metadatum.value = type;
break;
}
}
return typedMockItem;
}
const mockPublisher = (mockItem: Item): Item => {
const publishedMockItem = Object.assign(new Item(), mockItem) as Item;
publishedMockItem.metadata.push({
key: 'dc.publisher',
language: 'en_US',
value: 'Mock Publisher'
});
return publishedMockItem;
}
});

View File

@@ -0,0 +1,400 @@
import 'rxjs/add/operator/first'
import 'rxjs/add/operator/take'
import { Inject, Injectable } from '@angular/core';
import {
ActivatedRoute,
Event,
NavigationEnd,
Params,
Router
} from '@angular/router';
import { Meta, MetaDefinition, Title } from '@angular/platform-browser';
import { TranslateService } from '@ngx-translate/core';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import { RemoteData } from '../data/remote-data';
import { Bitstream } from '../shared/bitstream.model';
import { CacheableObject } from '../cache/object-cache.reducer';
import { DSpaceObject } from '../shared/dspace-object.model';
import { Item } from '../shared/item.model';
import { Metadatum } from '../shared/metadatum.model';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
@Injectable()
export class MetadataService {
private initialized: boolean;
private tagStore: Map<string, MetaDefinition[]>;
private currentObject: BehaviorSubject<DSpaceObject>;
constructor(
private router: Router,
private translate: TranslateService,
private meta: Meta,
private title: Title,
@Inject(GLOBAL_CONFIG) private envConfig: GlobalConfig
) {
// 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.router.events
.filter((event) => event instanceof NavigationEnd)
.map(() => this.router.routerState.root)
.map((route: ActivatedRoute) => {
route = this.getCurrentRoute(route);
return { params: route.params, data: route.data };
}).subscribe((routeInfo: any) => {
this.processRouteChange(routeInfo);
});
}
public processRemoteData(remoteData: RemoteData<CacheableObject>): void {
remoteData.payload.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) {
this.translate.get(routeInfo.data.value.title).take(1).subscribe((translatedTitle: string) => {
this.addMetaTag('title', translatedTitle);
this.title.setTitle(translatedTitle);
});
}
if (routeInfo.data.value.description) {
this.translate.get(routeInfo.data.value.description).take(1).subscribe((translatedDescription: string) => {
this.addMetaTag('description', translatedDescription);
});
}
}
private initialize(dspaceObject: DSpaceObject): void {
this.currentObject = new BehaviorSubject<DSpaceObject>(dspaceObject);
this.currentObject.asObservable().distinctUntilKeyChanged('uuid').subscribe(() => {
this.setMetaTags();
});
this.initialized = true;
}
private getCurrentRoute(route: ActivatedRoute): ActivatedRoute {
while (route.firstChild) {
route = route.firstChild;
}
return route;
}
private setMetaTags(): void {
this.clearMetaTags();
this.setTitleTag();
this.setDescriptionTag();
this.setCitationTitleTag();
this.setCitationAuthorTags();
this.setCitationDateTag();
this.setCitationISSNTag();
this.setCitationISBNTag();
this.setCitationLanguageTag();
this.setCitationKeywordsTag();
this.setCitationAbstractUrlTag();
this.setCitationPdfUrlTag();
if (this.isDissertation()) {
this.setCitationDissertationNameTag();
this.setCitationDissertationInstitutionTag();
}
if (this.isTechReport()) {
this.setCitationTechReportInstitutionTag();
}
// this.setCitationJournalTitleTag();
// this.setCitationVolumeTag();
// this.setCitationIssueTag();
// this.setCitationFirstPageTag();
// this.setCitationLastPageTag();
// this.setCitationDOITag();
// this.setCitationPMIDTag();
// this.setCitationFullTextTag();
// this.setCitationConferenceTag();
// this.setCitationPatentCountryTag();
// this.setCitationPatentNumberTag();
}
/**
* Add <meta name="title" ... > to the <head>
*/
private setTitleTag(): void {
const value = this.getMetaTagValue('dc.title');
this.addMetaTag('title', value);
this.title.setTitle(value);
}
/**
* Add <meta name="description" ... > to the <head>
*/
private setDescriptionTag(): void {
// TODO: truncate abstract
const value = this.getMetaTagValue('dc.description.abstract');
this.addMetaTag('desciption', value);
}
/**
* Add <meta name="citation_title" ... > to the <head>
*/
private setCitationTitleTag(): void {
const value = this.getMetaTagValue('dc.title');
this.addMetaTag('citation_title', value);
}
/**
* Add <meta name="citation_author" ... > to the <head>
*/
private setCitationAuthorTags(): void {
const values: string[] = this.getMetaTagValues(['dc.author', 'dc.contributor.author', 'dc.creator']);
this.addMetaTags('citation_author', values);
}
/**
* Add <meta name="citation_date" ... > to the <head>
*/
private setCitationDateTag(): void {
const value = this.getFirstMetaTagValue(['dc.date.copyright', 'dc.date.issued', 'dc.date.available', 'dc.date.accessioned']);
this.addMetaTag('citation_date', value);
}
/**
* Add <meta name="citation_issn" ... > to the <head>
*/
private setCitationISSNTag(): void {
const value = this.getMetaTagValue('dc.identifier.issn');
this.addMetaTag('citation_issn', value);
}
/**
* Add <meta name="citation_isbn" ... > to the <head>
*/
private setCitationISBNTag(): void {
const value = this.getMetaTagValue('dc.identifier.isbn');
this.addMetaTag('citation_isbn', value);
}
/**
* Add <meta name="citation_language" ... > to the <head>
*/
private setCitationLanguageTag(): void {
const value = this.getFirstMetaTagValue(['dc.language', 'dc.language.iso']);
this.addMetaTag('citation_language', value);
}
/**
* Add <meta name="citation_dissertation_name" ... > to the <head>
*/
private setCitationDissertationNameTag(): void {
const value = this.getMetaTagValue('dc.title');
this.addMetaTag('citation_dissertation_name', value);
}
/**
* Add <meta name="citation_dissertation_institution" ... > to the <head>
*/
private setCitationDissertationInstitutionTag(): void {
const value = this.getMetaTagValue('dc.publisher');
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');
this.addMetaTag('citation_technical_report_institution', value);
}
/**
* Add <meta name="citation_keywords" ... > to the <head>
*/
private setCitationKeywordsTag(): void {
const value = this.getMetaTagValuesAndCombine('dc.subject');
this.addMetaTag('citation_keywords', value);
}
/**
* Add <meta name="citation_abstract_html_url" ... > to the <head>
*/
private setCitationAbstractUrlTag(): void {
if (this.currentObject.value instanceof Item) {
const value = [this.envConfig.ui.baseUrl, this.router.url].join('');
this.addMetaTag('citation_abstract_html_url', value);
}
}
/**
* Add <meta name="citation_pdf_url" ... > to the <head>
*/
private setCitationPdfUrlTag(): void {
if (this.currentObject.value instanceof Item) {
const item = this.currentObject.value as Item;
// NOTE: Observable resolves many times with same data
// taking only two, fist one is empty array
item.getFiles().take(2).subscribe((bitstreams: Bitstream[]) => {
for (const bitstream of bitstreams) {
bitstream.format.payload.take(1).subscribe((format) => {
if (format.mimetype === 'application/pdf') {
this.addMetaTag('citation_pdf_url', bitstream.content);
}
});
}
});
}
}
/**
* Returns true if this._item is a dissertation
*
* @returns {boolean}
* true if this._item has a dc.type equal to 'Thesis'
*/
private isDissertation(): boolean {
let isDissertation = false;
for (const metadatum of this.currentObject.value.metadata) {
if (metadatum.key === 'dc.type') {
isDissertation = metadatum.value.toLowerCase() === 'thesis';
break;
}
}
return isDissertation;
}
/**
* Returns true if this._item is a technical report
*
* @returns {boolean}
* true if this._item has a dc.type equal to 'Technical Report'
*/
private isTechReport(): boolean {
let isTechReport = false;
for (const metadatum of this.currentObject.value.metadata) {
if (metadatum.key === 'dc.type') {
isTechReport = metadatum.value.toLowerCase() === 'technical report';
break;
}
}
return isTechReport;
}
private getMetaTagValue(key: string): string {
let value: string;
for (const metadatum of this.currentObject.value.metadata) {
if (metadatum.key === key) {
value = metadatum.value;
}
}
return value;
}
private getFirstMetaTagValue(keys: string[]): string {
let value: string;
for (const metadatum of this.currentObject.value.metadata) {
for (const key of keys) {
if (key === metadatum.key) {
value = metadatum.value;
break;
}
}
if (value !== undefined) {
break;
}
}
return value;
}
private getMetaTagValuesAndCombine(key: string): string {
return this.getMetaTagValues([key]).join('; ');
}
private getMetaTagValues(keys: string[]): string[] {
const values: string[] = [];
for (const metadatum of this.currentObject.value.metadata) {
for (const key of keys) {
if (key === metadatum.key) {
values.push(metadatum.value);
}
}
}
return values;
}
private addMetaTag(property: string, content: string): void {
if (content) {
const tag = { property, content } as MetaDefinition;
this.meta.addTag(tag);
this.storeTag(property, tag);
}
}
private addMetaTags(property: string, content: string[]): void {
for (const value of content) {
this.addMetaTag(property, value);
}
}
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);
}
public clearMetaTags() {
this.tagStore.forEach((tags: MetaDefinition[], property: string) => {
this.meta.removeTag("property='" + property + "'");
});
this.tagStore.clear();
}
public getTagStore(): Map<string, MetaDefinition[]> {
return this.tagStore;
}
}

View File

@@ -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;
}

View File

@@ -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<BitstreamFormat>;
/**
* An array of Items that are direct parents of this Bitstream
*/

View File

@@ -21,7 +21,7 @@ import { Store, StoreModule } from '@ngrx/store';
// Load the implementations that should be tested
import { FooterComponent } from './footer.component';
import { MockTranslateLoader } from '../shared/testing/mock-translate-loader';
import { MockTranslateLoader } from '../shared/mocks/mock-translate-loader';
let comp: FooterComponent;
let fixture: ComponentFixture<FooterComponent>;

View File

@@ -4,7 +4,7 @@ import { DebugElement } from '@angular/core';
import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-translate/core';
import { MockTranslateLoader } from '../testing/mock-translate-loader';
import { MockTranslateLoader } from '../mocks/mock-translate-loader';
import { ErrorComponent } from './error.component';
@@ -25,8 +25,8 @@ describe('ErrorComponent (inline template)', () => {
}
}),
],
declarations: [ ErrorComponent ], // declare the test component
providers: [ TranslateService ]
declarations: [ErrorComponent], // declare the test component
providers: [TranslateService]
}).compileComponents(); // compile template and css
}));

View File

@@ -4,7 +4,7 @@ import { DebugElement } from '@angular/core';
import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-translate/core';
import { MockTranslateLoader } from '../testing/mock-translate-loader';
import { MockTranslateLoader } from '../mocks/mock-translate-loader';
import { LoadingComponent } from './loading.component';
@@ -25,8 +25,8 @@ describe('LoadingComponent (inline template)', () => {
}
}),
],
declarations: [ LoadingComponent ], // declare the test component
providers: [ TranslateService ]
declarations: [LoadingComponent], // declare the test component
providers: [TranslateService]
}).compileComponents(); // compile template and css
}));

View File

@@ -0,0 +1,6 @@
import { Action } from '@ngrx/store';
export class MockAction implements Action {
type = null;
payload: {};
}

View File

@@ -0,0 +1,34 @@
import { Params } from '@angular/router';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
export class MockActivatedRoute {
private _testParams?: any;
// ActivatedRoute.params is Observable
private subject?: BehaviorSubject<any> = new BehaviorSubject(this.testParams);
params = this.subject.asObservable();
queryParams = this.subject.asObservable();
constructor(params?: Params) {
if (params) {
this.testParams = params;
} else {
this.testParams = {};
}
}
// Test parameters
get testParams() { return this._testParams; }
set testParams(params: {}) {
this._testParams = params;
this.subject.next(params);
}
// ActivatedRoute.snapshot.params
get snapshot() {
return { params: this.testParams };
}
}

View File

@@ -0,0 +1,19 @@
import { Observable } from 'rxjs/Observable';
// declare a stub service
export class MockHostWindowService {
private width: number;
constructor(width) {
this.setWidth(width);
}
setWidth(width) {
this.width = width;
}
isXs(): Observable<boolean> {
return Observable.of(this.width < 576);
}
}

View File

@@ -0,0 +1,273 @@
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',
isArchived: true,
isDiscoverable: true,
isWithdrawn: false,
bitstreams: {
self: {
_isScalar: true,
value: '1507836003548',
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([
{
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',
id: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
uuid: '0ec7ff22-f211-40ab-a69e-c819b0b1f357',
type: 'item',
name: 'Test PowerPoint Document',
metadata: [
{
key: 'dc.creator',
language: 'en_US',
value: 'Doe, Jane'
},
{
key: 'dc.date.accessioned',
language: null,
value: '1650-06-26T19:58:25Z'
},
{
key: 'dc.date.available',
language: null,
value: '1650-06-26T19:58:25Z'
},
{
key: 'dc.date.issued',
language: null,
value: '1650-06-26'
},
{
key: 'dc.identifier.issn',
language: 'en_US',
value: '123456789'
},
{
key: 'dc.identifier.uri',
language: null,
value: 'http://dspace7.4science.it/xmlui/handle/10673/6'
},
{
key: 'dc.description.abstract',
language: 'en_US',
value: 'This is really just a sample abstract. If it was a real abstract it would contain useful information about this test document. Sorry though, nothing useful in this paragraph. You probably shouldn\'t have even bothered to read it!'
},
{
key: 'dc.description.provenance',
language: 'en',
value: 'Made available in DSpace on 2012-06-26T19:58:25Z (GMT). No. of bitstreams: 2\r\ntest_ppt.ppt: 12707328 bytes, checksum: a353fc7d29b3c558c986f7463a41efd3 (MD5)\r\ntest_ppt.pptx: 12468572 bytes, checksum: 599305edb4ebee329667f2c35b14d1d6 (MD5)'
},
{
key: 'dc.description.provenance',
language: 'en',
value: 'Restored into DSpace on 2013-06-13T09:17:34Z (GMT).'
},
{
key: 'dc.description.provenance',
language: 'en',
value: 'Restored into DSpace on 2013-06-13T11:04:16Z (GMT).'
},
{
key: 'dc.description.provenance',
language: 'en',
value: 'Restored into DSpace on 2017-04-24T19:44:08Z (GMT).'
},
{
key: 'dc.language',
language: 'en_US',
value: 'en'
},
{
key: 'dc.rights',
language: 'en_US',
value: '© Jane Doe'
},
{
key: 'dc.subject',
language: 'en_US',
value: 'keyword1'
},
{
key: 'dc.subject',
language: 'en_US',
value: 'keyword2'
},
{
key: 'dc.subject',
language: 'en_US',
value: 'keyword3'
},
{
key: 'dc.title',
language: 'en_US',
value: 'Test PowerPoint Document'
},
{
key: 'dc.type',
language: 'en_US',
value: 'text'
}
],
owningCollection: {
self: {
_isScalar: true,
value: 'https://dspace7.4science.it/dspace-spring-rest/api/core/collections/1c11f3f1-ba1f-4f36-908a-3f1ea9a557eb',
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([]);
})
}
})
/* tslint:enable:no-shadowed-variable */

View File

@@ -0,0 +1,9 @@
export class MockMetadataService {
// tslint:disable-next-line:no-empty
public listenForRouteChange(): void {
}
}

View File

@@ -0,0 +1,4 @@
export class MockRouter {
// noinspection TypeScriptUnresolvedFunction
navigate = jasmine.createSpy('navigate');
}

View File

@@ -0,0 +1,23 @@
import { Action } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
export class MockStore<T> extends BehaviorSubject<T> {
constructor(private _initialState: T) {
super(_initialState);
}
dispatch = (action: Action): void => {
console.info();
}
select = <R>(pathOrMapFn: any): Observable<T> => {
return Observable.of(this.getValue());
}
nextState(_newState: T) {
this.next(_newState);
}
}

View File

@@ -0,0 +1,8 @@
import { TranslateLoader } from '@ngx-translate/core';
import { Observable } from 'rxjs/Observable';
export class MockTranslateLoader implements TranslateLoader {
getTranslation(lang: string): Observable<any> {
return Observable.of({});
}
}

View File

@@ -34,10 +34,10 @@ import Spy = jasmine.Spy;
import { PaginationComponent } from './pagination.component';
import { PaginationComponentOptions } from './pagination-component-options.model';
import { MockTranslateLoader } from '../testing/mock-translate-loader';
import { HostWindowServiceStub } from '../testing/host-window-service-stub';
import { ActivatedRouteStub } from '../testing/active-router-stub';
import { RouterStub } from '../testing/router-stub';
import { MockTranslateLoader } from '../mocks/mock-translate-loader';
import { MockHostWindowService } from '../mocks/mock-host-window-service';
import { MockActivatedRoute } from '../mocks/mock-active-router';
import { MockRouter } from '../mocks/mock-router';
import { HostWindowService } from '../host-window.service';
import { EnumKeysPipe } from '../utils/enum-keys-pipe';
@@ -45,7 +45,7 @@ import { SortOptions } from '../../core/cache/models/sort-options.model';
import { GLOBAL_CONFIG, ENV_CONFIG } from '../../../config';
function createTestComponent<T>(html: string, type: { new (...args: any[]): T }): ComponentFixture<T> {
function createTestComponent<T>(html: string, type: { new(...args: any[]): T }): ComponentFixture<T> {
TestBed.overrideComponent(type, {
set: { template: html }
});
@@ -123,19 +123,19 @@ describe('Pagination component', () => {
let testFixture: ComponentFixture<TestComponent>;
let de: DebugElement;
let html;
let hostWindowServiceStub: HostWindowServiceStub;
let hostWindowServiceStub: MockHostWindowService;
let activatedRouteStub: ActivatedRouteStub;
let routerStub: RouterStub;
let activatedRouteStub: MockActivatedRoute;
let routerStub: MockRouter;
// Define initial state and test state
const _initialState = { width: 1600, height: 770 };
// async beforeEach
beforeEach(async(() => {
activatedRouteStub = new ActivatedRouteStub();
routerStub = new RouterStub();
hostWindowServiceStub = new HostWindowServiceStub(_initialState.width);
activatedRouteStub = new MockActivatedRoute();
routerStub = new MockRouter();
hostWindowServiceStub = new MockHostWindowService(_initialState.width);
TestBed.configureTestingModule({
imports: [

View File

@@ -29,6 +29,7 @@ import { ThumbnailComponent } from '../thumbnail/thumbnail.component';
import { SearchResultListElementComponent } from '../object-list/search-result-list-element/search-result-list-element.component';
import { SearchFormComponent } from './search-form/search-form.component';
import { WrapperListElementComponent } from '../object-list/wrapper-list-element/wrapper-list-element.component';
import { ViewModeSwitchComponent } from './view-mode-switch/view-mode-switch.component';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -61,7 +62,8 @@ const COMPONENTS = [
PaginationComponent,
SearchFormComponent,
ThumbnailComponent,
WrapperListElementComponent
WrapperListElementComponent,
ViewModeSwitchComponent
];
const ENTRY_COMPONENTS = [

View File

@@ -0,0 +1,20 @@
<div class="btn-group" data-toggle="buttons">
<a routerLink="."
[queryParams]="{view: 'list'}"
queryParamsHandling="merge"
(click)="switchViewTo(viewModeEnum.List)"
routerLinkActive="active"
[class.active]="currentMode === viewModeEnum.List"
class="btn btn-secondary">
<i class="fa fa-list" title="{{'search.view-switch.show-list' | translate}}"></i>
</a>
<a routerLink="."
[queryParams]="{view: 'grid'}"
queryParamsHandling="merge"
(click)="switchViewTo(viewModeEnum.Grid)"
routerLinkActive="active"
[class.active]="currentMode !== viewModeEnum.List"
class="btn btn-secondary">
<i class="fa fa-th-large" title="{{'search.view-switch.show-grid' | translate}}"></i>
</a>
</div>

View File

@@ -0,0 +1 @@
@import '../../../styles/variables.scss';

View File

@@ -0,0 +1,77 @@
import { DebugElement } from '@angular/core';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { async, ComponentFixture, TestBed, fakeAsync, tick } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { MockTranslateLoader } from '../mocks/mock-translate-loader';
import { RouterTestingModule } from '@angular/router/testing';
import { Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { SearchService } from '../../+search-page/search-service/search.service';
import { ItemDataService } from './../../core/data/item-data.service';
import { ViewModeSwitchComponent } from './view-mode-switch.component';
import { ViewMode } from '../../+search-page/search-options.model';
@Component({ template: '' })
class DummyComponent { }
describe('ViewModeSwitchComponent', () => {
let comp: ViewModeSwitchComponent;
let fixture: ComponentFixture<ViewModeSwitchComponent>;
let searchService: SearchService;
let listButton: HTMLElement;
let gridButton: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
}),
RouterTestingModule.withRoutes([
{ path: 'search', component: DummyComponent, pathMatch: 'full' },
])
],
declarations: [
ViewModeSwitchComponent,
DummyComponent
],
providers: [
{ provide: ItemDataService, useValue: {} },
SearchService
],
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(ViewModeSwitchComponent);
comp = fixture.componentInstance; // ViewModeSwitchComponent test instance
fixture.detectChanges();
const debugElements = fixture.debugElement.queryAll(By.css('a'));
listButton = debugElements[0].nativeElement;
gridButton = debugElements[1].nativeElement;
searchService = fixture.debugElement.injector.get(SearchService);
});
it('should set list button as active when on list mode', fakeAsync(() => {
searchService.setViewMode(ViewMode.List);
tick();
fixture.detectChanges();
expect(comp.currentMode).toBe(ViewMode.List);
expect(listButton.classList).toContain('active');
expect(gridButton.classList).not.toContain('active');
}));
it('should set grid button as active when on grid mode', fakeAsync(() => {
searchService.setViewMode(ViewMode.Grid);
tick();
fixture.detectChanges();
expect(comp.currentMode).toBe(ViewMode.Grid);
expect(listButton.classList).not.toContain('active');
expect(gridButton.classList).toContain('active');
}));
});

View File

@@ -0,0 +1,37 @@
import { Subscription } from 'rxjs/Subscription';
import { Component, OnInit, OnDestroy } from '@angular/core';
import { ViewMode } from '../../+search-page/search-options.model';
import { SearchService } from './../../+search-page/search-service/search.service';
/**
* Component to switch between list and grid views.
*/
@Component({
selector: 'ds-view-mode-switch',
styleUrls: ['./view-mode-switch.component.scss'],
templateUrl: './view-mode-switch.component.html'
})
export class ViewModeSwitchComponent implements OnInit, OnDestroy {
currentMode: ViewMode = ViewMode.List;
viewModeEnum = ViewMode;
private sub: Subscription;
constructor(private searchService: SearchService) {
}
ngOnInit(): void {
this.sub = this.searchService.getViewMode().subscribe((viewMode: ViewMode) => {
this.currentMode = viewMode;
});
}
switchViewTo(viewMode: ViewMode) {
this.searchService.setViewMode(viewMode);
}
ngOnDestroy() {
if (this.sub !== undefined) {
this.sub.unsubscribe();
}
}
}

View File

@@ -1,31 +1,31 @@
import 'zone.js/dist/zone';
import 'reflect-metadata';
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { bootloader } from '@angularclass/bootloader';
import { load as loadWebFont } from 'webfontloader';
import { BrowserAppModule } from './app/browser-app.module';
import { ENV_CONFIG } from './config';
if (ENV_CONFIG.production) {
enableProdMode();
}
export function main() {
// Load fonts async
// https://github.com/typekit/webfontloader#configuration
loadWebFont({
google: {
families: ['Droid Sans']
}
});
return platformBrowserDynamic().bootstrapModule(BrowserAppModule);
}
// support async tag or hmr
bootloader(main);
import 'zone.js/dist/zone';
import 'reflect-metadata';
import { enableProdMode } from '@angular/core';
import { platformBrowserDynamic } from '@angular/platform-browser-dynamic';
import { bootloader } from '@angularclass/bootloader';
import { load as loadWebFont } from 'webfontloader';
import { BrowserAppModule } from './modules/app/browser-app.module';
import { ENV_CONFIG } from './config';
if (ENV_CONFIG.production) {
enableProdMode();
}
export function main() {
// Load fonts async
// https://github.com/typekit/webfontloader#configuration
loadWebFont({
google: {
families: ['Droid Sans']
}
});
return platformBrowserDynamic().bootstrapModule(BrowserAppModule);
}
// support async tag or hmr
bootloader(main);

View File

@@ -17,7 +17,7 @@ import { enableProdMode } from '@angular/core';
import { ngExpressEngine } from '@nguniversal/express-engine';
import { ServerAppModule } from './app/server-app.module';
import { ServerAppModule } from './modules/app/server-app.module';
import { serverApi, createMockApi } from './backend/api';

View File

@@ -11,15 +11,15 @@ import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { EffectsModule } from '@ngrx/effects';
import { TransferState } from '../modules/transfer-state/transfer-state';
import { BrowserTransferStateModule } from '../modules/transfer-state/browser-transfer-state.module';
import { BrowserTransferStoreEffects } from '../modules/transfer-store/browser-transfer-store.effects';
import { BrowserTransferStoreModule } from '../modules/transfer-store/browser-transfer-store.module';
import { TransferState } from '../transfer-state/transfer-state';
import { BrowserTransferStateModule } from '../transfer-state/browser-transfer-state.module';
import { BrowserTransferStoreEffects } from '../transfer-store/browser-transfer-store.effects';
import { BrowserTransferStoreModule } from '../transfer-store/browser-transfer-store.module';
import { AppModule } from './app.module';
import { CoreModule } from './core/core.module';
import { AppModule } from '../../app/app.module';
import { CoreModule } from '../../app/core/core.module';
import { AppComponent } from './app.component';
import { AppComponent } from '../../app/app.component';
export function init(cache: TransferState) {
return () => {

View File

@@ -16,21 +16,21 @@ import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { Store } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects';
import { TranslateUniversalLoader } from '../modules/translate-universal-loader';
import { TranslateUniversalLoader } from '../translate-universal-loader';
import { ServerTransferStateModule } from '../modules/transfer-state/server-transfer-state.module';
import { TransferState } from '../modules/transfer-state/transfer-state';
import { ServerTransferStateModule } from '../transfer-state/server-transfer-state.module';
import { TransferState } from '../transfer-state/transfer-state';
import { ServerTransferStoreEffects } from '../modules/transfer-store/server-transfer-store.effects';
import { ServerTransferStoreModule } from '../modules/transfer-store/server-transfer-store.module';
import { ServerTransferStoreEffects } from '../transfer-store/server-transfer-store.effects';
import { ServerTransferStoreModule } from '../transfer-store/server-transfer-store.module';
import { AppState } from './app.reducer';
import { AppState } from '../../app/app.reducer';
import { AppModule } from './app.module';
import { AppModule } from '../../app/app.module';
import { AppComponent } from './app.component';
import { AppComponent } from '../../app/app.component';
import { GLOBAL_CONFIG, GlobalConfig } from '../config';
import { GLOBAL_CONFIG, GlobalConfig } from '../../config';
export function boot(cache: TransferState, appRef: ApplicationRef, store: Store<AppState>, request: Request, config: GlobalConfig) {
// authentication mechanism goes here

View File

@@ -1,6 +1,6 @@
{
"extends": "../tsconfig.json",
"angularCompilerOptions": {
"entryModule": "./app/browser-app.module#BrowserAppModule"
"entryModule": "./modules/app/browser-app.module#BrowserAppModule"
}
}

View File

@@ -1,6 +1,6 @@
{
"extends": "../tsconfig.json",
"angularCompilerOptions": {
"entryModule": "./app/server-app.module#ServerAppModule"
"entryModule": "./modules/app/server-app.module#ServerAppModule"
}
}

View File

@@ -4,6 +4,6 @@
"sourceMap": true
},
"angularCompilerOptions": {
"entryModule": "./app/browser-app.module#BrowserAppModule"
"entryModule": "./modules/app/browser-app.module#BrowserAppModule"
}
}