Merge branch 'master' into improve-support-for-hal-links

This commit is contained in:
Art Lowel
2017-10-12 13:14:43 +02:00
82 changed files with 1491 additions and 457 deletions

View File

@@ -0,0 +1,53 @@
import { ProtractorPage } from './search-page.po';
import { browser } from 'protractor';
import { promise } from 'selenium-webdriver';
describe('protractor SearchPage', () => {
let page: ProtractorPage;
beforeEach(() => {
page = new ProtractorPage();
});
it('should contain query value when navigating to page with query parameter', () => {
const queryString = 'Interesting query string';
page.navigateToSearchWithQueryParameter(queryString);
page.getCurrentQuery().then((query: string) => {
expect<string>(query).toEqual(queryString);
});
});
it('should have right scope selected when navigating to page with scope parameter', () => {
const scope: promise.Promise<string> = page.getRandomScopeOption();
scope.then((scopeString: string) => {
page.navigateToSearchWithScopeParameter(scopeString);
page.getCurrentScope().then((s: string) => {
expect<string>(s).toEqual(scopeString);
});
});
});
it('should redirect to the correct url when scope was set and submit button was triggered', () => {
const scope: promise.Promise<string> = page.getRandomScopeOption();
scope.then((scopeString: string) => {
page.setCurrentScope(scopeString);
page.submitSearchForm();
browser.wait(() => {
return browser.getCurrentUrl().then((url: string) => {
return url.indexOf('scope=' + encodeURI(scopeString)) !== -1;
});
});
});
});
it('should redirect to the correct url when query was set and submit button was triggered', () => {
const queryString = 'Another interesting query string';
page.setCurrentQuery(queryString);
page.submitSearchForm();
browser.wait(() => {
return browser.getCurrentUrl().then((url: string) => {
return url.indexOf('query=' + encodeURI(queryString)) !== -1;
});
});
});
});

View File

@@ -0,0 +1,47 @@
import { browser, element, by } from 'protractor';
import { promise } from 'selenium-webdriver';
export class ProtractorPage {
SEARCH = '/search';
navigateToSearch() {
return browser.get(this.SEARCH);
}
navigateToSearchWithQueryParameter(query: string) {
return browser.get(this.SEARCH + '?query=' + query);
}
navigateToSearchWithScopeParameter(scope: string) {
return browser.get(this.SEARCH + '?scope=' + scope);
}
getCurrentScope(): promise.Promise<string> {
return element(by.tagName('select')).getAttribute('value');
}
getCurrentQuery(): promise.Promise<string> {
return element(by.tagName('input')).getAttribute('value');
}
setCurrentScope(scope: string) {
element(by.css('option[value="' + scope + '"]')).click();
}
setCurrentQuery(query: string) {
element(by.css('input[name="query"]')).sendKeys(query);
}
submitSearchForm() {
element(by.css('button.search-button')).click();
}
getRandomScopeOption(): promise.Promise<string> {
const options = element(by.css('select[name="scope"]')).all(by.tagName('option'));
return options.count().then((c: number) => {
const index: number = Math.floor(Math.random() * (c - 1));
return options.get(index + 1).getAttribute('value');
});
}
}

View File

@@ -27,18 +27,18 @@ module.exports = function (config) {
frameworks: ['jasmine'], frameworks: ['jasmine'],
plugins: [ plugins: [
require('karma-webpack'), require("istanbul-instrumenter-loader"),
require('karma-jasmine'),
require('karma-chrome-launcher'), require('karma-chrome-launcher'),
require('karma-phantomjs-launcher'),
require('karma-webdriver-launcher'),
require('karma-coverage'), require('karma-coverage'),
require('karma-remap-coverage'), require("karma-istanbul-preprocessor"),
require('karma-jasmine'),
require('karma-mocha-reporter'), require('karma-mocha-reporter'),
require('karma-phantomjs-launcher'),
require('karma-remap-coverage'),
require('karma-remap-istanbul'), require('karma-remap-istanbul'),
require('karma-sourcemap-loader'), require('karma-sourcemap-loader'),
require("istanbul-instrumenter-loader"), require('karma-webdriver-launcher'),
require("karma-istanbul-preprocessor") require('karma-webpack')
], ],
// list of files to exclude // list of files to exclude
@@ -59,7 +59,11 @@ module.exports = function (config) {
* available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor * available preprocessors: https://npmjs.org/browse/keyword/karma-preprocessor
*/ */
preprocessors: { preprocessors: {
'./spec-bundle.js': ['istanbul', 'webpack', 'sourcemap'] './spec-bundle.js': [
'istanbul',
'webpack',
'sourcemap'
]
}, },
// Webpack Config at ./webpack.test.js // Webpack Config at ./webpack.test.js
@@ -79,9 +83,9 @@ module.exports = function (config) {
remapIstanbulReporter: { remapIstanbulReporter: {
remapOptions: {}, //additional remap options remapOptions: {}, //additional remap options
reports: { reports: {
json: 'coverage/coverage.json', json: './coverage/coverage.json',
lcovonly: 'coverage/lcov.info', lcovonly: './coverage/lcov.info',
html: 'coverage/html/', html: './coverage/html/',
} }
}, },
@@ -111,7 +115,12 @@ module.exports = function (config) {
* possible values: 'dots', 'progress' * possible values: 'dots', 'progress'
* available reporters: https://npmjs.org/browse/keyword/karma-reporter * 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 // Karma web server port
port: 9876, port: 9876,

View File

@@ -12,14 +12,15 @@
}, },
"scripts": { "scripts": {
"global": "npm install -g @angular/cli marked node-gyp nodemon node-nightly npm-check-updates npm-run-all rimraf typescript ts-node typedoc webpack webpack-bundle-analyzer pm2 rollup", "global": "npm install -g @angular/cli marked node-gyp nodemon node-nightly npm-check-updates npm-run-all rimraf typescript ts-node typedoc webpack webpack-bundle-analyzer pm2 rollup",
"clean:coverage": "rimraf coverage", "clean:coverage": "yarn run rimraf coverage",
"clean:dist": "rimraf dist", "clean:dist": "yarn run rimraf dist",
"clean:doc": "rimraf doc", "clean:doc": "yarn run rimraf doc",
"clean:log": "rimraf *.log*", "clean:log": "yarn run rimraf *.log*",
"clean:json": "rimraf *.records.json", "clean:json": "yarn run rimraf *.records.json",
"clean:node": "rimraf node_modules", "clean:node": "yarn run rimraf node_modules",
"clean:prod": "yarn run clean:coverage && yarn run clean:doc && yarn run clean:dist && yarn run clean:log && yarn run clean:json", "clean:prod": "yarn run clean:coverage && yarn run clean:doc && yarn run clean:dist && yarn run clean:log && yarn run clean:json",
"clean": "yarn run clean:prod && yarn run clean:node", "clean": "yarn run clean:prod && yarn run clean:node",
"rimraf": "rimraf",
"prebuild": "yarn run clean:dist", "prebuild": "yarn run clean:dist",
"prebuild:aot": "yarn run prebuild", "prebuild:aot": "yarn run prebuild",
"prebuild:prod": "yarn run prebuild", "prebuild:prod": "yarn run prebuild",
@@ -113,7 +114,7 @@
"@angular/compiler": "4.4.4", "@angular/compiler": "4.4.4",
"@angular/compiler-cli": "4.4.4", "@angular/compiler-cli": "4.4.4",
"@ngrx/store-devtools": "4.0.0", "@ngrx/store-devtools": "4.0.0",
"@ngtools/webpack": "1.7.3", "@ngtools/webpack": "1.7.2",
"@types/cookie-parser": "1.4.1", "@types/cookie-parser": "1.4.1",
"@types/deep-freeze": "0.1.1", "@types/deep-freeze": "0.1.1",
"@types/express": "4.0.37", "@types/express": "4.0.37",

View File

@@ -78,5 +78,25 @@
"results": { "results": {
"title": "Search Results" "title": "Search Results"
} }
},
"loading": {
"default": "Loading...",
"top-level-communities": "Loading top level communities...",
"community": "Loading community...",
"collection": "Loading collection...",
"sub-collections": "Loading sub-collections...",
"items": "Loading items...",
"item": "Loading item...",
"objects": "Loading..."
},
"error": {
"default": "Error",
"top-level-communities": "Error fetching top level communities",
"community": "Error fetching community",
"collection": "Error fetching collection",
"sub-collections": "Error fetching sub-collections",
"items": "Error fetching items",
"item": "Error fetching item",
"objects": "Error fetching objects"
} }
} }

View File

