mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-12 20:43:08 +00:00
Merge branch 'response-cache-refactoring' into w2p-55946_Item-mapping-on-item-level
Conflicts: src/app/core/data/data.service.ts src/app/core/data/item-data.service.ts src/app/shared/shared.module.ts src/app/shared/testing/active-router-stub.ts
This commit is contained in:
13
angular.json
Normal file
13
angular.json
Normal 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"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -18,9 +18,17 @@ module.exports = {
|
|||||||
// Caching settings
|
// Caching settings
|
||||||
cache: {
|
cache: {
|
||||||
// NOTE: how long should objects be cached for by default
|
// NOTE: how long should objects be cached for by default
|
||||||
msToLive: 15 * 60 * 1000, // 15 minutes
|
msToLive: {
|
||||||
|
default: 15 * 60 * 1000, // 15 minutes
|
||||||
|
exportToZip: 5 * 1000 // 5 seconds
|
||||||
|
},
|
||||||
// msToLive: 1000, // 15 minutes
|
// msToLive: 1000, // 15 minutes
|
||||||
control: 'max-age=60' // revalidate browser
|
control: 'max-age=60', // revalidate browser
|
||||||
|
autoSync: {
|
||||||
|
defaultTime: 0,
|
||||||
|
maxBufferSize: 100,
|
||||||
|
timePerMethod: {'PATCH': 3} //time in seconds
|
||||||
|
}
|
||||||
},
|
},
|
||||||
// Form settings
|
// Form settings
|
||||||
form: {
|
form: {
|
||||||
|
169
package.json
169
package.json
@@ -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 --env.production && webpack --env.aot --env.client --env.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.0.0",
|
"@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",
|
||||||
@@ -102,7 +96,9 @@
|
|||||||
"core-js": "^2.5.7",
|
"core-js": "^2.5.7",
|
||||||
"express": "4.16.2",
|
"express": "4.16.2",
|
||||||
"express-session": "1.15.6",
|
"express-session": "1.15.6",
|
||||||
|
"fast-json-patch": "^2.0.7",
|
||||||
"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",
|
||||||
@@ -112,110 +108,117 @@
|
|||||||
"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/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"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -229,7 +229,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",
|
||||||
|
@@ -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()
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
|
@@ -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');
|
||||||
|
@@ -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
|
||||||
};
|
};
|
||||||
|
@@ -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';
|
||||||
|
@@ -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');
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -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';
|
||||||
|
@@ -1,16 +1,14 @@
|
|||||||
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 { 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 { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
|
||||||
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { CommonModule } from '@angular/common';
|
import { CommonModule } from '@angular/common';
|
||||||
import { ActivatedRoute, Router } from '@angular/router';
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { MockTranslateLoader } from '../../../shared/testing/mock-translate-loader';
|
|
||||||
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';
|
||||||
@@ -68,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
|
||||||
})
|
})
|
||||||
});
|
});
|
||||||
|
@@ -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';
|
||||||
|
@@ -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, ) => {
|
||||||
|
@@ -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, ) => {
|
||||||
|
@@ -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';
|
||||||
@@ -17,7 +15,7 @@ import { Item } from '../core/shared/item.model';
|
|||||||
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
import { fadeIn, fadeInOut } from '../shared/animations/fade';
|
||||||
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../shared/empty.util';
|
||||||
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
|
||||||
import { filter, flatMap, map } from 'rxjs/operators';
|
import { filter, flatMap, map, tap } from 'rxjs/operators';
|
||||||
import { SearchService } from '../+search-page/search-service/search.service';
|
import { SearchService } from '../+search-page/search-service/search.service';
|
||||||
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
|
||||||
import { toDSpaceObjectListRD } from '../core/shared/operators';
|
import { toDSpaceObjectListRD } from '../core/shared/operators';
|
||||||
@@ -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)),
|
||||||
|
@@ -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';
|
||||||
|
@@ -24,9 +24,11 @@
|
|||||||
[content]="communityPayload.copyrightText"
|
[content]="communityPayload.copyrightText"
|
||||||
[hasInnerHtml]="true">
|
[hasInnerHtml]="true">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
|
<ds-community-page-sub-collection-list
|
||||||
|
[community]="communityPayload"></ds-community-page-sub-collection-list>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="communityRD?.hasFailed" message="{{'error.community' | translate}}"></ds-error>
|
<ds-error *ngIf="communityRD?.hasFailed" message="{{'error.community' | translate}}"></ds-error>
|
||||||
<ds-loading *ngIf="communityRD?.isLoading" message="{{'loading.community' | translate}}"></ds-loading>
|
<ds-loading *ngIf="communityRD?.isLoading"
|
||||||
|
message="{{'loading.community' | translate}}"></ds-loading>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -1,7 +1,8 @@
|
|||||||
|
import { mergeMap, filter, map, first, tap } 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',
|
||||||
@@ -24,6 +24,8 @@ import { Observable } from 'rxjs/Observable';
|
|||||||
export class CommunityPageComponent implements OnInit, OnDestroy {
|
export class CommunityPageComponent implements OnInit, OnDestroy {
|
||||||
communityRD$: Observable<RemoteData<Community>>;
|
communityRD$: Observable<RemoteData<Community>>;
|
||||||
logoRD$: Observable<RemoteData<Bitstream>>;
|
logoRD$: Observable<RemoteData<Bitstream>>;
|
||||||
|
|
||||||
|
|
||||||
private subs: Subscription[] = [];
|
private subs: Subscription[] = [];
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@@ -35,15 +37,19 @@ 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 {
|
||||||
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -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';
|
||||||
|
@@ -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';
|
||||||
|
@@ -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';
|
||||||
@@ -17,6 +17,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
|
|||||||
changeDetection: ChangeDetectionStrategy.OnPush,
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
animations: [fadeInOut]
|
animations: [fadeInOut]
|
||||||
})
|
})
|
||||||
|
|
||||||
export class TopLevelCommunityListComponent {
|
export class TopLevelCommunityListComponent {
|
||||||
communitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
|
communitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
|
||||||
config: PaginationComponentOptions;
|
config: PaginationComponentOptions;
|
||||||
|
@@ -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(() => {
|
||||||
|
@@ -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));
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -1,6 +1,8 @@
|
|||||||
<div class="simple-view-element">
|
<div class="simple-view-element">
|
||||||
|
<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>
|
||||||
<div class="simple-view-element-body">
|
</span>
|
||||||
<ng-content></ng-content>
|
<div #content class="simple-view-element-body">
|
||||||
</div>
|
<ng-content></ng-content>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -0,0 +1,54 @@
|
|||||||
|
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { Component, DebugElement } from '@angular/core';
|
||||||
|
|
||||||
|
import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-component-with-content',
|
||||||
|
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
|
||||||
|
' <div class="my content">\n' +
|
||||||
|
' </div>\n' +
|
||||||
|
'</ds-metadata-field-wrapper>'
|
||||||
|
})
|
||||||
|
class ContentComponent {}
|
||||||
|
|
||||||
|
describe('MetadataFieldWrapperComponent', () => {
|
||||||
|
let component: MetadataFieldWrapperComponent;
|
||||||
|
let fixture: ComponentFixture<MetadataFieldWrapperComponent>;
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
declarations: [MetadataFieldWrapperComponent, ContentComponent]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(MetadataFieldWrapperComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
});
|
||||||
|
|
||||||
|
const wrapperSelector = '.simple-view-element';
|
||||||
|
const labelSelector = '.simple-view-element-header';
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeDefined();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should not show a label when there is no content', () => {
|
||||||
|
component.label = 'test label';
|
||||||
|
fixture.detectChanges();
|
||||||
|
const debugLabel = fixture.debugElement.query(By.css(labelSelector));
|
||||||
|
expect(debugLabel).toBeNull();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should show a label when there is content', () => {
|
||||||
|
const parentFixture = TestBed.createComponent(ContentComponent);
|
||||||
|
parentFixture.detectChanges();
|
||||||
|
const parentComponent = parentFixture.componentInstance;
|
||||||
|
const parentNative = parentFixture.nativeElement;
|
||||||
|
const nativeLabel = parentNative.querySelector(labelSelector);
|
||||||
|
expect(nativeLabel.textContent).toContain('test label');
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -1,4 +1,5 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Metadatum } from '../../../core/shared/metadatum.model';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
|
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
|
||||||
@@ -11,7 +12,7 @@ import { Component, Input } from '@angular/core';
|
|||||||
})
|
})
|
||||||
export class MetadataValuesComponent {
|
export class MetadataValuesComponent {
|
||||||
|
|
||||||
@Input() values: any;
|
@Input() values: Metadatum[];
|
||||||
|
|
||||||
@Input() separator: string;
|
@Input() separator: string;
|
||||||
|
|
||||||
|
@@ -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(
|
||||||
|
@@ -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),);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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';
|
||||||
|
@@ -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';
|
||||||
|
@@ -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()),);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -3,8 +3,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
|||||||
|
|
||||||
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';
|
||||||
|
|
||||||
@@ -16,7 +15,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(() => {
|
||||||
|
@@ -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';
|
||||||
|
@@ -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 */
|
||||||
|
@@ -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([]);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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 {
|
||||||
|
@@ -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 */
|
||||||
};
|
};
|
||||||
|
@@ -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
|
||||||
|
@@ -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: {
|
||||||
|
@@ -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],
|
||||||
|
@@ -1,6 +1,4 @@
|
|||||||
import { animate, state, style, transition, trigger } from '@angular/animations';
|
import { Component, OnInit } from '@angular/core';
|
||||||
import { Component, HostBinding, OnInit } from '@angular/core';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
import { FilterType } from '../../../search-service/filter-type.model';
|
import { FilterType } from '../../../search-service/filter-type.model';
|
||||||
import {
|
import {
|
||||||
facetLoad,
|
facetLoad,
|
||||||
|
@@ -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(() => {
|
||||||
|
@@ -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,22 @@ 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)
|
return this.filterService.getSelectedValuesForFilter(filterConfig).pipe(
|
||||||
.flatMap((isActive) => {
|
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),);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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';
|
||||||
|
@@ -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';
|
||||||
|
@@ -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();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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);
|
||||||
});
|
});
|
||||||
|
@@ -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>
|
||||||
|
@@ -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('');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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();
|
||||||
|
@@ -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 }
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -8,26 +8,25 @@ import { SearchService } from './search.service';
|
|||||||
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
|
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
|
||||||
import { RequestService } from '../../core/data/request.service';
|
import { RequestService } from '../../core/data/request.service';
|
||||||
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 { 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 { RequestEntry } from '../../core/data/request.reducer';
|
import { RequestEntry } from '../../core/data/request.reducer';
|
||||||
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 {
|
import {
|
||||||
FacetConfigSuccessResponse,
|
FacetConfigSuccessResponse,
|
||||||
SearchSuccessResponse
|
SearchSuccessResponse
|
||||||
} from '../../core/cache/response-cache.models';
|
} from '../../core/cache/response.models';
|
||||||
import { SearchQueryResponse } from './search-query-response.model';
|
import { SearchQueryResponse } from './search-query-response.model';
|
||||||
import { SearchFilterConfig } from './search-filter-config.model';
|
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 {
|
||||||
@@ -52,12 +51,11 @@ describe('SearchService', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
{ provide: ActivatedRoute, useValue: route },
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
{ provide: ResponseCacheService, useValue: getMockResponseCacheService() },
|
|
||||||
{ 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
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -84,14 +82,15 @@ describe('SearchService', () => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
const remoteDataBuildService = {
|
const remoteDataBuildService = {
|
||||||
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<any>) => {
|
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, payloadObs: Observable<any>) => {
|
||||||
return Observable.combineLatest(requestEntryObs,
|
return observableCombineLatest(requestEntryObs, payloadObs).pipe(
|
||||||
responseCacheObs, payloadObs, (req, res, pay) => {
|
map(([req, pay]) => {
|
||||||
return { req, res, pay };
|
return { req, 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, []));
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -109,12 +108,11 @@ describe('SearchService', () => {
|
|||||||
providers: [
|
providers: [
|
||||||
{ provide: Router, useValue: router },
|
{ provide: Router, useValue: router },
|
||||||
{ provide: ActivatedRoute, useValue: route },
|
{ provide: ActivatedRoute, useValue: route },
|
||||||
{ provide: ResponseCacheService, useValue: getMockResponseCacheService() },
|
|
||||||
{ 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
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
@@ -158,10 +156,8 @@ describe('SearchService', () => {
|
|||||||
const searchOptions = new PaginatedSearchOptions({});
|
const searchOptions = new PaginatedSearchOptions({});
|
||||||
const queryResponse = Object.assign(new SearchQueryResponse(), { objects: [] });
|
const queryResponse = Object.assign(new SearchQueryResponse(), { objects: [] });
|
||||||
const response = new SearchSuccessResponse(queryResponse, '200');
|
const response = new SearchSuccessResponse(queryResponse, '200');
|
||||||
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));
|
|
||||||
/* 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
|
||||||
@@ -179,19 +175,14 @@ describe('SearchService', () => {
|
|||||||
it('should call getByHref on the request service with the correct request url', () => {
|
it('should call getByHref on the request service with the correct request url', () => {
|
||||||
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint);
|
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint);
|
||||||
});
|
});
|
||||||
it('should call get on the request service with the correct request url', () => {
|
|
||||||
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(endPoint);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when getConfig is called without a scope', () => {
|
describe('when getConfig is called without a scope', () => {
|
||||||
const endPoint = 'http://endpoint.com/test/config';
|
const endPoint = 'http://endpoint.com/test/config';
|
||||||
const filterConfig = [new SearchFilterConfig()];
|
const filterConfig = [new SearchFilterConfig()];
|
||||||
const response = new FacetConfigSuccessResponse(filterConfig, '200');
|
const response = new FacetConfigSuccessResponse(filterConfig, '200');
|
||||||
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));
|
|
||||||
/* 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
|
||||||
@@ -209,9 +200,6 @@ describe('SearchService', () => {
|
|||||||
it('should call getByHref on the request service with the correct request url', () => {
|
it('should call getByHref on the request service with the correct request url', () => {
|
||||||
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint);
|
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint);
|
||||||
});
|
});
|
||||||
it('should call get on the request service with the correct request url', () => {
|
|
||||||
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(endPoint);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when getConfig is called with a scope', () => {
|
describe('when getConfig is called with a scope', () => {
|
||||||
@@ -220,10 +208,8 @@ describe('SearchService', () => {
|
|||||||
const requestUrl = endPoint + '?scope=' + scope;
|
const requestUrl = endPoint + '?scope=' + scope;
|
||||||
const filterConfig = [new SearchFilterConfig()];
|
const filterConfig = [new SearchFilterConfig()];
|
||||||
const response = new FacetConfigSuccessResponse(filterConfig, '200');
|
const response = new FacetConfigSuccessResponse(filterConfig, '200');
|
||||||
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));
|
|
||||||
/* 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
|
||||||
@@ -241,9 +227,6 @@ describe('SearchService', () => {
|
|||||||
it('should call getByHref on the request service with the correct request url', () => {
|
it('should call getByHref on the request service with the correct request url', () => {
|
||||||
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(requestUrl);
|
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(requestUrl);
|
||||||
});
|
});
|
||||||
it('should call get on the request service with the correct request url', () => {
|
|
||||||
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(requestUrl);
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,3 +1,4 @@
|
|||||||
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
|
||||||
import { Injectable, OnDestroy } from '@angular/core';
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
import {
|
import {
|
||||||
ActivatedRoute,
|
ActivatedRoute,
|
||||||
@@ -6,16 +7,13 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
UrlSegmentGroup
|
UrlSegmentGroup
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { Observable } from 'rxjs/Observable';
|
import { map, switchMap, tap } 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 {
|
||||||
FacetConfigSuccessResponse,
|
FacetConfigSuccessResponse,
|
||||||
FacetValueSuccessResponse,
|
FacetValueSuccessResponse,
|
||||||
SearchSuccessResponse
|
SearchSuccessResponse
|
||||||
} from '../../core/cache/response-cache.models';
|
} from '../../core/cache/response.models';
|
||||||
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
|
|
||||||
import { ResponseCacheService } from '../../core/cache/response-cache.service';
|
|
||||||
import { PaginatedList } from '../../core/data/paginated-list';
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
import { ResponseParsingService } from '../../core/data/parsing.service';
|
import { ResponseParsingService } from '../../core/data/parsing.service';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
@@ -24,7 +22,11 @@ import { RequestService } from '../../core/data/request.service';
|
|||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../core/shared/generic-constructor';
|
||||||
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
|
||||||
import { configureRequest, getSucceededRemoteData } from '../../core/shared/operators';
|
import {
|
||||||
|
configureRequest,
|
||||||
|
getResponseFromEntry,
|
||||||
|
getSucceededRemoteData
|
||||||
|
} from '../../core/shared/operators';
|
||||||
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../../core/url-combiner/url-combiner';
|
||||||
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { NormalizedSearchResult } from '../normalized-search-result.model';
|
import { NormalizedSearchResult } from '../normalized-search-result.model';
|
||||||
@@ -68,7 +70,6 @@ export class SearchService implements OnDestroy {
|
|||||||
|
|
||||||
constructor(private router: Router,
|
constructor(private router: Router,
|
||||||
private route: ActivatedRoute,
|
private route: ActivatedRoute,
|
||||||
protected responseCache: ResponseCacheService,
|
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
private rdb: RemoteDataBuildService,
|
private rdb: RemoteDataBuildService,
|
||||||
private halService: HALEndpointService,
|
private halService: HALEndpointService,
|
||||||
@@ -98,16 +99,12 @@ export class SearchService implements OnDestroy {
|
|||||||
configureRequest(this.requestService)
|
configureRequest(this.requestService)
|
||||||
);
|
);
|
||||||
const requestEntryObs = requestObs.pipe(
|
const requestEntryObs = requestObs.pipe(
|
||||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
);
|
|
||||||
|
|
||||||
const responseCacheObs = requestObs.pipe(
|
|
||||||
flatMap((request: RestRequest) => this.responseCache.get(request.href))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// get search results from response cache
|
// get search results from response cache
|
||||||
const sqrObs: Observable<SearchQueryResponse> = responseCacheObs.pipe(
|
const sqrObs: Observable<SearchQueryResponse> = requestEntryObs.pipe(
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
getResponseFromEntry(),
|
||||||
map((response: SearchSuccessResponse) => response.results)
|
map((response: SearchSuccessResponse) => response.results)
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -115,39 +112,43 @@ export class SearchService implements OnDestroy {
|
|||||||
// Turn list of observable remote data DSO's into observable remote data object with list of DSO
|
// Turn list of observable remote data DSO's into observable remote data object with list of DSO
|
||||||
const dsoObs: Observable<RemoteData<DSpaceObject[]>> = sqrObs.pipe(
|
const dsoObs: Observable<RemoteData<DSpaceObject[]>> = sqrObs.pipe(
|
||||||
map((sqr: SearchQueryResponse) => {
|
map((sqr: SearchQueryResponse) => {
|
||||||
return sqr.objects.map((nsr: NormalizedSearchResult) =>
|
return sqr.objects.map((nsr: NormalizedSearchResult) => {
|
||||||
this.rdb.buildSingle(nsr.dspaceObject));
|
return this.rdb.buildSingle(nsr.dspaceObject);
|
||||||
|
})
|
||||||
}),
|
}),
|
||||||
flatMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input))
|
switchMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input)),
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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) => {
|
||||||
|
let co = DSpaceObject;
|
||||||
|
if (dsos.payload[index]) {
|
||||||
|
const constructor: GenericConstructor<ListableObject> = dsos.payload[index].constructor as GenericConstructor<ListableObject>;
|
||||||
|
co = getSearchResultFor(constructor);
|
||||||
|
return Object.assign(new co(), object, {
|
||||||
|
dspaceObject: dsos.payload[index]
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
return sqr.objects.map((object: NormalizedSearchResult, index: number) => {
|
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
|
||||||
let co = DSpaceObject;
|
getResponseFromEntry(),
|
||||||
if (dsos.payload[index]) {
|
|
||||||
const constructor: GenericConstructor<ListableObject> = dsos.payload[index].constructor as GenericConstructor<ListableObject>;
|
|
||||||
co = getSearchResultFor(constructor);
|
|
||||||
return Object.assign(new co(), object, {
|
|
||||||
dspaceObject: dsos.payload[index]
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
|
|
||||||
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, payloadObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -179,21 +180,17 @@ export class SearchService implements OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const requestEntryObs = requestObs.pipe(
|
const requestEntryObs = requestObs.pipe(
|
||||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
);
|
|
||||||
|
|
||||||
const responseCacheObs = requestObs.pipe(
|
|
||||||
flatMap((request: RestRequest) => this.responseCache.get(request.href))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// get search results from response cache
|
// get search results from response cache
|
||||||
const facetConfigObs: Observable<SearchFilterConfig[]> = responseCacheObs.pipe(
|
const facetConfigObs: Observable<SearchFilterConfig[]> = requestEntryObs.pipe(
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
getResponseFromEntry(),
|
||||||
map((response: FacetConfigSuccessResponse) =>
|
map((response: FacetConfigSuccessResponse) =>
|
||||||
response.results.map((result: any) => Object.assign(new SearchFilterConfig(), result)))
|
response.results.map((result: any) => Object.assign(new SearchFilterConfig(), result)))
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs);
|
return this.rdb.toRemoteDataObservable(requestEntryObs, facetConfigObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -226,29 +223,27 @@ export class SearchService implements OnDestroy {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const requestEntryObs = requestObs.pipe(
|
const requestEntryObs = requestObs.pipe(
|
||||||
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
);
|
|
||||||
|
|
||||||
const responseCacheObs = requestObs.pipe(
|
|
||||||
flatMap((request: RestRequest) => this.responseCache.get(request.href))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
// get search results from response cache
|
// get search results from response cache
|
||||||
const facetValueObs: Observable<FacetValue[]> = responseCacheObs.pipe(
|
const facetValueObs: Observable<FacetValue[]> = requestEntryObs.pipe(
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
getResponseFromEntry(),
|
||||||
map((response: FacetValueSuccessResponse) => response.results)
|
map((response: FacetValueSuccessResponse) => response.results)
|
||||||
);
|
);
|
||||||
|
|
||||||
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
|
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
getResponseFromEntry(),
|
||||||
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, payloadObs);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -272,12 +267,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 +288,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;
|
||||||
}
|
}
|
||||||
});
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -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"]'));
|
||||||
|
@@ -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({
|
||||||
|
@@ -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';
|
||||||
|
@@ -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) {
|
||||||
|
|
||||||
|
@@ -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({
|
||||||
|
@@ -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)
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -92,7 +92,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'));
|
||||||
|
@@ -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';
|
||||||
|
|
||||||
@@ -62,10 +63,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());
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -45,54 +45,72 @@ 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(),
|
||||||
|
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(),
|
|
||||||
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 {
|
||||||
|
|
||||||
|
@@ -1,15 +1,16 @@
|
|||||||
|
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 { 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 { AuthStatusResponse, ErrorResponse } from '../cache/response.models';
|
||||||
import { AuthStatusResponse, ErrorResponse, RestResponse } 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';
|
||||||
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
import { getResponseFromEntry } from '../shared/operators';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class AuthRequestService {
|
export class AuthRequestService {
|
||||||
@@ -18,23 +19,22 @@ export class AuthRequestService {
|
|||||||
|
|
||||||
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
protected responseCache: ResponseCacheService,
|
|
||||||
protected requestService: RequestService) {
|
protected requestService: RequestService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
protected fetchRequest(request: RestRequest): Observable<any> {
|
protected fetchRequest(request: RestRequest): Observable<any> {
|
||||||
const [successResponse, errorResponse] = this.responseCache.get(request.href)
|
return this.requestService.getByUUID(request.uuid).pipe(
|
||||||
.map((entry: ResponseCacheEntry) => entry.response)
|
getResponseFromEntry(),
|
||||||
// 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 +42,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());
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -1,21 +1,18 @@
|
|||||||
import { AuthStatusResponse } from '../cache/response-cache.models';
|
import { AuthStatusResponse } from '../cache/response.models';
|
||||||
|
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
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 any;
|
||||||
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);
|
||||||
|
@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@angular/core';
|
|||||||
|
|
||||||
import { AuthObjectFactory } from './auth-object-factory';
|
import { AuthObjectFactory } from './auth-object-factory';
|
||||||
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
|
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
|
||||||
import { AuthStatusResponse, RestResponse } from '../cache/response-cache.models';
|
import { AuthStatusResponse, RestResponse } from '../cache/response.models';
|
||||||
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
import { GLOBAL_CONFIG } from '../../../config';
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
import { GlobalConfig } from '../../../config/global-config.interface';
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
@@ -27,7 +27,7 @@ export class AuthResponseParsingService extends BaseResponseParsingService imple
|
|||||||
|
|
||||||
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) {
|
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) {
|
||||||
const response = this.process<NormalizedAuthStatus, AuthType>(data.payload, request.href);
|
const response = this.process<NormalizedAuthStatus, AuthType>(data.payload, request.uuid);
|
||||||
return new AuthStatusResponse(response, data.statusCode);
|
return new AuthStatusResponse(response, data.statusCode);
|
||||||
} else {
|
} else {
|
||||||
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
|
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);
|
||||||
|
@@ -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}});
|
||||||
|
|
||||||
|
@@ -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
|
||||||
|
@@ -4,15 +4,15 @@ 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';
|
||||||
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { RestRequestMethod } from '../data/request.models';
|
|
||||||
import { RouterStub } from '../../shared/testing/router-stub';
|
import { RouterStub } from '../../shared/testing/router-stub';
|
||||||
import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer';
|
import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer';
|
||||||
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
|
||||||
describe(`AuthInterceptor`, () => {
|
describe(`AuthInterceptor`, () => {
|
||||||
let service: DSpaceRESTv2Service;
|
let service: DSpaceRESTv2Service;
|
||||||
@@ -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(() => {
|
||||||
@@ -49,7 +49,7 @@ describe(`AuthInterceptor`, () => {
|
|||||||
describe('when has a valid token', () => {
|
describe('when has a valid token', () => {
|
||||||
|
|
||||||
it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => {
|
it('should not add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => {
|
||||||
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/authn/login', 'password=password&user=user').subscribe((response) => {
|
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/authn/login', 'password=password&user=user').subscribe((response) => {
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -60,7 +60,7 @@ describe(`AuthInterceptor`, () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => {
|
it('should add an Authorization header when we’re sending a HTTP request to \'authn\' endpoint', () => {
|
||||||
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'test').subscribe((response) => {
|
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'test').subscribe((response) => {
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -85,11 +85,11 @@ describe(`AuthInterceptor`, () => {
|
|||||||
|
|
||||||
it('should redirect to login', () => {
|
it('should redirect to login', () => {
|
||||||
|
|
||||||
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user').subscribe((response) => {
|
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user').subscribe((response) => {
|
||||||
expect(response).toBeTruthy();
|
expect(response).toBeTruthy();
|
||||||
});
|
});
|
||||||
|
|
||||||
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user');
|
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user');
|
||||||
|
|
||||||
httpMock.expectNone('dspace-spring-rest/api/submission/workspaceitems');
|
httpMock.expectNone('dspace-spring-rest/api/submission/workspaceitems');
|
||||||
});
|
});
|
||||||
|
@@ -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)) {
|
} else if (this.isUnauthorized(error)) {
|
||||||
// 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;
|
||||||
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -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();
|
||||||
@@ -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
|
||||||
]
|
]
|
||||||
|
@@ -1,13 +1,22 @@
|
|||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import {
|
||||||
|
distinctUntilChanged,
|
||||||
|
filter,
|
||||||
|
first,
|
||||||
|
map,
|
||||||
|
startWith,
|
||||||
|
switchMap,
|
||||||
|
take,
|
||||||
|
withLatestFrom
|
||||||
|
} from 'rxjs/operators';
|
||||||
import { Inject, Injectable } from '@angular/core';
|
import { Inject, Injectable } 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 } 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';
|
||||||
@@ -55,21 +64,24 @@ export class AuthService {
|
|||||||
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 +114,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 +130,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 +170,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 +182,8 @@ export class AuthService {
|
|||||||
} else {
|
} else {
|
||||||
throw false;
|
throw false;
|
||||||
}
|
}
|
||||||
});
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -181,14 +195,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 +221,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 +233,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 +261,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 +274,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 +285,7 @@ export class AuthService {
|
|||||||
return token.expires - (60 * 5 * 1000) < Date.now();
|
return token.expires - (60 * 5 * 1000) < Date.now();
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -337,8 +354,8 @@ 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();
|
||||||
@@ -376,9 +393,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));
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -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);
|
||||||
|
@@ -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 {
|
||||||
|
|
||||||
|
@@ -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';
|
||||||
@@ -41,12 +41,13 @@ export class ServerAuthService extends AuthService {
|
|||||||
|
|
||||||
// TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole...
|
// TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole...
|
||||||
const person$ = this.rdbService.buildSingle<NormalizedEPerson, EPerson>(status.eperson.toString());
|
const person$ = this.rdbService.buildSingle<NormalizedEPerson, EPerson>(status.eperson.toString());
|
||||||
// person$.subscribe(() => console.log('test'));
|
return person$.pipe(
|
||||||
return person$.pipe(map((eperson) => eperson.payload));
|
map((eperson) => eperson.payload)
|
||||||
|
);
|
||||||
} else {
|
} else {
|
||||||
throw(new Error('Not authenticated'));
|
throw(new Error('Not authenticated'));
|
||||||
}
|
}
|
||||||
}))
|
}));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -1,20 +1,19 @@
|
|||||||
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 { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||||
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 { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models';
|
import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
import { BrowseDefinition } from '../shared/browse-definition.model';
|
import { BrowseDefinition } from '../shared/browse-definition.model';
|
||||||
import { BrowseService } from './browse.service';
|
import { BrowseService } from './browse.service';
|
||||||
|
import { RequestEntry } from '../data/request.reducer';
|
||||||
|
import { of as observableOf } from 'rxjs';
|
||||||
|
|
||||||
describe('BrowseService', () => {
|
describe('BrowseService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: BrowseService;
|
let service: BrowseService;
|
||||||
let responseCache: ResponseCacheService;
|
|
||||||
let requestService: RequestService;
|
let requestService: RequestService;
|
||||||
let rdbService: RemoteDataBuildService;
|
let rdbService: RemoteDataBuildService;
|
||||||
|
|
||||||
@@ -79,22 +78,14 @@ describe('BrowseService', () => {
|
|||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
|
||||||
function initMockResponseCacheService(isSuccessful: boolean) {
|
const getRequestEntry$ = (successful: boolean) => {
|
||||||
const rcs = getMockResponseCacheService();
|
return observableOf({
|
||||||
(rcs.get as any).and.returnValue(cold('b-', {
|
response: { isSuccessful: successful, payload: browseDefinitions } as any
|
||||||
b: {
|
} as RequestEntry)
|
||||||
response: {
|
};
|
||||||
isSuccessful,
|
|
||||||
payload: browseDefinitions,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}));
|
|
||||||
return rcs;
|
|
||||||
}
|
|
||||||
|
|
||||||
function initTestService() {
|
function initTestService() {
|
||||||
return new BrowseService(
|
return new BrowseService(
|
||||||
responseCache,
|
|
||||||
requestService,
|
requestService,
|
||||||
halService,
|
halService,
|
||||||
rdbService
|
rdbService
|
||||||
@@ -108,8 +99,7 @@ describe('BrowseService', () => {
|
|||||||
describe('getBrowseDefinitions', () => {
|
describe('getBrowseDefinitions', () => {
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
responseCache = initMockResponseCacheService(true);
|
requestService = getMockRequestService(getRequestEntry$(true));
|
||||||
requestService = getMockRequestService();
|
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(halService, 'getEndpoint').and
|
spyOn(halService, 'getEndpoint').and
|
||||||
@@ -147,8 +137,7 @@ describe('BrowseService', () => {
|
|||||||
const mockAuthorName = 'Donald Smith';
|
const mockAuthorName = 'Donald Smith';
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
responseCache = initMockResponseCacheService(true);
|
requestService = getMockRequestService(getRequestEntry$(true));
|
||||||
requestService = getMockRequestService();
|
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(service, 'getBrowseDefinitions').and
|
spyOn(service, 'getBrowseDefinitions').and
|
||||||
@@ -221,8 +210,7 @@ describe('BrowseService', () => {
|
|||||||
|
|
||||||
describe('if getBrowseDefinitions fires', () => {
|
describe('if getBrowseDefinitions fires', () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
responseCache = initMockResponseCacheService(true);
|
requestService = getMockRequestService(getRequestEntry$(true));
|
||||||
requestService = getMockRequestService();
|
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(service, 'getBrowseDefinitions').and
|
spyOn(service, 'getBrowseDefinitions').and
|
||||||
@@ -277,8 +265,7 @@ describe('BrowseService', () => {
|
|||||||
|
|
||||||
describe('if getBrowseDefinitions doesn\'t fire', () => {
|
describe('if getBrowseDefinitions doesn\'t fire', () => {
|
||||||
it('should return undefined', () => {
|
it('should return undefined', () => {
|
||||||
responseCache = initMockResponseCacheService(true);
|
requestService = getMockRequestService(getRequestEntry$(true));
|
||||||
requestService = getMockRequestService();
|
|
||||||
rdbService = getMockRemoteDataBuildService();
|
rdbService = getMockRemoteDataBuildService();
|
||||||
service = initTestService();
|
service = initTestService();
|
||||||
spyOn(service, 'getBrowseDefinitions').and
|
spyOn(service, 'getBrowseDefinitions').and
|
||||||
|
@@ -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,
|
||||||
@@ -11,16 +11,13 @@ import {
|
|||||||
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { SortOptions } from '../cache/models/sort-options.model';
|
import { SortOptions } from '../cache/models/sort-options.model';
|
||||||
import { GenericSuccessResponse } from '../cache/response-cache.models';
|
import { GenericSuccessResponse } from '../cache/response.models';
|
||||||
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
|
|
||||||
import { ResponseCacheService } from '../cache/response-cache.service';
|
|
||||||
import { PaginatedList } from '../data/paginated-list';
|
import { PaginatedList } from '../data/paginated-list';
|
||||||
import { RemoteData } from '../data/remote-data';
|
import { RemoteData } from '../data/remote-data';
|
||||||
import {
|
import {
|
||||||
BrowseEndpointRequest,
|
BrowseEndpointRequest,
|
||||||
BrowseEntriesRequest,
|
BrowseEntriesRequest,
|
||||||
BrowseItemsRequest,
|
BrowseItemsRequest,
|
||||||
GetRequest,
|
|
||||||
RestRequest
|
RestRequest
|
||||||
} from '../data/request.models';
|
} from '../data/request.models';
|
||||||
import { RequestService } from '../data/request.service';
|
import { RequestService } from '../data/request.service';
|
||||||
@@ -29,10 +26,9 @@ import { BrowseEntry } from '../shared/browse-entry.model';
|
|||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import {
|
import {
|
||||||
configureRequest,
|
configureRequest,
|
||||||
filterSuccessfulResponses, getBrowseDefinitionLinks,
|
filterSuccessfulResponses,
|
||||||
getRemoteDataPayload,
|
getBrowseDefinitionLinks,
|
||||||
getRequestFromSelflink,
|
getRemoteDataPayload, getRequestFromRequestHref
|
||||||
getResponseFromSelflink
|
|
||||||
} from '../shared/operators';
|
} from '../shared/operators';
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { Item } from '../shared/item.model';
|
import { Item } from '../shared/item.model';
|
||||||
@@ -56,7 +52,6 @@ export class BrowseService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected responseCache: ResponseCacheService,
|
|
||||||
protected requestService: RequestService,
|
protected requestService: RequestService,
|
||||||
protected halService: HALEndpointService,
|
protected halService: HALEndpointService,
|
||||||
private rdb: RemoteDataBuildService,
|
private rdb: RemoteDataBuildService,
|
||||||
@@ -72,11 +67,9 @@ export class BrowseService {
|
|||||||
);
|
);
|
||||||
|
|
||||||
const href$ = request$.pipe(map((request: RestRequest) => request.href));
|
const href$ = request$.pipe(map((request: RestRequest) => request.href));
|
||||||
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
|
const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService));
|
||||||
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
|
const payload$ = requestEntry$.pipe(
|
||||||
const payload$ = responseCache$.pipe(
|
|
||||||
filterSuccessfulResponses(),
|
filterSuccessfulResponses(),
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
|
||||||
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
|
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
|
||||||
ensureArrayHasValue(),
|
ensureArrayHasValue(),
|
||||||
map((definitions: BrowseDefinition[]) => definitions
|
map((definitions: BrowseDefinition[]) => definitions
|
||||||
@@ -84,7 +77,7 @@ export class BrowseService {
|
|||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBrowseEntriesFor(definitionID: string, options: {
|
getBrowseEntriesFor(definitionID: string, options: {
|
||||||
@@ -117,12 +110,10 @@ export class BrowseService {
|
|||||||
|
|
||||||
const href$ = request$.pipe(map((request: RestRequest) => request.href));
|
const href$ = request$.pipe(map((request: RestRequest) => request.href));
|
||||||
|
|
||||||
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
|
const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService));
|
||||||
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
|
|
||||||
|
|
||||||
const payload$ = responseCache$.pipe(
|
const payload$ = requestEntry$.pipe(
|
||||||
filterSuccessfulResponses(),
|
filterSuccessfulResponses(),
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
|
||||||
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
||||||
map((list: PaginatedList<BrowseEntry>) => Object.assign(list, {
|
map((list: PaginatedList<BrowseEntry>) => Object.assign(list, {
|
||||||
page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page
|
page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page
|
||||||
@@ -130,7 +121,7 @@ export class BrowseService {
|
|||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -174,12 +165,10 @@ export class BrowseService {
|
|||||||
|
|
||||||
const href$ = request$.pipe(map((request: RestRequest) => request.href));
|
const href$ = request$.pipe(map((request: RestRequest) => request.href));
|
||||||
|
|
||||||
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
|
const requestEntry$ = href$.pipe(getRequestFromRequestHref(this.requestService));
|
||||||
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
|
|
||||||
|
|
||||||
const payload$ = responseCache$.pipe(
|
const payload$ = requestEntry$.pipe(
|
||||||
filterSuccessfulResponses(),
|
filterSuccessfulResponses(),
|
||||||
map((entry: ResponseCacheEntry) => entry.response),
|
|
||||||
map((response: GenericSuccessResponse<Item[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
map((response: GenericSuccessResponse<Item[]>) => new PaginatedList(response.pageInfo, response.payload)),
|
||||||
map((list: PaginatedList<Item>) => Object.assign(list, {
|
map((list: PaginatedList<Item>) => Object.assign(list, {
|
||||||
page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page
|
page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page
|
||||||
@@ -187,7 +176,7 @@ export class BrowseService {
|
|||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
|
|
||||||
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> {
|
getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> {
|
||||||
|
59
src/app/core/cache/builders/remote-data-build.service.spec.ts
vendored
Normal file
59
src/app/core/cache/builders/remote-data-build.service.spec.ts
vendored
Normal 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);
|
||||||
|
});
|
||||||
|
|
||||||
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -1,6 +1,11 @@
|
|||||||
|
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, first, flatMap, map, startWith, switchMap } 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';
|
||||||
import { RemoteData } from '../../data/remote-data';
|
import { RemoteData } from '../../data/remote-data';
|
||||||
@@ -11,69 +16,63 @@ import { RequestService } from '../../data/request.service';
|
|||||||
|
|
||||||
import { NormalizedObject } from '../models/normalized-object.model';
|
import { NormalizedObject } from '../models/normalized-object.model';
|
||||||
import { ObjectCacheService } from '../object-cache.service';
|
import { ObjectCacheService } from '../object-cache.service';
|
||||||
import { DSOSuccessResponse, ErrorResponse } from '../response-cache.models';
|
import { DSOSuccessResponse, ErrorResponse } from '../response.models';
|
||||||
import { ResponseCacheEntry } from '../response-cache.reducer';
|
|
||||||
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 {
|
||||||
getRequestFromSelflink,
|
filterSuccessfulResponses,
|
||||||
getResourceLinksFromResponse,
|
getRequestFromRequestHref, getRequestFromRequestUUID,
|
||||||
getResponseFromSelflink,
|
getResourceLinksFromResponse
|
||||||
filterSuccessfulResponses
|
|
||||||
} from '../../shared/operators';
|
} from '../../shared/operators';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class RemoteDataBuildService {
|
export class RemoteDataBuildService {
|
||||||
constructor(protected objectCache: ObjectCacheService,
|
constructor(protected objectCache: ObjectCacheService,
|
||||||
protected responseCache: ResponseCacheService,
|
|
||||||
protected requestService: RequestService) {
|
protected requestService: RequestService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
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 requestUUID$ = href$.pipe(
|
||||||
this.objectCache.getRequestHrefBySelfLink(href)));
|
switchMap((href: string) =>
|
||||||
|
this.objectCache.getRequestUUIDBySelfLink(href)),
|
||||||
const requestEntry$ = Observable.race(
|
|
||||||
href$.pipe(getRequestFromSelflink(this.requestService)),
|
|
||||||
requestHref$.pipe(getRequestFromSelflink(this.requestService))
|
|
||||||
);
|
);
|
||||||
|
|
||||||
const responseCache$ = Observable.race(
|
const requestEntry$ = observableRace(
|
||||||
href$.pipe(getResponseFromSelflink(this.responseCache)),
|
href$.pipe(getRequestFromRequestHref(this.requestService)),
|
||||||
requestHref$.pipe(getResponseFromSelflink(this.responseCache))
|
requestUUID$.pipe(getRequestFromRequestUUID(this.requestService)),
|
||||||
|
).pipe(
|
||||||
|
first()
|
||||||
);
|
);
|
||||||
|
|
||||||
// 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)),
|
switchMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)),
|
||||||
startWith(undefined)
|
startWith(undefined)),
|
||||||
),
|
requestEntry$.pipe(
|
||||||
responseCache$.pipe(
|
|
||||||
getResourceLinksFromResponse(),
|
getResourceLinksFromResponse(),
|
||||||
flatMap((resourceSelfLinks: string[]) => {
|
switchMap((resourceSelfLinks: string[]) => {
|
||||||
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);
|
||||||
@@ -81,21 +80,21 @@ export class RemoteDataBuildService {
|
|||||||
startWith(undefined),
|
startWith(undefined),
|
||||||
distinctUntilChanged()
|
distinctUntilChanged()
|
||||||
);
|
);
|
||||||
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
|
return this.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, responseCache$: Observable<ResponseCacheEntry>, payload$: Observable<T>) {
|
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, payload$: Observable<T>) {
|
||||||
return Observable.combineLatest(requestEntry$, responseCache$.startWith(undefined), payload$,
|
return observableCombineLatest(requestEntry$, payload$).pipe(
|
||||||
(reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => {
|
map(([reqEntry, 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;
|
||||||
let error: RemoteDataError;
|
let error: RemoteDataError;
|
||||||
if (hasValue(resEntry) && hasValue(resEntry.response)) {
|
if (hasValue(reqEntry) && hasValue(reqEntry.response)) {
|
||||||
isSuccessful = resEntry.response.isSuccessful;
|
isSuccessful = reqEntry.response.isSuccessful;
|
||||||
const errorMessage = isSuccessful === false ? (resEntry.response as ErrorResponse).errorMessage : undefined;
|
const errorMessage = isSuccessful === false ? (reqEntry.response as ErrorResponse).errorMessage : undefined;
|
||||||
if (hasValue(errorMessage)) {
|
if (hasValue(errorMessage)) {
|
||||||
error = new RemoteDataError(resEntry.response.statusCode, errorMessage);
|
error = new RemoteDataError(reqEntry.response.statusCode, errorMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return new RemoteData(
|
return new RemoteData(
|
||||||
@@ -105,36 +104,34 @@ 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(getRequestFromRequestHref(this.requestService));
|
||||||
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
|
const tDomainList$ = requestEntry$.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(),
|
||||||
);
|
);
|
||||||
|
const pageInfo$ = requestEntry$.pipe(
|
||||||
const pageInfo$ = responseCache$.pipe(
|
|
||||||
filterSuccessfulResponses(),
|
filterSuccessfulResponses(),
|
||||||
map((entry: ResponseCacheEntry) => {
|
map((response: DSOSuccessResponse) => {
|
||||||
if (hasValue((entry.response as DSOSuccessResponse).pageInfo)) {
|
if (hasValue((response as DSOSuccessResponse).pageInfo)) {
|
||||||
const resPageInfo = (entry.response as DSOSuccessResponse).pageInfo;
|
const resPageInfo = (response as DSOSuccessResponse).pageInfo;
|
||||||
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
|
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
|
||||||
return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 });
|
return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 });
|
||||||
} else {
|
} else {
|
||||||
@@ -142,18 +139,19 @@ 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$, payload$);
|
||||||
}
|
}
|
||||||
|
|
||||||
build<TNormalized, TDomain>(normalized: TNormalized): TDomain {
|
build<TNormalized, TDomain>(normalized: TNormalized): TDomain {
|
||||||
const links: any = {};
|
const links: any = {};
|
||||||
|
|
||||||
const relationships = getRelationships(normalized.constructor) || [];
|
const relationships = getRelationships(normalized.constructor) || [];
|
||||||
|
|
||||||
relationships.forEach((relationship: string) => {
|
relationships.forEach((relationship: string) => {
|
||||||
@@ -190,13 +188,12 @@ 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;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
const domainModel = getMapsTo(normalized.constructor);
|
const domainModel = getMapsTo(normalized.constructor);
|
||||||
return Object.assign(new domainModel(), normalized, links);
|
return Object.assign(new domainModel(), normalized, links);
|
||||||
}
|
}
|
||||||
@@ -204,12 +201,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 +247,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) });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
65
src/app/core/cache/object-cache.actions.ts
vendored
65
src/app/core/cache/object-cache.actions.ts
vendored
@@ -2,6 +2,7 @@ import { Action } from '@ngrx/store';
|
|||||||
|
|
||||||
import { type } from '../../shared/ngrx/type';
|
import { type } from '../../shared/ngrx/type';
|
||||||
import { CacheableObject } from './object-cache.reducer';
|
import { CacheableObject } from './object-cache.reducer';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The list of ObjectCacheAction type definitions
|
* The list of ObjectCacheAction type definitions
|
||||||
@@ -9,7 +10,9 @@ import { CacheableObject } from './object-cache.reducer';
|
|||||||
export const ObjectCacheActionTypes = {
|
export const ObjectCacheActionTypes = {
|
||||||
ADD: type('dspace/core/cache/object/ADD'),
|
ADD: type('dspace/core/cache/object/ADD'),
|
||||||
REMOVE: type('dspace/core/cache/object/REMOVE'),
|
REMOVE: type('dspace/core/cache/object/REMOVE'),
|
||||||
RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS')
|
RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS'),
|
||||||
|
ADD_PATCH: type('dspace/core/cache/object/ADD_PATCH'),
|
||||||
|
APPLY_PATCH: type('dspace/core/cache/object/APPLY_PATCH')
|
||||||
};
|
};
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
@@ -22,7 +25,7 @@ export class AddToObjectCacheAction implements Action {
|
|||||||
objectToCache: CacheableObject;
|
objectToCache: CacheableObject;
|
||||||
timeAdded: number;
|
timeAdded: number;
|
||||||
msToLive: number;
|
msToLive: number;
|
||||||
requestHref: string;
|
requestUUID: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -39,8 +42,8 @@ export class AddToObjectCacheAction implements Action {
|
|||||||
* This isn't necessarily the same as the object's self
|
* This isn't necessarily the same as the object's self
|
||||||
* link, it could have been part of a list for example
|
* link, it could have been part of a list for example
|
||||||
*/
|
*/
|
||||||
constructor(objectToCache: CacheableObject, timeAdded: number, msToLive: number, requestHref: string) {
|
constructor(objectToCache: CacheableObject, timeAdded: number, msToLive: number, requestUUID: string) {
|
||||||
this.payload = { objectToCache, timeAdded, msToLive, requestHref };
|
this.payload = { objectToCache, timeAdded, msToLive, requestUUID };
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -54,11 +57,11 @@ export class RemoveFromObjectCacheAction implements Action {
|
|||||||
/**
|
/**
|
||||||
* Create a new RemoveFromObjectCacheAction
|
* Create a new RemoveFromObjectCacheAction
|
||||||
*
|
*
|
||||||
* @param uuid
|
* @param href
|
||||||
* the UUID of the object to remove
|
* the unique href of the object to remove
|
||||||
*/
|
*/
|
||||||
constructor(uuid: string) {
|
constructor(href: string) {
|
||||||
this.payload = uuid;
|
this.payload = href;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,6 +82,48 @@ export class ResetObjectCacheTimestampsAction implements Action {
|
|||||||
this.payload = newTimestamp;
|
this.payload = newTimestamp;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to add new operations to a specified cached object
|
||||||
|
*/
|
||||||
|
export class AddPatchObjectCacheAction implements Action {
|
||||||
|
type = ObjectCacheActionTypes.ADD_PATCH;
|
||||||
|
payload: {
|
||||||
|
href: string,
|
||||||
|
operations: Operation[]
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new AddPatchObjectCacheAction
|
||||||
|
*
|
||||||
|
* @param href
|
||||||
|
* the unique href of the object that should be updated
|
||||||
|
* @param operations
|
||||||
|
* the list of operations to add
|
||||||
|
*/
|
||||||
|
constructor(href: string, operations: Operation[]) {
|
||||||
|
this.payload = { href, operations };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to apply all existing operations to a specified cached object
|
||||||
|
*/
|
||||||
|
export class ApplyPatchObjectCacheAction implements Action {
|
||||||
|
type = ObjectCacheActionTypes.APPLY_PATCH;
|
||||||
|
payload: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new ApplyPatchObjectCacheAction
|
||||||
|
*
|
||||||
|
* @param href
|
||||||
|
* the unique href of the object that should be updated
|
||||||
|
*/
|
||||||
|
constructor(href: string) {
|
||||||
|
this.payload = href;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -87,4 +132,6 @@ export class ResetObjectCacheTimestampsAction implements Action {
|
|||||||
export type ObjectCacheAction
|
export type ObjectCacheAction
|
||||||
= AddToObjectCacheAction
|
= AddToObjectCacheAction
|
||||||
| RemoveFromObjectCacheAction
|
| RemoveFromObjectCacheAction
|
||||||
| ResetObjectCacheTimestampsAction;
|
| ResetObjectCacheTimestampsAction
|
||||||
|
| AddPatchObjectCacheAction
|
||||||
|
| ApplyPatchObjectCacheAction;
|
||||||
|
@@ -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';
|
||||||
|
11
src/app/core/cache/object-cache.effects.ts
vendored
11
src/app/core/cache/object-cache.effects.ts
vendored
@@ -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) {
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
59
src/app/core/cache/object-cache.reducer.spec.ts
vendored
59
src/app/core/cache/object-cache.reducer.spec.ts
vendored
@@ -2,9 +2,13 @@ import * as deepFreeze from 'deep-freeze';
|
|||||||
|
|
||||||
import { objectCacheReducer } from './object-cache.reducer';
|
import { objectCacheReducer } from './object-cache.reducer';
|
||||||
import {
|
import {
|
||||||
|
AddPatchObjectCacheAction,
|
||||||
AddToObjectCacheAction,
|
AddToObjectCacheAction,
|
||||||
RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction
|
ApplyPatchObjectCacheAction,
|
||||||
|
RemoveFromObjectCacheAction,
|
||||||
|
ResetObjectCacheTimestampsAction
|
||||||
} from './object-cache.actions';
|
} from './object-cache.actions';
|
||||||
|
import { Operation } from 'fast-json-patch';
|
||||||
|
|
||||||
class NullAction extends RemoveFromObjectCacheAction {
|
class NullAction extends RemoveFromObjectCacheAction {
|
||||||
type = null;
|
type = null;
|
||||||
@@ -16,8 +20,11 @@ class NullAction extends RemoveFromObjectCacheAction {
|
|||||||
}
|
}
|
||||||
|
|
||||||
describe('objectCacheReducer', () => {
|
describe('objectCacheReducer', () => {
|
||||||
|
const requestUUID1 = '8646169a-a8fc-4b31-a368-384c07867eb1';
|
||||||
|
const requestUUID2 = 'bd36820b-4bf7-4d58-bd80-b832058b7279';
|
||||||
const selfLink1 = 'https://localhost:8080/api/core/items/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
const selfLink1 = 'https://localhost:8080/api/core/items/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
const selfLink2 = 'https://localhost:8080/api/core/items/28b04544-1766-4e82-9728-c4e93544ecd3';
|
const selfLink2 = 'https://localhost:8080/api/core/items/28b04544-1766-4e82-9728-c4e93544ecd3';
|
||||||
|
const newName = 'new different name';
|
||||||
const testState = {
|
const testState = {
|
||||||
[selfLink1]: {
|
[selfLink1]: {
|
||||||
data: {
|
data: {
|
||||||
@@ -26,16 +33,20 @@ describe('objectCacheReducer', () => {
|
|||||||
},
|
},
|
||||||
timeAdded: new Date().getTime(),
|
timeAdded: new Date().getTime(),
|
||||||
msToLive: 900000,
|
msToLive: 900000,
|
||||||
requestHref: selfLink1
|
requestUUID: requestUUID1,
|
||||||
|
patches: [],
|
||||||
|
isDirty: false
|
||||||
},
|
},
|
||||||
[selfLink2]: {
|
[selfLink2]: {
|
||||||
data: {
|
data: {
|
||||||
self: selfLink2,
|
self: requestUUID2,
|
||||||
foo: 'baz'
|
foo: 'baz'
|
||||||
},
|
},
|
||||||
timeAdded: new Date().getTime(),
|
timeAdded: new Date().getTime(),
|
||||||
msToLive: 900000,
|
msToLive: 900000,
|
||||||
requestHref: selfLink2
|
requestUUID: selfLink2,
|
||||||
|
patches: [],
|
||||||
|
isDirty: false
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
deepFreeze(testState);
|
deepFreeze(testState);
|
||||||
@@ -59,8 +70,8 @@ describe('objectCacheReducer', () => {
|
|||||||
const objectToCache = { self: selfLink1 };
|
const objectToCache = { self: selfLink1 };
|
||||||
const timeAdded = new Date().getTime();
|
const timeAdded = new Date().getTime();
|
||||||
const msToLive = 900000;
|
const msToLive = 900000;
|
||||||
const requestHref = 'https://rest.api/endpoint/selfLink1';
|
const requestUUID = requestUUID1;
|
||||||
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
|
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestUUID);
|
||||||
const newState = objectCacheReducer(state, action);
|
const newState = objectCacheReducer(state, action);
|
||||||
|
|
||||||
expect(newState[selfLink1].data).toEqual(objectToCache);
|
expect(newState[selfLink1].data).toEqual(objectToCache);
|
||||||
@@ -72,8 +83,8 @@ describe('objectCacheReducer', () => {
|
|||||||
const objectToCache = { self: selfLink1, foo: 'baz', somethingElse: true };
|
const objectToCache = { self: selfLink1, foo: 'baz', somethingElse: true };
|
||||||
const timeAdded = new Date().getTime();
|
const timeAdded = new Date().getTime();
|
||||||
const msToLive = 900000;
|
const msToLive = 900000;
|
||||||
const requestHref = 'https://rest.api/endpoint/selfLink1';
|
const requestUUID = requestUUID1;
|
||||||
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
|
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestUUID);
|
||||||
const newState = objectCacheReducer(testState, action);
|
const newState = objectCacheReducer(testState, action);
|
||||||
|
|
||||||
/* tslint:disable:no-string-literal */
|
/* tslint:disable:no-string-literal */
|
||||||
@@ -87,8 +98,8 @@ describe('objectCacheReducer', () => {
|
|||||||
const objectToCache = { self: selfLink1 };
|
const objectToCache = { self: selfLink1 };
|
||||||
const timeAdded = new Date().getTime();
|
const timeAdded = new Date().getTime();
|
||||||
const msToLive = 900000;
|
const msToLive = 900000;
|
||||||
const requestHref = 'https://rest.api/endpoint/selfLink1';
|
const requestUUID = requestUUID1;
|
||||||
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestHref);
|
const action = new AddToObjectCacheAction(objectToCache, timeAdded, msToLive, requestUUID);
|
||||||
deepFreeze(state);
|
deepFreeze(state);
|
||||||
|
|
||||||
objectCacheReducer(state, action);
|
objectCacheReducer(state, action);
|
||||||
@@ -132,4 +143,32 @@ describe('objectCacheReducer', () => {
|
|||||||
objectCacheReducer(testState, action);
|
objectCacheReducer(testState, action);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should perform the ADD_PATCH action without affecting the previous state', () => {
|
||||||
|
const action = new AddPatchObjectCacheAction(selfLink1, [{
|
||||||
|
op: 'replace',
|
||||||
|
path: '/name',
|
||||||
|
value: 'random string'
|
||||||
|
}]);
|
||||||
|
// testState has already been frozen above
|
||||||
|
objectCacheReducer(testState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should when the ADD_PATCH action dispatched', () => {
|
||||||
|
const patch = [{ op: 'add', path: '/name', value: newName } as Operation];
|
||||||
|
const action = new AddPatchObjectCacheAction(selfLink1, patch);
|
||||||
|
const newState = objectCacheReducer(testState, action);
|
||||||
|
expect(newState[selfLink1].patches.map((p) => p.operations)).toContain(patch);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should when the APPLY_PATCH action dispatched', () => {
|
||||||
|
const patch = [{ op: 'add', path: '/name', value: newName } as Operation];
|
||||||
|
const addPatchAction = new AddPatchObjectCacheAction(selfLink1, patch);
|
||||||
|
const stateWithPatch = objectCacheReducer(testState, addPatchAction);
|
||||||
|
|
||||||
|
const action = new ApplyPatchObjectCacheAction(selfLink1);
|
||||||
|
const newState = objectCacheReducer(stateWithPatch, action);
|
||||||
|
expect(newState[selfLink1].patches).toEqual([]);
|
||||||
|
expect((newState[selfLink1].data as any).name).toEqual(newName);
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
81
src/app/core/cache/object-cache.reducer.ts
vendored
81
src/app/core/cache/object-cache.reducer.ts
vendored
@@ -1,10 +1,15 @@
|
|||||||
import {
|
import {
|
||||||
ObjectCacheAction, ObjectCacheActionTypes, AddToObjectCacheAction,
|
ObjectCacheAction,
|
||||||
RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction
|
ObjectCacheActionTypes,
|
||||||
|
AddToObjectCacheAction,
|
||||||
|
RemoveFromObjectCacheAction,
|
||||||
|
ResetObjectCacheTimestampsAction,
|
||||||
|
AddPatchObjectCacheAction, ApplyPatchObjectCacheAction
|
||||||
} from './object-cache.actions';
|
} from './object-cache.actions';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
||||||
import { CacheEntry } from './cache-entry';
|
import { CacheEntry } from './cache-entry';
|
||||||
import { ResourceType } from '../shared/resource-type';
|
import { ResourceType } from '../shared/resource-type';
|
||||||
|
import { applyPatch, Operation } from 'fast-json-patch';
|
||||||
|
|
||||||
export enum DirtyType {
|
export enum DirtyType {
|
||||||
Created = 'Created',
|
Created = 'Created',
|
||||||
@@ -12,7 +17,12 @@ export enum DirtyType {
|
|||||||
Deleted = 'Deleted'
|
Deleted = 'Deleted'
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
export interface Patch {
|
||||||
|
uuid?: string;
|
||||||
|
operations: Operation[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**conca
|
||||||
* An interface to represent objects that can be cached
|
* An interface to represent objects that can be cached
|
||||||
*
|
*
|
||||||
* A cacheable object should have a self link
|
* A cacheable object should have a self link
|
||||||
@@ -35,7 +45,9 @@ export class ObjectCacheEntry implements CacheEntry {
|
|||||||
data: CacheableObject;
|
data: CacheableObject;
|
||||||
timeAdded: number;
|
timeAdded: number;
|
||||||
msToLive: number;
|
msToLive: number;
|
||||||
requestHref: string;
|
requestUUID: string;
|
||||||
|
patches: Patch[] = [];
|
||||||
|
isDirty: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -76,6 +88,14 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi
|
|||||||
return resetObjectCacheTimestamps(state, action as ResetObjectCacheTimestampsAction)
|
return resetObjectCacheTimestamps(state, action as ResetObjectCacheTimestampsAction)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
case ObjectCacheActionTypes.ADD_PATCH: {
|
||||||
|
return addPatchObjectCache(state, action as AddPatchObjectCacheAction);
|
||||||
|
}
|
||||||
|
|
||||||
|
case ObjectCacheActionTypes.APPLY_PATCH: {
|
||||||
|
return applyPatchObjectCache(state, action as ApplyPatchObjectCacheAction);
|
||||||
|
}
|
||||||
|
|
||||||
default: {
|
default: {
|
||||||
return state;
|
return state;
|
||||||
}
|
}
|
||||||
@@ -93,12 +113,15 @@ export function objectCacheReducer(state = initialState, action: ObjectCacheActi
|
|||||||
* the new state, with the object added, or overwritten.
|
* the new state, with the object added, or overwritten.
|
||||||
*/
|
*/
|
||||||
function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState {
|
function addToObjectCache(state: ObjectCacheState, action: AddToObjectCacheAction): ObjectCacheState {
|
||||||
|
const existing = state[action.payload.objectToCache.self];
|
||||||
return Object.assign({}, state, {
|
return Object.assign({}, state, {
|
||||||
[action.payload.objectToCache.self]: {
|
[action.payload.objectToCache.self]: {
|
||||||
data: action.payload.objectToCache,
|
data: action.payload.objectToCache,
|
||||||
timeAdded: action.payload.timeAdded,
|
timeAdded: action.payload.timeAdded,
|
||||||
msToLive: action.payload.msToLive,
|
msToLive: action.payload.msToLive,
|
||||||
requestHref: action.payload.requestHref
|
requestUUID: action.payload.requestUUID,
|
||||||
|
isDirty: (hasValue(existing) ? isNotEmpty(existing.patches) : false),
|
||||||
|
patches: (hasValue(existing) ? existing.patches : [])
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -143,3 +166,49 @@ function resetObjectCacheTimestamps(state: ObjectCacheState, action: ResetObject
|
|||||||
});
|
});
|
||||||
return newState;
|
return newState;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add the list of patch operations to a cached object
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* an AddPatchObjectCacheAction
|
||||||
|
* @return ObjectCacheState
|
||||||
|
* the new state, with the new operations added to the state of the specified ObjectCacheEntry
|
||||||
|
*/
|
||||||
|
function addPatchObjectCache(state: ObjectCacheState, action: AddPatchObjectCacheAction): ObjectCacheState {
|
||||||
|
const uuid = action.payload.href;
|
||||||
|
const operations = action.payload.operations;
|
||||||
|
const newState = Object.assign({}, state);
|
||||||
|
if (hasValue(newState[uuid])) {
|
||||||
|
const patches = newState[uuid].patches;
|
||||||
|
newState[uuid] = Object.assign({}, newState[uuid], {
|
||||||
|
patches: [...patches, { operations } as Patch],
|
||||||
|
isDirty: true
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the list of patch operations to a cached object
|
||||||
|
*
|
||||||
|
* @param state
|
||||||
|
* the current state
|
||||||
|
* @param action
|
||||||
|
* an ApplyPatchObjectCacheAction
|
||||||
|
* @return ObjectCacheState
|
||||||
|
* the new state, with the new operations applied to the state of the specified ObjectCacheEntry
|
||||||
|
*/
|
||||||
|
function applyPatchObjectCache(state: ObjectCacheState, action: ApplyPatchObjectCacheAction): ObjectCacheState {
|
||||||
|
const uuid = action.payload;
|
||||||
|
const newState = Object.assign({}, state);
|
||||||
|
if (hasValue(newState[uuid])) {
|
||||||
|
// flatten two dimensional array
|
||||||
|
const flatPatch: Operation[] = [].concat(...newState[uuid].patches.map((patch) => patch.operations));
|
||||||
|
const newData = applyPatch(newState[uuid].data, flatPatch, undefined, false);
|
||||||
|
newState[uuid] = Object.assign({}, newState[uuid], { data: newData.newDocument, patches: [] });
|
||||||
|
}
|
||||||
|
return newState;
|
||||||
|
}
|
||||||
|
106
src/app/core/cache/object-cache.service.spec.ts
vendored
106
src/app/core/cache/object-cache.service.spec.ts
vendored
@@ -1,31 +1,53 @@
|
|||||||
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 {
|
||||||
|
AddPatchObjectCacheAction,
|
||||||
|
AddToObjectCacheAction, ApplyPatchObjectCacheAction,
|
||||||
|
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';
|
||||||
|
import { Operation } from '../../../../node_modules/fast-json-patch';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
import { AddToSSBAction } from './server-sync-buffer.actions';
|
||||||
|
import { Patch } from './object-cache.reducer';
|
||||||
|
|
||||||
describe('ObjectCacheService', () => {
|
describe('ObjectCacheService', () => {
|
||||||
let service: ObjectCacheService;
|
let service: ObjectCacheService;
|
||||||
let store: Store<CoreState>;
|
let store: Store<CoreState>;
|
||||||
|
|
||||||
const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
|
const requestUUID = '4d3a4ce8-a375-4b98-859b-39f0a014d736';
|
||||||
const timestamp = new Date().getTime();
|
const timestamp = new Date().getTime();
|
||||||
const msToLive = 900000;
|
const msToLive = 900000;
|
||||||
const objectToCache = {
|
let objectToCache = {
|
||||||
self: selfLink,
|
self: selfLink,
|
||||||
type: ResourceType.Item
|
type: ResourceType.Item
|
||||||
};
|
};
|
||||||
const cacheEntry = {
|
let cacheEntry;
|
||||||
data: objectToCache,
|
let invalidCacheEntry;
|
||||||
timeAdded: timestamp,
|
const operations = [{ op: 'replace', path: '/name', value: 'random string' } as Operation];
|
||||||
msToLive: msToLive
|
|
||||||
};
|
function init() {
|
||||||
const invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 });
|
objectToCache = {
|
||||||
|
self: selfLink,
|
||||||
|
type: ResourceType.Item
|
||||||
|
};
|
||||||
|
cacheEntry = {
|
||||||
|
data: objectToCache,
|
||||||
|
timeAdded: timestamp,
|
||||||
|
msToLive: msToLive
|
||||||
|
};
|
||||||
|
invalidCacheEntry = Object.assign({}, cacheEntry, { msToLive: -1 })
|
||||||
|
}
|
||||||
|
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
|
init();
|
||||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
store = new Store<CoreState>(undefined, undefined, undefined);
|
||||||
spyOn(store, 'dispatch');
|
spyOn(store, 'dispatch');
|
||||||
service = new ObjectCacheService(store);
|
service = new ObjectCacheService(store);
|
||||||
@@ -37,8 +59,8 @@ describe('ObjectCacheService', () => {
|
|||||||
|
|
||||||
describe('add', () => {
|
describe('add', () => {
|
||||||
it('should dispatch an ADD action with the object to add, the time to live, and the current timestamp', () => {
|
it('should dispatch an ADD action with the object to add, the time to live, and the current timestamp', () => {
|
||||||
service.add(objectToCache, msToLive, selfLink);
|
service.add(objectToCache, msToLive, requestUUID);
|
||||||
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, selfLink));
|
expect(store.dispatch).toHaveBeenCalledWith(new AddToObjectCacheAction(objectToCache, timestamp, msToLive, requestUUID));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -51,10 +73,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 +89,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 +106,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,22 +117,60 @@ 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);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('patch methods', () => {
|
||||||
|
it('should dispatch the correct actions when addPatch is called', () => {
|
||||||
|
service.addPatch(selfLink, operations);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new AddPatchObjectCacheAction(selfLink, operations));
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new AddToSSBAction(selfLink, RestRequestMethod.PATCH));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isDirty should return true when the patches list in the cache entry is not empty', () => {
|
||||||
|
cacheEntry.patches = [
|
||||||
|
{
|
||||||
|
operations: operations
|
||||||
|
} as Patch];
|
||||||
|
const result = (service as any).isDirty(cacheEntry);
|
||||||
|
expect(result).toBe(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('isDirty should return false when the patches list in the cache entry is empty', () => {
|
||||||
|
cacheEntry.patches = [];
|
||||||
|
const result = (service as any).isDirty(cacheEntry);
|
||||||
|
expect(result).toBe(false);
|
||||||
|
});
|
||||||
|
it('should dispatch the correct actions when applyPatchesToCachedObject is called', () => {
|
||||||
|
(service as any).applyPatchesToCachedObject(selfLink);
|
||||||
|
expect(store.dispatch).toHaveBeenCalledWith(new ApplyPatchObjectCacheAction(selfLink));
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
140
src/app/core/cache/object-cache.service.ts
vendored
140
src/app/core/cache/object-cache.service.ts
vendored
@@ -1,25 +1,33 @@
|
|||||||
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, } 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 {
|
||||||
import { hasNoValue } from '../../shared/empty.util';
|
AddPatchObjectCacheAction,
|
||||||
|
AddToObjectCacheAction,
|
||||||
|
ApplyPatchObjectCacheAction,
|
||||||
|
RemoveFromObjectCacheAction
|
||||||
|
} from './object-cache.actions';
|
||||||
|
import { hasNoValue, isNotEmpty } 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';
|
||||||
|
import { applyPatch, Operation } from 'fast-json-patch';
|
||||||
|
import { AddToSSBAction } from './server-sync-buffer.actions';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
|
||||||
function selfLinkFromUuidSelector(uuid: string): MemoizedSelector<CoreState, string> {
|
function selfLinkFromUuidSelector(uuid: string): MemoizedSelector<CoreState, string> {
|
||||||
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.OBJECT, uuid);
|
return pathSelector<CoreState, string>(coreSelector, 'index', IndexName.OBJECT, uuid);
|
||||||
}
|
}
|
||||||
|
|
||||||
function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector<CoreState, ObjectCacheEntry> {
|
function entryFromSelfLinkSelector(selfLink: string): MemoizedSelector<CoreState, ObjectCacheEntry> {
|
||||||
return pathSelector<CoreState, ObjectCacheEntry>(coreSelector, 'data/object', selfLink);
|
return pathSelector<CoreState, ObjectCacheEntry>(coreSelector, 'cache/object', selfLink);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -37,20 +45,18 @@ export class ObjectCacheService {
|
|||||||
* The object to add
|
* The object to add
|
||||||
* @param msToLive
|
* @param msToLive
|
||||||
* The number of milliseconds it should be cached for
|
* The number of milliseconds it should be cached for
|
||||||
* @param requestHref
|
* @param requestUUID
|
||||||
* The selfLink of the request that resulted in this object
|
* The UUID of the request that resulted in this object
|
||||||
* This isn't necessarily the same as the object's self
|
|
||||||
* link, it could have been part of a list for example
|
|
||||||
*/
|
*/
|
||||||
add(objectToCache: CacheableObject, msToLive: number, requestHref: string): void {
|
add(objectToCache: CacheableObject, msToLive: number, requestUUID: string): void {
|
||||||
this.store.dispatch(new AddToObjectCacheAction(objectToCache, new Date().getTime(), msToLive, requestHref));
|
this.store.dispatch(new AddToObjectCacheAction(objectToCache, new Date().getTime(), msToLive, requestUUID));
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Remove the object with the supplied UUID from the cache
|
* Remove the object with the supplied href from the cache
|
||||||
*
|
*
|
||||||
* @param uuid
|
* @param href
|
||||||
* The UUID of the object to be removed
|
* The unique href of the object to be removed
|
||||||
*/
|
*/
|
||||||
remove(uuid: string): void {
|
remove(uuid: string): void {
|
||||||
this.store.dispatch(new RemoveFromObjectCacheAction(uuid));
|
this.store.dispatch(new RemoveFromObjectCacheAction(uuid));
|
||||||
@@ -73,33 +79,51 @@ 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);
|
if (isNotEmpty(entry.patches)) {
|
||||||
|
const flatPatch: Operation[] = [].concat(...entry.patches.map((patch) => patch.operations));
|
||||||
|
const patchedData = applyPatch(entry.data, flatPatch, undefined, false).newDocument;
|
||||||
|
return Object.assign({}, entry, { data: patchedData });
|
||||||
|
} else {
|
||||||
|
return entry;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
),
|
||||||
|
map((entry: ObjectCacheEntry) => {
|
||||||
|
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> {
|
getRequestUUIDBySelfLink(selfLink: string): Observable<string> {
|
||||||
return this.getEntry(selfLink)
|
return this.getEntry(selfLink).pipe(
|
||||||
.map((entry: ObjectCacheEntry) => entry.requestHref)
|
map((entry: ObjectCacheEntry) => entry.requestUUID),
|
||||||
.distinctUntilChanged();
|
distinctUntilChanged());
|
||||||
}
|
}
|
||||||
|
|
||||||
getRequestHrefByUUID(uuid: string): Observable<string> {
|
getRequestUUIDByObjectUUID(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.getRequestUUIDBySelfLink(selfLink))
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -122,7 +146,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 +163,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 +183,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;
|
||||||
}
|
}
|
||||||
@@ -187,4 +212,39 @@ export class ObjectCacheService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add operations to the existing list of operations for an ObjectCacheEntry
|
||||||
|
* Makes sure the ServerSyncBuffer for this ObjectCacheEntry is updated
|
||||||
|
* @param {string} uuid
|
||||||
|
* the uuid of the ObjectCacheEntry
|
||||||
|
* @param {Operation[]} patch
|
||||||
|
* list of operations to perform
|
||||||
|
*/
|
||||||
|
public addPatch(selfLink: string, patch: Operation[]) {
|
||||||
|
this.store.dispatch(new AddPatchObjectCacheAction(selfLink, patch));
|
||||||
|
this.store.dispatch(new AddToSSBAction(selfLink, RestRequestMethod.PATCH));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check whether there are any unperformed operations for an ObjectCacheEntry
|
||||||
|
*
|
||||||
|
* @param entry
|
||||||
|
* the entry to check
|
||||||
|
* @return boolean
|
||||||
|
* false if the entry is there are no operations left in the ObjectCacheEntry, true otherwise
|
||||||
|
*/
|
||||||
|
private isDirty(entry: ObjectCacheEntry): boolean {
|
||||||
|
return isNotEmpty(entry.patches);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Apply the existing operations on an ObjectCacheEntry in the store
|
||||||
|
* NB: this does not make any server side changes
|
||||||
|
* @param {string} uuid
|
||||||
|
* the uuid of the ObjectCacheEntry
|
||||||
|
*/
|
||||||
|
private applyPatchesToCachedObject(selfLink: string) {
|
||||||
|
this.store.dispatch(new ApplyPatchObjectCacheAction(selfLink));
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
72
src/app/core/cache/response-cache.actions.ts
vendored
72
src/app/core/cache/response-cache.actions.ts
vendored
@@ -1,72 +0,0 @@
|
|||||||
import { Action } from '@ngrx/store';
|
|
||||||
|
|
||||||
import { type } from '../../shared/ngrx/type';
|
|
||||||
import { RestResponse } from './response-cache.models';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The list of ResponseCacheAction type definitions
|
|
||||||
*/
|
|
||||||
export const ResponseCacheActionTypes = {
|
|
||||||
ADD: type('dspace/core/cache/response/ADD'),
|
|
||||||
REMOVE: type('dspace/core/cache/response/REMOVE'),
|
|
||||||
RESET_TIMESTAMPS: type('dspace/core/cache/response/RESET_TIMESTAMPS')
|
|
||||||
};
|
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
|
||||||
export class ResponseCacheAddAction implements Action {
|
|
||||||
type = ResponseCacheActionTypes.ADD;
|
|
||||||
payload: {
|
|
||||||
key: string,
|
|
||||||
response: RestResponse
|
|
||||||
timeAdded: number;
|
|
||||||
msToLive: number;
|
|
||||||
};
|
|
||||||
|
|
||||||
constructor(key: string, response: RestResponse, timeAdded: number, msToLive: number) {
|
|
||||||
this.payload = { key, response, timeAdded, msToLive };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ngrx action to remove a request from the cache
|
|
||||||
*/
|
|
||||||
export class ResponseCacheRemoveAction implements Action {
|
|
||||||
type = ResponseCacheActionTypes.REMOVE;
|
|
||||||
payload: string;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new ResponseCacheRemoveAction
|
|
||||||
* @param key
|
|
||||||
* The key of the request to remove
|
|
||||||
*/
|
|
||||||
constructor(key: string) {
|
|
||||||
this.payload = key;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An ngrx action to reset the timeAdded property of all cached objects
|
|
||||||
*/
|
|
||||||
export class ResetResponseCacheTimestampsAction implements Action {
|
|
||||||
type = ResponseCacheActionTypes.RESET_TIMESTAMPS;
|
|
||||||
payload: number;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create a new ResetObjectCacheTimestampsAction
|
|
||||||
*
|
|
||||||
* @param newTimestamp
|
|
||||||
* the new timeAdded all objects should get
|
|
||||||
*/
|
|
||||||
constructor(newTimestamp: number) {
|
|
||||||
this.payload = newTimestamp;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/* tslint:enable:max-classes-per-file */
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A type to encompass all ResponseCacheActions
|
|
||||||
*/
|
|
||||||
export type ResponseCacheAction
|
|
||||||
= ResponseCacheAddAction
|
|
||||||
| ResponseCacheRemoveAction
|
|
||||||
| ResetResponseCacheTimestampsAction;
|
|
@@ -1,38 +0,0 @@
|
|||||||
import { TestBed } from '@angular/core/testing';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
import { provideMockActions } from '@ngrx/effects/testing';
|
|
||||||
import { cold, hot } from 'jasmine-marbles';
|
|
||||||
import { StoreActionTypes } from '../../store.actions';
|
|
||||||
import { ResponseCacheEffects } from './response-cache.effects';
|
|
||||||
import { ResetResponseCacheTimestampsAction } from './response-cache.actions';
|
|
||||||
|
|
||||||
describe('ResponseCacheEffects', () => {
|
|
||||||
let cacheEffects: ResponseCacheEffects;
|
|
||||||
let actions: Observable<any>;
|
|
||||||
const timestamp = 10000;
|
|
||||||
beforeEach(() => {
|
|
||||||
TestBed.configureTestingModule({
|
|
||||||
providers: [
|
|
||||||
ResponseCacheEffects,
|
|
||||||
provideMockActions(() => actions),
|
|
||||||
// other providers
|
|
||||||
],
|
|
||||||
});
|
|
||||||
|
|
||||||
cacheEffects = TestBed.get(ResponseCacheEffects);
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('fixTimestampsOnRehydrate$', () => {
|
|
||||||
|
|
||||||
it('should return a RESET_TIMESTAMPS action in response to a REHYDRATE action', () => {
|
|
||||||
spyOn(Date.prototype, 'getTime').and.callFake(() => {
|
|
||||||
return timestamp;
|
|
||||||
});
|
|
||||||
actions = hot('--a-', { a: { type: StoreActionTypes.REHYDRATE, payload: {} } });
|
|
||||||
|
|
||||||
const expected = cold('--b-', { b: new ResetResponseCacheTimestampsAction(new Date().getTime()) });
|
|
||||||
|
|
||||||
expect(cacheEffects.fixTimestampsOnRehydrate).toBeObservable(expected);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
24
src/app/core/cache/response-cache.effects.ts
vendored
24
src/app/core/cache/response-cache.effects.ts
vendored
@@ -1,24 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { Actions, Effect } from '@ngrx/effects';
|
|
||||||
|
|
||||||
import { ResetResponseCacheTimestampsAction } from './response-cache.actions';
|
|
||||||
import { StoreActionTypes } from '../../store.actions';
|
|
||||||
|
|
||||||
@Injectable()
|
|
||||||
export class ResponseCacheEffects {
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When the store is rehydrated in the browser, set all cache
|
|
||||||
* timestamps to 'now', because the time zone of the server can
|
|
||||||
* differ from the client.
|
|
||||||
*
|
|
||||||
* This assumes that the server cached everything a negligible
|
|
||||||
* time ago, and will likely need to be revisited later
|
|
||||||
*/
|
|
||||||
@Effect() fixTimestampsOnRehydrate = this.actions$
|
|
||||||
.ofType(StoreActionTypes.REHYDRATE)
|
|
||||||
.map(() => new ResetResponseCacheTimestampsAction(new Date().getTime()));
|
|
||||||
|
|
||||||
constructor(private actions$: Actions, ) { }
|
|
||||||
|
|
||||||
}
|
|
124
src/app/core/cache/response-cache.reducer.spec.ts
vendored
124
src/app/core/cache/response-cache.reducer.spec.ts
vendored
@@ -1,124 +0,0 @@
|
|||||||
import * as deepFreeze from 'deep-freeze';
|
|
||||||
|
|
||||||
import { responseCacheReducer, ResponseCacheState } from './response-cache.reducer';
|
|
||||||
|
|
||||||
import {
|
|
||||||
ResponseCacheRemoveAction,
|
|
||||||
ResetResponseCacheTimestampsAction, ResponseCacheAddAction
|
|
||||||
} from './response-cache.actions';
|
|
||||||
import { RestResponse } from './response-cache.models';
|
|
||||||
|
|
||||||
class NullAction extends ResponseCacheRemoveAction {
|
|
||||||
type = null;
|
|
||||||
payload = null;
|
|
||||||
|
|
||||||
constructor() {
|
|
||||||
super(null);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
describe('responseCacheReducer', () => {
|
|
||||||
const keys = ['125c17f89046283c5f0640722aac9feb', 'a06c3006a41caec5d635af099b0c780c'];
|
|
||||||
const msToLive = 900000;
|
|
||||||
const uuids = [
|
|
||||||
'9e32a2e2-6b91-4236-a361-995ccdc14c60',
|
|
||||||
'598ce822-c357-46f3-ab70-63724d02d6ad',
|
|
||||||
'be8325f7-243b-49f4-8a4b-df2b793ff3b5'
|
|
||||||
];
|
|
||||||
const testState: ResponseCacheState = {
|
|
||||||
[keys[0]]: {
|
|
||||||
key: keys[0],
|
|
||||||
response: new RestResponse(true, '200'),
|
|
||||||
timeAdded: new Date().getTime(),
|
|
||||||
msToLive: msToLive
|
|
||||||
},
|
|
||||||
[keys[1]]: {
|
|
||||||
key: keys[1],
|
|
||||||
response: new RestResponse(true, '200'),
|
|
||||||
timeAdded: new Date().getTime(),
|
|
||||||
msToLive: msToLive
|
|
||||||
}
|
|
||||||
};
|
|
||||||
deepFreeze(testState);
|
|
||||||
const errorState: {} = {
|
|
||||||
[keys[0]]: {
|
|
||||||
errorMessage: 'error',
|
|
||||||
resourceUUIDs: uuids
|
|
||||||
}
|
|
||||||
};
|
|
||||||
deepFreeze(errorState);
|
|
||||||
|
|
||||||
it('should return the current state when no valid actions have been made', () => {
|
|
||||||
const action = new NullAction();
|
|
||||||
const newState = responseCacheReducer(testState, action);
|
|
||||||
|
|
||||||
expect(newState).toEqual(testState);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should start with an empty cache', () => {
|
|
||||||
const action = new NullAction();
|
|
||||||
const initialState = responseCacheReducer(undefined, action);
|
|
||||||
|
|
||||||
expect(initialState).toEqual(Object.create(null));
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('ADD', () => {
|
|
||||||
const addTimeAdded = new Date().getTime();
|
|
||||||
const addMsToLive = 5;
|
|
||||||
const addResponse = new RestResponse(true, '200');
|
|
||||||
const action = new ResponseCacheAddAction(keys[0], addResponse, addTimeAdded, addMsToLive);
|
|
||||||
|
|
||||||
it('should perform the action without affecting the previous state', () => {
|
|
||||||
// testState has already been frozen above
|
|
||||||
responseCacheReducer(testState, action);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should add the response to the cached request', () => {
|
|
||||||
const newState = responseCacheReducer(testState, action);
|
|
||||||
expect(newState[keys[0]].timeAdded).toBe(addTimeAdded);
|
|
||||||
expect(newState[keys[0]].msToLive).toBe(addMsToLive);
|
|
||||||
expect(newState[keys[0]].response).toBe(addResponse);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('REMOVE', () => {
|
|
||||||
it('should perform the action without affecting the previous state', () => {
|
|
||||||
const action = new ResponseCacheRemoveAction(keys[0]);
|
|
||||||
// testState has already been frozen above
|
|
||||||
responseCacheReducer(testState, action);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should remove the specified request from the cache', () => {
|
|
||||||
const action = new ResponseCacheRemoveAction(keys[0]);
|
|
||||||
const newState = responseCacheReducer(testState, action);
|
|
||||||
expect(testState[keys[0]]).not.toBeUndefined();
|
|
||||||
expect(newState[keys[0]]).toBeUndefined();
|
|
||||||
});
|
|
||||||
|
|
||||||
it('shouldn\'t do anything when the specified key isn\'t cached', () => {
|
|
||||||
const wrongKey = 'this isn\'t cached';
|
|
||||||
const action = new ResponseCacheRemoveAction(wrongKey);
|
|
||||||
const newState = responseCacheReducer(testState, action);
|
|
||||||
expect(testState[wrongKey]).toBeUndefined();
|
|
||||||
expect(newState).toEqual(testState);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('RESET_TIMESTAMPS', () => {
|
|
||||||
const newTimeStamp = new Date().getTime();
|
|
||||||
const action = new ResetResponseCacheTimestampsAction(newTimeStamp);
|
|
||||||
|
|
||||||
it('should perform the action without affecting the previous state', () => {
|
|
||||||
// testState has already been frozen above
|
|
||||||
responseCacheReducer(testState, action);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should set the timestamp of all requests in the cache', () => {
|
|
||||||
const newState = responseCacheReducer(testState, action);
|
|
||||||
Object.keys(newState).forEach((key) => {
|
|
||||||
expect(newState[key].timeAdded).toEqual(newTimeStamp);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
|
||||||
});
|
|
111
src/app/core/cache/response-cache.reducer.ts
vendored
111
src/app/core/cache/response-cache.reducer.ts
vendored
@@ -1,111 +0,0 @@
|
|||||||
import {
|
|
||||||
ResponseCacheAction, ResponseCacheActionTypes,
|
|
||||||
ResponseCacheRemoveAction, ResetResponseCacheTimestampsAction,
|
|
||||||
ResponseCacheAddAction
|
|
||||||
} from './response-cache.actions';
|
|
||||||
import { CacheEntry } from './cache-entry';
|
|
||||||
import { hasValue } from '../../shared/empty.util';
|
|
||||||
import { RestResponse } from './response-cache.models';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* An entry in the ResponseCache
|
|
||||||
*/
|
|
||||||
export class ResponseCacheEntry implements CacheEntry {
|
|
||||||
key: string;
|
|
||||||
response: RestResponse;
|
|
||||||
timeAdded: number;
|
|
||||||
msToLive: number;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ResponseCache State
|
|
||||||
*/
|
|
||||||
export interface ResponseCacheState {
|
|
||||||
[key: string]: ResponseCacheEntry
|
|
||||||
}
|
|
||||||
|
|
||||||
// Object.create(null) ensures the object has no default js properties (e.g. `__proto__`)
|
|
||||||
const initialState = Object.create(null);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* The ResponseCache Reducer
|
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* the current state
|
|
||||||
* @param action
|
|
||||||
* the action to perform on the state
|
|
||||||
* @return ResponseCacheState
|
|
||||||
* the new state
|
|
||||||
*/
|
|
||||||
export function responseCacheReducer(state = initialState, action: ResponseCacheAction): ResponseCacheState {
|
|
||||||
switch (action.type) {
|
|
||||||
|
|
||||||
case ResponseCacheActionTypes.ADD: {
|
|
||||||
return addToCache(state, action as ResponseCacheAddAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
case ResponseCacheActionTypes.REMOVE: {
|
|
||||||
return removeFromCache(state, action as ResponseCacheRemoveAction);
|
|
||||||
}
|
|
||||||
|
|
||||||
case ResponseCacheActionTypes.RESET_TIMESTAMPS: {
|
|
||||||
return resetResponseCacheTimestamps(state, action as ResetResponseCacheTimestampsAction)
|
|
||||||
}
|
|
||||||
|
|
||||||
default: {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
function addToCache(state: ResponseCacheState, action: ResponseCacheAddAction): ResponseCacheState {
|
|
||||||
return Object.assign({}, state, {
|
|
||||||
[action.payload.key]: {
|
|
||||||
key: action.payload.key,
|
|
||||||
response: action.payload.response,
|
|
||||||
timeAdded: action.payload.timeAdded,
|
|
||||||
msToLive: action.payload.msToLive
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Remove a request from the cache
|
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* the current state
|
|
||||||
* @param action
|
|
||||||
* an ResponseCacheRemoveAction
|
|
||||||
* @return ResponseCacheState
|
|
||||||
* the new state, with the request removed if it existed.
|
|
||||||
*/
|
|
||||||
function removeFromCache(state: ResponseCacheState, action: ResponseCacheRemoveAction): ResponseCacheState {
|
|
||||||
if (hasValue(state[action.payload])) {
|
|
||||||
const newCache = Object.assign({}, state);
|
|
||||||
delete newCache[action.payload];
|
|
||||||
|
|
||||||
return newCache;
|
|
||||||
} else {
|
|
||||||
return state;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Set the timeAdded timestamp of every cached request to the specified value
|
|
||||||
*
|
|
||||||
* @param state
|
|
||||||
* the current state
|
|
||||||
* @param action
|
|
||||||
* a ResetResponseCacheTimestampsAction
|
|
||||||
* @return ResponseCacheState
|
|
||||||
* the new state, with all timeAdded timestamps set to the specified value
|
|
||||||
*/
|
|
||||||
function resetResponseCacheTimestamps(state: ResponseCacheState, action: ResetResponseCacheTimestampsAction): ResponseCacheState {
|
|
||||||
const newState = Object.create(null);
|
|
||||||
Object.keys(state).forEach((key) => {
|
|
||||||
newState[key] = Object.assign({}, state[key], {
|
|
||||||
timeAdded: action.payload
|
|
||||||
});
|
|
||||||
});
|
|
||||||
return newState;
|
|
||||||
}
|
|
@@ -1,82 +0,0 @@
|
|||||||
import { Store } from '@ngrx/store';
|
|
||||||
|
|
||||||
import { ResponseCacheService } from './response-cache.service';
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
import { CoreState } from '../core.reducers';
|
|
||||||
import { RestResponse } from './response-cache.models';
|
|
||||||
import { ResponseCacheEntry } from './response-cache.reducer';
|
|
||||||
|
|
||||||
describe('ResponseCacheService', () => {
|
|
||||||
let service: ResponseCacheService;
|
|
||||||
let store: Store<CoreState>;
|
|
||||||
|
|
||||||
const keys = ['125c17f89046283c5f0640722aac9feb', 'a06c3006a41caec5d635af099b0c780c'];
|
|
||||||
const timestamp = new Date().getTime();
|
|
||||||
const validCacheEntry = (key) => {
|
|
||||||
return {
|
|
||||||
key: key,
|
|
||||||
response: new RestResponse(true, '200'),
|
|
||||||
timeAdded: timestamp,
|
|
||||||
msToLive: 24 * 60 * 60 * 1000 // a day
|
|
||||||
}
|
|
||||||
};
|
|
||||||
const invalidCacheEntry = (key) => {
|
|
||||||
return {
|
|
||||||
key: key,
|
|
||||||
response: new RestResponse(true, '200'),
|
|
||||||
timeAdded: 0,
|
|
||||||
msToLive: 0
|
|
||||||
}
|
|
||||||
};
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
store = new Store<CoreState>(undefined, undefined, undefined);
|
|
||||||
spyOn(store, 'dispatch');
|
|
||||||
service = new ResponseCacheService(store);
|
|
||||||
spyOn(Date.prototype, 'getTime').and.callFake(() => {
|
|
||||||
return timestamp;
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('get', () => {
|
|
||||||
it('should return an observable of the cached request with the specified key', () => {
|
|
||||||
spyOn(store, 'select').and.callFake((...args: any[]) => {
|
|
||||||
return Observable.of(validCacheEntry(keys[1]));
|
|
||||||
});
|
|
||||||
|
|
||||||
let testObj: ResponseCacheEntry;
|
|
||||||
service.get(keys[1]).first().subscribe((entry) => {
|
|
||||||
testObj = entry;
|
|
||||||
});
|
|
||||||
expect(testObj.key).toEqual(keys[1]);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should not return a cached request that has exceeded its time to live', () => {
|
|
||||||
spyOn(store, 'select').and.callFake((...args: any[]) => {
|
|
||||||
return Observable.of(invalidCacheEntry(keys[1]));
|
|
||||||
});
|
|
||||||
|
|
||||||
let getObsHasFired = false;
|
|
||||||
const subscription = service.get(keys[1]).subscribe((entry) => getObsHasFired = true);
|
|
||||||
expect(getObsHasFired).toBe(false);
|
|
||||||
subscription.unsubscribe();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('has', () => {
|
|
||||||
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])));
|
|
||||||
expect(service.has(keys[1])).toBe(true);
|
|
||||||
});
|
|
||||||
|
|
||||||
it('should return false if the request with the supplied key isn\'t cached', () => {
|
|
||||||
spyOn(store, 'select').and.returnValue(Observable.of(undefined));
|
|
||||||
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', () => {
|
|
||||||
spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry(keys[1])));
|
|
||||||
expect(service.has(keys[1])).toBe(false);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
95
src/app/core/cache/response-cache.service.ts
vendored
95
src/app/core/cache/response-cache.service.ts
vendored
@@ -1,95 +0,0 @@
|
|||||||
import { Injectable } from '@angular/core';
|
|
||||||
import { MemoizedSelector, Store } from '@ngrx/store';
|
|
||||||
|
|
||||||
import { Observable } from 'rxjs/Observable';
|
|
||||||
|
|
||||||
import { ResponseCacheEntry } from './response-cache.reducer';
|
|
||||||
import { hasNoValue } from '../../shared/empty.util';
|
|
||||||
import { ResponseCacheRemoveAction, ResponseCacheAddAction } from './response-cache.actions';
|
|
||||||
import { RestResponse } from './response-cache.models';
|
|
||||||
import { coreSelector, CoreState } from '../core.reducers';
|
|
||||||
import { pathSelector } from '../shared/selectors';
|
|
||||||
|
|
||||||
function entryFromKeySelector(key: string): MemoizedSelector<CoreState, ResponseCacheEntry> {
|
|
||||||
return pathSelector<CoreState, ResponseCacheEntry>(coreSelector, 'data/response', key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* A service to interact with the response cache
|
|
||||||
*/
|
|
||||||
@Injectable()
|
|
||||||
export class ResponseCacheService {
|
|
||||||
constructor(
|
|
||||||
private store: Store<CoreState>
|
|
||||||
) { }
|
|
||||||
|
|
||||||
add(key: string, response: RestResponse, msToLive: number): Observable<ResponseCacheEntry> {
|
|
||||||
if (!this.has(key)) {
|
|
||||||
this.store.dispatch(new ResponseCacheAddAction(key, response, new Date().getTime(), msToLive));
|
|
||||||
}
|
|
||||||
return this.get(key);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an observable of the response with the specified key
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* the key of the response to get
|
|
||||||
* @return Observable<ResponseCacheEntry>
|
|
||||||
* an observable of the ResponseCacheEntry with the specified key
|
|
||||||
*/
|
|
||||||
get(key: string): Observable<ResponseCacheEntry> {
|
|
||||||
return this.store.select(entryFromKeySelector(key))
|
|
||||||
.filter((entry: ResponseCacheEntry) => this.isValid(entry))
|
|
||||||
.distinctUntilChanged()
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check whether the response with the specified key is cached
|
|
||||||
*
|
|
||||||
* @param key
|
|
||||||
* the key of the response to check
|
|
||||||
* @return boolean
|
|
||||||
* true if the response with the specified key is cached,
|
|
||||||
* false otherwise
|
|
||||||
*/
|
|
||||||
has(key: string): boolean {
|
|
||||||
let result: boolean;
|
|
||||||
|
|
||||||
this.store.select(entryFromKeySelector(key))
|
|
||||||
.take(1)
|
|
||||||
.subscribe((entry: ResponseCacheEntry) => {
|
|
||||||
result = this.isValid(entry);
|
|
||||||
});
|
|
||||||
|
|
||||||
return result;
|
|
||||||
}
|
|
||||||
|
|
||||||
remove(key: string): void {
|
|
||||||
if (this.has(key)) {
|
|
||||||
this.store.dispatch(new ResponseCacheRemoveAction(key));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/**
|
|
||||||
* Check whether a ResponseCacheEntry should still be cached
|
|
||||||
*
|
|
||||||
* @param entry
|
|
||||||
* the entry to check
|
|
||||||
* @return boolean
|
|
||||||
* false if the entry is null, undefined, or its time to
|
|
||||||
* live has been exceeded, true otherwise
|
|
||||||
*/
|
|
||||||
private isValid(entry: ResponseCacheEntry): boolean {
|
|
||||||
if (hasNoValue(entry)) {
|
|
||||||
return false;
|
|
||||||
} else {
|
|
||||||
const timeOutdated = entry.timeAdded + entry.msToLive;
|
|
||||||
const isOutDated = new Date().getTime() > timeOutdated;
|
|
||||||
if (isOutDated) {
|
|
||||||
this.store.dispatch(new ResponseCacheRemoveAction(entry.key));
|
|
||||||
}
|
|
||||||
return !isOutDated;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
@@ -13,7 +13,7 @@ import { AuthStatus } from '../auth/models/auth-status.model';
|
|||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
export class RestResponse {
|
export class RestResponse {
|
||||||
public toCache = true;
|
public timeAdded: number;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
public isSuccessful: boolean,
|
public isSuccessful: boolean,
|
||||||
@@ -140,7 +140,7 @@ export class ErrorResponse extends RestResponse {
|
|||||||
|
|
||||||
constructor(error: RequestError) {
|
constructor(error: RequestError) {
|
||||||
super(false, error.statusText);
|
super(false, error.statusText);
|
||||||
console.error(error);
|
// console.error(error);
|
||||||
this.errorMessage = error.message;
|
this.errorMessage = error.message;
|
||||||
}
|
}
|
||||||
}
|
}
|
82
src/app/core/cache/server-sync-buffer.actions.ts
vendored
Normal file
82
src/app/core/cache/server-sync-buffer.actions.ts
vendored
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
|
||||||
|
import { type } from '../../shared/ngrx/type';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of ServerSyncBufferAction type definitions
|
||||||
|
*/
|
||||||
|
export const ServerSyncBufferActionTypes = {
|
||||||
|
ADD: type('dspace/core/cache/syncbuffer/ADD'),
|
||||||
|
COMMIT: type('dspace/core/cache/syncbuffer/COMMIT'),
|
||||||
|
EMPTY: type('dspace/core/cache/syncbuffer/EMPTY'),
|
||||||
|
};
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to add a new cached object to the server sync buffer
|
||||||
|
*/
|
||||||
|
export class AddToSSBAction implements Action {
|
||||||
|
type = ServerSyncBufferActionTypes.ADD;
|
||||||
|
payload: {
|
||||||
|
href: string,
|
||||||
|
method: RestRequestMethod
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new AddToSSBAction
|
||||||
|
*
|
||||||
|
* @param href
|
||||||
|
* the unique href of the cached object entry that should be updated
|
||||||
|
*/
|
||||||
|
constructor(href: string, method: RestRequestMethod) {
|
||||||
|
this.payload = { href, method: method };
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An ngrx action to commit everything (for a certain method, when specified) in the ServerSyncBuffer to the server
|
||||||
|
*/
|
||||||
|
export class CommitSSBAction implements Action {
|
||||||
|
type = ServerSyncBufferActionTypes.COMMIT;
|
||||||
|
payload?: RestRequestMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new CommitSSBAction
|
||||||
|
*
|
||||||
|
* @param method
|
||||||
|
* an optional method for which the ServerSyncBuffer should send its entries to the server
|
||||||
|
*/
|
||||||
|
constructor(method?: RestRequestMethod) {
|
||||||
|
this.payload = method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* An ngrx action to remove everything (for a certain method, when specified) from the ServerSyncBuffer to the server
|
||||||
|
*/
|
||||||
|
export class EmptySSBAction implements Action {
|
||||||
|
type = ServerSyncBufferActionTypes.EMPTY;
|
||||||
|
payload?: RestRequestMethod;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create a new EmptySSBAction
|
||||||
|
*
|
||||||
|
* @param method
|
||||||
|
* an optional method for which the ServerSyncBuffer should remove its entries
|
||||||
|
* if this parameter is omitted, the buffer will be emptied as a whole
|
||||||
|
*/
|
||||||
|
constructor(method?: RestRequestMethod) {
|
||||||
|
this.payload = method;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tslint:enable:max-classes-per-file */
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A type to encompass all ServerSyncBufferActions
|
||||||
|
*/
|
||||||
|
export type ServerSyncBufferAction
|
||||||
|
= AddToSSBAction
|
||||||
|
| CommitSSBAction
|
||||||
|
| EmptySSBAction
|
139
src/app/core/cache/server-sync-buffer.effects.spec.ts
vendored
Normal file
139
src/app/core/cache/server-sync-buffer.effects.spec.ts
vendored
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
import { TestBed } from '@angular/core/testing';
|
||||||
|
import { Observable, of as observableOf } from 'rxjs';
|
||||||
|
import { provideMockActions } from '@ngrx/effects/testing';
|
||||||
|
import { cold, hot } from 'jasmine-marbles';
|
||||||
|
import { ServerSyncBufferEffects } from './server-sync-buffer.effects';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import {
|
||||||
|
CommitSSBAction,
|
||||||
|
EmptySSBAction,
|
||||||
|
ServerSyncBufferActionTypes
|
||||||
|
} from './server-sync-buffer.actions';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { ObjectCacheService } from './object-cache.service';
|
||||||
|
import { MockStore } from '../../shared/testing/mock-store';
|
||||||
|
import { ObjectCacheState } from './object-cache.reducer';
|
||||||
|
import * as operators from 'rxjs/operators';
|
||||||
|
import { spyOnOperator } from '../../shared/testing/utils';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
|
import { ApplyPatchObjectCacheAction } from './object-cache.actions';
|
||||||
|
|
||||||
|
describe('ServerSyncBufferEffects', () => {
|
||||||
|
let ssbEffects: ServerSyncBufferEffects;
|
||||||
|
let actions: Observable<any>;
|
||||||
|
const testConfig = {
|
||||||
|
cache:
|
||||||
|
{
|
||||||
|
autoSync:
|
||||||
|
{
|
||||||
|
timePerMethod: {},
|
||||||
|
defaultTime: 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const selfLink = 'https://rest.api/endpoint/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
|
let store;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
store = new MockStore<ObjectCacheState>({});
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
providers: [
|
||||||
|
ServerSyncBufferEffects,
|
||||||
|
provideMockActions(() => actions),
|
||||||
|
{ provide: GLOBAL_CONFIG, useValue: testConfig },
|
||||||
|
{ provide: RequestService, useValue: getMockRequestService() },
|
||||||
|
{
|
||||||
|
provide: ObjectCacheService, useValue: {
|
||||||
|
getBySelfLink: (link) => {
|
||||||
|
const object = new DSpaceObject();
|
||||||
|
object.self = link;
|
||||||
|
return observableOf(object);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{ provide: Store, useValue: store }
|
||||||
|
// other providers
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
ssbEffects = TestBed.get(ServerSyncBufferEffects);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('setTimeoutForServerSync', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOnOperator(operators, 'delay').and.returnValue((v) => v);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should return a COMMIT action in response to an ADD action', () => {
|
||||||
|
actions = hot('a', {
|
||||||
|
a: {
|
||||||
|
type: ServerSyncBufferActionTypes.ADD,
|
||||||
|
payload: { href: selfLink, method: RestRequestMethod.PUT }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = cold('b', { b: new CommitSSBAction(RestRequestMethod.PUT) });
|
||||||
|
|
||||||
|
expect(ssbEffects.setTimeoutForServerSync).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('commitServerSyncBuffer', () => {
|
||||||
|
describe('when the buffer is not empty', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).core = Object({});
|
||||||
|
(state as any).core['cache/syncbuffer'] = {
|
||||||
|
buffer: [{
|
||||||
|
href: selfLink,
|
||||||
|
method: RestRequestMethod.PATCH
|
||||||
|
}]
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should return a list of actions in response to a COMMIT action', () => {
|
||||||
|
actions = hot('a', {
|
||||||
|
a: {
|
||||||
|
type: ServerSyncBufferActionTypes.COMMIT,
|
||||||
|
payload: RestRequestMethod.PATCH
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
const expected = cold('(bc)', {
|
||||||
|
b: new ApplyPatchObjectCacheAction(selfLink),
|
||||||
|
c: new EmptySSBAction(RestRequestMethod.PATCH)
|
||||||
|
});
|
||||||
|
|
||||||
|
expect(ssbEffects.commitServerSyncBuffer).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the buffer is empty', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
store
|
||||||
|
.subscribe((state) => {
|
||||||
|
(state as any).core = Object({});
|
||||||
|
(state as any).core['cache/syncbuffer'] = {
|
||||||
|
buffer: []
|
||||||
|
};
|
||||||
|
});
|
||||||
|
});
|
||||||
|
it('should return a placeholder action in response to a COMMIT action', () => {
|
||||||
|
store.subscribe();
|
||||||
|
actions = hot('a', {
|
||||||
|
a: {
|
||||||
|
type: ServerSyncBufferActionTypes.COMMIT,
|
||||||
|
payload: { method: RestRequestMethod.PATCH }
|
||||||
|
}
|
||||||
|
});
|
||||||
|
const expected = cold('b', { b: { type: 'NO_ACTION' } });
|
||||||
|
|
||||||
|
expect(ssbEffects.commitServerSyncBuffer).toBeObservable(expected);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
122
src/app/core/cache/server-sync-buffer.effects.ts
vendored
Normal file
122
src/app/core/cache/server-sync-buffer.effects.ts
vendored
Normal file
@@ -0,0 +1,122 @@
|
|||||||
|
import { delay, exhaustMap, first, map, switchMap, tap } from 'rxjs/operators';
|
||||||
|
import { Inject, Injectable } from '@angular/core';
|
||||||
|
import { Actions, Effect, ofType } from '@ngrx/effects';
|
||||||
|
import {
|
||||||
|
AddToSSBAction,
|
||||||
|
CommitSSBAction,
|
||||||
|
EmptySSBAction,
|
||||||
|
ServerSyncBufferActionTypes
|
||||||
|
} from './server-sync-buffer.actions';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { coreSelector, CoreState } from '../core.reducers';
|
||||||
|
import { Action, createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
|
||||||
|
import { ServerSyncBufferEntry, ServerSyncBufferState } from './server-sync-buffer.reducer';
|
||||||
|
import { combineLatest as observableCombineLatest, of as observableOf } from 'rxjs';
|
||||||
|
import { RequestService } from '../data/request.service';
|
||||||
|
import { PutRequest } from '../data/request.models';
|
||||||
|
import { ObjectCacheService } from './object-cache.service';
|
||||||
|
import { ApplyPatchObjectCacheAction } from './object-cache.actions';
|
||||||
|
import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer';
|
||||||
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
import { hasValue, isNotEmpty, isNotUndefined } from '../../shared/empty.util';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ServerSyncBufferEffects {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When an ADDToSSBAction is dispatched
|
||||||
|
* Set a time out (configurable per method type)
|
||||||
|
* Then dispatch a CommitSSBAction
|
||||||
|
* When the delay is running, no new AddToSSBActions are processed in this effect
|
||||||
|
*/
|
||||||
|
@Effect() setTimeoutForServerSync = this.actions$
|
||||||
|
.pipe(
|
||||||
|
ofType(ServerSyncBufferActionTypes.ADD),
|
||||||
|
exhaustMap((action: AddToSSBAction) => {
|
||||||
|
const autoSyncConfig = this.EnvConfig.cache.autoSync;
|
||||||
|
const timeoutInSeconds = autoSyncConfig.timePerMethod[action.payload.method] || autoSyncConfig.defaultTime;
|
||||||
|
return observableOf(new CommitSSBAction(action.payload.method)).pipe(
|
||||||
|
delay(timeoutInSeconds * 1000),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a CommitSSBAction is dispatched
|
||||||
|
* Create a list of actions for each entry in the current buffer state to be dispatched
|
||||||
|
* When the list of actions is not empty, also dispatch an EmptySSBAction
|
||||||
|
* When the list is empty dispatch a NO_ACTION placeholder action
|
||||||
|
*/
|
||||||
|
@Effect() commitServerSyncBuffer = this.actions$
|
||||||
|
.pipe(
|
||||||
|
ofType(ServerSyncBufferActionTypes.COMMIT),
|
||||||
|
switchMap((action: CommitSSBAction) => {
|
||||||
|
return this.store.pipe(
|
||||||
|
select(serverSyncBufferSelector()),
|
||||||
|
first(), /* necessary, otherwise delay will not have any effect after the first run */
|
||||||
|
switchMap((bufferState: ServerSyncBufferState) => {
|
||||||
|
const actions: Array<Observable<Action>> = bufferState.buffer
|
||||||
|
.filter((entry: ServerSyncBufferEntry) => {
|
||||||
|
/* If there's a request method, filter
|
||||||
|
If there's no filter, commit everything */
|
||||||
|
if (hasValue(action.payload)) {
|
||||||
|
return entry.method === action.payload;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
})
|
||||||
|
.map((entry: ServerSyncBufferEntry) => {
|
||||||
|
if (entry.method === RestRequestMethod.PATCH) {
|
||||||
|
return this.applyPatch(entry.href);
|
||||||
|
} else {
|
||||||
|
/* TODO implement for other request method types */
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
/* Add extra action to array, to make sure the ServerSyncBuffer is emptied afterwards */
|
||||||
|
if (isNotEmpty(actions) && isNotUndefined(actions[0])) {
|
||||||
|
return observableCombineLatest(...actions).pipe(
|
||||||
|
switchMap((array) => [...array, new EmptySSBAction(action.payload)])
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return observableOf({ type: 'NO_ACTION' });
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
})
|
||||||
|
);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* private method to create an ApplyPatchObjectCacheAction based on a cache entry
|
||||||
|
* and to do the actual patch request to the server
|
||||||
|
* @param {string} href The self link of the cache entry
|
||||||
|
* @returns {Observable<Action>} ApplyPatchObjectCacheAction to be dispatched
|
||||||
|
*/
|
||||||
|
private applyPatch(href: string): Observable<Action> {
|
||||||
|
const patchObject = this.objectCache.getBySelfLink(href).pipe(first());
|
||||||
|
|
||||||
|
return patchObject.pipe(
|
||||||
|
map((object) => {
|
||||||
|
const serializedObject = new DSpaceRESTv2Serializer(object.constructor as GenericConstructor<{}>).serialize(object);
|
||||||
|
|
||||||
|
this.requestService.configure(new PutRequest(this.requestService.generateRequestId(), href, serializedObject));
|
||||||
|
|
||||||
|
return new ApplyPatchObjectCacheAction(href)
|
||||||
|
})
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
constructor(private actions$: Actions,
|
||||||
|
private store: Store<CoreState>,
|
||||||
|
private requestService: RequestService,
|
||||||
|
private objectCache: ObjectCacheService,
|
||||||
|
@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) {
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export function serverSyncBufferSelector(): MemoizedSelector<CoreState, ServerSyncBufferState> {
|
||||||
|
return createSelector(coreSelector, (state: CoreState) => state['cache/syncbuffer']);
|
||||||
|
}
|
85
src/app/core/cache/server-sync-buffer.reducer.spec.ts
vendored
Normal file
85
src/app/core/cache/server-sync-buffer.reducer.spec.ts
vendored
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import * as deepFreeze from 'deep-freeze';
|
||||||
|
import { RemoveFromObjectCacheAction } from './object-cache.actions';
|
||||||
|
import { serverSyncBufferReducer } from './server-sync-buffer.reducer';
|
||||||
|
import { RestRequestMethod } from '../data/rest-request-method';
|
||||||
|
import { AddToSSBAction, EmptySSBAction } from './server-sync-buffer.actions';
|
||||||
|
|
||||||
|
class NullAction extends RemoveFromObjectCacheAction {
|
||||||
|
type = null;
|
||||||
|
payload = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(null);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('serverSyncBufferReducer', () => {
|
||||||
|
const selfLink1 = 'https://localhost:8080/api/core/items/1698f1d3-be98-4c51-9fd8-6bfedcbd59b7';
|
||||||
|
const selfLink2 = 'https://localhost:8080/api/core/items/28b04544-1766-4e82-9728-c4e93544ecd3';
|
||||||
|
const testState = {
|
||||||
|
buffer:
|
||||||
|
[
|
||||||
|
{
|
||||||
|
href: selfLink1,
|
||||||
|
method: RestRequestMethod.PATCH,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
href: selfLink2,
|
||||||
|
method: RestRequestMethod.GET,
|
||||||
|
}
|
||||||
|
]
|
||||||
|
};
|
||||||
|
const newSelfLink = 'https://localhost:8080/api/core/items/1ce6b5ae-97e1-4e5a-b4b0-f9029bad10c0';
|
||||||
|
|
||||||
|
deepFreeze(testState);
|
||||||
|
|
||||||
|
it('should return the current state when no valid actions have been made', () => {
|
||||||
|
const action = new NullAction();
|
||||||
|
const newState = serverSyncBufferReducer(testState, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(testState);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start with an empty buffer array', () => {
|
||||||
|
const action = new NullAction();
|
||||||
|
const initialState = serverSyncBufferReducer(undefined, action);
|
||||||
|
|
||||||
|
expect(initialState).toEqual({ buffer: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform the ADD action without affecting the previous state', () => {
|
||||||
|
const action = new AddToSSBAction(selfLink1, RestRequestMethod.POST);
|
||||||
|
// testState has already been frozen above
|
||||||
|
serverSyncBufferReducer(testState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should perform the EMPTY action without affecting the previous state', () => {
|
||||||
|
const action = new EmptySSBAction();
|
||||||
|
// testState has already been frozen above
|
||||||
|
serverSyncBufferReducer(testState, action);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should empty the buffer if the EmptySSBAction is dispatched without a payload', () => {
|
||||||
|
const action = new EmptySSBAction();
|
||||||
|
// testState has already been frozen above
|
||||||
|
const emptyState = serverSyncBufferReducer(testState, action);
|
||||||
|
expect(emptyState).toEqual({ buffer: [] });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should empty the buffer partially if the EmptySSBAction is dispatched with a payload', () => {
|
||||||
|
const action = new EmptySSBAction(RestRequestMethod.PATCH);
|
||||||
|
// testState has already been frozen above
|
||||||
|
const emptyState = serverSyncBufferReducer(testState, action);
|
||||||
|
expect(emptyState).toEqual({ buffer: testState.buffer.filter((entry) => entry.method !== RestRequestMethod.PATCH) });
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should add an entry to the buffer if the AddSSBAction is dispatched', () => {
|
||||||
|
const action = new AddToSSBAction(newSelfLink, RestRequestMethod.PUT);
|
||||||
|
// testState has already been frozen above
|
||||||
|
const newState = serverSyncBufferReducer(testState, action);
|
||||||
|
expect(newState.buffer).toContain({
|
||||||
|
href: newSelfLink, method: RestRequestMethod.PUT
|
||||||
|
})
|
||||||
|
;
|
||||||
|
})
|
||||||
|
});
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user