Merge remote-tracking branch 'origin/master' into w2p-44024_simple-search-with-select

Conflicts:
	package.json
	src/app/core/cache/builders/remote-data-build.service.ts
	src/app/shared/shared.module.ts
This commit is contained in:
Lotte Hofstede
2017-09-20 16:07:34 +02:00
43 changed files with 325 additions and 278 deletions

View File

@@ -15,8 +15,6 @@ node_js:
cache:
yarn: true
directories:
- node_modules
bundler_args: --retry 5

View File

@@ -80,10 +80,9 @@
"@angularclass/bootloader": "1.0.1",
"@angularclass/idle-preload": "1.0.4",
"@ng-bootstrap/ng-bootstrap": "^1.0.0-beta.1",
"@ngrx/core": "1.2.0",
"@ngrx/effects": "2.0.4",
"@ngrx/router-store": "1.2.6",
"@ngrx/store": "2.2.3",
"@ngrx/effects": "^4.0.5",
"@ngrx/router-store": "^4.0.4",
"@ngrx/store": "^4.0.3",
"@nguniversal/express-engine": "1.0.0-beta.2",
"@ngx-translate/core": "7.1.0",
"@ngx-translate/http-loader": "0.1.0",
@@ -113,7 +112,7 @@
"devDependencies": {
"@angular/compiler": "4.3.1",
"@angular/compiler-cli": "4.3.1",
"@ngrx/store-devtools": "3.2.4",
"@ngrx/store-devtools": "^4.0.0",
"@ngtools/webpack": "1.5.1",
"@types/cookie-parser": "1.3.30",
"@types/deep-freeze": "0.1.1",
@@ -143,6 +142,7 @@
"imports-loader": "0.7.1",
"istanbul-instrumenter-loader": "2.0.0",
"jasmine-core": "2.6.4",
"jasmine-marbles": "^0.1.0",
"jasmine-spec-reporter": "4.1.1",
"json-loader": "0.5.4",
"karma": "1.7.0",
@@ -158,7 +158,6 @@
"karma-sourcemap-loader": "0.3.7",
"karma-webdriver-launcher": "1.0.5",
"karma-webpack": "2.0.4",
"ngrx-store-freeze": "0.1.9",
"node-sass": "4.5.3",
"nodemon": "1.11.0",
"npm-run-all": "4.0.2",

View File

@@ -45,7 +45,7 @@ describe('App component', () => {
return TestBed.configureTestingModule({
imports: [
CommonModule,
StoreModule.provideStore({}),
StoreModule.forRoot({}),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,

View File

@@ -1,11 +1,8 @@
import { EffectsModule } from '@ngrx/effects';
import { HeaderEffects } from './header/header.effects';
import { StoreEffects } from './store.effects';
import { coreEffects } from './core/core.effects';
export const effects = [
...coreEffects, // TODO: should probably be imported in coreModule
EffectsModule.run(StoreEffects),
EffectsModule.run(HeaderEffects)
export const appEffects = [
StoreEffects,
HeaderEffects
];

View File

@@ -0,0 +1,38 @@
import { isNotEmpty } from './shared/empty.util';
import { StoreActionTypes } from './store.actions';
// fallback ngrx debugger
let actionCounter = 0;
export function debugMetaReducer(reducer) {
return (state, action) => {
if (isNotEmpty(console.debug)) {
actionCounter++;
console.debug('@ngrx action', actionCounter, action.type);
console.debug('state', state);
console.debug('action', action);
console.debug('------------------------------------');
}
return reducer(state, action);
}
}
export function universalMetaReducer(reducer) {
return (state, action) => {
switch (action.type) {
case StoreActionTypes.REHYDRATE:
state = Object.assign({}, state, action.payload);
break;
case StoreActionTypes.REPLAY:
default:
break;
}
return reducer(state, action);
}
}
export const appMetaReducers = [
// debugMetaReducer,
universalMetaReducer,
];

View File

@@ -2,12 +2,11 @@ import { NgModule } from '@angular/core';
import { HttpModule } from '@angular/http';
import { FormsModule } from '@angular/forms';
import { StoreModule, Store } from '@ngrx/store';
import { RouterStoreModule } from '@ngrx/router-store';
import { StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { rootReducer, AppState } from './app.reducer';
import { effects } from './app.effects';
import { appReducers } from './app.reducer';
import { appEffects } from './app.effects';
import { CoreModule } from './core/core.module';
import { SharedModule } from './shared/shared.module';
@@ -28,6 +27,8 @@ import { HeaderComponent } from './header/header.component';
import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
import { GLOBAL_CONFIG, ENV_CONFIG } from '../config';
import { EffectsModule } from '@ngrx/effects';
import { appMetaReducers } from './app.metareducers';
export function getConfig() {
return ENV_CONFIG;
@@ -46,10 +47,9 @@ export function getConfig() {
CommunityPageModule,
SearchPageModule,
AppRoutingModule,
StoreModule.provideStore(rootReducer),
RouterStoreModule.connectRouter(),
StoreDevtoolsModule.instrumentOnlyWithExtension(),
effects
StoreModule.forRoot(appReducers, { metaReducers: appMetaReducers }),
StoreDevtoolsModule.instrument({ maxAge: 50 }),
EffectsModule.forRoot(appEffects)
],
providers: [
{ provide: GLOBAL_CONFIG, useFactory: (getConfig) },

View File

@@ -1,45 +1,17 @@
import { combineReducers, ActionReducer } from '@ngrx/store';
import { routerReducer, RouterState } from '@ngrx/router-store';
import { storeFreeze } from 'ngrx-store-freeze';
import { compose } from '@ngrx/core';
import { ActionReducerMap } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store';
import { headerReducer, HeaderState } from './header/header.reducer';
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
import { CoreState, coreReducer } from './core/core.reducers';
import { StoreActionTypes } from './store.actions';
import { ENV_CONFIG } from '../config';
export interface AppState {
core: CoreState;
router: RouterState;
router: fromRouter.RouterReducerState;
hostWindow: HostWindowState;
header: HeaderState;
}
export const reducers = {
core: coreReducer,
router: routerReducer,
export const appReducers: ActionReducerMap<AppState> = {
router: fromRouter.routerReducer,
hostWindow: hostWindowReducer,
header: headerReducer
};
export function rootReducer(state: any, action: any) {
switch (action.type) {
case StoreActionTypes.REHYDRATE:
state = Object.assign({}, state, action.payload);
break;
case StoreActionTypes.REPLAY:
break;
default:
}
let root: ActionReducer<any>;
// TODO: attempt to not use InjectionToken GLOBAL_CONFIG over GlobalConfig ENV_CONFIG
if (ENV_CONFIG.production) {
root = combineReducers(reducers)(state, action);
} else {
root = compose(storeFreeze, combineReducers)(reducers)(state, action);
}
return root;
}

View File

@@ -25,6 +25,8 @@ import { CoreModule } from './core/core.module';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer';
export function init(cache: TransferState) {
return () => {
@@ -56,7 +58,8 @@ export function HttpLoaderFactory(http: Http) {
BrowserDataLoaderModule,
BrowserTransferStateModule,
BrowserTransferStoreModule,
EffectsModule.run(BrowserTransferStoreEffects),
EffectsModule.forRoot([BrowserTransferStoreEffects]),
StoreRouterConnectingModule,
BrowserAnimationsModule,
AppModule
],
@@ -68,6 +71,10 @@ export function HttpLoaderFactory(http: Http) {
deps: [
TransferState
]
},
{
provide: RouterStateSerializer,
useClass: DSpaceRouterStateSerializer
}
]
})

View File

@@ -1,12 +1,10 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { CacheableObject } from '../object-cache.reducer';
import { ObjectCacheService } from '../object-cache.service';
import { RequestService } from '../../data/request.service';
import { ResponseCacheService } from '../response-cache.service';
import { CoreState } from '../../core.reducers';
import { RequestEntry } from '../../data/request.reducer';
import { hasValue, isNotEmpty } from '../../../shared/empty.util';
import { ResponseCacheEntry } from '../response-cache.reducer';
@@ -20,10 +18,12 @@ import { PageInfo } from '../../shared/page-info.model';
@Injectable()
export class RemoteDataBuildService {
constructor(protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected store: Store<CoreState>,) {
constructor(
protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService,
protected requestService: RequestService
) {
}
buildSingle<TNormalized extends CacheableObject, TDomain>(href: string,
@@ -31,9 +31,9 @@ export class RemoteDataBuildService {
const requestHrefObs = this.objectCache.getRequestHrefBySelfLink(href);
const requestObs = Observable.race(
this.store.select<RequestEntry>('core', 'data', 'request', href).filter((entry) => hasValue(entry)),
this.requestService.get(href).filter((entry) => hasValue(entry)),
requestHrefObs.flatMap((requestHref) =>
this.store.select<RequestEntry>('core', 'data', 'request', requestHref)).filter((entry) => hasValue(entry))
this.requestService.get(requestHref)).filter((entry) => hasValue(entry))
);
const responseCacheObs = Observable.race(
@@ -97,7 +97,7 @@ export class RemoteDataBuildService {
).filter((normalized) => hasValue(normalized))
.map((normalized: TNormalized) => {
return this.build<TNormalized, TDomain>(normalized);
});
}).distinctUntilChanged();
return new RemoteData(
href,
@@ -113,7 +113,7 @@ export class RemoteDataBuildService {
buildList<TNormalized extends CacheableObject, TDomain>(href: string,
normalizedType: GenericConstructor<TNormalized>): RemoteData<TDomain[]> {
const requestObs = this.store.select<RequestEntry>('core', 'data', 'request', href)
const requestObs = this.requestService.get(href)
.filter((entry) => hasValue(entry));
const responseCacheObs = this.responseCache.get(href).filter((entry) => hasValue(entry));

View File

@@ -1,18 +0,0 @@
import { combineReducers } from '@ngrx/store';
import { ResponseCacheState, responseCacheReducer } from './response-cache.reducer';
import { ObjectCacheState, objectCacheReducer } from './object-cache.reducer';
export interface CacheState {
response: ResponseCacheState,
object: ObjectCacheState
}
export const reducers = {
response: responseCacheReducer,
object: objectCacheReducer
};
export function cacheReducer(state: any, action: any) {
return combineReducers(reducers)(state, action);
}

View File

@@ -48,7 +48,7 @@ const initialState: ObjectCacheState = Object.create(null);
* @return ObjectCacheState
* the new state
*/
export const objectCacheReducer = (state = initialState, action: ObjectCacheAction): ObjectCacheState => {
export function objectCacheReducer(state = initialState, action: ObjectCacheAction): ObjectCacheState {
switch (action.type) {
case ObjectCacheActionTypes.ADD: {
@@ -67,7 +67,7 @@ export const objectCacheReducer = (state = initialState, action: ObjectCacheActi
return state;
}
}
};
}
/**
* Add an object to the cache

View File

@@ -2,8 +2,9 @@ import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { ObjectCacheService } from './object-cache.service';
import { ObjectCacheState, CacheableObject } from './object-cache.reducer';
import { CacheableObject } from './object-cache.reducer';
import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
import { CoreState } from '../core.reducers';
class TestClass implements CacheableObject {
constructor(
@@ -18,7 +19,7 @@ class TestClass implements CacheableObject {
describe('ObjectCacheService', () => {
let service: ObjectCacheService;
let store: Store<ObjectCacheState>;
let store: Store<CoreState>;
const uuid = '1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
const requestHref = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
@@ -36,7 +37,7 @@ describe('ObjectCacheService', () => {
const invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 });
beforeEach(() => {
store = new Store<ObjectCacheState>(undefined, undefined, undefined);
store = new Store<CoreState>(undefined, undefined, undefined);
spyOn(store, 'dispatch');
service = new ObjectCacheService(store);

View File

@@ -1,12 +1,22 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { MemoizedSelector, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { ObjectCacheState, ObjectCacheEntry, CacheableObject } from './object-cache.reducer';
import { ObjectCacheEntry, CacheableObject } from './object-cache.reducer';
import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
import { hasNoValue } from '../../shared/empty.util';
import { GenericConstructor } from '../shared/generic-constructor';
import { CoreState } from '../core.reducers';
import { keySelector } from '../shared/selectors';
function objectFromUuidSelector(uuid: string): MemoizedSelector<CoreState, ObjectCacheEntry> {
return keySelector<ObjectCacheEntry>('data/object', uuid);
}
function uuidFromHrefSelector(href: string): MemoizedSelector<CoreState, string> {
return keySelector<string>('index/href', href);
}
/**
* A service to interact with the object cache
@@ -14,7 +24,7 @@ import { GenericConstructor } from '../shared/generic-constructor';
@Injectable()
export class ObjectCacheService {
constructor(
private store: Store<ObjectCacheState>
private store: Store<CoreState>
) { }
/**
@@ -65,12 +75,12 @@ export class ObjectCacheService {
}
getBySelfLink<T extends CacheableObject>(href: string, type: GenericConstructor<T>): Observable<T> {
return this.store.select<string>('core', 'index', 'href', href)
return this.store.select(uuidFromHrefSelector(href))
.flatMap((uuid: string) => this.get(uuid, type))
}
private getEntry(uuid: string): Observable<ObjectCacheEntry> {
return this.store.select<ObjectCacheEntry>('core', 'cache', 'object', uuid)
return this.store.select(objectFromUuidSelector(uuid))
.filter((entry) => this.isValid(entry))
.distinctUntilChanged();
}
@@ -82,7 +92,7 @@ export class ObjectCacheService {
}
getRequestHrefBySelfLink(self: string): Observable<string> {
return this.store.select<string>('core', 'index', 'href', self)
return this.store.select(uuidFromHrefSelector(self))
.flatMap((uuid: string) => this.getRequestHref(uuid));
}
@@ -123,7 +133,7 @@ export class ObjectCacheService {
has(uuid: string): boolean {
let result: boolean;
this.store.select<ObjectCacheEntry>('core', 'cache', 'object', uuid)
this.store.select(objectFromUuidSelector(uuid))
.take(1)
.subscribe((entry) => result = this.isValid(entry));
@@ -142,7 +152,7 @@ export class ObjectCacheService {
hasBySelfLink(href: string): boolean {
let result = false;
this.store.select<string>('core', 'index', 'href', href)
this.store.select(uuidFromHrefSelector(href))
.take(1)
.subscribe((uuid: string) => result = this.has(uuid));

View File

@@ -37,7 +37,7 @@ const initialState = Object.create(null);
* @return ResponseCacheState
* the new state
*/
export const responseCacheReducer = (state = initialState, action: ResponseCacheAction): ResponseCacheState => {
export function responseCacheReducer(state = initialState, action: ResponseCacheAction): ResponseCacheState {
switch (action.type) {
case ResponseCacheActionTypes.ADD: {
@@ -56,7 +56,7 @@ export const responseCacheReducer = (state = initialState, action: ResponseCache
return state;
}
}
};
}
function addToCache(state: ResponseCacheState, action: ResponseCacheAddAction): ResponseCacheState {
return Object.assign({}, state, {

View File

@@ -1,12 +1,18 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { MemoizedSelector, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { ResponseCacheState, ResponseCacheEntry } from './response-cache.reducer';
import { ResponseCacheEntry } from './response-cache.reducer';
import { hasNoValue } from '../../shared/empty.util';
import { ResponseCacheRemoveAction, ResponseCacheAddAction } from './response-cache.actions';
import { Response } from './response-cache.models';
import { CoreState } from '../core.reducers';
import { keySelector } from '../shared/selectors';
function entryFromKeySelector(key: string): MemoizedSelector<CoreState, ResponseCacheEntry> {
return keySelector<ResponseCacheEntry>('data/response', key);
}
/**
* A service to interact with the response cache
@@ -14,7 +20,7 @@ import { Response } from './response-cache.models';
@Injectable()
export class ResponseCacheService {
constructor(
private store: Store<ResponseCacheState>
private store: Store<CoreState>
) { }
add(key: string, response: Response, msToLive: number): Observable<ResponseCacheEntry> {
@@ -34,8 +40,9 @@ export class ResponseCacheService {
* an observable of the ResponseCacheEntry with the specified key
*/
get(key: string): Observable<ResponseCacheEntry> {
return this.store.select<ResponseCacheEntry>('core', 'cache', 'response', key)
.filter((entry) => this.isValid(entry))
return this.store.select(entryFromKeySelector(key))
.filter((entry: ResponseCacheEntry) => this.isValid(entry))
.distinctUntilChanged()
}
/**
@@ -50,9 +57,9 @@ export class ResponseCacheService {
has(key: string): boolean {
let result: boolean;
this.store.select<ResponseCacheEntry>('core', 'cache', 'response', key)
this.store.select(entryFromKeySelector(key))
.take(1)
.subscribe((entry) => {
.subscribe((entry: ResponseCacheEntry) => {
result = this.isValid(entry);
});

View File

@@ -1,4 +1,3 @@
import { EffectsModule } from '@ngrx/effects';
import { ObjectCacheEffects } from './data/object-cache.effects';
import { RequestCacheEffects } from './data/request-cache.effects';
@@ -6,7 +5,8 @@ import { HrefIndexEffects } from './index/href-index.effects';
import { RequestEffects } from './data/request.effects';
export const coreEffects = [
EffectsModule.run(RequestEffects),
EffectsModule.run(ObjectCacheEffects),
EffectsModule.run(HrefIndexEffects),
RequestCacheEffects,
RequestEffects,
ObjectCacheEffects,
HrefIndexEffects,
];

View File

@@ -14,10 +14,16 @@ import { RequestService } from './data/request.service';
import { RemoteDataBuildService } from './cache/builders/remote-data-build.service';
import { CommunityDataService } from './data/community-data.service';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { coreEffects } from './core.effects';
import { EffectsModule } from '@ngrx/effects';
import { StoreModule } from '@ngrx/store';
import { coreReducers } from './core.reducers';
const IMPORTS = [
CommonModule,
SharedModule
SharedModule,
StoreModule.forFeature('core', coreReducers, { }),
EffectsModule.forFeature(coreEffects)
];
const DECLARATIONS = [

View File

@@ -1,21 +1,22 @@
import { combineReducers } from '@ngrx/store';
import { ActionReducerMap, createFeatureSelector } from '@ngrx/store';
import { CacheState, cacheReducer } from './cache/cache.reducers';
import { IndexState, indexReducer } from './index/index.reducers';
import { DataState, dataReducer } from './data/data.reducers';
import { responseCacheReducer, ResponseCacheState } from './cache/response-cache.reducer';
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
import { hrefIndexReducer, HrefIndexState } from './index/href-index.reducer';
import { requestReducer, RequestState } from './data/request.reducer';
export interface CoreState {
cache: CacheState,
index: IndexState,
data: DataState
'data/object': ObjectCacheState,
'data/response': ResponseCacheState,
'data/request': RequestState,
'index/href': HrefIndexState
}
export const reducers = {
cache: cacheReducer,
index: indexReducer,
data: dataReducer
export const coreReducers: ActionReducerMap<CoreState> = {
'data/object': objectCacheReducer,
'data/response': responseCacheReducer,
'data/request': requestReducer,
'index/href': hrefIndexReducer
};
export function coreReducer(state: any, action: any) {
return combineReducers(reducers)(state, action);
}
export const coreSelector = createFeatureSelector<CoreState>('core');

View File

@@ -1,15 +0,0 @@
import { combineReducers } from '@ngrx/store';
import { RequestState, requestReducer } from './request.reducer';
export interface DataState {
request: RequestState
}
export const reducers = {
request: requestReducer
};
export function dataReducer(state: any, action: any) {
return combineReducers(reducers)(state, action);
}

View File

@@ -1,8 +1,8 @@
import { Injectable, Inject } from '@angular/core';
import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects';
import { ObjectCacheActionTypes } from '../cache/object-cache.actions';
import { ResetResponseCacheTimestampsAction } from '../cache/response-cache.actions';
import { StoreActionTypes } from '../../store.actions';
@Injectable()
export class RequestCacheEffects {
@@ -14,17 +14,9 @@ export class RequestCacheEffects {
*
* This assumes that the server cached everything a negligible
* time ago, and will likely need to be revisited later
*
* This effect should listen for StoreActionTypes.REHYDRATE,
* but can't because you can only have one effect listen to
* an action atm. Github issue:
* https://github.com/ngrx/effects/issues/87
*
* It's listening for ObjectCacheActionTypes.RESET_TIMESTAMPS
* instead, until there's a solution.
*/
@Effect() fixTimestampsOnRehydrate = this.actions$
.ofType(ObjectCacheActionTypes.RESET_TIMESTAMPS)
.ofType(StoreActionTypes.REHYDRATE)
.map(() => new ResetResponseCacheTimestampsAction(new Date().getTime()));
constructor(private actions$: Actions, ) { }

View File

@@ -19,7 +19,7 @@ export interface RequestState {
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState = Object.create(null);
export const requestReducer = (state = initialState, action: RequestAction): RequestState => {
export function requestReducer(state = initialState, action: RequestAction): RequestState {
switch (action.type) {
case RequestActionTypes.CONFIGURE: {
@@ -38,7 +38,7 @@ export const requestReducer = (state = initialState, action: RequestAction): Req
return state;
}
}
};
}
function configureRequest(state: RequestState, action: RequestConfigureAction): RequestState {
return Object.assign({}, state, {

View File

@@ -1,12 +1,10 @@
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { request } from 'http';
import { MemoizedSelector, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { RequestEntry, RequestState } from './request.reducer';
import { RequestEntry } from './request.reducer';
import { Request } from './request.models';
import { hasValue } from '../../shared/empty.util';
import { RequestConfigureAction, RequestExecuteAction } from './request.actions';
@@ -15,6 +13,12 @@ import { ObjectCacheService } from '../cache/object-cache.service';
import { CacheableObject } from '../cache/object-cache.reducer';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { SuccessResponse } from '../cache/response-cache.models';
import { CoreState } from '../core.reducers';
import { keySelector } from '../shared/selectors';
function entryFromHrefSelector(href: string): MemoizedSelector<CoreState, RequestEntry> {
return keySelector<RequestEntry>('data/request', href);
}
@Injectable()
export class RequestService {
@@ -22,13 +26,13 @@ export class RequestService {
constructor(
private objectCache: ObjectCacheService,
private responseCache: ResponseCacheService,
private store: Store<RequestState>
private store: Store<CoreState>
) {
}
isPending(href: string): boolean {
let isPending = false;
this.store.select<RequestEntry>('core', 'data', 'request', href)
this.store.select(entryFromHrefSelector(href))
.take(1)
.subscribe((re: RequestEntry) => {
isPending = (hasValue(re) && !re.completed)
@@ -38,7 +42,7 @@ export class RequestService {
}
get(href: string): Observable<RequestEntry> {
return this.store.select<RequestEntry>('core', 'data', 'request', href);
return this.store.select(entryFromHrefSelector(href));
}
configure<T extends CacheableObject>(request: Request<T>): void {

View File

@@ -33,7 +33,7 @@ describe('Footer component', () => {
// async beforeEach
beforeEach(async(() => {
return TestBed.configureTestingModule({
imports: [CommonModule, StoreModule.provideStore({}), TranslateModule.forRoot({
imports: [CommonModule, StoreModule.forRoot({}), TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: MockTranslateLoader

View File

@@ -12,7 +12,7 @@ export interface HrefIndexState {
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
const initialState: HrefIndexState = Object.create(null);
export const hrefIndexReducer = (state = initialState, action: HrefIndexAction): HrefIndexState => {
export function hrefIndexReducer(state = initialState, action: HrefIndexAction): HrefIndexState {
switch (action.type) {
case HrefIndexActionTypes.ADD: {
@@ -27,7 +27,7 @@ export const hrefIndexReducer = (state = initialState, action: HrefIndexAction):
return state;
}
}
};
}
function addToHrefIndex(state: HrefIndexState, action: AddToHrefIndexAction): HrefIndexState {
return Object.assign({}, state, {

View File

@@ -1,15 +0,0 @@
import { combineReducers } from '@ngrx/store';
import { HrefIndexState, hrefIndexReducer } from './href-index.reducer';
export interface IndexState {
href: HrefIndexState
}
export const reducers = {
href: hrefIndexReducer
};
export function indexReducer(state: any, action: any) {
return combineReducers(reducers)(state, action);
}

View File

@@ -0,0 +1,13 @@
import { createSelector, MemoizedSelector } from '@ngrx/store';
import { coreSelector, CoreState } from '../core.reducers';
import { hasValue } from '../../shared/empty.util';
export function keySelector<T>(subState: string, key: string): MemoizedSelector<CoreState, T> {
return createSelector(coreSelector, (state: CoreState) => {
if (hasValue(state[subState])) {
return state[subState][key];
} else {
return undefined;
}
});
}

View File

@@ -21,7 +21,7 @@ describe('HeaderComponent', () => {
// async beforeEach
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [StoreModule.provideStore({}), TranslateModule.forRoot(), NgbCollapseModule.forRoot()],
imports: [StoreModule.forRoot({}), TranslateModule.forRoot(), NgbCollapseModule.forRoot()],
declarations: [HeaderComponent]
})
.compileComponents(); // compile template and css
@@ -70,7 +70,7 @@ describe('HeaderComponent', () => {
beforeEach(() => {
menu = fixture.debugElement.query(By.css('#collapsingNav')).nativeElement;
spyOn(store, 'select').and.returnValue(Observable.of({ navCollapsed: false }));
spyOn(store, 'select').and.returnValue(Observable.of(false));
fixture.detectChanges();
});

View File

@@ -1,9 +1,13 @@
import { Component, OnInit } from '@angular/core';
import { Store } from '@ngrx/store';
import { createSelector, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { HeaderState } from './header.reducer';
import { HeaderToggleAction } from './header.actions';
import { AppState } from '../app.reducer';
const headerStateSelector = (state: AppState) => state.header;
const navCollapsedSelector = createSelector(headerStateSelector, (header: HeaderState) => header.navCollapsed);
@Component({
selector: 'ds-header',
@@ -14,14 +18,12 @@ export class HeaderComponent implements OnInit {
public isNavBarCollapsed: Observable<boolean>;
constructor(
private store: Store<HeaderState>
private store: Store<AppState>
) {
}
ngOnInit(): void {
this.isNavBarCollapsed = this.store.select('header')
// unwrap navCollapsed
.map(({ navCollapsed }: HeaderState) => navCollapsed);
this.isNavBarCollapsed = this.store.select(navCollapsedSelector);
}
public toggle(): void {

View File

@@ -1,41 +1,36 @@
import { TestBed, inject } from '@angular/core/testing';
import { EffectsTestingModule, EffectsRunner } from '@ngrx/effects/testing';
import { routerActions } from '@ngrx/router-store';
import { TestBed } from '@angular/core/testing';
import { HeaderEffects } from './header.effects';
import { HeaderCollapseAction } from './header.actions';
import { HostWindowResizeAction } from '../shared/host-window.actions';
import { Observable } from 'rxjs/Observable';
import { provideMockActions } from '@ngrx/effects/testing';
import { cold, hot } from 'jasmine-marbles';
import * as fromRouter from '@ngrx/router-store';
describe('HeaderEffects', () => {
beforeEach(() => TestBed.configureTestingModule({
imports: [
EffectsTestingModule
],
providers: [
HeaderEffects
]
}));
let runner: EffectsRunner;
let headerEffects: HeaderEffects;
let actions: Observable<any>;
beforeEach(inject([
EffectsRunner, HeaderEffects
],
(_runner, _headerEffects) => {
runner = _runner;
headerEffects = _headerEffects;
}
));
beforeEach(() => {
TestBed.configureTestingModule({
providers: [
HeaderEffects,
provideMockActions(() => actions),
// other providers
],
});
headerEffects = TestBed.get(HeaderEffects);
});
describe('resize$', () => {
it('should return a COLLAPSE action in response to a RESIZE action', () => {
runner.queue(new HostWindowResizeAction(800, 600));
actions = hot('--a-', { a: new HostWindowResizeAction(800, 600) });
headerEffects.resize$.subscribe((result) => {
expect(result).toEqual(new HeaderCollapseAction());
});
const expected = cold('--b-', { b: new HeaderCollapseAction() });
expect(headerEffects.resize$).toBeObservable(expected);
});
});
@@ -43,11 +38,11 @@ describe('HeaderEffects', () => {
describe('routeChange$', () => {
it('should return a COLLAPSE action in response to an UPDATE_LOCATION action', () => {
runner.queue({ type: routerActions.UPDATE_LOCATION });
actions = hot('--a-', { a: { type: fromRouter.ROUTER_NAVIGATION } });
headerEffects.resize$.subscribe((result) => {
expect(result).toEqual(new HeaderCollapseAction());
});
const expected = cold('--b-', { b: new HeaderCollapseAction() });
expect(headerEffects.routeChange$).toBeObservable(expected);
});
});

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core';
import { Effect, Actions } from '@ngrx/effects'
import { routerActions } from '@ngrx/router-store';
import * as fromRouter from '@ngrx/router-store';
import { HostWindowActionTypes } from '../shared/host-window.actions';
import { HeaderCollapseAction } from './header.actions';
@@ -13,7 +13,7 @@ export class HeaderEffects {
.map(() => new HeaderCollapseAction());
@Effect() routeChange$ = this.actions$
.ofType(routerActions.UPDATE_LOCATION)
.ofType(fromRouter.ROUTER_NAVIGATION)
.map(() => new HeaderCollapseAction());
constructor(private actions$: Actions) {

View File

@@ -8,7 +8,7 @@ const initialState: HeaderState = {
navCollapsed: true
};
export const headerReducer = (state = initialState, action: HeaderAction): HeaderState => {
export function headerReducer(state = initialState, action: HeaderAction): HeaderState {
switch (action.type) {
case HeaderActionTypes.COLLAPSE: {
@@ -35,4 +35,4 @@ export const headerReducer = (state = initialState, action: HeaderAction): Heade
return state;
}
}
};
}

View File

@@ -1,4 +1,4 @@
<ds-metadata-field-wrapper [label]="label | translate">
<ds-metadata-field-wrapper *ngIf="(files | async)?.length > 0" [label]="label | translate">
<div class="file-section">
<a *ngFor="let file of (files | async); let last=last;" [href]="file?.content" [download]="file?.name">
<span>{{file?.name}}</span>

View File

@@ -1,12 +1,14 @@
import { Component } from '@angular/core';
import { ServerResponseService } from '../shared/server-response.service';
import { Component, ChangeDetectionStrategy } from '@angular/core';
@Component({
selector: 'ds-pagenotfound',
styleUrls: ['./pagenotfound.component.scss'],
templateUrl: './pagenotfound.component.html'
templateUrl: './pagenotfound.component.html',
changeDetection: ChangeDetectionStrategy.OnPush
})
export class PageNotFoundComponent {
data: any = {};
constructor(responseService: ServerResponseService) {
responseService.setNotFound();
}
}

View File

@@ -1,7 +1,7 @@
import 'rxjs/add/operator/filter';
import 'rxjs/add/operator/first';
import { ApplicationRef, Inject, NgModule, APP_BOOTSTRAP_LISTENER } from '@angular/core';
import { ApplicationRef, NgModule, APP_BOOTSTRAP_LISTENER } from '@angular/core';
import { RouterModule } from '@angular/router';
import { ServerModule } from '@angular/platform-server';
import { BrowserModule } from '@angular/platform-browser';
@@ -16,14 +16,13 @@ import { TranslateModule, TranslateLoader } from '@ngx-translate/core';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { Store } from '@ngrx/store';
import { Actions, EffectsModule } from '@ngrx/effects';
import { EffectsModule } from '@ngrx/effects';
import { TranslateUniversalLoader } from '../modules/translate-universal-loader';
import { ServerTransferStateModule } from '../modules/transfer-state/server-transfer-state.module';
import { TransferState } from '../modules/transfer-state/transfer-state';
import { TransferStoreEffects } from '../modules/transfer-store/transfer-store.effects';
import { ServerTransferStoreEffects } from '../modules/transfer-store/server-transfer-store.effects';
import { ServerTransferStoreModule } from '../modules/transfer-store/server-transfer-store.module';
@@ -32,15 +31,14 @@ import { ServerCookiesModule } from '../modules/cookies/server-cookies.module';
import { ServerDataLoaderModule } from '../modules/data-loader/server-data-loader.module';
import { AppState } from './app.reducer';
import { effects } from './app.effects';
import { SharedModule } from './shared/shared.module';
import { CoreModule } from './core/core.module';
import { AppModule } from './app.module';
import { AppComponent } from './app.component';
import { GLOBAL_CONFIG, GlobalConfig } from '../config';
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
import { DSpaceRouterStateSerializer } from './shared/ngrx/dspace-router-state-serializer';
export function boot(cache: TransferState, appRef: ApplicationRef, store: Store<AppState>, request: Request, config: GlobalConfig) {
// authentication mechanism goes here
@@ -61,6 +59,7 @@ export function UniversalLoaderFactory() {
appId: 'ds-app-id'
}),
RouterModule.forRoot([], { useHash: false }),
StoreRouterConnectingModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
@@ -74,7 +73,7 @@ export function UniversalLoaderFactory() {
ServerDataLoaderModule,
ServerTransferStateModule,
ServerTransferStoreModule,
EffectsModule.run(ServerTransferStoreEffects),
EffectsModule.forRoot([ServerTransferStoreEffects]),
NoopAnimationsModule,
AppModule
],
@@ -90,6 +89,10 @@ export function UniversalLoaderFactory() {
REQUEST,
GLOBAL_CONFIG
]
},
{
provide: RouterStateSerializer,
useClass: DSpaceRouterStateSerializer
}
]
})

View File

@@ -10,7 +10,7 @@ const initialState: HostWindowState = {
height: null
};
export const hostWindowReducer = (state = initialState, action: HostWindowAction): HostWindowState => {
export function hostWindowReducer(state = initialState, action: HostWindowAction): HostWindowState {
switch (action.type) {
case HostWindowActionTypes.RESIZE: {
@@ -21,4 +21,4 @@ export const hostWindowReducer = (state = initialState, action: HostWindowAction
return state;
}
}
};
}

View File

@@ -1,17 +1,18 @@
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { AppState } from '../app.reducer';
import { HostWindowState } from './host-window.reducer';
import { HostWindowService } from './host-window.service';
import { HostWindowState } from './host-window.reducer';
describe('HostWindowService', () => {
let service: HostWindowService;
let store: Store<HostWindowState>;
let store: Store<AppState>;
describe('', () => {
beforeEach(() => {
const _initialState = { width: 1600, height: 770 };
store = new Store<HostWindowState>(undefined, undefined, Observable.of(_initialState));
const _initialState = { hostWindow: { width: 1600, height: 770 } };
store = new Store<AppState>(Observable.of(_initialState), undefined, undefined);
service = new HostWindowService(store);
});
@@ -46,8 +47,8 @@ describe('HostWindowService', () => {
describe('', () => {
beforeEach(() => {
const _initialState = { width: 1100, height: 770 };
store = new Store<HostWindowState>(undefined, undefined, Observable.of(_initialState));
const _initialState = { hostWindow: { width: 1100, height: 770 } };
store = new Store<AppState>(Observable.of(_initialState), undefined, undefined);
service = new HostWindowService(store);
});
@@ -82,8 +83,8 @@ describe('HostWindowService', () => {
describe('', () => {
beforeEach(() => {
const _initialState = { width: 800, height: 770 };
store = new Store<HostWindowState>(undefined, undefined, Observable.of(_initialState));
const _initialState = { hostWindow: { width: 800, height: 770 } };
store = new Store<AppState>(Observable.of(_initialState), undefined, undefined);
service = new HostWindowService(store);
});
@@ -118,8 +119,8 @@ describe('HostWindowService', () => {
describe('', () => {
beforeEach(() => {
const _initialState = { width: 600, height: 770 };
store = new Store<HostWindowState>(undefined, undefined, Observable.of(_initialState));
const _initialState = { hostWindow: { width: 600, height: 770 } };
store = new Store<AppState>(Observable.of(_initialState), undefined, undefined);
service = new HostWindowService(store);
});
@@ -154,8 +155,8 @@ describe('HostWindowService', () => {
describe('', () => {
beforeEach(() => {
const _initialState = { width: 400, height: 770 };
store = new Store<HostWindowState>(undefined, undefined, Observable.of(_initialState));
const _initialState = { hostWindow: { width: 400, height: 770 } };
store = new Store<AppState>(Observable.of(_initialState), undefined, undefined);
service = new HostWindowService(store);
});

View File

@@ -1,9 +1,10 @@
import { HostWindowState } from './host-window.reducer';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store';
import { createSelector, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { hasValue } from './empty.util';
import { AppState } from '../app.reducer';
// TODO: ideally we should get these from sass somehow
export enum GridBreakpoint {
@@ -14,16 +15,19 @@ export enum GridBreakpoint {
XL = 1200
}
const hostWindowStateSelector = (state: AppState) => state.hostWindow;
const widthSelector = createSelector(hostWindowStateSelector, (hostWindow: HostWindowState) => hostWindow.width);
@Injectable()
export class HostWindowService {
constructor(
private store: Store<HostWindowState>
private store: Store<AppState>
) {
}
private getWidthObs(): Observable<number> {
return this.store.select<number>('hostWindow', 'width')
return this.store.select(widthSelector)
.filter((width) => hasValue(width));
}

View File

@@ -0,0 +1,18 @@
import { RouterStateSerializer } from '@ngrx/router-store';
import { Params, RouterStateSnapshot } from '@angular/router';
export interface RouterStateUrl {
url: string;
queryParams: Params;
}
export class DSpaceRouterStateSerializer implements RouterStateSerializer<RouterStateUrl> {
serialize(routerState: RouterStateSnapshot): RouterStateUrl {
const { url } = routerState;
const queryParams = routerState.root.queryParams;
// Only return an object including the URL and query params
// instead of the entire snapshot
return { url, queryParams };
}
}

View File

@@ -140,7 +140,7 @@ describe('Pagination component', () => {
TestBed.configureTestingModule({
imports: [
CommonModule,
StoreModule.provideStore({}),
StoreModule.forRoot({}),
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,

View File

@@ -0,0 +1,26 @@
import { RESPONSE } from '@nguniversal/express-engine/tokens';
import { Inject, Injectable, Optional } from '@angular/core';
import { Response } from 'express';
@Injectable()
export class ServerResponseService {
private response: Response;
constructor(@Optional() @Inject(RESPONSE) response: any) {
this.response = response;
}
setStatus(code: number, message?: string): this {
if (this.response) {
this.response.statusCode = code;
if (message) {
this.response.statusMessage = message;
}
}
return this;
}
setNotFound(message = 'Not found'): this {
return this.setStatus(404, message)
}
}

View File

@@ -27,6 +27,7 @@ import { TruncatePipe } from './utils/truncate.pipe';
import { WrapperListElementComponent } from '../object-list/wrapper-list-element/wrapper-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 { ServerResponseService } from './server-response.service';
const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -71,7 +72,8 @@ const ENTRY_COMPONENTS = [
const PROVIDERS = [
ApiService,
HostWindowService,
{ provide: NativeWindowService, useFactory: NativeWindowFactory }
{ provide: NativeWindowService, useFactory: NativeWindowFactory },
ServerResponseService
];
@NgModule({

View File

@@ -8,6 +8,7 @@ import { StoreAction, StoreActionTypes } from '../../app/store.actions';
import { AppState } from '../../app/app.reducer';
import { GLOBAL_CONFIG, GlobalConfig } from '../../config';
import { RouterNavigationAction } from '@ngrx/router-store';
@Injectable()
export class BrowserTransferState extends TransferState {

View File

@@ -90,25 +90,21 @@
version "1.0.0-beta.1"
resolved "https://registry.yarnpkg.com/@ng-bootstrap/ng-bootstrap/-/ng-bootstrap-1.0.0-beta.1.tgz#a7d5935293df22a2275bf572f2197b45136e3c52"
"@ngrx/core@1.2.0":
version "1.2.0"
resolved "https://registry.yarnpkg.com/@ngrx/core/-/core-1.2.0.tgz#882b46abafa2e0e6d887cb71a1b2c2fa3e6d0dc6"
"@ngrx/effects@^4.0.5":
version "4.0.5"
resolved "https://registry.yarnpkg.com/@ngrx/effects/-/effects-4.0.5.tgz#1224763800621b7305f9b18bc17ee09b25c861d1"
"@ngrx/effects@2.0.4":
version "2.0.4"
resolved "https://registry.yarnpkg.com/@ngrx/effects/-/effects-2.0.4.tgz#418eee5e1032fa66de5bbf1855653bb1951f12a4"
"@ngrx/router-store@^4.0.4":
version "4.0.4"
resolved "https://registry.yarnpkg.com/@ngrx/router-store/-/router-store-4.0.4.tgz#ab59f35aae93465088384faf009e21b22edd456a"
"@ngrx/router-store@1.2.6":
version "1.2.6"
resolved "https://registry.yarnpkg.com/@ngrx/router-store/-/router-store-1.2.6.tgz#a2eb0ca515e9b367781f1030250dd64bb73c086b"
"@ngrx/store-devtools@^4.0.0":
version "4.0.0"
resolved "https://registry.yarnpkg.com/@ngrx/store-devtools/-/store-devtools-4.0.0.tgz#b79c24773217df7fd9735ad21f9cbf2533c96e04"
"@ngrx/store-devtools@3.2.4":
version "3.2.4"
resolved "https://registry.yarnpkg.com/@ngrx/store-devtools/-/store-devtools-3.2.4.tgz#2ce4d13bf34848a9e51ec87e3b125ed67b51e550"
"@ngrx/store@2.2.3":
version "2.2.3"
resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-2.2.3.tgz#e7bd1149f1c44208f1cc4744353f0f98a0f1f57b"
"@ngrx/store@^4.0.3":
version "4.0.3"
resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-4.0.3.tgz#36abacdfa19bfb8506e40de80bae06050a1e15e9"
"@ngtools/webpack@1.5.1":
version "1.5.1"
@@ -1731,10 +1727,6 @@ deep-extend@~0.4.0:
version "0.4.2"
resolved "https://registry.yarnpkg.com/deep-extend/-/deep-extend-0.4.2.tgz#48b699c27e334bf89f10892be432f6e4c7d34a7f"
deep-freeze-strict@^1.1.1:
version "1.1.1"
resolved "https://registry.yarnpkg.com/deep-freeze-strict/-/deep-freeze-strict-1.1.1.tgz#77d0583ca24a69be4bbd9ac2fae415d55523e5b0"
deep-freeze@0.0.1:
version "0.0.1"
resolved "https://registry.yarnpkg.com/deep-freeze/-/deep-freeze-0.0.1.tgz#3a0b0005de18672819dfd38cd31f91179c893e84"
@@ -3487,6 +3479,12 @@ jasmine-core@2.6.4, jasmine-core@~2.6.0:
version "2.6.4"
resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.6.4.tgz#dec926cd0a9fa287fb6db5c755fa487e74cecac5"
jasmine-marbles@^0.1.0:
version "0.1.0"
resolved "https://registry.yarnpkg.com/jasmine-marbles/-/jasmine-marbles-0.1.0.tgz#c9ecdc64e20b6cf55b49a10201a5be33907dadcc"
dependencies:
lodash.isequal "^4.5.0"
jasmine-spec-reporter@4.1.1:
version "4.1.1"
resolved "https://registry.yarnpkg.com/jasmine-spec-reporter/-/jasmine-spec-reporter-4.1.1.tgz#5a6d58ab5d61bea7309fbc279239511756b1b588"
@@ -3936,6 +3934,10 @@ lodash.isarray@^3.0.0:
version "3.0.4"
resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55"
lodash.isequal@^4.5.0:
version "4.5.0"
resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0"
lodash.keys@^3.0.0:
version "3.1.2"
resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a"
@@ -4359,12 +4361,6 @@ nested-error-stacks@^1.0.0:
dependencies:
inherits "~2.0.1"
ngrx-store-freeze@0.1.9:
version "0.1.9"
resolved "https://registry.yarnpkg.com/ngrx-store-freeze/-/ngrx-store-freeze-0.1.9.tgz#b20f18f21fd5efc4e1b1e05f6f279674d0f70c81"
dependencies:
deep-freeze-strict "^1.1.1"
ngx-pagination@3.0.1:
version "3.0.1"
resolved "https://registry.yarnpkg.com/ngx-pagination/-/ngx-pagination-3.0.1.tgz#5a8000e40c0424d9c41c9d6d592562e1547abf24"