@@ -3,10 +3,12 @@ import commonjs from 'rollup-plugin-commonjs';
import uglify from 'rollup-plugin-uglify' import uglify from 'rollup-plugin-uglify'
export default { export default {
entry: 'dist/client.js', input: 'dist/client.js',
dest: 'dist/client.js', output: {
sourceMap: false, file: 'dist/client.js',
format: 'iife', format: 'iife',
},
sourceMap: false,
plugins: [ plugins: [
nodeResolve({ nodeResolve({
jsnext: true, jsnext: true,

View File

@@ -38,25 +38,11 @@ testing.TestBed.initTestEnvironment(
browser.platformBrowserDynamicTesting() browser.platformBrowserDynamicTesting()
); );
/* var tests = require.context('./src', true, /\.spec\.ts$/);
* 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/);
/* tests.keys().forEach(tests);
* 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);
}
// requires and returns all modules that match // includes all modules into test coverage
var modules = requireAll(testContext); const modules = require.context('./src/app', true, /\.module\.ts$/);
modules.keys().forEach(modules);

View File

@@ -1,8 +1,9 @@
<div class="collection-page"> <div class="collection-page">
<div *ngIf="collectionData.hasSucceeded | async"> <div *ngIf="collectionData.hasSucceeded | async" @fadeInOut>
<div *ngIf="collectionData.payload | async; let collectionPayload">
<!-- Collection Name --> <!-- Collection Name -->
<ds-comcol-page-header <ds-comcol-page-header
[name]="(collectionData.payload | async)?.name"> [name]="collectionPayload.name">
</ds-comcol-page-header> </ds-comcol-page-header>
<!-- Collection logo --> <!-- Collection logo -->
<ds-comcol-page-logo *ngIf="logoData" <ds-comcol-page-logo *ngIf="logoData"
@@ -11,30 +12,39 @@
</ds-comcol-page-logo> </ds-comcol-page-logo>
<!-- Introductionary text --> <!-- Introductionary text -->
<ds-comcol-page-content <ds-comcol-page-content
[content]="(collectionData.payload | async)?.introductoryText" [content]="collectionPayload.introductoryText"
[hasInnerHtml]="true"> [hasInnerHtml]="true">
</ds-comcol-page-content> </ds-comcol-page-content>
<!-- News --> <!-- News -->
<ds-comcol-page-content <ds-comcol-page-content
[content]="(collectionData.payload | async)?.sidebarText" [content]="collectionPayload.sidebarText"
[hasInnerHtml]="true" [hasInnerHtml]="true"
[title]="'community.page.news'"> [title]="'community.page.news'">
</ds-comcol-page-content> </ds-comcol-page-content>
<!-- Copyright --> <!-- Copyright -->
<ds-comcol-page-content <ds-comcol-page-content
[content]="(collectionData.payload | async)?.copyrightText" [content]="collectionPayload.copyrightText"
[hasInnerHtml]="true"> [hasInnerHtml]="true">
</ds-comcol-page-content> </ds-comcol-page-content>
<!-- Licence --> <!-- Licence -->
<ds-comcol-page-content <ds-comcol-page-content
[content]="(collectionData.payload | async)?.license" [content]="collectionPayload.license"
[title]="'collection.page.license'"> [title]="'collection.page.license'">
</ds-comcol-page-content> </ds-comcol-page-content>
</div> </div>
<br>
<div *ngIf="(itemData.hasSucceeded | async)">
<h2>{{'collection.page.browse.recent.head' | translate}}</h2>
<ds-object-list [config]="paginationConfig" [sortConfig]="sortConfig"
[objects]="itemData" [hideGear]="false"></ds-object-list>
</div> </div>
<ds-error *ngIf="collectionData.hasFailed | async" message="{{'error.collection' | translate}}"></ds-error>
<ds-loading *ngIf="collectionData.isLoading | async" message="{{'loading.collection' | translate}}"></ds-loading>
<br>
<div *ngIf="itemData.hasSucceeded | async" @fadeIn>
<h2>{{'collection.page.browse.recent.head' | translate}}</h2>
<ds-object-list
[config]="paginationConfig"
[sortConfig]="sortConfig"
[objects]="itemData"
[hideGear]="false">
</ds-object-list>
</div>
<ds-error *ngIf="itemData.hasFailed | async" message="{{'error.items' | translate}}"></ds-error>
<ds-loading *ngIf="itemData.isLoading | async" message="{{'loading.items' | translate}}"></ds-loading>
</div> </div>

View File

@@ -1,28 +1,30 @@
import { import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
ChangeDetectionStrategy, ChangeDetectorRef, Component, OnDestroy, import { ActivatedRoute } from '@angular/router';
OnInit import { Observable } from 'rxjs/Observable';
} from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
import { SortOptions } from '../core/cache/models/sort-options.model';
import { Collection } from '../core/shared/collection.model';
import { Bitstream } from '../core/shared/bitstream.model';
import { RemoteData } from '../core/data/remote-data';
import { CollectionDataService } from '../core/data/collection-data.service'; import { CollectionDataService } from '../core/data/collection-data.service';
import { ItemDataService } from '../core/data/item-data.service'; import { ItemDataService } from '../core/data/item-data.service';
import { RemoteData } from '../core/data/remote-data';
import { Bitstream } from '../core/shared/bitstream.model';
import { Collection } from '../core/shared/collection.model';
import { Item } from '../core/shared/item.model'; import { Item } from '../core/shared/item.model';
import { SortOptions, SortDirection } from '../core/cache/models/sort-options.model';
import { fadeIn, fadeInOut } from '../shared/animations/fade';
import { hasValue, isNotEmpty } from '../shared/empty.util';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-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';
@Component({ @Component({
selector: 'ds-collection-page', selector: 'ds-collection-page',
styleUrls: ['./collection-page.component.scss'], styleUrls: ['./collection-page.component.scss'],
templateUrl: './collection-page.component.html', templateUrl: './collection-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
animations: [
fadeIn,
fadeInOut
]
}) })
export class CollectionPageComponent implements OnInit, OnDestroy { export class CollectionPageComponent implements OnInit, OnDestroy {
collectionData: RemoteData<Collection>; collectionData: RemoteData<Collection>;

View File

@@ -1,6 +1,7 @@
<div class="community-page" *ngIf="communityData.hasSucceeded | async"> <div class="community-page" *ngIf="communityData.hasSucceeded | async" @fadeInOut>
<div *ngIf="communityData.payload | async; let communityPayload">
<!-- Community name --> <!-- Community name -->
<ds-comcol-page-header [name]="(communityData.payload | async)?.name"></ds-comcol-page-header> <ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
<!-- Community logo --> <!-- Community logo -->
<ds-comcol-page-logo *ngIf="logoData" <ds-comcol-page-logo *ngIf="logoData"
[logo]="logoData.payload | async" [logo]="logoData.payload | async"
@@ -8,19 +9,22 @@
</ds-comcol-page-logo> </ds-comcol-page-logo>
<!-- Introductionary text --> <!-- Introductionary text -->
<ds-comcol-page-content <ds-comcol-page-content
[content]="(communityData.payload | async)?.introductoryText" [content]="communityPayload.introductoryText"
[hasInnerHtml]="true"> [hasInnerHtml]="true">
</ds-comcol-page-content> </ds-comcol-page-content>
<!-- News --> <!-- News -->
<ds-comcol-page-content <ds-comcol-page-content
[content]="(communityData.payload | async)?.sidebarText" [content]="communityPayload.sidebarText"
[hasInnerHtml]="true" [hasInnerHtml]="true"
[title]="'community.page.news'"> [title]="'community.page.news'">
</ds-comcol-page-content> </ds-comcol-page-content>
<!-- Copyright --> <!-- Copyright -->
<ds-comcol-page-content <ds-comcol-page-content
[content]="(communityData.payload | async)?.copyrightText" [content]="communityPayload.copyrightText"
[hasInnerHtml]="true"> [hasInnerHtml]="true">
</ds-comcol-page-content> </ds-comcol-page-content>
<ds-community-page-sub-collection-list></ds-community-page-sub-collection-list> <ds-community-page-sub-collection-list></ds-community-page-sub-collection-list>
</div>
</div> </div>
<ds-error *ngIf="communityData.hasFailed | async" message="{{'error.community' | translate}}"></ds-error>
<ds-loading *ngIf="communityData.isLoading | async" message="{{'loading.community' | translate}}"></ds-loading>

View File

@@ -2,18 +2,21 @@ import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute, Params } from '@angular/router';
import { Subscription } from 'rxjs/Subscription'; import { Subscription } from 'rxjs/Subscription';
import { CommunityDataService } from '../core/data/community-data.service';
import { RemoteData } from '../core/data/remote-data';
import { Bitstream } from '../core/shared/bitstream.model';
import { Community } from '../core/shared/community.model'; import { Community } from '../core/shared/community.model';
import { Bitstream } from '../core/shared/bitstream.model';
import { RemoteData } from '../core/data/remote-data'; import { fadeInOut } from '../shared/animations/fade';
import { CommunityDataService } from '../core/data/community-data.service';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
@Component({ @Component({
selector: 'ds-community-page', selector: 'ds-community-page',
styleUrls: ['./community-page.component.scss'], styleUrls: ['./community-page.component.scss'],
templateUrl: './community-page.component.html', templateUrl: './community-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
animations: [fadeInOut]
}) })
export class CommunityPageComponent implements OnInit, OnDestroy { export class CommunityPageComponent implements OnInit, OnDestroy {
communityData: RemoteData<Community>; communityData: RemoteData<Community>;

View File

@@ -1,4 +1,4 @@
<div *ngIf="subCollections.hasSucceeded | async"> <div *ngIf="subCollections.hasSucceeded | async" @fadeIn>
<h2>{{'community.sub-collection-list.head' | translate}}</h2> <h2>{{'community.sub-collection-list.head' | translate}}</h2>
<ul> <ul>
<li *ngFor="let collection of (subCollections.payload | async)"> <li *ngFor="let collection of (subCollections.payload | async)">
@@ -9,3 +9,5 @@
</li> </li>
</ul> </ul>
</div> </div>
<ds-error *ngIf="subCollections.hasFailed | async" message="{{'error.sub-collections' | translate}}"></ds-error>
<ds-loading *ngIf="subCollections.isLoading | async" message="{{'loading.sub-collections' | translate}}"></ds-loading>

View File

@@ -4,10 +4,13 @@ import { CollectionDataService } from '../../core/data/collection-data.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { fadeIn } from '../../shared/animations/fade';
@Component({ @Component({
selector: 'ds-community-page-sub-collection-list', selector: 'ds-community-page-sub-collection-list',
styleUrls: ['./community-page-sub-collection-list.component.scss'], styleUrls: ['./community-page-sub-collection-list.component.scss'],
templateUrl: './community-page-sub-collection-list.component.html', templateUrl: './community-page-sub-collection-list.component.html',
animations:[fadeIn]
}) })
export class CommunityPageSubCollectionListComponent implements OnInit { export class CommunityPageSubCollectionListComponent implements OnInit {
subCollections: RemoteData<Collection[]>; subCollections: RemoteData<Collection[]>;

View File

@@ -0,0 +1,13 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { HomePageComponent } from './home-page.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: '', component: HomePageComponent, pathMatch: 'full' }
])
]
})
export class HomePageRoutingModule { }

View File

@@ -0,0 +1,10 @@
import { Component } from '@angular/core';
@Component({
selector: 'ds-home-page',
styleUrls: ['./home-page.component.scss'],
templateUrl: './home-page.component.html'
})
export class HomePageComponent {
}

View File

@@ -1,24 +1,24 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { NgModule } from '@angular/core';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { HomeComponent } from './home.component';
import { HomeRoutingModule } from './home-routing.module';
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
import { HomeNewsComponent } from './home-news/home-news.component'; import { HomeNewsComponent } from './home-news/home-news.component';
import { HomePageRoutingModule } from './home-page-routing.module';
import { HomePageComponent } from './home-page.component';
import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component';
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, CommonModule,
SharedModule, SharedModule,
HomeRoutingModule HomePageRoutingModule
], ],
declarations: [ declarations: [
HomeComponent, HomePageComponent,
TopLevelCommunityListComponent, TopLevelCommunityListComponent,
HomeNewsComponent HomeNewsComponent
] ]
}) })
export class HomeModule { export class HomePageModule {
} }

View File

@@ -0,0 +1,13 @@
<div *ngIf="topLevelCommunities.hasSucceeded | async" @fadeInOut>
<h2>{{'home.top-level-communities.head' | translate}}</h2>
<p class="lead">{{'home.top-level-communities.help' | translate}}</p>
<ds-object-list
[config]="config"
[sortConfig]="sortConfig"
[objects]="topLevelCommunities"
[hideGear]="true"
(paginationChange)="updatePage($event)">
</ds-object-list>
</div>
<ds-error *ngIf="topLevelCommunities.hasFailed | async" message="{{'error.top-level-communites' | translate}}"></ds-error>
<ds-loading *ngIf="topLevelCommunities.isLoading | async" message="{{'loading.top-level-communities' | translate}}"></ds-loading>

View File

@@ -1,18 +1,20 @@
import { Component, OnInit, ChangeDetectorRef, ChangeDetectionStrategy } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { SortOptions } from '../../core/cache/models/sort-options.model';
import { CommunityDataService } from '../../core/data/community-data.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { CommunityDataService } from '../../core/data/community-data.service';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { fadeInOut } from '../../shared/animations/fade';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortOptions, SortDirection } from '../../core/cache/models/sort-options.model';
import { ActivatedRoute } from '@angular/router';
@Component({ @Component({
selector: 'ds-top-level-community-list', selector: 'ds-top-level-community-list',
styleUrls: ['./top-level-community-list.component.scss'], styleUrls: ['./top-level-community-list.component.scss'],
templateUrl: './top-level-community-list.component.html', templateUrl: './top-level-community-list.component.html',
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [fadeInOut]
}) })
export class TopLevelCommunityListComponent { export class TopLevelCommunityListComponent {
topLevelCommunities: RemoteData<Community[]>; topLevelCommunities: RemoteData<Community[]>;
config: PaginationComponentOptions; config: PaginationComponentOptions;
@@ -21,8 +23,7 @@ export class TopLevelCommunityListComponent {
constructor(private cds: CommunityDataService) { constructor(private cds: CommunityDataService) {
this.config = new PaginationComponentOptions(); this.config = new PaginationComponentOptions();
this.config.id = 'top-level-pagination'; this.config.id = 'top-level-pagination';
this.config.pageSizeOptions = [4]; this.config.pageSize = 5;
this.config.pageSize = 4;
this.config.currentPage = 1; this.config.currentPage = 1;
this.sortConfig = new SortOptions(); this.sortConfig = new SortOptions();

View File

@@ -1,13 +0,0 @@
import { NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { HomeComponent } from './home.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: '', component: HomeComponent, pathMatch: 'full' }
])
]
})
export class HomeRoutingModule { }

View File

@@ -1,11 +0,0 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
@Component({
selector: 'ds-home',
styleUrls: ['./home.component.scss'],
templateUrl: './home.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class HomeComponent {
}

View File

@@ -1,6 +0,0 @@
<div *ngIf="topLevelCommunities.hasSucceeded | async">
<h2>{{'home.top-level-communities.head' | translate}}</h2>
<p class="lead">{{'home.top-level-communities.help' | translate}}</p>
<ds-object-list [config]="config" [sortConfig]="sortConfig"
[objects]="topLevelCommunities" [hideGear]="true" (paginationChange)="updatePage($event)"></ds-object-list>
</div>

View File

@@ -1,12 +1,11 @@
<div class="item-page" *ngIf="item.hasSucceeded | async"> <div class="item-page" *ngIf="item.hasSucceeded | async" @fadeInOut>
<ds-item-page-title-field [item]="item.payload | async"></ds-item-page-title-field> <div *ngIf="item.payload | async; let itemPayload">
<ds-item-page-title-field [item]="itemPayload"></ds-item-page-title-field>
<div class="simple-view-link"> <div class="simple-view-link">
<a class="btn btn-outline-primary col-4" [routerLink]="['/items/' + (item.payload | async)?.id]"> <a class="btn btn-outline-primary col-4" [routerLink]="['/items/' + itemPayload.id]">
{{"item.page.link.simple" | translate}} {{"item.page.link.simple" | translate}}
</a> </a>
</div> </div>
<table class="table table-responsive table-striped"> <table class="table table-responsive table-striped">
<tbody> <tbody>
<tr *ngFor="let metadatum of (metadata | async)"> <tr *ngFor="let metadatum of (metadata | async)">
@@ -16,10 +15,9 @@
</tr> </tr>
</tbody> </tbody>
</table> </table>
<ds-item-page-full-file-section [item]="itemPayload"></ds-item-page-full-file-section>
<ds-item-page-full-file-section [item]="item.payload | async"></ds-item-page-full-file-section> <ds-item-page-collections [item]="itemPayload"></ds-item-page-collections>
</div>
<ds-item-page-collections [item]="item.payload | async"></ds-item-page-collections>
</div> </div>
<ds-error *ngIf="item.hasFailed | async" message="{{'error.item' | translate}}"></ds-error>
<ds-loading *ngIf="item.isLoading | async" message="{{'loading.item' | translate}}"></ds-loading>

View File

@@ -1,4 +1,6 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { animate, state, transition, trigger, style, keyframes } from '@angular/animations';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { ItemPageComponent } from '../simple/item-page.component'; import { ItemPageComponent } from '../simple/item-page.component';
@@ -8,6 +10,8 @@ import { ActivatedRoute } from '@angular/router';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { fadeInOut } from '../../shared/animations/fade';
/** /**
* This component renders a simple item page. * This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents. * The route parameter 'id' is used to request the item it represents.
@@ -18,6 +22,7 @@ import { Item } from '../../core/shared/item.model';
selector: 'ds-full-item-page', selector: 'ds-full-item-page',
styleUrls: ['./full-item-page.component.scss'], styleUrls: ['./full-item-page.component.scss'],
templateUrl: './full-item-page.component.html', templateUrl: './full-item-page.component.html',
animations: [fadeInOut]
}) })
export class FullItemPageComponent extends ItemPageComponent implements OnInit { export class FullItemPageComponent extends ItemPageComponent implements OnInit {

View File

@@ -1,26 +1,27 @@
<div class="item-page" *ngIf="item.hasSucceeded | async"> <div class="item-page" *ngIf="item.hasSucceeded | async" @fadeInOut>
<ds-item-page-title-field [item]="item.payload | async"></ds-item-page-title-field> <div *ngIf="item.payload | async; let itemPayload">
<ds-item-page-title-field [item]="itemPayload"></ds-item-page-title-field>
<div class="row"> <div class="row">
<div class="col-xs-12 col-md-4"> <div class="col-xs-12 col-md-4">
<ds-metadata-field-wrapper> <ds-metadata-field-wrapper>
<ds-thumbnail [thumbnail]="thumbnail | async"></ds-thumbnail> <ds-thumbnail [thumbnail]="thumbnail | async"></ds-thumbnail>
</ds-metadata-field-wrapper> </ds-metadata-field-wrapper>
<ds-item-page-file-section [item]="item.payload | async"></ds-item-page-file-section> <ds-item-page-file-section [item]="itemPayload"></ds-item-page-file-section>
<ds-item-page-date-field [item]="item.payload | async"></ds-item-page-date-field> <ds-item-page-date-field [item]="itemPayload"></ds-item-page-date-field>
<ds-item-page-author-field [item]="item.payload | async"></ds-item-page-author-field> <ds-item-page-author-field [item]="itemPayload"></ds-item-page-author-field>
</div> </div>
<div class="col-xs-12 col-md-6"> <div class="col-xs-12 col-md-6">
<ds-item-page-abstract-field <ds-item-page-abstract-field [item]="itemPayload"></ds-item-page-abstract-field>
[item]="item.payload | async"></ds-item-page-abstract-field> <ds-item-page-uri-field [item]="itemPayload"></ds-item-page-uri-field>
<ds-item-page-uri-field [item]="item.payload | async"></ds-item-page-uri-field> <ds-item-page-collections [item]="itemPayload"></ds-item-page-collections>
<ds-item-page-collections [item]="item.payload | async"></ds-item-page-collections>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="['/items/' + (item.payload | async)?.id + '/full']"> <a class="btn btn-outline-primary" [routerLink]="['/items/' + itemPayload.id + '/full']">
{{"item.page.link.full" | translate}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>
</div> </div>
</div>
</div> </div>
</div> </div>
<ds-error *ngIf="item.hasFailed | async" message="{{'error.item' | translate}}"></ds-error>
<ds-loading *ngIf="item.isLoading | async" message="{{'loading.item' | translate}}"></ds-loading>

View File

@@ -1,12 +1,15 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Item } from '../../core/shared/item.model'; import { Observable } from 'rxjs/Observable';
import { ItemDataService } from '../../core/data/item-data.service'; import { ItemDataService } from '../../core/data/item-data.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Bitstream } from '../../core/shared/bitstream.model'; import { Bitstream } from '../../core/shared/bitstream.model';
import { Item } from '../../core/shared/item.model';
import { fadeInOut } from '../../shared/animations/fade';
/** /**
* This component renders a simple item page. * This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents. * The route parameter 'id' is used to request the item it represents.
@@ -16,7 +19,8 @@ import { Bitstream } from '../../core/shared/bitstream.model';
selector: 'ds-item-page', selector: 'ds-item-page',
styleUrls: ['./item-page.component.scss'], styleUrls: ['./item-page.component.scss'],
templateUrl: './item-page.component.html', templateUrl: './item-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush changeDetection: ChangeDetectionStrategy.OnPush,
animations: [fadeInOut]
}) })
export class ItemPageComponent implements OnInit { export class ItemPageComponent implements OnInit {

View File

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

View File

@@ -0,0 +1,9 @@
<div class="search-page">
<ds-search-form
[query]="query"
[scope]="scopeObject?.payload | async"
[currentParams]="currentParams"
[scopes]="scopeList?.payload">
</ds-search-form>
<ds-search-results [searchResults]="results" [searchConfig]="searchOptions"></ds-search-results>
</div>

View File

@@ -0,0 +1,110 @@
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { ActivatedRoute, Router } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { CommunityDataService } from '../core/data/community-data.service';
import { SearchPageComponent } from './search-page.component';
import { SearchService } from './search-service/search.service';
import { Community } from '../core/shared/community.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
describe('SearchPageComponent', () => {
let comp: SearchPageComponent;
let fixture: ComponentFixture<SearchPageComponent>;
let searchServiceObject: SearchService;
const mockResults = ['test', 'data'];
const searchServiceStub = {
search: () => mockResults
};
const queryParam = 'test query';
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
const activatedRouteStub = {
queryParams: Observable.of({
query: queryParam,
scope: scopeParam
})
};
const mockCommunityList = [];
const communityDataServiceStub = {
findAll: () => mockCommunityList,
findById: () => new Community()
};
class RouterStub {
navigateByUrl(url: string) {
return url;
}
}
beforeEach(async(() => {
TestBed.configureTestingModule({
// imports: [ SearchPageModule ],
declarations: [SearchPageComponent],
providers: [
{ provide: SearchService, useValue: searchServiceStub },
{ provide: ActivatedRoute, useValue: activatedRouteStub },
{ provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: Router, useClass: RouterStub }
],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchPageComponent);
comp = fixture.componentInstance; // SearchPageComponent test instance
fixture.detectChanges();
searchServiceObject = (comp as any).service;
});
it('should set the scope and query based on the route parameters', () => {
expect(comp.query).toBe(queryParam);
expect((comp as any).scope).toBe(scopeParam);
});
describe('when update search results is called', () => {
let pagination;
let sort;
beforeEach(() => {
pagination = Object.assign(
{},
new PaginationComponentOptions(),
{
currentPage: 5,
pageSize: 15
}
);
sort = Object.assign({},
new SortOptions(),
{
direction: SortDirection.Ascending,
field: 'test-field'
}
);
});
it('should call the search function of the search service with the right parameters', () => {
spyOn(searchServiceObject, 'search').and.callThrough();
(comp as any).updateSearchResults({
pagination: pagination,
sort: sort
});
expect(searchServiceObject.search).toHaveBeenCalledWith(queryParam, scopeParam, {
pagination: pagination,
sort: sort
});
});
it('should update the results', () => {
spyOn(searchServiceObject, 'search').and.callThrough();
(comp as any).updateSearchResults({});
expect(comp.results as any).toBe(mockResults);
});
});
});

View File

@@ -1,12 +1,12 @@
import { Component, OnInit, OnDestroy } from '@angular/core'; import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core';
import { SearchService } from '../search/search.service'; import { SearchService } from './search-service/search.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { SearchResult } from '../search/search-result.model'; import { SearchResult } from './search-result.model';
import { DSpaceObject } from '../core/shared/dspace-object.model'; import { DSpaceObject } from '../core/shared/dspace-object.model';
import { SortOptions } from '../core/cache/models/sort-options.model'; import { SortOptions } from '../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { SearchOptions } from '../search/search-options.model'; import { SearchOptions } from './search-options.model';
import { CommunityDataService } from '../core/data/community-data.service'; import { CommunityDataService } from '../core/data/community-data.service';
import { isNotEmpty } from '../shared/empty.util'; import { isNotEmpty } from '../shared/empty.util';
import { Community } from '../core/shared/community.model'; import { Community } from '../core/shared/community.model';
@@ -21,20 +21,25 @@ import { Community } from '../core/shared/community.model';
selector: 'ds-search-page', selector: 'ds-search-page',
styleUrls: ['./search-page.component.scss'], styleUrls: ['./search-page.component.scss'],
templateUrl: './search-page.component.html', templateUrl: './search-page.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
}) })
export class SearchPageComponent implements OnInit, OnDestroy { export class SearchPageComponent implements OnInit, OnDestroy {
private sub; private sub;
query: string;
private scope: string; private scope: string;
query: string;
scopeObject: RemoteData<DSpaceObject>; scopeObject: RemoteData<DSpaceObject>;
results: RemoteData<Array<SearchResult<DSpaceObject>>>; results: RemoteData<Array<SearchResult<DSpaceObject>>>;
currentParams = {}; currentParams = {};
searchOptions: SearchOptions; searchOptions: SearchOptions;
scopeList: RemoteData<Community[]>; scopeList: RemoteData<Community[]>;
constructor(private service: SearchService, constructor(
private service: SearchService,
private route: ActivatedRoute, private route: ActivatedRoute,
private communityService: CommunityDataService,) { private communityService: CommunityDataService
) {
this.scopeList = communityService.findAll(); this.scopeList = communityService.findAll();
// Initial pagination config // Initial pagination config
const pagination: PaginationComponentOptions = new PaginationComponentOptions(); const pagination: PaginationComponentOptions = new PaginationComponentOptions();
@@ -80,7 +85,6 @@ export class SearchPageComponent implements OnInit, OnDestroy {
private updateSearchResults(searchOptions) { private updateSearchResults(searchOptions) {
// Resolve search results // Resolve search results
this.results = this.service.search(this.query, this.scope, searchOptions); this.results = this.service.search(this.query, this.scope, searchOptions);
} }
ngOnDestroy() { ngOnDestroy() {

View File

@@ -7,11 +7,11 @@ import { TranslateModule } from '@ngx-translate/core';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { SearchPageRoutingModule } from './search-page-routing.module'; import { SearchPageRoutingModule } from './search-page-routing.module';
import { SearchPageComponent } from './search-page.component'; import { SearchPageComponent } from './search-page.component';
import { SearchResultsComponent } from './search-results/search-results.compontent'; import { SearchResultsComponent } from './search-results/search-results.component';
import { SearchModule } from '../search/search.module';
import { ItemSearchResultListElementComponent } from '../object-list/search-result-list-element/item-search-result/item-search-result-list-element.component'; import { ItemSearchResultListElementComponent } from '../object-list/search-result-list-element/item-search-result/item-search-result-list-element.component';
import { CollectionSearchResultListElementComponent } from '../object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component'; import { CollectionSearchResultListElementComponent } from '../object-list/search-result-list-element/collection-search-result/collection-search-result-list-element.component';
import { CommunitySearchResultListElementComponent } from '../object-list/search-result-list-element/community-search-result/community-search-result-list-element.component'; import { CommunitySearchResultListElementComponent } from '../object-list/search-result-list-element/community-search-result/community-search-result-list-element.component';
import { SearchService } from './search-service/search.service';
@NgModule({ @NgModule({
imports: [ imports: [
@@ -19,8 +19,7 @@ import { CommunitySearchResultListElementComponent } from '../object-list/search
CommonModule, CommonModule,
TranslateModule, TranslateModule,
RouterModule, RouterModule,
SharedModule, SharedModule
SearchModule
], ],
declarations: [ declarations: [
SearchPageComponent, SearchPageComponent,
@@ -29,6 +28,9 @@ import { CommunitySearchResultListElementComponent } from '../object-list/search
CollectionSearchResultListElementComponent, CollectionSearchResultListElementComponent,
CommunitySearchResultListElementComponent CommunitySearchResultListElementComponent
], ],
providers: [
SearchService
],
entryComponents: [ entryComponents: [
ItemSearchResultListElementComponent, ItemSearchResultListElementComponent,
CollectionSearchResultListElementComponent, CollectionSearchResultListElementComponent,

View File

@@ -0,0 +1,7 @@
<h2 *ngIf="(searchResults.payload | async)?.length > 0">{{ 'search.results.title' | translate }}</h2>
<ds-object-list
[config]="searchConfig.pagination"
[sortConfig]="searchConfig.sort"
[objects]="searchResults"
[hideGear]="false">
</ds-object-list>

View File

@@ -0,0 +1,142 @@
import { ComponentFixture, TestBed, async, tick, fakeAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core';
import { ResourceType } from '../../core/shared/resource-type';
import { Community } from '../../core/shared/community.model';
import { TranslateModule } from '@ngx-translate/core';
import { SearchResultsComponent } from './search-results.component';
describe('SearchResultsComponent', () => {
let comp: SearchResultsComponent;
let fixture: ComponentFixture<SearchResultsComponent>;
let heading: DebugElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()],
declarations: [SearchResultsComponent],
schemas: [NO_ERRORS_SCHEMA]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchResultsComponent);
comp = fixture.componentInstance; // SearchFormComponent test instance
heading = fixture.debugElement.query(By.css('heading'));
});
it('should display heading when results are not empty', fakeAsync(() => {
(comp as any).searchResults = 'test';
(comp as any).searchConfig = {pagination: ''};
fixture.detectChanges();
tick();
expect(heading).toBeDefined();
}));
it('should not display heading when results is empty', () => {
expect(heading).toBeNull();
});
});
export const objects = [
Object.assign(new Community(), {
handle: '10673/11',
logo: {
self: {
_isScalar: true,
value: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/10b636d0-7890-4968-bcd6-0d83bf4e2b42',
scheduler: null
}
},
collections: {
self: {
_isScalar: true,
value: '1506937433727',
scheduler: null
}
},
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/communities/7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
type: ResourceType.Community,
name: 'OR2017 - Demonstration',
metadata: [
{
key: 'dc.description',
language: null,
value: ''
},
{
key: 'dc.description.abstract',
language: null,
value: 'This is a test community to hold content for the OR2017 demostration'
},
{
key: 'dc.description.tableofcontents',
language: null,
value: ''
},
{
key: 'dc.rights',
language: null,
value: ''
},
{
key: 'dc.title',
language: null,
value: 'OR2017 - Demonstration'
}
]
}),
Object.assign(new Community(),
{
handle: '10673/1',
logo: {
self: {
_isScalar: true,
value: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/f446c17d-6d51-45ea-a610-d58a73642d40',
scheduler: null
}
},
collections: {
self: {
_isScalar: true,
value: '1506937433727',
scheduler: null
}
},
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/communities/9076bd16-e69a-48d6-9e41-0238cb40d863',
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
type: ResourceType.Community,
name: 'Sample Community',
metadata: [
{
key: 'dc.description',
language: null,
value: '<p>This is the introductory text for the <em>Sample Community</em> on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).</p>\r\n<p><strong>DSpace Communities may contain one or more Sub-Communities or Collections (of Items).</strong></p>\r\n<p>This particular Community has its own logo (the <a href=\'http://www.duraspace.org/\'>DuraSpace</a> logo).</p>'
},
{
key: 'dc.description.abstract',
language: null,
value: 'This is a sample top-level community'
},
{
key: 'dc.description.tableofcontents',
language: null,
value: '<p>This is the <em>news section</em> for this <em>Sample Community</em>. System or Community Administrators (of this Community) can edit this News field.</p>'
},
{
key: 'dc.rights',
language: null,
value: '<p><em>If this Community had special copyright text to display, it would be displayed here.</em></p>'
},
{
key: 'dc.title',
language: null,
value: 'Sample Community'
}
]
}
)
];

View File

@@ -1,20 +1,18 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { SearchResult } from '../../search/search-result.model'; import { SearchResult } from '../search-result.model';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { SearchOptions } from '../../search/search-options.model'; import { SearchOptions } from '../search-options.model';
/** /**
* This component renders a simple item page. * This component renders a simple item page.
* The route parameter 'id' is used to request the item it represents. * The route parameter 'id' is used to request the item it represents.
* All fields of the item that should be displayed, are defined in its template. * All fields of the item that should be displayed, are defined in its template.
*/ */
@Component({ @Component({
selector: 'ds-search-results', selector: 'ds-search-results',
templateUrl: './search-results.component.html', templateUrl: './search-results.component.html',
}) })
export class SearchResultsComponent { export class SearchResultsComponent {
@Input() searchResults: RemoteData<Array<SearchResult<DSpaceObject>>>; @Input() searchResults: RemoteData<Array<SearchResult<DSpaceObject>>>;
@Input() searchConfig: SearchOptions; @Input() searchConfig: SearchOptions;

View File

@@ -0,0 +1,7 @@
export class FacetValue {
value: string;
count: number;
search: string;
}

View File

@@ -0,0 +1,5 @@
export enum FilterType {
text,
range,
hierarchy
}

View File

@@ -0,0 +1,16 @@
import { FilterType } from './filter-type.model';
export class SearchFilterConfig {
name: string;
type: FilterType;
hasFacets: boolean;
isOpenByDefault: boolean;
/**
* Name of this configuration that can be used in a url
* @returns Parameter name
*/
get paramName(): string {
return 'f.' + this.name;
}
}

View File

@@ -1,15 +1,18 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { SearchResult } from './search-result.model'; import { SearchResult } from '../search-result.model';
import { ItemDataService } from '../core/data/item-data.service'; import { ItemDataService } from '../../core/data/item-data.service';
import { PageInfo } from '../core/shared/page-info.model'; import { PageInfo } from '../../core/shared/page-info.model';
import { DSpaceObject } from '../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { SearchOptions } from './search-options.model'; import { SearchOptions } from '../search-options.model';
import { hasValue, isNotEmpty } from '../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { Metadatum } from '../core/shared/metadatum.model'; import { Metadatum } from '../../core/shared/metadatum.model';
import { Item } from '../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { ItemSearchResult } from '../object-list/search-result-list-element/item-search-result/item-search-result.model'; import { ItemSearchResult } from '../../object-list/search-result-list-element/item-search-result/item-search-result.model';
import { SearchFilterConfig } from './search-filter-config.model';
import { FilterType } from './filter-type.model';
import { FacetValue } from './facet-value.model';
function shuffle(array: any[]) { function shuffle(array: any[]) {
let i = 0; let i = 0;
@@ -42,6 +45,37 @@ export class SearchService {
'<em>The QSAR DataBank (QsarDB) repository</em>', '<em>The QSAR DataBank (QsarDB) repository</em>',
); );
config: SearchFilterConfig[] = [
Object.assign(new SearchFilterConfig(),
{
name: 'scope',
type: FilterType.hierarchy,
hasFacets: true,
isOpenByDefault: true
}),
Object.assign(new SearchFilterConfig(),
{
name: 'author',
type: FilterType.text,
hasFacets: true,
isOpenByDefault: false
}),
Object.assign(new SearchFilterConfig(),
{
name: 'date',
type: FilterType.range,
hasFacets: true,
isOpenByDefault: false
}),
Object.assign(new SearchFilterConfig(),
{
name: 'subject',
type: FilterType.text,
hasFacets: false,
isOpenByDefault: false
})
];
constructor(private itemDataService: ItemDataService) { constructor(private itemDataService: ItemDataService) {
} }
@@ -63,8 +97,7 @@ export class SearchService {
if (isNotEmpty(searchOptions) && hasValue(searchOptions.sort.field)) { if (isNotEmpty(searchOptions) && hasValue(searchOptions.sort.field)) {
self += `&sortField=${searchOptions.sort.field}`; self += `&sortField=${searchOptions.sort.field}`;
} }
const requestPending = Observable.of(false);
const responsePending = Observable.of(false);
const errorMessage = Observable.of(undefined); const errorMessage = Observable.of(undefined);
const statusCode = Observable.of('200'); const statusCode = Observable.of('200');
const returningPageInfo = new PageInfo(); const returningPageInfo = new PageInfo();
@@ -84,8 +117,8 @@ export class SearchService {
}); });
const pageInfo = itemsRD.pageInfo.map((info: PageInfo) => { const pageInfo = itemsRD.pageInfo.map((info: PageInfo) => {
info.totalElements = info.totalElements > 20 ? 20 : info.totalElements; const totalElements = info.totalElements > 20 ? 20 : info.totalElements;
return info; return Object.assign({}, info, { totalElements: totalElements });
}); });
const payload = itemsRD.payload.map((items: Item[]) => { const payload = itemsRD.payload.map((items: Item[]) => {
@@ -103,8 +136,8 @@ export class SearchService {
return new RemoteData( return new RemoteData(
Observable.of(self), Observable.of(self),
requestPending, itemsRD.isRequestPending,
responsePending, itemsRD.isResponsePending,
itemsRD.hasSucceeded, itemsRD.hasSucceeded,
errorMessage, errorMessage,
statusCode, statusCode,
@@ -112,4 +145,51 @@ export class SearchService {
payload payload
) )
} }
getConfig(): RemoteData<SearchFilterConfig[]> {
const requestPending = Observable.of(false);
const responsePending = Observable.of(false);
const isSuccessful = Observable.of(true);
const errorMessage = Observable.of(undefined);
const statusCode = Observable.of('200');
const returningPageInfo = Observable.of(new PageInfo());
return new RemoteData(
Observable.of('https://dspace7.4science.it/dspace-spring-rest/api/search'),
requestPending,
responsePending,
isSuccessful,
errorMessage,
statusCode,
returningPageInfo,
Observable.of(this.config)
);
}
getFacetValuesFor(searchFilterConfigName: string): RemoteData<FacetValue[]> {
const values: FacetValue[] = [];
for (let i = 0; i < 5; i++) {
const value = searchFilterConfigName + ' ' + (i + 1);
values.push({
value: value,
count: Math.floor(Math.random() * 20) + 20 * (5 - i), // make sure first results have the highest (random) count
search: 'https://dspace7.4science.it/dspace-spring-rest/api/search?f.' + searchFilterConfigName + '=' + encodeURI(value)
});
}
const requestPending = Observable.of(false);
const responsePending = Observable.of(false);
const isSuccessful = Observable.of(true);
const errorMessage = Observable.of(undefined);
const statusCode = Observable.of('200');
const returningPageInfo = Observable.of(new PageInfo());
return new RemoteData(
Observable.of('https://dspace7.4science.it/dspace-spring-rest/api/search'),
requestPending,
responsePending,
isSuccessful,
errorMessage,
statusCode,
returningPageInfo,
Observable.of(values)
);
}
} }

View File

@@ -7,10 +7,11 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
imports: [ imports: [
RouterModule.forRoot([ RouterModule.forRoot([
{ path: '', redirectTo: '/home', pathMatch: 'full' }, { path: '', redirectTo: '/home', pathMatch: 'full' },
{ path: 'home', loadChildren: './+home/home.module#HomeModule' }, { path: 'home', loadChildren: './+home-page/home-page.module#HomePageModule' },
{ path: 'communities', loadChildren: './+community-page/community-page.module#CommunityPageModule' }, { path: 'communities', loadChildren: './+community-page/community-page.module#CommunityPageModule' },
{ path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' }, { path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
{ path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' }, { path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
{ path: '**', pathMatch: 'full', component: PageNotFoundComponent }, { path: '**', pathMatch: 'full', component: PageNotFoundComponent },
]) ])
], ],

View File

@@ -1,7 +1,6 @@
import { NgModule } from '@angular/core'; import { NgModule } from '@angular/core';
import { CommonModule, APP_BASE_HREF } from '@angular/common'; import { CommonModule, APP_BASE_HREF } from '@angular/common';
import { HttpModule } from '@angular/http'; import { HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';

View File

@@ -49,12 +49,20 @@ export class RemoteDataBuildService {
requestHrefObs.flatMap((requestHref) => this.responseCache.get(requestHref)).filter((entry) => hasValue(entry)) requestHrefObs.flatMap((requestHref) => this.responseCache.get(requestHref)).filter((entry) => hasValue(entry))
); );
const requestPending = requestObs.map((entry: RequestEntry) => entry.requestPending).distinctUntilChanged(); const requestPending = requestObs
.map((entry: RequestEntry) => entry.requestPending)
.startWith(true)
.distinctUntilChanged();
const responsePending = requestObs.map((entry: RequestEntry) => entry.responsePending).distinctUntilChanged(); const responsePending = requestObs
.map((entry: RequestEntry) => entry.responsePending)
.startWith(false)
.distinctUntilChanged();
const isSuccessFul = responseCacheObs const isSuccessFul = responseCacheObs
.map((entry: ResponseCacheEntry) => entry.response.isSuccessful).distinctUntilChanged(); .map((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.startWith(false)
.distinctUntilChanged();
const errorMessage = responseCacheObs const errorMessage = responseCacheObs
.filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful) .filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful)
@@ -133,12 +141,20 @@ export class RemoteDataBuildService {
const responseCacheObs = hrefObs.flatMap((href: string) => this.responseCache.get(href)) const responseCacheObs = hrefObs.flatMap((href: string) => this.responseCache.get(href))
.filter((entry) => hasValue(entry)); .filter((entry) => hasValue(entry));
const requestPending = requestObs.map((entry: RequestEntry) => entry.requestPending).distinctUntilChanged(); const requestPending = requestObs
.map((entry: RequestEntry) => entry.requestPending)
.startWith(true)
.distinctUntilChanged();
const responsePending = requestObs.map((entry: RequestEntry) => entry.responsePending).distinctUntilChanged(); const responsePending = requestObs
.map((entry: RequestEntry) => entry.responsePending)
.startWith(false)
.distinctUntilChanged();
const isSuccessFul = responseCacheObs const isSuccessFul = responseCacheObs
.map((entry: ResponseCacheEntry) => entry.response.isSuccessful).distinctUntilChanged(); .map((entry: ResponseCacheEntry) => entry.response.isSuccessful)
.startWith(false)
.distinctUntilChanged();
const errorMessage = responseCacheObs const errorMessage = responseCacheObs
.filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful) .filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful)

View File

@@ -18,7 +18,7 @@ export abstract class DataService<TNormalized extends CacheableObject, TDomain>
protected abstract rdbService: RemoteDataBuildService; protected abstract rdbService: RemoteDataBuildService;
protected abstract store: Store<CoreState>; protected abstract store: Store<CoreState>;
protected abstract linkName: string; protected abstract linkName: string;
protected abstract EnvConfig: GlobalConfig protected abstract EnvConfig: GlobalConfig;
constructor( constructor(
private normalizedResourceType: GenericConstructor<TNormalized>, private normalizedResourceType: GenericConstructor<TNormalized>,

View File

@@ -1,4 +1,5 @@
<ds-pagination [paginationOptions]="config" <ds-pagination
[paginationOptions]="config"
[pageInfoState]="pageInfo" [pageInfoState]="pageInfo"
[collectionSize]="(pageInfo | async)?.totalElements" [collectionSize]="(pageInfo | async)?.totalElements"
[sortOptions]="sortConfig" [sortOptions]="sortConfig"
@@ -9,10 +10,11 @@
(sortDirectionChange)="onSortDirectionChange($event)" (sortDirectionChange)="onSortDirectionChange($event)"
(sortFieldChange)="onSortDirectionChange($event)" (sortFieldChange)="onSortDirectionChange($event)"
(paginationChange)="onPaginationChange($event)"> (paginationChange)="onPaginationChange($event)">
<ul *ngIf="objects.hasSucceeded | async"> <!--class="list-unstyled"--> <ul *ngIf="objects.hasSucceeded | async" @fadeIn> <!--class="list-unstyled"-->
<li *ngFor="let object of (objects.payload | async) | paginate: { itemsPerPage: (pageInfo | async)?.elementsPerPage, currentPage: (pageInfo | async)?.currentPage, totalItems: (pageInfo | async)?.totalElements }"> <li *ngFor="let object of (objects.payload | async) | paginate: { itemsPerPage: (pageInfo | async)?.elementsPerPage, currentPage: (pageInfo | async)?.currentPage, totalItems: (pageInfo | async)?.totalElements }">
<ds-wrapper-list-element [object]="object"></ds-wrapper-list-element> <ds-wrapper-list-element [object]="object"></ds-wrapper-list-element>
</li> </li>
</ul> </ul>
<ds-error *ngIf="objects.hasFailed | async" message="{{'error.objects' | translate}}"></ds-error>
<ds-loading *ngIf="objects.isLoading | async" message="{{'loading.objects' | translate}}"></ds-loading>
</ds-pagination> </ds-pagination>

View File

@@ -1,29 +1,35 @@
import { import {
ChangeDetectionStrategy,
ChangeDetectorRef,
Component, Component,
EventEmitter, EventEmitter,
Input, Input,
ViewEncapsulation, OnChanges,
ChangeDetectionStrategy,
OnInit, OnInit,
Output, SimpleChanges, OnChanges, ChangeDetectorRef, DoCheck Output,
SimpleChanges,
ViewEncapsulation
} from '@angular/core'; } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs/Observable';
import { RemoteData } from '../../core/data/remote-data'; import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { PageInfo } from '../../core/shared/page-info.model';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; import { RemoteData } from '../core/data/remote-data';
import { PageInfo } from '../core/shared/page-info.model';
import { ListableObject } from '../object-list/listable-object/listable-object.model';
import { SortOptions, SortDirection } from '../../core/cache/models/sort-options.model'; import { fadeIn } from '../shared/animations/fade';
import { ListableObject } from '../../object-list/listable-object/listable-object.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
@Component({ @Component({
changeDetection: ChangeDetectionStrategy.Default, changeDetection: ChangeDetectionStrategy.Default,
encapsulation: ViewEncapsulation.Emulated, encapsulation: ViewEncapsulation.Emulated,
selector: 'ds-object-list', selector: 'ds-object-list',
styleUrls: ['../../object-list/object-list.component.scss'], styleUrls: ['./object-list.component.scss'],
templateUrl: '../../object-list/object-list.component.html' templateUrl: './object-list.component.html',
animations: [fadeIn]
}) })
export class ObjectListComponent implements OnChanges, OnInit { export class ObjectListComponent implements OnChanges, OnInit {

View File

@@ -1,4 +1,4 @@
import { SearchResult } from '../../../search/search-result.model'; import { SearchResult } from '../../../+search-page/search-result.model';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
export class CollectionSearchResult extends SearchResult<Collection> { export class CollectionSearchResult extends SearchResult<Collection> {

View File

@@ -1,4 +1,4 @@
import { SearchResult } from '../../../search/search-result.model'; import { SearchResult } from '../../../+search-page/search-result.model';
import { Community } from '../../../core/shared/community.model'; import { Community } from '../../../core/shared/community.model';
export class CommunitySearchResult extends SearchResult<Community> { export class CommunitySearchResult extends SearchResult<Community> {

View File

@@ -1,4 +1,4 @@
import { SearchResult } from '../../../search/search-result.model'; import { SearchResult } from '../../../+search-page/search-result.model';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
export class ItemSearchResult extends SearchResult<Item> { export class ItemSearchResult extends SearchResult<Item> {

View File

@@ -2,7 +2,7 @@ import { Component, Inject } from '@angular/core';
import { ObjectListElementComponent } from '../object-list-element/object-list-element.component'; import { ObjectListElementComponent } from '../object-list-element/object-list-element.component';
import { ListableObject } from '../listable-object/listable-object.model'; import { ListableObject } from '../listable-object/listable-object.model';
import { SearchResult } from '../../search/search-result.model'; import { SearchResult } from '../../+search-page/search-result.model';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { Metadatum } from '../../core/shared/metadatum.model'; import { Metadatum } from '../../core/shared/metadatum.model';
import { isEmpty, hasNoValue } from '../../shared/empty.util'; import { isEmpty, hasNoValue } from '../../shared/empty.util';

View File

@@ -1,4 +0,0 @@
<div class="search-page">
<ds-search-form [query]="query" [scope]="scopeObject?.payload | async" [currentParams]="currentParams" [scopes]="scopeList?.payload"></ds-search-form>
<ds-search-results [searchResults]="results" [searchConfig]="searchOptions"></ds-search-results>
</div>

View File

@@ -1,3 +0,0 @@
<h2 *ngIf="(searchResults.payload | async)?.length > 0">{{ 'search.results.title' | translate }}</h2>
<ds-object-list [config]="searchConfig.pagination" [sortConfig]="searchConfig.sort"
[objects]="searchResults" [hideGear]="false"></ds-object-list>

View File

@@ -1,17 +0,0 @@
import { NgModule } from '@angular/core';
import { CoreModule } from '../core/core.module';
import { SearchService } from './search.service';
@NgModule({
imports: [
],
declarations: [
],
exports: [
],
providers: [
SearchService
]
})
export class SearchModule { }

View File

@@ -0,0 +1,24 @@
import { animate, style, transition, trigger } from '@angular/animations';
const fadeEnter = transition(':enter', [
style({ opacity: 0 }),
animate(300, style({ opacity: 1 }))
]);
const fadeLeave = transition(':leave', [
style({ opacity: 1 }),
animate(400, style({ opacity: 0 }))
]);
export const fadeIn = trigger('fadeIn', [
fadeEnter
]);
export const fadeOut = trigger('fadeOut', [
fadeLeave
]);
export const fadeInOut = trigger('fadeInOut', [
fadeEnter,
fadeLeave
]);

View File

@@ -0,0 +1,3 @@
<div>
<label>{{ message }}</label>
</div>

View File

@@ -0,0 +1,58 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-translate/core';
import { MockTranslateLoader } from '../testing/mock-translate-loader';
import { ErrorComponent } from './error.component';
describe('ErrorComponent (inline template)', () => {
let comp: ErrorComponent;
let fixture: ComponentFixture<ErrorComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
}),
],
declarations: [ ErrorComponent ], // declare the test component
providers: [ TranslateService ]
}).compileComponents(); // compile template and css
}));
beforeEach(() => {
fixture = TestBed.createComponent(ErrorComponent);
comp = fixture.componentInstance; // ErrorComponent test instance
// query for the message <label> by CSS element selector
de = fixture.debugElement.query(By.css('label'));
el = de.nativeElement;
});
it('should create', () => {
expect(comp).toBeTruthy();
});
it('should display default message', () => {
fixture.detectChanges();
expect(el.textContent).toContain(comp.message);
});
it('should display input message', () => {
comp.message = 'Test Message';
fixture.detectChanges();
expect(el.textContent).toContain('Test Message');
});
});

View File

@@ -0,0 +1,36 @@
import { Component, Input } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs/Subscription';
@Component({
selector: 'ds-error',
styleUrls: ['./error.component.scss'],
templateUrl: './error.component.html'
})
export class ErrorComponent {
@Input() message = 'Error...';
private subscription: Subscription;
constructor(private translate: TranslateService) {
}
ngOnInit() {
if (this.message === undefined) {
this.subscription = this.translate.get('error.default').subscribe((message: string) => {
this.message = message;
});
}
}
ngOnDestroy() {
if (this.subscription !== undefined) {
this.subscription.unsubscribe();
}
}
}

View File

@@ -0,0 +1,15 @@
<div>
<label>{{ message }}</label>
<div class="loader">
<span class="l-1"></span>
<span class="l-2"></span>
<span class="l-3"></span>
<span class="l-4"></span>
<span class="l-5"></span>
<span class="l-6"></span>
<span class="l-7"></span>
<span class="l-8"></span>
<span class="l-9"></span>
<span class="l-10"></span>
</div>
</div>

View File

@@ -0,0 +1,73 @@
.loader {
margin: 0px 25px;
}
span {
display: block;
margin: 0 auto;
}
span[class*="l-"] {
height: 4px;
width: 4px;
background: #000;
display: inline-block;
margin: 12px 2px;
border-radius: 100%;
-webkit-border-radius: 100%;
-moz-border-radius: 100%;
-webkit-animation: loader 2s infinite;
-webkit-animation-timing-function: cubic-bezier(0.030, 0.615, 0.995, 0.415);
-webkit-animation-fill-mode: both;
-moz-animation: loader 2s infinite;
-moz-animation-timing-function: cubic-bezier(0.030, 0.615, 0.995, 0.415);
-moz-animation-fill-mode: both;
-ms-animation: loader 2s infinite;
-ms-animation-timing-function: cubic-bezier(0.030, 0.615, 0.995, 0.415);
-ms-animation-fill-mode: both;
animation: loader 2s infinite;
animation-timing-function: cubic-bezier(0.030, 0.615, 0.995, 0.415);
animation-fill-mode: both;
}
span.l-1 {-webkit-animation-delay: 1s;animation-delay: 1s;-ms-animation-delay: 1s;-moz-animation-delay: 1s;}
span.l-2 {-webkit-animation-delay: 0.9s;animation-delay: 0.9s;-ms-animation-delay: 0.9s;-moz-animation-delay: 0.9s;}
span.l-3 {-webkit-animation-delay: 0.8s;animation-delay: 0.8s;-ms-animation-delay: 0.8s;-moz-animation-delay: 0.8s;}
span.l-4 {-webkit-animation-delay: 0.7s;animation-delay: 0.7s;-ms-animation-delay: 0.7s;-moz-animation-delay: 0.7s;}
span.l-5 {-webkit-animation-delay: 0.6s;animation-delay: 0.6s;-ms-animation-delay: 0.6s;-moz-animation-delay: 0.6s;}
span.l-6 {-webkit-animation-delay: 0.5s;animation-delay: 0.5s;-ms-animation-delay: 0.5s;-moz-animation-delay: 0.5s;}
span.l-7 {-webkit-animation-delay: 0.4s;animation-delay: 0.4s;-ms-animation-delay: 0.4s;-moz-animation-delay: 0.4s;}
span.l-8 {-webkit-animation-delay: 0.3s;animation-delay: 0.3s;-ms-animation-delay: 0.3s;-moz-animation-delay: 0.3s;}
span.l-9 {-webkit-animation-delay: 0.2s;animation-delay: 0.2s;-ms-animation-delay: 0.2s;-moz-animation-delay: 0.2s;}
span.l-9 {-webkit-animation-delay: 0.1s;animation-delay: 0.1s;-ms-animation-delay: 0.1s;-moz-animation-delay: 0.1s;}
span.l-10 {-webkit-animation-delay: 0s;animation-delay: 0s;-ms-animation-delay: 0s;-moz-animation-delay: 0s;}
@-webkit-keyframes loader {
0% {-webkit-transform: translateX(-30px); opacity: 0;}
25% {opacity: 1;}
50% {-webkit-transform: translateX(30px); opacity: 0;}
100% {opacity: 0;}
}
@-moz-keyframes loader {
0% {-moz-transform: translateX(-30px); opacity: 0;}
25% {opacity: 1;}
50% {-moz-transform: translateX(30px); opacity: 0;}
100% {opacity: 0;}
}
@-keyframes loader {
0% {-transform: translateX(-30px); opacity: 0;}
25% {opacity: 1;}
50% {-transform: translateX(30px); opacity: 0;}
100% {opacity: 0;}
}
@-ms-keyframes loader {
0% {-ms-transform: translateX(-30px); opacity: 0;}
25% {opacity: 1;}
50% {-ms-transform: translateX(30px); opacity: 0;}
100% {opacity: 0;}
}

View File

@@ -0,0 +1,58 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { TranslateModule, TranslateLoader, TranslateService } from '@ngx-translate/core';
import { MockTranslateLoader } from '../testing/mock-translate-loader';
import { LoadingComponent } from './loading.component';
describe('LoadingComponent (inline template)', () => {
let comp: LoadingComponent;
let fixture: ComponentFixture<LoadingComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader
}
}),
],
declarations: [ LoadingComponent ], // declare the test component
providers: [ TranslateService ]
}).compileComponents(); // compile template and css
}));
beforeEach(() => {
fixture = TestBed.createComponent(LoadingComponent);
comp = fixture.componentInstance; // LoadingComponent test instance
// query for the message <label> by CSS element selector
de = fixture.debugElement.query(By.css('label'));
el = de.nativeElement;
});
it('should create', () => {
expect(comp).toBeTruthy();
});
it('should display default message', () => {
fixture.detectChanges();
expect(el.textContent).toContain(comp.message);
});
it('should display input message', () => {
comp.message = 'Test Message';
fixture.detectChanges();
expect(el.textContent).toContain('Test Message');
});
});

View File

@@ -0,0 +1,36 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core';
import { TranslateService } from '@ngx-translate/core';
import { Subscription } from 'rxjs/Subscription';
@Component({
selector: 'ds-loading',
styleUrls: ['./loading.component.scss'],
templateUrl: './loading.component.html'
})
export class LoadingComponent implements OnDestroy, OnInit {
@Input() message: string;
private subscription: Subscription;
constructor(private translate: TranslateService) {
}
ngOnInit() {
if (this.message === undefined) {
this.subscription = this.translate.get('loading.default').subscribe((message: string) => {
this.message = message;
});
}
}
ngOnDestroy() {
if (this.subscription !== undefined) {
this.subscription.unsubscribe();
}
}
}

View File

@@ -0,0 +1,204 @@
import { ComponentFixture, TestBed, async, tick, fakeAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { DebugElement } from '@angular/core';
import { SearchFormComponent } from './search-form.component';
import { Observable } from 'rxjs/Observable';
import { FormsModule } from '@angular/forms';
import { ResourceType } from '../../core/shared/resource-type';
import { RouterTestingModule } from '@angular/router/testing';
import { Community } from '../../core/shared/community.model';
import { TranslateModule } from '@ngx-translate/core';
describe('SearchFormComponent', () => {
let comp: SearchFormComponent;
let fixture: ComponentFixture<SearchFormComponent>;
let de: DebugElement;
let el: HTMLElement;
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [FormsModule, RouterTestingModule, TranslateModule.forRoot()],
declarations: [SearchFormComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(SearchFormComponent);
comp = fixture.componentInstance; // SearchFormComponent test instance
de = fixture.debugElement.query(By.css('form'));
el = de.nativeElement;
});
it('should display scopes when available with default and all scopes', () => {
comp.scopes = Observable.of(objects);
fixture.detectChanges();
const select: HTMLElement = de.query(By.css('select')).nativeElement;
expect(select).toBeDefined();
const options: HTMLCollection = select.children;
const defOption: Element = options.item(0);
expect(defOption.getAttribute('value')).toBe('');
let index = 1;
objects.forEach((object) => {
expect(options.item(index).textContent).toBe(object.name);
expect(options.item(index).getAttribute('value')).toBe(object.uuid);
index++;
});
});
it('should not display scopes when empty', () => {
fixture.detectChanges();
const select = de.query(By.css('select'));
expect(select).toBeNull();
});
it('should display set query value in input field', fakeAsync(() => {
const testString = 'This is a test query';
comp.query = testString;
fixture.detectChanges();
tick();
const queryInput = de.query(By.css('input')).nativeElement;
expect(queryInput.value).toBe(testString);
}));
it('should select correct scope option in scope select', fakeAsync(() => {
comp.scopes = Observable.of(objects);
fixture.detectChanges();
const testCommunity = objects[1];
comp.scope = testCommunity;
fixture.detectChanges();
tick();
const scopeSelect = de.query(By.css('select')).nativeElement;
expect(scopeSelect.value).toBe(testCommunity.id);
}));
// it('should call updateSearch when clicking the submit button with correct parameters', fakeAsync(() => {
// comp.query = 'Test String'
// fixture.detectChanges();
// spyOn(comp, 'updateSearch').and.callThrough();
// fixture.detectChanges();
//
// const submit = de.query(By.css('button.search-button')).nativeElement;
// const scope = '123456';
// const query = 'test';
// const select = de.query(By.css('select')).nativeElement;
// const input = de.query(By.css('input')).nativeElement;
//
// tick();
// select.value = scope;
// input.value = query;
//
// fixture.detectChanges();
//
// submit.click();
//
// expect(comp.updateSearch).toHaveBeenCalledWith({ scope: scope, query: query });
// }));
});
export const objects = [
Object.assign(new Community(), {
handle: '10673/11',
logo: {
self: {
_isScalar: true,
value: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/10b636d0-7890-4968-bcd6-0d83bf4e2b42',
scheduler: null
}
},
collections: {
self: {
_isScalar: true,
value: '1506937433727',
scheduler: null
}
},
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/communities/7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
id: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
uuid: '7669c72a-3f2a-451f-a3b9-9210e7a4c02f',
type: ResourceType.Community,
name: 'OR2017 - Demonstration',
metadata: [
{
key: 'dc.description',
language: null,
value: ''
},
{
key: 'dc.description.abstract',
language: null,
value: 'This is a test community to hold content for the OR2017 demostration'
},
{
key: 'dc.description.tableofcontents',
language: null,
value: ''
},
{
key: 'dc.rights',
language: null,
value: ''
},
{
key: 'dc.title',
language: null,
value: 'OR2017 - Demonstration'
}
]
}),
Object.assign(new Community(),
{
handle: '10673/1',
logo: {
self: {
_isScalar: true,
value: 'https://dspace7.4science.it/dspace-spring-rest/api/core/bitstreams/f446c17d-6d51-45ea-a610-d58a73642d40',
scheduler: null
}
},
collections: {
self: {
_isScalar: true,
value: '1506937433727',
scheduler: null
}
},
self: 'https://dspace7.4science.it/dspace-spring-rest/api/core/communities/9076bd16-e69a-48d6-9e41-0238cb40d863',
id: '9076bd16-e69a-48d6-9e41-0238cb40d863',
uuid: '9076bd16-e69a-48d6-9e41-0238cb40d863',
type: ResourceType.Community,
name: 'Sample Community',
metadata: [
{
key: 'dc.description',
language: null,
value: '<p>This is the introductory text for the <em>Sample Community</em> on the DSpace Demonstration Site. It is editable by System or Community Administrators (of this Community).</p>\r\n<p><strong>DSpace Communities may contain one or more Sub-Communities or Collections (of Items).</strong></p>\r\n<p>This particular Community has its own logo (the <a href=\'http://www.duraspace.org/\'>DuraSpace</a> logo).</p>'
},
{
key: 'dc.description.abstract',
language: null,
value: 'This is a sample top-level community'
},
{
key: 'dc.description.tableofcontents',
language: null,
value: '<p>This is the <em>news section</em> for this <em>Sample Community</em>. System or Community Administrators (of this Community) can edit this News field.</p>'
},
{
key: 'dc.rights',
language: null,
value: '<p><em>If this Community had special copyright text to display, it would be displayed here.</em></p>'
},
{
key: 'dc.title',
language: null,
value: 'Sample Community'
}
]
}
)
];

View File

@@ -1,4 +1,4 @@
import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Component, Input } from '@angular/core';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { isNotEmpty, hasValue, isEmpty } from '../empty.util'; import { isNotEmpty, hasValue, isEmpty } from '../empty.util';
@@ -13,16 +13,14 @@ import { Observable } from 'rxjs/Observable';
@Component({ @Component({
selector: 'ds-search-form', selector: 'ds-search-form',
styleUrls: ['./search-form.component.scss'], styleUrls: ['./search-form.component.scss'],
templateUrl: './search-form.component.html', templateUrl: './search-form.component.html'
}) })
export class SearchFormComponent implements OnInit, OnDestroy { export class SearchFormComponent {
@Input() query: string; @Input() query: string;
selectedId = ''; selectedId = '';
// Optional existing search parameters // Optional existing search parameters
@Input() currentParams: {}; @Input() currentParams: {};
@Input() scopes: Observable<DSpaceObject[]>; @Input() scopes: Observable<DSpaceObject[]>;
scopeOptions: string[] = [];
sub;
@Input() @Input()
set scope(dso: DSpaceObject) { set scope(dso: DSpaceObject) {
@@ -31,19 +29,6 @@ export class SearchFormComponent implements OnInit, OnDestroy {
} }
} }
ngOnInit(): void {
if (this.scopes) {
this.sub =
this.scopes
.filter((scopes: DSpaceObject[]) => isEmpty(scopes))
.subscribe((scopes: DSpaceObject[]) => {
this.scopeOptions = scopes
.map((scope: DSpaceObject) => scope.id);
}
);
}
}
constructor(private router: Router) { constructor(private router: Router) {
} }
@@ -75,9 +60,4 @@ export class SearchFormComponent implements OnInit, OnDestroy {
return id1 === id2; return id1 === id2;
} }
ngOnDestroy(): void {
if (this.sub) {
this.sub.unsubscribe();
}
}
} }

View File

@@ -9,24 +9,26 @@ import { TranslateModule } from '@ngx-translate/core';
import { NgxPaginationModule } from 'ngx-pagination'; import { NgxPaginationModule } from 'ngx-pagination';
import { PaginationComponent } from './pagination/pagination.component'; import { EnumKeysPipe } from './utils/enum-keys-pipe';
import { FileSizePipe } from './utils/file-size-pipe'; import { FileSizePipe } from './utils/file-size-pipe';
import { ThumbnailComponent } from '../thumbnail/thumbnail.component';
import { SafeUrlPipe } from './utils/safe-url-pipe'; import { SafeUrlPipe } from './utils/safe-url-pipe';
import { TruncatePipe } from './utils/truncate.pipe';
import { CollectionListElementComponent } from '../object-list/collection-list-element/collection-list-element.component';
import { ComcolPageContentComponent } from './comcol-page-content/comcol-page-content.component'; import { ComcolPageContentComponent } from './comcol-page-content/comcol-page-content.component';
import { ComcolPageHeaderComponent } from './comcol-page-header/comcol-page-header.component'; import { ComcolPageHeaderComponent } from './comcol-page-header/comcol-page-header.component';
import { ComcolPageLogoComponent } from './comcol-page-logo/comcol-page-logo.component'; import { ComcolPageLogoComponent } from './comcol-page-logo/comcol-page-logo.component';
import { EnumKeysPipe } from './utils/enum-keys-pipe';
import { ObjectListComponent } from './object-list/object-list.component';
import { ObjectListElementComponent } from '../object-list/object-list-element/object-list-element.component';
import { ItemListElementComponent } from '../object-list/item-list-element/item-list-element.component';
import { CommunityListElementComponent } from '../object-list/community-list-element/community-list-element.component'; import { CommunityListElementComponent } from '../object-list/community-list-element/community-list-element.component';
import { CollectionListElementComponent } from '../object-list/collection-list-element/collection-list-element.component'; import { ErrorComponent } from './error/error.component';
import { TruncatePipe } from './utils/truncate.pipe'; import { LoadingComponent } from './loading/loading.component';
import { WrapperListElementComponent } from '../object-list/wrapper-list-element/wrapper-list-element.component'; import { ItemListElementComponent } from '../object-list/item-list-element/item-list-element.component';
import { ObjectListComponent } from '../object-list/object-list.component';
import { ObjectListElementComponent } from '../object-list/object-list-element/object-list-element.component';
import { PaginationComponent } from './pagination/pagination.component';
import { ThumbnailComponent } from '../thumbnail/thumbnail.component';
import { SearchResultListElementComponent } from '../object-list/search-result-list-element/search-result-list-element.component'; import { SearchResultListElementComponent } from '../object-list/search-result-list-element/search-result-list-element.component';
import { SearchFormComponent } from './search-form/search-form.component'; import { SearchFormComponent } from './search-form/search-form.component';
import { WrapperListElementComponent } from '../object-list/wrapper-list-element/wrapper-list-element.component';
const MODULES = [ const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here // Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -41,30 +43,32 @@ const MODULES = [
const PIPES = [ const PIPES = [
// put shared pipes here // put shared pipes here
EnumKeysPipe,
FileSizePipe, FileSizePipe,
SafeUrlPipe, SafeUrlPipe,
EnumKeysPipe,
TruncatePipe TruncatePipe
]; ];
const COMPONENTS = [ const COMPONENTS = [
// put shared components here // put shared components here
PaginationComponent,
ThumbnailComponent,
ComcolPageContentComponent, ComcolPageContentComponent,
ComcolPageHeaderComponent, ComcolPageHeaderComponent,
ComcolPageLogoComponent, ComcolPageLogoComponent,
ErrorComponent,
LoadingComponent,
ObjectListComponent, ObjectListComponent,
ObjectListElementComponent, ObjectListElementComponent,
WrapperListElementComponent, PaginationComponent,
SearchFormComponent SearchFormComponent,
ThumbnailComponent,
WrapperListElementComponent
]; ];
const ENTRY_COMPONENTS = [ const ENTRY_COMPONENTS = [
// put shared entry components (components that are created dynamically) here // put shared entry components (components that are created dynamically) here
ItemListElementComponent,
CollectionListElementComponent, CollectionListElementComponent,
CommunityListElementComponent, CommunityListElementComponent,
ItemListElementComponent,
SearchResultListElementComponent SearchResultListElementComponent
]; ];

View File

@@ -7,7 +7,7 @@ import { bootloader } from '@angularclass/bootloader';
import { load as loadWebFont } from 'webfontloader'; import { load as loadWebFont } from 'webfontloader';
import { BrowserAppModule } from './app/browser-app.module'; import { BrowserAppModule } from './modules/app/browser-app.module';
import { ENV_CONFIG } from './config'; import { ENV_CONFIG } from './config';

View File

@@ -8,18 +8,14 @@ import * as https from 'https';
import * as morgan from 'morgan'; import * as morgan from 'morgan';
import * as express from 'express'; import * as express from 'express';
import * as bodyParser from 'body-parser'; import * as bodyParser from 'body-parser';
import * as session from 'express-session';
import * as compression from 'compression'; import * as compression from 'compression';
import * as cookieParser from 'cookie-parser'; import * as cookieParser from 'cookie-parser';
import { platformServer, renderModuleFactory } from '@angular/platform-server';
import { enableProdMode } from '@angular/core'; import { enableProdMode } from '@angular/core';
import { ngExpressEngine } from '@nguniversal/express-engine'; 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';
import { ROUTES } from './routes'; import { ROUTES } from './routes';
import { ENV_CONFIG } from './config'; import { ENV_CONFIG } from './config';

View File

@@ -1,24 +1,24 @@
import { NgModule, APP_INITIALIZER } from '@angular/core'; import { HttpClient, HttpClientModule } from '@angular/common/http';
import { HttpClientModule, HttpClient } from '@angular/common/http'; import { APP_INITIALIZER, NgModule } from '@angular/core';
import { RouterModule } from '@angular/router';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { BrowserAnimationsModule } from '@angular/platform-browser/animations';
import { RouterModule } from '@angular/router';
import { IdlePreload, IdlePreloadModule } from '@angularclass/idle-preload'; import { IdlePreload, IdlePreloadModule } from '@angularclass/idle-preload';
import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';
import { TransferState } from '../modules/transfer-state/transfer-state'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { BrowserTransferStateModule } from '../modules/transfer-state/browser-transfer-state.module'; import { TranslateHttpLoader } from '@ngx-translate/http-loader';
import { BrowserTransferStoreEffects } from '../modules/transfer-store/browser-transfer-store.effects';
import { BrowserTransferStoreModule } from '../modules/transfer-store/browser-transfer-store.module';
import { AppModule } from './app.module'; import { AppComponent } from '../../app/app.component';
import { CoreModule } from './core/core.module';
import { AppComponent } from './app.component'; import { AppModule } from '../../app/app.module';
import { BrowserTransferStateModule } from '../transfer-state/browser-transfer-state.module';
import { TransferState } from '../transfer-state/transfer-state';
import { BrowserTransferStoreEffects } from '../transfer-store/browser-transfer-store.effects';
import { BrowserTransferStoreModule } from '../transfer-store/browser-transfer-store.module';
export function init(cache: TransferState) { export function init(cache: TransferState) {
return () => { return () => {
@@ -45,6 +45,7 @@ export function createTranslateLoader(http: HttpClient) {
preloadingStrategy: preloadingStrategy:
IdlePreload IdlePreload
}), }),
BrowserAnimationsModule,
BrowserTransferStateModule, BrowserTransferStateModule,
BrowserTransferStoreModule, BrowserTransferStoreModule,
TranslateModule.forRoot({ TranslateModule.forRoot({

View File

@@ -5,6 +5,7 @@ import { ApplicationRef, NgModule, APP_BOOTSTRAP_LISTENER } from '@angular/core'
import { RouterModule } from '@angular/router'; import { RouterModule } from '@angular/router';
import { ServerModule } from '@angular/platform-server'; import { ServerModule } from '@angular/platform-server';
import { BrowserModule } from '@angular/platform-browser'; import { BrowserModule } from '@angular/platform-browser';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { Request } from 'express'; import { Request } from 'express';
@@ -15,21 +16,21 @@ import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { EffectsModule } from '@ngrx/effects'; 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 { ServerTransferStateModule } from '../transfer-state/server-transfer-state.module';
import { TransferState } from '../modules/transfer-state/transfer-state'; import { TransferState } from '../transfer-state/transfer-state';
import { ServerTransferStoreEffects } from '../modules/transfer-store/server-transfer-store.effects'; import { ServerTransferStoreEffects } from '../transfer-store/server-transfer-store.effects';
import { ServerTransferStoreModule } from '../modules/transfer-store/server-transfer-store.module'; import { ServerTransferStoreModule } from '../transfer-store/server-transfer-store.module';
import { AppState } from './app.reducer'; import { AppState } from '../../app/app.reducer';
import { AppModule } from './app.module'; import { AppModule } from '../../app/app.module';
import { AppComponent } from './app.component'; import { AppComponent } from '../../app/app.component';
import { GLOBAL_CONFIG, GlobalConfig } from '../config'; import { GLOBAL_CONFIG, GlobalConfig } from '../../config';
export function boot(cache: TransferState, appRef: ApplicationRef, store: Store<AppState>, request: Request, config: GlobalConfig) { export function boot(cache: TransferState, appRef: ApplicationRef, store: Store<AppState>, request: Request, config: GlobalConfig) {
// authentication mechanism goes here // authentication mechanism goes here
@@ -53,6 +54,7 @@ export function createTranslateLoader() {
RouterModule.forRoot([], { RouterModule.forRoot([], {
useHash: false useHash: false
}), }),
NoopAnimationsModule,
ServerTransferStateModule, ServerTransferStateModule,
ServerTransferStoreModule, ServerTransferStoreModule,
TranslateModule.forRoot({ TranslateModule.forRoot({

View File

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

View File

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

View File

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

View File

@@ -5,9 +5,10 @@ const {
/** /**
* Webpack Plugins * Webpack Plugins
*/ */
const ProvidePlugin = require('webpack/lib/ProvidePlugin'); const ContextReplacementPlugin = require('webpack/lib/ContextReplacementPlugin');
const DefinePlugin = require('webpack/lib/DefinePlugin'); const DefinePlugin = require('webpack/lib/DefinePlugin');
const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin'); const LoaderOptionsPlugin = require('webpack/lib/LoaderOptionsPlugin');
const ProvidePlugin = require('webpack/lib/ProvidePlugin');
/** /**
* Webpack Constants * Webpack Constants
@@ -75,8 +76,9 @@ module.exports = function (options) {
loader: 'source-map-loader', loader: 'source-map-loader',
exclude: [ exclude: [
// these packages have problems with their sourcemaps // these packages have problems with their sourcemaps
root('node_modules/rxjs'), root('node_modules/@angular'),
root('node_modules/@angular') root('node_modules/@nguniversal'),
root('node_modules/rxjs')
] ]
}, },
@@ -221,6 +223,11 @@ module.exports = function (options) {
*/ */
plugins: [ plugins: [
new ContextReplacementPlugin(
/angular(\\|\/)core(\\|\/)@angular/,
root('./src'), {}
),
/** /**
* Plugin: DefinePlugin * Plugin: DefinePlugin
* Description: Define free variables. * Description: Define free variables.

View File

@@ -106,9 +106,9 @@
version "4.0.3" version "4.0.3"
resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-4.0.3.tgz#36abacdfa19bfb8506e40de80bae06050a1e15e9" resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-4.0.3.tgz#36abacdfa19bfb8506e40de80bae06050a1e15e9"
"@ngtools/webpack@1.7.3": "@ngtools/webpack@1.7.2":
version "1.7.3" version "1.7.2"
resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-1.7.3.tgz#20d5bcca0d902e1f3e5accf4922f482539c93a3b" resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-1.7.2.tgz#3fc4de01786dcc2f50d8cbaaa117311e56799977"
dependencies: dependencies:
enhanced-resolve "^3.1.0" enhanced-resolve "^3.1.0"
loader-utils "^1.0.2" loader-utils "^1.0.2"