Merge remote-tracking branch 'remotes/origin/master' into submission

# Conflicts:
#	package.json
#	src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts
#	src/app/+search-page/search-service/search.service.spec.ts
#	src/app/app.module.ts
#	src/app/core/auth/auth.interceptor.ts
#	src/app/core/cache/response-cache.service.spec.ts
#	src/app/core/data/browse-response-parsing.service.spec.ts
#	src/app/core/data/data.service.spec.ts
#	src/app/core/data/request.service.spec.ts
#	src/app/core/data/request.service.ts
#	src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts
#	src/app/core/integration/integration.service.ts
#	src/app/core/metadata/metadata.service.spec.ts
#	src/app/core/registry/registry.service.spec.ts
#	src/app/core/shared/collection.model.ts
#	src/app/core/shared/item.model.ts
#	src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts
#	src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts
#	src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts
#	src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts
#	src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts
#	src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts
#	src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts
#	src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts
#	src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts
#	src/app/shared/form/builder/form-builder.service.spec.ts
#	src/app/shared/form/form.service.spec.ts
#	src/app/shared/notifications/notification/notification.component.spec.ts
#	src/app/shared/services/route.service.spec.ts
#	src/app/shared/services/route.service.ts
#	src/app/shared/shared.module.ts
#	yarn.lock
This commit is contained in:
Giuseppe Digilio
2018-12-13 20:28:11 +01:00
237 changed files with 6869 additions and 7210 deletions

13
angular.json Normal file
View File

@@ -0,0 +1,13 @@
{
"$schema": "./node_modules/@angular/cli/lib/config/schema.json",
"version": 1,
"cli": {
"defaultCollection": "@ngrx/schematics"
},
"projects": {
"core": {
"root": "",
"projectType": "application"
}
}
}

View File

@@ -23,9 +23,9 @@
"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",
"build": "webpack --progress", "build": "webpack --progress --mode development",
"build:aot": "webpack --env.aot --env.server && webpack --env.aot --env.client", "build:aot": "webpack --env.aot --env.server --mode development && webpack --env.aot --env.client --mode development",
"build:prod": "webpack --env.aot --env.server -p && webpack --env.aot --env.client -p", "build:prod": "webpack --env.aot --env.server --mode production && webpack --env.aot --env.client --mode production",
"postbuild:prod": "yarn run rollup", "postbuild:prod": "yarn run rollup",
"rollup": "rollup -c rollup.config.js", "rollup": "rollup -c rollup.config.js",
"prestart": "yarn run build:prod", "prestart": "yarn run build:prod",
@@ -40,21 +40,15 @@
"server": "node dist/server.js", "server": "node dist/server.js",
"server:watch": "nodemon dist/server.js", "server:watch": "nodemon dist/server.js",
"server:watch:debug": "nodemon --debug dist/server.js", "server:watch:debug": "nodemon --debug dist/server.js",
"webpack:watch": "webpack -w", "webpack:watch": "webpack -w --mode development",
"webpack:watch:aot": "webpack -w --env.aot --env.server && webpack -w --env.aot --env.client",
"webpack:watch:prod": "webpack -w --env.aot --env.server -p && webpack -w --env.aot --env.client -p",
"watch": "yarn run build && npm-run-all -p webpack:watch server:watch", "watch": "yarn run build && npm-run-all -p webpack:watch server:watch",
"watch:aot": "yarn run build:aot && npm-run-all -p webpack:watch:aot server:watch",
"watch:prod": "yarn run build:prod && npm-run-all -p webpack:watch:prod server:watch",
"watch:debug": "yarn run build && npm-run-all -p webpack:watch server:watch:debug", "watch:debug": "yarn run build && npm-run-all -p webpack:watch server:watch:debug",
"watch:debug:aot": "yarn run build:aot && npm-run-all -p webpack:watch:aot server:watch:debug",
"watch:debug:prod": "yarn run build:prod && npm-run-all -p webpack:watch:prod server:watch:debug",
"predebug": "yarn run build", "predebug": "yarn run build",
"predebug:server": "yarn run build", "predebug:server": "yarn run build",
"debug": "node --debug-brk dist/server.js", "debug": "node --debug-brk dist/server.js",
"debug:server": "node-nightly --inspect --debug-brk dist/server.js", "debug:server": "node-nightly --inspect --debug-brk dist/server.js",
"debug:build": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js", "debug:build": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js --mode development",
"debug:build:prod": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js --env.aot --env.client --env.server -p", "debug:build:prod": "node-nightly --inspect --debug-brk node_modules/webpack/bin/webpack.js --env.aot --env.client --env.server --mode production",
"ci": "yarn run lint && yarn run build:aot && yarn run test:headless && npm-run-all -p -r server e2e", "ci": "yarn run lint && yarn run build:aot && yarn run test:headless && npm-run-all -p -r server e2e",
"protractor": "node node_modules/protractor/bin/protractor", "protractor": "node node_modules/protractor/bin/protractor",
"pree2e": "yarn run webdriver:update", "pree2e": "yarn run webdriver:update",
@@ -69,31 +63,31 @@
"coverage": "http-server -c-1 -o -p 9875 ./coverage" "coverage": "http-server -c-1 -o -p 9875 ./coverage"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "^5.2.5", "@angular/animations": "^6.1.4",
"@angular/common": "^5.2.5", "@angular/cli": "^6.1.5",
"@angular/core": "^5.2.5", "@angular/common": "^6.1.4",
"@angular/forms": "^5.2.5", "@angular/core": "^6.1.4",
"@angular/http": "^5.2.5", "@angular/forms": "^6.1.4",
"@angular/platform-browser": "^5.2.5", "@angular/http": "^6.1.4",
"@angular/platform-browser-dynamic": "^5.2.5", "@angular/platform-browser": "^6.1.4",
"@angular/platform-server": "^5.2.5", "@angular/platform-browser-dynamic": "^6.1.4",
"@angular/router": "^5.2.5", "@angular/platform-server": "^6.1.4",
"@angular/router": "^6.1.4",
"@angularclass/bootloader": "1.0.1", "@angularclass/bootloader": "1.0.1",
"@ng-bootstrap/ng-bootstrap": "1.1.2", "@ng-bootstrap/ng-bootstrap": "^2.0.0",
"@ng-dynamic-forms/core": "5.4.7", "@ng-dynamic-forms/core": "6.0.9",
"@ng-dynamic-forms/ui-ng-bootstrap": "5.4.7", "@ng-dynamic-forms/ui-ng-bootstrap": "6.0.9",
"@ngrx/effects": "^5.1.0", "@ngrx/effects": "^6.1.0",
"@ngrx/router-store": "^5.0.1", "@ngrx/router-store": "^6.1.0",
"@ngrx/store": "^5.1.0", "@ngrx/store": "^6.1.0",
"@nguniversal/express-engine": "5.0.0", "@nguniversal/express-engine": "6.1.0",
"@ngx-translate/core": "9.1.1", "@ngx-translate/core": "10.0.2",
"@ngx-translate/http-loader": "2.0.1", "@ngx-translate/http-loader": "3.0.1",
"@nicky-lenaers/ngx-scroll-to": "^0.6.0", "@nicky-lenaers/ngx-scroll-to": "^1.0.0",
"angular-idle-preload": "2.0.4", "angular-idle-preload": "3.0.0",
"angular-sortablejs": "^2.5.0", "angular-sortablejs": "^2.5.0",
"angular2-moment": "^1.9.0", "angular2-text-mask": "9.0.0",
"angular2-text-mask": "8.0.4", "angulartics2": "^6.2.0",
"angulartics2": "^5.2.0",
"body-parser": "1.18.2", "body-parser": "1.18.2",
"bootstrap": "4.1.3", "bootstrap": "4.1.3",
"cerialize": "0.1.18", "cerialize": "0.1.18",
@@ -104,6 +98,7 @@
"express-session": "1.15.6", "express-session": "1.15.6",
"file-saver": "^1.3.8", "file-saver": "^1.3.8",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"fork-ts-checker-webpack-plugin": "^0.4.10",
"http-server": "0.11.1", "http-server": "0.11.1",
"https": "1.0.0", "https": "1.0.0",
"js-cookie": "2.2.0", "js-cookie": "2.2.0",
@@ -113,111 +108,118 @@
"methods": "1.1.2", "methods": "1.1.2",
"moment": "^2.22.1", "moment": "^2.22.1",
"morgan": "1.9.0", "morgan": "1.9.0",
"ng-mocks": "^6.2.1",
"ng2-file-upload": "1.2.1", "ng2-file-upload": "1.2.1",
"ng2-nouislider": "^1.7.11", "ng2-nouislider": "^1.7.11",
"ngx-infinite-scroll": "0.8.2", "ngx-bootstrap": "^3.0.1",
"ngx-infinite-scroll": "6.0.1",
"ngx-moment": "^3.1.0",
"ngx-pagination": "3.0.3", "ngx-pagination": "3.0.3",
"nouislider": "^11.0.0", "nouislider": "^11.0.0",
"pem": "1.12.3", "pem": "1.12.3",
"reflect-metadata": "0.1.12", "reflect-metadata": "0.1.12",
"rxjs": "5.5.6", "rxjs": "6.2.2",
"sortablejs": "1.7.0", "sortablejs": "1.7.0",
"text-mask-core": "5.0.1", "text-mask-core": "5.0.1",
"ts-loader": "^5.2.1",
"ts-md5": "^1.2.4", "ts-md5": "^1.2.4",
"uuid": "^3.2.1", "uuid": "^3.2.1",
"webfontloader": "1.6.28", "webfontloader": "1.6.28",
"zone.js": "0.8.20" "webpack-cli": "^3.1.0",
"zone.js": "^0.8.26"
}, },
"devDependencies": { "devDependencies": {
"@angular/compiler": "^5.2.5", "@angular/compiler": "^6.1.4",
"@angular/compiler-cli": "^5.2.5", "@angular/compiler-cli": "^6.1.4",
"@ngrx/store-devtools": "^5.1.0", "@ngrx/entity": "^6.1.0",
"@ngtools/webpack": "^1.10.0", "@ngrx/schematics": "^6.1.0",
"@ngrx/store-devtools": "^6.1.0",
"@ngtools/webpack": "^6.1.5",
"@schematics/angular": "^0.7.5",
"@types/acorn": "^4.0.3", "@types/acorn": "^4.0.3",
"@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.11.1", "@types/express": "^4.11.1",
"@types/express-serve-static-core": "4.11.1", "@types/express-serve-static-core": "4.16.0",
"@types/file-saver": "^1.3.0", "@types/file-saver": "^1.3.0",
"@types/hammerjs": "2.0.35", "@types/hammerjs": "2.0.35",
"@types/jasmine": "^2.8.6", "@types/jasmine": "^2.8.6",
"@types/js-cookie": "2.1.0", "@types/js-cookie": "2.1.0",
"@types/lodash": "^4.14.110",
"@types/memory-cache": "0.2.0", "@types/memory-cache": "0.2.0",
"@types/mime": "2.0.0", "@types/mime": "2.0.0",
"@types/node": "^9.4.6", "@types/node": "^10.9.4",
"@types/serve-static": "1.13.1", "@types/serve-static": "1.13.2",
"@types/uuid": "^3.4.3", "@types/uuid": "^3.4.3",
"@types/webfontloader": "1.6.29", "@types/webfontloader": "1.6.29",
"ajv": "^6.1.1", "ajv": "^6.1.1",
"ajv-keywords": "^3.1.0", "ajv-keywords": "^3.1.0",
"angular2-template-loader": "0.6.2", "angular2-template-loader": "0.6.2",
"autoprefixer": "^8.0.0", "autoprefixer": "^9.1.3",
"awesome-typescript-loader": "3.4.1",
"caniuse-lite": "^1.0.30000697", "caniuse-lite": "^1.0.30000697",
"codelyzer": "^4.1.0", "codelyzer": "^4.4.4",
"compression-webpack-plugin": "^1.1.6", "compression-webpack-plugin": "^1.1.6",
"copy-webpack-plugin": "^4.4.1", "copy-webpack-plugin": "^4.4.1",
"coveralls": "3.0.0", "coveralls": "3.0.0",
"css-loader": "0.28.9", "css-loader": "1.0.0",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"exports-loader": "^0.7.0", "exports-loader": "^0.7.0",
"html-webpack-plugin": "2.30.1", "html-webpack-plugin": "^4.0.0-alpha",
"imports-loader": "0.7.1", "imports-loader": "0.8.0",
"istanbul-instrumenter-loader": "3.0.0", "istanbul-instrumenter-loader": "3.0.1",
"jasmine-core": "^2.99.1", "jasmine-core": "^3.2.1",
"jasmine-marbles": "0.2.0", "jasmine-marbles": "0.3.1",
"jasmine-spec-reporter": "4.2.1", "jasmine-spec-reporter": "4.2.1",
"json-loader": "0.5.7", "karma": "3.0.0",
"karma": "2.0.0",
"karma-chrome-launcher": "2.2.0", "karma-chrome-launcher": "2.2.0",
"karma-cli": "1.0.1", "karma-cli": "1.0.1",
"karma-coverage": "1.1.1", "karma-coverage": "1.1.2",
"karma-istanbul-preprocessor": "0.0.2", "karma-istanbul-preprocessor": "0.0.2",
"karma-jasmine": "1.1.1", "karma-jasmine": "1.1.2",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"karma-phantomjs-launcher": "1.0.4", "karma-phantomjs-launcher": "1.0.4",
"karma-remap-coverage": "^0.1.5", "karma-remap-coverage": "^0.1.5",
"karma-remap-istanbul": "0.6.0", "karma-remap-istanbul": "0.6.0",
"karma-sourcemap-loader": "0.3.7", "karma-sourcemap-loader": "0.3.7",
"karma-webdriver-launcher": "1.0.5", "karma-webdriver-launcher": "1.0.5",
"karma-webpack": "2.0.9", "karma-webpack": "3.0.0",
"ngrx-store-freeze": "^0.2.1", "ngrx-store-freeze": "^0.2.4",
"node-sass": "^4.7.2", "node-sass": "^4.7.2",
"nodemon": "^1.15.0", "nodemon": "^1.15.0",
"npm-run-all": "4.1.2", "npm-run-all": "4.1.3",
"postcss": "^6.0.18", "postcss": "^7.0.2",
"postcss-apply": "0.8.0", "postcss-apply": "0.11.0",
"postcss-cli": "^5.0.0", "postcss-cli": "^6.0.0",
"postcss-cssnext": "3.1.0", "postcss-cssnext": "3.1.0",
"postcss-loader": "^2.1.0", "postcss-loader": "^3.0.0",
"postcss-responsive-type": "1.0.0", "postcss-responsive-type": "1.0.0",
"postcss-smart-import": "0.7.6", "postcss-smart-import": "0.7.6",
"protractor": "^5.3.0", "protractor": "^5.3.0",
"protractor-istanbul-plugin": "2.0.0", "protractor-istanbul-plugin": "2.0.0",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"resolve-url-loader": "2.2.1", "resolve-url-loader": "^2.3.0",
"rimraf": "2.6.2", "rimraf": "2.6.2",
"rollup": "^0.56.0", "rollup": "^0.65.0",
"rollup-plugin-commonjs": "^8.3.0", "rollup-plugin-commonjs": "^9.1.6",
"rollup-plugin-node-globals": "1.1.0", "rollup-plugin-node-globals": "1.2.1",
"rollup-plugin-node-resolve": "^3.0.3", "rollup-plugin-node-resolve": "^3.0.3",
"rollup-plugin-uglify": "3.0.0", "rollup-plugin-terser": "^2.0.2",
"sass-loader": "6.0.6", "sass-loader": "7.1.0",
"script-ext-html-webpack-plugin": "1.8.8", "script-ext-html-webpack-plugin": "2.0.1",
"source-map": "0.6.1", "source-map": "0.7.3",
"source-map-loader": "0.2.3", "source-map-loader": "0.2.4",
"string-replace-loader": "1.3.0", "string-replace-loader": "2.1.1",
"to-string-loader": "1.1.5", "to-string-loader": "1.1.5",
"ts-helpers": "1.1.2", "ts-helpers": "1.1.2",
"ts-node": "4.1.0", "ts-node": "4.1.0",
"tslint": "5.9.1", "tslint": "5.11.0",
"typedoc": "^0.9.0", "typedoc": "^0.9.0",
"typescript": "2.6.2", "typescript": "^2.9.1",
"webpack": "^3.11.0", "webpack": "^4.17.1",
"webpack-bundle-analyzer": "^2.10.0", "webpack-bundle-analyzer": "^2.13.1",
"webpack-dev-middleware": "^2.0.5", "webpack-dev-middleware": "3.2.0",
"webpack-dev-server": "2.11.1", "webpack-dev-server": "^3.1.5",
"webpack-merge": "4.1.1", "webpack-merge": "4.1.4",
"webpack-node-externals": "1.6.0" "webpack-node-externals": "1.7.2"
} }
} }

View File

@@ -92,7 +92,8 @@
}, },
"results": { "results": {
"head": "Search Results", "head": "Search Results",
"no-results": "There were no results for this search" "no-results": "Your search returned no results. Having trouble finding what you're looking for? Try putting",
"no-results-link": "quotes around it"
}, },
"sidebar": { "sidebar": {
"close": "Back to results", "close": "Back to results",

View File

@@ -1,6 +1,6 @@
import nodeResolve from 'rollup-plugin-node-resolve' import nodeResolve from 'rollup-plugin-node-resolve'
import commonjs from 'rollup-plugin-commonjs'; import commonjs from 'rollup-plugin-commonjs';
import uglify from 'rollup-plugin-uglify' import terser from 'rollup-plugin-terser'
export default { export default {
input: 'dist/client.js', input: 'dist/client.js',
@@ -8,7 +8,6 @@ export default {
file: 'dist/client.js', file: 'dist/client.js',
format: 'iife', format: 'iife',
}, },
sourcemap: false,
plugins: [ plugins: [
nodeResolve({ nodeResolve({
jsnext: true, jsnext: true,
@@ -17,6 +16,6 @@ export default {
commonjs({ commonjs({
include: 'node_modules/rxjs/**' include: 'node_modules/rxjs/**'
}), }),
uglify() terser.terser()
] ]
} }

View File

@@ -28,7 +28,7 @@ require('zone.js/dist/async-test');
require('zone.js/dist/fake-async-test'); require('zone.js/dist/fake-async-test');
// RxJS // RxJS
require('rxjs/Rx'); require('rxjs');
var testing = require('@angular/core/testing'); var testing = require('@angular/core/testing');
var browser = require('@angular/platform-browser-dynamic/testing'); var browser = require('@angular/platform-browser-dynamic/testing');

View File

@@ -1,14 +1,13 @@
import { BitstreamFormatsComponent } from './bitstream-formats.component'; import { BitstreamFormatsComponent } from './bitstream-formats.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RegistryService } from '../../../core/registry/registry.service'; import { RegistryService } from '../../../core/registry/registry.service';
import { Observable } from 'rxjs/Observable'; import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { SharedModule } from '../../../shared/shared.module';
import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { PaginationComponent } from '../../../shared/pagination/pagination.component';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe'; import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
@@ -53,7 +52,7 @@ describe('BitstreamFormatsComponent', () => {
extensions: null extensions: null
} }
]; ];
const mockFormats = Observable.of(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockFormatsList))); const mockFormats = observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockFormatsList)));
const registryServiceStub = { const registryServiceStub = {
getBitstreamFormats: () => mockFormats getBitstreamFormats: () => mockFormats
}; };

View File

@@ -1,6 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RegistryService } from '../../../core/registry/registry.service'; import { RegistryService } from '../../../core/registry/registry.service';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { BitstreamFormat } from '../../../core/registry/mock-bitstream-format.model'; import { BitstreamFormat } from '../../../core/registry/mock-bitstream-format.model';

View File

@@ -1,6 +1,6 @@
import { MetadataRegistryComponent } from './metadata-registry.component'; import { MetadataRegistryComponent } from './metadata-registry.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable'; import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@@ -8,7 +8,6 @@ import { By } from '@angular/platform-browser';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { RegistryService } from '../../../core/registry/registry.service'; import { RegistryService } from '../../../core/registry/registry.service';
import { SharedModule } from '../../../shared/shared.module';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe'; import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
import { PaginationComponent } from '../../../shared/pagination/pagination.component'; import { PaginationComponent } from '../../../shared/pagination/pagination.component';
@@ -33,7 +32,7 @@ describe('MetadataRegistryComponent', () => {
namespace: 'http://dspace.org/mockschema' namespace: 'http://dspace.org/mockschema'
} }
]; ];
const mockSchemas = Observable.of(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockSchemasList))); const mockSchemas = observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockSchemasList)));
const registryServiceStub = { const registryServiceStub = {
getMetadataSchemas: () => mockSchemas getMetadataSchemas: () => mockSchemas
}; };
@@ -68,5 +67,4 @@ describe('MetadataRegistryComponent', () => {
const mockName: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas tr:nth-child(2) td:nth-child(3)')).nativeElement; const mockName: HTMLElement = fixture.debugElement.query(By.css('#metadata-schemas tr:nth-child(2) td:nth-child(3)')).nativeElement;
expect(mockName.textContent).toBe('mock'); expect(mockName.textContent).toBe('mock');
}); });
}); });

View File

@@ -1,6 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { RegistryService } from '../../../core/registry/registry.service'; import { RegistryService } from '../../../core/registry/registry.service';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { MetadataSchema } from '../../../core/metadata/metadataschema.model'; import { MetadataSchema } from '../../../core/metadata/metadataschema.model';

View File

@@ -1,9 +1,6 @@
import { MetadataSchemaComponent } from './metadata-schema.component'; import { MetadataSchemaComponent } from './metadata-schema.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { of as observableOf } from 'rxjs';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { MetadataSchema } from '../../../core/metadata/metadataschema.model'; import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
@@ -69,15 +66,15 @@ describe('MetadataSchemaComponent', () => {
schema: mockSchemasList[1] schema: mockSchemasList[1]
} }
]; ];
const mockSchemas = Observable.of(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockSchemasList))); const mockSchemas = observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockSchemasList)));
const registryServiceStub = { const registryServiceStub = {
getMetadataSchemas: () => mockSchemas, getMetadataSchemas: () => mockSchemas,
getMetadataFieldsBySchema: (schema: MetadataSchema) => Observable.of(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockFieldsList.filter((value) => value.schema === schema)))), getMetadataFieldsBySchema: (schema: MetadataSchema) => observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockFieldsList.filter((value) => value.schema === schema)))),
getMetadataSchemaByName: (schemaName: string) => Observable.of(new RemoteData(false, false, true, undefined, mockSchemasList.filter((value) => value.prefix === schemaName)[0])) getMetadataSchemaByName: (schemaName: string) => observableOf(new RemoteData(false, false, true, undefined, mockSchemasList.filter((value) => value.prefix === schemaName)[0]))
}; };
const schemaNameParam = 'mock'; const schemaNameParam = 'mock';
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), { const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
params: Observable.of({ params: observableOf({
schemaName: schemaNameParam schemaName: schemaNameParam
}) })
}); });
@@ -87,10 +84,10 @@ describe('MetadataSchemaComponent', () => {
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()], imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe], declarations: [MetadataSchemaComponent, PaginationComponent, EnumKeysPipe],
providers: [ providers: [
{provide: RegistryService, useValue: registryServiceStub}, { provide: RegistryService, useValue: registryServiceStub },
{provide: ActivatedRoute, useValue: activatedRouteStub}, { provide: ActivatedRoute, useValue: activatedRouteStub },
{provide: HostWindowService, useValue: new HostWindowServiceStub(0)}, { provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{provide: Router, useValue: new RouterStub()} { provide: Router, useValue: new RouterStub() }
] ]
}).compileComponents(); }).compileComponents();
})); }));

View File

@@ -1,7 +1,7 @@
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { RegistryService } from '../../../core/registry/registry.service'; import { RegistryService } from '../../../core/registry/registry.service';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { MetadataField } from '../../../core/metadata/metadatafield.model'; import { MetadataField } from '../../../core/metadata/metadatafield.model';

View File

