diff --git a/e2e/app.e2e-spec.ts b/e2e/app.e2e-spec.ts index 79b9f251dd..f5ac9094d0 100644 --- a/e2e/app.e2e-spec.ts +++ b/e2e/app.e2e-spec.ts @@ -7,9 +7,9 @@ describe('protractor App', () => { page = new ProtractorPage(); }); - it('should display title "DSpace"', () => { + it('should display translated title "DSpace Angular :: Home"', () => { page.navigateTo(); - expect(page.getPageTitleText()).toEqual('DSpace'); + expect(page.getPageTitleText()).toEqual('DSpace Angular :: Home'); }); it('should display header "Welcome to DSpace"', () => { diff --git a/karma.conf.js b/karma.conf.js index 8f51e61344..e43191d8ee 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -27,18 +27,18 @@ module.exports = function (config) { frameworks: ['jasmine'], plugins: [ - require('karma-webpack'), - require('karma-jasmine'), + require("istanbul-instrumenter-loader"), require('karma-chrome-launcher'), - require('karma-phantomjs-launcher'), - require('karma-webdriver-launcher'), require('karma-coverage'), - require('karma-remap-coverage'), + require("karma-istanbul-preprocessor"), + require('karma-jasmine'), require('karma-mocha-reporter'), + require('karma-phantomjs-launcher'), + require('karma-remap-coverage'), require('karma-remap-istanbul'), require('karma-sourcemap-loader'), - require("istanbul-instrumenter-loader"), - require("karma-istanbul-preprocessor") + require('karma-webdriver-launcher'), + require('karma-webpack') ], // list of files to exclude @@ -59,7 +59,11 @@ module.exports = function (config) { * available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor */ preprocessors: { - './spec-bundle.js': ['istanbul', 'webpack', 'sourcemap'] + './spec-bundle.js': [ + 'istanbul', + 'webpack', + 'sourcemap' + ] }, // Webpack Config at ./webpack.test.js @@ -79,9 +83,9 @@ module.exports = function (config) { remapIstanbulReporter: { remapOptions: {}, //additional remap options reports: { - json: 'coverage/coverage.json', - lcovonly: 'coverage/lcov.info', - html: 'coverage/html/', + json: './coverage/coverage.json', + lcovonly: './coverage/lcov.info', + html: './coverage/html/', } }, @@ -111,7 +115,12 @@ module.exports = function (config) { * possible values: 'dots', 'progress' * available reporters: https://npmjs.org/browse/keyword/karma-reporter */ - reporters: ['mocha', 'coverage', 'remap-coverage', 'karma-remap-istanbul'], + reporters: [ + 'mocha', + 'coverage', + 'remap-coverage', + 'karma-remap-istanbul' + ], // Karma web server port port: 9876, diff --git a/package.json b/package.json index 02bddc485e..3959872257 100644 --- a/package.json +++ b/package.json @@ -68,15 +68,15 @@ "coverage": "http-server -c-1 -o -p 9875 ./coverage" }, "dependencies": { - "@angular/animations": "4.4.4", - "@angular/common": "4.4.4", - "@angular/core": "4.4.4", - "@angular/forms": "4.4.4", - "@angular/http": "4.4.4", - "@angular/platform-browser": "4.4.4", - "@angular/platform-browser-dynamic": "4.4.4", - "@angular/platform-server": "4.4.4", - "@angular/router": "4.4.4", + "@angular/animations": "4.4.5", + "@angular/common": "4.4.5", + "@angular/core": "4.4.5", + "@angular/forms": "4.4.5", + "@angular/http": "4.4.5", + "@angular/platform-browser": "4.4.5", + "@angular/platform-browser-dynamic": "4.4.5", + "@angular/platform-server": "4.4.5", + "@angular/router": "4.4.5", "@angularclass/bootloader": "1.0.1", "@angularclass/idle-preload": "1.0.4", "@ng-bootstrap/ng-bootstrap": "1.0.0-beta.5", @@ -88,11 +88,11 @@ "@ngx-translate/http-loader": "2.0.0", "body-parser": "1.18.2", "bootstrap": "v4.0.0-beta", - "cerialize": "0.1.16", + "cerialize": "0.1.18", "compression": "1.7.1", "cookie-parser": "1.4.3", "core-js": "2.5.1", - "express": "4.16.1", + "express": "4.16.2", "express-session": "1.15.6", "font-awesome": "4.7.0", "http-server": "0.10.0", @@ -102,7 +102,7 @@ "methods": "1.1.2", "morgan": "1.9.0", "ngx-pagination": "3.0.1", - "pem": "1.12.0", + "pem": "1.12.3", "reflect-metadata": "0.1.10", "rxjs": "5.4.3", "ts-md5": "1.2.2", @@ -110,10 +110,10 @@ "zone.js": "0.8.18" }, "devDependencies": { - "@angular/compiler": "4.4.4", - "@angular/compiler-cli": "4.4.4", + "@angular/compiler": "4.4.5", + "@angular/compiler-cli": "4.4.5", "@ngrx/store-devtools": "4.0.0", - "@ngtools/webpack": "1.7.2", + "@ngtools/webpack": "1.7.4", "@types/cookie-parser": "1.4.1", "@types/deep-freeze": "0.1.1", "@types/express": "4.0.37", @@ -122,19 +122,19 @@ "@types/jasmine": "2.6.0", "@types/memory-cache": "0.0.31", "@types/mime": "2.0.0", - "@types/node": "8.0.26", + "@types/node": "8.0.34", "@types/serve-static": "1.7.32", "@types/source-map": "0.5.1", "@types/webfontloader": "1.6.29", "ajv": "5.2.3", "ajv-keywords": "2.1.0", "angular2-template-loader": "0.6.2", - "autoprefixer": "7.1.4", + "autoprefixer": "7.1.5", "awesome-typescript-loader": "3.2.3", "caniuse-lite": "1.0.30000697", - "codelyzer": "3.2.0", + "codelyzer": "3.2.1", "compression-webpack-plugin": "1.0.1", - "copy-webpack-plugin": "4.1.0", + "copy-webpack-plugin": "4.1.1", "coveralls": "3.0.0", "css-loader": "0.28.7", "deep-freeze": "0.0.1", @@ -143,7 +143,7 @@ "imports-loader": "0.7.1", "istanbul-instrumenter-loader": "3.0.0", "jasmine-core": "2.8.0", - "jasmine-marbles": "0.1.0", + "jasmine-marbles": "0.2.0", "jasmine-spec-reporter": "4.2.1", "json-loader": "0.5.7", "karma": "1.7.1", @@ -158,7 +158,7 @@ "karma-remap-istanbul": "0.6.0", "karma-sourcemap-loader": "0.3.7", "karma-webdriver-launcher": "1.0.5", - "karma-webpack": "2.0.4", + "karma-webpack": "2.0.5", "ngrx-store-freeze": "0.2.0", "node-sass": "4.5.3", "nodemon": "1.12.1", @@ -167,13 +167,13 @@ "postcss-apply": "0.8.0", "postcss-cli": "4.1.1", "postcss-cssnext": "3.0.2", - "postcss-loader": "2.0.6", + "postcss-loader": "2.0.7", "postcss-responsive-type": "1.0.0", "postcss-smart-import": "0.7.5", "protractor": "5.1.2", "protractor-istanbul-plugin": "2.0.0", "raw-loader": "0.5.1", - "resolve-url-loader": "2.1.0", + "resolve-url-loader": "2.1.1", "rimraf": "2.6.2", "rollup": "0.50.0", "rollup-plugin-commonjs": "8.2.1", @@ -188,9 +188,9 @@ "ts-helpers": "1.1.2", "ts-node": "3.3.0", "tslint": "5.7.0", - "typedoc": "0.8.0", + "typedoc": "0.9.0", "typescript": "2.5.3", - "webpack": "3.6.0", + "webpack": "3.7.1", "webpack-bundle-analyzer": "2.9.0", "webpack-dev-middleware": "1.12.0", "webpack-dev-server": "2.9.1", diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 4218ebba86..8f41b90b13 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -65,12 +65,16 @@ } }, "home": { + "title": "DSpace Angular :: Home", + "description": "", "top-level-communities": { "head": "Communities in DSpace", "help": "Select a community to browse its collections." } }, "search": { + "title": "DSpace Angular :: Search", + "description": "", "form": { "search": "Search", "search_dspace": "Search DSpace" @@ -82,6 +86,10 @@ "close": "Back to results", "open": "Search Tools", "results": "results" + }, + "view-switch": { + "show-list": "Show as list", + "show-grid": "Show as grid" } }, "loading": { diff --git a/spec-bundle.js b/spec-bundle.js index 36026d530f..b9df9bec5e 100644 --- a/spec-bundle.js +++ b/spec-bundle.js @@ -38,25 +38,11 @@ testing.TestBed.initTestEnvironment( browser.platformBrowserDynamicTesting() ); -/* - * Ok, this is kinda crazy. We can use the context method on - * require that webpack created in order to tell webpack - * what files we actually want to require or import. - * Below, context will be a function/object with file names as keys. - * Using that regex we are saying look in ../src then find - * any file that ends with spec.ts and get its path. By passing in true - * we say do this recursively - */ -var testContext = require.context('./src', true, /\.spec\.ts/); +var tests = require.context('./src', true, /\.spec\.ts$/); -/* - * get all the files, for each file, call the context function - * that will require the file and load it up here. Context will - * loop and require those spec files here - */ -function requireAll(requireContext) { - return requireContext.keys().map(requireContext); -} +tests.keys().forEach(tests); -// requires and returns all modules that match -var modules = requireAll(testContext); +// includes all modules into test coverage +const modules = require.context('./src/app', true, /\.module\.ts$/); + +modules.keys().forEach(modules); diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index c62643acc2..c886aa655c 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -10,4 +10,6 @@ import { CollectionPageComponent } from './collection-page.component'; ]) ] }) -export class CollectionPageRoutingModule { } +export class CollectionPageRoutingModule { + +} diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index 65bf708727..30d9c17fe3 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -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; diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index e090a93087..6fd5cc8cb5 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -10,4 +10,6 @@ import { CommunityPageComponent } from './community-page.component'; ]) ] }) -export class CommunityPageRoutingModule { } +export class CommunityPageRoutingModule { + +} diff --git a/src/app/+community-page/community-page.component.ts b/src/app/+community-page/community-page.component.ts index 1295e14521..0cd94658be 100644 --- a/src/app/+community-page/community-page.component.ts +++ b/src/app/+community-page/community-page.component.ts @@ -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()); } } diff --git a/src/app/+home-page/home-page-routing.module.ts b/src/app/+home-page/home-page-routing.module.ts index e68b633a6d..d7dcc18f49 100644 --- a/src/app/+home-page/home-page-routing.module.ts +++ b/src/app/+home-page/home-page-routing.module.ts @@ -6,7 +6,7 @@ import { HomePageComponent } from './home-page.component'; @NgModule({ imports: [ RouterModule.forChild([ - { path: '', component: HomePageComponent, pathMatch: 'full' } + { path: '', component: HomePageComponent, pathMatch: 'full', data: { title: 'home.title' } } ]) ] }) diff --git a/src/app/+item-page/full/full-item-page.component.ts b/src/app/+item-page/full/full-item-page.component.ts index 337c598021..270cf1fcae 100644 --- a/src/app/+item-page/full/full-item-page.component.ts +++ b/src/app/+item-page/full/full-item-page.component.ts @@ -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; - 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 **/ diff --git a/src/app/+item-page/simple/item-page.component.ts b/src/app/+item-page/simple/item-page.component.ts index 8f1bd18b4e..ce9283e144 100644 --- a/src/app/+item-page/simple/item-page.component.ts +++ b/src/app/+item-page/simple/item-page.component.ts @@ -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; - 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()); } diff --git a/src/app/+search-page/search-options.model.ts b/src/app/+search-page/search-options.model.ts index fd4a5accf6..7f93c3ace1 100644 --- a/src/app/+search-page/search-options.model.ts +++ b/src/app/+search-page/search-options.model.ts @@ -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; } diff --git a/src/app/+search-page/search-page-routing.module.ts b/src/app/+search-page/search-page-routing.module.ts index de2d64c6c9..65cca99a34 100644 --- a/src/app/+search-page/search-page-routing.module.ts +++ b/src/app/+search-page/search-page-routing.module.ts @@ -6,7 +6,7 @@ import { SearchPageComponent } from './search-page.component'; @NgModule({ imports: [ RouterModule.forChild([ - { path: '', component: SearchPageComponent } + { path: '', component: SearchPageComponent, data: { title: 'search.title' } } ]) ] }) diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html index 6b26c11ea1..04d3b13e43 100644 --- a/src/app/+search-page/search-page.component.html +++ b/src/app/+search-page/search-page.component.html @@ -3,6 +3,8 @@ + +
+ +
- \ No newline at end of file + diff --git a/src/app/+search-page/search-page.component.spec.ts b/src/app/+search-page/search-page.component.spec.ts index a3e314db69..0b00021ed6 100644 --- a/src/app/+search-page/search-page.component.spec.ts +++ b/src/app/+search-page/search-page.component.spec.ts @@ -104,7 +104,7 @@ describe('SearchPageComponent', () => { (comp as any).updateSearchResults({}); expect(comp.results as any).toBe(mockResults); - }); + }); }); }); diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index 64727245dc..01569711ab 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -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'; diff --git a/src/app/+search-page/search-service/search.service.spec.ts b/src/app/+search-page/search-service/search.service.spec.ts new file mode 100644 index 0000000000..489ac76763 --- /dev/null +++ b/src/app/+search-page/search-service/search.service.spec.ts @@ -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); + })); + +}); diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index e2804960ef..a06cbd90b8 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -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 { + 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); + } } diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 99cf1068e4..0854c5756e 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -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; 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] diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 9ea58365df..221c1c37d1 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -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 + private store: Store, + 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); } diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index 646d7a26b5..baa3250549 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -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 = { - 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 = { + router: fromRouter.routerReducer, + hostWindow: hostWindowReducer, + header: headerReducer +}; diff --git a/src/app/core/cache/models/normalized-bitstream-format.model.ts b/src/app/core/cache/models/normalized-bitstream-format.model.ts index 2813794011..bb8b049a1c 100644 --- a/src/app/core/cache/models/normalized-bitstream-format.model.ts +++ b/src/app/core/cache/models/normalized-bitstream-format.model.ts @@ -1,13 +1,30 @@ -import { inheritSerialization } from 'cerialize'; +import { inheritSerialization, autoserialize } from 'cerialize'; +import { mapsTo } from '../builders/build-decorators'; + +import { BitstreamFormat } from '../../shared/bitstream-format.model'; import { NormalizedObject } from './normalized-object.model'; +@mapsTo(BitstreamFormat) @inheritSerialization(NormalizedObject) export class NormalizedBitstreamFormat extends NormalizedObject { - // TODO: this class was created as a placeholder when we connected to the live rest api - get uuid(): string { - return this.self; - } + @autoserialize + shortDescription: string; + + @autoserialize + description: string; + + @autoserialize + mimetype: string; + + @autoserialize + supportLevel: number; + + @autoserialize + internal: boolean; + + @autoserialize + extensions: string; } diff --git a/src/app/core/cache/models/normalized-bitstream.model.ts b/src/app/core/cache/models/normalized-bitstream.model.ts index ba5343e252..db8002a874 100644 --- a/src/app/core/cache/models/normalized-bitstream.model.ts +++ b/src/app/core/cache/models/normalized-bitstream.model.ts @@ -21,16 +21,11 @@ export class NormalizedBitstream extends NormalizedDSpaceObject { @autoserialize content: string; - /** - * The mime type of this Bitstream - */ - @autoserialize - mimetype: string; - /** * The format of this Bitstream */ @autoserialize + @relationship(ResourceType.BitstreamFormat, false) format: string; /** diff --git a/src/app/core/cache/models/normalized-object-factory.ts b/src/app/core/cache/models/normalized-object-factory.ts index e9f3b1c9e6..3c67b18b3e 100644 --- a/src/app/core/cache/models/normalized-object-factory.ts +++ b/src/app/core/cache/models/normalized-object-factory.ts @@ -15,11 +15,9 @@ export class NormalizedObjectFactory { case ResourceType.Bitstream: { return NormalizedBitstream } - // commented out for now, bitstreamformats aren't used in the UI yet - // and slow things down noticeably - // case ResourceType.BitstreamFormat: { - // return NormalizedBitstreamFormat - // } + case ResourceType.BitstreamFormat: { + return NormalizedBitstreamFormat + } case ResourceType.Bundle: { return NormalizedBundle } diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 9742a6b500..b782f1d4fc 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -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, diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index e48e7a8bb8..d1054d69ef 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -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 } public isEnabledOnRestApi(): Observable { - return this.getEndpointMap() + return this.getEndpointMap() .map((map: EndpointMap) => isNotEmpty(map[this.linkName])) .startWith(undefined) .distinctUntilChanged(); diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts new file mode 100644 index 0000000000..1258751f58 --- /dev/null +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -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: `` +}) +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; + + let objectCacheService: ObjectCacheService; + let responseCacheService: ResponseCacheService; + let requestService: RequestService; + let remoteDataBuildService: RemoteDataBuildService; + let itemDataService: ItemDataService; + + let location: Location; + let router: Router; + let fixture: ComponentFixture; + + let tagStore: Map; + + let envConfig: GlobalConfig; + + beforeEach(() => { + + store = new Store(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 => { + return new RemoteData( + 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; + } + +}); diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts new file mode 100644 index 0000000000..32b002e721 --- /dev/null +++ b/src/app/core/metadata/metadata.service.ts @@ -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; + + private currentObject: BehaviorSubject; + + 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(); + } + + 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): 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); + 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 to the + */ + private setTitleTag(): void { + const value = this.getMetaTagValue('dc.title'); + this.addMetaTag('title', value); + this.title.setTitle(value); + } + + /** + * Add to the + */ + private setDescriptionTag(): void { + // TODO: truncate abstract + const value = this.getMetaTagValue('dc.description.abstract'); + this.addMetaTag('desciption', value); + } + + /** + * Add to the + */ + private setCitationTitleTag(): void { + const value = this.getMetaTagValue('dc.title'); + this.addMetaTag('citation_title', value); + } + + /** + * Add to the + */ + private setCitationAuthorTags(): void { + const values: string[] = this.getMetaTagValues(['dc.author', 'dc.contributor.author', 'dc.creator']); + this.addMetaTags('citation_author', values); + } + + /** + * Add to the + */ + 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 to the + */ + private setCitationISSNTag(): void { + const value = this.getMetaTagValue('dc.identifier.issn'); + this.addMetaTag('citation_issn', value); + } + + /** + * Add to the + */ + private setCitationISBNTag(): void { + const value = this.getMetaTagValue('dc.identifier.isbn'); + this.addMetaTag('citation_isbn', value); + } + + /** + * Add to the + */ + private setCitationLanguageTag(): void { + const value = this.getFirstMetaTagValue(['dc.language', 'dc.language.iso']); + this.addMetaTag('citation_language', value); + } + + /** + * Add to the + */ + private setCitationDissertationNameTag(): void { + const value = this.getMetaTagValue('dc.title'); + this.addMetaTag('citation_dissertation_name', value); + } + + /** + * Add to the + */ + private setCitationDissertationInstitutionTag(): void { + const value = this.getMetaTagValue('dc.publisher'); + this.addMetaTag('citation_dissertation_institution', value); + } + + /** + * Add to the + */ + private setCitationTechReportInstitutionTag(): void { + const value = this.getMetaTagValue('dc.publisher'); + this.addMetaTag('citation_technical_report_institution', value); + } + + /** + * Add to the + */ + private setCitationKeywordsTag(): void { + const value = this.getMetaTagValuesAndCombine('dc.subject'); + this.addMetaTag('citation_keywords', value); + } + + /** + * Add to the + */ + 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 to the + */ + 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 { + return this.tagStore; + } + +} diff --git a/src/app/core/shared/bitstream-format.model.ts b/src/app/core/shared/bitstream-format.model.ts new file mode 100644 index 0000000000..c0f6be29c9 --- /dev/null +++ b/src/app/core/shared/bitstream-format.model.ts @@ -0,0 +1,17 @@ +import { DSpaceObject } from './dspace-object.model'; + +export class BitstreamFormat extends DSpaceObject { + + shortDescription: string; + + description: string; + + mimetype: string; + + supportLevel: number; + + internal: boolean; + + extensions: string; + +} diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index 5e4ee929d4..0b77a7b032 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -1,6 +1,7 @@ import { DSpaceObject } from './dspace-object.model'; import { RemoteData } from '../data/remote-data'; import { Item } from './item.model'; +import { BitstreamFormat } from './bitstream-format.model'; export class Bitstream extends DSpaceObject { @@ -9,11 +10,6 @@ export class Bitstream extends DSpaceObject { */ sizeBytes: number; - /** - * The mime type of this Bitstream - */ - mimetype: string; - /** * The description of this Bitstream */ @@ -24,6 +20,11 @@ export class Bitstream extends DSpaceObject { */ bundleName: string; + /** + * An array of Bitstream Format of this Bitstream + */ + format: RemoteData; + /** * An array of Items that are direct parents of this Bitstream */ diff --git a/src/app/footer/footer.component.spec.ts b/src/app/footer/footer.component.spec.ts index 6d56726764..dde432e1ef 100644 --- a/src/app/footer/footer.component.spec.ts +++ b/src/app/footer/footer.component.spec.ts @@ -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; diff --git a/src/app/shared/error/error.component.spec.ts b/src/app/shared/error/error.component.spec.ts index a0226f7f86..7335f93aed 100644 --- a/src/app/shared/error/error.component.spec.ts +++ b/src/app/shared/error/error.component.spec.ts @@ -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 })); diff --git a/src/app/shared/loading/loading.component.spec.ts b/src/app/shared/loading/loading.component.spec.ts index 0b758b8218..aca9673282 100644 --- a/src/app/shared/loading/loading.component.spec.ts +++ b/src/app/shared/loading/loading.component.spec.ts @@ -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 })); diff --git a/src/app/shared/mocks/mock-action.ts b/src/app/shared/mocks/mock-action.ts new file mode 100644 index 0000000000..0f619c8aff --- /dev/null +++ b/src/app/shared/mocks/mock-action.ts @@ -0,0 +1,6 @@ +import { Action } from '@ngrx/store'; + +export class MockAction implements Action { + type = null; + payload: {}; +} diff --git a/src/app/shared/mocks/mock-active-router.ts b/src/app/shared/mocks/mock-active-router.ts new file mode 100644 index 0000000000..391b9c3426 --- /dev/null +++ b/src/app/shared/mocks/mock-active-router.ts @@ -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 = 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 }; + } +} diff --git a/src/app/shared/mocks/mock-host-window-service.ts b/src/app/shared/mocks/mock-host-window-service.ts new file mode 100644 index 0000000000..104e712682 --- /dev/null +++ b/src/app/shared/mocks/mock-host-window-service.ts @@ -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 { + return Observable.of(this.width < 576); + } +} diff --git a/src/app/shared/mocks/mock-item.ts b/src/app/shared/mocks/mock-item.ts new file mode 100644 index 0000000000..0331491aa0 --- /dev/null +++ b/src/app/shared/mocks/mock-item.ts @@ -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 */ diff --git a/src/app/shared/mocks/mock-metadata-service.ts b/src/app/shared/mocks/mock-metadata-service.ts new file mode 100644 index 0000000000..8456e92b92 --- /dev/null +++ b/src/app/shared/mocks/mock-metadata-service.ts @@ -0,0 +1,9 @@ + +export class MockMetadataService { + + // tslint:disable-next-line:no-empty + public listenForRouteChange(): void { + + } + +} diff --git a/src/app/shared/mocks/mock-router.ts b/src/app/shared/mocks/mock-router.ts new file mode 100644 index 0000000000..054c63d4c0 --- /dev/null +++ b/src/app/shared/mocks/mock-router.ts @@ -0,0 +1,4 @@ +export class MockRouter { + // noinspection TypeScriptUnresolvedFunction + navigate = jasmine.createSpy('navigate'); +} diff --git a/src/app/shared/mocks/mock-store.ts b/src/app/shared/mocks/mock-store.ts new file mode 100644 index 0000000000..c619b5aa77 --- /dev/null +++ b/src/app/shared/mocks/mock-store.ts @@ -0,0 +1,23 @@ +import { Action } from '@ngrx/store'; +import { Observable } from 'rxjs/Observable'; +import { BehaviorSubject } from 'rxjs/BehaviorSubject'; + +export class MockStore extends BehaviorSubject { + + constructor(private _initialState: T) { + super(_initialState); + } + + dispatch = (action: Action): void => { + console.info(); + } + + select = (pathOrMapFn: any): Observable => { + return Observable.of(this.getValue()); + } + + nextState(_newState: T) { + this.next(_newState); + } + +} diff --git a/src/app/shared/mocks/mock-translate-loader.ts b/src/app/shared/mocks/mock-translate-loader.ts new file mode 100644 index 0000000000..6e22066f8a --- /dev/null +++ b/src/app/shared/mocks/mock-translate-loader.ts @@ -0,0 +1,8 @@ +import { TranslateLoader } from '@ngx-translate/core'; +import { Observable } from 'rxjs/Observable'; + +export class MockTranslateLoader implements TranslateLoader { + getTranslation(lang: string): Observable { + return Observable.of({}); + } +} diff --git a/src/app/shared/pagination/pagination.component.spec.ts b/src/app/shared/pagination/pagination.component.spec.ts index 92d7ba693f..a4b9e5fcea 100644 --- a/src/app/shared/pagination/pagination.component.spec.ts +++ b/src/app/shared/pagination/pagination.component.spec.ts @@ -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(html: string, type: { new (...args: any[]): T }): ComponentFixture { +function createTestComponent(html: string, type: { new(...args: any[]): T }): ComponentFixture { TestBed.overrideComponent(type, { set: { template: html } }); @@ -123,19 +123,19 @@ describe('Pagination component', () => { let testFixture: ComponentFixture; 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: [ diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index 99634543eb..5b6146b7a4 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -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 = [ diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.html b/src/app/shared/view-mode-switch/view-mode-switch.component.html new file mode 100644 index 0000000000..fb5e51a095 --- /dev/null +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.html @@ -0,0 +1,20 @@ + \ No newline at end of file diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.scss b/src/app/shared/view-mode-switch/view-mode-switch.component.scss new file mode 100644 index 0000000000..ad84b72f8c --- /dev/null +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.scss @@ -0,0 +1 @@ +@import '../../../styles/variables.scss'; \ No newline at end of file diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts new file mode 100644 index 0000000000..a8486d011d --- /dev/null +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts @@ -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; + 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'); + })); +}); diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.ts b/src/app/shared/view-mode-switch/view-mode-switch.component.ts new file mode 100644 index 0000000000..f6e04816fb --- /dev/null +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.ts @@ -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(); + } + } +} diff --git a/src/main.browser.ts b/src/main.browser.ts index 3f228b9d13..55e346108f 100644 --- a/src/main.browser.ts +++ b/src/main.browser.ts @@ -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); diff --git a/src/main.server.ts b/src/main.server.ts index f787f30514..aae5b89a62 100644 --- a/src/main.server.ts +++ b/src/main.server.ts @@ -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'; diff --git a/src/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts similarity index 76% rename from src/app/browser-app.module.ts rename to src/modules/app/browser-app.module.ts index 0904ba9ecc..aad1af9f4e 100644 --- a/src/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -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 () => { diff --git a/src/app/server-app.module.ts b/src/modules/app/server-app.module.ts similarity index 74% rename from src/app/server-app.module.ts rename to src/modules/app/server-app.module.ts index 0eea0cac24..d97c13a7bb 100644 --- a/src/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -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, request: Request, config: GlobalConfig) { // authentication mechanism goes here diff --git a/src/tsconfig.browser.json b/src/tsconfig.browser.json index a14de3972a..f7140b9fe4 100644 --- a/src/tsconfig.browser.json +++ b/src/tsconfig.browser.json @@ -1,6 +1,6 @@ { "extends": "../tsconfig.json", "angularCompilerOptions": { - "entryModule": "./app/browser-app.module#BrowserAppModule" + "entryModule": "./modules/app/browser-app.module#BrowserAppModule" } } diff --git a/src/tsconfig.server.json b/src/tsconfig.server.json index b86ab358f9..480b685a2a 100644 --- a/src/tsconfig.server.json +++ b/src/tsconfig.server.json @@ -1,6 +1,6 @@ { "extends": "../tsconfig.json", "angularCompilerOptions": { - "entryModule": "./app/server-app.module#ServerAppModule" + "entryModule": "./modules/app/server-app.module#ServerAppModule" } } diff --git a/src/tsconfig.test.json b/src/tsconfig.test.json index 32a466a3e9..712ad1ab1c 100644 --- a/src/tsconfig.test.json +++ b/src/tsconfig.test.json @@ -4,6 +4,6 @@ "sourceMap": true }, "angularCompilerOptions": { - "entryModule": "./app/browser-app.module#BrowserAppModule" + "entryModule": "./modules/app/browser-app.module#BrowserAppModule" } } diff --git a/webpack/webpack.test.js b/webpack/webpack.test.js index 818e348c7e..703da033a2 100644 --- a/webpack/webpack.test.js +++ b/webpack/webpack.test.js @@ -225,8 +225,7 @@ module.exports = function (options) { new ContextReplacementPlugin( /angular(\\|\/)core(\\|\/)@angular/, - root('./src'), - {} + root('./src'), {} ), /** diff --git a/yarn.lock b/yarn.lock index 054ae414d5..2fcafccd02 100644 --- a/yarn.lock +++ b/yarn.lock @@ -2,79 +2,79 @@ # yarn lockfile v1 -"@angular/animations@4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-4.4.4.tgz#a2f9353604347abe15df98292058842f52f08bc2" +"@angular/animations@4.4.5": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@angular/animations/-/animations-4.4.5.tgz#5a5a551d757e5a5560098f6f8535c102d93954d7" dependencies: tslib "^1.7.1" -"@angular/common@4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@angular/common/-/common-4.4.4.tgz#ae0a818aaa0c6a3f0901e7b80bd94e1c22eb9365" +"@angular/common@4.4.5": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@angular/common/-/common-4.4.5.tgz#bd5179dc922adbf4c3ea6dfb19e73cb849ffdc37" dependencies: tslib "^1.7.1" -"@angular/compiler-cli@4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-4.4.4.tgz#063080a497d9175396825050222c717da184f6cf" +"@angular/compiler-cli@4.4.5": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@angular/compiler-cli/-/compiler-cli-4.4.5.tgz#61fa0336acd1a208c5f1c5c6d4df679e99953248" dependencies: - "@angular/tsc-wrapped" "4.4.4" + "@angular/tsc-wrapped" "4.4.5" minimist "^1.2.0" reflect-metadata "^0.1.2" -"@angular/compiler@4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-4.4.4.tgz#326eb0029d9a3541aaca124def9adc51c36f2b41" +"@angular/compiler@4.4.5": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@angular/compiler/-/compiler-4.4.5.tgz#8721a5910f2bb52f09e2d404cad264f35ede5902" dependencies: tslib "^1.7.1" -"@angular/core@4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@angular/core/-/core-4.4.4.tgz#bd37ecf54158f97489996c9386bd222f80a32f5c" +"@angular/core@4.4.5": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@angular/core/-/core-4.4.5.tgz#54acbcbda11719f883c786a906974abeb132f1a0" dependencies: tslib "^1.7.1" -"@angular/forms@4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-4.4.4.tgz#4db3790509b6b10f1db8a7c1b7f52187cf64cfd4" +"@angular/forms@4.4.5": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@angular/forms/-/forms-4.4.5.tgz#e9552086232aab2ce1d08ef198b62204ea13c43b" dependencies: tslib "^1.7.1" -"@angular/http@4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@angular/http/-/http-4.4.4.tgz#667faf616bb624168eafae6ee92e5eba23a9d1f2" +"@angular/http@4.4.5": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@angular/http/-/http-4.4.5.tgz#2c735ed842401fc2356419268e288dcf2396e84f" dependencies: tslib "^1.7.1" -"@angular/platform-browser-dynamic@4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-4.4.4.tgz#c3c9eb854a528556a07054127932e527fa932e14" +"@angular/platform-browser-dynamic@4.4.5": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@angular/platform-browser-dynamic/-/platform-browser-dynamic-4.4.5.tgz#774dbdc1d90f775dbf1e319f6ed42b260623b61f" dependencies: tslib "^1.7.1" -"@angular/platform-browser@4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-4.4.4.tgz#a3898e2e7ba9d84ffa0d47144c6971179c75aee6" +"@angular/platform-browser@4.4.5": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@angular/platform-browser/-/platform-browser-4.4.5.tgz#74eb91c0b758126f26d53ee56c7cf4668bd9cac5" dependencies: tslib "^1.7.1" -"@angular/platform-server@4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-4.4.4.tgz#73ee41fa1cec8628fcc03174727b27cb0031b22a" +"@angular/platform-server@4.4.5": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@angular/platform-server/-/platform-server-4.4.5.tgz#76f23b2c384ed7395dc1793cf85978883ba2cb50" dependencies: parse5 "^3.0.1" tslib "^1.7.1" xhr2 "^0.1.4" -"@angular/router@4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@angular/router/-/router-4.4.4.tgz#7be391096e843cb3e04f9f05d1d65a88df9bc7cf" +"@angular/router@4.4.5": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@angular/router/-/router-4.4.5.tgz#f73130cf487d9a32cc1988afda59665f44a28a89" dependencies: tslib "^1.7.1" -"@angular/tsc-wrapped@4.4.4": - version "4.4.4" - resolved "https://registry.yarnpkg.com/@angular/tsc-wrapped/-/tsc-wrapped-4.4.4.tgz#9841821e55616b826ca160250fe85e15fc74ffc3" +"@angular/tsc-wrapped@4.4.5": + version "4.4.5" + resolved "https://registry.yarnpkg.com/@angular/tsc-wrapped/-/tsc-wrapped-4.4.5.tgz#30a0cbb43a663aa75dca984894be4813778ddc9c" dependencies: tsickle "^0.21.0" @@ -106,9 +106,9 @@ version "4.0.3" resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-4.0.3.tgz#36abacdfa19bfb8506e40de80bae06050a1e15e9" -"@ngtools/webpack@1.7.2": - version "1.7.2" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-1.7.2.tgz#3fc4de01786dcc2f50d8cbaaa117311e56799977" +"@ngtools/webpack@1.7.4": + version "1.7.4" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-1.7.4.tgz#5015c47ebd339045dd89a1bef0497f4524d2c8ed" dependencies: enhanced-resolve "^3.1.0" loader-utils "^1.0.2" @@ -150,42 +150,35 @@ "@types/express-serve-static-core" "*" "@types/serve-static" "*" -"@types/fs-extra@^4.0.0": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.2.tgz#7b9b1bbf85962cbe029b5a83c9b530d7c75af3ba" +"@types/fs-extra@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.0.tgz#1dd742ad5c9bce308f7a52d02ebc01421bc9102f" dependencies: "@types/node" "*" -"@types/glob@*": - version "5.0.32" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.32.tgz#aec5cfe987c72f099fdb1184452986aa506d5e8f" - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - "@types/hammerjs@2.0.35": version "2.0.35" resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.35.tgz#7b7c950c7d54593e23bffc8d2b4feba9866a7277" -"@types/handlebars@^4.0.31": - version "4.0.36" - resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.36.tgz#ff57c77fa1ab6713bb446534ddc4d979707a3a79" +"@types/handlebars@4.0.31": + version "4.0.31" + resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.31.tgz#a7fba66fafe42713aee88eeca8db91192efe6e72" -"@types/highlight.js@^9.1.8": - version "9.1.10" - resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.1.10.tgz#b621f809cd9573b80992b90cffc5788208e3069c" +"@types/highlight.js@9.1.8": + version "9.1.8" + resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.1.8.tgz#d227f18bcb8f3f187e16965f2444859a04689758" "@types/jasmine@2.6.0": version "2.6.0" resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.6.0.tgz#997b41a27752b4850af2683bc4a8d8222c25bd02" -"@types/lodash@^4.14.37": - version "4.14.76" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.76.tgz#87874f766774d54e89589697340be9496fb8bf70" +"@types/lodash@4.14.74": + version "4.14.74" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.74.tgz#ac3bd8db988e7f7038e5d22bd76a7ba13f876168" -"@types/marked@0.0.28": - version "0.0.28" - resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.0.28.tgz#44ba754e9fa51432583e8eb30a7c4dd249b52faa" +"@types/marked@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.3.0.tgz#583c223dd33385a1dda01aaf77b0cd0411c4b524" "@types/memory-cache@0.0.31": version "0.0.31" @@ -195,13 +188,13 @@ version "2.0.0" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" -"@types/minimatch@*", "@types/minimatch@^2.0.29": +"@types/minimatch@2.0.29": version "2.0.29" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-2.0.29.tgz#5002e14f75e2d71e564281df0431c8c1b4a2a36a" -"@types/node@*", "@types/node@8.0.26": - version "8.0.26" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.26.tgz#4d58be925306fd22b1141085535a0268b8beb189" +"@types/node@*", "@types/node@8.0.34": + version "8.0.34" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.34.tgz#55f801fa2ddb2a40dd6dfc15ecfe1dde9c129fe9" "@types/node@^6.0.46": version "6.0.88" @@ -222,11 +215,10 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" -"@types/shelljs@^0.7.0": - version "0.7.4" - resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.4.tgz#137b5f31306eaff4de120ffe5b9d74b297809cfc" +"@types/shelljs@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.0.tgz#229c157c6bc1e67d6b990e6c5e18dbd2ff58cff0" dependencies: - "@types/glob" "*" "@types/node" "*" "@types/source-map@0.5.1": @@ -576,15 +568,15 @@ atob@~1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/atob/-/atob-1.1.3.tgz#95f13629b12c3a51a5d215abdce2aa9f32f80773" -autoprefixer@7.1.4, autoprefixer@^7.1.1: - version "7.1.4" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.4.tgz#960847dbaa4016bc8e8e52ec891cbf8f1257a748" +autoprefixer@7.1.5, autoprefixer@^7.1.1: + version "7.1.5" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.5.tgz#d65d14b83c7cd1dd7bc801daa00557addf5a06b2" dependencies: - browserslist "^2.4.0" - caniuse-lite "^1.0.30000726" + browserslist "^2.5.0" + caniuse-lite "^1.0.30000744" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^6.0.11" + postcss "^6.0.13" postcss-value-parser "^3.2.3" autoprefixer@^6.3.1: @@ -790,13 +782,9 @@ blocking-proxy@0.0.5: dependencies: minimist "^1.2.0" -bluebird@^2.10.2: - version "2.11.0" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" - -bluebird@^3.3.0, bluebird@^3.4.7: - version "3.5.0" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" +bluebird@^3.3.0, bluebird@^3.4.7, bluebird@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" @@ -972,12 +960,12 @@ browserslist@^1.3.6, browserslist@^1.5.2, browserslist@^1.7.6: caniuse-db "^1.0.30000639" electron-to-chromium "^1.2.7" -browserslist@^2.0.0, browserslist@^2.4.0: - version "2.4.0" - resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.4.0.tgz#693ee93d01e66468a6348da5498e011f578f87f8" +browserslist@^2.0.0, browserslist@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.5.1.tgz#68e4bc536bbcc6086d62843a2ffccea8396821c6" dependencies: - caniuse-lite "^1.0.30000718" - electron-to-chromium "^1.3.18" + caniuse-lite "^1.0.30000744" + electron-to-chromium "^1.3.24" buffer-crc32@^0.2.1: version "0.2.13" @@ -1089,9 +1077,9 @@ caniuse-lite@1.0.30000697: version "1.0.30000697" resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000697.tgz#125fb00604b63fbb188db96a667ce2922dcd6cdd" -caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000718, caniuse-lite@^1.0.30000726: - version "1.0.30000740" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000740.tgz#f2c4c04d6564eb812e61006841700ad557f6f973" +caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000744: + version "1.0.30000745" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000745.tgz#20d6fede1157a4935133502946fc7e0e6b880da5" capture-stack-trace@^1.0.0: version "1.0.0" @@ -1112,11 +1100,11 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -cerialize@0.1.16: - version "0.1.16" - resolved "https://registry.yarnpkg.com/cerialize/-/cerialize-0.1.16.tgz#88678bffbd7817a90aa5b58a8c66d6bdca3035be" +cerialize@0.1.18: + version "0.1.18" + resolved "https://registry.yarnpkg.com/cerialize/-/cerialize-0.1.18.tgz#d0f4f1b61cec7e4ed16a3eda0cac2bc99787414d" dependencies: - typescript "^2.1.6" + typescript "^2.5.0" chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" @@ -1245,9 +1233,9 @@ code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" -codelyzer@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/codelyzer/-/codelyzer-3.2.0.tgz#68eb0a67771ea73006b517053c3035c1838abf14" +codelyzer@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/codelyzer/-/codelyzer-3.2.1.tgz#5b1ac75f7e0eb04647842ee29a322bf2167e7229" dependencies: app-root-path "^2.0.1" css-selector-tokenizer "^0.7.0" @@ -1472,17 +1460,17 @@ copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" -copy-webpack-plugin@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.1.0.tgz#292a040318fe8ae3b1d7996ef05dfb483eb0b647" +copy-webpack-plugin@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.1.1.tgz#53ae69e04955ebfa9fda411f54cbb968531d71fd" dependencies: - bluebird "^2.10.2" - fs-extra "^0.26.4" - glob "^6.0.4" - is-glob "^3.1.0" + bluebird "^3.5.1" + fs-extra "^4.0.2" + glob "^7.1.2" + is-glob "^4.0.0" loader-utils "^0.2.15" lodash "^4.3.0" - minimatch "^3.0.0" + minimatch "^3.0.4" node-dir "^0.1.10" core-js@2.5.1, core-js@^2.2.0, core-js@^2.4.0: @@ -2028,7 +2016,7 @@ ejs@^2.5.6: version "2.5.7" resolved "https://registry.yarnpkg.com/ejs/-/ejs-2.5.7.tgz#cc872c168880ae3c7189762fd5ffc00896c9518a" -electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.18: +electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.24: version "1.3.24" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.24.tgz#9b7b88bb05ceb9fa016a177833cc2dde388f21b6" @@ -2400,9 +2388,9 @@ express-session@1.15.6: uid-safe "~2.1.5" utils-merge "1.0.1" -express@4.16.1, express@^4.13.3, express@^4.15.2: - version "4.16.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.1.tgz#6b33b560183c9b253b7b62144df33a4654ac9ed0" +express@4.16.2, express@^4.13.3, express@^4.15.2: + version "4.16.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" dependencies: accepts "~1.3.4" array-flatten "1.1.1" @@ -2666,17 +2654,7 @@ fs-extra@^0.22.1: jsonfile "^2.1.0" rimraf "^2.2.8" -fs-extra@^0.26.4: - version "0.26.7" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.26.7.tgz#9ae1fdd94897798edab76d0918cf42d0c3184fa9" - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" - path-is-absolute "^1.0.0" - rimraf "^2.2.8" - -fs-extra@^4.0.0, fs-extra@^4.0.1: +fs-extra@^4.0.0, fs-extra@^4.0.1, fs-extra@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.2.tgz#f91704c53d1b461f893452b0c307d9997647ab6b" dependencies: @@ -2802,17 +2780,7 @@ glob@^5.0.15: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@~7.1.1: +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -3430,7 +3398,7 @@ is-extglob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" -is-extglob@^2.1.0: +is-extglob@^2.1.0, is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -3462,6 +3430,12 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" +is-glob@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + dependencies: + is-extglob "^2.1.1" + is-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" @@ -3665,11 +3639,11 @@ jasmine-core@2.8.0, jasmine-core@~2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" -jasmine-marbles@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/jasmine-marbles/-/jasmine-marbles-0.1.0.tgz#c9ecdc64e20b6cf55b49a10201a5be33907dadcc" +jasmine-marbles@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/jasmine-marbles/-/jasmine-marbles-0.2.0.tgz#b893d8508b75790b634876d3a1bea1345d65c156" dependencies: - lodash.isequal "^4.5.0" + lodash "^4.5.0" jasmine-spec-reporter@4.2.1: version "4.2.1" @@ -3867,9 +3841,9 @@ karma-webdriver-launcher@1.0.5: dependencies: wd "^1.0.0" -karma-webpack@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.4.tgz#3e2d4f48ba94a878e1c66bb8e1ae6128987a175b" +karma-webpack@2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.5.tgz#4f56887e32cf4f9583391c2388415de06af06efd" dependencies: async "~0.9.0" loader-utils "^0.2.5" @@ -4131,10 +4105,6 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - lodash.keys@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" @@ -5133,9 +5103,9 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" -pem@1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/pem/-/pem-1.12.0.tgz#603d8207b9b5f83225e37ffcc36268c3aa3fecf3" +pem@1.12.3: + version "1.12.3" + resolved "https://registry.yarnpkg.com/pem/-/pem-1.12.3.tgz#b1fb5c8b79da8d18146c27fee79b0d4ddf9905b3" dependencies: md5 "^2.2.1" os-tmpdir "^1.0.1" @@ -5488,12 +5458,12 @@ postcss-load-plugins@^2.3.0: cosmiconfig "^2.1.1" object-assign "^4.1.0" -postcss-loader@2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.6.tgz#8c7e0055a3df1889abc6bad52dd45b2f41bbc6fc" +postcss-loader@2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.7.tgz#4d2da1489cee0a14f72c0d9440c9ee7eded34345" dependencies: loader-utils "^1.1.0" - postcss "^6.0.2" + postcss "^6.0.0" postcss-load-config "^1.2.0" schema-utils "^0.3.0" @@ -5759,7 +5729,7 @@ postcss-zindex@^2.0.1: postcss "^5.0.4" uniqs "^2.0.0" -postcss@6.0.13: +postcss@6.0.13, postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.11, postcss@^6.0.13, postcss@^6.0.3, postcss@^6.0.5, postcss@^6.0.6, postcss@^6.0.8: version "6.0.13" resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.13.tgz#b9ecab4ee00c89db3ec931145bd9590bbf3f125f" dependencies: @@ -5776,14 +5746,6 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0 source-map "^0.5.6" supports-color "^3.2.3" -postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.11, postcss@^6.0.2, postcss@^6.0.3, postcss@^6.0.5, postcss@^6.0.6, postcss@^6.0.8: - version "6.0.12" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.12.tgz#6b0155089d2d212f7bd6a0cecd4c58c007403535" - dependencies: - chalk "^2.1.0" - source-map "^0.5.7" - supports-color "^4.4.0" - prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -6332,9 +6294,9 @@ requires-port@1.0.x, requires-port@1.x.x: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" -resolve-url-loader@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-2.1.0.tgz#27c95cc16a4353923fdbdc2dbaf5eef22232c477" +resolve-url-loader@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-2.1.1.tgz#5354e87381aae348371e555172c50816708e6c1c" dependencies: adjust-sourcemap-loader "^1.1.0" camelcase "^4.0.0" @@ -6873,10 +6835,14 @@ source-map-url@~0.3.0: version "0.3.0" resolved "https://registry.yarnpkg.com/source-map-url/-/source-map-url-0.3.0.tgz#7ecaf13b57bcd09da8a40c5d269db33799d4aaf9" -source-map@0.5.x, source-map@>=0.5.6, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@^0.5.7, source-map@~0.5.1, source-map@~0.5.3: +source-map@0.5.x, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, source-map@~0.5.1, source-map@~0.5.3: version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" +source-map@>=0.5.6, source-map@^0.6.1: + version "0.6.1" + resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" + source-map@^0.1.38, source-map@^0.1.41, source-map@~0.1.33: version "0.1.43" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.1.43.tgz#c24bc146ca517c1471f5dacbe2571b2b7f9e3346" @@ -6889,10 +6855,6 @@ source-map@^0.4.2, source-map@^0.4.4: dependencies: amdefine ">=0.0.4" -source-map@^0.6.1: - version "0.6.1" - resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" - source-map@~0.2.0: version "0.2.0" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.2.0.tgz#dab73fbcfc2ba819b4de03bd6f6eaa48164b3f9d" @@ -6958,11 +6920,7 @@ split@0.3: dependencies: through "2" -sprintf-js@^1.0.3: - version "1.1.1" - resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.1.1.tgz#36be78320afe5801f6cea3ee78b6e5aab940ea0c" - -sprintf-js@~1.0.2: +sprintf-js@^1.0.3, sprintf-js@~1.0.2: version "1.0.3" resolved "https://registry.yarnpkg.com/sprintf-js/-/sprintf-js-1.0.3.tgz#04e6926f662895354f3dd015203633b857297e2c" @@ -7409,17 +7367,17 @@ typedoc-default-themes@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.5.0.tgz#6dc2433e78ed8bea8e887a3acde2f31785bd6227" -typedoc@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.8.0.tgz#d7172bc6a29964f451b7609c005beadadefe2361" +typedoc@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.9.0.tgz#159bff7c7784ce5b91d86f3e4cc8928e62040957" dependencies: - "@types/fs-extra" "^4.0.0" - "@types/handlebars" "^4.0.31" - "@types/highlight.js" "^9.1.8" - "@types/lodash" "^4.14.37" - "@types/marked" "0.0.28" - "@types/minimatch" "^2.0.29" - "@types/shelljs" "^0.7.0" + "@types/fs-extra" "4.0.0" + "@types/handlebars" "4.0.31" + "@types/highlight.js" "9.1.8" + "@types/lodash" "4.14.74" + "@types/marked" "0.3.0" + "@types/minimatch" "2.0.29" + "@types/shelljs" "0.7.0" fs-extra "^4.0.0" handlebars "^4.0.6" highlight.js "^9.0.0" @@ -7435,7 +7393,7 @@ typescript@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.1.tgz#c3ccb16ddaa0b2314de031e7e6fee89e5ba346bc" -typescript@2.5.3, typescript@^2.1.6: +typescript@2.5.3, typescript@^2.5.0: version "2.5.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.3.tgz#df3dcdc38f3beb800d4bc322646b04a3f6ca7f0d" @@ -7843,9 +7801,9 @@ webpack-sources@^1.0.1: source-list-map "^2.0.0" source-map "~0.5.3" -webpack@3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.6.0.tgz#a89a929fbee205d35a4fa2cc487be9cbec8898bc" +webpack@3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.7.1.tgz#6046b5c415ff7df7a0dc54c5b6b86098e8b952da" dependencies: acorn "^5.0.0" acorn-dynamic-import "^2.0.0"