@@ -1,14 +1,13 @@
import {combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { ItemDataService } from '../../core/data/item-data.service'; import { ItemDataService } from '../../core/data/item-data.service';
import { Observable } from 'rxjs/Observable';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { Subscription } from 'rxjs/Subscription';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { Metadatum } from '../../core/shared/metadatum.model';
import { BrowseService } from '../../core/browse/browse.service'; import { BrowseService } from '../../core/browse/browse.service';
import { BrowseEntry } from '../../core/shared/browse-entry.model'; import { BrowseEntry } from '../../core/shared/browse-entry.model';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
@@ -47,7 +46,7 @@ export class BrowseByAuthorPageComponent implements OnInit {
sort: this.sortConfig sort: this.sortConfig
}); });
this.subs.push( this.subs.push(
Observable.combineLatest( observableCombineLatest(
this.route.params, this.route.params,
this.route.queryParams, this.route.queryParams,
(params, queryParams, ) => { (params, queryParams, ) => {

View File

@@ -1,13 +1,13 @@
import {combineLatest as observableCombineLatest, Observable , Subscription } from 'rxjs';
import { Component, OnInit } from '@angular/core'; import { Component, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { ItemDataService } from '../../core/data/item-data.service'; import { ItemDataService } from '../../core/data/item-data.service';
import { Observable } from 'rxjs/Observable';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { Item } from '../../core/shared/item.model'; import { Item } from '../../core/shared/item.model';
import { Subscription } from 'rxjs/Subscription';
import { ActivatedRoute, PRIMARY_OUTLET, UrlSegmentGroup } from '@angular/router'; import { ActivatedRoute, PRIMARY_OUTLET, UrlSegmentGroup } from '@angular/router';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
@@ -45,7 +45,7 @@ export class BrowseByTitlePageComponent implements OnInit {
sort: this.sortConfig sort: this.sortConfig
}); });
this.subs.push( this.subs.push(
Observable.combineLatest( observableCombineLatest(
this.route.params, this.route.params,
this.route.queryParams, this.route.queryParams,
(params, queryParams, ) => { (params, queryParams, ) => {

View File

@@ -1,8 +1,6 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable'; import { Observable, Subscription } from 'rxjs';
import { Subscription } from 'rxjs/Subscription';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { CollectionDataService } from '../core/data/collection-data.service'; import { CollectionDataService } from '../core/data/collection-data.service';
import { PaginatedList } from '../core/data/paginated-list'; import { PaginatedList } from '../core/data/paginated-list';
@@ -56,7 +54,9 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
} }
ngOnInit(): void { ngOnInit(): void {
this.collectionRD$ = this.route.data.map((data) => data.collection); this.collectionRD$ = this.route.data.pipe(
map((data) => data.collection)
);
this.logoRD$ = this.collectionRD$.pipe( this.logoRD$ = this.collectionRD$.pipe(
map((rd: RemoteData<Collection>) => rd.payload), map((rd: RemoteData<Collection>) => rd.payload),
filter((collection: Collection) => hasValue(collection)), filter((collection: Collection) => hasValue(collection)),
@@ -75,8 +75,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
pagination: pagination, pagination: pagination,
sort: this.sortConfig sort: this.sortConfig
}); });
})); })
);
} }
updatePage(searchOptions) { updatePage(searchOptions) {

View File

@@ -1,7 +1,7 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Collection } from '../core/shared/collection.model'; import { Collection } from '../core/shared/collection.model';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { CollectionDataService } from '../core/data/collection-data.service'; import { CollectionDataService } from '../core/data/collection-data.service';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { getSucceededRemoteData } from '../core/shared/operators'; import { getSucceededRemoteData } from '../core/shared/operators';

View File

@@ -1,7 +1,8 @@
import {mergeMap, filter, map} from 'rxjs/operators';
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Subscription'; import { Subscription, Observable } from 'rxjs';
import { CommunityDataService } from '../core/data/community-data.service'; import { CommunityDataService } from '../core/data/community-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';
@@ -12,7 +13,6 @@ import { MetadataService } from '../core/metadata/metadata.service';
import { fadeInOut } from '../shared/animations/fade'; import { fadeInOut } from '../shared/animations/fade';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { Observable } from 'rxjs/Observable';
@Component({ @Component({
selector: 'ds-community-page', selector: 'ds-community-page',
@@ -35,11 +35,11 @@ export class CommunityPageComponent implements OnInit, OnDestroy {
} }
ngOnInit(): void { ngOnInit(): void {
this.communityRD$ = this.route.data.map((data) => data.community); this.communityRD$ = this.route.data.pipe(map((data) => data.community));
this.logoRD$ = this.communityRD$ this.logoRD$ = this.communityRD$.pipe(
.map((rd: RemoteData<Community>) => rd.payload) map((rd: RemoteData<Community>) => rd.payload),
.filter((community: Community) => hasValue(community)) filter((community: Community) => hasValue(community)),
.flatMap((community: Community) => community.logo); mergeMap((community: Community) => community.logo));
} }
ngOnDestroy(): void { ngOnDestroy(): void {

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { getSucceededRemoteData } from '../core/shared/operators'; import { getSucceededRemoteData } from '../core/shared/operators';
import { Community } from '../core/shared/community.model'; import { Community } from '../core/shared/community.model';

View File

@@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
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';

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component } from '@angular/core'; import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';

View File

@@ -6,7 +6,7 @@ import { Collection } from '../../../core/shared/collection.model';
import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service';
import { getMockRemoteDataBuildService } from '../../../shared/mocks/mock-remote-data-build.service'; import { getMockRemoteDataBuildService } from '../../../shared/mocks/mock-remote-data-build.service';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { Observable } from 'rxjs/Observable'; import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data'; import { RemoteData } from '../../../core/data/remote-data';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@@ -22,8 +22,8 @@ const mockCollection1: Collection = Object.assign(new Collection(), {
}] }]
}); });
const succeededMockItem: Item = Object.assign(new Item(), {owningCollection: Observable.of(new RemoteData(false, false, true, null, mockCollection1))}); const succeededMockItem: Item = Object.assign(new Item(), {owningCollection: observableOf(new RemoteData(false, false, true, null, mockCollection1))});
const failedMockItem: Item = Object.assign(new Item(), {owningCollection: Observable.of(new RemoteData(false, false, false, null, mockCollection1))}); const failedMockItem: Item = Object.assign(new Item(), {owningCollection: observableOf(new RemoteData(false, false, false, null, mockCollection1))});
describe('CollectionsComponent', () => { describe('CollectionsComponent', () => {
beforeEach(async(() => { beforeEach(async(() => {

View File

@@ -1,5 +1,7 @@
import {map} from 'rxjs/operators';
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { Collection } from '../../../core/shared/collection.model'; import { Collection } from '../../../core/shared/collection.model';
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
@@ -35,11 +37,11 @@ export class CollectionsComponent implements OnInit {
// TODO: this should use parents, but the collections // TODO: this should use parents, but the collections
// for an Item aren't returned by the REST API yet, // for an Item aren't returned by the REST API yet,
// only the owning collection // only the owning collection
this.collections = this.item.owner.map((rd: RemoteData<Collection>) => [rd.payload]); this.collections = this.item.owner.pipe(map((rd: RemoteData<Collection>) => [rd.payload]));
} }
hasSucceeded() { hasSucceeded() {
return this.item.owner.map((rd: RemoteData<Collection>) => rd.hasSucceeded); return this.item.owner.pipe(map((rd: RemoteData<Collection>) => rd.hasSucceeded));
} }
} }

View File

@@ -1,7 +1,5 @@
<div class="simple-view-element"> <div class="simple-view-element" [class.d-none]="content.textContent.trim().length === 0">
<span *ngIf="content.children.length != 0"> <h5 class="simple-view-element-header" *ngIf="label">{{ label }}</h5>
<h5 class="simple-view-element-header" *ngIf="label">{{ label }}</h5>
</span>
<div #content class="simple-view-element-body"> <div #content class="simple-view-element-body">
<ng-content></ng-content> <ng-content></ng-content>
</div> </div>

View File

@@ -7,7 +7,8 @@ import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.componen
@Component({ @Component({
selector: 'ds-component-with-content', selector: 'ds-component-with-content',
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' + template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
' <div class="my content">\n' + ' <div class="my-content">\n' +
' <span></span>\n' +
' </div>\n' + ' </div>\n' +
'</ds-metadata-field-wrapper>' '</ds-metadata-field-wrapper>'
}) })
@@ -30,25 +31,37 @@ describe('MetadataFieldWrapperComponent', () => {
const wrapperSelector = '.simple-view-element'; const wrapperSelector = '.simple-view-element';
const labelSelector = '.simple-view-element-header'; const labelSelector = '.simple-view-element-header';
const contentSelector = '.my-content';
it('should create', () => { it('should create', () => {
expect(component).toBeDefined(); expect(component).toBeDefined();
}); });
it('should not show a label when there is no content', () => { it('should not show the component when there is no content', () => {
component.label = 'test label'; component.label = 'test label';
fixture.detectChanges(); fixture.detectChanges();
const debugLabel = fixture.debugElement.query(By.css(labelSelector)); const parentNative = fixture.nativeElement;
expect(debugLabel).toBeNull(); const nativeWrapper = parentNative.querySelector(wrapperSelector);
expect(nativeWrapper.classList.contains('d-none')).toBe(true);
}); });
it('should show a label when there is content', () => { it('should not show the component when there is DOM content but no text', () => {
const parentFixture = TestBed.createComponent(ContentComponent); const parentFixture = TestBed.createComponent(ContentComponent);
parentFixture.detectChanges(); parentFixture.detectChanges();
const parentComponent = parentFixture.componentInstance;
const parentNative = parentFixture.nativeElement; const parentNative = parentFixture.nativeElement;
const nativeLabel = parentNative.querySelector(labelSelector); const nativeWrapper = parentNative.querySelector(wrapperSelector);
expect(nativeLabel.textContent).toContain('test label'); expect(nativeWrapper.classList.contains('d-none')).toBe(true);
});
it('should show the component when there is text content', () => {
const parentFixture = TestBed.createComponent(ContentComponent);
parentFixture.detectChanges();
const parentNative = parentFixture.nativeElement;
const nativeContent = parentNative.querySelector(contentSelector);
nativeContent.textContent = 'lorem ipsum';
const nativeWrapper = parentNative.querySelector(wrapperSelector);
parentFixture.detectChanges();
expect(nativeWrapper.classList.contains('d-none')).toBe(false);
}); });
}); });

View File

@@ -1,10 +1,10 @@
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Bitstream } from '../../../../core/shared/bitstream.model';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';
import { FileSectionComponent } from '../../../simple/field-components/file-section/file-section.component'; import { FileSectionComponent } from '../../../simple/field-components/file-section/file-section.component';
import { hasValue } from '../../../../shared/empty.util'; import { map } from 'rxjs/operators';
/** /**
* This component renders the file section of the item * This component renders the file section of the item
@@ -33,7 +33,7 @@ export class FullFileSectionComponent extends FileSectionComponent implements On
initialize(): void { initialize(): void {
const originals = this.item.getFiles(); const originals = this.item.getFiles();
const licenses = this.item.getBitstreamsByBundleName('LICENSE'); const licenses = this.item.getBitstreamsByBundleName('LICENSE');
this.bitstreamsObs = Observable.combineLatest(originals, licenses, (o, l) => [...o, ...l]); this.bitstreamsObs = observableCombineLatest(originals, licenses).pipe(map(([o, l]) => [...o, ...l]));
this.bitstreamsObs.subscribe( this.bitstreamsObs.subscribe(
(files) => (files) =>
files.forEach( files.forEach(

View File

@@ -1,7 +1,9 @@
import {filter, map} from 'rxjs/operators';
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 { Observable } from 'rxjs';
import { ItemPageComponent } from '../simple/item-page.component'; import { ItemPageComponent } from '../simple/item-page.component';
import { Metadatum } from '../../core/shared/metadatum.model'; import { Metadatum } from '../../core/shared/metadatum.model';
@@ -41,9 +43,9 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit {
/*** AoT inheritance fix, will hopefully be resolved in the near future **/ /*** AoT inheritance fix, will hopefully be resolved in the near future **/
ngOnInit(): void { ngOnInit(): void {
super.ngOnInit(); super.ngOnInit();
this.metadata$ = this.itemRD$ this.metadata$ = this.itemRD$.pipe(
.map((rd: RemoteData<Item>) => rd.payload) map((rd: RemoteData<Item>) => rd.payload),
.filter((item: Item) => hasValue(item)) filter((item: Item) => hasValue(item)),
.map((item: Item) => item.metadata); map((item: Item) => item.metadata),);
} }
} }

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { getSucceededRemoteData } from '../core/shared/operators'; import { getSucceededRemoteData } from '../core/shared/operators';
import { ItemDataService } from '../core/data/item-data.service'; import { ItemDataService } from '../core/data/item-data.service';

View File

@@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { Bitstream } from '../../../../core/shared/bitstream.model'; import { Bitstream } from '../../../../core/shared/bitstream.model';
import { Item } from '../../../../core/shared/item.model'; import { Item } from '../../../../core/shared/item.model';

View File

@@ -1,7 +1,9 @@
import {mergeMap, filter, map} from 'rxjs/operators';
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 { Observable } from 'rxjs';
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';
@@ -44,11 +46,11 @@ export class ItemPageComponent implements OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.itemRD$ = this.route.data.map((data) => data.item); this.itemRD$ = this.route.data.pipe(map((data) => data.item));
this.metadataService.processRemoteData(this.itemRD$); this.metadataService.processRemoteData(this.itemRD$);
this.thumbnail$ = this.itemRD$ this.thumbnail$ = this.itemRD$.pipe(
.map((rd: RemoteData<Item>) => rd.payload) map((rd: RemoteData<Item>) => rd.payload),
.filter((item: Item) => hasValue(item)) filter((item: Item) => hasValue(item)),
.flatMap((item: Item) => item.getThumbnail()); mergeMap((item: Item) => item.getThumbnail()),);
} }
} }

View File

@@ -4,8 +4,7 @@ import { ActivatedRoute } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs/Observable'; import { of as observableOf } from 'rxjs';
import 'rxjs/add/observable/of';
import { LoginPageComponent } from './login-page.component'; import { LoginPageComponent } from './login-page.component';
import { ActivatedRouteStub } from '../shared/testing/active-router-stub'; import { ActivatedRouteStub } from '../shared/testing/active-router-stub';
@@ -21,7 +20,7 @@ describe('LoginPageComponent', () => {
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
dispatch: {}, dispatch: {},
/* tslint:enable:no-empty */ /* tslint:enable:no-empty */
select: Observable.of(true) select: observableOf(true)
}); });
beforeEach(async(() => { beforeEach(async(() => {

View File

@@ -1,4 +1,3 @@
import 'rxjs/add/observable/of';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; import { SortDirection, 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 { PaginatedSearchOptions } from './paginated-search-options.model'; import { PaginatedSearchOptions } from './paginated-search-options.model';

View File

@@ -7,7 +7,7 @@ import { SearchFilterConfig } from '../../../search-service/search-filter-config
import { FilterType } from '../../../search-service/filter-type.model'; import { FilterType } from '../../../search-service/filter-type.model';
import { FacetValue } from '../../../search-service/facet-value.model'; import { FacetValue } from '../../../search-service/facet-value.model';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { Observable } from 'rxjs/Observable'; import { of as observableOf } from 'rxjs';
import { SearchService } from '../../../search-service/search.service'; import { SearchService } from '../../../search-service/search.service';
import { SearchServiceStub } from '../../../../shared/testing/search-service-stub'; import { SearchServiceStub } from '../../../../shared/testing/search-service-stub';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
@@ -54,9 +54,9 @@ describe('SearchFacetFilterComponent', () => {
let filterService; let filterService;
let searchService; let searchService;
let router; let router;
const page = Observable.of(0); const page = observableOf(0);
const mockValues = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), values))); const mockValues = observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), values)));
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule],
@@ -65,11 +65,11 @@ describe('SearchFacetFilterComponent', () => {
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) }, { provide: SearchService, useValue: new SearchServiceStub(searchLink) },
{ provide: Router, useValue: new RouterStub() }, { provide: Router, useValue: new RouterStub() },
{ provide: FILTER_CONFIG, useValue: new SearchFilterConfig() }, { provide: FILTER_CONFIG, useValue: new SearchFilterConfig() },
{ provide: RemoteDataBuildService, useValue: {aggregate: () => Observable.of({})} }, { provide: RemoteDataBuildService, useValue: {aggregate: () => observableOf({})} },
{ provide: SearchConfigurationService, useValue: {searchOptions: Observable.of({})} }, { provide: SearchConfigurationService, useValue: {searchOptions: observableOf({})} },
{ {
provide: SearchFilterService, useValue: { provide: SearchFilterService, useValue: {
getSelectedValuesForFilter: () => Observable.of(selectedValues), getSelectedValuesForFilter: () => observableOf(selectedValues),
isFilterActiveWithValue: (paramName: string, filterValue: string) => true, isFilterActiveWithValue: (paramName: string, filterValue: string) => true,
getPage: (paramName: string) => page, getPage: (paramName: string) => page,
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */

View File

@@ -1,23 +1,26 @@
import {
combineLatest as observableCombineLatest,
of as observableOf,
BehaviorSubject,
Observable,
Subject,
Subscription
} from 'rxjs';
import { switchMap, distinctUntilChanged, first, map } from 'rxjs/operators';
import { animate, state, style, transition, trigger } from '@angular/animations'; import { animate, state, style, transition, trigger } from '@angular/animations';
import { Component, Inject, OnDestroy, OnInit } from '@angular/core'; import { Component, Inject, OnDestroy, OnInit } from '@angular/core';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Observable } from 'rxjs/Observable';
import { Subject } from 'rxjs/Subject';
import { Subscription } from 'rxjs/Subscription';
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
import { PaginatedList } from '../../../../core/data/paginated-list'; import { PaginatedList } from '../../../../core/data/paginated-list';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
import { hasNoValue, hasValue, isNotEmpty } from '../../../../shared/empty.util'; import { hasNoValue, hasValue, isNotEmpty } from '../../../../shared/empty.util';
import { EmphasizePipe } from '../../../../shared/utils/emphasize.pipe'; import { EmphasizePipe } from '../../../../shared/utils/emphasize.pipe';
import { SearchOptions } from '../../../search-options.model';
import { FacetValue } from '../../../search-service/facet-value.model'; import { FacetValue } from '../../../search-service/facet-value.model';
import { SearchFilterConfig } from '../../../search-service/search-filter-config.model'; import { SearchFilterConfig } from '../../../search-service/search-filter-config.model';
import { SearchService } from '../../../search-service/search.service'; import { SearchService } from '../../../search-service/search.service';
import { FILTER_CONFIG, SearchFilterService } from '../search-filter.service'; import { FILTER_CONFIG, SearchFilterService } from '../search-filter.service';
import { SearchConfigurationService } from '../../../search-service/search-configuration.service'; import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
import { getSucceededRemoteData } from '../../../../core/shared/operators'; import { getSucceededRemoteData } from '../../../../core/shared/operators';
import { map } from 'rxjs/operators';
@Component({ @Component({
selector: 'ds-search-facet-filter', selector: 'ds-search-facet-filter',
@@ -56,7 +59,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
/** /**
* Emits the result values for this filter found by the current filter query * Emits the result values for this filter found by the current filter query
*/ */
filterSearchResults: Observable<any[]> = Observable.of([]); filterSearchResults: Observable<any[]> = observableOf([]);
/** /**
* Emits the active values for this filter * Emits the active values for this filter
@@ -82,25 +85,28 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
*/ */
ngOnInit(): void { ngOnInit(): void {
this.filterValues$ = new BehaviorSubject(new RemoteData(true, false, undefined, undefined, undefined)); this.filterValues$ = new BehaviorSubject(new RemoteData(true, false, undefined, undefined, undefined));
this.currentPage = this.getCurrentPage().distinctUntilChanged(); this.currentPage = this.getCurrentPage().pipe(distinctUntilChanged());
this.selectedValues = this.filterService.getSelectedValuesForFilter(this.filterConfig); this.selectedValues = this.filterService.getSelectedValuesForFilter(this.filterConfig);
const searchOptions = this.searchConfigService.searchOptions; const searchOptions = this.searchConfigService.searchOptions;
this.subs.push(this.searchConfigService.searchOptions.subscribe(() => this.updateFilterValueList())); this.subs.push(this.searchConfigService.searchOptions.subscribe(() => this.updateFilterValueList()));
const facetValues = Observable.combineLatest(searchOptions, this.currentPage, (options, page) => { const facetValues = observableCombineLatest(searchOptions, this.currentPage).pipe(
return { options, page } map(([options, page]) => {
}).switchMap(({ options, page }) => { return { options, page }
return this.searchService.getFacetValuesFor(this.filterConfig, page, options) }),
.pipe( switchMap(({ options, page }) => {
getSucceededRemoteData(), return this.searchService.getFacetValuesFor(this.filterConfig, page, options)
map((results) => { .pipe(
return { getSucceededRemoteData(),
values: Observable.of(results), map((results) => {
page: page return {
}; values: observableOf(results),
} page: page
};
}
)
) )
) })
}); );
let filterValues = []; let filterValues = [];
this.subs.push(facetValues.subscribe((facetOutcome) => { this.subs.push(facetValues.subscribe((facetOutcome) => {
const newValues$ = facetOutcome.values; const newValues$ = facetOutcome.values;
@@ -120,7 +126,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
this.animationState = 'ready'; this.animationState = 'ready';
this.filterValues$.next(rd); this.filterValues$.next(rd);
})); }));
this.subs.push(newValues$.first().subscribe((rd) => { this.subs.push(newValues$.pipe(first()).subscribe((rd) => {
this.isLastPage$.next(hasNoValue(rd.payload.next)) this.isLastPage$.next(hasNoValue(rd.payload.next))
})); }));
})); }));
@@ -183,7 +189,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
* @param data The string from the input field * @param data The string from the input field
*/ */
onSubmit(data: any) { onSubmit(data: any) {
this.selectedValues.first().subscribe((selectedValues) => { this.selectedValues.pipe(first()).subscribe((selectedValues) => {
if (isNotEmpty(data)) { if (isNotEmpty(data)) {
this.router.navigate([this.getSearchLink()], { this.router.navigate([this.getSearchLink()], {
queryParams: queryParams:
@@ -192,7 +198,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
}); });
this.filter = ''; this.filter = '';
} }
this.filterSearchResults = Observable.of([]); this.filterSearchResults = observableOf([]);
} }
) )
} }
@@ -214,12 +220,12 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
* @returns {Observable<any>} The changed filter parameters * @returns {Observable<any>} The changed filter parameters
*/ */
getRemoveParams(value: string): Observable<any> { getRemoveParams(value: string): Observable<any> {
return this.selectedValues.map((selectedValues) => { return this.selectedValues.pipe(map((selectedValues) => {
return { return {
[this.filterConfig.paramName]: selectedValues.filter((v) => v !== value), [this.filterConfig.paramName]: selectedValues.filter((v) => v !== value),
page: 1 page: 1
}; };
}); }));
} }
/** /**
@@ -228,12 +234,12 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
* @returns {Observable<any>} The changed filter parameters * @returns {Observable<any>} The changed filter parameters
*/ */
getAddParams(value: string): Observable<any> { getAddParams(value: string): Observable<any> {
return this.selectedValues.map((selectedValues) => { return this.selectedValues.pipe(map((selectedValues) => {
return { return {
[this.filterConfig.paramName]: [...selectedValues, value], [this.filterConfig.paramName]: [...selectedValues, value],
page: 1 page: 1
}; };
}); }));
} }
/** /**
@@ -252,7 +258,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
*/ */
findSuggestions(data): void { findSuggestions(data): void {
if (isNotEmpty(data)) { if (isNotEmpty(data)) {
this.searchConfigService.searchOptions.first().subscribe( this.searchConfigService.searchOptions.pipe(first()).subscribe(
(options) => { (options) => {
this.filterSearchResults = this.searchService.getFacetValuesFor(this.filterConfig, 1, options, data.toLowerCase()) this.filterSearchResults = this.searchService.getFacetValuesFor(this.filterConfig, 1, options, data.toLowerCase())
.pipe( .pipe(
@@ -267,7 +273,7 @@ export class SearchFacetFilterComponent implements OnInit, OnDestroy {
} }
) )
} else { } else {
this.filterSearchResults = Observable.of([]); this.filterSearchResults = observableOf([]);
} }
} }

View File

@@ -3,7 +3,7 @@ import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs/Observable'; import { Observable, of as observableOf } from 'rxjs';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { SearchFilterService } from './search-filter.service'; import { SearchFilterService } from './search-filter.service';
import { SearchService } from '../../search-service/search.service'; import { SearchService } from '../../search-service/search.service';
@@ -38,19 +38,19 @@ describe('SearchFilterComponent', () => {
initialExpand: (filter) => { initialExpand: (filter) => {
}, },
getSelectedValuesForFilter: (filter) => { getSelectedValuesForFilter: (filter) => {
return Observable.of([filterName1, filterName2, filterName3]) return observableOf([filterName1, filterName2, filterName3])
}, },
isFilterActive: (filter) => { isFilterActive: (filter) => {
return Observable.of([filterName1, filterName2, filterName3].indexOf(filter) >= 0); return observableOf([filterName1, filterName2, filterName3].indexOf(filter) >= 0);
}, },
isCollapsed: (filter) => { isCollapsed: (filter) => {
return Observable.of(true) return observableOf(true)
} }
/* tslint:enable:no-empty */ /* tslint:enable:no-empty */
}; };
let filterService; let filterService;
const mockResults = Observable.of(['test', 'data']); const mockResults = observableOf(['test', 'data']);
const searchServiceStub = { const searchServiceStub = {
getFacetValuesFor: (filter) => mockResults getFacetValuesFor: (filter) => mockResults
}; };
@@ -140,7 +140,7 @@ describe('SearchFilterComponent', () => {
describe('when isCollapsed is called and the filter is collapsed', () => { describe('when isCollapsed is called and the filter is collapsed', () => {
let isActive: Observable<boolean>; let isActive: Observable<boolean>;
beforeEach(() => { beforeEach(() => {
filterService.isCollapsed = () => Observable.of(true); filterService.isCollapsed = () => observableOf(true);
isActive = comp.isCollapsed(); isActive = comp.isCollapsed();
}); });
@@ -155,7 +155,7 @@ describe('SearchFilterComponent', () => {
describe('when isCollapsed is called and the filter is not collapsed', () => { describe('when isCollapsed is called and the filter is not collapsed', () => {
let isActive: Observable<boolean>; let isActive: Observable<boolean>;
beforeEach(() => { beforeEach(() => {
filterService.isCollapsed = () => Observable.of(false); filterService.isCollapsed = () => observableOf(false);
isActive = comp.isCollapsed(); isActive = comp.isCollapsed();
}); });

View File

@@ -1,7 +1,9 @@
import {first} from 'rxjs/operators';
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { SearchFilterConfig } from '../../search-service/search-filter-config.model'; import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
import { SearchFilterService } from './search-filter.service'; import { SearchFilterService } from './search-filter.service';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { slide } from '../../../shared/animations/slide'; import { slide } from '../../../shared/animations/slide';
import { isNotEmpty } from '../../../shared/empty.util'; import { isNotEmpty } from '../../../shared/empty.util';
@@ -35,7 +37,7 @@ export class SearchFilterComponent implements OnInit {
* Else, the filter should initially be collapsed * Else, the filter should initially be collapsed
*/ */
ngOnInit() { ngOnInit() {
this.getSelectedValues().first().subscribe((isActive) => { this.getSelectedValues().pipe(first()).subscribe((isActive) => {
if (this.filter.isOpenByDefault || isNotEmpty(isActive)) { if (this.filter.isOpenByDefault || isNotEmpty(isActive)) {
this.initialExpand(); this.initialExpand();
} else { } else {

View File

@@ -1,16 +1,20 @@
import { Observable } from 'rxjs/Observable';
import { SearchFilterService } from './search-filter.service'; import { SearchFilterService } from './search-filter.service';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { import {
SearchFilterCollapseAction, SearchFilterDecrementPageAction, SearchFilterExpandAction, SearchFilterCollapseAction,
SearchFilterDecrementPageAction,
SearchFilterExpandAction,
SearchFilterIncrementPageAction, SearchFilterIncrementPageAction,
SearchFilterInitialCollapseAction, SearchFilterInitialExpandAction, SearchFilterResetPageAction, SearchFilterInitialCollapseAction,
SearchFilterInitialExpandAction,
SearchFilterResetPageAction,
SearchFilterToggleAction SearchFilterToggleAction
} from './search-filter.actions'; } from './search-filter.actions';
import { SearchFiltersState } from './search-filter.reducer'; import { SearchFiltersState } from './search-filter.reducer';
import { SearchFilterConfig } from '../../search-service/search-filter-config.model'; import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
import { FilterType } from '../../search-service/filter-type.model'; import { FilterType } from '../../search-service/filter-type.model';
import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub'; import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub';
import { of as observableOf } from 'rxjs';
describe('SearchFilterService', () => { describe('SearchFilterService', () => {
let service: SearchFilterService; let service: SearchFilterService;
@@ -28,7 +32,7 @@ describe('SearchFilterService', () => {
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
dispatch: {}, dispatch: {},
/* tslint:enable:no-empty */ /* tslint:enable:no-empty */
select: Observable.of(true) select: observableOf(true)
}); });
const routeServiceStub: any = { const routeServiceStub: any = {
@@ -42,10 +46,10 @@ describe('SearchFilterService', () => {
addQueryParameterValue: (param: string, value: string) => { addQueryParameterValue: (param: string, value: string) => {
}, },
getQueryParameterValues: (param: string) => { getQueryParameterValues: (param: string) => {
return Observable.of({}); return observableOf({});
}, },
getQueryParamsWithPrefix: (param: string) => { getQueryParamsWithPrefix: (param: string) => {
return Observable.of({}); return observableOf({});
} }
/* tslint:enable:no-empty */ /* tslint:enable:no-empty */
}; };

View File

@@ -1,8 +1,8 @@
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { Injectable, InjectionToken } from '@angular/core'; import { Injectable, InjectionToken } from '@angular/core';
import { distinctUntilChanged, map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { SearchFiltersState, SearchFilterState } from './search-filter.reducer'; import { SearchFiltersState, SearchFilterState } from './search-filter.reducer';
import { createSelector, MemoizedSelector, Store } from '@ngrx/store'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { import {
SearchFilterCollapseAction, SearchFilterCollapseAction,
SearchFilterDecrementPageAction, SearchFilterDecrementPageAction,
@@ -13,14 +13,10 @@ import {
SearchFilterResetPageAction, SearchFilterResetPageAction,
SearchFilterToggleAction SearchFilterToggleAction
} from './search-filter.actions'; } from './search-filter.actions';
import { hasValue, isEmpty, isNotEmpty, } from '../../../shared/empty.util'; import { hasValue, isNotEmpty, } from '../../../shared/empty.util';
import { SearchFilterConfig } from '../../search-service/search-filter-config.model'; import { SearchFilterConfig } from '../../search-service/search-filter-config.model';
import { RouteService } from '../../../shared/services/route.service'; import { RouteService } from '../../../shared/services/route.service';
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; import { Params } from '@angular/router';
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
import { SearchOptions } from '../../search-options.model';
import { PaginatedSearchOptions } from '../../paginated-search-options.model';
import { ActivatedRoute, Params } from '@angular/router';
const filterStateSelector = (state: SearchFiltersState) => state.searchFilter; const filterStateSelector = (state: SearchFiltersState) => state.searchFilter;
@@ -63,13 +59,19 @@ export class SearchFilterService {
*/ */
getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable<string[]> { getSelectedValuesForFilter(filterConfig: SearchFilterConfig): Observable<string[]> {
const values$ = this.routeService.getQueryParameterValues(filterConfig.paramName); const values$ = this.routeService.getQueryParameterValues(filterConfig.paramName);
const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').map((params: Params) => [].concat(...Object.values(params))); const prefixValues$ = this.routeService.getQueryParamsWithPrefix(filterConfig.paramName + '.').pipe(
return Observable.combineLatest(values$, prefixValues$, (values, prefixValues) => { map((params: Params) => [].concat(...Object.values(params)))
if (isNotEmpty(values)) { );
return values;
} return observableCombineLatest(values$, prefixValues$).pipe(
return prefixValues; map(([values, prefixValues]) => {
}) if (isNotEmpty(values)) {
return values;
}
return prefixValues;
}
)
)
} }
/** /**
@@ -78,14 +80,16 @@ export class SearchFilterService {
* @returns {Observable<boolean>} Emits the current collapsed state of the given filter, if it's unavailable, return false * @returns {Observable<boolean>} Emits the current collapsed state of the given filter, if it's unavailable, return false
*/ */
isCollapsed(filterName: string): Observable<boolean> { isCollapsed(filterName: string): Observable<boolean> {
return this.store.select(filterByNameSelector(filterName)) return this.store.pipe(
.map((object: SearchFilterState) => { select(filterByNameSelector(filterName)),
map((object: SearchFilterState) => {
if (object) { if (object) {
return object.filterCollapsed; return object.filterCollapsed;
} else { } else {
return false; return false;
} }
}); })
);
} }
/** /**
@@ -94,14 +98,15 @@ export class SearchFilterService {
* @returns {Observable<boolean>} Emits the current page state of the given filter, if it's unavailable, return 1 * @returns {Observable<boolean>} Emits the current page state of the given filter, if it's unavailable, return 1
*/ */
getPage(filterName: string): Observable<number> { getPage(filterName: string): Observable<number> {
return this.store.select(filterByNameSelector(filterName)) return this.store.pipe(
.map((object: SearchFilterState) => { select(filterByNameSelector(filterName)),
map((object: SearchFilterState) => {
if (object) { if (object) {
return object.page; return object.page;
} else { } else {
return 1; return 1;
} }
}); }));
} }
/** /**
@@ -159,6 +164,7 @@ export class SearchFilterService {
public incrementPage(filterName: string): void { public incrementPage(filterName: string): void {
this.store.dispatch(new SearchFilterIncrementPageAction(filterName)); this.store.dispatch(new SearchFilterIncrementPageAction(filterName));
} }
/** /**
* Dispatches a reset page action to the store for a given filter * Dispatches a reset page action to the store for a given filter
* @param {string} filterName The filter for which the action is dispatched * @param {string} filterName The filter for which the action is dispatched

View File

@@ -7,7 +7,7 @@ import { SearchFilterConfig } from '../../../search-service/search-filter-config
import { FilterType } from '../../../search-service/filter-type.model'; import { FilterType } from '../../../search-service/filter-type.model';
import { FacetValue } from '../../../search-service/facet-value.model'; import { FacetValue } from '../../../search-service/facet-value.model';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { Observable } from 'rxjs/Observable'; import { of as observableOf } from 'rxjs'
import { SearchService } from '../../../search-service/search.service'; import { SearchService } from '../../../search-service/search.service';
import { SearchServiceStub } from '../../../../shared/testing/search-service-stub'; import { SearchServiceStub } from '../../../../shared/testing/search-service-stub';
import { RemoteData } from '../../../../core/data/remote-data'; import { RemoteData } from '../../../../core/data/remote-data';
@@ -56,13 +56,13 @@ describe('SearchRangeFilterComponent', () => {
]; ];
const searchLink = '/search'; const searchLink = '/search';
const selectedValues = Observable.of([value1]); const selectedValues = observableOf([value1]);
let filterService; let filterService;
let searchService; let searchService;
let router; let router;
const page = Observable.of(0); const page = observableOf(0);
const mockValues = Observable.of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), values))); const mockValues = observableOf(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), values)));
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule], imports: [TranslateModule.forRoot(), NoopAnimationsModule, FormsModule],
@@ -71,10 +71,10 @@ describe('SearchRangeFilterComponent', () => {
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) }, { provide: SearchService, useValue: new SearchServiceStub(searchLink) },
{ provide: Router, useValue: new RouterStub() }, { provide: Router, useValue: new RouterStub() },
{ provide: FILTER_CONFIG, useValue: mockFilterConfig }, { provide: FILTER_CONFIG, useValue: mockFilterConfig },
{ provide: RemoteDataBuildService, useValue: {aggregate: () => Observable.of({})} }, { provide: RemoteDataBuildService, useValue: {aggregate: () => observableOf({})} },
{ provide: RouteService, useValue: {getQueryParameterValue: () => Observable.of({})} }, { provide: RouteService, useValue: {getQueryParameterValue: () => observableOf({})} },
{ provide: SearchConfigurationService, useValue: { { provide: SearchConfigurationService, useValue: {
searchOptions: Observable.of({}) } searchOptions: observableOf({}) }
}, },
{ {
provide: SearchFilterService, useValue: { provide: SearchFilterService, useValue: {

View File

@@ -1,3 +1,10 @@
import {
of as observableOf,
combineLatest as observableCombineLatest,
Observable,
Subscription
} from 'rxjs';
import { map, startWith } from 'rxjs/operators';
import { isPlatformBrowser } from '@angular/common'; import { isPlatformBrowser } from '@angular/common';
import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core'; import { Component, Inject, OnDestroy, OnInit, PLATFORM_ID } from '@angular/core';
import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../../../core/cache/builders/remote-data-build.service';
@@ -12,10 +19,8 @@ import { FILTER_CONFIG, SearchFilterService } from '../search-filter.service';
import { SearchService } from '../../../search-service/search.service'; import { SearchService } from '../../../search-service/search.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import * as moment from 'moment'; import * as moment from 'moment';
import { Observable } from 'rxjs/Observable';
import { RouteService } from '../../../../shared/services/route.service'; import { RouteService } from '../../../../shared/services/route.service';
import { hasValue } from '../../../../shared/empty.util'; import { hasValue } from '../../../../shared/empty.util';
import { Subscription } from 'rxjs/Subscription';
import { SearchConfigurationService } from '../../../search-service/search-configuration.service'; import { SearchConfigurationService } from '../../../search-service/search-configuration.service';
/** /**
@@ -80,13 +85,15 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
super.ngOnInit(); super.ngOnInit();
this.min = moment(this.filterConfig.minValue, dateFormats).year() || this.min; this.min = moment(this.filterConfig.minValue, dateFormats).year() || this.min;
this.max = moment(this.filterConfig.maxValue, dateFormats).year() || this.max; this.max = moment(this.filterConfig.maxValue, dateFormats).year() || this.max;
const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + minSuffix).startWith(undefined); const iniMin = this.route.getQueryParameterValue(this.filterConfig.paramName + minSuffix).pipe(startWith(undefined));
const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + maxSuffix).startWith(undefined); const iniMax = this.route.getQueryParameterValue(this.filterConfig.paramName + maxSuffix).pipe(startWith(undefined));
this.sub = Observable.combineLatest(iniMin, iniMax, (min, max) => { this.sub = observableCombineLatest(iniMin, iniMax).pipe(
const minimum = hasValue(min) ? min : this.min; map(([min, max]) => {
const maximum = hasValue(max) ? max : this.max; const minimum = hasValue(min) ? min : this.min;
return [minimum, maximum] const maximum = hasValue(max) ? max : this.max;
}).subscribe((minmax) => this.range = minmax); return [minimum, maximum]
})
).subscribe((minmax) => this.range = minmax);
} }
/** /**
@@ -98,7 +105,7 @@ export class SearchRangeFilterComponent extends SearchFacetFilterComponent imple
const parts = value.split(rangeDelimiter); const parts = value.split(rangeDelimiter);
const min = parts.length > 1 ? parts[0].trim() : value; const min = parts.length > 1 ? parts[0].trim() : value;
const max = parts.length > 1 ? parts[1].trim() : value; const max = parts.length > 1 ? parts[1].trim() : value;
return Observable.of( return observableOf(
{ {
[this.filterConfig.paramName + minSuffix]: [min], [this.filterConfig.paramName + minSuffix]: [min],
[this.filterConfig.paramName + maxSuffix]: [max], [this.filterConfig.paramName + maxSuffix]: [max],

View File

@@ -1,6 +1,6 @@
import { animate, state, style, transition, trigger } from '@angular/animations'; import { animate, state, style, transition, trigger } from '@angular/animations';
import { Component, HostBinding, OnInit } from '@angular/core'; import { Component, HostBinding, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { FilterType } from '../../../search-service/filter-type.model'; import { FilterType } from '../../../search-service/filter-type.model';
import { import {
facetLoad, facetLoad,

View File

@@ -7,8 +7,8 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { SearchFilterService } from './search-filter/search-filter.service'; import { SearchFilterService } from './search-filter/search-filter.service';
import { SearchFiltersComponent } from './search-filters.component'; import { SearchFiltersComponent } from './search-filters.component';
import { SearchService } from '../search-service/search.service'; import { SearchService } from '../search-service/search.service';
import { Observable } from 'rxjs/Observable';
import { SearchConfigurationService } from '../search-service/search-configuration.service'; import { SearchConfigurationService } from '../search-service/search-configuration.service';
import { of as observableOf } from 'rxjs';
describe('SearchFiltersComponent', () => { describe('SearchFiltersComponent', () => {
let comp: SearchFiltersComponent; let comp: SearchFiltersComponent;
@@ -17,7 +17,7 @@ describe('SearchFiltersComponent', () => {
const searchServiceStub = { const searchServiceStub = {
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
getConfig: () => getConfig: () =>
Observable.of({ hasSucceeded: true, payload: [] }), observableOf({ hasSucceeded: true, payload: [] }),
getClearFiltersQueryParams: () => { getClearFiltersQueryParams: () => {
}, },
getSearchLink: () => { getSearchLink: () => {
@@ -31,7 +31,7 @@ describe('SearchFiltersComponent', () => {
}; };
const searchConfigServiceStub = jasmine.createSpyObj('SearchConfigurationService', { const searchConfigServiceStub = jasmine.createSpyObj('SearchConfigurationService', {
getCurrentFrontendFilters: Observable.of({}) getCurrentFrontendFilters: observableOf({})
}); });
beforeEach(async(() => { beforeEach(async(() => {

View File

@@ -1,8 +1,10 @@
import { Observable, of as observableOf } from 'rxjs';
import { filter, map, mergeMap, startWith, switchMap } from 'rxjs/operators';
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { SearchService } from '../search-service/search.service'; import { SearchService } from '../search-service/search.service';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { SearchFilterConfig } from '../search-service/search-filter-config.model'; import { SearchFilterConfig } from '../search-service/search-filter-config.model';
import { Observable } from 'rxjs/Observable';
import { SearchConfigurationService } from '../search-service/search-configuration.service'; import { SearchConfigurationService } from '../search-service/search-configuration.service';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { SearchFilterService } from './search-filter/search-filter.service'; import { SearchFilterService } from './search-filter/search-filter.service';
@@ -37,10 +39,10 @@ export class SearchFiltersComponent {
*/ */
constructor(private searchService: SearchService, private searchConfigService: SearchConfigurationService, private filterService: SearchFilterService) { constructor(private searchService: SearchService, private searchConfigService: SearchConfigurationService, private filterService: SearchFilterService) {
this.filters = searchService.getConfig().pipe(getSucceededRemoteData()); this.filters = searchService.getConfig().pipe(getSucceededRemoteData());
this.clearParams = searchConfigService.getCurrentFrontendFilters().map((filters) => { this.clearParams = searchConfigService.getCurrentFrontendFilters().pipe(map((filters) => {
Object.keys(filters).forEach((f) => filters[f] = null); Object.keys(filters).forEach((f) => filters[f] = null);
return filters; return filters;
}); }));
} }
/** /**
@@ -55,22 +57,23 @@ export class SearchFiltersComponent {
* @param {SearchFilterConfig} filter The filter to check for * @param {SearchFilterConfig} filter The filter to check for
* @returns {Observable<boolean>} Emits true whenever a given filter config should be shown * @returns {Observable<boolean>} Emits true whenever a given filter config should be shown
*/ */
isActive(filter: SearchFilterConfig): Observable<boolean> { isActive(filterConfig: SearchFilterConfig): Observable<boolean> {
return this.filterService.getSelectedValuesForFilter(filter) // console.log(filter.name);
.flatMap((isActive) => { return this.filterService.getSelectedValuesForFilter(filterConfig).pipe(
mergeMap((isActive) => {
if (isNotEmpty(isActive)) { if (isNotEmpty(isActive)) {
return Observable.of(true); return observableOf(true);
} else { } else {
return this.searchConfigService.searchOptions return this.searchConfigService.searchOptions.pipe(
.switchMap((options) => { switchMap((options) => {
return this.searchService.getFacetValuesFor(filter, 1, options) return this.searchService.getFacetValuesFor(filterConfig, 1, options).pipe(
.filter((RD) => !RD.isLoading) filter((RD) => !RD.isLoading),
.map((valuesRD) => { map((valuesRD) => {
return valuesRD.payload.totalElements > 0 return valuesRD.payload.totalElements > 0
}) }),)
} }
) ))
} }
}).startWith(true); }),startWith(true),);
} }
} }

View File

@@ -6,7 +6,7 @@ import { SearchService } from '../search-service/search.service';
import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core';
import { FormsModule } from '@angular/forms'; import { FormsModule } from '@angular/forms';
import { SearchServiceStub } from '../../shared/testing/search-service-stub'; import { SearchServiceStub } from '../../shared/testing/search-service-stub';
import { Observable } from 'rxjs/Observable'; import { Observable, of as observableOf } from 'rxjs';
import { Params } from '@angular/router'; import { Params } from '@angular/router';
import { ObjectKeysPipe } from '../../shared/utils/object-keys-pipe'; import { ObjectKeysPipe } from '../../shared/utils/object-keys-pipe';
import { SearchConfigurationService } from '../search-service/search-configuration.service'; import { SearchConfigurationService } from '../search-service/search-configuration.service';
@@ -35,7 +35,7 @@ describe('SearchLabelsComponent', () => {
declarations: [SearchLabelsComponent, ObjectKeysPipe], declarations: [SearchLabelsComponent, ObjectKeysPipe],
providers: [ providers: [
{ provide: SearchService, useValue: new SearchServiceStub(searchLink) }, { provide: SearchService, useValue: new SearchServiceStub(searchLink) },
{ provide: SearchConfigurationService, useValue: {getCurrentFrontendFilters : () => Observable.of({})} } { provide: SearchConfigurationService, useValue: {getCurrentFrontendFilters : () => observableOf({})} }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).overrideComponent(SearchLabelsComponent, { }).overrideComponent(SearchLabelsComponent, {
@@ -47,7 +47,7 @@ describe('SearchLabelsComponent', () => {
fixture = TestBed.createComponent(SearchLabelsComponent); fixture = TestBed.createComponent(SearchLabelsComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
searchService = (comp as any).searchService; searchService = (comp as any).searchService;
(comp as any).appliedFilters = Observable.of(mockFilters); (comp as any).appliedFilters = observableOf(mockFilters);
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -1,6 +1,6 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { SearchService } from '../search-service/search.service'; import { SearchService } from '../search-service/search.service';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { Params } from '@angular/router'; import { Params } from '@angular/router';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';

View File

@@ -1,4 +1,3 @@
import 'rxjs/add/observable/of';
import { PaginatedSearchOptions } from './paginated-search-options.model'; import { PaginatedSearchOptions } from './paginated-search-options.model';
import { SearchOptions } from './search-options.model'; import { SearchOptions } from './search-options.model';
import { SearchFilter } from './search-filter.model'; import { SearchFilter } from './search-filter.model';

View File

@@ -5,8 +5,7 @@ import { RouterTestingModule } from '@angular/router/testing';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { cold, hot } from 'jasmine-marbles'; import { cold, hot } from 'jasmine-marbles';
import { Observable } from 'rxjs/Observable'; import { of as observableOf } from 'rxjs';
import 'rxjs/add/observable/of';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { CommunityDataService } from '../core/data/community-data.service'; import { CommunityDataService } from '../core/data/community-data.service';
import { HostWindowService } from '../shared/host-window.service'; import { HostWindowService } from '../shared/host-window.service';
@@ -30,18 +29,18 @@ describe('SearchPageComponent', () => {
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
dispatch: {}, dispatch: {},
/* tslint:enable:no-empty */ /* tslint:enable:no-empty */
select: Observable.of(true) select: observableOf(true)
}); });
const pagination: PaginationComponentOptions = new PaginationComponentOptions(); const pagination: PaginationComponentOptions = new PaginationComponentOptions();
pagination.id = 'search-results-pagination'; pagination.id = 'search-results-pagination';
pagination.currentPage = 1; pagination.currentPage = 1;
pagination.pageSize = 10; pagination.pageSize = 10;
const sort: SortOptions = new SortOptions('score', SortDirection.DESC); const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
const mockResults = Observable.of(new RemoteData(false, false, true, null, ['test', 'data'])); const mockResults = observableOf(new RemoteData(false, false, true, null, ['test', 'data']));
const searchServiceStub = jasmine.createSpyObj('SearchService', { const searchServiceStub = jasmine.createSpyObj('SearchService', {
search: mockResults, search: mockResults,
getSearchLink: '/search', getSearchLink: '/search',
getScopes: Observable.of(['test-scope']) getScopes: observableOf(['test-scope'])
}); });
const queryParam = 'test query'; const queryParam = 'test query';
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f'; const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
@@ -52,15 +51,15 @@ describe('SearchPageComponent', () => {
sort sort
}; };
const activatedRouteStub = { const activatedRouteStub = {
queryParams: Observable.of({ queryParams: observableOf({
query: queryParam, query: queryParam,
scope: scopeParam scope: scopeParam
}) })
}; };
const sidebarService = { const sidebarService = {
isCollapsed: Observable.of(true), isCollapsed: observableOf(true),
collapse: () => this.isCollapsed = Observable.of(true), collapse: () => this.isCollapsed = observableOf(true),
expand: () => this.isCollapsed = Observable.of(false) expand: () => this.isCollapsed = observableOf(false)
}; };
beforeEach(async(() => { beforeEach(async(() => {
@@ -80,9 +79,9 @@ describe('SearchPageComponent', () => {
{ {
provide: HostWindowService, useValue: jasmine.createSpyObj('hostWindowService', provide: HostWindowService, useValue: jasmine.createSpyObj('hostWindowService',
{ {
isXs: Observable.of(true), isXs: observableOf(true),
isSm: Observable.of(false), isSm: observableOf(false),
isXsOrSm: Observable.of(true) isXsOrSm: observableOf(true)
}) })
}, },
{ {
@@ -98,7 +97,7 @@ describe('SearchPageComponent', () => {
paginatedSearchOptions: hot('a', { paginatedSearchOptions: hot('a', {
a: paginatedSearchOptions a: paginatedSearchOptions
}), }),
getCurrentScope: (a) => Observable.of('test-id') getCurrentScope: (a) => observableOf('test-id')
} }
}, },
], ],
@@ -154,7 +153,7 @@ describe('SearchPageComponent', () => {
beforeEach(() => { beforeEach(() => {
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement; menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
comp.isSidebarCollapsed = () => Observable.of(true); comp.isSidebarCollapsed = () => observableOf(true);
fixture.detectChanges(); fixture.detectChanges();
}); });
@@ -169,7 +168,7 @@ describe('SearchPageComponent', () => {
beforeEach(() => { beforeEach(() => {
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement; menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
comp.isSidebarCollapsed = () => Observable.of(false); comp.isSidebarCollapsed = () => observableOf(false);
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -1,6 +1,6 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { BehaviorSubject, Observable, Subscription } from 'rxjs';
import { flatMap, switchMap, } from 'rxjs/operators'; import { switchMap, } from 'rxjs/operators';
import { PaginatedList } from '../core/data/paginated-list'; import { PaginatedList } from '../core/data/paginated-list';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
import { DSpaceObject } from '../core/shared/dspace-object.model'; import { DSpaceObject } from '../core/shared/dspace-object.model';
@@ -11,9 +11,7 @@ import { SearchFilterService } from './search-filters/search-filter/search-filte
import { SearchResult } from './search-result.model'; import { SearchResult } from './search-result.model';
import { SearchService } from './search-service/search.service'; import { SearchService } from './search-service/search.service';
import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; import { SearchSidebarService } from './search-sidebar/search-sidebar.service';
import { Subscription } from 'rxjs/Subscription';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { SearchConfigurationService } from './search-service/search-configuration.service'; import { SearchConfigurationService } from './search-service/search-configuration.service';
import { getSucceededRemoteData } from '../core/shared/operators'; import { getSucceededRemoteData } from '../core/shared/operators';
@@ -78,8 +76,8 @@ export class SearchPageComponent implements OnInit {
*/ */
ngOnInit(): void { ngOnInit(): void {
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions; this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
this.sub = this.searchOptions$ this.sub = this.searchOptions$.pipe(
.switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData())) switchMap((options) => this.service.search(options).pipe(getSucceededRemoteData())))
.subscribe((results) => { .subscribe((results) => {
this.resultsRD$.next(results); this.resultsRD$.next(results);
}); });

View File

@@ -7,5 +7,12 @@
[hideGear]="true"> [hideGear]="true">
</ds-viewable-collection></div> </ds-viewable-collection></div>
<ds-loading *ngIf="!searchResults || searchResults?.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading> <ds-loading *ngIf="!searchResults || searchResults?.isLoading" message="{{'loading.search-results' | translate}}"></ds-loading>
<ds-error *ngIf="searchResults?.hasFailed" message="{{'error.search-results' | translate}}"></ds-error> <ds-error *ngIf="searchResults?.hasFailed && (!searchResults?.error || searchResults?.error?.statusCode != 400)" message="{{'error.search-results' | translate}}"></ds-error>
<ds-error *ngIf="searchResults?.payload?.page.length == 0" message="{{'search.results.no-results' | translate}}"></ds-error> <div *ngIf="searchResults?.payload?.page.length == 0 || searchResults?.error?.statusCode == 400">
{{ 'search.results.no-results' | translate }}
<a [routerLink]="['/search']"
[queryParams]="{ query: surroundStringWithQuotes(searchConfig?.query) }"
queryParamsHandling="merge">
{{"search.results.no-results-link" | translate}}
</a>
</div>

View File

@@ -1,40 +1,92 @@
import { ComponentFixture, TestBed, async, tick, fakeAsync } from '@angular/core/testing'; import { ComponentFixture, TestBed, async, tick, fakeAsync } from '@angular/core/testing';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { NO_ERRORS_SCHEMA } from '@angular/core';
import { ResourceType } from '../../core/shared/resource-type'; import { ResourceType } from '../../core/shared/resource-type';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { SearchResultsComponent } from './search-results.component'; import { SearchResultsComponent } from './search-results.component';
import { QueryParamsDirectiveStub } from '../../shared/testing/query-params-directive-stub';
describe('SearchResultsComponent', () => { describe('SearchResultsComponent', () => {
let comp: SearchResultsComponent; let comp: SearchResultsComponent;
let fixture: ComponentFixture<SearchResultsComponent>; let fixture: ComponentFixture<SearchResultsComponent>;
let heading: DebugElement;
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot()], imports: [TranslateModule.forRoot(), NoopAnimationsModule],
declarations: [SearchResultsComponent], declarations: [
SearchResultsComponent,
QueryParamsDirectiveStub],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
})); }));
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(SearchResultsComponent); fixture = TestBed.createComponent(SearchResultsComponent);
comp = fixture.componentInstance; // SearchFormComponent test instance comp = fixture.componentInstance; // SearchResultsComponent test instance
heading = fixture.debugElement.query(By.css('heading'));
}); });
it('should display heading when results are not empty', fakeAsync(() => { it('should display results when results are not empty', () => {
(comp as any).searchResults = 'test'; (comp as any).searchResults = { hasSucceeded: true, isLoading: false, payload: { page: { length: 2 } } };
(comp as any).searchConfig = {pagination: ''}; (comp as any).searchConfig = {};
fixture.detectChanges(); fixture.detectChanges();
tick(); expect(fixture.debugElement.query(By.css('ds-viewable-collection'))).not.toBeNull();
expect(heading).toBeDefined(); });
}));
it('should not display heading when results is empty', () => { it('should not display link when results are not empty', () => {
expect(heading).toBeNull(); (comp as any).searchResults = { hasSucceeded: true, isLoading: false, payload: { page: { length: 2 } } };
(comp as any).searchConfig = {};
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('a'))).toBeNull();
});
it('should display error message if error is != 400', () => {
(comp as any).searchResults = { hasFailed: true, error: { statusCode: 500 } };
fixture.detectChanges();
expect(fixture.debugElement.query(By.css('ds-error'))).not.toBeNull();
});
it('should display link with new search where query is quoted if search return a error 400', () => {
(comp as any).searchResults = { hasFailed: true, error: { statusCode: 400 } };
(comp as any).searchConfig = { query: 'foobar' };
fixture.detectChanges();
const linkDes = fixture.debugElement.queryAll(By.directive(QueryParamsDirectiveStub));
// get attached link directive instances
// using each DebugElement's injector
const routerLinkQuery = linkDes.map((de) => de.injector.get(QueryParamsDirectiveStub));
expect(routerLinkQuery.length).toBe(1, 'should have 1 router link with query params');
expect(routerLinkQuery[0].queryParams.query).toBe('"foobar"', 'query params should be "foobar"');
});
it('should display link with new search where query is quoted if search result is empty', () => {
(comp as any).searchResults = { payload: { page: { length: 0 } } };
(comp as any).searchConfig = { query: 'foobar' };
fixture.detectChanges();
const linkDes = fixture.debugElement.queryAll(By.directive(QueryParamsDirectiveStub));
// get attached link directive instances
// using each DebugElement's injector
const routerLinkQuery = linkDes.map((de) => de.injector.get(QueryParamsDirectiveStub));
expect(routerLinkQuery.length).toBe(1, 'should have 1 router link with query params');
expect(routerLinkQuery[0].queryParams.query).toBe('"foobar"', 'query params should be "foobar"');
});
it('should add quotes around the given string', () => {
expect(comp.surroundStringWithQuotes('teststring')).toEqual('"teststring"');
});
it('should not add quotes around the given string if they are already there', () => {
expect(comp.surroundStringWithQuotes('"teststring"')).toEqual('"teststring"');
});
it('should not add quotes around a given empty string', () => {
expect(comp.surroundStringWithQuotes('')).toEqual('');
}); });
}); });

View File

@@ -6,6 +6,7 @@ import { SearchOptions } from '../search-options.model';
import { SearchResult } from '../search-result.model'; import { SearchResult } from '../search-result.model';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { ViewMode } from '../../core/shared/view-mode.model'; import { ViewMode } from '../../core/shared/view-mode.model';
import { isNotEmpty } from '../../shared/empty.util';
@Component({ @Component({
selector: 'ds-search-results', selector: 'ds-search-results',
@@ -35,4 +36,16 @@ export class SearchResultsComponent {
*/ */
@Input() viewMode: ViewMode; @Input() viewMode: ViewMode;
/**
* Method to change the given string by surrounding it by quotes if not already present.
*/
surroundStringWithQuotes(input: string): string {
let result = input;
if (isNotEmpty(result) && !(result.startsWith('\"') && result.endsWith('\"'))) {
result = `"${result}"`;
}
return result;
}
} }

View File

@@ -3,8 +3,8 @@ import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { PaginatedSearchOptions } from '../paginated-search-options.model'; import { PaginatedSearchOptions } from '../paginated-search-options.model';
import { Observable } from 'rxjs/Observable';
import { SearchFilter } from '../search-filter.model'; import { SearchFilter } from '../search-filter.model';
import { of as observableOf } from 'rxjs';
describe('SearchConfigurationService', () => { describe('SearchConfigurationService', () => {
let service: SearchConfigurationService; let service: SearchConfigurationService;
@@ -24,8 +24,8 @@ describe('SearchConfigurationService', () => {
const backendFilters = [new SearchFilter('f.author', ['another value']), new SearchFilter('f.date', ['[2013 TO 2018]'])]; const backendFilters = [new SearchFilter('f.author', ['another value']), new SearchFilter('f.date', ['[2013 TO 2018]'])];
const spy = jasmine.createSpyObj('RouteService', { const spy = jasmine.createSpyObj('RouteService', {
getQueryParameterValue: Observable.of(value1), getQueryParameterValue: observableOf(value1),
getQueryParamsWithPrefix: Observable.of(prefixFilter) getQueryParamsWithPrefix: observableOf(prefixFilter)
}); });
const activatedRoute: any = new ActivatedRouteStub(); const activatedRoute: any = new ActivatedRouteStub();

View File

@@ -1,15 +1,21 @@
import {
BehaviorSubject,
combineLatest as observableCombineLatest,
merge as observableMerge,
Observable,
of as observableOf,
Subscription
} from 'rxjs';
import { filter, map } from 'rxjs/operators';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, 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-options.model'; import { SearchOptions } from '../search-options.model';
import { Observable } from 'rxjs/Observable';
import { ActivatedRoute, Params } from '@angular/router'; import { ActivatedRoute, Params } from '@angular/router';
import { PaginatedSearchOptions } from '../paginated-search-options.model'; import { PaginatedSearchOptions } from '../paginated-search-options.model';
import { Injectable, OnDestroy } from '@angular/core'; import { Injectable, OnDestroy } from '@angular/core';
import { RouteService } from '../../shared/services/route.service'; import { RouteService } from '../../shared/services/route.service';
import { hasNoValue, hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { hasNoValue, hasValue, isNotEmpty } from '../../shared/empty.util';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { BehaviorSubject } from 'rxjs/BehaviorSubject';
import { Subscription } from 'rxjs/Subscription';
import { getSucceededRemoteData } from '../../core/shared/operators'; import { getSucceededRemoteData } from '../../core/shared/operators';
import { SearchFilter } from '../search-filter.model'; import { SearchFilter } from '../search-filter.model';
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
@@ -87,27 +93,27 @@ export class SearchConfigurationService implements OnDestroy {
* @returns {Observable<string>} Emits the current scope's identifier * @returns {Observable<string>} Emits the current scope's identifier
*/ */
getCurrentScope(defaultScope: string) { getCurrentScope(defaultScope: string) {
return this.routeService.getQueryParameterValue('scope').map((scope) => { return this.routeService.getQueryParameterValue('scope').pipe(map((scope) => {
return scope || defaultScope; return scope || defaultScope;
}); }));
} }
/** /**
* @returns {Observable<string>} Emits the current query string * @returns {Observable<string>} Emits the current query string
*/ */
getCurrentQuery(defaultQuery: string) { getCurrentQuery(defaultQuery: string) {
return this.routeService.getQueryParameterValue('query').map((query) => { return this.routeService.getQueryParameterValue('query').pipe(map((query) => {
return query || defaultQuery; return query || defaultQuery;
}); }));
} }
/** /**
* @returns {Observable<number>} Emits the current DSpaceObject type as a number * @returns {Observable<number>} Emits the current DSpaceObject type as a number
*/ */
getCurrentDSOType(): Observable<DSpaceObjectType> { getCurrentDSOType(): Observable<DSpaceObjectType> {
return this.routeService.getQueryParameterValue('dsoType') return this.routeService.getQueryParameterValue('dsoType').pipe(
.filter((type) => hasValue(type) && hasValue(DSpaceObjectType[type.toUpperCase()])) filter((type) => hasValue(type) && hasValue(DSpaceObjectType[type.toUpperCase()])),
.map((type) => DSpaceObjectType[type.toUpperCase()]); map((type) => DSpaceObjectType[type.toUpperCase()]),);
} }
/** /**
@@ -116,12 +122,13 @@ export class SearchConfigurationService implements OnDestroy {
getCurrentPagination(defaultPagination: PaginationComponentOptions): Observable<PaginationComponentOptions> { getCurrentPagination(defaultPagination: PaginationComponentOptions): Observable<PaginationComponentOptions> {
const page$ = this.routeService.getQueryParameterValue('page'); const page$ = this.routeService.getQueryParameterValue('page');
const size$ = this.routeService.getQueryParameterValue('pageSize'); const size$ = this.routeService.getQueryParameterValue('pageSize');
return Observable.combineLatest(page$, size$, (page, size) => { return observableCombineLatest(page$, size$).pipe(map(([page, size]) => {
return Object.assign(new PaginationComponentOptions(), defaultPagination, { return Object.assign(new PaginationComponentOptions(), defaultPagination, {
currentPage: page || defaultPagination.currentPage, currentPage: page || defaultPagination.currentPage,
pageSize: size || defaultPagination.pageSize pageSize: size || defaultPagination.pageSize
}); });
}); })
);
} }
/** /**
@@ -130,7 +137,7 @@ export class SearchConfigurationService implements OnDestroy {
getCurrentSort(defaultSort: SortOptions): Observable<SortOptions> { getCurrentSort(defaultSort: SortOptions): Observable<SortOptions> {
const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection'); const sortDirection$ = this.routeService.getQueryParameterValue('sortDirection');
const sortField$ = this.routeService.getQueryParameterValue('sortField'); const sortField$ = this.routeService.getQueryParameterValue('sortField');
return Observable.combineLatest(sortDirection$, sortField$, (sortDirection, sortField) => { return observableCombineLatest(sortDirection$, sortField$).pipe(map(([sortDirection, sortField]) => {
// Dirty fix because sometimes the observable value is null somehow // Dirty fix because sometimes the observable value is null somehow
sortField = this.route.snapshot.queryParamMap.get('sortField'); sortField = this.route.snapshot.queryParamMap.get('sortField');
@@ -138,20 +145,21 @@ export class SearchConfigurationService implements OnDestroy {
const direction = SortDirection[sortDirection] || defaultSort.direction; const direction = SortDirection[sortDirection] || defaultSort.direction;
return new SortOptions(field, direction) return new SortOptions(field, direction)
} }
) )
);
} }
/** /**
* @returns {Observable<Params>} Emits the current active filters with their values as they are sent to the backend * @returns {Observable<Params>} Emits the current active filters with their values as they are sent to the backend
*/ */
getCurrentFilters(): Observable<SearchFilter[]> { getCurrentFilters(): Observable<SearchFilter[]> {
return this.routeService.getQueryParamsWithPrefix('f.').map((filterParams) => { return this.routeService.getQueryParamsWithPrefix('f.').pipe(map((filterParams) => {
if (isNotEmpty(filterParams)) { if (isNotEmpty(filterParams)) {
const filters = []; const filters = [];
Object.keys(filterParams).forEach((key) => { Object.keys(filterParams).forEach((key) => {
if (key.endsWith('.min') || key.endsWith('.max')) { if (key.endsWith('.min') || key.endsWith('.max')) {
const realKey = key.slice(0, -4); const realKey = key.slice(0, -4);
if (hasNoValue(filters.find((filter) => filter.key === realKey))) { if (hasNoValue(filters.find((f) => f.key === realKey))) {
const min = filterParams[realKey + '.min'] ? filterParams[realKey + '.min'][0] : '*'; const min = filterParams[realKey + '.min'] ? filterParams[realKey + '.min'][0] : '*';
const max = filterParams[realKey + '.max'] ? filterParams[realKey + '.max'][0] : '*'; const max = filterParams[realKey + '.max'] ? filterParams[realKey + '.max'][0] : '*';
filters.push(new SearchFilter(realKey, ['[' + min + ' TO ' + max + ']'])); filters.push(new SearchFilter(realKey, ['[' + min + ' TO ' + max + ']']));
@@ -163,7 +171,7 @@ export class SearchConfigurationService implements OnDestroy {
return filters; return filters;
} }
return []; return [];
}); }));
} }
/** /**
@@ -179,7 +187,7 @@ export class SearchConfigurationService implements OnDestroy {
* @returns {Subscription} The subscription to unsubscribe from * @returns {Subscription} The subscription to unsubscribe from
*/ */
subscribeToSearchOptions(defaults: SearchOptions): Subscription { subscribeToSearchOptions(defaults: SearchOptions): Subscription {
return Observable.merge( return observableMerge(
this.getScopePart(defaults.scope), this.getScopePart(defaults.scope),
this.getQueryPart(defaults.query), this.getQueryPart(defaults.query),
this.getDSOTypePart(), this.getDSOTypePart(),
@@ -197,7 +205,7 @@ export class SearchConfigurationService implements OnDestroy {
* @returns {Subscription} The subscription to unsubscribe from * @returns {Subscription} The subscription to unsubscribe from
*/ */
subscribeToPaginatedSearchOptions(defaults: PaginatedSearchOptions): Subscription { subscribeToPaginatedSearchOptions(defaults: PaginatedSearchOptions): Subscription {
return Observable.merge( return observableMerge(
this.getPaginationPart(defaults.pagination), this.getPaginationPart(defaults.pagination),
this.getSortPart(defaults.sort), this.getSortPart(defaults.sort),
this.getScopePart(defaults.scope), this.getScopePart(defaults.scope),
@@ -222,7 +230,7 @@ export class SearchConfigurationService implements OnDestroy {
scope: this.defaultScope, scope: this.defaultScope,
query: this.defaultQuery query: this.defaultQuery
}); });
this._defaults = Observable.of(new RemoteData(false, false, true, null, options)); this._defaults = observableOf(new RemoteData(false, false, true, null, options));
} }
return this._defaults; return this._defaults;
} }
@@ -240,53 +248,53 @@ export class SearchConfigurationService implements OnDestroy {
* @returns {Observable<string>} Emits the current scope's identifier * @returns {Observable<string>} Emits the current scope's identifier
*/ */
private getScopePart(defaultScope: string): Observable<any> { private getScopePart(defaultScope: string): Observable<any> {
return this.getCurrentScope(defaultScope).map((scope) => { return this.getCurrentScope(defaultScope).pipe(map((scope) => {
return { scope } return { scope }
}); }));
} }
/** /**
* @returns {Observable<string>} Emits the current query string as a partial SearchOptions object * @returns {Observable<string>} Emits the current query string as a partial SearchOptions object
*/ */
private getQueryPart(defaultQuery: string): Observable<any> { private getQueryPart(defaultQuery: string): Observable<any> {
return this.getCurrentQuery(defaultQuery).map((query) => { return this.getCurrentQuery(defaultQuery).pipe(map((query) => {
return { query } return { query }
}); }));
} }
/** /**
* @returns {Observable<string>} Emits the current query string as a partial SearchOptions object * @returns {Observable<string>} Emits the current query string as a partial SearchOptions object
*/ */
private getDSOTypePart(): Observable<any> { private getDSOTypePart(): Observable<any> {
return this.getCurrentDSOType().map((dsoType) => { return this.getCurrentDSOType().pipe(map((dsoType) => {
return { dsoType } return { dsoType }
}); }));
} }
/** /**
* @returns {Observable<string>} Emits the current pagination settings as a partial SearchOptions object * @returns {Observable<string>} Emits the current pagination settings as a partial SearchOptions object
*/ */
private getPaginationPart(defaultPagination: PaginationComponentOptions): Observable<any> { private getPaginationPart(defaultPagination: PaginationComponentOptions): Observable<any> {
return this.getCurrentPagination(defaultPagination).map((pagination) => { return this.getCurrentPagination(defaultPagination).pipe(map((pagination) => {
return { pagination } return { pagination }
}); }));
} }
/** /**
* @returns {Observable<string>} Emits the current sorting settings as a partial SearchOptions object * @returns {Observable<string>} Emits the current sorting settings as a partial SearchOptions object
*/ */
private getSortPart(defaultSort: SortOptions): Observable<any> { private getSortPart(defaultSort: SortOptions): Observable<any> {
return this.getCurrentSort(defaultSort).map((sort) => { return this.getCurrentSort(defaultSort).pipe(map((sort) => {
return { sort } return { sort }
}); }));
} }
/** /**
* @returns {Observable<Params>} Emits the current active filters as a partial SearchOptions object * @returns {Observable<Params>} Emits the current active filters as a partial SearchOptions object
*/ */
private getFiltersPart(): Observable<any> { private getFiltersPart(): Observable<any> {
return this.getCurrentFilters().map((filters) => { return this.getCurrentFilters().pipe(map((filters) => {
return { filters } return { filters }
}); }));
} }
} }

View File

@@ -12,9 +12,7 @@ import { ResponseCacheService } from '../../core/cache/response-cache.service';
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
import { RouterStub } from '../../shared/testing/router-stub'; import { RouterStub } from '../../shared/testing/router-stub';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service'; import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { Observable } from 'rxjs/Observable'; import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
import 'rxjs/add/observable/of';
import 'rxjs/add/observable/combineLatest';
import { PaginatedSearchOptions } from '../paginated-search-options.model'; import { PaginatedSearchOptions } from '../paginated-search-options.model';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer'; import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
@@ -30,6 +28,8 @@ import { SearchFilterConfig } from './search-filter-config.model';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { ViewMode } from '../../core/shared/view-mode.model'; import { ViewMode } from '../../core/shared/view-mode.model';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({ template: '' }) @Component({ template: '' })
class DummyComponent { class DummyComponent {
@@ -58,8 +58,8 @@ describe('SearchService', () => {
{ provide: RequestService, useValue: getMockRequestService() }, { provide: RequestService, useValue: getMockRequestService() },
{ provide: RemoteDataBuildService, useValue: {} }, { provide: RemoteDataBuildService, useValue: {} },
{ provide: HALEndpointService, useValue: {} }, { provide: HALEndpointService, useValue: {} },
{ provide: CommunityDataService, useValue: {}}, { provide: CommunityDataService, useValue: {} },
{ provide: DSpaceObjectDataService, useValue: {}}, { provide: DSpaceObjectDataService, useValue: {} },
SearchService SearchService
], ],
}); });
@@ -87,13 +87,15 @@ describe('SearchService', () => {
const remoteDataBuildService = { const remoteDataBuildService = {
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<any>) => { toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<any>) => {
return Observable.combineLatest(requestEntryObs, return observableCombineLatest(requestEntryObs,
responseCacheObs, payloadObs, (req, res, pay) => { responseCacheObs, payloadObs).pipe(
map(([req, res, pay]) => {
return { req, res, pay }; return { req, res, pay };
}); })
);
}, },
aggregate: (input: Array<Observable<RemoteData<any>>>): Observable<RemoteData<any[]>> => { aggregate: (input: Array<Observable<RemoteData<any>>>): Observable<RemoteData<any[]>> => {
return Observable.of(new RemoteData(false, false, true, null, [])); return observableOf(new RemoteData(false, false, true, null, []));
} }
}; };
@@ -115,8 +117,8 @@ describe('SearchService', () => {
{ provide: RequestService, useValue: getMockRequestService() }, { provide: RequestService, useValue: getMockRequestService() },
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService }, { provide: RemoteDataBuildService, useValue: remoteDataBuildService },
{ provide: HALEndpointService, useValue: halService }, { provide: HALEndpointService, useValue: halService },
{ provide: CommunityDataService, useValue: {}}, { provide: CommunityDataService, useValue: {} },
{ provide: DSpaceObjectDataService, useValue: {}}, { provide: DSpaceObjectDataService, useValue: {} },
SearchService SearchService
], ],
}); });
@@ -162,8 +164,8 @@ describe('SearchService', () => {
const response = new SearchSuccessResponse(queryResponse, 200, 'OK'); const response = new SearchSuccessResponse(queryResponse, 200, 'OK');
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
beforeEach(() => { beforeEach(() => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint)); spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
(searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry)); (searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry));
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
searchService.search(searchOptions).subscribe((t) => { searchService.search(searchOptions).subscribe((t) => {
}); // subscribe to make sure all methods are called }); // subscribe to make sure all methods are called
@@ -192,8 +194,8 @@ describe('SearchService', () => {
const response = new FacetConfigSuccessResponse(filterConfig, 200, 'OK'); const response = new FacetConfigSuccessResponse(filterConfig, 200, 'OK');
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
beforeEach(() => { beforeEach(() => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint)); spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
(searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry)); (searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry));
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
searchService.getConfig(null).subscribe((t) => { searchService.getConfig(null).subscribe((t) => {
}); // subscribe to make sure all methods are called }); // subscribe to make sure all methods are called
@@ -224,8 +226,8 @@ describe('SearchService', () => {
const response = new FacetConfigSuccessResponse(filterConfig, 200, 'OK'); const response = new FacetConfigSuccessResponse(filterConfig, 200, 'OK');
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
beforeEach(() => { beforeEach(() => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint)); spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
(searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry)); (searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry));
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
searchService.getConfig(scope).subscribe((t) => { searchService.getConfig(scope).subscribe((t) => {
}); // subscribe to make sure all methods are called }); // subscribe to make sure all methods are called

View File

@@ -1,3 +1,4 @@
import { of as observableOf, combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { Injectable, OnDestroy } from '@angular/core'; import { Injectable, OnDestroy } from '@angular/core';
import { import {
ActivatedRoute, ActivatedRoute,
@@ -6,7 +7,6 @@ import {
Router, Router,
UrlSegmentGroup UrlSegmentGroup
} from '@angular/router'; } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { flatMap, map, switchMap } from 'rxjs/operators'; import { flatMap, map, switchMap } from 'rxjs/operators';
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
import { import {
@@ -122,30 +122,33 @@ export class SearchService implements OnDestroy {
); );
// Create search results again with the correct dso objects linked to each result // Create search results again with the correct dso objects linked to each result
const tDomainListObs = Observable.combineLatest(sqrObs, dsoObs, (sqr: SearchQueryResponse, dsos: RemoteData<DSpaceObject[]>) => { const tDomainListObs = observableCombineLatest(sqrObs, dsoObs).pipe(
map(([sqr, dsos]) => {
return sqr.objects.map((object: NormalizedSearchResult, index: number) => { return sqr.objects.map((object: NormalizedSearchResult, index: number) => {
let co = DSpaceObject; let co = DSpaceObject;
if (dsos.payload[index]) { if (dsos.payload[index]) {
const constructor: GenericConstructor<ListableObject> = dsos.payload[index].constructor as GenericConstructor<ListableObject>; const constructor: GenericConstructor<ListableObject> = dsos.payload[index].constructor as GenericConstructor<ListableObject>;
co = getSearchResultFor(constructor); co = getSearchResultFor(constructor);
return Object.assign(new co(), object, { return Object.assign(new co(), object, {
dspaceObject: dsos.payload[index] dspaceObject: dsos.payload[index]
}); });
} else { } else {
return undefined; return undefined;
} }
}); });
}); })
);
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe( const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
map((entry: ResponseCacheEntry) => entry.response), map((entry: ResponseCacheEntry) => entry.response),
map((response: FacetValueSuccessResponse) => response.pageInfo) map((response: FacetValueSuccessResponse) => response.pageInfo)
); );
const payloadObs = Observable.combineLatest(tDomainListObs, pageInfoObs, (tDomainList, pageInfo) => { const payloadObs = observableCombineLatest(tDomainListObs, pageInfoObs).pipe(
return new PaginatedList(pageInfo, tDomainList); map(([tDomainList, pageInfo]) => {
}); return new PaginatedList(pageInfo, tDomainList);
})
);
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
} }
@@ -244,9 +247,11 @@ export class SearchService implements OnDestroy {
map((response: FacetValueSuccessResponse) => response.pageInfo) map((response: FacetValueSuccessResponse) => response.pageInfo)
); );
const payloadObs = Observable.combineLatest(facetValueObs, pageInfoObs, (facetValue, pageInfo) => { const payloadObs = observableCombineLatest(facetValueObs, pageInfoObs).pipe(
return new PaginatedList(pageInfo, facetValue); map(([facetValue, pageInfo]) => {
}); return new PaginatedList(pageInfo, facetValue);
})
);
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
} }
@@ -272,12 +277,14 @@ export class SearchService implements OnDestroy {
switchMap((dsoRD: RemoteData<DSpaceObject>) => { switchMap((dsoRD: RemoteData<DSpaceObject>) => {
if (dsoRD.payload.type === ResourceType.Community) { if (dsoRD.payload.type === ResourceType.Community) {
const community: Community = dsoRD.payload as Community; const community: Community = dsoRD.payload as Community;
return Observable.combineLatest(community.subcommunities, community.collections, (subCommunities, collections) => { return observableCombineLatest(community.subcommunities, community.collections).pipe(
/*if this is a community, we also need to show the direct children*/ map(([subCommunities, collections]) => {
return [community, ...subCommunities.payload.page, ...collections.payload.page] /*if this is a community, we also need to show the direct children*/
}) return [community, ...subCommunities.payload.page, ...collections.payload.page]
})
);
} else { } else {
return Observable.of([dsoRD.payload]); return observableOf([dsoRD.payload]);
} }
} }
)); ));
@@ -291,13 +298,13 @@ export class SearchService implements OnDestroy {
* @returns {Observable<ViewMode>} The current view mode * @returns {Observable<ViewMode>} The current view mode
*/ */
getViewMode(): Observable<ViewMode> { getViewMode(): Observable<ViewMode> {
return this.route.queryParams.map((params) => { return this.route.queryParams.pipe(map((params) => {
if (isNotEmpty(params.view) && hasValue(params.view)) { if (isNotEmpty(params.view) && hasValue(params.view)) {
return params.view; return params.view;
} else { } else {
return ViewMode.List; return ViewMode.List;
} }
}); }));
} }
/** /**

View File

@@ -1,7 +1,7 @@
import { SearchService } from '../search-service/search.service'; import { SearchService } from '../search-service/search.service';
import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { SearchSettingsComponent } from './search-settings.component'; import { SearchSettingsComponent } from './search-settings.component';
import { Observable } from 'rxjs/Observable'; import { of as observableOf } from 'rxjs';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
@@ -15,6 +15,7 @@ import { SearchFilterService } from '../search-filters/search-filter/search-filt
import { hot } from 'jasmine-marbles'; import { hot } from 'jasmine-marbles';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { SearchConfigurationService } from '../search-service/search-configuration.service'; import { SearchConfigurationService } from '../search-service/search-configuration.service';
import { first } from 'rxjs/operators';
describe('SearchSettingsComponent', () => { describe('SearchSettingsComponent', () => {
@@ -43,16 +44,16 @@ describe('SearchSettingsComponent', () => {
}; };
const activatedRouteStub = { const activatedRouteStub = {
queryParams: Observable.of({ queryParams: observableOf({
query: queryParam, query: queryParam,
scope: scopeParam scope: scopeParam
}) })
}; };
const sidebarService = { const sidebarService = {
isCollapsed: Observable.of(true), isCollapsed: observableOf(true),
collapse: () => this.isCollapsed = Observable.of(true), collapse: () => this.isCollapsed = observableOf(true),
expand: () => this.isCollapsed = Observable.of(false) expand: () => this.isCollapsed = observableOf(false)
}; };
beforeEach(async(() => { beforeEach(async(() => {
@@ -101,7 +102,7 @@ describe('SearchSettingsComponent', () => {
}); });
it('it should show the order settings with the respective selectable options', () => { it('it should show the order settings with the respective selectable options', () => {
(comp as any).searchOptions$.first().subscribe((options) => { (comp as any).searchOptions$.pipe(first()).subscribe((options) => {
fixture.detectChanges(); fixture.detectChanges();
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings')); const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
expect(orderSetting).toBeDefined(); expect(orderSetting).toBeDefined();
@@ -111,7 +112,7 @@ describe('SearchSettingsComponent', () => {
}); });
it('it should show the size settings with the respective selectable options', () => { it('it should show the size settings with the respective selectable options', () => {
(comp as any).searchOptions$.first().subscribe((options) => { (comp as any).searchOptions$.pipe(first()).subscribe((options) => {
fixture.detectChanges(); fixture.detectChanges();
const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings')); const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
expect(pageSizeSetting).toBeDefined(); expect(pageSizeSetting).toBeDefined();
@@ -122,7 +123,7 @@ describe('SearchSettingsComponent', () => {
}); });
it('should have the proper order value selected by default', () => { it('should have the proper order value selected by default', () => {
(comp as any).searchOptions$.first().subscribe((options) => { (comp as any).searchOptions$.pipe(first()).subscribe((options) => {
fixture.detectChanges(); fixture.detectChanges();
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings')); const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
const childElementToBeSelected = orderSetting.query(By.css('.form-control option[value="0"][selected="selected"]')); const childElementToBeSelected = orderSetting.query(By.css('.form-control option[value="0"][selected="selected"]'));
@@ -131,7 +132,7 @@ describe('SearchSettingsComponent', () => {
}); });
it('should have the proper rpp value selected by default', () => { it('should have the proper rpp value selected by default', () => {
(comp as any).searchOptions$.first().subscribe((options) => { (comp as any).searchOptions$.pipe(first()).subscribe((options) => {
fixture.detectChanges(); fixture.detectChanges();
const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings')); const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
const childElementToBeSelected = pageSizeSetting.query(By.css('.form-control option[value="10"][selected="selected"]')); const childElementToBeSelected = pageSizeSetting.query(By.css('.form-control option[value="10"][selected="selected"]'));

View File

@@ -3,8 +3,7 @@ import { SearchService } from '../search-service/search.service';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { ActivatedRoute, NavigationExtras, Router } from '@angular/router'; import { ActivatedRoute, NavigationExtras, Router } from '@angular/router';
import { PaginatedSearchOptions } from '../paginated-search-options.model'; import { PaginatedSearchOptions } from '../paginated-search-options.model';
import { SearchFilterService } from '../search-filters/search-filter/search-filter.service'; import { Observable } from 'rxjs';
import { Observable } from 'rxjs/Observable';
import { SearchConfigurationService } from '../search-service/search-configuration.service'; import { SearchConfigurationService } from '../search-service/search-configuration.service';
@Component({ @Component({

View File

@@ -1,5 +1,5 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { provideMockActions } from '@ngrx/effects/testing'; import { provideMockActions } from '@ngrx/effects/testing';
import { cold, hot } from 'jasmine-marbles'; import { cold, hot } from 'jasmine-marbles';
import * as fromRouter from '@ngrx/router-store'; import * as fromRouter from '@ngrx/router-store';

View File

@@ -1,5 +1,6 @@
import { map, tap, filter } from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Effect, Actions } from '@ngrx/effects' import { Effect, Actions, ofType } from '@ngrx/effects'
import * as fromRouter from '@ngrx/router-store'; import * as fromRouter from '@ngrx/router-store';
import { SearchSidebarCollapseAction } from './search-sidebar.actions'; import { SearchSidebarCollapseAction } from './search-sidebar.actions';
@@ -12,10 +13,14 @@ import { URLBaser } from '../../core/url-baser/url-baser';
export class SearchSidebarEffects { export class SearchSidebarEffects {
private previousPath: string; private previousPath: string;
@Effect() routeChange$ = this.actions$ @Effect() routeChange$ = this.actions$
.ofType(fromRouter.ROUTER_NAVIGATION) .pipe(
.filter((action) => this.previousPath !== this.getBaseUrl(action)) ofType(fromRouter.ROUTER_NAVIGATION),
.do((action) => {this.previousPath = this.getBaseUrl(action)}) filter((action) => this.previousPath !== this.getBaseUrl(action)),
.map(() => new SearchSidebarCollapseAction()); tap((action) => {
this.previousPath = this.getBaseUrl(action)
}),
map(() => new SearchSidebarCollapseAction())
);
constructor(private actions$: Actions) { constructor(private actions$: Actions) {

View File

@@ -1,9 +1,8 @@
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { SearchSidebarService } from './search-sidebar.service'; import { SearchSidebarService } from './search-sidebar.service';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { async, inject, TestBed } from '@angular/core/testing'; import { async, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable'; import { of as observableOf } from 'rxjs';
import 'rxjs/add/observable/of';
import { SearchSidebarCollapseAction, SearchSidebarExpandAction } from './search-sidebar.actions'; import { SearchSidebarCollapseAction, SearchSidebarExpandAction } from './search-sidebar.actions';
import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowService } from '../../shared/host-window.service';
@@ -13,13 +12,13 @@ describe('SearchSidebarService', () => {
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
dispatch: {}, dispatch: {},
/* tslint:enable:no-empty */ /* tslint:enable:no-empty */
select: Observable.of(true) pipe: observableOf(true)
}); });
const windowService = jasmine.createSpyObj('hostWindowService', const windowService = jasmine.createSpyObj('hostWindowService',
{ {
isXs: Observable.of(true), isXs: observableOf(true),
isSm: Observable.of(false), isSm: observableOf(false),
isXsOrSm: Observable.of(true) isXsOrSm: observableOf(true)
}); });
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({

View File

@@ -1,10 +1,11 @@
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { SearchSidebarState } from './search-sidebar.reducer'; import { SearchSidebarState } from './search-sidebar.reducer';
import { createSelector, Store } from '@ngrx/store'; import { createSelector, select, Store } from '@ngrx/store';
import { SearchSidebarCollapseAction, SearchSidebarExpandAction } from './search-sidebar.actions'; import { SearchSidebarCollapseAction, SearchSidebarExpandAction } from './search-sidebar.actions';
import { Observable } from 'rxjs/Observable';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowService } from '../../shared/host-window.service';
import { map } from 'rxjs/operators';
const sidebarStateSelector = (state: AppState) => state.searchSidebar; const sidebarStateSelector = (state: AppState) => state.searchSidebar;
const sidebarCollapsedSelector = createSelector(sidebarStateSelector, (sidebar: SearchSidebarState) => sidebar.sidebarCollapsed); const sidebarCollapsedSelector = createSelector(sidebarStateSelector, (sidebar: SearchSidebarState) => sidebar.sidebarCollapsed);
@@ -26,7 +27,7 @@ export class SearchSidebarService {
constructor(private store: Store<AppState>, private windowService: HostWindowService) { constructor(private store: Store<AppState>, private windowService: HostWindowService) {
this.isXsOrSm$ = this.windowService.isXsOrSm(); this.isXsOrSm$ = this.windowService.isXsOrSm();
this.isCollapsedInStore = this.store.select(sidebarCollapsedSelector); this.isCollapsedInStore = this.store.pipe(select(sidebarCollapsedSelector));
} }
/** /**
@@ -34,10 +35,12 @@ export class SearchSidebarService {
* @returns {Observable<boolean>} Emits true if the user's screen size is mobile or when the state in the store is currently collapsed * @returns {Observable<boolean>} Emits true if the user's screen size is mobile or when the state in the store is currently collapsed
*/ */
get isCollapsed(): Observable<boolean> { get isCollapsed(): Observable<boolean> {
return Observable.combineLatest( return observableCombineLatest(
this.isXsOrSm$, this.isXsOrSm$,
this.isCollapsedInStore, this.isCollapsedInStore
(mobile, store) => mobile ? store : true); ).pipe(
map(([mobile, store]) => mobile ? store : true)
);
} }
/** /**

View File

@@ -97,7 +97,7 @@ describe('App component', () => {
let store: Store<HostWindowState>; let store: Store<HostWindowState>;
beforeEach(() => { beforeEach(() => {
store = fixture.debugElement.injector.get(Store); store = fixture.debugElement.injector.get(Store) as Store<HostWindowState>;
spyOn(store, 'dispatch'); spyOn(store, 'dispatch');
window.dispatchEvent(new Event('resize')); window.dispatchEvent(new Event('resize'));

View File

@@ -1,3 +1,4 @@
import { filter, first, take } from 'rxjs/operators';
import { import {
AfterViewInit, AfterViewInit,
ChangeDetectionStrategy, ChangeDetectionStrategy,
@@ -9,7 +10,7 @@ import {
} from '@angular/core'; } from '@angular/core';
import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router'; import { NavigationCancel, NavigationEnd, NavigationStart, Router } from '@angular/router';
import { Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
@@ -66,10 +67,10 @@ export class AppComponent implements OnInit, AfterViewInit {
this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight); this.dispatchWindowSize(this._window.nativeWindow.innerWidth, this._window.nativeWindow.innerHeight);
// Whether is not authenticathed try to retrieve a possible stored auth token // Whether is not authenticathed try to retrieve a possible stored auth token
this.store.select(isAuthenticated) this.store.pipe(select(isAuthenticated),
.take(1) first(),
.filter((authenticated) => !authenticated) filter((authenticated) => !authenticated)
.subscribe((authenticated) => this.authService.checkAuthenticationToken()); ).subscribe((authenticated) => this.authService.checkAuthenticationToken());
} }

View File

@@ -46,55 +46,73 @@ export function getMetaReducers(config: GlobalConfig): Array<MetaReducer<AppStat
return config.debug ? [...metaReducers, ...debugMetaReducers] : metaReducers; return config.debug ? [...metaReducers, ...debugMetaReducers] : metaReducers;
} }
const DEV_MODULES: any[] = []; const IMPORTS = [
CommonModule,
SharedModule,
HttpClientModule,
AppRoutingModule,
CoreModule.forRoot(),
ScrollToModule.forRoot(),
NgbModule.forRoot(),
TranslateModule.forRoot(),
EffectsModule.forRoot(appEffects),
StoreModule.forRoot(appReducers),
StoreRouterConnectingModule,
];
if (!ENV_CONFIG.production) { IMPORTS.push(
DEV_MODULES.push(StoreDevtoolsModule.instrument({ maxAge: 500 })); StoreDevtoolsModule.instrument({
} maxAge: 100,
logOnly: ENV_CONFIG.production,
})
);
const PROVIDERS = [
{
provide: GLOBAL_CONFIG,
useFactory: (getConfig)
},
{
provide: APP_BASE_HREF,
useFactory: (getBase)
},
{
provide: META_REDUCERS,
useFactory: getMetaReducers,
deps: [GLOBAL_CONFIG]
},
{
provide: RouterStateSerializer,
useClass: DSpaceRouterStateSerializer
}
];
const DECLARATIONS = [
AppComponent,
HeaderComponent,
FooterComponent,
PageNotFoundComponent,
NotificationComponent,
NotificationsBoardComponent
];
const EXPORTS = [
AppComponent
];
@NgModule({ @NgModule({
imports: [ imports: [
CommonModule, ...IMPORTS
SharedModule,
HttpClientModule,
AppRoutingModule,
CoreModule.forRoot(),
ScrollToModule.forRoot(),
NgbModule.forRoot(),
TranslateModule.forRoot(),
EffectsModule.forRoot(appEffects),
StoreModule.forRoot(appReducers),
StoreRouterConnectingModule,
...DEV_MODULES
], ],
providers: [ providers: [
{ ...PROVIDERS
provide: GLOBAL_CONFIG,
useFactory: (getConfig)
},
{
provide: APP_BASE_HREF,
useFactory: (getBase)
},
{
provide: META_REDUCERS,
useFactory: getMetaReducers,
deps: [GLOBAL_CONFIG]
},
{
provide: RouterStateSerializer,
useClass: DSpaceRouterStateSerializer
}
], ],
declarations: [ declarations: [
AppComponent, ...DECLARATIONS
HeaderComponent,
FooterComponent,
PageNotFoundComponent,
NotificationComponent,
NotificationsBoardComponent
], ],
exports: [AppComponent] exports: [
...EXPORTS
]
}) })
export class AppModule { export class AppModule {

View File

@@ -1,14 +1,15 @@
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
import { Inject, Injectable } from '@angular/core'; import { Inject, Injectable } from '@angular/core';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { GLOBAL_CONFIG } from '../../../config'; import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';
import { Observable } from 'rxjs/Observable';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
import { AuthGetRequest, AuthPostRequest, PostRequest, RestRequest } from '../data/request.models'; import { AuthGetRequest, AuthPostRequest, PostRequest, RestRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { AuthStatusResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; import { AuthStatusResponse, ErrorResponse } from '../cache/response-cache.models';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
@Injectable() @Injectable()
@@ -23,18 +24,18 @@ export class AuthRequestService {
} }
protected fetchRequest(request: RestRequest): Observable<any> { protected fetchRequest(request: RestRequest): Observable<any> {
const [successResponse, errorResponse] = this.responseCache.get(request.href) return this.responseCache.get(request.href).pipe(
.map((entry: ResponseCacheEntry) => entry.response) map((entry: ResponseCacheEntry) => entry.response),
// TODO to review when https://github.com/DSpace/dspace-angular/issues/217 will be fixed // TODO to review when https://github.com/DSpace/dspace-angular/issues/217 will be fixed
.do(() => this.responseCache.remove(request.href)) tap(() => this.responseCache.remove(request.href)),
.partition((response: RestResponse) => response.isSuccessful); mergeMap((response) => {
return Observable.merge( if (response.isSuccessful && isNotEmpty(response)) {
errorResponse.flatMap((response: ErrorResponse) => return observableOf((response as AuthStatusResponse).response);
Observable.throw(new Error(response.errorMessage))), } else if (!response.isSuccessful) {
successResponse return observableThrowError(new Error((response as ErrorResponse).errorMessage));
.filter((response: AuthStatusResponse) => isNotEmpty(response)) }
.map((response: AuthStatusResponse) => response.response) })
.distinctUntilChanged()); );
} }
protected getEndpointByMethod(endpoint: string, method: string): string { protected getEndpointByMethod(endpoint: string, method: string): string {
@@ -42,24 +43,24 @@ export class AuthRequestService {
} }
public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<any> { public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable<any> {
return this.halService.getEndpoint(this.linkName) return this.halService.getEndpoint(this.linkName).pipe(
.filter((href: string) => isNotEmpty(href)) filter((href: string) => isNotEmpty(href)),
.map((endpointURL) => this.getEndpointByMethod(endpointURL, method)) map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
.distinctUntilChanged() distinctUntilChanged(),
.map((endpointURL: string) => new AuthPostRequest(this.requestService.generateRequestId(), endpointURL, body, options)) map((endpointURL: string) => new AuthPostRequest(this.requestService.generateRequestId(), endpointURL, body, options)),
.do((request: PostRequest) => this.requestService.configure(request, true)) tap((request: PostRequest) => this.requestService.configure(request, true)),
.flatMap((request: PostRequest) => this.fetchRequest(request)) mergeMap((request: PostRequest) => this.fetchRequest(request)),
.distinctUntilChanged(); distinctUntilChanged());
} }
public getRequest(method: string, options?: HttpOptions): Observable<any> { public getRequest(method: string, options?: HttpOptions): Observable<any> {
return this.halService.getEndpoint(this.linkName) return this.halService.getEndpoint(this.linkName).pipe(
.filter((href: string) => isNotEmpty(href)) filter((href: string) => isNotEmpty(href)),
.map((endpointURL) => this.getEndpointByMethod(endpointURL, method)) map((endpointURL) => this.getEndpointByMethod(endpointURL, method)),
.distinctUntilChanged() distinctUntilChanged(),
.map((endpointURL: string) => new AuthGetRequest(this.requestService.generateRequestId(), endpointURL, options)) map((endpointURL: string) => new AuthGetRequest(this.requestService.generateRequestId(), endpointURL, options)),
.do((request: PostRequest) => this.requestService.configure(request, true)) tap((request: PostRequest) => this.requestService.configure(request, true)),
.flatMap((request: PostRequest) => this.fetchRequest(request)) mergeMap((request: PostRequest) => this.fetchRequest(request)),
.distinctUntilChanged(); distinctUntilChanged());
} }
} }

View File

@@ -2,20 +2,18 @@ import { AuthStatusResponse } from '../cache/response-cache.models';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface'; import { GlobalConfig } from '../../../config/global-config.interface';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { AuthStatus } from './models/auth-status.model'; import { AuthStatus } from './models/auth-status.model';
import { AuthResponseParsingService } from './auth-response-parsing.service'; import { AuthResponseParsingService } from './auth-response-parsing.service';
import { AuthGetRequest, AuthPostRequest } from '../data/request.models'; import { AuthGetRequest, AuthPostRequest } from '../data/request.models';
import { getMockStore } from '../../shared/mocks/mock-store'; import { MockStore } from '../../shared/testing/mock-store';
import { ObjectCacheState } from '../cache/object-cache.reducer';
describe('AuthResponseParsingService', () => { describe('AuthResponseParsingService', () => {
let service: AuthResponseParsingService; let service: AuthResponseParsingService;
const EnvConfig = {cache: {msToLive: 1000}} as GlobalConfig; const EnvConfig = { cache: { msToLive: 1000 } } as GlobalConfig;
const store = getMockStore() as Store<CoreState>; const store = new MockStore<ObjectCacheState>({});
const objectCacheService = new ObjectCacheService(store); const objectCacheService = new ObjectCacheService(store as any);
beforeEach(() => { beforeEach(() => {
service = new AuthResponseParsingService(EnvConfig, objectCacheService); service = new AuthResponseParsingService(EnvConfig, objectCacheService);

View File

@@ -4,8 +4,7 @@ import { provideMockActions } from '@ngrx/effects/testing';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { cold, hot } from 'jasmine-marbles'; import { cold, hot } from 'jasmine-marbles';
import { Observable } from 'rxjs/Observable'; import { Observable, of as observableOf, throwError as observableThrow } from 'rxjs';
import 'rxjs/add/observable/of'
import { AuthEffects } from './auth.effects'; import { AuthEffects } from './auth.effects';
import { import {
@@ -30,16 +29,21 @@ import { EPersonMock } from '../../shared/testing/eperson-mock';
describe('AuthEffects', () => { describe('AuthEffects', () => {
let authEffects: AuthEffects; let authEffects: AuthEffects;
let actions: Observable<any>; let actions: Observable<any>;
const authServiceStub = new AuthServiceStub(); let authServiceStub;
const store: Store<TruncatablesState> = jasmine.createSpyObj('store', { const store: Store<TruncatablesState> = jasmine.createSpyObj('store', {
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
dispatch: {}, dispatch: {},
/* tslint:enable:no-empty */ /* tslint:enable:no-empty */
select: Observable.of(true) select: observableOf(true)
}); });
const token = authServiceStub.getToken(); let token;
function init() {
authServiceStub = new AuthServiceStub();
token = authServiceStub.getToken();
}
beforeEach(() => { beforeEach(() => {
init();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
AuthEffects, AuthEffects,
@@ -71,7 +75,7 @@ describe('AuthEffects', () => {
describe('when credentials are wrong', () => { describe('when credentials are wrong', () => {
it('should return a AUTHENTICATE_ERROR action in response to a AUTHENTICATE action', () => { it('should return a AUTHENTICATE_ERROR action in response to a AUTHENTICATE action', () => {
spyOn((authEffects as any).authService, 'authenticate').and.returnValue(Observable.throw(new Error('Message Error test'))); spyOn((authEffects as any).authService, 'authenticate').and.returnValue(observableThrow(new Error('Message Error test')));
actions = hot('--a-', { actions = hot('--a-', {
a: { a: {
@@ -112,7 +116,7 @@ describe('AuthEffects', () => {
describe('when token is not valid', () => { describe('when token is not valid', () => {
it('should return a AUTHENTICATED_ERROR action in response to a AUTHENTICATED action', () => { it('should return a AUTHENTICATED_ERROR action in response to a AUTHENTICATED action', () => {
spyOn((authEffects as any).authService, 'authenticatedUser').and.returnValue(Observable.throw(new Error('Message Error test'))); spyOn((authEffects as any).authService, 'authenticatedUser').and.returnValue(observableThrow(new Error('Message Error test')));
actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}}); actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}});
@@ -138,7 +142,7 @@ describe('AuthEffects', () => {
describe('when check token failed', () => { describe('when check token failed', () => {
it('should return a CHECK_AUTHENTICATION_TOKEN_ERROR action in response to a CHECK_AUTHENTICATION_TOKEN action', () => { it('should return a CHECK_AUTHENTICATION_TOKEN_ERROR action in response to a CHECK_AUTHENTICATION_TOKEN action', () => {
spyOn((authEffects as any).authService, 'hasValidAuthenticationToken').and.returnValue(Observable.throw('')); spyOn((authEffects as any).authService, 'hasValidAuthenticationToken').and.returnValue(observableThrow(''));
actions = hot('--a-', {a: {type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN, payload: token}}); actions = hot('--a-', {a: {type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN, payload: token}});
@@ -164,7 +168,7 @@ describe('AuthEffects', () => {
describe('when refresh token failed', () => { describe('when refresh token failed', () => {
it('should return a REFRESH_TOKEN_ERROR action in response to a REFRESH_TOKEN action', () => { it('should return a REFRESH_TOKEN_ERROR action in response to a REFRESH_TOKEN action', () => {
spyOn((authEffects as any).authService, 'refreshAuthenticationToken').and.returnValue(Observable.throw('')); spyOn((authEffects as any).authService, 'refreshAuthenticationToken').and.returnValue(observableThrow(''));
actions = hot('--a-', {a: {type: AuthActionTypes.REFRESH_TOKEN, payload: token}}); actions = hot('--a-', {a: {type: AuthActionTypes.REFRESH_TOKEN, payload: token}});
@@ -190,7 +194,7 @@ describe('AuthEffects', () => {
describe('when refresh token failed', () => { describe('when refresh token failed', () => {
it('should return a REFRESH_TOKEN_ERROR action in response to a LOG_OUT action', () => { it('should return a REFRESH_TOKEN_ERROR action in response to a LOG_OUT action', () => {
spyOn((authEffects as any).authService, 'logout').and.returnValue(Observable.throw(new Error('Message Error test'))); spyOn((authEffects as any).authService, 'logout').and.returnValue(observableThrow(new Error('Message Error test')));
actions = hot('--a-', {a: {type: AuthActionTypes.LOG_OUT, payload: token}}); actions = hot('--a-', {a: {type: AuthActionTypes.LOG_OUT, payload: token}});

View File

@@ -1,11 +1,11 @@
import { of as observableOf, Observable } from 'rxjs';
import { filter, debounceTime, switchMap, take, tap, catchError, map, first } from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
// import @ngrx // import @ngrx
import { Actions, Effect } from '@ngrx/effects'; import { Actions, Effect, ofType } from '@ngrx/effects';
import { Action, Store } from '@ngrx/store'; import { Action, select, Store } from '@ngrx/store';
// import rxjs
import { Observable } from 'rxjs/Observable';
// import services // import services
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@@ -43,112 +43,131 @@ export class AuthEffects {
* @method authenticate * @method authenticate
*/ */
@Effect() @Effect()
public authenticate$: Observable<Action> = this.actions$ public authenticate$: Observable<Action> = this.actions$.pipe(
.ofType(AuthActionTypes.AUTHENTICATE) ofType(AuthActionTypes.AUTHENTICATE),
.switchMap((action: AuthenticateAction) => { switchMap((action: AuthenticateAction) => {
return this.authService.authenticate(action.payload.email, action.payload.password) return this.authService.authenticate(action.payload.email, action.payload.password).pipe(
.first() first(),
.map((response: AuthStatus) => new AuthenticationSuccessAction(response.token)) map((response: AuthStatus) => new AuthenticationSuccessAction(response.token)),
.catch((error) => Observable.of(new AuthenticationErrorAction(error))); catchError((error) => observableOf(new AuthenticationErrorAction(error)))
}); );
})
);
@Effect() @Effect()
public authenticateSuccess$: Observable<Action> = this.actions$ public authenticateSuccess$: Observable<Action> = this.actions$.pipe(
.ofType(AuthActionTypes.AUTHENTICATE_SUCCESS) ofType(AuthActionTypes.AUTHENTICATE_SUCCESS),
.do((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)) tap((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)),
.map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload)); map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload))
);
@Effect() @Effect()
public authenticated$: Observable<Action> = this.actions$ public authenticated$: Observable<Action> = this.actions$.pipe(
.ofType(AuthActionTypes.AUTHENTICATED) ofType(AuthActionTypes.AUTHENTICATED),
.switchMap((action: AuthenticatedAction) => { switchMap((action: AuthenticatedAction) => {
return this.authService.authenticatedUser(action.payload) return this.authService.authenticatedUser(action.payload).pipe(
.map((user: EPerson) => new AuthenticatedSuccessAction((user !== null), action.payload, user)) map((user: EPerson) => new AuthenticatedSuccessAction((user !== null), action.payload, user)),
.catch((error) => Observable.of(new AuthenticatedErrorAction(error))); catchError((error) => observableOf(new AuthenticatedErrorAction(error))),);
}); })
);
// It means "reacts to this action but don't send another" // It means "reacts to this action but don't send another"
@Effect({dispatch: false}) @Effect({ dispatch: false })
public authenticatedError$: Observable<Action> = this.actions$ public authenticatedError$: Observable<Action> = this.actions$.pipe(
.ofType(AuthActionTypes.AUTHENTICATED_ERROR) ofType(AuthActionTypes.AUTHENTICATED_ERROR),
.do((action: LogOutSuccessAction) => this.authService.removeToken()); tap((action: LogOutSuccessAction) => this.authService.removeToken())
);
@Effect() @Effect()
public checkToken$: Observable<Action> = this.actions$ public checkToken$: Observable<Action> = this.actions$.pipe(ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN),
.ofType(AuthActionTypes.CHECK_AUTHENTICATION_TOKEN) switchMap(() => {
.switchMap(() => { return this.authService.hasValidAuthenticationToken().pipe(
return this.authService.hasValidAuthenticationToken() map((token: AuthTokenInfo) => new AuthenticatedAction(token)),
.map((token: AuthTokenInfo) => new AuthenticatedAction(token)) catchError((error) => observableOf(new CheckAuthenticationTokenErrorAction()))
.catch((error) => Observable.of(new CheckAuthenticationTokenErrorAction())); );
}); })
);
@Effect() @Effect()
public createUser$: Observable<Action> = this.actions$ public createUser$: Observable<Action> = this.actions$.pipe(
.ofType(AuthActionTypes.REGISTRATION) ofType(AuthActionTypes.REGISTRATION),
.debounceTime(500) // to remove when functionality is implemented debounceTime(500), // to remove when functionality is implemented
.switchMap((action: RegistrationAction) => { switchMap((action: RegistrationAction) => {
return this.authService.create(action.payload) return this.authService.create(action.payload).pipe(
.map((user: EPerson) => new RegistrationSuccessAction(user)) map((user: EPerson) => new RegistrationSuccessAction(user)),
.catch((error) => Observable.of(new RegistrationErrorAction(error))); catchError((error) => observableOf(new RegistrationErrorAction(error)))
}); );
})
);
@Effect() @Effect()
public refreshToken$: Observable<Action> = this.actions$ public refreshToken$: Observable<Action> = this.actions$.pipe(ofType(AuthActionTypes.REFRESH_TOKEN),
.ofType(AuthActionTypes.REFRESH_TOKEN) switchMap((action: RefreshTokenAction) => {
.switchMap((action: RefreshTokenAction) => { return this.authService.refreshAuthenticationToken(action.payload).pipe(
return this.authService.refreshAuthenticationToken(action.payload) map((token: AuthTokenInfo) => new RefreshTokenSuccessAction(token)),
.map((token: AuthTokenInfo) => new RefreshTokenSuccessAction(token)) catchError((error) => observableOf(new RefreshTokenErrorAction()))
.catch((error) => Observable.of(new RefreshTokenErrorAction())); );
}); })
);
// It means "reacts to this action but don't send another" // It means "reacts to this action but don't send another"
@Effect({dispatch: false}) @Effect({ dispatch: false })
public refreshTokenSuccess$: Observable<Action> = this.actions$ public refreshTokenSuccess$: Observable<Action> = this.actions$.pipe(
.ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS) ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS),
.do((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload)); tap((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload))
);
/** /**
* When the store is rehydrated in the browser, * When the store is rehydrated in the browser,
* clear a possible invalid token or authentication errors * clear a possible invalid token or authentication errors
*/ */
@Effect({dispatch: false}) @Effect({ dispatch: false })
public clearInvalidTokenOnRehydrate$: Observable<any> = this.actions$ public clearInvalidTokenOnRehydrate$: Observable<any> = this.actions$.pipe(
.ofType(StoreActionTypes.REHYDRATE) ofType(StoreActionTypes.REHYDRATE),
.switchMap(() => { switchMap(() => {
return this.store.select(isAuthenticated) return this.store.pipe(
.take(1) select(isAuthenticated),
.filter((authenticated) => !authenticated) first(),
.do(() => this.authService.removeToken()) filter((authenticated) => !authenticated),
.do(() => this.authService.resetAuthenticationError()); tap(() => this.authService.removeToken()),
}); tap(() => this.authService.resetAuthenticationError())
);
}));
@Effect() @Effect()
public logOut$: Observable<Action> = this.actions$ public logOut$: Observable<Action> = this.actions$
.ofType(AuthActionTypes.LOG_OUT) .pipe(
.switchMap(() => { ofType(AuthActionTypes.LOG_OUT),
return this.authService.logout() switchMap(() => {
.map((value) => new LogOutSuccessAction()) return this.authService.logout().pipe(
.catch((error) => Observable.of(new LogOutErrorAction(error))); map((value) => new LogOutSuccessAction()),
}); catchError((error) => observableOf(new LogOutErrorAction(error)))
);
})
);
@Effect({dispatch: false}) @Effect({ dispatch: false })
public logOutSuccess$: Observable<Action> = this.actions$ public logOutSuccess$: Observable<Action> = this.actions$
.ofType(AuthActionTypes.LOG_OUT_SUCCESS) .pipe(ofType(AuthActionTypes.LOG_OUT_SUCCESS),
.do(() => this.authService.removeToken()) tap(() => this.authService.removeToken()),
.do(() => this.authService.clearRedirectUrl()) tap(() => this.authService.clearRedirectUrl()),
.do(() => this.authService.refreshAfterLogout()); tap(() => this.authService.refreshAfterLogout())
);
@Effect({dispatch: false}) @Effect({ dispatch: false })
public redirectToLogin$: Observable<Action> = this.actions$ public redirectToLogin$: Observable<Action> = this.actions$
.ofType(AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED) .pipe(ofType(AuthActionTypes.REDIRECT_AUTHENTICATION_REQUIRED),
.do(() => this.authService.removeToken()) tap(() => this.authService.removeToken()),
.do(() => this.authService.redirectToLogin()); tap(() => this.authService.redirectToLogin())
);
@Effect({dispatch: false}) @Effect({ dispatch: false })
public redirectToLoginTokenExpired$: Observable<Action> = this.actions$ public redirectToLoginTokenExpired$: Observable<Action> = this.actions$
.ofType(AuthActionTypes.REDIRECT_TOKEN_EXPIRED) .pipe(
.do(() => this.authService.removeToken()) ofType(AuthActionTypes.REDIRECT_TOKEN_EXPIRED),
.do(() => this.authService.redirectToLoginWhenTokenExpired()); tap(() => this.authService.removeToken()),
tap(() => this.authService.redirectToLoginWhenTokenExpired())
);
/** /**
* @constructor * @constructor

View File

@@ -4,7 +4,7 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable'; import { of as observableOf } from 'rxjs';
import { AuthInterceptor } from './auth.interceptor'; import { AuthInterceptor } from './auth.interceptor';
import { AuthService } from './auth.service'; import { AuthService } from './auth.service';
@@ -23,7 +23,7 @@ describe(`AuthInterceptor`, () => {
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
dispatch: {}, dispatch: {},
/* tslint:enable:no-empty */ /* tslint:enable:no-empty */
select: Observable.of(true) select: observableOf(true)
}); });
beforeEach(() => { beforeEach(() => {

View File

@@ -1,13 +1,16 @@
import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs';
import { catchError, filter, map } from 'rxjs/operators';
import { Injectable, Injector } from '@angular/core'; import { Injectable, Injector } from '@angular/core';
import { import {
HttpEvent, HttpInterceptor, HttpHandler, HttpRequest, HttpResponse, HttpErrorResponse,
HttpErrorResponse, HttpResponseBase HttpEvent,
HttpHandler,
HttpInterceptor,
HttpRequest,
HttpResponse,
HttpResponseBase
} from '@angular/common/http'; } from '@angular/common/http';
import { Observable } from 'rxjs/Rx';
import 'rxjs/add/observable/throw'
import 'rxjs/add/operator/catch';
import { find } from 'lodash'; import { find } from 'lodash';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
@@ -79,11 +82,11 @@ export class AuthInterceptor implements HttpInterceptor {
// The access token is expired // The access token is expired
// Redirect to the login route // Redirect to the login route
this.store.dispatch(new RedirectWhenTokenExpiredAction('auth.messages.expired')); this.store.dispatch(new RedirectWhenTokenExpiredAction('auth.messages.expired'));
return Observable.of(null); return observableOf(null);
} else if (!this.isAuthRequest(req) && isNotEmpty(token)) { } else if (!this.isAuthRequest(req) && isNotEmpty(token)) {
// Intercept a request that is not to the authentication endpoint // Intercept a request that is not to the authentication endpoint
authService.isTokenExpiring() authService.isTokenExpiring().pipe(
.filter((isExpiring) => isExpiring) filter((isExpiring) => isExpiring))
.subscribe(() => { .subscribe(() => {
// If the current request url is already in the refresh token request list, skip it // If the current request url is already in the refresh token request list, skip it
if (isUndefined(find(this.refreshTokenRequestUrls, req.url))) { if (isUndefined(find(this.refreshTokenRequestUrls, req.url))) {
@@ -101,8 +104,8 @@ export class AuthInterceptor implements HttpInterceptor {
} }
// Pass on the new request instead of the original request. // Pass on the new request instead of the original request.
return next.handle(newReq) return next.handle(newReq).pipe(
.map((response) => { map((response) => {
// Intercept a Login/Logout response // Intercept a Login/Logout response
if (response instanceof HttpResponse && this.isSuccess(response) && (this.isLoginResponse(response) || this.isLogoutResponse(response))) { if (response instanceof HttpResponse && this.isSuccess(response) && (this.isLoginResponse(response) || this.isLogoutResponse(response))) {
// It's a success Login/Logout response // It's a success Login/Logout response
@@ -122,8 +125,8 @@ export class AuthInterceptor implements HttpInterceptor {
} else { } else {
return response; return response;
} }
}) }),
.catch((error, caught) => { catchError((error, caught) => {
// Intercept an error response // Intercept an error response
if (error instanceof HttpErrorResponse) { if (error instanceof HttpErrorResponse) {
// Checks if is a response from a request to an authentication endpoint // Checks if is a response from a request to an authentication endpoint
@@ -138,7 +141,7 @@ export class AuthInterceptor implements HttpInterceptor {
statusText: error.statusText, statusText: error.statusText,
url: error.url url: error.url
}); });
return Observable.of(authResponse); return observableOf(authResponse);
} else if (this.isUnauthorized(error) && isNotNull(token) && authService.isTokenExpired()) { } else if (this.isUnauthorized(error) && isNotNull(token) && authService.isTokenExpired()) {
// The access token provided is expired, revoked, malformed, or invalid for other reasons // The access token provided is expired, revoked, malformed, or invalid for other reasons
// Redirect to the login route // Redirect to the login route
@@ -146,8 +149,7 @@ export class AuthInterceptor implements HttpInterceptor {
} }
} }
// Return error response as is. // Return error response as is.
return Observable.throw(error); return observableThrowError(error);
}) as any; })) as any;
} }
} }

View File

@@ -3,9 +3,8 @@ import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { Store, StoreModule } from '@ngrx/store'; import { Store, StoreModule } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { REQUEST } from '@nguniversal/express-engine/tokens'; import { REQUEST } from '@nguniversal/express-engine/tokens';
import 'rxjs/add/observable/of'; import { of as observableOf } from 'rxjs';
import { authReducer, AuthState } from './auth.reducer'; import { authReducer, AuthState } from './auth.reducer';
import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service'; import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service';
@@ -29,41 +28,53 @@ describe('AuthService test', () => {
const mockStore: Store<AuthState> = jasmine.createSpyObj('store', { const mockStore: Store<AuthState> = jasmine.createSpyObj('store', {
dispatch: {}, dispatch: {},
select: Observable.of(true) pipe: observableOf(true)
}); });
let authService: AuthService; let authService: AuthService;
const authRequest = new AuthRequestServiceStub(); let authRequest;
const window = new NativeWindowRef(); const window = new NativeWindowRef();
const routerStub = new RouterStub(); const routerStub = new RouterStub();
const routeStub = new ActivatedRouteStub(); let routeStub;
let storage: CookieService; let storage: CookieService;
const token: AuthTokenInfo = new AuthTokenInfo('test_token'); let token: AuthTokenInfo;
token.expires = Date.now() + (1000 * 60 * 60); let authenticatedState;
let authenticatedState = {
authenticated: true,
loaded: true,
loading: false,
authToken: token,
user: EPersonMock
};
const rdbService = getMockRemoteDataBuildService(); const rdbService = getMockRemoteDataBuildService();
describe('', () => {
function init() {
token = new AuthTokenInfo('test_token');
token.expires = Date.now() + (1000 * 60 * 60);
authenticatedState = {
authenticated: true,
loaded: true,
loading: false,
authToken: token,
user: EPersonMock
};
authRequest = new AuthRequestServiceStub();
routeStub = new ActivatedRouteStub();
}
beforeEach(() => {
init();
});
describe('', () => {
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
CommonModule, CommonModule,
StoreModule.forRoot({authReducer}), StoreModule.forRoot({ authReducer }),
], ],
declarations: [], declarations: [],
providers: [ providers: [
{provide: AuthRequestService, useValue: authRequest}, { provide: AuthRequestService, useValue: authRequest },
{provide: NativeWindowService, useValue: window}, { provide: NativeWindowService, useValue: window },
{provide: REQUEST, useValue: {}}, { provide: REQUEST, useValue: {} },
{provide: Router, useValue: routerStub}, { provide: Router, useValue: routerStub },
{provide: ActivatedRoute, useValue: routeStub}, { provide: ActivatedRoute, useValue: routeStub },
{provide: Store, useValue: mockStore}, {provide: Store, useValue: mockStore},
{provide: RemoteDataBuildService, useValue: rdbService}, { provide: RemoteDataBuildService, useValue: rdbService },
CookieService, CookieService,
AuthService AuthService
], ],
@@ -116,15 +127,16 @@ describe('AuthService test', () => {
describe('', () => { describe('', () => {
beforeEach(async(() => { beforeEach(async(() => {
init();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
StoreModule.forRoot({authReducer}) StoreModule.forRoot({ authReducer })
], ],
providers: [ providers: [
{provide: AuthRequestService, useValue: authRequest}, { provide: AuthRequestService, useValue: authRequest },
{provide: REQUEST, useValue: {}}, { provide: REQUEST, useValue: {} },
{provide: Router, useValue: routerStub}, { provide: Router, useValue: routerStub },
{provide: RemoteDataBuildService, useValue: rdbService}, { provide: RemoteDataBuildService, useValue: rdbService },
CookieService CookieService
] ]
}).compileComponents(); }).compileComponents();
@@ -136,7 +148,7 @@ describe('AuthService test', () => {
(state as any).core = Object.create({}); (state as any).core = Object.create({});
(state as any).core.auth = authenticatedState; (state as any).core.auth = authenticatedState;
}); });
authService = new AuthService({}, window, authReqService, router, cookieService, store, rdbService); authService = new AuthService({}, window, undefined, authReqService, router, cookieService, store, rdbService);
})); }));
it('should return true when user is logged in', () => { it('should return true when user is logged in', () => {
@@ -168,12 +180,12 @@ describe('AuthService test', () => {
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
StoreModule.forRoot({authReducer}) StoreModule.forRoot({ authReducer })
], ],
providers: [ providers: [
{provide: AuthRequestService, useValue: authRequest}, { provide: AuthRequestService, useValue: authRequest },
{provide: REQUEST, useValue: {}}, { provide: REQUEST, useValue: {} },
{provide: Router, useValue: routerStub}, { provide: Router, useValue: routerStub },
ClientCookieService, ClientCookieService,
CookieService CookieService
] ]
@@ -195,7 +207,7 @@ describe('AuthService test', () => {
(state as any).core = Object.create({}); (state as any).core = Object.create({});
(state as any).core.auth = authenticatedState; (state as any).core.auth = authenticatedState;
}); });
authService = new AuthService({}, window, authReqService, router, cookieService, store, rdbService); authService = new AuthService({}, window, undefined, authReqService, router, cookieService, store, rdbService);
storage = (authService as any).storage; storage = (authService as any).storage;
spyOn(storage, 'get'); spyOn(storage, 'get');
spyOn(storage, 'remove'); spyOn(storage, 'remove');

View File

@@ -1,13 +1,22 @@
import { Inject, Injectable } from '@angular/core'; import { Observable, of as observableOf } from 'rxjs';
import {
distinctUntilChanged,
filter,
first,
map,
startWith,
switchMap,
take,
withLatestFrom
} from 'rxjs/operators';
import { Inject, Injectable, Optional } from '@angular/core';
import { PRIMARY_OUTLET, Router, UrlSegmentGroup, UrlTree } from '@angular/router'; import { PRIMARY_OUTLET, Router, UrlSegmentGroup, UrlTree } from '@angular/router';
import { HttpHeaders } from '@angular/common/http'; import { HttpHeaders } from '@angular/common/http';
import { REQUEST } from '@nguniversal/express-engine/tokens'; import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
import { RouterReducerState } from '@ngrx/router-store'; import { RouterReducerState } from '@ngrx/router-store';
import { Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
import { CookieAttributes } from 'js-cookie'; import { CookieAttributes } from 'js-cookie';
import { Observable } from 'rxjs/Observable';
import { map, switchMap, withLatestFrom } from 'rxjs/operators';
import { EPerson } from '../eperson/models/eperson.model'; import { EPerson } from '../eperson/models/eperson.model';
import { AuthRequestService } from './auth-request.service'; import { AuthRequestService } from './auth-request.service';
@@ -50,26 +59,30 @@ export class AuthService {
constructor(@Inject(REQUEST) protected req: any, constructor(@Inject(REQUEST) protected req: any,
@Inject(NativeWindowService) protected _window: NativeWindowRef, @Inject(NativeWindowService) protected _window: NativeWindowRef,
protected authRequestService: AuthRequestService, protected authRequestService: AuthRequestService,
@Optional() @Inject(RESPONSE) private response: any,
protected router: Router, protected router: Router,
protected storage: CookieService, protected storage: CookieService,
protected store: Store<AppState>, protected store: Store<AppState>,
protected rdbService: RemoteDataBuildService protected rdbService: RemoteDataBuildService
) { ) {
this.store.select(isAuthenticated) this.store.pipe(
.startWith(false) select(isAuthenticated),
.subscribe((authenticated: boolean) => this._authenticated = authenticated); startWith(false)
).subscribe((authenticated: boolean) => this._authenticated = authenticated);
// If current route is different from the one setted in authentication guard // If current route is different from the one setted in authentication guard
// and is not the login route, clear redirect url and messages // and is not the login route, clear redirect url and messages
const routeUrl$ = this.store.select(routerStateSelector) const routeUrl$ = this.store.pipe(
.filter((routerState: RouterReducerState) => isNotUndefined(routerState) && isNotUndefined(routerState.state)) select(routerStateSelector),
.filter((routerState: RouterReducerState) => !this.isLoginRoute(routerState.state.url)) filter((routerState: RouterReducerState) => isNotUndefined(routerState) && isNotUndefined(routerState.state)),
.map((routerState: RouterReducerState) => routerState.state.url); filter((routerState: RouterReducerState) => !this.isLoginRoute(routerState.state.url)),
const redirectUrl$ = this.store.select(getRedirectUrl).distinctUntilChanged(); map((routerState: RouterReducerState) => routerState.state.url)
);
const redirectUrl$ = this.store.pipe(select(getRedirectUrl), distinctUntilChanged());
routeUrl$.pipe( routeUrl$.pipe(
withLatestFrom(redirectUrl$), withLatestFrom(redirectUrl$),
map(([routeUrl, redirectUrl]) => [routeUrl, redirectUrl]) map(([routeUrl, redirectUrl]) => [routeUrl, redirectUrl])
).filter(([routeUrl, redirectUrl]) => isNotEmpty(redirectUrl) && (routeUrl !== redirectUrl)) ).pipe(filter(([routeUrl, redirectUrl]) => isNotEmpty(redirectUrl) && (routeUrl !== redirectUrl)))
.subscribe(() => { .subscribe(() => {
this.clearRedirectUrl(); this.clearRedirectUrl();
}); });
@@ -102,14 +115,14 @@ export class AuthService {
let headers = new HttpHeaders(); let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
options.headers = headers; options.headers = headers;
return this.authRequestService.postToEndpoint('login', body, options) return this.authRequestService.postToEndpoint('login', body, options).pipe(
.map((status: AuthStatus) => { map((status: AuthStatus) => {
if (status.authenticated) { if (status.authenticated) {
return status; return status;
} else { } else {
throw(new Error('Invalid email or password')); throw(new Error('Invalid email or password'));
} }
}) }))
} }
@@ -118,7 +131,7 @@ export class AuthService {
* @returns {Observable<boolean>} * @returns {Observable<boolean>}
*/ */
public isAuthenticated(): Observable<boolean> { public isAuthenticated(): Observable<boolean> {
return this.store.select(isAuthenticated); return this.store.pipe(select(isAuthenticated));
} }
/** /**
@@ -158,9 +171,10 @@ export class AuthService {
* Checks if token is present into storage and is not expired * Checks if token is present into storage and is not expired
*/ */
public hasValidAuthenticationToken(): Observable<AuthTokenInfo> { public hasValidAuthenticationToken(): Observable<AuthTokenInfo> {
return this.store.select(getAuthenticationToken) return this.store.pipe(
.take(1) select(getAuthenticationToken),
.map((authTokenInfo: AuthTokenInfo) => { take(1),
map((authTokenInfo: AuthTokenInfo) => {
let token: AuthTokenInfo; let token: AuthTokenInfo;
// Retrieve authentication token info and check if is valid // Retrieve authentication token info and check if is valid
token = isNotEmpty(authTokenInfo) ? authTokenInfo : this.storage.get(TOKENITEM); token = isNotEmpty(authTokenInfo) ? authTokenInfo : this.storage.get(TOKENITEM);
@@ -169,7 +183,8 @@ export class AuthService {
} else { } else {
throw false; throw false;
} }
}); })
);
} }
/** /**
@@ -181,14 +196,14 @@ export class AuthService {
headers = headers.append('Accept', 'application/json'); headers = headers.append('Accept', 'application/json');
headers = headers.append('Authorization', `Bearer ${token.accessToken}`); headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
options.headers = headers; options.headers = headers;
return this.authRequestService.postToEndpoint('login', {}, options) return this.authRequestService.postToEndpoint('login', {}, options).pipe(
.map((status: AuthStatus) => { map((status: AuthStatus) => {
if (status.authenticated) { if (status.authenticated) {
return status.token; return status.token;
} else { } else {
throw(new Error('Not authenticated')); throw(new Error('Not authenticated'));
} }
}); }));
} }
/** /**
@@ -207,7 +222,7 @@ export class AuthService {
// details and then return the new user object // details and then return the new user object
// but, let's just return the new user for this example. // but, let's just return the new user for this example.
// this._authenticated = true; // this._authenticated = true;
return Observable.of(user); return observableOf(user);
} }
/** /**
@@ -219,14 +234,15 @@ export class AuthService {
let headers = new HttpHeaders(); let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded'); headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
const options: HttpOptions = Object.create({ headers, responseType: 'text' }); const options: HttpOptions = Object.create({ headers, responseType: 'text' });
return this.authRequestService.getRequest('logout', options) return this.authRequestService.getRequest('logout', options).pipe(
.map((status: AuthStatus) => { map((status: AuthStatus) => {
if (!status.authenticated) { if (!status.authenticated) {
return true; return true;
} else { } else {
throw(new Error('auth.errors.invalid-user')); throw(new Error('auth.errors.invalid-user'));
} }
}) }))
} }
/** /**
@@ -246,7 +262,7 @@ export class AuthService {
*/ */
public getToken(): AuthTokenInfo { public getToken(): AuthTokenInfo {
let token: AuthTokenInfo; let token: AuthTokenInfo;
this.store.select(getAuthenticationToken) this.store.pipe(select(getAuthenticationToken))
.subscribe((authTokenInfo: AuthTokenInfo) => { .subscribe((authTokenInfo: AuthTokenInfo) => {
// Retrieve authentication token info and check if is valid // Retrieve authentication token info and check if is valid
token = authTokenInfo || null; token = authTokenInfo || null;
@@ -259,9 +275,10 @@ export class AuthService {
* @returns {boolean} * @returns {boolean}
*/ */
public isTokenExpiring(): Observable<boolean> { public isTokenExpiring(): Observable<boolean> {
return this.store.select(isTokenRefreshing) return this.store.pipe(
.first() select(isTokenRefreshing),
.map((isRefreshing: boolean) => { first(),
map((isRefreshing: boolean) => {
if (this.isTokenExpired() || isRefreshing) { if (this.isTokenExpired() || isRefreshing) {
return false; return false;
} else { } else {
@@ -269,6 +286,7 @@ export class AuthService {
return token.expires - (60 * 5 * 1000) < Date.now(); return token.expires - (60 * 5 * 1000) < Date.now();
} }
}) })
)
} }
/** /**
@@ -328,6 +346,10 @@ export class AuthService {
if (this._window.nativeWindow.location) { if (this._window.nativeWindow.location) {
// Hard redirect to login page, so that all state is definitely lost // Hard redirect to login page, so that all state is definitely lost
this._window.nativeWindow.location.href = redirectUrl; this._window.nativeWindow.location.href = redirectUrl;
} else if (this.response) {
if (!this.response._headerSent) {
this.response.redirect(302, redirectUrl);
}
} else { } else {
this.router.navigateByUrl(redirectUrl); this.router.navigateByUrl(redirectUrl);
} }
@@ -337,17 +359,13 @@ export class AuthService {
* Redirect to the route navigated before the login * Redirect to the route navigated before the login
*/ */
public redirectToPreviousUrl() { public redirectToPreviousUrl() {
this.getRedirectUrl() this.getRedirectUrl().pipe(
.first() first())
.subscribe((redirectUrl) => { .subscribe((redirectUrl) => {
if (isNotEmpty(redirectUrl)) { if (isNotEmpty(redirectUrl)) {
this.clearRedirectUrl(); this.clearRedirectUrl();
this.router.onSameUrlNavigation = 'reload';
// override the route reuse strategy
this.router.routeReuseStrategy.shouldReuseRoute = () => {
return false;
};
this.router.navigated = false;
const url = decodeURIComponent(redirectUrl); const url = decodeURIComponent(redirectUrl);
this.router.navigateByUrl(url); this.router.navigateByUrl(url);
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */ /* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
@@ -376,9 +394,9 @@ export class AuthService {
getRedirectUrl(): Observable<string> { getRedirectUrl(): Observable<string> {
const redirectUrl = this.storage.get(REDIRECT_COOKIE); const redirectUrl = this.storage.get(REDIRECT_COOKIE);
if (isNotEmpty(redirectUrl)) { if (isNotEmpty(redirectUrl)) {
return Observable.of(redirectUrl); return observableOf(redirectUrl);
} else { } else {
return this.store.select(getRedirectUrl); return this.store.pipe(select(getRedirectUrl));
} }
} }

View File

@@ -1,8 +1,10 @@
import {take} from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router, RouterStateSnapshot } from '@angular/router'; import { ActivatedRouteSnapshot, CanActivate, CanLoad, Route, Router, RouterStateSnapshot } from '@angular/router';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { Store } from '@ngrx/store'; import { select, Store } from '@ngrx/store';
// reducers // reducers
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
@@ -52,12 +54,12 @@ export class AuthenticatedGuard implements CanActivate, CanLoad {
private handleAuth(url: string): Observable<boolean> { private handleAuth(url: string): Observable<boolean> {
// get observable // get observable
const observable = this.store.select(isAuthenticated); const observable = this.store.pipe(select(isAuthenticated));
// redirect to sign in page if user is not authenticated // redirect to sign in page if user is not authenticated
observable observable.pipe(
// .filter(() => isEmpty(this.router.routerState.snapshot.url) || this.router.routerState.snapshot.url === url) // .filter(() => isEmpty(this.router.routerState.snapshot.url) || this.router.routerState.snapshot.url === url)
.take(1) take(1))
.subscribe((authenticated) => { .subscribe((authenticated) => {
if (!authenticated) { if (!authenticated) {
this.authService.setRedirectUrl(url); this.authService.setRedirectUrl(url);

View File

@@ -2,7 +2,7 @@ import { AuthError } from './auth-error.model';
import { AuthTokenInfo } from './auth-token-info.model'; import { AuthTokenInfo } from './auth-token-info.model';
import { EPerson } from '../../eperson/models/eperson.model'; import { EPerson } from '../../eperson/models/eperson.model';
import { RemoteData } from '../../data/remote-data'; import { RemoteData } from '../../data/remote-data';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
export class AuthStatus { export class AuthStatus {

View File

@@ -1,7 +1,7 @@
import { first, map, switchMap } from 'rxjs/operators'; import { first, map, switchMap } from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { HttpHeaders } from '@angular/common/http'; import { HttpHeaders } from '@angular/common/http';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { AuthStatus } from './models/auth-status.model'; import { AuthStatus } from './models/auth-status.model';

View File

@@ -1,5 +1,5 @@
import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/Rx'; import { TestScheduler } from 'rxjs/testing';
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service'; import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service'; import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { distinctUntilChanged, map, startWith } from 'rxjs/operators'; import { distinctUntilChanged, map, startWith } from 'rxjs/operators';
import { import {
ensureArrayHasValue, ensureArrayHasValue,

View File

@@ -0,0 +1,59 @@
import { RemoteDataBuildService } from './remote-data-build.service';
import { Item } from '../../shared/item.model';
import { PaginatedList } from '../../data/paginated-list';
import { PageInfo } from '../../shared/page-info.model';
import { RemoteData } from '../../data/remote-data';
import { of as observableOf } from 'rxjs';
const pageInfo = new PageInfo();
const array = [
Object.assign(new Item(), {
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Item nr 1'
}]
}),
Object.assign(new Item(), {
metadata: [
{
key: 'dc.title',
language: 'en_US',
value: 'Item nr 2'
}]
})
];
const paginatedList = new PaginatedList(pageInfo, array);
const arrayRD = new RemoteData(false, false, true, undefined, array);
const paginatedListRD = new RemoteData(false, false, true, undefined, paginatedList);
describe('RemoteDataBuildService', () => {
let service: RemoteDataBuildService;
beforeEach(() => {
service = new RemoteDataBuildService(undefined, undefined, undefined);
});
describe('when toPaginatedList is called', () => {
let expected: RemoteData<PaginatedList<Item>>;
beforeEach(() => {
expected = paginatedListRD;
});
it('should return the correct remoteData of a paginatedList when the input is a (remoteData of an) array', () => {
const result = (service as any).toPaginatedList(observableOf(arrayRD), pageInfo);
result.subscribe((resultRD) => {
expect(resultRD).toEqual(expected);
});
});
it('should return the correct remoteData of a paginatedList when the input is a (remoteData of a) paginated list', () => {
const result = (service as any).toPaginatedList(observableOf(paginatedListRD), pageInfo);
result.subscribe((resultRD) => {
expect(resultRD).toEqual(expected);
});
});
});
});

View File

@@ -1,5 +1,10 @@
import {
combineLatest as observableCombineLatest,
Observable,
of as observableOf,
race as observableRace
} from 'rxjs';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { distinctUntilChanged, flatMap, map, startWith } from 'rxjs/operators'; import { distinctUntilChanged, flatMap, map, startWith } from 'rxjs/operators';
import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util';
import { PaginatedList } from '../../data/paginated-list'; import { PaginatedList } from '../../data/paginated-list';
@@ -17,10 +22,10 @@ import { ResponseCacheService } from '../response-cache.service';
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators'; import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
import { PageInfo } from '../../shared/page-info.model'; import { PageInfo } from '../../shared/page-info.model';
import { import {
filterSuccessfulResponses,
getRequestFromSelflink, getRequestFromSelflink,
getResourceLinksFromResponse, getResourceLinksFromResponse,
getResponseFromSelflink, getResponseFromSelflink
filterSuccessfulResponses
} from '../../shared/operators'; } from '../../shared/operators';
@Injectable() @Injectable()
@@ -32,24 +37,24 @@ export class RemoteDataBuildService {
buildSingle<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<TDomain>> { buildSingle<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<TDomain>> {
if (typeof href$ === 'string') { if (typeof href$ === 'string') {
href$ = Observable.of(href$); href$ = observableOf(href$);
} }
const requestHref$ = href$.pipe(flatMap((href: string) => const requestHref$ = href$.pipe(flatMap((href: string) =>
this.objectCache.getRequestHrefBySelfLink(href))); this.objectCache.getRequestHrefBySelfLink(href)));
const requestEntry$ = Observable.race( const requestEntry$ = observableRace(
href$.pipe(getRequestFromSelflink(this.requestService)), href$.pipe(getRequestFromSelflink(this.requestService)),
requestHref$.pipe(getRequestFromSelflink(this.requestService)) requestHref$.pipe(getRequestFromSelflink(this.requestService))
); );
const responseCache$ = Observable.race( const responseCache$ = observableRace(
href$.pipe(getResponseFromSelflink(this.responseCache)), href$.pipe(getResponseFromSelflink(this.responseCache)),
requestHref$.pipe(getResponseFromSelflink(this.responseCache)) requestHref$.pipe(getResponseFromSelflink(this.responseCache))
); );
// always use self link if that is cached, only if it isn't, get it via the response. // always use self link if that is cached, only if it isn't, get it via the response.
const payload$ = const payload$ =
Observable.combineLatest( observableCombineLatest(
href$.pipe( href$.pipe(
flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)), flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)),
startWith(undefined) startWith(undefined)
@@ -60,20 +65,20 @@ export class RemoteDataBuildService {
if (isNotEmpty(resourceSelfLinks)) { if (isNotEmpty(resourceSelfLinks)) {
return this.objectCache.getBySelfLink(resourceSelfLinks[0]); return this.objectCache.getBySelfLink(resourceSelfLinks[0]);
} else { } else {
return Observable.of(undefined); return observableOf(undefined);
} }
}), }),
distinctUntilChanged(), distinctUntilChanged(),
startWith(undefined) startWith(undefined)
), )
(fromSelfLink, fromResponse) => { ).pipe(
map(([fromSelfLink, fromResponse]) => {
if (hasValue(fromSelfLink)) { if (hasValue(fromSelfLink)) {
return fromSelfLink; return fromSelfLink;
} else { } else {
return fromResponse; return fromResponse;
} }
} }),
).pipe(
hasValueOperator(), hasValueOperator(),
map((normalized: TNormalized) => { map((normalized: TNormalized) => {
return this.build<TNormalized, TDomain>(normalized); return this.build<TNormalized, TDomain>(normalized);
@@ -85,8 +90,8 @@ export class RemoteDataBuildService {
} }
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, responseCache$: Observable<ResponseCacheEntry>, payload$: Observable<T>) { toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, responseCache$: Observable<ResponseCacheEntry>, payload$: Observable<T>) {
return Observable.combineLatest(requestEntry$, responseCache$.startWith(undefined), payload$, return observableCombineLatest(requestEntry$, responseCache$.pipe(startWith(undefined)), payload$).pipe(
(reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => { map(([reqEntry, resEntry, payload]) => {
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true; const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false; const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
let isSuccessful: boolean; let isSuccessful: boolean;
@@ -105,12 +110,13 @@ export class RemoteDataBuildService {
error, error,
payload payload
); );
}); })
);
} }
buildList<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<PaginatedList<TDomain>>> { buildList<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<PaginatedList<TDomain>>> {
if (typeof href$ === 'string') { if (typeof href$ === 'string') {
href$ = Observable.of(href$); href$ = observableOf(href$);
} }
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
@@ -119,12 +125,12 @@ export class RemoteDataBuildService {
const tDomainList$ = responseCache$.pipe( const tDomainList$ = responseCache$.pipe(
getResourceLinksFromResponse(), getResourceLinksFromResponse(),
flatMap((resourceUUIDs: string[]) => { flatMap((resourceUUIDs: string[]) => {
return this.objectCache.getList(resourceUUIDs) return this.objectCache.getList(resourceUUIDs).pipe(
.map((normList: TNormalized[]) => { map((normList: TNormalized[]) => {
return normList.map((normalized: TNormalized) => { return normList.map((normalized: TNormalized) => {
return this.build<TNormalized, TDomain>(normalized); return this.build<TNormalized, TDomain>(normalized);
}); });
}); }));
}), }),
startWith([]), startWith([]),
distinctUntilChanged() distinctUntilChanged()
@@ -142,11 +148,13 @@ export class RemoteDataBuildService {
} }
} }
}) })
); );
const payload$ = Observable.combineLatest(tDomainList$, pageInfo$, (tDomainList, pageInfo) => { const payload$ = observableCombineLatest(tDomainList$, pageInfo$).pipe(
return new PaginatedList(pageInfo, tDomainList); map(([tDomainList, pageInfo]) => {
}); return new PaginatedList(pageInfo, tDomainList);
})
);
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$); return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
} }
@@ -190,7 +198,7 @@ export class RemoteDataBuildService {
} }
if (hasValue(normalized[relationship].page)) { if (hasValue(normalized[relationship].page)) {
links[relationship] = this.aggregatePaginatedList(result, normalized[relationship].pageInfo); links[relationship] = this.toPaginatedList(result, normalized[relationship].pageInfo);
} else { } else {
links[relationship] = result; links[relationship] = result;
} }
@@ -204,12 +212,11 @@ export class RemoteDataBuildService {
aggregate<T>(input: Array<Observable<RemoteData<T>>>): Observable<RemoteData<T[]>> { aggregate<T>(input: Array<Observable<RemoteData<T>>>): Observable<RemoteData<T[]>> {
if (isEmpty(input)) { if (isEmpty(input)) {
return Observable.of(new RemoteData(false, false, true, null, [])); return observableOf(new RemoteData(false, false, true, null, []));
} }
return Observable.combineLatest( return observableCombineLatest(...input).pipe(
...input, map((arr) => {
(...arr: Array<RemoteData<T>>) => {
const requestPending: boolean = arr const requestPending: boolean = arr
.map((d: RemoteData<T>) => d.isRequestPending) .map((d: RemoteData<T>) => d.isRequestPending)
.every((b: boolean) => b === true); .every((b: boolean) => b === true);
@@ -251,11 +258,19 @@ export class RemoteDataBuildService {
error, error,
payload payload
); );
}) }))
} }
aggregatePaginatedList<T>(input: Observable<RemoteData<T[]>>, pageInfo: PageInfo): Observable<RemoteData<PaginatedList<T>>> { private toPaginatedList<T>(input: Observable<RemoteData<T[] | PaginatedList<T>>>, pageInfo: PageInfo): Observable<RemoteData<PaginatedList<T>>> {
return input.map((rd) => Object.assign(rd, {payload: new PaginatedList(pageInfo, rd.payload)})); return input.pipe(
map((rd: RemoteData<T[] | PaginatedList<T>>) => {
if (Array.isArray(rd.payload)) {
return Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload) })
} else {
return Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload.page) });
}
})
);
} }
} }

View File

@@ -1,5 +1,5 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { provideMockActions } from '@ngrx/effects/testing'; import { provideMockActions } from '@ngrx/effects/testing';
import { cold, hot } from 'jasmine-marbles'; import { cold, hot } from 'jasmine-marbles';
import { ObjectCacheEffects } from './object-cache.effects'; import { ObjectCacheEffects } from './object-cache.effects';

View File

@@ -1,5 +1,6 @@
import { map } from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects'; import { Actions, Effect, ofType } from '@ngrx/effects';
import { StoreActionTypes } from '../../store.actions'; import { StoreActionTypes } from '../../store.actions';
import { ResetObjectCacheTimestampsAction } from './object-cache.actions'; import { ResetObjectCacheTimestampsAction } from './object-cache.actions';
@@ -16,9 +17,11 @@ export class ObjectCacheEffects {
* time ago, and will likely need to be revisited later * time ago, and will likely need to be revisited later
*/ */
@Effect() fixTimestampsOnRehydrate = this.actions$ @Effect() fixTimestampsOnRehydrate = this.actions$
.ofType(StoreActionTypes.REHYDRATE) .pipe(ofType(StoreActionTypes.REHYDRATE),
.map(() => new ResetObjectCacheTimestampsAction(new Date().getTime())); map(() => new ResetObjectCacheTimestampsAction(new Date().getTime()))
);
constructor(private actions$: Actions) { } constructor(private actions$: Actions) {
}
} }

View File

@@ -1,11 +1,13 @@
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable'; import { of as observableOf } from 'rxjs';
import { ObjectCacheService } from './object-cache.service'; import { ObjectCacheService } from './object-cache.service';
import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { ResourceType } from '../shared/resource-type'; import { ResourceType } from '../shared/resource-type';
import { NormalizedItem } from './models/normalized-item.model'; import { NormalizedItem } from './models/normalized-item.model';
import { first } from 'rxjs/operators';
import * as ngrx from '@ngrx/store';
describe('ObjectCacheService', () => { describe('ObjectCacheService', () => {
let service: ObjectCacheService; let service: ObjectCacheService;
@@ -51,10 +53,14 @@ describe('ObjectCacheService', () => {
describe('getBySelfLink', () => { describe('getBySelfLink', () => {
it('should return an observable of the cached object with the specified self link and type', () => { it('should return an observable of the cached object with the specified self link and type', () => {
spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry)); spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => {
return () => observableOf(cacheEntry);
};
});
// due to the implementation of spyOn above, this subscribe will be synchronous // due to the implementation of spyOn above, this subscribe will be synchronous
service.getBySelfLink(selfLink).take(1).subscribe((o) => { service.getBySelfLink(selfLink).pipe(first()).subscribe((o) => {
expect(o.self).toBe(selfLink); expect(o.self).toBe(selfLink);
// this only works if testObj is an instance of TestClass // this only works if testObj is an instance of TestClass
expect(o instanceof NormalizedItem).toBeTruthy(); expect(o instanceof NormalizedItem).toBeTruthy();
@@ -63,7 +69,11 @@ describe('ObjectCacheService', () => {
}); });
it('should not return a cached object that has exceeded its time to live', () => { it('should not return a cached object that has exceeded its time to live', () => {
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry)); spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => {
return () => observableOf(invalidCacheEntry);
};
});
let getObsHasFired = false; let getObsHasFired = false;
const subscription = service.getBySelfLink(selfLink).subscribe((o) => getObsHasFired = true); const subscription = service.getBySelfLink(selfLink).subscribe((o) => getObsHasFired = true);
@@ -76,9 +86,9 @@ describe('ObjectCacheService', () => {
it('should return an observable of the array of cached objects with the specified self link and type', () => { it('should return an observable of the array of cached objects with the specified self link and type', () => {
const item = new NormalizedItem(); const item = new NormalizedItem();
item.self = selfLink; item.self = selfLink;
spyOn(service, 'getBySelfLink').and.returnValue(Observable.of(item)); spyOn(service, 'getBySelfLink').and.returnValue(observableOf(item));
service.getList([selfLink, selfLink]).take(1).subscribe((arr) => { service.getList([selfLink, selfLink]).pipe(first()).subscribe((arr) => {
expect(arr[0].self).toBe(selfLink); expect(arr[0].self).toBe(selfLink);
expect(arr[0] instanceof NormalizedItem).toBeTruthy(); expect(arr[0] instanceof NormalizedItem).toBeTruthy();
}); });
@@ -87,19 +97,31 @@ describe('ObjectCacheService', () => {
describe('has', () => { describe('has', () => {
it('should return true if the object with the supplied self link is cached and still valid', () => { it('should return true if the object with the supplied self link is cached and still valid', () => {
spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry)); spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => {
return () => observableOf(cacheEntry);
};
});
expect(service.hasBySelfLink(selfLink)).toBe(true); expect(service.hasBySelfLink(selfLink)).toBe(true);
}); });
it("should return false if the object with the supplied self link isn't cached", () => { it("should return false if the object with the supplied self link isn't cached", () => {
spyOn(store, 'select').and.returnValue(Observable.of(undefined)); spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => {
return () => observableOf(undefined);
};
});
expect(service.hasBySelfLink(selfLink)).toBe(false); expect(service.hasBySelfLink(selfLink)).toBe(false);
}); });
it('should return false if the object with the supplied self link is cached but has exceeded its time to live', () => { it('should return false if the object with the supplied self link is cached but has exceeded its time to live', () => {
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry)); spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => {
return () => observableOf(invalidCacheEntry);
};
});
expect(service.hasBySelfLink(selfLink)).toBe(false); expect(service.hasBySelfLink(selfLink)).toBe(false);
}); });

View File

@@ -1,16 +1,16 @@
import { Injectable } from '@angular/core'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { MemoizedSelector, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable'; import { distinctUntilChanged, filter, first, map, mergeMap, take } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { MemoizedSelector, select, Store } from '@ngrx/store';
import { IndexName } from '../index/index.reducer'; import { IndexName } from '../index/index.reducer';
import { ObjectCacheEntry, CacheableObject } from './object-cache.reducer'; import { CacheableObject, ObjectCacheEntry } from './object-cache.reducer';
import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions';
import { hasNoValue } from '../../shared/empty.util'; import { hasNoValue } from '../../shared/empty.util';
import { GenericConstructor } from '../shared/generic-constructor'; import { GenericConstructor } from '../shared/generic-constructor';
import { coreSelector, CoreState } from '../core.reducers'; import { coreSelector, CoreState } from '../core.reducers';
import { pathSelector } from '../shared/selectors'; import { pathSelector } from '../shared/selectors';
import { Item } from '../shared/item.model';
import { NormalizedObjectFactory } from './models/normalized-object-factory'; import { NormalizedObjectFactory } from './models/normalized-object-factory';
import { NormalizedObject } from './models/normalized-object.model'; import { NormalizedObject } from './models/normalized-object.model';
@@ -73,33 +73,40 @@ export class ObjectCacheService {
* An observable of the requested object * An observable of the requested object
*/ */
getByUUID<T extends NormalizedObject>(uuid: string): Observable<T> { getByUUID<T extends NormalizedObject>(uuid: string): Observable<T> {
return this.store.select(selfLinkFromUuidSelector(uuid)) return this.store.pipe(
.flatMap((selfLink: string) => this.getBySelfLink(selfLink)) select(selfLinkFromUuidSelector(uuid)),
mergeMap((selfLink: string) => this.getBySelfLink(selfLink)
)
)
} }
getBySelfLink<T extends NormalizedObject>(selfLink: string): Observable<T> { getBySelfLink<T extends NormalizedObject>(selfLink: string): Observable<T> {
return this.getEntry(selfLink) return this.getEntry(selfLink).pipe(
.map((entry: ObjectCacheEntry) => { map((entry: ObjectCacheEntry) => {
const type: GenericConstructor<NormalizedObject>= NormalizedObjectFactory.getConstructor(entry.data.type); const type: GenericConstructor<NormalizedObject> = NormalizedObjectFactory.getConstructor(entry.data.type);
return Object.assign(new type(), entry.data) as T return Object.assign(new type(), entry.data) as T
}); }));
} }
private getEntry(selfLink: string): Observable<ObjectCacheEntry> { private getEntry(selfLink: string): Observable<ObjectCacheEntry> {
return this.store.select(entryFromSelfLinkSelector(selfLink)) return this.store.pipe(
.filter((entry) => this.isValid(entry)) select(entryFromSelfLinkSelector(selfLink)),
.distinctUntilChanged(); filter((entry) => this.isValid(entry)),
distinctUntilChanged()
);
} }
getRequestHrefBySelfLink(selfLink: string): Observable<string> { getRequestHrefBySelfLink(selfLink: string): Observable<string> {
return this.getEntry(selfLink) return this.getEntry(selfLink).pipe(
.map((entry: ObjectCacheEntry) => entry.requestHref) map((entry: ObjectCacheEntry) => entry.requestHref),
.distinctUntilChanged(); distinctUntilChanged(),);
} }
getRequestHrefByUUID(uuid: string): Observable<string> { getRequestHrefByUUID(uuid: string): Observable<string> {
return this.store.select(selfLinkFromUuidSelector(uuid)) return this.store.pipe(
.flatMap((selfLink: string) => this.getRequestHrefBySelfLink(selfLink)); select(selfLinkFromUuidSelector(uuid)),
mergeMap((selfLink: string) => this.getRequestHrefBySelfLink(selfLink))
);
} }
/** /**
@@ -122,7 +129,7 @@ export class ObjectCacheService {
* @return Observable<Array<T>> * @return Observable<Array<T>>
*/ */
getList<T extends NormalizedObject>(selfLinks: string[]): Observable<T[]> { getList<T extends NormalizedObject>(selfLinks: string[]): Observable<T[]> {
return Observable.combineLatest( return observableCombineLatest(
selfLinks.map((selfLink: string) => this.getBySelfLink<T>(selfLink)) selfLinks.map((selfLink: string) => this.getBySelfLink<T>(selfLink))
); );
} }
@@ -139,9 +146,10 @@ export class ObjectCacheService {
hasByUUID(uuid: string): boolean { hasByUUID(uuid: string): boolean {
let result: boolean; let result: boolean;
this.store.select(selfLinkFromUuidSelector(uuid)) this.store.pipe(
.take(1) select(selfLinkFromUuidSelector(uuid)),
.subscribe((selfLink: string) => result = this.hasBySelfLink(selfLink)); first()
).subscribe((selfLink: string) => result = this.hasBySelfLink(selfLink));
return result; return result;
} }
@@ -158,9 +166,9 @@ export class ObjectCacheService {
hasBySelfLink(selfLink: string): boolean { hasBySelfLink(selfLink: string): boolean {
let result = false; let result = false;
this.store.select(entryFromSelfLinkSelector(selfLink)) this.store.pipe(select(entryFromSelfLinkSelector(selfLink)),
.take(1) first()
.subscribe((entry: ObjectCacheEntry) => result = this.isValid(entry)); ).subscribe((entry: ObjectCacheEntry) => result = this.isValid(entry));
return result; return result;
} }

View File

@@ -1,5 +1,5 @@
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { provideMockActions } from '@ngrx/effects/testing'; import { provideMockActions } from '@ngrx/effects/testing';
import { cold, hot } from 'jasmine-marbles'; import { cold, hot } from 'jasmine-marbles';
import { StoreActionTypes } from '../../store.actions'; import { StoreActionTypes } from '../../store.actions';

View File

@@ -1,5 +1,6 @@
import { map } from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Actions, Effect } from '@ngrx/effects'; import { Actions, Effect, ofType } from '@ngrx/effects';
import { ResetResponseCacheTimestampsAction } from './response-cache.actions'; import { ResetResponseCacheTimestampsAction } from './response-cache.actions';
import { StoreActionTypes } from '../../store.actions'; import { StoreActionTypes } from '../../store.actions';
@@ -16,9 +17,11 @@ export class ResponseCacheEffects {
* time ago, and will likely need to be revisited later * time ago, and will likely need to be revisited later
*/ */
@Effect() fixTimestampsOnRehydrate = this.actions$ @Effect() fixTimestampsOnRehydrate = this.actions$
.ofType(StoreActionTypes.REHYDRATE) .pipe(ofType(StoreActionTypes.REHYDRATE),
.map(() => new ResetResponseCacheTimestampsAction(new Date().getTime())); map(() => new ResetResponseCacheTimestampsAction(new Date().getTime()))
);
constructor(private actions$: Actions, ) { } constructor(private actions$: Actions,) {
}
} }

View File

@@ -1,11 +1,13 @@
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { ResponseCacheService } from './response-cache.service'; import { ResponseCacheService } from './response-cache.service';
import { Observable } from 'rxjs/Observable'; import { of as observableOf } from 'rxjs';
import 'rxjs/add/observable/of';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { RestResponse } from './response-cache.models'; import { RestResponse } from './response-cache.models';
import { ResponseCacheEntry } from './response-cache.reducer'; import { ResponseCacheEntry } from './response-cache.reducer';
import { first } from 'rxjs/operators';
import * as ngrx from '@ngrx/store'
import { cold } from 'jasmine-marbles';
describe('ResponseCacheService', () => { describe('ResponseCacheService', () => {
let service: ResponseCacheService; let service: ResponseCacheService;
@@ -41,20 +43,23 @@ describe('ResponseCacheService', () => {
describe('get', () => { describe('get', () => {
it('should return an observable of the cached request with the specified key', () => { it('should return an observable of the cached request with the specified key', () => {
spyOn(store, 'select').and.callFake((...args: any[]) => { spyOnProperty(ngrx, 'select').and.callFake(() => {
return Observable.of(validCacheEntry(keys[1])); return () => {
return () => observableOf(validCacheEntry(keys[1]));
};
}); });
let testObj: ResponseCacheEntry; let testObj: ResponseCacheEntry;
service.get(keys[1]).first().subscribe((entry) => { service.get(keys[1]).pipe(first()).subscribe((entry) => {
testObj = entry; testObj = entry;
}); });
expect(testObj.key).toEqual(keys[1]); expect(testObj.key).toEqual(keys[1]);
}); });
it('should not return a cached request that has exceeded its time to live', () => { it('should not return a cached request that has exceeded its time to live', () => {
spyOn(store, 'select').and.callFake((...args: any[]) => { spyOnProperty(ngrx, 'select').and.callFake(() => {
return Observable.of(invalidCacheEntry(keys[1])); return () => {
return () => observableOf(invalidCacheEntry(keys[1]));
};
}); });
let getObsHasFired = false; let getObsHasFired = false;
@@ -66,17 +71,29 @@ describe('ResponseCacheService', () => {
describe('has', () => { describe('has', () => {
it('should return true if the request with the supplied key is cached and still valid', () => { it('should return true if the request with the supplied key is cached and still valid', () => {
spyOn(store, 'select').and.returnValue(Observable.of(validCacheEntry(keys[1]))); spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => {
return () => observableOf(validCacheEntry(keys[1]));
};
});
expect(service.has(keys[1])).toBe(true); expect(service.has(keys[1])).toBe(true);
}); });
it('should return false if the request with the supplied key isn\'t cached', () => { it('should return false if the request with the supplied key isn\'t cached', () => {
spyOn(store, 'select').and.returnValue(Observable.of(undefined)); spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => {
return () => observableOf(undefined);
};
});
expect(service.has(keys[1])).toBe(false); expect(service.has(keys[1])).toBe(false);
}); });
it('should return false if the request with the supplied key is cached but has exceeded its time to live', () => { it('should return false if the request with the supplied key is cached but has exceeded its time to live', () => {
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry(keys[1]))); spyOnProperty(ngrx, 'select').and.callFake(() => {
return () => {
return () => observableOf(invalidCacheEntry(keys[1]));
};
});
expect(service.has(keys[1])).toBe(false); expect(service.has(keys[1])).toBe(false);
}); });
}); });

View File

@@ -1,7 +1,8 @@
import { filter, take, distinctUntilChanged, first } from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { MemoizedSelector, Store } from '@ngrx/store'; import { MemoizedSelector, select, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { ResponseCacheEntry } from './response-cache.reducer'; import { ResponseCacheEntry } from './response-cache.reducer';
import { hasNoValue } from '../../shared/empty.util'; import { hasNoValue } from '../../shared/empty.util';
@@ -21,7 +22,8 @@ function entryFromKeySelector(key: string): MemoizedSelector<CoreState, Response
export class ResponseCacheService { export class ResponseCacheService {
constructor( constructor(
private store: Store<CoreState> private store: Store<CoreState>
) { } ) {
}
add(key: string, response: RestResponse, msToLive: number): Observable<ResponseCacheEntry> { add(key: string, response: RestResponse, msToLive: number): Observable<ResponseCacheEntry> {
if (!this.has(key)) { if (!this.has(key)) {
@@ -39,9 +41,11 @@ export class ResponseCacheService {
* an observable of the ResponseCacheEntry with the specified key * an observable of the ResponseCacheEntry with the specified key
*/ */
get(key: string): Observable<ResponseCacheEntry> { get(key: string): Observable<ResponseCacheEntry> {
return this.store.select(entryFromKeySelector(key)) return this.store.pipe(
.filter((entry: ResponseCacheEntry) => this.isValid(entry)) select(entryFromKeySelector(key)),
.distinctUntilChanged() filter((entry: ResponseCacheEntry) => this.isValid(entry)),
distinctUntilChanged()
)
} }
/** /**
@@ -56,11 +60,11 @@ export class ResponseCacheService {
has(key: string): boolean { has(key: string): boolean {
let result: boolean; let result: boolean;
this.store.select(entryFromKeySelector(key)) this.store.pipe(select(entryFromKeySelector(key)),
.take(1) first()
.subscribe((entry: ResponseCacheEntry) => { ).subscribe((entry: ResponseCacheEntry) => {
result = this.isValid(entry); result = this.isValid(entry);
}); });
return result; return result;
} }
@@ -70,6 +74,7 @@ export class ResponseCacheService {
this.store.dispatch(new ResponseCacheRemoveAction(key)); this.store.dispatch(new ResponseCacheRemoveAction(key));
} }
} }
/** /**
* Check whether a ResponseCacheEntry should still be cached * Check whether a ResponseCacheEntry should still be cached
* *

View File

@@ -1,5 +1,5 @@
import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/Rx'; import { TestScheduler } from 'rxjs/testing';
import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { ConfigService } from './config.service'; import { ConfigService } from './config.service';
@@ -56,11 +56,11 @@ describe('ConfigService', () => {
} }
beforeEach(() => { beforeEach(() => {
scheduler = getTestScheduler();
responseCache = initMockResponseCacheService(true); responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService(); requestService = getMockRequestService();
service = initTestService();
scheduler = getTestScheduler();
halService = new HALEndpointServiceStub(configEndpoint); halService = new HALEndpointServiceStub(configEndpoint);
service = initTestService();
}); });
describe('getConfigByHref', () => { describe('getConfigByHref', () => {

View File

@@ -1,8 +1,8 @@
import { Observable } from 'rxjs/Observable'; import { Observable, of as observableOf, throwError as observableThrowError, merge as observableMerge } from 'rxjs';
import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators';
import { RequestService } from '../data/request.service'; import { RequestService } from '../data/request.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; import { ConfigSuccessResponse } from '../cache/response-cache.models';
import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.models'; import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
@@ -18,16 +18,17 @@ export abstract class ConfigService {
protected abstract halService: HALEndpointService; protected abstract halService: HALEndpointService;
protected getConfig(request: RestRequest): Observable<ConfigData> { protected getConfig(request: RestRequest): Observable<ConfigData> {
const [successResponse, errorResponse] = this.responseCache.get(request.href) const responses = this.responseCache.get(request.href).pipe(map((entry: ResponseCacheEntry) => entry.response));
.map((entry: ResponseCacheEntry) => entry.response) const errorResponses = responses.pipe(
.partition((response: RestResponse) => response.isSuccessful); filter((response) => !response.isSuccessful),
return Observable.merge( mergeMap(() => observableThrowError(new Error(`Couldn't retrieve the config`)))
errorResponse.flatMap((response: ErrorResponse) => );
Observable.throw(new Error(`Couldn't retrieve the config`))), const successResponses = responses.pipe(
successResponse filter((response) => response.isSuccessful && isNotEmpty(response) && isNotEmpty((response as ConfigSuccessResponse).configDefinition)),
.filter((response: ConfigSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.configDefinition)) map((response: ConfigSuccessResponse) => new ConfigData(response.pageInfo, response.configDefinition))
.map((response: ConfigSuccessResponse) => new ConfigData(response.pageInfo, response.configDefinition)) );
.distinctUntilChanged()); return observableMerge(errorResponses, successResponses);
} }
protected getConfigByNameHref(endpoint, resourceName): string { protected getConfigByNameHref(endpoint, resourceName): string {
@@ -65,13 +66,13 @@ export abstract class ConfigService {
} }
public getConfigAll(): Observable<ConfigData> { public getConfigAll(): Observable<ConfigData> {
return this.halService.getEndpoint(this.linkPath) return this.halService.getEndpoint(this.linkPath).pipe(
.filter((href: string) => isNotEmpty(href)) filter((href: string) => isNotEmpty(href)),
.distinctUntilChanged() distinctUntilChanged(),
.map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)) map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)),
.do((request: RestRequest) => this.requestService.configure(request)) tap((request: RestRequest) => this.requestService.configure(request)),
.flatMap((request: RestRequest) => this.getConfig(request)) mergeMap((request: RestRequest) => this.getConfig(request)),
.distinctUntilChanged(); distinctUntilChanged());
} }
public getConfigByHref(href: string): Observable<ConfigData> { public getConfigByHref(href: string): Observable<ConfigData> {
@@ -82,25 +83,26 @@ export abstract class ConfigService {
} }
public getConfigByName(name: string): Observable<ConfigData> { public getConfigByName(name: string): Observable<ConfigData> {
return this.halService.getEndpoint(this.linkPath) return this.halService.getEndpoint(this.linkPath).pipe(
.map((endpoint: string) => this.getConfigByNameHref(endpoint, name)) map((endpoint: string) => this.getConfigByNameHref(endpoint, name)),
.filter((href: string) => isNotEmpty(href)) filter((href: string) => isNotEmpty(href)),
.distinctUntilChanged() distinctUntilChanged(),
.map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)) map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)),
.do((request: RestRequest) => this.requestService.configure(request)) tap((request: RestRequest) => this.requestService.configure(request)),
.flatMap((request: RestRequest) => this.getConfig(request)) mergeMap((request: RestRequest) => this.getConfig(request)),
.distinctUntilChanged(); distinctUntilChanged());
} }
public getConfigBySearch(options: FindAllOptions = {}): Observable<ConfigData> { public getConfigBySearch(options: FindAllOptions = {}): Observable<ConfigData> {
return this.halService.getEndpoint(this.linkPath) console.log(this.halService.getEndpoint(this.linkPath));
.map((endpoint: string) => this.getConfigSearchHref(endpoint, options)) return this.halService.getEndpoint(this.linkPath).pipe(
.filter((href: string) => isNotEmpty(href)) map((endpoint: string) => this.getConfigSearchHref(endpoint, options)),
.distinctUntilChanged() filter((href: string) => isNotEmpty(href)),
.map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)) distinctUntilChanged(),
.do((request: RestRequest) => this.requestService.configure(request)) map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)),
.flatMap((request: RestRequest) => this.getConfig(request)) tap((request: RestRequest) => this.requestService.configure(request)),
.distinctUntilChanged(); mergeMap((request: RestRequest) => this.getConfig(request)),
distinctUntilChanged());
} }
} }

View File

@@ -127,7 +127,7 @@ export abstract class BaseResponseParsingService {
} }
processPageInfo(payload: any): PageInfo { processPageInfo(payload: any): PageInfo {
if (isNotEmpty(payload.page)) { if (hasValue(payload.page)) {
const pageObj = Object.assign({}, payload.page, { _links: payload._links }); const pageObj = Object.assign({}, payload.page, { _links: payload._links });
const pageInfoObject = new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj); const pageInfoObject = new DSpaceRESTv2Serializer(PageInfo).deserialize(pageObj);
if (pageInfoObject.currentPage >= 0) { if (pageInfoObject.currentPage >= 0) {

View File

@@ -10,134 +10,148 @@ describe('BrowseResponseParsingService', () => {
beforeEach(() => { beforeEach(() => {
service = new BrowseResponseParsingService(); service = new BrowseResponseParsingService();
}); });
let validRequest;
let validResponse;
let invalidResponse1;
let invalidResponse2;
let invalidResponse3;
let definitions;
describe('parse', () => { describe('parse', () => {
const validRequest = new BrowseEndpointRequest('client/b186e8ce-e99c-4183-bc9a-42b4821bdb78', 'https://rest.api/discover/browses'); beforeEach(() => {
validRequest = new BrowseEndpointRequest('client/b186e8ce-e99c-4183-bc9a-42b4821bdb78', 'https://rest.api/discover/browses');
const validResponse = { validResponse = {
payload: { payload: {
_embedded: { _embedded: {
browses: [{ browses: [{
metadataBrowse: false, metadataBrowse: false,
sortOptions: [{ name: 'title', metadata: 'dc.title' }, { sortOptions: [{ name: 'title', metadata: 'dc.title' }, {
name: 'dateissued',
metadata: 'dc.date.issued'
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }],
order: 'ASC',
type: 'browse',
metadata: ['dc.date.issued'],
_links: {
self: { href: 'https://rest.api/discover/browses/dateissued' },
items: { href: 'https://rest.api/discover/browses/dateissued/items' }
}
}, {
metadataBrowse: true,
sortOptions: [{ name: 'title', metadata: 'dc.title' }, {
name: 'dateissued',
metadata: 'dc.date.issued'
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }],
order: 'ASC',
type: 'browse',
metadata: ['dc.contributor.*', 'dc.creator'],
_links: {
self: { href: 'https://rest.api/discover/browses/author' },
entries: { href: 'https://rest.api/discover/browses/author/entries' },
items: { href: 'https://rest.api/discover/browses/author/items' }
}
}]
},
_links: { self: { href: 'https://rest.api/discover/browses' } },
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
}, statusCode: 200, statusText: 'OK'
} as DSpaceRESTV2Response;
invalidResponse1 = {
payload: {
_embedded: {
browse: {
metadataBrowse: false,
sortOptions: [{ name: 'title', metadata: 'dc.title' }, {
name: 'dateissued',
metadata: 'dc.date.issued'
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }],
order: 'ASC',
type: 'browse',
metadata: ['dc.date.issued'],
_links: {
self: { href: 'https://rest.api/discover/browses/dateissued' },
items: { href: 'https://rest.api/discover/browses/dateissued/items' }
}
}
},
_links: { self: { href: 'https://rest.api/discover/browses' } },
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
}, statusCode: 200, statusText: 'OK'
} as DSpaceRESTV2Response;
invalidResponse2 = {
payload: {
_links: { self: { href: 'https://rest.api/discover/browses' } },
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
}, statusCode: 200, statusText: 'OK'
} as DSpaceRESTV2Response;
invalidResponse3 = {
payload: {
_links: { self: { href: 'https://rest.api/discover/browses' } },
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
}, statusCode: 500, statusText: 'Internal Server Error'
} as DSpaceRESTV2Response;
definitions = [
Object.assign(new BrowseDefinition(), {
metadataBrowse: false,
sortOptions: [
{
name: 'title',
metadata: 'dc.title'
},
{
name: 'dateissued', name: 'dateissued',
metadata: 'dc.date.issued' metadata: 'dc.date.issued'
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }], },
order: 'ASC', {
type: 'browse', name: 'dateaccessioned',
metadata: ['dc.date.issued'], metadata: 'dc.date.accessioned'
_links: {
self: { href: 'https://rest.api/discover/browses/dateissued' },
items: { href: 'https://rest.api/discover/browses/dateissued/items' }
} }
}, { ],
metadataBrowse: true, defaultSortOrder: 'ASC',
sortOptions: [{ name: 'title', metadata: 'dc.title' }, { type: 'browse',
metadataKeys: [
'dc.date.issued'
],
_links: {
self: 'https://rest.api/discover/browses/dateissued',
items: 'https://rest.api/discover/browses/dateissued/items'
}
}),
Object.assign(new BrowseDefinition(), {
metadataBrowse: true,
sortOptions: [
{
name: 'title',
metadata: 'dc.title'
},
{
name: 'dateissued', name: 'dateissued',
metadata: 'dc.date.issued' metadata: 'dc.date.issued'
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }], },
order: 'ASC', {
type: 'browse', name: 'dateaccessioned',
metadata: ['dc.contributor.*', 'dc.creator'], metadata: 'dc.date.accessioned'
_links: {
self: { href: 'https://rest.api/discover/browses/author' },
entries: { href: 'https://rest.api/discover/browses/author/entries' },
items: { href: 'https://rest.api/discover/browses/author/items' }
}
}]
},
_links: { self: { href: 'https://rest.api/discover/browses' } },
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
}, statusCode: 200, statusText: 'OK'
} as DSpaceRESTV2Response;
const invalidResponse1 = {
payload: {
_embedded: {
browse: {
metadataBrowse: false,
sortOptions: [{ name: 'title', metadata: 'dc.title' }, {
name: 'dateissued',
metadata: 'dc.date.issued'
}, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }],
order: 'ASC',
type: 'browse',
metadata: ['dc.date.issued'],
_links: {
self: { href: 'https://rest.api/discover/browses/dateissued' },
items: { href: 'https://rest.api/discover/browses/dateissued/items' }
} }
],
defaultSortOrder: 'ASC',
type: 'browse',
metadataKeys: [
'dc.contributor.*',
'dc.creator'
],
_links: {
self: 'https://rest.api/discover/browses/author',
entries: 'https://rest.api/discover/browses/author/entries',
items: 'https://rest.api/discover/browses/author/items'
} }
}, })
_links: { self: { href: 'https://rest.api/discover/browses' } }, ];
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } });
}, statusCode: 200, statusText: 'OK'
} as DSpaceRESTV2Response;
const invalidResponse2 = {
payload: {
_links: { self: { href: 'https://rest.api/discover/browses' } },
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
}, statusCode: 200, statusText: 'OK'
} as DSpaceRESTV2Response ;
const invalidResponse3 = {
payload: {
_links: { self: { href: 'https://rest.api/discover/browses' } },
page: { size: 20, totalElements: 2, totalPages: 1, number: 0 }
}, statusCode: 500, statusText: 'Internal Server Error'
} as DSpaceRESTV2Response;
const definitions = [
Object.assign(new BrowseDefinition(), {
metadataBrowse: false,
sortOptions: [
{
name: 'title',
metadata: 'dc.title'
},
{
name: 'dateissued',
metadata: 'dc.date.issued'
},
{
name: 'dateaccessioned',
metadata: 'dc.date.accessioned'
}
],
defaultSortOrder: 'ASC',
type: 'browse',
metadataKeys: [
'dc.date.issued'
],
_links: { }
}),
Object.assign(new BrowseDefinition(), {
metadataBrowse: true,
sortOptions: [
{
name: 'title',
metadata: 'dc.title'
},
{
name: 'dateissued',
metadata: 'dc.date.issued'
},
{
name: 'dateaccessioned',
metadata: 'dc.date.accessioned'
}
],
defaultSortOrder: 'ASC',
type: 'browse',
metadataKeys: [
'dc.contributor.*',
'dc.creator'
],
_links: { }
})
];
it('should return a GenericSuccessResponse if data contains a valid browse endpoint response', () => { it('should return a GenericSuccessResponse if data contains a valid browse endpoint response', () => {
const response = service.parse(validRequest, validResponse); const response = service.parse(validRequest, validResponse);
expect(response.constructor).toBe(GenericSuccessResponse); expect(response.constructor).toBe(GenericSuccessResponse);

View File

@@ -1,10 +1,9 @@
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/Rx'; import { TestScheduler } from 'rxjs/testing';
import { GlobalConfig } from '../../../config'; import { GlobalConfig } from '../../../config';
import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedCommunity } from '../cache/models/normalized-community.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';

View File

@@ -1,9 +1,8 @@
import { Observable } from 'rxjs/Observable'; import { distinctUntilChanged, filter, map, mergeMap, take, tap } from 'rxjs/operators';
import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs';
import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { NormalizedCommunity } from '../cache/models/normalized-community.model';
import { CacheableObject } from '../cache/object-cache.reducer';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { CommunityDataService } from './community-data.service'; import { CommunityDataService } from './community-data.service';
@@ -12,7 +11,7 @@ import { FindAllOptions, FindByIDRequest } from './request.models';
import { NormalizedObject } from '../cache/models/normalized-object.model'; import { NormalizedObject } from '../cache/models/normalized-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
export abstract class ComColDataService<TNormalized extends NormalizedObject, TDomain> extends DataService<TNormalized, TDomain> { export abstract class ComColDataService<TNormalized extends NormalizedObject, TDomain> extends DataService<TNormalized, TDomain> {
protected abstract cds: CommunityDataService; protected abstract cds: CommunityDataService;
protected abstract objectCache: ObjectCacheService; protected abstract objectCache: ObjectCacheService;
protected abstract halService: HALEndpointService; protected abstract halService: HALEndpointService;
@@ -31,29 +30,30 @@ export abstract class ComColDataService<TNormalized extends NormalizedObject, TD
if (isEmpty(options.scopeID)) { if (isEmpty(options.scopeID)) {
return this.halService.getEndpoint(this.linkPath); return this.halService.getEndpoint(this.linkPath);
} else { } else {
const scopeCommunityHrefObs = this.cds.getEndpoint() const scopeCommunityHrefObs = this.cds.getEndpoint().pipe(
.flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, options.scopeID)) mergeMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, options.scopeID)),
.filter((href: string) => isNotEmpty(href)) filter((href: string) => isNotEmpty(href)),
.take(1) take(1),
.do((href: string) => { tap((href: string) => {
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, options.scopeID); const request = new FindByIDRequest(this.requestService.generateRequestId(), href, options.scopeID);
this.requestService.configure(request); this.requestService.configure(request);
}); }));
const [successResponse, errorResponse] = scopeCommunityHrefObs const responses = scopeCommunityHrefObs.pipe(
.flatMap((href: string) => this.responseCache.get(href)) mergeMap((href: string) => this.responseCache.get(href)),
.map((entry: ResponseCacheEntry) => entry.response) map((entry: ResponseCacheEntry) => entry.response));
.share() const errorResponses = responses.pipe(
.partition((response: RestResponse) => response.isSuccessful); filter((response) => !response.isSuccessful),
mergeMap(() => observableThrowError(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`)))
);
const successResponses = responses.pipe(
filter((response) => response.isSuccessful),
mergeMap(() => this.objectCache.getByUUID(options.scopeID)),
map((nc: NormalizedCommunity) => nc._links[this.linkPath]),
filter((href) => isNotEmpty(href))
);
return Observable.merge( return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged());
errorResponse.flatMap((response: ErrorResponse) =>
Observable.throw(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`))),
successResponse
.flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(options.scopeID))
.map((nc: NormalizedCommunity) => nc._links[this.linkPath])
.filter((href) => isNotEmpty(href))
).distinctUntilChanged();
} }
} }
} }

View File

@@ -1,3 +1,5 @@
import {mergeMap, filter, take} from 'rxjs/operators';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
@@ -13,7 +15,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import { FindAllOptions, FindAllRequest } from './request.models'; import { FindAllOptions, FindAllRequest } from './request.models';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { PaginatedList } from './paginated-list'; import { PaginatedList } from './paginated-list';
@Injectable() @Injectable()
@@ -39,11 +41,12 @@ export class CommunityDataService extends ComColDataService<NormalizedCommunity,
} }
findTop(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<Community>>> { findTop(options: FindAllOptions = {}): Observable<RemoteData<PaginatedList<Community>>> {
const hrefObs = this.getFindAllHref(options); const hrefObs = this.halService.getEndpoint(this.topLinkPath).pipe(filter((href: string) => isNotEmpty(href)),
mergeMap((endpoint: string) => this.getFindAllHref(options)),);
hrefObs hrefObs.pipe(
.filter((href: string) => hasValue(href)) filter((href: string) => hasValue(href)),
.take(1) take(1),)
.subscribe((href: string) => { .subscribe((href: string) => {
const request = new FindAllRequest(this.requestService.generateRequestId(), href, options); const request = new FindAllRequest(this.requestService.generateRequestId(), href, options);
this.requestService.configure(request); this.requestService.configure(request);

View File

@@ -6,10 +6,10 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import 'rxjs/add/observable/of';
import { FindAllOptions } from './request.models'; import { FindAllOptions } from './request.models';
import { SortOptions, SortDirection } from '../cache/models/sort-options.model'; import { SortOptions, SortDirection } from '../cache/models/sort-options.model';
import { of as observableOf } from 'rxjs';
const endpoint = 'https://rest.api/core'; const endpoint = 'https://rest.api/core';
@@ -18,108 +18,108 @@ class NormalizedTestObject extends NormalizedObject {
} }
class TestService extends DataService<NormalizedTestObject, any> { class TestService extends DataService<NormalizedTestObject, any> {
protected forceBypassCache = false; protected forceBypassCache = false;
constructor( constructor(
protected responseCache: ResponseCacheService, protected responseCache: ResponseCacheService,
protected requestService: RequestService, protected requestService: RequestService,
protected rdbService: RemoteDataBuildService, protected rdbService: RemoteDataBuildService,
protected store: Store<CoreState>, protected store: Store<CoreState>,
protected linkPath: string, protected linkPath: string,
protected halService: HALEndpointService protected halService: HALEndpointService
) { ) {
super(); super();
} }
public getBrowseEndpoint(options: FindAllOptions): Observable<string> { public getBrowseEndpoint(options: FindAllOptions): Observable<string> {
return Observable.of(endpoint); return observableOf(endpoint);
} }
} }
describe('DataService', () => { describe('DataService', () => {
let service: TestService; let service: TestService;
let options: FindAllOptions; let options: FindAllOptions;
const responseCache = {} as ResponseCacheService; const responseCache = {} as ResponseCacheService;
const requestService = {} as RequestService; const requestService = {} as RequestService;
const halService = {} as HALEndpointService; const halService = {} as HALEndpointService;
const rdbService = {} as RemoteDataBuildService; const rdbService = {} as RemoteDataBuildService;
const store = {} as Store<CoreState>; const store = {} as Store<CoreState>;
function initTestService(): TestService { function initTestService(): TestService {
return new TestService( return new TestService(
responseCache, responseCache,
requestService, requestService,
rdbService, rdbService,
store, store,
endpoint, endpoint,
halService halService
); );
} }
service = initTestService(); service = initTestService();
describe('getFindAllHref', () => { describe('getFindAllHref', () => {
it('should return an observable with the endpoint', () => { it('should return an observable with the endpoint', () => {
options = {}; options = {};
(service as any).getFindAllHref(options).subscribe((value) => { (service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(endpoint); expect(value).toBe(endpoint);
} }
); );
});
it('should include page in href if currentPage provided in options', () => {
options = { currentPage: 2 };
const expected = `${endpoint}?page=${options.currentPage - 1}`;
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
});
it('should include size in href if elementsPerPage provided in options', () => {
options = { elementsPerPage: 5 };
const expected = `${endpoint}?size=${options.elementsPerPage}`;
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
});
it('should include sort href if SortOptions provided in options', () => {
const sortOptions = new SortOptions('field1', SortDirection.ASC);
options = { sort: sortOptions};
const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`;
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
});
it('should include startsWith in href if startsWith provided in options', () => {
options = { startsWith: 'ab' };
const expected = `${endpoint}?startsWith=${options.startsWith}`;
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
});
it('should include all provided options in href', () => {
const sortOptions = new SortOptions('field1', SortDirection.DESC)
options = {
currentPage: 6,
elementsPerPage: 10,
sort: sortOptions,
startsWith: 'ab'
}
const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` +
`&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`;
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
})
}); });
it('should include page in href if currentPage provided in options', () => {
options = { currentPage: 2 };
const expected = `${endpoint}?page=${options.currentPage - 1}`;
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
});
it('should include size in href if elementsPerPage provided in options', () => {
options = { elementsPerPage: 5 };
const expected = `${endpoint}?size=${options.elementsPerPage}`;
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
});
it('should include sort href if SortOptions provided in options', () => {
const sortOptions = new SortOptions('field1', SortDirection.ASC);
options = { sort: sortOptions };
const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`;
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
});
it('should include startsWith in href if startsWith provided in options', () => {
options = { startsWith: 'ab' };
const expected = `${endpoint}?startsWith=${options.startsWith}`;
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
});
it('should include all provided options in href', () => {
const sortOptions = new SortOptions('field1', SortDirection.DESC)
options = {
currentPage: 6,
elementsPerPage: 10,
sort: sortOptions,
startsWith: 'ab'
}
const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` +
`&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`;
(service as any).getFindAllHref(options).subscribe((value) => {
expect(value).toBe(expected);
});
})
});
}); });

View File

@@ -1,6 +1,6 @@
import { filter, take } from 'rxjs/operators'; import { distinctUntilChanged, filter, take, first, map } from 'rxjs/operators';
import { of as observableOf, Observable } from 'rxjs';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
@@ -30,7 +30,7 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
let result: Observable<string>; let result: Observable<string>;
const args = []; const args = [];
result = this.getBrowseEndpoint(options).distinctUntilChanged(); result = this.getBrowseEndpoint(options).pipe(distinctUntilChanged());
if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { if (hasValue(options.currentPage) && typeof options.currentPage === 'number') {
/* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */ /* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */
@@ -50,7 +50,7 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
} }
if (isNotEmpty(args)) { if (isNotEmpty(args)) {
return result.map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString()); return result.pipe(map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString()));
} else { } else {
return result; return result;
} }
@@ -111,11 +111,11 @@ export abstract class DataService<TNormalized extends NormalizedObject, TDomain>
} }
findById(id: string): Observable<RemoteData<TDomain>> { findById(id: string): Observable<RemoteData<TDomain>> {
const hrefObs = this.halService.getEndpoint(this.linkPath) const hrefObs = this.halService.getEndpoint(this.linkPath).pipe(
.map((endpoint: string) => this.getFindByIDHref(endpoint, id)); map((endpoint: string) => this.getFindByIDHref(endpoint, id)));
hrefObs hrefObs.pipe(
.first((href: string) => hasValue(href)) first((href: string) => hasValue(href)))
.subscribe((href: string) => { .subscribe((href: string) => {
const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id); const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id);
this.requestService.configure(request, this.forceBypassCache); this.requestService.configure(request, this.forceBypassCache);

View File

@@ -1,5 +1,5 @@
import { cold, getTestScheduler } from 'jasmine-marbles'; import { cold, getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from '../../../../node_modules/rxjs'; import { TestScheduler } from 'rxjs/testing';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { DSpaceObject } from '../shared/dspace-object.model'; import { DSpaceObject } from '../shared/dspace-object.model';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';

View File

@@ -1,6 +1,6 @@
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model'; import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';

View File

@@ -1,6 +1,6 @@
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { cold, getTestScheduler } from 'jasmine-marbles'; import { cold, getTestScheduler } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/Rx'; import { TestScheduler } from 'rxjs/testing';
import { BrowseService } from '../browse/browse.service'; import { BrowseService } from '../browse/browse.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';

View File

@@ -1,9 +1,9 @@
import { Inject, Injectable } from '@angular/core';
import {distinctUntilChanged, map, filter} from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable'; import { Observable } from 'rxjs';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { isNotEmpty } from '../../shared/empty.util';
import { isEmpty, isNotEmpty } from '../../shared/empty.util';
import { BrowseService } from '../browse/browse.service'; import { BrowseService } from '../browse/browse.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedItem } from '../cache/models/normalized-item.model'; import { NormalizedItem } from '../cache/models/normalized-item.model';
@@ -43,10 +43,10 @@ export class ItemDataService extends DataService<NormalizedItem, Item> {
if (options.sort && options.sort.field) { if (options.sort && options.sort.field) {
field = options.sort.field; field = options.sort.field;
} }
return this.bs.getBrowseURLFor(field, this.linkPath) return this.bs.getBrowseURLFor(field, this.linkPath).pipe(
.filter((href: string) => isNotEmpty(href)) filter((href: string) => isNotEmpty(href)),
.map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}`).toString()) map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}`).toString()),
.distinctUntilChanged(); distinctUntilChanged(),);
} }
} }

View File

@@ -1,9 +1,9 @@
import {of as observableOf, Observable } from 'rxjs';
import { Inject, Injectable, Injector } from '@angular/core'; import { Inject, Injectable, Injector } from '@angular/core';
import { Request } from '@angular/http'; import { Request } from '@angular/http';
import { RequestArgs } from '@angular/http/src/interfaces'; import { RequestArgs } from '@angular/http/src/interfaces';
import { Actions, Effect, ofType } from '@ngrx/effects'; import { Actions, Effect, ofType } from '@ngrx/effects';
// tslint:disable-next-line:import-blacklist
import { Observable } from 'rxjs';
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
import { isNotEmpty } from '../../shared/empty.util'; import { isNotEmpty } from '../../shared/empty.util';
@@ -30,7 +30,8 @@ export const addToResponseCacheAndCompleteAction = (request: RestRequest, respon
@Injectable() @Injectable()
export class RequestEffects { export class RequestEffects {
@Effect() execute = this.actions$.ofType(RequestActionTypes.EXECUTE).pipe( @Effect() execute = this.actions$.pipe(
ofType(RequestActionTypes.EXECUTE),
flatMap((action: RequestExecuteAction) => { flatMap((action: RequestExecuteAction) => {
return this.requestService.getByUUID(action.payload).pipe( return this.requestService.getByUUID(action.payload).pipe(
take(1) take(1)
@@ -46,7 +47,7 @@ export class RequestEffects {
return this.restApi.request(request.method, request.href, body, request.options).pipe( return this.restApi.request(request.method, request.href, body, request.options).pipe(
map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)), map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)),
addToResponseCacheAndCompleteAction(request, this.responseCache, this.EnvConfig), addToResponseCacheAndCompleteAction(request, this.responseCache, this.EnvConfig),
catchError((error: RequestError) => Observable.of(new ErrorResponse(error)).pipe( catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe(
addToResponseCacheAndCompleteAction(request, this.responseCache, this.EnvConfig) addToResponseCacheAndCompleteAction(request, this.responseCache, this.EnvConfig)
)) ))
); );

View File

@@ -1,10 +1,9 @@
import { Store } from '@ngrx/store'; import * as ngrx from '@ngrx/store';
import { cold, hot } from 'jasmine-marbles'; import { ActionsSubject, Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import 'rxjs/add/observable/of' import { of as observableOf } from 'rxjs';
import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service';
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service'; import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
import { getMockStore } from '../../shared/mocks/mock-store';
import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service'; import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { ResponseCacheService } from '../cache/response-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service';
@@ -18,11 +17,15 @@ import {
OptionsRequest, OptionsRequest,
PatchRequest, PatchRequest,
PostRequest, PostRequest,
PutRequest, RestRequest PutRequest,
RestRequest
} from './request.models'; } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { TestScheduler } from 'rxjs/testing';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
describe('RequestService', () => { describe('RequestService', () => {
let scheduler: TestScheduler;
let service: RequestService; let service: RequestService;
let serviceAsAny: any; let serviceAsAny: any;
let objectCache: ObjectCacheService; let objectCache: ObjectCacheService;
@@ -39,19 +42,26 @@ describe('RequestService', () => {
const testOptionsRequest = new OptionsRequest(testUUID, testHref); const testOptionsRequest = new OptionsRequest(testUUID, testHref);
const testHeadRequest = new HeadRequest(testUUID, testHref); const testHeadRequest = new HeadRequest(testUUID, testHref);
const testPatchRequest = new PatchRequest(testUUID, testHref); const testPatchRequest = new PatchRequest(testUUID, testHref);
let selectSpy;
beforeEach(() => { beforeEach(() => {
scheduler = getTestScheduler();
objectCache = getMockObjectCacheService(); objectCache = getMockObjectCacheService();
(objectCache.hasBySelfLink as any).and.returnValue(false); (objectCache.hasBySelfLink as any).and.returnValue(false);
responseCache = getMockResponseCacheService(); responseCache = getMockResponseCacheService();
(responseCache.has as any).and.returnValue(false); (responseCache.has as any).and.returnValue(false);
(responseCache.get as any).and.returnValue(Observable.of(undefined)); (responseCache.get as any).and.returnValue(observableOf(undefined));
uuidService = getMockUUIDService(); uuidService = getMockUUIDService();
store = getMockStore<CoreState>(); store = new Store<CoreState>(new BehaviorSubject({}), new ActionsSubject(), null);
(store.select as any).and.returnValue(Observable.of(undefined)); selectSpy = spyOnProperty(ngrx, 'select')
selectSpy.and.callFake(() => {
return () => {
return () => cold('a', { a: undefined });
};
});
service = new RequestService( service = new RequestService(
objectCache, objectCache,
@@ -74,7 +84,7 @@ describe('RequestService', () => {
describe('isPending', () => { describe('isPending', () => {
describe('before the request is configured', () => { describe('before the request is configured', () => {
beforeEach(() => { beforeEach(() => {
spyOn(service, 'getByHref').and.returnValue(Observable.of(undefined)); spyOn(service, 'getByHref').and.returnValue(observableOf(undefined));
}); });
it('should return false', () => { it('should return false', () => {
@@ -87,7 +97,7 @@ describe('RequestService', () => {
describe('when the request has been configured but hasn\'t reached the store yet', () => { describe('when the request has been configured but hasn\'t reached the store yet', () => {
beforeEach(() => { beforeEach(() => {
spyOn(service, 'getByHref').and.returnValue(Observable.of(undefined)); spyOn(service, 'getByHref').and.returnValue(observableOf(undefined));
serviceAsAny.requestsOnTheirWayToTheStore = [testHref]; serviceAsAny.requestsOnTheirWayToTheStore = [testHref];
}); });
@@ -101,7 +111,7 @@ describe('RequestService', () => {
describe('when the request has reached the store, before the server responds', () => { describe('when the request has reached the store, before the server responds', () => {
beforeEach(() => { beforeEach(() => {
spyOn(service, 'getByHref').and.returnValue(Observable.of({ spyOn(service, 'getByHref').and.returnValue(observableOf({
completed: false completed: false
})) }))
}); });
@@ -116,7 +126,7 @@ describe('RequestService', () => {
describe('after the server responds', () => { describe('after the server responds', () => {
beforeEach(() => { beforeEach(() => {
spyOn(service, 'getByHref').and.returnValues(Observable.of({ spyOn(service, 'getByHref').and.returnValues(observableOf({
completed: true completed: true
})); }));
}); });
@@ -134,11 +144,15 @@ describe('RequestService', () => {
describe('getByUUID', () => { describe('getByUUID', () => {
describe('if the request with the specified UUID exists in the store', () => { describe('if the request with the specified UUID exists in the store', () => {
beforeEach(() => { beforeEach(() => {
(store.select as any).and.returnValues(hot('a', { selectSpy.and.callFake(() => {
a: { return () => {
completed: true return () => hot('a', {
} a: {
})); completed: true
}
});
};
});
}); });
it('should return an Observable of the RequestEntry', () => { it('should return an Observable of the RequestEntry', () => {
@@ -155,9 +169,11 @@ describe('RequestService', () => {
describe('if the request with the specified UUID doesn\'t exist in the store', () => { describe('if the request with the specified UUID doesn\'t exist in the store', () => {
beforeEach(() => { beforeEach(() => {
(store.select as any).and.returnValues(hot('a', { selectSpy.and.callFake(() => {
a: undefined return () => {
})); return () => hot('a', { a: undefined });
};
});
}); });
it('should return an Observable of undefined', () => { it('should return an Observable of undefined', () => {
@@ -175,9 +191,11 @@ describe('RequestService', () => {
describe('getByHref', () => { describe('getByHref', () => {
describe('when the request with the specified href exists in the store', () => { describe('when the request with the specified href exists in the store', () => {
beforeEach(() => { beforeEach(() => {
(store.select as any).and.returnValues(hot('a', { selectSpy.and.callFake(() => {
a: testUUID return () => {
})); return () => hot('a', { a: testUUID });
};
});
spyOn(service, 'getByUUID').and.returnValue(cold('b', { spyOn(service, 'getByUUID').and.returnValue(cold('b', {
b: { b: {
completed: true completed: true
@@ -199,9 +217,11 @@ describe('RequestService', () => {
describe('when the request with the specified href doesn\'t exist in the store', () => { describe('when the request with the specified href doesn\'t exist in the store', () => {
beforeEach(() => { beforeEach(() => {
(store.select as any).and.returnValues(hot('a', { selectSpy.and.callFake(() => {
a: undefined return () => {
})); return () => hot('a', { a: undefined });
};
});
spyOn(service, 'getByUUID').and.returnValue(cold('b', { spyOn(service, 'getByUUID').and.returnValue(cold('b', {
b: undefined b: undefined
})); }));
@@ -242,7 +262,8 @@ describe('RequestService', () => {
}); });
it('should dispatch the request', () => { it('should dispatch the request', () => {
service.configure(request); scheduler.schedule(() => service.configure(request));
scheduler.flush();
expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(request); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(request);
}); });
}); });
@@ -302,7 +323,7 @@ describe('RequestService', () => {
describe('and it\'s a DSOSuccessResponse', () => { describe('and it\'s a DSOSuccessResponse', () => {
beforeEach(() => { beforeEach(() => {
(responseCache.get as any).and.returnValues(Observable.of({ (responseCache.get as any).and.returnValues(observableOf({
response: { response: {
isSuccessful: true, isSuccessful: true,
resourceSelfLinks: [ resourceSelfLinks: [
@@ -335,7 +356,7 @@ describe('RequestService', () => {
beforeEach(() => { beforeEach(() => {
(objectCache.hasBySelfLink as any).and.returnValues(false); (objectCache.hasBySelfLink as any).and.returnValues(false);
(responseCache.has as any).and.returnValues(true); (responseCache.has as any).and.returnValues(true);
(responseCache.get as any).and.returnValues(Observable.of({ (responseCache.get as any).and.returnValues(observableOf({
response: { response: {
isSuccessful: true isSuccessful: true
} }
@@ -377,6 +398,10 @@ describe('RequestService', () => {
}); });
describe('dispatchRequest', () => { describe('dispatchRequest', () => {
beforeEach(() => {
spyOn(store, 'dispatch');
});
it('should dispatch a RequestConfigureAction', () => { it('should dispatch a RequestConfigureAction', () => {
const request = testGetRequest; const request = testGetRequest;
serviceAsAny.dispatchRequest(request); serviceAsAny.dispatchRequest(request);
@@ -431,7 +456,7 @@ describe('RequestService', () => {
describe('when the request is added to the store', () => { describe('when the request is added to the store', () => {
beforeEach(() => { beforeEach(() => {
spyOn(service, 'getByHref').and.returnValue(Observable.of({ spyOn(service, 'getByHref').and.returnValue(observableOf({
request, request,
requestPending: false, requestPending: false,
responsePending: true, responsePending: true,
@@ -440,6 +465,11 @@ describe('RequestService', () => {
}); });
it('should stop tracking the request', () => { it('should stop tracking the request', () => {
selectSpy.and.callFake(() => {
return () => {
return () => observableOf({ request });
};
});
serviceAsAny.trackRequestsOnTheirWayToTheStore(request); serviceAsAny.trackRequestsOnTheirWayToTheStore(request);
expect(service.getByHref).toHaveBeenCalledWith(request.href); expect(service.getByHref).toHaveBeenCalledWith(request.href);
expect(serviceAsAny.requestsOnTheirWayToTheStore.includes(request.href)).toBeFalsy(); expect(serviceAsAny.requestsOnTheirWayToTheStore.includes(request.href)).toBeFalsy();

Some files were not shown because too many files have changed in this diff Show More