From 863cd964c13fd743de555a7add2e0e030af44cfc Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 4 Oct 2017 17:17:54 +0200 Subject: [PATCH 01/22] Cast to any is no longer necessary, string enums are supported as of typescript 2.4 --- src/app/core/data/remote-data.ts | 8 ++++---- src/app/core/shared/resource-type.ts | 12 ++++++------ 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/app/core/data/remote-data.ts b/src/app/core/data/remote-data.ts index b9f58a5567..e3c7a17ef8 100644 --- a/src/app/core/data/remote-data.ts +++ b/src/app/core/data/remote-data.ts @@ -3,10 +3,10 @@ import { Observable } from 'rxjs/Observable'; import { PageInfo } from '../shared/page-info.model'; export enum RemoteDataState { - RequestPending = 'RequestPending' as any, - ResponsePending = 'ResponsePending' as any, - Failed = 'Failed' as any, - Success = 'Success' as any + RequestPending = 'RequestPending', + ResponsePending = 'ResponsePending', + Failed = 'Failed', + Success = 'Success' } /** diff --git a/src/app/core/shared/resource-type.ts b/src/app/core/shared/resource-type.ts index 15e707b016..f3554e18cf 100644 --- a/src/app/core/shared/resource-type.ts +++ b/src/app/core/shared/resource-type.ts @@ -3,10 +3,10 @@ * https://github.com/Microsoft/TypeScript/pull/15486 */ export enum ResourceType { - Bundle = 'bundle' as any, - Bitstream = 'bitstream' as any, - BitstreamFormat = 'bitstreamformat' as any, - Item = 'item' as any, - Collection = 'collection' as any, - Community = 'community' as any, + Bundle = 'bundle', + Bitstream = 'bitstream', + BitstreamFormat = 'bitstreamformat', + Item = 'item', + Collection = 'collection', + Community = 'community', } From 1cbc63d77c3cd5cdb9c333563b92c8aeb31d0a75 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 11 Oct 2017 16:06:54 +0200 Subject: [PATCH 02/22] Fixed an issue where the debugMetareducers wouldn't be active even though debug was enabled in the config --- src/app/app.module.ts | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 3ec0cdebab..01ad0691aa 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -41,10 +41,7 @@ export function getBase() { export function getMetaReducers(config: GlobalConfig): Array> { const metaReducers: Array> = config.production ? appMetaReducers : [...appMetaReducers, storeFreeze]; - if (config.debug) { - metaReducers.concat(debugMetaReducers) - } - return metaReducers; + return config.debug ? [...metaReducers, ...debugMetaReducers] : metaReducers; } const DEV_MODULES: any[] = []; From 3580fe4ffbcace1f6e9d22429f6a6af5f545c4be Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 12 Oct 2017 10:44:35 +0200 Subject: [PATCH 03/22] Update dependencies to fix an issue where webpack would sometimes lowercase absolute paths --- package.json | 32 +++---- yarn.lock | 262 +++++++++++++++++++++++++-------------------------- 2 files changed, 147 insertions(+), 147 deletions(-) diff --git a/package.json b/package.json index f758eae6e8..2f0d024f89 100644 --- a/package.json +++ b/package.json @@ -88,11 +88,11 @@ "@ngx-translate/http-loader": "2.0.0", "body-parser": "1.18.2", "bootstrap": "v4.0.0-beta", - "cerialize": "0.1.16", + "cerialize": "0.1.18", "compression": "1.7.1", "cookie-parser": "1.4.3", "core-js": "2.5.1", - "express": "4.16.1", + "express": "4.16.2", "express-session": "1.15.6", "font-awesome": "4.7.0", "http-server": "0.10.0", @@ -102,7 +102,7 @@ "methods": "1.1.2", "morgan": "1.9.0", "ngx-pagination": "3.0.1", - "pem": "1.12.0", + "pem": "1.12.3", "reflect-metadata": "0.1.10", "rxjs": "5.4.3", "ts-md5": "1.2.2", @@ -113,7 +113,7 @@ "@angular/compiler": "4.4.4", "@angular/compiler-cli": "4.4.4", "@ngrx/store-devtools": "4.0.0", - "@ngtools/webpack": "1.7.2", + "@ngtools/webpack": "1.7.3", "@types/cookie-parser": "1.4.1", "@types/deep-freeze": "0.1.1", "@types/express": "4.0.37", @@ -122,19 +122,19 @@ "@types/jasmine": "2.6.0", "@types/memory-cache": "0.0.31", "@types/mime": "2.0.0", - "@types/node": "8.0.26", + "@types/node": "8.0.34", "@types/serve-static": "1.7.32", "@types/source-map": "0.5.1", "@types/webfontloader": "1.6.29", "ajv": "5.2.3", "ajv-keywords": "2.1.0", "angular2-template-loader": "0.6.2", - "autoprefixer": "7.1.4", + "autoprefixer": "7.1.5", "awesome-typescript-loader": "3.2.3", - "caniuse-lite": "1.0.30000697", - "codelyzer": "3.2.0", + "caniuse-lite": "1.0.30000746", + "codelyzer": "3.2.1", "compression-webpack-plugin": "1.0.1", - "copy-webpack-plugin": "4.1.0", + "copy-webpack-plugin": "4.1.1", "coveralls": "3.0.0", "css-loader": "0.28.7", "deep-freeze": "0.0.1", @@ -143,7 +143,7 @@ "imports-loader": "0.7.1", "istanbul-instrumenter-loader": "3.0.0", "jasmine-core": "2.8.0", - "jasmine-marbles": "0.1.0", + "jasmine-marbles": "0.2.0", "jasmine-spec-reporter": "4.2.1", "json-loader": "0.5.7", "karma": "1.7.1", @@ -158,22 +158,22 @@ "karma-remap-istanbul": "0.6.0", "karma-sourcemap-loader": "0.3.7", "karma-webdriver-launcher": "1.0.5", - "karma-webpack": "2.0.4", + "karma-webpack": "2.0.5", "ngrx-store-freeze": "0.2.0", "node-sass": "4.5.3", "nodemon": "1.12.1", "npm-run-all": "4.1.1", - "postcss": "6.0.12", + "postcss": "6.0.13", "postcss-apply": "0.8.0", "postcss-cli": "4.1.1", "postcss-cssnext": "3.0.2", - "postcss-loader": "2.0.6", + "postcss-loader": "2.0.7", "postcss-responsive-type": "1.0.0", "postcss-smart-import": "0.7.5", "protractor": "5.1.2", "protractor-istanbul-plugin": "2.0.0", "raw-loader": "0.5.1", - "resolve-url-loader": "2.1.0", + "resolve-url-loader": "2.1.1", "rimraf": "2.6.2", "rollup": "0.50.0", "rollup-plugin-commonjs": "8.2.1", @@ -188,9 +188,9 @@ "ts-helpers": "1.1.2", "ts-node": "3.3.0", "tslint": "5.7.0", - "typedoc": "0.8.0", + "typedoc": "0.9.0", "typescript": "2.5.3", - "webpack": "3.6.0", + "webpack": "3.7.1", "webpack-bundle-analyzer": "2.9.0", "webpack-dev-middleware": "1.12.0", "webpack-dev-server": "2.9.1", diff --git a/yarn.lock b/yarn.lock index d70e3b3a2d..05ff3b126e 100644 --- a/yarn.lock +++ b/yarn.lock @@ -106,9 +106,9 @@ version "4.0.3" resolved "https://registry.yarnpkg.com/@ngrx/store/-/store-4.0.3.tgz#36abacdfa19bfb8506e40de80bae06050a1e15e9" -"@ngtools/webpack@1.7.2": - version "1.7.2" - resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-1.7.2.tgz#3fc4de01786dcc2f50d8cbaaa117311e56799977" +"@ngtools/webpack@1.7.3": + version "1.7.3" + resolved "https://registry.yarnpkg.com/@ngtools/webpack/-/webpack-1.7.3.tgz#20d5bcca0d902e1f3e5accf4922f482539c93a3b" dependencies: enhanced-resolve "^3.1.0" loader-utils "^1.0.2" @@ -150,42 +150,35 @@ "@types/express-serve-static-core" "*" "@types/serve-static" "*" -"@types/fs-extra@^4.0.0": - version "4.0.2" - resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.2.tgz#7b9b1bbf85962cbe029b5a83c9b530d7c75af3ba" +"@types/fs-extra@4.0.0": + version "4.0.0" + resolved "https://registry.yarnpkg.com/@types/fs-extra/-/fs-extra-4.0.0.tgz#1dd742ad5c9bce308f7a52d02ebc01421bc9102f" dependencies: "@types/node" "*" -"@types/glob@*": - version "5.0.32" - resolved "https://registry.yarnpkg.com/@types/glob/-/glob-5.0.32.tgz#aec5cfe987c72f099fdb1184452986aa506d5e8f" - dependencies: - "@types/minimatch" "*" - "@types/node" "*" - "@types/hammerjs@2.0.35": version "2.0.35" resolved "https://registry.yarnpkg.com/@types/hammerjs/-/hammerjs-2.0.35.tgz#7b7c950c7d54593e23bffc8d2b4feba9866a7277" -"@types/handlebars@^4.0.31": - version "4.0.36" - resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.36.tgz#ff57c77fa1ab6713bb446534ddc4d979707a3a79" +"@types/handlebars@4.0.31": + version "4.0.31" + resolved "https://registry.yarnpkg.com/@types/handlebars/-/handlebars-4.0.31.tgz#a7fba66fafe42713aee88eeca8db91192efe6e72" -"@types/highlight.js@^9.1.8": - version "9.1.10" - resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.1.10.tgz#b621f809cd9573b80992b90cffc5788208e3069c" +"@types/highlight.js@9.1.8": + version "9.1.8" + resolved "https://registry.yarnpkg.com/@types/highlight.js/-/highlight.js-9.1.8.tgz#d227f18bcb8f3f187e16965f2444859a04689758" "@types/jasmine@2.6.0": version "2.6.0" resolved "https://registry.yarnpkg.com/@types/jasmine/-/jasmine-2.6.0.tgz#997b41a27752b4850af2683bc4a8d8222c25bd02" -"@types/lodash@^4.14.37": - version "4.14.76" - resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.76.tgz#87874f766774d54e89589697340be9496fb8bf70" +"@types/lodash@4.14.74": + version "4.14.74" + resolved "https://registry.yarnpkg.com/@types/lodash/-/lodash-4.14.74.tgz#ac3bd8db988e7f7038e5d22bd76a7ba13f876168" -"@types/marked@0.0.28": - version "0.0.28" - resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.0.28.tgz#44ba754e9fa51432583e8eb30a7c4dd249b52faa" +"@types/marked@0.3.0": + version "0.3.0" + resolved "https://registry.yarnpkg.com/@types/marked/-/marked-0.3.0.tgz#583c223dd33385a1dda01aaf77b0cd0411c4b524" "@types/memory-cache@0.0.31": version "0.0.31" @@ -195,11 +188,7 @@ version "2.0.0" resolved "https://registry.yarnpkg.com/@types/mime/-/mime-2.0.0.tgz#5a7306e367c539b9f6543499de8dd519fac37a8b" -"@types/minimatch@*": - version "3.0.1" - resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-3.0.1.tgz#b683eb60be358304ef146f5775db4c0e3696a550" - -"@types/minimatch@^2.0.29": +"@types/minimatch@2.0.29": version "2.0.29" resolved "https://registry.yarnpkg.com/@types/minimatch/-/minimatch-2.0.29.tgz#5002e14f75e2d71e564281df0431c8c1b4a2a36a" @@ -207,9 +196,9 @@ version "8.0.31" resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.31.tgz#d9af61093cf4bfc9f066ca34de0175012cfb0ce9" -"@types/node@8.0.26": - version "8.0.26" - resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.26.tgz#4d58be925306fd22b1141085535a0268b8beb189" +"@types/node@8.0.34": + version "8.0.34" + resolved "https://registry.yarnpkg.com/@types/node/-/node-8.0.34.tgz#55f801fa2ddb2a40dd6dfc15ecfe1dde9c129fe9" "@types/node@^6.0.46": version "6.0.88" @@ -230,11 +219,10 @@ "@types/express-serve-static-core" "*" "@types/mime" "*" -"@types/shelljs@^0.7.0": - version "0.7.4" - resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.4.tgz#137b5f31306eaff4de120ffe5b9d74b297809cfc" +"@types/shelljs@0.7.0": + version "0.7.0" + resolved "https://registry.yarnpkg.com/@types/shelljs/-/shelljs-0.7.0.tgz#229c157c6bc1e67d6b990e6c5e18dbd2ff58cff0" dependencies: - "@types/glob" "*" "@types/node" "*" "@types/source-map@0.5.1": @@ -594,15 +582,15 @@ atob@~1.1.0: version "1.1.3" resolved "https://registry.yarnpkg.com/atob/-/atob-1.1.3.tgz#95f13629b12c3a51a5d215abdce2aa9f32f80773" -autoprefixer@7.1.4, autoprefixer@^7.1.1: - version "7.1.4" - resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.4.tgz#960847dbaa4016bc8e8e52ec891cbf8f1257a748" +autoprefixer@7.1.5: + version "7.1.5" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.5.tgz#d65d14b83c7cd1dd7bc801daa00557addf5a06b2" dependencies: - browserslist "^2.4.0" - caniuse-lite "^1.0.30000726" + browserslist "^2.5.0" + caniuse-lite "^1.0.30000744" normalize-range "^0.1.2" num2fraction "^1.2.2" - postcss "^6.0.11" + postcss "^6.0.13" postcss-value-parser "^3.2.3" autoprefixer@^6.3.1: @@ -616,6 +604,17 @@ autoprefixer@^6.3.1: postcss "^5.2.16" postcss-value-parser "^3.2.3" +autoprefixer@^7.1.1: + version "7.1.4" + resolved "https://registry.yarnpkg.com/autoprefixer/-/autoprefixer-7.1.4.tgz#960847dbaa4016bc8e8e52ec891cbf8f1257a748" + dependencies: + browserslist "^2.4.0" + caniuse-lite "^1.0.30000726" + normalize-range "^0.1.2" + num2fraction "^1.2.2" + postcss "^6.0.11" + postcss-value-parser "^3.2.3" + awesome-typescript-loader@3.2.3: version "3.2.3" resolved "https://registry.yarnpkg.com/awesome-typescript-loader/-/awesome-typescript-loader-3.2.3.tgz#aa2119b7c808a031e2b28945b031450a8975367f" @@ -808,14 +807,14 @@ blocking-proxy@0.0.5: dependencies: minimist "^1.2.0" -bluebird@^2.10.2: - version "2.11.0" - resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-2.11.0.tgz#534b9033c022c9579c56ba3b3e5a5caafbb650e1" - bluebird@^3.3.0, bluebird@^3.4.7: version "3.5.0" resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.0.tgz#791420d7f551eea2897453a8a77653f96606d67c" +bluebird@^3.5.1: + version "3.5.1" + resolved "https://registry.yarnpkg.com/bluebird/-/bluebird-3.5.1.tgz#d9551f9de98f1fcda1e683d17ee91a0602ee2eb9" + bn.js@^4.0.0, bn.js@^4.1.0, bn.js@^4.1.1, bn.js@^4.4.0: version "4.11.8" resolved "https://registry.yarnpkg.com/bn.js/-/bn.js-4.11.8.tgz#2cde09eb5ee341f484746bb0309b3253b1b1442f" @@ -997,6 +996,13 @@ browserslist@^2.0.0, browserslist@^2.4.0: caniuse-lite "^1.0.30000718" electron-to-chromium "^1.3.18" +browserslist@^2.5.0: + version "2.5.1" + resolved "https://registry.yarnpkg.com/browserslist/-/browserslist-2.5.1.tgz#68e4bc536bbcc6086d62843a2ffccea8396821c6" + dependencies: + caniuse-lite "^1.0.30000744" + electron-to-chromium "^1.3.24" + buffer-crc32@^0.2.1: version "0.2.13" resolved "https://registry.yarnpkg.com/buffer-crc32/-/buffer-crc32-0.2.13.tgz#0d333e3f00eac50aa1454abd30ef8c2a5d9a7242" @@ -1103,9 +1109,9 @@ caniuse-db@^1.0.30000529, caniuse-db@^1.0.30000634, caniuse-db@^1.0.30000639: version "1.0.30000740" resolved "https://registry.yarnpkg.com/caniuse-db/-/caniuse-db-1.0.30000740.tgz#03fcaaa176e3ed075895f72d46c1a12149bbeac9" -caniuse-lite@1.0.30000697: - version "1.0.30000697" - resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000697.tgz#125fb00604b63fbb188db96a667ce2922dcd6cdd" +caniuse-lite@1.0.30000746, caniuse-lite@^1.0.30000744: + version "1.0.30000746" + resolved "https://registry.yarnpkg.com/caniuse-lite/-/caniuse-lite-1.0.30000746.tgz#c64f95a3925cfd30207a308ed76c1ae96ea09ea0" caniuse-lite@^1.0.0, caniuse-lite@^1.0.30000718, caniuse-lite@^1.0.30000726: version "1.0.30000740" @@ -1130,11 +1136,11 @@ center-align@^0.1.1: align-text "^0.1.3" lazy-cache "^1.0.3" -cerialize@0.1.16: - version "0.1.16" - resolved "https://registry.yarnpkg.com/cerialize/-/cerialize-0.1.16.tgz#88678bffbd7817a90aa5b58a8c66d6bdca3035be" +cerialize@0.1.18: + version "0.1.18" + resolved "https://registry.yarnpkg.com/cerialize/-/cerialize-0.1.18.tgz#d0f4f1b61cec7e4ed16a3eda0cac2bc99787414d" dependencies: - typescript "^2.1.6" + typescript "^2.5.0" chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3: version "1.1.3" @@ -1263,9 +1269,9 @@ code-point-at@^1.0.0: version "1.1.0" resolved "https://registry.yarnpkg.com/code-point-at/-/code-point-at-1.1.0.tgz#0d070b4d043a5bea33a2f1a40e2edb3d9a4ccf77" -codelyzer@3.2.0: - version "3.2.0" - resolved "https://registry.yarnpkg.com/codelyzer/-/codelyzer-3.2.0.tgz#68eb0a67771ea73006b517053c3035c1838abf14" +codelyzer@3.2.1: + version "3.2.1" + resolved "https://registry.yarnpkg.com/codelyzer/-/codelyzer-3.2.1.tgz#5b1ac75f7e0eb04647842ee29a322bf2167e7229" dependencies: app-root-path "^2.0.1" css-selector-tokenizer "^0.7.0" @@ -1490,17 +1496,17 @@ copy-descriptor@^0.1.0: version "0.1.1" resolved "https://registry.yarnpkg.com/copy-descriptor/-/copy-descriptor-0.1.1.tgz#676f6eb3c39997c2ee1ac3a924fd6124748f578d" -copy-webpack-plugin@4.1.0: - version "4.1.0" - resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.1.0.tgz#292a040318fe8ae3b1d7996ef05dfb483eb0b647" +copy-webpack-plugin@4.1.1: + version "4.1.1" + resolved "https://registry.yarnpkg.com/copy-webpack-plugin/-/copy-webpack-plugin-4.1.1.tgz#53ae69e04955ebfa9fda411f54cbb968531d71fd" dependencies: - bluebird "^2.10.2" - fs-extra "^0.26.4" - glob "^6.0.4" - is-glob "^3.1.0" + bluebird "^3.5.1" + fs-extra "^4.0.2" + glob "^7.1.2" + is-glob "^4.0.0" loader-utils "^0.2.15" lodash "^4.3.0" - minimatch "^3.0.0" + minimatch "^3.0.4" node-dir "^0.1.10" core-js@2.5.1, core-js@^2.2.0, core-js@^2.4.0: @@ -2054,6 +2060,10 @@ electron-to-chromium@^1.2.7, electron-to-chromium@^1.3.18: version "1.3.24" resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.24.tgz#9b7b88bb05ceb9fa016a177833cc2dde388f21b6" +electron-to-chromium@^1.3.24: + version "1.3.26" + resolved "https://registry.yarnpkg.com/electron-to-chromium/-/electron-to-chromium-1.3.26.tgz#996427294861a74d9c7c82b9260ea301e8c02d66" + elliptic@^6.0.0: version "6.4.0" resolved "https://registry.yarnpkg.com/elliptic/-/elliptic-6.4.0.tgz#cac9af8762c85836187003c8dfe193e5e2eae5df" @@ -2422,9 +2432,9 @@ express-session@1.15.6: uid-safe "~2.1.5" utils-merge "1.0.1" -express@4.16.1: - version "4.16.1" - resolved "https://registry.yarnpkg.com/express/-/express-4.16.1.tgz#6b33b560183c9b253b7b62144df33a4654ac9ed0" +express@4.16.2: + version "4.16.2" + resolved "https://registry.yarnpkg.com/express/-/express-4.16.2.tgz#e35c6dfe2d64b7dca0a5cd4f21781be3299e076c" dependencies: accepts "~1.3.4" array-flatten "1.1.1" @@ -2723,17 +2733,7 @@ fs-extra@^0.22.1: jsonfile "^2.1.0" rimraf "^2.2.8" -fs-extra@^0.26.4: - version "0.26.7" - resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-0.26.7.tgz#9ae1fdd94897798edab76d0918cf42d0c3184fa9" - dependencies: - graceful-fs "^4.1.2" - jsonfile "^2.1.0" - klaw "^1.0.0" - path-is-absolute "^1.0.0" - rimraf "^2.2.8" - -fs-extra@^4.0.0, fs-extra@^4.0.1: +fs-extra@^4.0.0, fs-extra@^4.0.1, fs-extra@^4.0.2: version "4.0.2" resolved "https://registry.yarnpkg.com/fs-extra/-/fs-extra-4.0.2.tgz#f91704c53d1b461f893452b0c307d9997647ab6b" dependencies: @@ -2859,17 +2859,7 @@ glob@^5.0.15: once "^1.3.0" path-is-absolute "^1.0.0" -glob@^6.0.4: - version "6.0.4" - resolved "https://registry.yarnpkg.com/glob/-/glob-6.0.4.tgz#0f08860f6a155127b2fadd4f9ce24b1aab6e4d22" - dependencies: - inflight "^1.0.4" - inherits "2" - minimatch "2 || 3" - once "^1.3.0" - path-is-absolute "^1.0.0" - -glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@~7.1.1: +glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.1, glob@^7.1.2, glob@~7.1.1: version "7.1.2" resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.2.tgz#c19c9df9a028702d678612384a6552404c636d15" dependencies: @@ -3487,7 +3477,7 @@ is-extglob@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-1.0.0.tgz#ac468177c4943405a092fc8f29760c6ffc6206c0" -is-extglob@^2.1.0: +is-extglob@^2.1.0, is-extglob@^2.1.1: version "2.1.1" resolved "https://registry.yarnpkg.com/is-extglob/-/is-extglob-2.1.1.tgz#a88c02535791f02ed37c76a1b9ea9773c833f8c2" @@ -3519,6 +3509,12 @@ is-glob@^3.1.0: dependencies: is-extglob "^2.1.0" +is-glob@^4.0.0: + version "4.0.0" + resolved "https://registry.yarnpkg.com/is-glob/-/is-glob-4.0.0.tgz#9521c76845cc2610a85203ddf080a958c2ffabc0" + dependencies: + is-extglob "^2.1.1" + is-module@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/is-module/-/is-module-1.0.0.tgz#3258fb69f78c14d5b815d664336b4cffb6441591" @@ -3722,11 +3718,11 @@ jasmine-core@2.8.0, jasmine-core@~2.8.0: version "2.8.0" resolved "https://registry.yarnpkg.com/jasmine-core/-/jasmine-core-2.8.0.tgz#bcc979ae1f9fd05701e45e52e65d3a5d63f1a24e" -jasmine-marbles@0.1.0: - version "0.1.0" - resolved "https://registry.yarnpkg.com/jasmine-marbles/-/jasmine-marbles-0.1.0.tgz#c9ecdc64e20b6cf55b49a10201a5be33907dadcc" +jasmine-marbles@0.2.0: + version "0.2.0" + resolved "https://registry.yarnpkg.com/jasmine-marbles/-/jasmine-marbles-0.2.0.tgz#b893d8508b75790b634876d3a1bea1345d65c156" dependencies: - lodash.isequal "^4.5.0" + lodash "^4.5.0" jasmine-spec-reporter@4.2.1: version "4.2.1" @@ -3924,9 +3920,9 @@ karma-webdriver-launcher@1.0.5: dependencies: wd "^1.0.0" -karma-webpack@2.0.4: - version "2.0.4" - resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.4.tgz#3e2d4f48ba94a878e1c66bb8e1ae6128987a175b" +karma-webpack@2.0.5: + version "2.0.5" + resolved "https://registry.yarnpkg.com/karma-webpack/-/karma-webpack-2.0.5.tgz#4f56887e32cf4f9583391c2388415de06af06efd" dependencies: async "~0.9.0" loader-utils "^0.2.5" @@ -4188,10 +4184,6 @@ lodash.isarray@^3.0.0: version "3.0.4" resolved "https://registry.yarnpkg.com/lodash.isarray/-/lodash.isarray-3.0.4.tgz#79e4eb88c36a8122af86f844aa9bcd851b5fbb55" -lodash.isequal@^4.5.0: - version "4.5.0" - resolved "https://registry.yarnpkg.com/lodash.isequal/-/lodash.isequal-4.5.0.tgz#415c4478f2bcc30120c22ce10ed3226f7d3e18e0" - lodash.keys@^3.0.0: version "3.1.2" resolved "https://registry.yarnpkg.com/lodash.keys/-/lodash.keys-3.1.2.tgz#4dbc0472b156be50a0b286855d1bd0b0c656098a" @@ -5190,9 +5182,9 @@ pbkdf2@^3.0.3: safe-buffer "^5.0.1" sha.js "^2.4.8" -pem@1.12.0: - version "1.12.0" - resolved "https://registry.yarnpkg.com/pem/-/pem-1.12.0.tgz#603d8207b9b5f83225e37ffcc36268c3aa3fecf3" +pem@1.12.3: + version "1.12.3" + resolved "https://registry.yarnpkg.com/pem/-/pem-1.12.3.tgz#b1fb5c8b79da8d18146c27fee79b0d4ddf9905b3" dependencies: md5 "^2.2.1" os-tmpdir "^1.0.1" @@ -5545,12 +5537,12 @@ postcss-load-plugins@^2.3.0: cosmiconfig "^2.1.1" object-assign "^4.1.0" -postcss-loader@2.0.6: - version "2.0.6" - resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.6.tgz#8c7e0055a3df1889abc6bad52dd45b2f41bbc6fc" +postcss-loader@2.0.7: + version "2.0.7" + resolved "https://registry.yarnpkg.com/postcss-loader/-/postcss-loader-2.0.7.tgz#4d2da1489cee0a14f72c0d9440c9ee7eded34345" dependencies: loader-utils "^1.1.0" - postcss "^6.0.2" + postcss "^6.0.0" postcss-load-config "^1.2.0" schema-utils "^0.3.0" @@ -5816,12 +5808,12 @@ postcss-zindex@^2.0.1: postcss "^5.0.4" uniqs "^2.0.0" -postcss@6.0.12, postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.11, postcss@^6.0.2, postcss@^6.0.3, postcss@^6.0.5, postcss@^6.0.6, postcss@^6.0.8: - version "6.0.12" - resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.12.tgz#6b0155089d2d212f7bd6a0cecd4c58c007403535" +postcss@6.0.13, postcss@^6.0.13: + version "6.0.13" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.13.tgz#b9ecab4ee00c89db3ec931145bd9590bbf3f125f" dependencies: chalk "^2.1.0" - source-map "^0.5.7" + source-map "^0.6.1" supports-color "^4.4.0" postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0.14, postcss@^5.0.16, postcss@^5.0.2, postcss@^5.0.4, postcss@^5.0.5, postcss@^5.0.6, postcss@^5.0.8, postcss@^5.2.16, postcss@^5.2.6: @@ -5833,6 +5825,14 @@ postcss@^5.0.10, postcss@^5.0.11, postcss@^5.0.12, postcss@^5.0.13, postcss@^5.0 source-map "^0.5.6" supports-color "^3.2.3" +postcss@^6.0.0, postcss@^6.0.1, postcss@^6.0.11, postcss@^6.0.3, postcss@^6.0.5, postcss@^6.0.6, postcss@^6.0.8: + version "6.0.12" + resolved "https://registry.yarnpkg.com/postcss/-/postcss-6.0.12.tgz#6b0155089d2d212f7bd6a0cecd4c58c007403535" + dependencies: + chalk "^2.1.0" + source-map "^0.5.7" + supports-color "^4.4.0" + prelude-ls@~1.1.2: version "1.1.2" resolved "https://registry.yarnpkg.com/prelude-ls/-/prelude-ls-1.1.2.tgz#21932a549f5e52ffd9a827f570e04be62a97da54" @@ -6385,9 +6385,9 @@ requires-port@1.0.x, requires-port@1.x.x: version "1.0.0" resolved "https://registry.yarnpkg.com/requires-port/-/requires-port-1.0.0.tgz#925d2601d39ac485e091cf0da5c6e694dc3dcaff" -resolve-url-loader@2.1.0: - version "2.1.0" - resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-2.1.0.tgz#27c95cc16a4353923fdbdc2dbaf5eef22232c477" +resolve-url-loader@2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/resolve-url-loader/-/resolve-url-loader-2.1.1.tgz#5354e87381aae348371e555172c50816708e6c1c" dependencies: adjust-sourcemap-loader "^1.1.0" camelcase "^4.0.0" @@ -6957,7 +6957,7 @@ source-map@0.5.x, source-map@^0.5.1, source-map@^0.5.3, source-map@^0.5.6, sourc version "0.5.7" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.5.7.tgz#8a039d2d1021d22d1ea14c80d8ea468ba2ef3fcc" -source-map@>=0.5.6: +source-map@>=0.5.6, source-map@^0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/source-map/-/source-map-0.6.1.tgz#74722af32e9614e9c287a8d0bbde48b5e2f1a263" @@ -7502,17 +7502,17 @@ typedoc-default-themes@^0.5.0: version "0.5.0" resolved "https://registry.yarnpkg.com/typedoc-default-themes/-/typedoc-default-themes-0.5.0.tgz#6dc2433e78ed8bea8e887a3acde2f31785bd6227" -typedoc@0.8.0: - version "0.8.0" - resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.8.0.tgz#d7172bc6a29964f451b7609c005beadadefe2361" +typedoc@0.9.0: + version "0.9.0" + resolved "https://registry.yarnpkg.com/typedoc/-/typedoc-0.9.0.tgz#159bff7c7784ce5b91d86f3e4cc8928e62040957" dependencies: - "@types/fs-extra" "^4.0.0" - "@types/handlebars" "^4.0.31" - "@types/highlight.js" "^9.1.8" - "@types/lodash" "^4.14.37" - "@types/marked" "0.0.28" - "@types/minimatch" "^2.0.29" - "@types/shelljs" "^0.7.0" + "@types/fs-extra" "4.0.0" + "@types/handlebars" "4.0.31" + "@types/highlight.js" "9.1.8" + "@types/lodash" "4.14.74" + "@types/marked" "0.3.0" + "@types/minimatch" "2.0.29" + "@types/shelljs" "0.7.0" fs-extra "^4.0.0" handlebars "^4.0.6" highlight.js "^9.0.0" @@ -7528,7 +7528,7 @@ typescript@2.4.1: version "2.4.1" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.4.1.tgz#c3ccb16ddaa0b2314de031e7e6fee89e5ba346bc" -typescript@2.5.3, typescript@^2.1.6: +typescript@2.5.3, typescript@^2.5.0: version "2.5.3" resolved "https://registry.yarnpkg.com/typescript/-/typescript-2.5.3.tgz#df3dcdc38f3beb800d4bc322646b04a3f6ca7f0d" @@ -7936,9 +7936,9 @@ webpack-sources@^1.0.1: source-list-map "^2.0.0" source-map "~0.5.3" -webpack@3.6.0: - version "3.6.0" - resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.6.0.tgz#a89a929fbee205d35a4fa2cc487be9cbec8898bc" +webpack@3.7.1: + version "3.7.1" + resolved "https://registry.yarnpkg.com/webpack/-/webpack-3.7.1.tgz#6046b5c415ff7df7a0dc54c5b6b86098e8b952da" dependencies: acorn "^5.0.0" acorn-dynamic-import "^2.0.0" From a84eb533bea23b0e4798121be9b760fca458a8db Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 12 Oct 2017 12:51:58 +0200 Subject: [PATCH 04/22] get browse endpoints from hal links --- .../collection-page.component.ts | 4 +- .../community-page.component.ts | 3 +- src/app/+home/home.component.ts | 5 +- .../top-level-community-list.component.ts | 2 + .../+item-page/simple/item-page.component.ts | 3 +- src/app/browser-app.module.ts | 1 + src/app/core/browse/browse.service.ts | 62 +++++++++++++ .../models/normalized-dspace-object.model.ts | 10 +++ .../cache/models/normalized-object.model.ts | 4 + src/app/core/cache/response-cache.models.ts | 10 +++ src/app/core/core.module.ts | 4 + .../data/browse-response-parsing.service.ts | 28 ++++++ src/app/core/data/collection-data.service.ts | 40 ++++++++- src/app/core/data/community-data.service.ts | 38 +++++++- src/app/core/data/data.service.ts | 87 +++++++------------ src/app/core/data/item-data.service.ts | 16 +++- src/app/core/data/request.models.ts | 11 +++ src/app/core/data/request.service.ts | 12 ++- .../data/root-response-parsing.service.ts | 7 +- .../core/shared/browse-definition.model.ts | 24 +++++ src/app/core/shared/hal-endpoint.service.ts | 46 ++++++++++ src/app/core/shared/sort-option.model.ts | 9 ++ 22 files changed, 346 insertions(+), 80 deletions(-) create mode 100644 src/app/core/browse/browse.service.ts create mode 100644 src/app/core/data/browse-response-parsing.service.ts create mode 100644 src/app/core/shared/browse-definition.model.ts create mode 100644 src/app/core/shared/hal-endpoint.service.ts create mode 100644 src/app/core/shared/sort-option.model.ts diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index ca349ce5bc..e0043f2845 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -22,6 +22,7 @@ import { Observable } from 'rxjs/Observable'; selector: 'ds-collection-page', styleUrls: ['./collection-page.component.scss'], templateUrl: './collection-page.component.html', + changeDetection: ChangeDetectionStrategy.OnPush }) export class CollectionPageComponent implements OnInit, OnDestroy { collectionData: RemoteData; @@ -37,8 +38,7 @@ export class CollectionPageComponent implements OnInit, OnDestroy { private route: ActivatedRoute) { this.paginationConfig = new PaginationComponentOptions(); this.paginationConfig.id = 'collection-page-pagination'; - this.paginationConfig.pageSizeOptions = [4]; - this.paginationConfig.pageSize = 4; + this.paginationConfig.pageSize = 5; this.paginationConfig.currentPage = 1; this.sortConfig = new SortOptions(); } diff --git a/src/app/+community-page/community-page.component.ts b/src/app/+community-page/community-page.component.ts index 2bd19b0f06..4ed516637d 100644 --- a/src/app/+community-page/community-page.component.ts +++ b/src/app/+community-page/community-page.component.ts @@ -1,4 +1,4 @@ -import { Component, OnDestroy, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute, Params } from '@angular/router'; import { Subscription } from 'rxjs/Subscription'; @@ -13,6 +13,7 @@ import { hasValue } from '../shared/empty.util'; selector: 'ds-community-page', styleUrls: ['./community-page.component.scss'], templateUrl: './community-page.component.html', + changeDetection: ChangeDetectionStrategy.OnPush }) export class CommunityPageComponent implements OnInit, OnDestroy { communityData: RemoteData; diff --git a/src/app/+home/home.component.ts b/src/app/+home/home.component.ts index d1222b8ae0..d9127a13a6 100644 --- a/src/app/+home/home.component.ts +++ b/src/app/+home/home.component.ts @@ -1,9 +1,10 @@ -import { Component } from '@angular/core'; +import { ChangeDetectionStrategy, Component } from '@angular/core'; @Component({ selector: 'ds-home', styleUrls: ['./home.component.scss'], - templateUrl: './home.component.html' + templateUrl: './home.component.html', + changeDetection: ChangeDetectionStrategy.OnPush }) export class HomeComponent { diff --git a/src/app/+home/top-level-community-list/top-level-community-list.component.ts b/src/app/+home/top-level-community-list/top-level-community-list.component.ts index a3882d7036..783265d110 100644 --- a/src/app/+home/top-level-community-list/top-level-community-list.component.ts +++ b/src/app/+home/top-level-community-list/top-level-community-list.component.ts @@ -40,5 +40,7 @@ export class TopLevelCommunityListComponent { elementsPerPage: data.pageSize, sort: { field: data.sortField, direction: data.sortDirection } }); + this.cds.getScopedEndpoint('7669c72a-3f2a-451f-a3b9-9210e7a4c02f') + .subscribe((c) => console.log('communities', c)) } } diff --git a/src/app/+item-page/simple/item-page.component.ts b/src/app/+item-page/simple/item-page.component.ts index dd37036c21..9ed694e80e 100644 --- a/src/app/+item-page/simple/item-page.component.ts +++ b/src/app/+item-page/simple/item-page.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs/Observable'; @@ -16,6 +16,7 @@ import { Bitstream } from '../../core/shared/bitstream.model'; selector: 'ds-item-page', styleUrls: ['./item-page.component.scss'], templateUrl: './item-page.component.html', + changeDetection: ChangeDetectionStrategy.OnPush }) export class ItemPageComponent implements OnInit { diff --git a/src/app/browser-app.module.ts b/src/app/browser-app.module.ts index a8e6b08312..8201fbc092 100644 --- a/src/app/browser-app.module.ts +++ b/src/app/browser-app.module.ts @@ -40,6 +40,7 @@ export function createTranslateLoader(http: HttpClient) { // forRoot ensures the providers are only created once IdlePreloadModule.forRoot(), RouterModule.forRoot([], { + // enableTracing: true, useHash: false, preloadingStrategy: IdlePreload diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts new file mode 100644 index 0000000000..0e4ff9da37 --- /dev/null +++ b/src/app/core/browse/browse.service.ts @@ -0,0 +1,62 @@ +import { Inject, Injectable } from '@angular/core'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { RequestService } from '../data/request.service'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { GLOBAL_CONFIG } from '../../../config'; +import { BrowseEndpointRequest, RestRequest } from '../data/request.models'; +import { ResponseCacheEntry } from '../cache/response-cache.reducer'; +import { BrowseSuccessResponse } from '../cache/response-cache.models'; +import { isNotEmpty } from '../../shared/empty.util'; +import { BrowseDefinition } from '../shared/browse-definition.model'; +import { Observable } from 'rxjs/Observable'; + +@Injectable() +export class BrowseService extends HALEndpointService { + protected linkName = 'browses'; + + private static toSearchKeyArray(metadatumKey: string): string[] { + const keyParts = metadatumKey.split('.'); + const searchFor = []; + searchFor.push('*'); + for (let i = 0; i < keyParts.length - 1; i++) { + const prevParts = keyParts.slice(0, i + 1); + const nextPart = [...prevParts, '*'].join('.'); + searchFor.push(nextPart); + } + searchFor.push(metadatumKey); + return searchFor; + } + + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig) { + super(); + } + + getBrowseURLFor(metadatumKey: string, linkName: string): Observable { + const searchKeyArray = BrowseService.toSearchKeyArray(metadatumKey); + return this.getEndpoint() + .filter((href: string) => isNotEmpty(href)) + .distinctUntilChanged() + .map((endpointURL: string) => new BrowseEndpointRequest(endpointURL)) + .do((request: RestRequest) => { + setTimeout(() => { + this.requestService.configure(request); + }, 0); + }) + .flatMap((request: RestRequest) => this.responseCache.get(request.href) + .map((entry: ResponseCacheEntry) => entry.response) + .filter((response: BrowseSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.browseDefinitions)) + .map((response: BrowseSuccessResponse) => response.browseDefinitions) + .map((browseDefinitions: BrowseDefinition[]) => browseDefinitions + .find((def: BrowseDefinition) => { + const matchingKeys = def.metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0); + return matchingKeys.length > 0 + }) + ).map((def: BrowseDefinition) => def._links[linkName]) + ); + } + +} diff --git a/src/app/core/cache/models/normalized-dspace-object.model.ts b/src/app/core/cache/models/normalized-dspace-object.model.ts index 3c6ca663c6..da42ea5a9b 100644 --- a/src/app/core/cache/models/normalized-dspace-object.model.ts +++ b/src/app/core/cache/models/normalized-dspace-object.model.ts @@ -66,4 +66,14 @@ export abstract class NormalizedDSpaceObject extends NormalizedObject { @autoserialize owner: string; + /** + * The links to all related resources returned by the rest api. + * + * Repeated here to make the serialization work, + * inheritSerialization doesn't seem to work for more than one level + */ + @autoserialize + _links: { + [name: string]: string + } } diff --git a/src/app/core/cache/models/normalized-object.model.ts b/src/app/core/cache/models/normalized-object.model.ts index 7c4154eae9..b26bd90b2a 100644 --- a/src/app/core/cache/models/normalized-object.model.ts +++ b/src/app/core/cache/models/normalized-object.model.ts @@ -17,4 +17,8 @@ export abstract class NormalizedObject implements CacheableObject { @autoserialize uuid: string; + @autoserialize + _links: { + [name: string]: string + } } diff --git a/src/app/core/cache/response-cache.models.ts b/src/app/core/cache/response-cache.models.ts index 8444a86490..4e3939b425 100644 --- a/src/app/core/cache/response-cache.models.ts +++ b/src/app/core/cache/response-cache.models.ts @@ -1,5 +1,6 @@ import { RequestError } from '../data/request.models'; import { PageInfo } from '../shared/page-info.model'; +import { BrowseDefinition } from '../shared/browse-definition.model'; /* tslint:disable:max-classes-per-file */ export class RestResponse { @@ -32,6 +33,15 @@ export class RootSuccessResponse extends RestResponse { } } +export class BrowseSuccessResponse extends RestResponse { + constructor( + public browseDefinitions: BrowseDefinition[], + public statusCode: string + ) { + super(true, statusCode); + } +} + export class ErrorResponse extends RestResponse { errorMessage: string; diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 9742a6b500..3372e06e47 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -24,6 +24,8 @@ import { HostWindowService } from '../shared/host-window.service'; import { NativeWindowFactory, NativeWindowService } from '../shared/window.service'; import { ServerResponseService } from '../shared/server-response.service'; +import { BrowseService } from './browse/browse.service'; +import { BrowseResponseParsingService } from './data/browse-response-parsing.service'; const IMPORTS = [ CommonModule, @@ -54,6 +56,8 @@ const PROVIDERS = [ ResponseCacheService, RootResponseParsingService, ServerResponseService, + BrowseResponseParsingService, + BrowseService, { provide: NativeWindowService, useFactory: NativeWindowFactory } ]; diff --git a/src/app/core/data/browse-response-parsing.service.ts b/src/app/core/data/browse-response-parsing.service.ts new file mode 100644 index 0000000000..8633e7269a --- /dev/null +++ b/src/app/core/data/browse-response-parsing.service.ts @@ -0,0 +1,28 @@ +import { Injectable } from '@angular/core'; +import { ResponseParsingService } from './parsing.service'; +import { RestRequest } from './request.models'; +import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model'; +import { BrowseSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; +import { isNotEmpty } from '../../shared/empty.util'; +import { DSpaceRESTv2Serializer } from '../dspace-rest-v2/dspace-rest-v2.serializer'; +import { BrowseDefinition } from '../shared/browse-definition.model'; + +@Injectable() +export class BrowseResponseParsingService implements ResponseParsingService { + + parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse { + if (isNotEmpty(data.payload) && isNotEmpty(data.payload._embedded) + && Array.isArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]])) { + const serializer = new DSpaceRESTv2Serializer(BrowseDefinition); + const browseDefinitions = serializer.deserializeArray(data.payload._embedded[Object.keys(data.payload._embedded)[0]]); + return new BrowseSuccessResponse(browseDefinitions, data.statusCode); + } else { + return new ErrorResponse( + Object.assign( + new Error('Unexpected response from browse endpoint'), + { statusText: data.statusCode } + ) + ); + } + } +} diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index ec765c3cb1..e3c43910ab 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -9,20 +9,54 @@ import { CoreState } from '../core.reducers'; import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; +import { Observable } from 'rxjs/Observable'; +import { CommunityDataService } from './community-data.service'; +import { FindByIDRequest } from './request.models'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { NormalizedCommunity } from '../cache/models/normalized-community.model'; +import { isNotEmpty } from '../../shared/empty.util'; @Injectable() export class CollectionDataService extends DataService { protected linkName = 'collections'; - protected browseEndpoint = '/discover/browses/dateissued/collections'; constructor( protected responseCache: ResponseCacheService, protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected store: Store, - @Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + private cds: CommunityDataService, + protected objectCache: ObjectCacheService ) { - super(NormalizedCollection, EnvConfig); + super(NormalizedCollection); } + /** + * Get the scoped endpoint URL by fetching the object with + * the given scopeID and returning its HAL link with this + * data-service's linkName + * + * @param {string} scopeID + * the id of the scope object + * @return { Observable } + * an Observable containing the scoped URL + */ + public getScopedEndpoint(scopeID: string): Observable { + this.cds.getEndpoint() + .map((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID)) + .filter((href: string) => isNotEmpty(href)) + .take(1) + .subscribe((href: string) => { + const request = new FindByIDRequest(href, scopeID); + setTimeout(() => { + this.requestService.configure(request); + }, 0); + }); + + return this.objectCache.getByUUID(scopeID, NormalizedCommunity) + .map((nc: NormalizedCommunity) => nc._links[this.linkName]) + .filter((href) => isNotEmpty(href)) + .distinctUntilChanged(); + } } diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 532bce5ee6..5fdf3f4026 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -10,20 +10,52 @@ import { CoreState } from '../core.reducers'; import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; +import { Observable } from 'rxjs/Observable'; +import { isNotEmpty } from '../../shared/empty.util'; +import { FindByIDRequest } from './request.models'; +import { ObjectCacheService } from '../cache/object-cache.service'; @Injectable() export class CommunityDataService extends DataService { protected linkName = 'communities'; - protected browseEndpoint = '/discover/browses/dateissued/communities'; constructor( protected responseCache: ResponseCacheService, protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected store: Store, - @Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + protected objectCache: ObjectCacheService ) { - super(NormalizedCommunity, EnvConfig); + super(NormalizedCommunity); + } + + /** + * Get the scoped endpoint URL by fetching the object with + * the given scopeID and returning its HAL link with this + * data-service's linkName + * + * @param {string} scopeID + * the id of the scope object + * @return { Observable } + * an Observable containing the scoped URL + */ + public getScopedEndpoint(scopeID: string): Observable { + this.getEndpoint() + .map((endpoint: string) => this.getFindByIDHref(endpoint, scopeID)) + .filter((href: string) => isNotEmpty(href)) + .take(1) + .subscribe((href: string) => { + const request = new FindByIDRequest(href, scopeID); + setTimeout(() => { + this.requestService.configure(request); + }, 0); + }); + + return this.objectCache.getByUUID(scopeID, NormalizedCommunity) + .map((nc: NormalizedCommunity) => nc._links[this.linkName]) + .filter((href) => isNotEmpty(href)) + .distinctUntilChanged(); } } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index e48e7a8bb8..cabf452f01 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,71 +1,41 @@ -import { ResponseCacheService } from '../cache/response-cache.service'; -import { CacheableObject } from '../cache/object-cache.reducer'; -import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { RemoteData } from './remote-data'; -import { - FindAllOptions, FindAllRequest, FindByIDRequest, RestRequest, - RootEndpointRequest -} from './request.models'; import { Store } from '@ngrx/store'; -import { CoreState } from '../core.reducers'; -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { GenericConstructor } from '../shared/generic-constructor'; -import { GlobalConfig } from '../../../config'; -import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { Observable } from 'rxjs/Observable'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { EndpointMap, RootSuccessResponse } from '../cache/response-cache.models'; +import { GlobalConfig } from '../../../config'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { CoreState } from '../core.reducers'; +import { GenericConstructor } from '../shared/generic-constructor'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { RemoteData } from './remote-data'; +import { FindAllOptions, FindAllRequest, FindByIDRequest, RestRequest } from './request.models'; +import { RequestService } from './request.service'; -export abstract class DataService { +export abstract class DataService extends HALEndpointService { protected abstract responseCache: ResponseCacheService; protected abstract requestService: RequestService; protected abstract rdbService: RemoteDataBuildService; protected abstract store: Store; protected abstract linkName: string; - protected abstract browseEndpoint: string; + protected abstract EnvConfig: GlobalConfig constructor( private normalizedResourceType: GenericConstructor, - protected EnvConfig: GlobalConfig ) { - + super(); } - private getEndpointMap(): Observable { - const request = new RootEndpointRequest(this.EnvConfig); - this.requestService.configure(request); - return this.responseCache.get(request.href) - .map((entry: ResponseCacheEntry) => entry.response) - .filter((response: RootSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap)) - .map((response: RootSuccessResponse) => response.endpointMap) - .distinctUntilChanged(); - } + public abstract getScopedEndpoint(scope: string): Observable - public getEndpoint(): Observable { - const request = new RootEndpointRequest(this.EnvConfig); - this.requestService.configure(request); - return this.getEndpointMap() - .map((map: EndpointMap) => map[this.linkName]) - .distinctUntilChanged(); - } - - public isEnabledOnRestApi(): Observable { - return this.getEndpointMap() - .map((map: EndpointMap) => isNotEmpty(map[this.linkName])) - .startWith(undefined) - .distinctUntilChanged(); - } - - protected getFindAllHref(endpoint, options: FindAllOptions = {}): string { + protected getFindAllHref(endpoint, options: FindAllOptions = {}): Observable { let result; const args = []; if (hasValue(options.scopeID)) { - result = new RESTURLCombiner(this.EnvConfig, this.browseEndpoint).toString(); - args.push(`scope=${options.scopeID}`); + result = this.getScopedEndpoint(options.scopeID); } else { - result = endpoint; + result = Observable.of(endpoint); } if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { @@ -86,25 +56,28 @@ export abstract class DataService } if (isNotEmpty(args)) { - result = `${result}?${args.join('&')}`; + return result.map((href: string) => `${href}?${args.join('&')}`); + } else { + return result; } - return result; } findAll(options: FindAllOptions = {}): RemoteData { const hrefObs = this.getEndpoint() - .map((endpoint: string) => this.getFindAllHref(endpoint, options)); + .flatMap((endpoint: string) => this.getFindAllHref(endpoint, options)); hrefObs .subscribe((href: string) => { const request = new FindAllRequest(href, options); - this.requestService.configure(request); + setTimeout(() => { + this.requestService.configure(request); + }, 0); }); return this.rdbService.buildList(hrefObs, this.normalizedResourceType); } - protected getFindByIDHref(endpoint, resourceID): string { + getFindByIDHref(endpoint, resourceID): string { return `${endpoint}/${resourceID}`; } @@ -115,14 +88,18 @@ export abstract class DataService hrefObs .subscribe((href: string) => { const request = new FindByIDRequest(href, id); - this.requestService.configure(request); + setTimeout(() => { + this.requestService.configure(request); + }, 0); }); return this.rdbService.buildSingle(hrefObs, this.normalizedResourceType); } findByHref(href: string): RemoteData { - this.requestService.configure(new RestRequest(href)); + setTimeout(() => { + this.requestService.configure(new RestRequest(href)); + }, 0); return this.rdbService.buildSingle(href, this.normalizedResourceType); // return this.rdbService.buildSingle(href)); } diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index d155910b4e..bbb03acb1d 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -10,19 +10,29 @@ import { NormalizedItem } from '../cache/models/normalized-item.model'; import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; +import { Observable } from 'rxjs/Observable'; +import { BrowseService } from '../browse/browse.service'; +import { isNotEmpty } from '../../shared/empty.util'; @Injectable() export class ItemDataService extends DataService { protected linkName = 'items'; - protected browseEndpoint = '/discover/browses/dateissued/items'; constructor( protected responseCache: ResponseCacheService, protected requestService: RequestService, protected rdbService: RemoteDataBuildService, protected store: Store, - @Inject(GLOBAL_CONFIG) EnvConfig: GlobalConfig + @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, + private bs: BrowseService ) { - super(NormalizedItem, EnvConfig); + super(NormalizedItem); } + + public getScopedEndpoint(scopeID: string): Observable { + return this.bs.getBrowseURLFor('dc.date.issued', this.linkName) + .filter((href) => isNotEmpty(href)) + .distinctUntilChanged(); + } + } diff --git a/src/app/core/data/request.models.ts b/src/app/core/data/request.models.ts index 8c415e71ef..ab3e38d9cd 100644 --- a/src/app/core/data/request.models.ts +++ b/src/app/core/data/request.models.ts @@ -5,6 +5,7 @@ import { RESTURLCombiner } from '../url-combiner/rest-url-combiner'; import { DSOResponseParsingService } from './dso-response-parsing.service'; import { ResponseParsingService } from './parsing.service'; import { RootResponseParsingService } from './root-response-parsing.service'; +import { BrowseResponseParsingService } from './browse-response-parsing.service'; /* tslint:disable:max-classes-per-file */ export class RestRequest { @@ -53,6 +54,16 @@ export class RootEndpointRequest extends RestRequest { } } +export class BrowseEndpointRequest extends RestRequest { + constructor(href: string) { + super(href); + } + + getResponseParser(): GenericConstructor { + return BrowseResponseParsingService; + } +} + export class RequestError extends Error { statusText: string; } diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index e6b4f816f1..4231f9efbb 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -6,7 +6,7 @@ import { Observable } from 'rxjs/Observable'; import { hasValue } from '../../shared/empty.util'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { DSOSuccessResponse } from '../cache/response-cache.models'; +import { DSOSuccessResponse, RestResponse } from '../cache/response-cache.models'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; @@ -47,16 +47,20 @@ export class RequestService { configure(request: RestRequest): void { let isCached = this.objectCache.hasBySelfLink(request.href); - + // console.log('request.href', request.href); if (!isCached && this.responseCache.has(request.href)) { - const [dsoSuccessResponse, otherSuccessResponse] = this.responseCache.get(request.href) + const [successResponse, errorResponse] = this.responseCache.get(request.href) .take(1) - .filter((entry: ResponseCacheEntry) => entry.response.isSuccessful) .map((entry: ResponseCacheEntry) => entry.response) + .share() + .partition((response: RestResponse) => response.isSuccessful); + + const [dsoSuccessResponse, otherSuccessResponse] = successResponse .share() .partition((response: DSOSuccessResponse) => hasValue(response.resourceSelfLinks)); Observable.merge( + errorResponse.map(() => true), // TODO add a configurable number of retries in case of an error. otherSuccessResponse.map(() => true), dsoSuccessResponse // a DSOSuccessResponse should only be considered cached if all its resources are cached .map((response: DSOSuccessResponse) => response.resourceSelfLinks) diff --git a/src/app/core/data/root-response-parsing.service.ts b/src/app/core/data/root-response-parsing.service.ts index 016a501685..a3e7fc22a3 100644 --- a/src/app/core/data/root-response-parsing.service.ts +++ b/src/app/core/data/root-response-parsing.service.ts @@ -19,12 +19,7 @@ export class RootResponseParsingService implements ResponseParsingService { if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links)) { const links = data.payload._links; for (const link of Object.keys(links)) { - let href = links[link].href; - // TODO temporary workaround as these endpoint paths are relative, but should be absolute - if (isNotEmpty(href) && !href.startsWith('http')) { - href = new RESTURLCombiner(this.EnvConfig, href.substring(this.EnvConfig.rest.nameSpace.length)).toString(); - } - links[link] = href; + links[link] = links[link].href; } return new RootSuccessResponse(links, data.statusCode); } else { diff --git a/src/app/core/shared/browse-definition.model.ts b/src/app/core/shared/browse-definition.model.ts new file mode 100644 index 0000000000..bdb91167b0 --- /dev/null +++ b/src/app/core/shared/browse-definition.model.ts @@ -0,0 +1,24 @@ +import { autoserialize, autoserializeAs } from 'cerialize'; +import { SortOption } from './sort-option.model'; + +export class BrowseDefinition { + @autoserialize + metadataBrowse: boolean; + + @autoserialize + sortOptions: SortOption[]; + + @autoserializeAs('order') + defaultSortOrder: string; + + @autoserialize + type: string; + + @autoserializeAs('metadata') + metadataKeys: string[]; + + @autoserialize + _links: { + [name: string]: string + } +} diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts new file mode 100644 index 0000000000..58c647ead8 --- /dev/null +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -0,0 +1,46 @@ +import { Observable } from 'rxjs/Observable'; +import { RequestService } from '../data/request.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { EndpointMap, RootSuccessResponse } from '../cache/response-cache.models'; +import { RootEndpointRequest } from '../data/request.models'; +import { ResponseCacheEntry } from '../cache/response-cache.reducer'; +import { isNotEmpty } from '../../shared/empty.util'; + +export abstract class HALEndpointService { + protected abstract responseCache: ResponseCacheService; + protected abstract requestService: RequestService; + protected abstract linkName: string; + protected abstract EnvConfig: GlobalConfig; + + protected getEndpointMap(): Observable { + const request = new RootEndpointRequest(this.EnvConfig); + setTimeout(() => { + this.requestService.configure(request); + }, 0); + return this.responseCache.get(request.href) + .map((entry: ResponseCacheEntry) => entry.response) + .filter((response: RootSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap)) + .map((response: RootSuccessResponse) => response.endpointMap) + .distinctUntilChanged(); + } + + public getEndpoint(): Observable { + return this.getEndpointMap() + .do((map: EndpointMap) => { + if (!this.linkName) { + console.log('map', this) + } + }) + .map((map: EndpointMap) => map[this.linkName]) + .distinctUntilChanged(); + } + + public isEnabledOnRestApi(): Observable { + return this.getEndpointMap() + .map((map: EndpointMap) => isNotEmpty(map[this.linkName])) + .startWith(undefined) + .distinctUntilChanged(); + } + +} diff --git a/src/app/core/shared/sort-option.model.ts b/src/app/core/shared/sort-option.model.ts new file mode 100644 index 0000000000..c735e87b9a --- /dev/null +++ b/src/app/core/shared/sort-option.model.ts @@ -0,0 +1,9 @@ +import { autoserialize } from 'cerialize'; + +export class SortOption { + @autoserialize + name: string; + + @autoserialize + metadata: string; +} From 3131077a9101e54ac2228fa127c379d7236b74da Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 12 Oct 2017 14:06:37 +0200 Subject: [PATCH 05/22] added tests for BrowseResponseParsingService --- .../browse-response-parsing.service.spec.ts | 164 ++++++++++++++++++ 1 file changed, 164 insertions(+) create mode 100644 src/app/core/data/browse-response-parsing.service.spec.ts diff --git a/src/app/core/data/browse-response-parsing.service.spec.ts b/src/app/core/data/browse-response-parsing.service.spec.ts new file mode 100644 index 0000000000..5f27519a93 --- /dev/null +++ b/src/app/core/data/browse-response-parsing.service.spec.ts @@ -0,0 +1,164 @@ +import { BrowseResponseParsingService } from './browse-response-parsing.service'; +import { BrowseEndpointRequest } from './request.models'; +import { BrowseSuccessResponse, ErrorResponse } from '../cache/response-cache.models'; +import { BrowseDefinition } from '../shared/browse-definition.model'; + +describe('BrowseResponseParsingService', () => { + let service: BrowseResponseParsingService; + + beforeEach(() => { + service = new BrowseResponseParsingService(); + }); + + describe('parse', () => { + const validRequest = new BrowseEndpointRequest('https://rest.api/discover/browses'); + + const validResponse = { + payload: { + _embedded: { + browses: [{ + metadataBrowse: false, + sortOptions: [{ name: 'title', metadata: 'dc.title' }, { + name: 'dateissued', + metadata: 'dc.date.issued' + }, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }], + order: 'ASC', + type: 'browse', + metadata: ['dc.date.issued'], + _links: { + self: { href: 'https://rest.api/discover/browses/dateissued' }, + items: { href: 'https://rest.api/discover/browses/dateissued/items' } + } + }, { + metadataBrowse: true, + sortOptions: [{ name: 'title', metadata: 'dc.title' }, { + name: 'dateissued', + metadata: 'dc.date.issued' + }, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }], + order: 'ASC', + type: 'browse', + metadata: ['dc.contributor.*', 'dc.creator'], + _links: { + self: { href: 'https://rest.api/discover/browses/author' }, + entries: { href: 'https://rest.api/discover/browses/author/entries' }, + items: { href: 'https://rest.api/discover/browses/author/items' } + } + }] + }, + _links: { self: { href: 'https://rest.api/discover/browses' } }, + page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } + }, statusCode: '200' + }; + + const invalidResponse1 = { + payload: { + _embedded: { + browse: { + metadataBrowse: false, + sortOptions: [{ name: 'title', metadata: 'dc.title' }, { + name: 'dateissued', + metadata: 'dc.date.issued' + }, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }], + order: 'ASC', + type: 'browse', + metadata: ['dc.date.issued'], + _links: { + self: { href: 'https://rest.api/discover/browses/dateissued' }, + items: { href: 'https://rest.api/discover/browses/dateissued/items' } + } + } + }, + _links: { self: { href: 'https://rest.api/discover/browses' } }, + page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } + }, statusCode: '200' + }; + + const invalidResponse2 = { + payload: { + browses: [{}, {}], + _links: { self: { href: 'https://rest.api/discover/browses' } }, + page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } + }, statusCode: '200' + }; + + const invalidResponse3 = { + payload: { + _links: { self: { href: 'https://rest.api/discover/browses' } }, + page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } + }, statusCode: '500' + }; + + const definitions = [ + Object.assign(new BrowseDefinition(), { + metadataBrowse: false, + sortOptions: [ + { + name: 'title', + metadata: 'dc.title' + }, + { + name: 'dateissued', + metadata: 'dc.date.issued' + }, + { + name: 'dateaccessioned', + metadata: 'dc.date.accessioned' + } + ], + defaultSortOrder: 'ASC', + type: 'browse', + metadataKeys: [ + 'dc.date.issued' + ], + _links: { } + }), + Object.assign(new BrowseDefinition(), { + metadataBrowse: true, + sortOptions: [ + { + name: 'title', + metadata: 'dc.title' + }, + { + name: 'dateissued', + metadata: 'dc.date.issued' + }, + { + name: 'dateaccessioned', + metadata: 'dc.date.accessioned' + } + ], + defaultSortOrder: 'ASC', + type: 'browse', + metadataKeys: [ + 'dc.contributor.*', + 'dc.creator' + ], + _links: { } + }) + ]; + + it('should return a BrowseSuccessResponse if data contains a valid browse endpoint response', () => { + const response = service.parse(validRequest, validResponse); + expect(response.constructor).toBe(BrowseSuccessResponse); + }); + + it('should return an ErrorResponse if data contains an invalid browse endpoint response', () => { + const response1 = service.parse(validRequest, invalidResponse1); + const response2 = service.parse(validRequest, invalidResponse2); + expect(response1.constructor).toBe(ErrorResponse); + expect(response2.constructor).toBe(ErrorResponse); + }); + + it('should return an ErrorResponse if data contains a statuscode other than 200', () => { + const response = service.parse(validRequest, invalidResponse3); + expect(response.constructor).toBe(ErrorResponse); + }); + + it('should return a BrowseSuccessResponse with the BrowseDefinitions in data', () => { + const response = service.parse(validRequest, validResponse); + expect((response as BrowseSuccessResponse).browseDefinitions).toEqual(definitions); + }); + + }); +}); From ebc400947fdb20202259f3445066b21ace5a40fc Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 12 Oct 2017 15:14:36 +0200 Subject: [PATCH 06/22] added tests for HALEndpointService.getEndpointMap --- .../core/shared/hal-endpoint.service.spec.ts | 69 +++++++++++++++++++ 1 file changed, 69 insertions(+) create mode 100644 src/app/core/shared/hal-endpoint.service.spec.ts diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts new file mode 100644 index 0000000000..c8fab70822 --- /dev/null +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -0,0 +1,69 @@ +import { cold, hot } from 'jasmine-marbles'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { RootEndpointRequest } from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { HALEndpointService } from './hal-endpoint.service'; + +describe('HALEndpointService', () => { + let service: HALEndpointService; + let responseCache: ResponseCacheService; + let requestService: RequestService; + let envConfig: GlobalConfig; + + const endpointMap = { + test: 'https://rest.api/test', + }; + + /* tslint:disable:no-shadowed-variable */ + class TestService extends HALEndpointService { + protected linkName = 'test'; + + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected EnvConfig: GlobalConfig) { + super(); + } + } + /* tslint:enable:no-shadowed-variable */ + + describe('getEndpointMap', () => { + beforeEach(() => { + responseCache = jasmine.createSpyObj('responseCache', { + get: hot('--a-', { a: { + response: { endpointMap: endpointMap } + }}) + }); + + requestService = jasmine.createSpyObj('requestService', ['configure']); + + envConfig = { + rest: { baseUrl: 'https://rest.api/' } + } as any; + + service = new TestService( + responseCache, + requestService, + envConfig + ); + }); + + it('should configure a new RootEndpointRequest', (done) => { + (service as any).getEndpointMap(); + const expected = new RootEndpointRequest(envConfig); + setTimeout(() => { + expect(requestService.configure).toHaveBeenCalledWith(expected); + done(); + }, 0); + }); + + it('should return an Observable of the endpoint map', () => { + const result = (service as any).getEndpointMap(); + const expected = cold('--b-', { b: endpointMap }); + expect(result).toBeObservable(expected); + }); + + }); + +}); From a0be759fa072b31a0b760a0ef4084e5d2aa2ff3d Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Fri, 13 Oct 2017 16:44:42 +0200 Subject: [PATCH 07/22] added HalEndpointService spec --- .../core/shared/hal-endpoint.service.spec.ts | 83 +++++++++++++++++-- src/app/core/shared/hal-endpoint.service.ts | 5 -- 2 files changed, 76 insertions(+), 12 deletions(-) diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts index c8fab70822..8b38d39676 100644 --- a/src/app/core/shared/hal-endpoint.service.spec.ts +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -19,21 +19,23 @@ describe('HALEndpointService', () => { class TestService extends HALEndpointService { protected linkName = 'test'; - constructor( - protected responseCache: ResponseCacheService, - protected requestService: RequestService, - protected EnvConfig: GlobalConfig) { + constructor(protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected EnvConfig: GlobalConfig) { super(); } } + /* tslint:enable:no-shadowed-variable */ describe('getEndpointMap', () => { beforeEach(() => { responseCache = jasmine.createSpyObj('responseCache', { - get: hot('--a-', { a: { - response: { endpointMap: endpointMap } - }}) + get: hot('--a-', { + a: { + response: { endpointMap: endpointMap } + } + }) }); requestService = jasmine.createSpyObj('requestService', ['configure']); @@ -66,4 +68,71 @@ describe('HALEndpointService', () => { }); + describe('getEndpoint', () => { + beforeEach(() => { + service = new TestService( + responseCache, + requestService, + envConfig + ); + + spyOn(service as any, 'getEndpointMap').and + .returnValue(hot('--a-', { a: endpointMap })); + }); + + it('should return the endpoint URL for the service\'s linkName', () => { + const result = service.getEndpoint(); + const expected = cold('--b-', { b: endpointMap.test }); + expect(result).toBeObservable(expected); + }); + + it('should return undefined for a linkName that isn\'t in the endpoint map', () => { + (service as any).linkName = 'unknown'; + const result = service.getEndpoint(); + const expected = cold('--b-', { b: undefined }); + expect(result).toBeObservable(expected); + }); + + }); + + describe('isEnabledOnRestApi', () => { + beforeEach(() => { + service = new TestService( + responseCache, + requestService, + envConfig + ); + + }); + + it('should return undefined as long as getEndpointMap hasn\'t fired', () => { + spyOn(service as any, 'getEndpointMap').and + .returnValue(hot('----')); + + const result = service.isEnabledOnRestApi(); + const expected = cold('b---', { b: undefined }); + expect(result).toBeObservable(expected); + }); + + it('should return true if the service\'s linkName is in the endpoint map', () => { + spyOn(service as any, 'getEndpointMap').and + .returnValue(hot('--a-', { a: endpointMap })); + + const result = service.isEnabledOnRestApi(); + const expected = cold('b-c-', { b: undefined, c: true }); + expect(result).toBeObservable(expected); + }); + + it('should return false if the service\'s linkName isn\'t in the endpoint map', () => { + spyOn(service as any, 'getEndpointMap').and + .returnValue(hot('--a-', { a: endpointMap })); + + (service as any).linkName = 'unknown'; + const result = service.isEnabledOnRestApi(); + const expected = cold('b-c-', { b: undefined, c: false }); + expect(result).toBeObservable(expected); + }); + + }); + }); diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 58c647ead8..bb5ec0134e 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -27,11 +27,6 @@ export abstract class HALEndpointService { public getEndpoint(): Observable { return this.getEndpointMap() - .do((map: EndpointMap) => { - if (!this.linkName) { - console.log('map', this) - } - }) .map((map: EndpointMap) => map[this.linkName]) .distinctUntilChanged(); } From f2bce8fc2abd7e993467672e0a4abf99aa253c6f Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Tue, 17 Oct 2017 10:18:35 +0200 Subject: [PATCH 08/22] added tests for browse.service.ts --- src/app/core/browse/browse.service.spec.ts | 175 +++++++++++++++++++++ src/app/core/browse/browse.service.ts | 8 +- 2 files changed, 180 insertions(+), 3 deletions(-) create mode 100644 src/app/core/browse/browse.service.spec.ts diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts new file mode 100644 index 0000000000..f2970f93bc --- /dev/null +++ b/src/app/core/browse/browse.service.spec.ts @@ -0,0 +1,175 @@ +import { BrowseService } from './browse.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { RequestService } from '../data/request.service'; +import { GlobalConfig } from '../../../config'; +import { hot, cold } from 'jasmine-marbles'; +import { BrowseDefinition } from '../shared/browse-definition.model'; +import { BrowseEndpointRequest } from '../data/request.models'; + +fdescribe('BrowseService', () => { + let service: BrowseService; + let responseCache: ResponseCacheService; + let requestService: RequestService; + + const envConfig = {} as GlobalConfig; + const browsesEndpointURL = 'https://rest.api/browses'; + const browseDefinitions = [ + Object.assign(new BrowseDefinition(), { + metadataBrowse: false, + sortOptions: [ + { + name: 'title', + metadata: 'dc.title' + }, + { + name: 'dateissued', + metadata: 'dc.date.issued' + }, + { + name: 'dateaccessioned', + metadata: 'dc.date.accessioned' + } + ], + defaultSortOrder: 'ASC', + type: 'browse', + metadataKeys: [ + 'dc.date.issued' + ], + _links: { + self: 'https://rest.api/discover/browses/dateissued', + items: 'https://rest.api/discover/browses/dateissued/items' + } + }), + Object.assign(new BrowseDefinition(), { + metadataBrowse: true, + sortOptions: [ + { + name: 'title', + metadata: 'dc.title' + }, + { + name: 'dateissued', + metadata: 'dc.date.issued' + }, + { + name: 'dateaccessioned', + metadata: 'dc.date.accessioned' + } + ], + defaultSortOrder: 'ASC', + type: 'browse', + metadataKeys: [ + 'dc.contributor.*', + 'dc.creator' + ], + _links: { + self: 'https://rest.api/discover/browses/author', + entries: 'https://rest.api/discover/browses/author/entries', + items: 'https://rest.api/discover/browses/author/items' + } + }) + ]; + + beforeEach(() => { + responseCache = jasmine.createSpyObj('responseCache', { + get: cold('b-', { + b: { + response: { browseDefinitions } + } + }) + }); + + requestService = jasmine.createSpyObj('requestService', ['configure']); + + service = new BrowseService( + responseCache, + requestService, + envConfig + ); + }); + + describe('getBrowseURLFor', () => { + + describe('when getEndpoint fires', () => { + beforeEach(() => { + spyOn(service, 'getEndpoint').and + .returnValue(hot('--a-', { a: browsesEndpointURL })); + }); + + it('should return the URL for the given metadatumKey and linkName', () => { + const metadatumKey = 'dc.date.issued'; + const linkName = 'items'; + const expectedURL = browseDefinitions[0]._links[linkName]; + + const result = service.getBrowseURLFor(metadatumKey, linkName); + const expected = cold('c-d-', { c: undefined, d: expectedURL }); + + expect(result).toBeObservable(expected); + }); + + it('should work when the definition uses a wildcard in the metadatumKey', () => { + const metadatumKey = 'dc.contributor.author'; // should match dc.contributor.* in the definition + const linkName = 'items'; + const expectedURL = browseDefinitions[1]._links[linkName]; + + const result = service.getBrowseURLFor(metadatumKey, linkName); + const expected = cold('c-d-', { c: undefined, d: expectedURL }); + + expect(result).toBeObservable(expected); + }); + + it('should return undefined when the key doesn\'t match', () => { + const metadatumKey = 'dc.title'; // isn't in the definitions + const linkName = 'items'; + + const result = service.getBrowseURLFor(metadatumKey, linkName); + const expected = cold('c---', { c: undefined }); + + expect(result).toBeObservable(expected); + }); + + it('should return undefined when the link doesn\'t match', () => { + const metadatumKey = 'dc.date.issued'; + const linkName = 'collections'; // isn't in the definitions + + const result = service.getBrowseURLFor(metadatumKey, linkName); + const expected = cold('c---', { c: undefined }); + + expect(result).toBeObservable(expected); + }); + + it('should configure a new BrowseEndpointRequest', (done) => { + const metadatumKey = 'dc.date.issued'; + const linkName = 'items'; + const expectedURL = browseDefinitions[0]._links[linkName]; + const expectedReq = new BrowseEndpointRequest(browsesEndpointURL); + + const result = service.getBrowseURLFor(metadatumKey, linkName); + const expectedObs = cold('c-d-', { c: undefined, d: expectedURL }); + + expect(result).toBeObservable(expectedObs); + + setTimeout(() => { + expect(requestService.configure).toHaveBeenCalledWith(expectedReq); + done(); + }, 0); + + }); + + }); + + describe('when getEndpoint doesn\'t fire', () => { + it('should return undefined as long as getEndpoint hasn\'t fired', () => { + spyOn(service, 'getEndpoint').and + .returnValue(hot('----')); + + const metadatumKey = 'dc.date.issued'; + const linkName = 'items'; + + const result = service.getBrowseURLFor(metadatumKey, linkName); + const expected = cold('b---', { b: undefined }); + expect(result).toBeObservable(expected); + }); + }); + }); +}); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index 0e4ff9da37..e250b9fa63 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -53,10 +53,12 @@ export class BrowseService extends HALEndpointService { .map((browseDefinitions: BrowseDefinition[]) => browseDefinitions .find((def: BrowseDefinition) => { const matchingKeys = def.metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0); - return matchingKeys.length > 0 + return isNotEmpty(matchingKeys); }) - ).map((def: BrowseDefinition) => def._links[linkName]) - ); + ).filter((def: BrowseDefinition) => isNotEmpty(def) && isNotEmpty(def._links)) + .map((def: BrowseDefinition) => def._links[linkName]) + ).startWith(undefined) + .distinctUntilChanged(); } } From 912388c0ab69be913ff0bd025924e759497a455d Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 18 Oct 2017 11:51:01 +0200 Subject: [PATCH 09/22] added tests for CollectionDataService --- src/app/core/browse/browse.service.spec.ts | 19 +-- .../core/data/collection-data.service.spec.ts | 138 ++++++++++++++++++ src/app/core/data/collection-data.service.ts | 26 +++- .../core/metadata/metadata.service.spec.ts | 2 + 4 files changed, 169 insertions(+), 16 deletions(-) create mode 100644 src/app/core/data/collection-data.service.spec.ts diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index f2970f93bc..96d0f3eb62 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -2,11 +2,13 @@ import { BrowseService } from './browse.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { RequestService } from '../data/request.service'; import { GlobalConfig } from '../../../config'; -import { hot, cold } from 'jasmine-marbles'; +import { hot, cold, getTestScheduler } from 'jasmine-marbles'; import { BrowseDefinition } from '../shared/browse-definition.model'; import { BrowseEndpointRequest } from '../data/request.models'; +import { TestScheduler } from 'rxjs/Rx'; -fdescribe('BrowseService', () => { +describe('BrowseService', () => { + let scheduler: TestScheduler; let service: BrowseService; let responseCache: ResponseCacheService; let requestService: RequestService; @@ -71,6 +73,8 @@ fdescribe('BrowseService', () => { ]; beforeEach(() => { + scheduler = getTestScheduler(); + responseCache = jasmine.createSpyObj('responseCache', { get: cold('b-', { b: { @@ -141,16 +145,13 @@ fdescribe('BrowseService', () => { it('should configure a new BrowseEndpointRequest', (done) => { const metadatumKey = 'dc.date.issued'; const linkName = 'items'; - const expectedURL = browseDefinitions[0]._links[linkName]; - const expectedReq = new BrowseEndpointRequest(browsesEndpointURL); + const expected = new BrowseEndpointRequest(browsesEndpointURL); - const result = service.getBrowseURLFor(metadatumKey, linkName); - const expectedObs = cold('c-d-', { c: undefined, d: expectedURL }); - - expect(result).toBeObservable(expectedObs); + scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkName).subscribe()); + scheduler.flush(); setTimeout(() => { - expect(requestService.configure).toHaveBeenCalledWith(expectedReq); + expect(requestService.configure).toHaveBeenCalledWith(expected); done(); }, 0); diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/collection-data.service.spec.ts new file mode 100644 index 0000000000..c953fd1166 --- /dev/null +++ b/src/app/core/data/collection-data.service.spec.ts @@ -0,0 +1,138 @@ +import { CollectionDataService } from './collection-data.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { CoreState } from '../core.reducers'; +import { Store } from '@ngrx/store'; +import { GlobalConfig } from '../../../config'; +import { CommunityDataService } from './community-data.service'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; +import { FindByIDRequest } from './request.models'; +import { NormalizedCommunity } from '../cache/models/normalized-community.model'; +import { TestScheduler } from 'rxjs/Rx'; + +describe('CollectionDataService', () => { + let scheduler: TestScheduler; + let service: CollectionDataService; + let responseCache: ResponseCacheService; + let requestService: RequestService; + let cds: CommunityDataService; + let objectCache: ObjectCacheService; + + const rdbService = {} as RemoteDataBuildService; + const store = {} as Store; + const EnvConfig = {} as GlobalConfig; + + const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; + const communitiesEndpoint = 'https://rest.api/core/communities'; + const communityEndpoint = `${communitiesEndpoint}/${scopeID}`; + const scopedCollectionsEndpoint = `${communityEndpoint}/collections`; + + function initMockCommunityDataService(): CommunityDataService { + return jasmine.createSpyObj('responseCache', { + getEndpoint: hot('--a-', { a: communitiesEndpoint }), + getFindByIDHref: cold('b-', { b: communityEndpoint }) + }); + } + + function initMockRequestService(): RequestService { + return jasmine.createSpyObj('requestService', ['configure']); + } + + function initMockResponceCacheService(isSuccessful: boolean): ResponseCacheService { + return jasmine.createSpyObj('responseCache', { + get: cold('c-', { + c: { response: { isSuccessful } } + }) + }); + } + + function initMockObjectCacheService(): ObjectCacheService { + return jasmine.createSpyObj('objectCache', { + getByUUID: cold('d-', { + d: { + _links: { + collections: scopedCollectionsEndpoint + } + } + }) + }); + } + + function initTestCollectionDataService(): CollectionDataService { + return new CollectionDataService( + responseCache, + requestService, + rdbService, + store, + EnvConfig, + cds, + objectCache + ); + } + + describe('getScopedEndpoint', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + }); + + it('should configure a new FindByIDRequest for the scope Community', (done) => { + cds = initMockCommunityDataService(); + requestService = initMockRequestService(); + objectCache = initMockObjectCacheService(); + responseCache = initMockResponceCacheService(true); + service = initTestCollectionDataService(); + + const expected = new FindByIDRequest(communityEndpoint, scopeID); + + scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe()); + scheduler.flush(); + + setTimeout(() => { + expect(requestService.configure).toHaveBeenCalledWith(expected); + done(); + }, 0); + }); + + describe('if the scope Community can be found', () => { + beforeEach(() => { + cds = initMockCommunityDataService(); + requestService = initMockRequestService(); + objectCache = initMockObjectCacheService(); + responseCache = initMockResponceCacheService(true); + service = initTestCollectionDataService(); + }); + + it('should fetch the scope Community from the cache', () => { + scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe()); + scheduler.flush(); + expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID, NormalizedCommunity); + }); + + it('should return the endpoint to fetch collections within the given scope', () => { + const result = service.getScopedEndpoint(scopeID); + const expected = cold('--e-', { e: scopedCollectionsEndpoint }); + + expect(result).toBeObservable(expected); + }); + }); + + describe('if the scope Community can\'t be found', () => { + beforeEach(() => { + cds = initMockCommunityDataService(); + requestService = initMockRequestService(); + objectCache = initMockObjectCacheService(); + responseCache = initMockResponceCacheService(false); + service = initTestCollectionDataService(); + }); + + it('should throw an error', () => { + const result = service.getScopedEndpoint(scopeID); + const expected = cold('--#-', undefined, new Error(`The Community with scope ${scopeID} couldn't be retrieved`)); + + expect(result).toBeObservable(expected); + }); + }); + }); +}); diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index e3c43910ab..70c50a1f4e 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -15,6 +15,8 @@ import { FindByIDRequest } from './request.models'; import { ObjectCacheService } from '../cache/object-cache.service'; import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { isNotEmpty } from '../../shared/empty.util'; +import { ResponseCacheEntry } from '../cache/response-cache.reducer'; +import { DSOSuccessResponse, RestResponse } from '../cache/response-cache.models'; @Injectable() export class CollectionDataService extends DataService { @@ -43,20 +45,30 @@ export class CollectionDataService extends DataService containing the scoped URL */ public getScopedEndpoint(scopeID: string): Observable { - this.cds.getEndpoint() - .map((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID)) + const scopeCommunityHrefObs = this.cds.getEndpoint() + .flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID)) .filter((href: string) => isNotEmpty(href)) .take(1) - .subscribe((href: string) => { + .do((href: string) => { const request = new FindByIDRequest(href, scopeID); setTimeout(() => { this.requestService.configure(request); }, 0); }); - return this.objectCache.getByUUID(scopeID, NormalizedCommunity) - .map((nc: NormalizedCommunity) => nc._links[this.linkName]) - .filter((href) => isNotEmpty(href)) - .distinctUntilChanged(); + const [successResponse, errorResponse] = scopeCommunityHrefObs + .flatMap((href: string) => this.responseCache.get(href)) + .map((entry: ResponseCacheEntry) => entry.response) + .share() + .partition((response: RestResponse) => response.isSuccessful); + + return Observable.merge( + errorResponse.flatMap((response: DSOSuccessResponse) => + Observable.throw(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))), + successResponse + .flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID, NormalizedCommunity)) + .map((nc: NormalizedCommunity) => nc._links[this.linkName]) + .filter((href) => isNotEmpty(href)) + ).distinctUntilChanged(); } } diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 1258751f58..88fe174ef6 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -30,6 +30,7 @@ import { Item } from '../../core/shared/item.model'; import { MockItem } from '../../shared/mocks/mock-item'; import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; +import { BrowseService } from '../browse/browse.service'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -111,6 +112,7 @@ describe('MetadataService', () => { Meta, Title, ItemDataService, + BrowseService, MetadataService ], schemas: [CUSTOM_ELEMENTS_SCHEMA] From dcea432606cbb19768f3ce35ae8d8cd6dbccdf9f Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 18 Oct 2017 15:41:00 +0200 Subject: [PATCH 10/22] refactored getScopeEndpoint method for Collection- and CommunityDataService --- .../top-level-community-list.component.ts | 2 - src/app/core/browse/browse.service.spec.ts | 2 +- src/app/core/data/collection-data.service.ts | 65 +++-------------- ...ce.spec.ts => comcol-data.service.spec.ts} | 69 +++++++++++++------ src/app/core/data/comcol-data.service.ts | 54 +++++++++++++++ src/app/core/data/community-data.service.ts | 50 +++----------- src/app/core/data/data.service.ts | 2 +- 7 files changed, 122 insertions(+), 122 deletions(-) rename src/app/core/data/{collection-data.service.spec.ts => comcol-data.service.spec.ts} (76%) create mode 100644 src/app/core/data/comcol-data.service.ts diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts index 4986ff9ec9..3be32f5c6e 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts @@ -41,7 +41,5 @@ export class TopLevelCommunityListComponent { elementsPerPage: data.pageSize, sort: { field: data.sortField, direction: data.sortDirection } }); - this.cds.getScopedEndpoint('7669c72a-3f2a-451f-a3b9-9210e7a4c02f') - .subscribe((c) => console.log('communities', c)) } } diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index 96d0f3eb62..faf9174c21 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -142,7 +142,7 @@ describe('BrowseService', () => { expect(result).toBeObservable(expected); }); - it('should configure a new BrowseEndpointRequest', (done) => { + it('should configure a new BrowseEndpointRequest', (done: DoneFn) => { const metadatumKey = 'dc.date.issued'; const linkName = 'items'; const expected = new BrowseEndpointRequest(browsesEndpointURL); diff --git a/src/app/core/data/collection-data.service.ts b/src/app/core/data/collection-data.service.ts index 70c50a1f4e..f9f581128b 100644 --- a/src/app/core/data/collection-data.service.ts +++ b/src/app/core/data/collection-data.service.ts @@ -1,25 +1,18 @@ import { Inject, Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; - -import { DataService } from './data.service'; -import { Collection } from '../shared/collection.model'; -import { ResponseCacheService } from '../cache/response-cache.service'; -import { NormalizedCollection } from '../cache/models/normalized-collection.model'; -import { CoreState } from '../core.reducers'; -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; -import { Observable } from 'rxjs/Observable'; -import { CommunityDataService } from './community-data.service'; -import { FindByIDRequest } from './request.models'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NormalizedCollection } from '../cache/models/normalized-collection.model'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { NormalizedCommunity } from '../cache/models/normalized-community.model'; -import { isNotEmpty } from '../../shared/empty.util'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { DSOSuccessResponse, RestResponse } from '../cache/response-cache.models'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { CoreState } from '../core.reducers'; +import { Collection } from '../shared/collection.model'; +import { ComColDataService } from './comcol-data.service'; +import { CommunityDataService } from './community-data.service'; +import { RequestService } from './request.service'; @Injectable() -export class CollectionDataService extends DataService { +export class CollectionDataService extends ComColDataService { protected linkName = 'collections'; constructor( @@ -28,47 +21,9 @@ export class CollectionDataService extends DataService, @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, - private cds: CommunityDataService, + protected cds: CommunityDataService, protected objectCache: ObjectCacheService ) { super(NormalizedCollection); } - - /** - * Get the scoped endpoint URL by fetching the object with - * the given scopeID and returning its HAL link with this - * data-service's linkName - * - * @param {string} scopeID - * the id of the scope object - * @return { Observable } - * an Observable containing the scoped URL - */ - public getScopedEndpoint(scopeID: string): Observable { - const scopeCommunityHrefObs = this.cds.getEndpoint() - .flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID)) - .filter((href: string) => isNotEmpty(href)) - .take(1) - .do((href: string) => { - const request = new FindByIDRequest(href, scopeID); - setTimeout(() => { - this.requestService.configure(request); - }, 0); - }); - - const [successResponse, errorResponse] = scopeCommunityHrefObs - .flatMap((href: string) => this.responseCache.get(href)) - .map((entry: ResponseCacheEntry) => entry.response) - .share() - .partition((response: RestResponse) => response.isSuccessful); - - return Observable.merge( - errorResponse.flatMap((response: DSOSuccessResponse) => - Observable.throw(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))), - successResponse - .flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID, NormalizedCommunity)) - .map((nc: NormalizedCommunity) => nc._links[this.linkName]) - .filter((href) => isNotEmpty(href)) - ).distinctUntilChanged(); - } } diff --git a/src/app/core/data/collection-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts similarity index 76% rename from src/app/core/data/collection-data.service.spec.ts rename to src/app/core/data/comcol-data.service.spec.ts index c953fd1166..f96ace4452 100644 --- a/src/app/core/data/collection-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -1,20 +1,45 @@ -import { CollectionDataService } from './collection-data.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { CoreState } from '../core.reducers'; import { Store } from '@ngrx/store'; -import { GlobalConfig } from '../../../config'; -import { CommunityDataService } from './community-data.service'; -import { ObjectCacheService } from '../cache/object-cache.service'; import { cold, getTestScheduler, hot } from 'jasmine-marbles'; -import { FindByIDRequest } from './request.models'; -import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { TestScheduler } from 'rxjs/Rx'; +import { GlobalConfig } from '../../../config'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NormalizedCommunity } from '../cache/models/normalized-community.model'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { CoreState } from '../core.reducers'; +import { ComColDataService } from './comcol-data.service'; +import { CommunityDataService } from './community-data.service'; +import { FindByIDRequest } from './request.models'; +import { RequestService } from './request.service'; -describe('CollectionDataService', () => { +const LINK_NAME = 'test'; + +/* tslint:disable:max-classes-per-file */ +class NormalizedTestObject implements CacheableObject { + self: string; +} + +class TestService extends ComColDataService { + protected linkName = LINK_NAME; + + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected EnvConfig: GlobalConfig, + protected cds: CommunityDataService, + protected objectCache: ObjectCacheService + ) { + super(NormalizedTestObject); + } +} +/* tslint:enable:max-classes-per-file */ + +describe('ComColDataService', () => { let scheduler: TestScheduler; - let service: CollectionDataService; + let service: TestService; let responseCache: ResponseCacheService; let requestService: RequestService; let cds: CommunityDataService; @@ -27,7 +52,7 @@ describe('CollectionDataService', () => { const scopeID = 'd9d30c0c-69b7-4369-8397-ca67c888974d'; const communitiesEndpoint = 'https://rest.api/core/communities'; const communityEndpoint = `${communitiesEndpoint}/${scopeID}`; - const scopedCollectionsEndpoint = `${communityEndpoint}/collections`; + const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`; function initMockCommunityDataService(): CommunityDataService { return jasmine.createSpyObj('responseCache', { @@ -53,15 +78,15 @@ describe('CollectionDataService', () => { getByUUID: cold('d-', { d: { _links: { - collections: scopedCollectionsEndpoint + [LINK_NAME]: scopedEndpoint } } }) }); } - function initTestCollectionDataService(): CollectionDataService { - return new CollectionDataService( + function initTestService(): TestService { + return new TestService( responseCache, requestService, rdbService, @@ -77,12 +102,12 @@ describe('CollectionDataService', () => { scheduler = getTestScheduler(); }); - it('should configure a new FindByIDRequest for the scope Community', (done) => { + it('should configure a new FindByIDRequest for the scope Community', (done: DoneFn) => { cds = initMockCommunityDataService(); requestService = initMockRequestService(); objectCache = initMockObjectCacheService(); responseCache = initMockResponceCacheService(true); - service = initTestCollectionDataService(); + service = initTestService(); const expected = new FindByIDRequest(communityEndpoint, scopeID); @@ -101,7 +126,7 @@ describe('CollectionDataService', () => { requestService = initMockRequestService(); objectCache = initMockObjectCacheService(); responseCache = initMockResponceCacheService(true); - service = initTestCollectionDataService(); + service = initTestService(); }); it('should fetch the scope Community from the cache', () => { @@ -110,9 +135,9 @@ describe('CollectionDataService', () => { expect(objectCache.getByUUID).toHaveBeenCalledWith(scopeID, NormalizedCommunity); }); - it('should return the endpoint to fetch collections within the given scope', () => { + it('should return the endpoint to fetch resources within the given scope', () => { const result = service.getScopedEndpoint(scopeID); - const expected = cold('--e-', { e: scopedCollectionsEndpoint }); + const expected = cold('--e-', { e: scopedEndpoint }); expect(result).toBeObservable(expected); }); @@ -124,7 +149,7 @@ describe('CollectionDataService', () => { requestService = initMockRequestService(); objectCache = initMockObjectCacheService(); responseCache = initMockResponceCacheService(false); - service = initTestCollectionDataService(); + service = initTestService(); }); it('should throw an error', () => { diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts new file mode 100644 index 0000000000..3917499914 --- /dev/null +++ b/src/app/core/data/comcol-data.service.ts @@ -0,0 +1,54 @@ +import { Observable } from 'rxjs/Observable'; +import { isNotEmpty } from '../../shared/empty.util'; +import { NormalizedCommunity } from '../cache/models/normalized-community.model'; +import { CacheableObject } from '../cache/object-cache.reducer'; +import { DSOSuccessResponse, RestResponse } from '../cache/response-cache.models'; +import { ResponseCacheEntry } from '../cache/response-cache.reducer'; +import { CommunityDataService } from './community-data.service'; + +import { DataService } from './data.service'; +import { FindByIDRequest } from './request.models'; +import { ObjectCacheService } from '../cache/object-cache.service'; + +export abstract class ComColDataService extends DataService { + protected abstract cds: CommunityDataService; + protected abstract objectCache: ObjectCacheService; + + /** + * Get the scoped endpoint URL by fetching the object with + * the given scopeID and returning its HAL link with this + * data-service's linkName + * + * @param {string} scopeID + * the id of the scope object + * @return { Observable } + * an Observable containing the scoped URL + */ + public getScopedEndpoint(scopeID: string): Observable { + const scopeCommunityHrefObs = this.cds.getEndpoint() + .flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID)) + .filter((href: string) => isNotEmpty(href)) + .take(1) + .do((href: string) => { + const request = new FindByIDRequest(href, scopeID); + setTimeout(() => { + this.requestService.configure(request); + }, 0); + }); + + const [successResponse, errorResponse] = scopeCommunityHrefObs + .flatMap((href: string) => this.responseCache.get(href)) + .map((entry: ResponseCacheEntry) => entry.response) + .share() + .partition((response: RestResponse) => response.isSuccessful); + + return Observable.merge( + errorResponse.flatMap((response: DSOSuccessResponse) => + Observable.throw(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))), + successResponse + .flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID, NormalizedCommunity)) + .map((nc: NormalizedCommunity) => nc._links[this.linkName]) + .filter((href) => isNotEmpty(href)) + ).distinctUntilChanged(); + } +} diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 5fdf3f4026..bbee96ab47 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -1,23 +1,20 @@ import { Inject, Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; - -import { DataService } from './data.service'; -import { Community } from '../shared/community.model'; -import { ResponseCacheService } from '../cache/response-cache.service'; -import { NormalizedCommunity } from '../cache/models/normalized-community.model'; -import { CoreState } from '../core.reducers'; -import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; -import { Observable } from 'rxjs/Observable'; -import { isNotEmpty } from '../../shared/empty.util'; -import { FindByIDRequest } from './request.models'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { ObjectCacheService } from '../cache/object-cache.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { CoreState } from '../core.reducers'; +import { Community } from '../shared/community.model'; +import { ComColDataService } from './comcol-data.service'; +import { RequestService } from './request.service'; @Injectable() -export class CommunityDataService extends DataService { +export class CommunityDataService extends ComColDataService { protected linkName = 'communities'; + protected cds = this; constructor( protected responseCache: ResponseCacheService, @@ -29,33 +26,4 @@ export class CommunityDataService extends DataService } - * an Observable containing the scoped URL - */ - public getScopedEndpoint(scopeID: string): Observable { - this.getEndpoint() - .map((endpoint: string) => this.getFindByIDHref(endpoint, scopeID)) - .filter((href: string) => isNotEmpty(href)) - .take(1) - .subscribe((href: string) => { - const request = new FindByIDRequest(href, scopeID); - setTimeout(() => { - this.requestService.configure(request); - }, 0); - }); - - return this.objectCache.getByUUID(scopeID, NormalizedCommunity) - .map((nc: NormalizedCommunity) => nc._links[this.linkName]) - .filter((href) => isNotEmpty(href)) - .distinctUntilChanged(); - } - } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 77b2a44814..ed9d84f04a 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -21,7 +21,7 @@ export abstract class DataService protected abstract EnvConfig: GlobalConfig; constructor( - private normalizedResourceType: GenericConstructor, + protected normalizedResourceType: GenericConstructor, ) { super(); } From 91c6c60de8f8a50842ef9c6cf5fd3b1990b9885c Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 19 Oct 2017 13:30:13 +0200 Subject: [PATCH 11/22] added HalEndpointService spec --- src/app/core/data/item-data.service.spec.ts | 94 +++++++++++++++++++++ src/app/core/data/item-data.service.ts | 29 ++++--- 2 files changed, 111 insertions(+), 12 deletions(-) create mode 100644 src/app/core/data/item-data.service.spec.ts diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts new file mode 100644 index 0000000000..7d610bfaae --- /dev/null +++ b/src/app/core/data/item-data.service.spec.ts @@ -0,0 +1,94 @@ +import { Store } from '@ngrx/store'; +import { cold, getTestScheduler } from 'jasmine-marbles'; +import { TestScheduler } from 'rxjs/Rx'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { BrowseService } from '../browse/browse.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { CoreState } from '../core.reducers'; +import { ItemDataService } from './item-data.service'; +import { RequestService } from './request.service'; + +describe('ItemDataService', () => { + let scheduler: TestScheduler; + let service: ItemDataService; + let bs: BrowseService; + + const requestService = {} as RequestService; + const responseCache = {} as ResponseCacheService; + const rdbService = {} as RemoteDataBuildService; + const store = {} as Store; + const EnvConfig = {} as GlobalConfig; + + const scopeID = '4af28e99-6a9c-4036-a199-e1b587046d39'; + const browsesEndpoint = 'https://rest.api/discover/browses'; + const itemBrowseEndpoint = `${browsesEndpoint}/author/items`; + const scopedEndpoint = `${itemBrowseEndpoint}?scope=${scopeID}`; + const serviceEndpoint = `https://rest.api/core/items`; + const browseError = new Error('getBrowseURL failed'); + + function initMockBrowseService(isSuccessful: boolean) { + const obs = isSuccessful ? + cold('--a-', { a: itemBrowseEndpoint }) : + cold('--#-', undefined, browseError); + return jasmine.createSpyObj('bs', { + getBrowseURLFor: obs + }); + } + + function initTestService() { + return new ItemDataService( + responseCache, + requestService, + rdbService, + store, + EnvConfig, + bs + ); + } + + describe('getScopedEndpoint', () => { + beforeEach(() => { + scheduler = getTestScheduler(); + }); + + it('should return the endpoint to fetch Items within the given scope', () => { + bs = initMockBrowseService(true); + service = initTestService(); + + const result = service.getScopedEndpoint(scopeID); + const expected = cold('--b-', { b: scopedEndpoint }); + + expect(result).toBeObservable(expected); + }); + + describe('if the dc.date.issue browse isn\'t configured for items', () => { + beforeEach(() => { + bs = initMockBrowseService(false); + service = initTestService(); + }); + it('should throw an error', () => { + const result = service.getScopedEndpoint(scopeID); + const expected = cold('--#-', undefined, browseError); + + expect(result).toBeObservable(expected); + }); + }); + + describe('if the scope is not specified', () => { + beforeEach(() => { + bs = initMockBrowseService(true); + service = initTestService(); + spyOn(service, 'getEndpoint').and.returnValue(cold('--b-', { b: serviceEndpoint })) + }); + + it('should return this.getEndpoint()', () => { + const result = service.getScopedEndpoint(undefined); + const expected = cold('--c-', { c: serviceEndpoint }); + + expect(result).toBeObservable(expected); + }); + }); + + }); +}); diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index bbb03acb1d..addb290e90 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -1,18 +1,19 @@ import { Inject, Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; - -import { DataService } from './data.service'; -import { Item } from '../shared/item.model'; +import { Observable } from 'rxjs/Observable'; +import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; +import { isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { BrowseService } from '../browse/browse.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NormalizedItem } from '../cache/models/normalized-item.model'; import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; -import { NormalizedItem } from '../cache/models/normalized-item.model'; +import { Item } from '../shared/item.model'; +import { URLCombiner } from '../url-combiner/url-combiner'; + +import { DataService } from './data.service'; import { RequestService } from './request.service'; -import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; -import { Observable } from 'rxjs/Observable'; -import { BrowseService } from '../browse/browse.service'; -import { isNotEmpty } from '../../shared/empty.util'; @Injectable() export class ItemDataService extends DataService { @@ -30,9 +31,13 @@ export class ItemDataService extends DataService { } public getScopedEndpoint(scopeID: string): Observable { - return this.bs.getBrowseURLFor('dc.date.issued', this.linkName) - .filter((href) => isNotEmpty(href)) - .distinctUntilChanged(); + if (isEmpty(scopeID)) { + return this.getEndpoint(); + } else { + return this.bs.getBrowseURLFor('dc.date.issued', this.linkName) + .filter((href: string) => isNotEmpty(href)) + .distinctUntilChanged(); + } } } From 376686856b0d01f81b57ee76ad8afa41a7074b38 Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Thu, 19 Oct 2017 15:48:18 +0200 Subject: [PATCH 12/22] forgot to commit a change --- src/app/core/data/item-data.service.ts | 1 + 1 file changed, 1 insertion(+) diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index addb290e90..7e978e0879 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -36,6 +36,7 @@ export class ItemDataService extends DataService { } else { return this.bs.getBrowseURLFor('dc.date.issued', this.linkName) .filter((href: string) => isNotEmpty(href)) + .map((href: string) => new URLCombiner(href, `?scope=${scopeID}`).toString()) .distinctUntilChanged(); } } From feca3662905e8acaedd196e93be277580ddcbf7c Mon Sep 17 00:00:00 2001 From: Art Lowel Date: Wed, 25 Oct 2017 13:31:10 +0200 Subject: [PATCH 13/22] refactor done, todo fix tests --- resources/i18n/en.json | 6 +- .../collection-page.component.html | 19 +- .../collection-page.component.ts | 32 ++- .../community-page.component.html | 10 +- .../community-page.component.ts | 10 +- ...ty-page-sub-collection-list.component.html | 8 +- ...nity-page-sub-collection-list.component.ts | 3 +- .../top-level-community-list.component.html | 8 +- .../top-level-community-list.component.ts | 3 +- .../collections/collections.component.ts | 3 +- .../full/full-item-page.component.html | 8 +- .../full/full-item-page.component.ts | 11 +- .../simple/item-page.component.html | 8 +- .../+item-page/simple/item-page.component.ts | 8 +- .../+search-page/search-page.component.html | 6 +- .../search-page.component.spec.ts | 6 +- src/app/+search-page/search-page.component.ts | 25 +- .../search-results.component.html | 18 +- .../search-results.component.ts | 7 +- .../search-service/search.service.ts | 96 ++++--- src/app/app.component.ts | 9 +- src/app/core/browse/browse.service.spec.ts | 67 +++-- src/app/core/browse/browse.service.ts | 61 +++-- .../builders/remote-data-build.service.ts | 254 +++++++---------- src/app/core/cache/response-cache.service.ts | 2 +- src/app/core/data/comcol-data.service.spec.ts | 28 +- src/app/core/data/comcol-data.service.ts | 52 ++-- src/app/core/data/data.service.ts | 33 ++- src/app/core/data/remote-data.ts | 67 ++--- src/app/core/data/request.service.ts | 25 ++ .../core/metadata/metadata.service.spec.ts | 39 +-- src/app/core/metadata/metadata.service.ts | 32 ++- src/app/core/shared/bitstream.model.ts | 7 +- src/app/core/shared/bundle.model.ts | 9 +- src/app/core/shared/collection.model.ts | 9 +- src/app/core/shared/community.model.ts | 9 +- src/app/core/shared/dspace-object.model.ts | 5 +- .../core/shared/hal-endpoint.service.spec.ts | 7 +- src/app/core/shared/hal-endpoint.service.ts | 5 +- src/app/core/shared/item.model.spec.ts | 28 +- src/app/core/shared/item.model.ts | 19 +- .../object-list/object-list.component.html | 8 +- src/app/object-list/object-list.component.ts | 54 ++-- .../pagenotfound/pagenotfound.component.ts | 2 +- src/app/shared/mocks/mock-item.ts | 256 +++++++----------- .../search-form/search-form.component.html | 6 +- .../search-form/search-form.component.spec.ts | 7 +- .../search-form/search-form.component.ts | 2 +- 48 files changed, 691 insertions(+), 706 deletions(-) diff --git a/resources/i18n/en.json b/resources/i18n/en.json index 2239d605cc..e65321cf39 100644 --- a/resources/i18n/en.json +++ b/resources/i18n/en.json @@ -91,7 +91,8 @@ "sub-collections": "Loading sub-collections...", "items": "Loading items...", "item": "Loading item...", - "objects": "Loading..." + "objects": "Loading...", + "search-results": "Loading search results..." }, "error": { "default": "Error", @@ -101,6 +102,7 @@ "sub-collections": "Error fetching sub-collections", "items": "Error fetching items", "item": "Error fetching item", - "objects": "Error fetching objects" + "objects": "Error fetching objects", + "search-results": "Error fetching search results" } } diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index e385de08b4..b0a544eb37 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -1,13 +1,13 @@
-
-
+
+
@@ -33,18 +33,19 @@
- - + +
-
+

{{'collection.page.browse.recent.head' | translate}}

+ [hideGear]="false" + (paginationChange)="onPaginationChange($event)">
- - + +
diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index 0bc86af045..2ca547dada 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -7,13 +7,13 @@ import { SortOptions } from '../core/cache/models/sort-options.model'; import { CollectionDataService } from '../core/data/collection-data.service'; import { ItemDataService } from '../core/data/item-data.service'; import { RemoteData } from '../core/data/remote-data'; + +import { MetadataService } from '../core/metadata/metadata.service'; import { Bitstream } from '../core/shared/bitstream.model'; import { Collection } from '../core/shared/collection.model'; import { Item } from '../core/shared/item.model'; -import { MetadataService } from '../core/metadata/metadata.service'; - import { fadeIn, fadeInOut } from '../shared/animations/fade'; import { hasValue, isNotEmpty } from '../shared/empty.util'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; @@ -29,9 +29,9 @@ import { PaginationComponentOptions } from '../shared/pagination/pagination-comp ] }) export class CollectionPageComponent implements OnInit, OnDestroy { - collectionData: RemoteData; + collectionData: Observable>; itemData: RemoteData; - logoData: RemoteData; + logoData: Observable>; paginationConfig: PaginationComponentOptions; sortConfig: SortOptions; private subs: Subscription[] = []; @@ -62,7 +62,10 @@ export class CollectionPageComponent implements OnInit, OnDestroy { this.collectionId = params.id; this.collectionData = this.collectionDataService.findById(this.collectionId); this.metadata.processRemoteData(this.collectionData); - this.subs.push(this.collectionData.payload.subscribe((collection) => this.logoData = collection.logo)); + this.subs.push(this.collectionData + .map((rd: RemoteData) => rd.payload) + .filter((collection: Collection) => hasValue(collection)) + .subscribe((collection: Collection) => this.logoData = collection.logo)); const page = +params.page || this.paginationConfig.currentPage; const pageSize = +params.pageSize || this.paginationConfig.pageSize; @@ -84,12 +87,14 @@ export class CollectionPageComponent implements OnInit, OnDestroy { } updatePage(searchOptions) { - this.itemData = this.itemDataService.findAll({ + this.subs.push(this.itemDataService.findAll({ scopeID: this.collectionId, currentPage: searchOptions.pagination.currentPage, elementsPerPage: searchOptions.pagination.pageSize, sort: searchOptions.sort - }); + }).subscribe((rd: RemoteData) => { + this.itemData = rd; + })); } ngOnDestroy(): void { @@ -99,4 +104,17 @@ export class CollectionPageComponent implements OnInit, OnDestroy { isNotEmpty(object: any) { return isNotEmpty(object); } + + onPaginationChange(event) { + this.updatePage({ + pagination: { + currentPage: event.page, + pageSize: event.pageSize + }, + sort: { + field: event.sortField, + direction: event.sortDirection + } + }) + } } diff --git a/src/app/+community-page/community-page.component.html b/src/app/+community-page/community-page.component.html index 3935ffccae..e9f91e3f10 100644 --- a/src/app/+community-page/community-page.component.html +++ b/src/app/+community-page/community-page.component.html @@ -1,10 +1,10 @@ -
-
+
+
@@ -26,5 +26,5 @@
- - + + diff --git a/src/app/+community-page/community-page.component.ts b/src/app/+community-page/community-page.component.ts index c50df3b40c..40b57bc4cd 100644 --- a/src/app/+community-page/community-page.component.ts +++ b/src/app/+community-page/community-page.component.ts @@ -12,6 +12,7 @@ import { MetadataService } from '../core/metadata/metadata.service'; import { fadeInOut } from '../shared/animations/fade'; import { hasValue } from '../shared/empty.util'; +import { Observable } from 'rxjs/Observable'; @Component({ selector: 'ds-community-page', @@ -21,8 +22,8 @@ import { hasValue } from '../shared/empty.util'; animations: [fadeInOut] }) export class CommunityPageComponent implements OnInit, OnDestroy { - communityData: RemoteData; - logoData: RemoteData; + communityData: Observable>; + logoData: Observable>; private subs: Subscription[] = []; constructor( @@ -37,7 +38,10 @@ export class CommunityPageComponent implements OnInit, OnDestroy { this.route.params.subscribe((params: Params) => { this.communityData = this.communityDataService.findById(params.id); this.metadata.processRemoteData(this.communityData); - this.subs.push(this.communityData.payload.subscribe((community) => this.logoData = community.logo)); + this.subs.push(this.communityData + .map((rd: RemoteData) => rd.payload) + .filter((community: Community) => hasValue(community)) + .subscribe((community: Community) => this.logoData = community.logo)); }); } diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html index 31dc2cd326..16de33a642 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html @@ -1,7 +1,7 @@ -
+

{{'community.sub-collection-list.head' | translate}}

- - + + diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts index 618890a60c..0f310d4aec 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts @@ -5,6 +5,7 @@ import { RemoteData } from '../../core/data/remote-data'; import { Collection } from '../../core/shared/collection.model'; import { fadeIn } from '../../shared/animations/fade'; +import { Observable } from 'rxjs/Observable'; @Component({ selector: 'ds-community-page-sub-collection-list', @@ -13,7 +14,7 @@ import { fadeIn } from '../../shared/animations/fade'; animations:[fadeIn] }) export class CommunityPageSubCollectionListComponent implements OnInit { - subCollections: RemoteData; + subCollections: Observable>; constructor(private cds: CollectionDataService) { diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.html b/src/app/+home-page/top-level-community-list/top-level-community-list.component.html index a5bd6c5c5d..e3e995f545 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.html +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.html @@ -1,13 +1,13 @@ -
+

{{'home.top-level-communities.head' | translate}}

{{'home.top-level-communities.help' | translate}}

- - \ No newline at end of file + + diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts index 3be32f5c6e..b7510db5ed 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts @@ -1,4 +1,5 @@ import { ChangeDetectionStrategy, Component } from '@angular/core'; +import { Observable } from 'rxjs/Observable'; import { SortOptions } from '../../core/cache/models/sort-options.model'; import { CommunityDataService } from '../../core/data/community-data.service'; @@ -16,7 +17,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c animations: [fadeInOut] }) export class TopLevelCommunityListComponent { - topLevelCommunities: RemoteData; + topLevelCommunities: Observable>; config: PaginationComponentOptions; sortConfig: SortOptions; diff --git a/src/app/+item-page/field-components/collections/collections.component.ts b/src/app/+item-page/field-components/collections/collections.component.ts index c3db3135e3..8b7b5d7f58 100644 --- a/src/app/+item-page/field-components/collections/collections.component.ts +++ b/src/app/+item-page/field-components/collections/collections.component.ts @@ -4,6 +4,7 @@ import { Observable } from 'rxjs/Observable'; import { Collection } from '../../../core/shared/collection.model'; import { Item } from '../../../core/shared/item.model'; import { RemoteDataBuildService } from '../../../core/cache/builders/remote-data-build.service'; +import { RemoteData } from '../../../core/data/remote-data'; /** * This component renders the parent collections section of the item @@ -34,7 +35,7 @@ export class CollectionsComponent implements OnInit { // TODO: this should use parents, but the collections // for an Item aren't returned by the REST API yet, // only the owning collection - this.collections = this.item.owner.payload.map((c) => [c]); + this.collections = this.item.owner.map((rd: RemoteData) => [rd.payload]); } } diff --git a/src/app/+item-page/full/full-item-page.component.html b/src/app/+item-page/full/full-item-page.component.html index 7b25f88753..998be0eb76 100644 --- a/src/app/+item-page/full/full-item-page.component.html +++ b/src/app/+item-page/full/full-item-page.component.html @@ -1,5 +1,5 @@ -
-
+
+ - - + + diff --git a/src/app/+item-page/full/full-item-page.component.ts b/src/app/+item-page/full/full-item-page.component.ts index 270cf1fcae..1a26b92f13 100644 --- a/src/app/+item-page/full/full-item-page.component.ts +++ b/src/app/+item-page/full/full-item-page.component.ts @@ -1,4 +1,4 @@ -import { Component, OnInit } from '@angular/core'; +import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; import { Observable } from 'rxjs/Observable'; @@ -13,6 +13,7 @@ import { Item } from '../../core/shared/item.model'; import { MetadataService } from '../../core/metadata/metadata.service'; import { fadeInOut } from '../../shared/animations/fade'; +import { hasValue } from '../../shared/empty.util'; /** * This component renders a simple item page. @@ -24,11 +25,12 @@ import { fadeInOut } from '../../shared/animations/fade'; selector: 'ds-full-item-page', styleUrls: ['./full-item-page.component.scss'], templateUrl: './full-item-page.component.html', + changeDetection: ChangeDetectionStrategy.OnPush, animations: [fadeInOut] }) export class FullItemPageComponent extends ItemPageComponent implements OnInit { - item: RemoteData; + item: Observable>; metadata: Observable; @@ -43,7 +45,10 @@ export class FullItemPageComponent extends ItemPageComponent implements OnInit { initialize(params) { super.initialize(params); - this.metadata = this.item.payload.map((i) => i.metadata); + this.metadata = this.item + .map((rd: RemoteData) => rd.payload) + .filter((item: Item) => hasValue(item)) + .map((item: Item) => item.metadata); } } diff --git a/src/app/+item-page/simple/item-page.component.html b/src/app/+item-page/simple/item-page.component.html index 10ceec2a6f..3ce80a0eed 100644 --- a/src/app/+item-page/simple/item-page.component.html +++ b/src/app/+item-page/simple/item-page.component.html @@ -1,5 +1,5 @@ -
-
+
+
@@ -23,5 +23,5 @@
- - + + diff --git a/src/app/+item-page/simple/item-page.component.ts b/src/app/+item-page/simple/item-page.component.ts index 50c36fc76e..7856315810 100644 --- a/src/app/+item-page/simple/item-page.component.ts +++ b/src/app/+item-page/simple/item-page.component.ts @@ -11,6 +11,7 @@ import { Item } from '../../core/shared/item.model'; import { MetadataService } from '../../core/metadata/metadata.service'; import { fadeInOut } from '../../shared/animations/fade'; +import { hasValue } from '../../shared/empty.util'; /** * This component renders a simple item page. @@ -30,7 +31,7 @@ export class ItemPageComponent implements OnInit { private sub: any; - item: RemoteData; + item: Observable>; thumbnail: Observable; @@ -52,7 +53,10 @@ export class ItemPageComponent implements OnInit { this.id = +params.id; this.item = this.items.findById(params.id); this.metadataService.processRemoteData(this.item); - this.thumbnail = this.item.payload.flatMap((i) => i.getThumbnail()); + this.thumbnail = this.item + .map((rd: RemoteData) => rd.payload) + .filter((item: Item) => hasValue(item)) + .flatMap((item: Item) => item.getThumbnail()); } } diff --git a/src/app/+search-page/search-page.component.html b/src/app/+search-page/search-page.component.html index d5e3a831eb..4833b4c363 100644 --- a/src/app/+search-page/search-page.component.html +++ b/src/app/+search-page/search-page.component.html @@ -1,9 +1,9 @@
+ [scopes]="(scopeList | async)?.payload"> - +
diff --git a/src/app/+search-page/search-page.component.spec.ts b/src/app/+search-page/search-page.component.spec.ts index 0b00021ed6..0b80b16196 100644 --- a/src/app/+search-page/search-page.component.spec.ts +++ b/src/app/+search-page/search-page.component.spec.ts @@ -9,7 +9,7 @@ import { Community } from '../core/shared/community.model'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model'; -describe('SearchPageComponent', () => { +fdescribe('SearchPageComponent', () => { let comp: SearchPageComponent; let fixture: ComponentFixture; let searchServiceObject: SearchService; @@ -27,8 +27,8 @@ describe('SearchPageComponent', () => { }; const mockCommunityList = []; const communityDataServiceStub = { - findAll: () => mockCommunityList, - findById: () => new Community() + findAll: () => Observable.of(mockCommunityList), + findById: () => Observable.of(new Community()) }; class RouterStub { diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index 1592dc3c24..79b813a1b6 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -1,15 +1,16 @@ -import { Component, OnInit, OnDestroy, ChangeDetectionStrategy } from '@angular/core'; -import { SearchService } from './search-service/search.service'; +import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core'; import { ActivatedRoute } from '@angular/router'; -import { RemoteData } from '../core/data/remote-data'; -import { SearchResult } from './search-result.model'; -import { DSpaceObject } from '../core/shared/dspace-object.model'; +import { Observable } from 'rxjs/Observable'; import { SortOptions } from '../core/cache/models/sort-options.model'; +import { CommunityDataService } from '../core/data/community-data.service'; +import { RemoteData } from '../core/data/remote-data'; +import { Community } from '../core/shared/community.model'; +import { DSpaceObject } from '../core/shared/dspace-object.model'; +import { isNotEmpty } from '../shared/empty.util'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { SearchOptions } from './search-options.model'; -import { CommunityDataService } from '../core/data/community-data.service'; -import { isNotEmpty } from '../shared/empty.util'; -import { Community } from '../core/shared/community.model'; +import { SearchResult } from './search-result.model'; +import { SearchService } from './search-service/search.service'; /** * This component renders a simple item page. @@ -21,7 +22,7 @@ import { Community } from '../core/shared/community.model'; selector: 'ds-search-page', styleUrls: ['./search-page.component.scss'], templateUrl: './search-page.component.html', - changeDetection: ChangeDetectionStrategy.OnPush + changeDetection: ChangeDetectionStrategy.OnPush, }) export class SearchPageComponent implements OnInit, OnDestroy { @@ -29,11 +30,11 @@ export class SearchPageComponent implements OnInit, OnDestroy { private scope: string; query: string; - scopeObject: RemoteData; - results: RemoteData>>; + scopeObject: Observable>; + results: Observable>>>; currentParams = {}; searchOptions: SearchOptions; - scopeList: RemoteData; + scopeList: Observable>; constructor( private service: SearchService, diff --git a/src/app/+search-page/search-results/search-results.component.html b/src/app/+search-page/search-results/search-results.component.html index 08e63d598d..808a952d8b 100644 --- a/src/app/+search-page/search-results/search-results.component.html +++ b/src/app/+search-page/search-results/search-results.component.html @@ -1,7 +1,11 @@ -

{{ 'search.results.title' | translate }}

- - \ No newline at end of file +
+

{{ 'search.results.title' | translate }}

+ + +
+ + diff --git a/src/app/+search-page/search-results/search-results.component.ts b/src/app/+search-page/search-results/search-results.component.ts index c645489694..4733699f95 100644 --- a/src/app/+search-page/search-results/search-results.component.ts +++ b/src/app/+search-page/search-results/search-results.component.ts @@ -1,8 +1,9 @@ import { Component, Input } from '@angular/core'; import { RemoteData } from '../../core/data/remote-data'; -import { SearchResult } from '../search-result.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; +import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { SearchOptions } from '../search-options.model'; +import { SearchResult } from '../search-result.model'; /** * This component renders a simple item page. @@ -12,6 +13,10 @@ import { SearchOptions } from '../search-options.model'; @Component({ selector: 'ds-search-results', templateUrl: './search-results.component.html', + animations: [ + fadeIn, + fadeInOut + ] }) export class SearchResultsComponent { @Input() searchResults: RemoteData>>; diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index e2804960ef..27035b92ee 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -80,7 +80,7 @@ export class SearchService { } - search(query: string, scopeId?: string, searchOptions?: SearchOptions): RemoteData>> { + search(query: string, scopeId?: string, searchOptions?: SearchOptions): Observable>>> { let self = `https://dspace7.4science.it/dspace-spring-rest/api/search?query=${query}`; if (hasValue(scopeId)) { self += `&scope=${scopeId}`; @@ -98,8 +98,8 @@ export class SearchService { self += `&sortField=${searchOptions.sort.field}`; } - const errorMessage = Observable.of(undefined); - const statusCode = Observable.of('200'); + const errorMessage = undefined; + const statusCode = '200'; const returningPageInfo = new PageInfo(); if (isNotEmpty(searchOptions)) { @@ -110,19 +110,20 @@ export class SearchService { returningPageInfo.currentPage = 1; } - const itemsRD = this.itemDataService.findAll({ + const itemsObs = this.itemDataService.findAll({ scopeID: scopeId, currentPage: returningPageInfo.currentPage, elementsPerPage: returningPageInfo.elementsPerPage }); - const pageInfo = itemsRD.pageInfo.map((info: PageInfo) => { - const totalElements = info.totalElements > 20 ? 20 : info.totalElements; - return Object.assign({}, info, { totalElements: totalElements }); - }); + return itemsObs + .filter((rd: RemoteData) => rd.hasSucceeded) + .map((rd: RemoteData) => { - const payload = itemsRD.payload.map((items: Item[]) => { - return shuffle(items) + const totalElements = rd.pageInfo.totalElements > 20 ? 20 : rd.pageInfo.totalElements; + const pageInfo = Object.assign({}, rd.pageInfo, { totalElements: totalElements }); + + const payload = shuffle(rd.payload) .map((item: Item, index: number) => { const mockResult: SearchResult = new ItemSearchResult(); mockResult.dspaceObject = item; @@ -132,40 +133,49 @@ export class SearchService { mockResult.hitHighlights = new Array(highlight); return mockResult; }); - }); - return new RemoteData( - Observable.of(self), - itemsRD.isRequestPending, - itemsRD.isResponsePending, - itemsRD.hasSucceeded, - errorMessage, - statusCode, - pageInfo, - payload - ) + return new RemoteData( + self, + rd.isRequestPending, + rd.isResponsePending, + rd.hasSucceeded, + errorMessage, + statusCode, + pageInfo, + payload + ) + }).startWith(new RemoteData( + '', + true, + false, + undefined, + undefined, + undefined, + undefined, + undefined + )); } - getConfig(): RemoteData { - const requestPending = Observable.of(false); - const responsePending = Observable.of(false); - const isSuccessful = Observable.of(true); - const errorMessage = Observable.of(undefined); - const statusCode = Observable.of('200'); - const returningPageInfo = Observable.of(new PageInfo()); - return new RemoteData( - Observable.of('https://dspace7.4science.it/dspace-spring-rest/api/search'), + getConfig(): Observable> { + const requestPending = false; + const responsePending = false; + const isSuccessful = true; + const errorMessage = undefined; + const statusCode = '200'; + const returningPageInfo = new PageInfo(); + return Observable.of(new RemoteData( + 'https://dspace7.4science.it/dspace-spring-rest/api/search', requestPending, responsePending, isSuccessful, errorMessage, statusCode, returningPageInfo, - Observable.of(this.config) - ); + this.config + )); } - getFacetValuesFor(searchFilterConfigName: string): RemoteData { + getFacetValuesFor(searchFilterConfigName: string): Observable> { const values: FacetValue[] = []; for (let i = 0; i < 5; i++) { const value = searchFilterConfigName + ' ' + (i + 1); @@ -175,21 +185,21 @@ export class SearchService { search: 'https://dspace7.4science.it/dspace-spring-rest/api/search?f.' + searchFilterConfigName + '=' + encodeURI(value) }); } - const requestPending = Observable.of(false); - const responsePending = Observable.of(false); - const isSuccessful = Observable.of(true); - const errorMessage = Observable.of(undefined); - const statusCode = Observable.of('200'); - const returningPageInfo = Observable.of(new PageInfo()); - return new RemoteData( - Observable.of('https://dspace7.4science.it/dspace-spring-rest/api/search'), + const requestPending = false; + const responsePending = false; + const isSuccessful = true; + const errorMessage = undefined; + const statusCode = '200'; + const returningPageInfo = new PageInfo(); + return Observable.of(new RemoteData( + 'https://dspace7.4science.it/dspace-spring-rest/api/search', requestPending, responsePending, isSuccessful, errorMessage, statusCode, returningPageInfo, - Observable.of(values) - ); + values + )); } } diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 221c1c37d1..435b44b933 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -4,7 +4,7 @@ import { Inject, ViewEncapsulation, OnInit, - HostListener + HostListener, SimpleChanges, OnChanges } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; @@ -26,7 +26,7 @@ import { GLOBAL_CONFIG, GlobalConfig } from '../config'; changeDetection: ChangeDetectionStrategy.Default, encapsulation: ViewEncapsulation.None }) -export class AppComponent implements OnInit { +export class AppComponent implements OnInit, OnChanges { constructor( @Inject(GLOBAL_CONFIG) public config: GlobalConfig, @@ -71,4 +71,9 @@ export class AppComponent implements OnInit { ); } + ngOnChanges(changes: SimpleChanges): void { + console.log('AppComponent: onchanges called', changes); + } + + } diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index faf9174c21..65b32b3c0b 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -72,30 +72,42 @@ describe('BrowseService', () => { }) ]; - beforeEach(() => { - scheduler = getTestScheduler(); - - responseCache = jasmine.createSpyObj('responseCache', { + function initMockResponseCacheService(isSuccessful: boolean) { + return jasmine.createSpyObj('responseCache', { get: cold('b-', { b: { - response: { browseDefinitions } + response: { + isSuccessful, + browseDefinitions, + } } }) }); + } - requestService = jasmine.createSpyObj('requestService', ['configure']); + function initMockRequestService() { + return jasmine.createSpyObj('requestService', ['configure']); + } - service = new BrowseService( + function initTestService() { + return new BrowseService( responseCache, requestService, envConfig ); + } + + beforeEach(() => { + scheduler = getTestScheduler(); }); describe('getBrowseURLFor', () => { - describe('when getEndpoint fires', () => { + describe('if getEndpoint fires', () => { beforeEach(() => { + responseCache = initMockResponseCacheService(true); + requestService = initMockRequestService(); + service = initTestService(); spyOn(service, 'getEndpoint').and .returnValue(hot('--a-', { a: browsesEndpointURL })); }); @@ -122,27 +134,27 @@ describe('BrowseService', () => { expect(result).toBeObservable(expected); }); - it('should return undefined when the key doesn\'t match', () => { + it('should throw an error when the key doesn\'t match', () => { const metadatumKey = 'dc.title'; // isn't in the definitions const linkName = 'items'; const result = service.getBrowseURLFor(metadatumKey, linkName); - const expected = cold('c---', { c: undefined }); + const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`)); expect(result).toBeObservable(expected); }); - it('should return undefined when the link doesn\'t match', () => { + it('should throw an error when the link doesn\'t match', () => { const metadatumKey = 'dc.date.issued'; const linkName = 'collections'; // isn't in the definitions const result = service.getBrowseURLFor(metadatumKey, linkName); - const expected = cold('c---', { c: undefined }); + const expected = cold('c-#-', { c: undefined }, new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`)); expect(result).toBeObservable(expected); }); - it('should configure a new BrowseEndpointRequest', (done: DoneFn) => { + it('should configure a new BrowseEndpointRequest', () => { const metadatumKey = 'dc.date.issued'; const linkName = 'items'; const expected = new BrowseEndpointRequest(browsesEndpointURL); @@ -150,17 +162,17 @@ describe('BrowseService', () => { scheduler.schedule(() => service.getBrowseURLFor(metadatumKey, linkName).subscribe()); scheduler.flush(); - setTimeout(() => { - expect(requestService.configure).toHaveBeenCalledWith(expected); - done(); - }, 0); + expect(requestService.configure).toHaveBeenCalledWith(expected); }); }); - describe('when getEndpoint doesn\'t fire', () => { - it('should return undefined as long as getEndpoint hasn\'t fired', () => { + describe('if getEndpoint doesn\'t fire', () => { + it('should return undefined', () => { + responseCache = initMockResponseCacheService(true); + requestService = initMockRequestService(); + service = initTestService(); spyOn(service, 'getEndpoint').and .returnValue(hot('----')); @@ -172,5 +184,22 @@ describe('BrowseService', () => { expect(result).toBeObservable(expected); }); }); + + describe('if the browses endpoint can\'t be retrieved', () => { + it('should throw an error', () => { + responseCache = initMockResponseCacheService(false); + requestService = initMockRequestService(); + service = initTestService(); + spyOn(service, 'getEndpoint').and + .returnValue(hot('--a-', { a: browsesEndpointURL })); + + const metadatumKey = 'dc.date.issued'; + const linkName = 'items'; + + const result = service.getBrowseURLFor(metadatumKey, linkName); + const expected = cold('c-#-', { c: undefined }, new Error(`Couldn't retrieve the browses endpoint`)); + expect(result).toBeObservable(expected); + }); + }); }); }); diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index e250b9fa63..6d8d504b82 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -1,15 +1,15 @@ import { Inject, Injectable } from '@angular/core'; -import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { ResponseCacheService } from '../cache/response-cache.service'; -import { RequestService } from '../data/request.service'; -import { GlobalConfig } from '../../../config/global-config.interface'; -import { GLOBAL_CONFIG } from '../../../config'; -import { BrowseEndpointRequest, RestRequest } from '../data/request.models'; -import { ResponseCacheEntry } from '../cache/response-cache.reducer'; -import { BrowseSuccessResponse } from '../cache/response-cache.models'; -import { isNotEmpty } from '../../shared/empty.util'; -import { BrowseDefinition } from '../shared/browse-definition.model'; import { Observable } from 'rxjs/Observable'; +import { GLOBAL_CONFIG } from '../../../config'; +import { GlobalConfig } from '../../../config/global-config.interface'; +import { isEmpty, isNotEmpty } from '../../shared/empty.util'; +import { BrowseSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; +import { ResponseCacheEntry } from '../cache/response-cache.reducer'; +import { ResponseCacheService } from '../cache/response-cache.service'; +import { BrowseEndpointRequest, RestRequest } from '../data/request.models'; +import { RequestService } from '../data/request.service'; +import { BrowseDefinition } from '../shared/browse-definition.model'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; @Injectable() export class BrowseService extends HALEndpointService { @@ -41,23 +41,32 @@ export class BrowseService extends HALEndpointService { .filter((href: string) => isNotEmpty(href)) .distinctUntilChanged() .map((endpointURL: string) => new BrowseEndpointRequest(endpointURL)) - .do((request: RestRequest) => { - setTimeout(() => { - this.requestService.configure(request); - }, 0); - }) - .flatMap((request: RestRequest) => this.responseCache.get(request.href) - .map((entry: ResponseCacheEntry) => entry.response) - .filter((response: BrowseSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.browseDefinitions)) - .map((response: BrowseSuccessResponse) => response.browseDefinitions) - .map((browseDefinitions: BrowseDefinition[]) => browseDefinitions - .find((def: BrowseDefinition) => { - const matchingKeys = def.metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0); - return isNotEmpty(matchingKeys); + .do((request: RestRequest) => this.requestService.configure(request)) + .flatMap((request: RestRequest) => { + const [successResponse, errorResponse] = this.responseCache.get(request.href) + .map((entry: ResponseCacheEntry) => entry.response) + .partition((response: RestResponse) => response.isSuccessful); + + return Observable.merge( + errorResponse.flatMap((response: ErrorResponse) => + Observable.throw(new Error(`Couldn't retrieve the browses endpoint`))), + successResponse + .filter((response: BrowseSuccessResponse) => isNotEmpty(response.browseDefinitions)) + .map((response: BrowseSuccessResponse) => response.browseDefinitions) + .map((browseDefinitions: BrowseDefinition[]) => browseDefinitions + .find((def: BrowseDefinition) => { + const matchingKeys = def.metadataKeys.find((key: string) => searchKeyArray.indexOf(key) >= 0); + return isNotEmpty(matchingKeys); + }) + ).map((def: BrowseDefinition) => { + if (isEmpty(def) || isEmpty(def._links) || isEmpty(def._links[linkName])) { + throw new Error(`A browse endpoint for ${linkName} on ${metadatumKey} isn't configured`); + } else { + return def._links[linkName]; + } }) - ).filter((def: BrowseDefinition) => isNotEmpty(def) && isNotEmpty(def._links)) - .map((def: BrowseDefinition) => def._links[linkName]) - ).startWith(undefined) + ); + }).startWith(undefined) .distinctUntilChanged(); } diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 046250efda..2e3fc01b52 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -28,7 +28,7 @@ export class RemoteDataBuildService { buildSingle( hrefObs: string | Observable, normalizedType: GenericConstructor - ): RemoteData { + ): Observable> { if (typeof hrefObs === 'string') { hrefObs = Observable.of(hrefObs); } @@ -49,46 +49,8 @@ export class RemoteDataBuildService { requestHrefObs.flatMap((requestHref) => this.responseCache.get(requestHref)).filter((entry) => hasValue(entry)) ); - const requestPending = requestObs - .map((entry: RequestEntry) => entry.requestPending) - .startWith(true) - .distinctUntilChanged(); - - const responsePending = requestObs - .map((entry: RequestEntry) => entry.responsePending) - .startWith(false) - .distinctUntilChanged(); - - const isSuccessFul = responseCacheObs - .map((entry: ResponseCacheEntry) => entry.response.isSuccessful) - .startWith(false) - .distinctUntilChanged(); - - const errorMessage = responseCacheObs - .filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful) - .map((entry: ResponseCacheEntry) => (entry.response as ErrorResponse).errorMessage) - .distinctUntilChanged(); - - const statusCode = responseCacheObs - .map((entry: ResponseCacheEntry) => entry.response.statusCode) - .distinctUntilChanged(); - - /* tslint:disable:no-string-literal */ - const pageInfo = responseCacheObs - .filter((entry: ResponseCacheEntry) => hasValue(entry.response) && hasValue(entry.response['pageInfo'])) - .map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).pageInfo) - .map((pInfo: PageInfo) => { - if (isNotEmpty(pageInfo) && pInfo.currentPage >= 0) { - return Object.assign({}, pInfo, {currentPage: pInfo.currentPage + 1}); - } else { - return pInfo; - } - }) - .distinctUntilChanged(); - /* tslint:enable:no-string-literal */ - // always use self link if that is cached, only if it isn't, get it via the response. - const payload = + const payloadObs = Observable.combineLatest( hrefObs.flatMap((href: string) => this.objectCache.getBySelfLink(href, normalizedType)) .startWith(undefined), @@ -114,24 +76,53 @@ export class RemoteDataBuildService { ).filter((normalized) => hasValue(normalized)) .map((normalized: TNormalized) => { return this.build(normalized); - }).distinctUntilChanged(); + }) + .startWith(undefined) + .distinctUntilChanged(); + return this.toRemoteDataObservable(hrefObs, requestObs, responseCacheObs, payloadObs); + } - return new RemoteData( - hrefObs, - requestPending, - responsePending, - isSuccessFul, - errorMessage, - statusCode, - pageInfo, - payload - ); + private toRemoteDataObservable(hrefObs: Observable, requestObs: Observable, responseCacheObs: Observable, payloadObs: Observable) { + return Observable.combineLatest(hrefObs, requestObs, responseCacheObs.startWith(undefined), payloadObs, + (href: string, reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => { + const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true; + const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false; + let isSuccessFul: boolean; + let errorMessage: string; + let statusCode: string; + let pageInfo: PageInfo; + if (hasValue(resEntry) && hasValue(resEntry.response)) { + isSuccessFul = resEntry.response.isSuccessful; + errorMessage = isSuccessFul === false ? (resEntry.response as ErrorResponse).errorMessage : undefined; + statusCode = resEntry.response.statusCode; + + if (hasValue((resEntry.response as DSOSuccessResponse).pageInfo)) { + const resPageInfo = (resEntry.response as DSOSuccessResponse).pageInfo; + if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) { + pageInfo = Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 }); + } else { + pageInfo = resPageInfo; + } + } + } + + return new RemoteData( + href, + requestPending, + responsePending, + isSuccessFul, + errorMessage, + statusCode, + pageInfo, + payload + ); + }); } buildList( hrefObs: string | Observable, normalizedType: GenericConstructor - ): RemoteData { + ): Observable> { if (typeof hrefObs === 'string') { hrefObs = Observable.of(hrefObs); } @@ -141,38 +132,7 @@ export class RemoteDataBuildService { const responseCacheObs = hrefObs.flatMap((href: string) => this.responseCache.get(href)) .filter((entry) => hasValue(entry)); - const requestPending = requestObs - .map((entry: RequestEntry) => entry.requestPending) - .startWith(true) - .distinctUntilChanged(); - - const responsePending = requestObs - .map((entry: RequestEntry) => entry.responsePending) - .startWith(false) - .distinctUntilChanged(); - - const isSuccessFul = responseCacheObs - .map((entry: ResponseCacheEntry) => entry.response.isSuccessful) - .startWith(false) - .distinctUntilChanged(); - - const errorMessage = responseCacheObs - .filter((entry: ResponseCacheEntry) => !entry.response.isSuccessful) - .map((entry: ResponseCacheEntry) => (entry.response as ErrorResponse).errorMessage) - .distinctUntilChanged(); - - const statusCode = responseCacheObs - .map((entry: ResponseCacheEntry) => entry.response.statusCode) - .distinctUntilChanged(); - - /* tslint:disable:no-string-literal */ - const pageInfo = responseCacheObs - .filter((entry: ResponseCacheEntry) => hasValue(entry.response) && hasValue(entry.response['pageInfo'])) - .map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).pageInfo) - .distinctUntilChanged(); - /* tslint:enable:no-string-literal */ - - const payload = responseCacheObs + const payloadObs = responseCacheObs .filter((entry: ResponseCacheEntry) => entry.response.isSuccessful) .map((entry: ResponseCacheEntry) => (entry.response as DSOSuccessResponse).resourceSelfLinks) .flatMap((resourceUUIDs: string[]) => { @@ -183,18 +143,10 @@ export class RemoteDataBuildService { }); }); }) + .startWith([]) .distinctUntilChanged(); - return new RemoteData( - hrefObs, - requestPending, - responsePending, - isSuccessFul, - errorMessage, - statusCode, - pageInfo, - payload - ); + return this.toRemoteDataObservable(hrefObs, requestObs, responseCacheObs, payloadObs); } build(normalized: TNormalized): TDomain { @@ -207,13 +159,9 @@ export class RemoteDataBuildService { const { resourceType, isList } = getRelationMetadata(normalized, relationship); const resourceConstructor = NormalizedObjectFactory.getConstructor(resourceType); if (Array.isArray(normalized[relationship])) { - // without the setTimeout, the actions inside requestService.configure - // are dispatched, but sometimes don't arrive. I'm unsure why atm. - setTimeout(() => { - normalized[relationship].forEach((href: string) => { - this.requestService.configure(new RestRequest(href)) - }); - }, 0); + normalized[relationship].forEach((href: string) => { + this.requestService.configure(new RestRequest(href)) + }); const rdArr = []; normalized[relationship].forEach((href: string) => { @@ -226,11 +174,7 @@ export class RemoteDataBuildService { links[relationship] = rdArr[0]; } } else { - // without the setTimeout, the actions inside requestService.configure - // are dispatched, but sometimes don't arrive. I'm unsure why atm. - setTimeout(() => { - this.requestService.configure(new RestRequest(normalized[relationship])); - }, 0); + this.requestService.configure(new RestRequest(normalized[relationship])); // The rest API can return a single URL to represent a list of resources (e.g. /items/:id/bitstreams) // in that case only 1 href will be stored in the normalized obj (so the isArray above fails), @@ -248,63 +192,55 @@ export class RemoteDataBuildService { return Object.assign(new domainModel(), normalized, links); } - aggregate(input: Array>): RemoteData { - const requestPending = Observable.combineLatest( - ...input.map((rd) => rd.isRequestPending), - ).map((...pendingArray) => pendingArray.every((e) => e === true)) - .distinctUntilChanged(); + aggregate(input: Array>>): Observable> { + return Observable.combineLatest( + ...input, + (...arr: Array>) => { + const requestPending: boolean = arr + .map((d: RemoteData) => d.isRequestPending) + .every((b: boolean) => b === true); - const responsePending = Observable.combineLatest( - ...input.map((rd) => rd.isResponsePending), - ).map((...pendingArray) => pendingArray.every((e) => e === true)) - .distinctUntilChanged(); + const responsePending: boolean = arr + .map((d: RemoteData) => d.isResponsePending) + .every((b: boolean) => b === true); - const isSuccessFul = Observable.combineLatest( - ...input.map((rd) => rd.hasSucceeded), - ).map((...successArray) => successArray.every((e) => e === true)) - .distinctUntilChanged(); + const isSuccessFul: boolean = arr + .map((d: RemoteData) => d.hasSucceeded) + .every((b: boolean) => b === true); - const errorMessage = Observable.combineLatest( - ...input.map((rd) => rd.errorMessage), - ).map((...errors) => errors - .map((e, idx) => { - if (hasValue(e)) { - return `[${idx}]: ${e}`; - } + const errorMessage: string = arr + .map((d: RemoteData) => d.errorMessage) + .map((e: string, idx: number) => { + if (hasValue(e)) { + return `[${idx}]: ${e}`; + } + }).filter((e: string) => hasValue(e)) + .join(', '); + + const statusCode: string = arr + .map((d: RemoteData) => d.statusCode) + .map((c: string, idx: number) => { + if (hasValue(c)) { + return `[${idx}]: ${c}`; + } + }).filter((c: string) => hasValue(c)) + .join(', '); + + const pageInfo = undefined; + + const payload: T[] = arr.map((d: RemoteData) => d.payload); + + return new RemoteData( + `dspace-angular://aggregated/object/${new Date().getTime()}`, + requestPending, + responsePending, + isSuccessFul, + errorMessage, + statusCode, + pageInfo, + payload + ); }) - .filter((e) => hasValue(e)) - .join(', ') - ); - - const statusCode = Observable.combineLatest( - ...input.map((rd) => rd.statusCode), - ).map((...statusCodes) => statusCodes - .map((code, idx) => { - if (hasValue(code)) { - return `[${idx}]: ${code}`; - } - }) - .filter((c) => hasValue(c)) - .join(', ') - ); - - const pageInfo = Observable.of(undefined); - - const payload = Observable.combineLatest(...input.map((rd) => rd.payload)) as Observable; - - return new RemoteData( - // This is an aggregated object, it doesn't necessarily correspond - // to a single REST endpoint, so instead of a self link, use the - // current time in ms for a somewhat unique id - Observable.of(`${new Date().getTime()}`), - requestPending, - responsePending, - isSuccessFul, - errorMessage, - statusCode, - pageInfo, - payload - ); } } diff --git a/src/app/core/cache/response-cache.service.ts b/src/app/core/cache/response-cache.service.ts index eac76c519e..d734940496 100644 --- a/src/app/core/cache/response-cache.service.ts +++ b/src/app/core/cache/response-cache.service.ts @@ -4,7 +4,7 @@ import { MemoizedSelector, Store } from '@ngrx/store'; import { Observable } from 'rxjs/Observable'; import { ResponseCacheEntry } from './response-cache.reducer'; -import { hasNoValue } from '../../shared/empty.util'; +import { hasNoValue, hasValue } from '../../shared/empty.util'; import { ResponseCacheRemoveAction, ResponseCacheAddAction } from './response-cache.actions'; import { RestResponse } from './response-cache.models'; import { CoreState } from '../core.reducers'; diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index f96ace4452..be7949826f 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -53,6 +53,7 @@ describe('ComColDataService', () => { const communitiesEndpoint = 'https://rest.api/core/communities'; const communityEndpoint = `${communitiesEndpoint}/${scopeID}`; const scopedEndpoint = `${communityEndpoint}/${LINK_NAME}`; + const serviceEndpoint = `https://rest.api/core/${LINK_NAME}`; function initMockCommunityDataService(): CommunityDataService { return jasmine.createSpyObj('responseCache', { @@ -102,7 +103,7 @@ describe('ComColDataService', () => { scheduler = getTestScheduler(); }); - it('should configure a new FindByIDRequest for the scope Community', (done: DoneFn) => { + it('should configure a new FindByIDRequest for the scope Community', () => { cds = initMockCommunityDataService(); requestService = initMockRequestService(); objectCache = initMockObjectCacheService(); @@ -114,10 +115,7 @@ describe('ComColDataService', () => { scheduler.schedule(() => service.getScopedEndpoint(scopeID).subscribe()); scheduler.flush(); - setTimeout(() => { - expect(requestService.configure).toHaveBeenCalledWith(expected); - done(); - }, 0); + expect(requestService.configure).toHaveBeenCalledWith(expected); }); describe('if the scope Community can be found', () => { @@ -159,5 +157,25 @@ describe('ComColDataService', () => { expect(result).toBeObservable(expected); }); }); + + describe('if the scope is not specified', () => { + beforeEach(() => { + cds = initMockCommunityDataService(); + requestService = initMockRequestService(); + objectCache = initMockObjectCacheService(); + responseCache = initMockResponceCacheService(true); + service = initTestService(); + }); + + it('should return this.getEndpoint()', () => { + spyOn(service, 'getEndpoint').and.returnValue(cold('--e-', { e: serviceEndpoint })) + + const result = service.getScopedEndpoint(undefined); + const expected = cold('--f-', { f: serviceEndpoint }); + + expect(result).toBeObservable(expected); + }); + }); + }); }); diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 3917499914..17d2fb313c 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -1,14 +1,14 @@ import { Observable } from 'rxjs/Observable'; -import { isNotEmpty } from '../../shared/empty.util'; +import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { CacheableObject } from '../cache/object-cache.reducer'; -import { DSOSuccessResponse, RestResponse } from '../cache/response-cache.models'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { CommunityDataService } from './community-data.service'; import { DataService } from './data.service'; import { FindByIDRequest } from './request.models'; -import { ObjectCacheService } from '../cache/object-cache.service'; export abstract class ComColDataService extends DataService { protected abstract cds: CommunityDataService; @@ -25,30 +25,32 @@ export abstract class ComColDataService containing the scoped URL */ public getScopedEndpoint(scopeID: string): Observable { - const scopeCommunityHrefObs = this.cds.getEndpoint() - .flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID)) - .filter((href: string) => isNotEmpty(href)) - .take(1) - .do((href: string) => { - const request = new FindByIDRequest(href, scopeID); - setTimeout(() => { + if (isEmpty(scopeID)) { + return this.getEndpoint(); + } else { + const scopeCommunityHrefObs = this.cds.getEndpoint() + .flatMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, scopeID)) + .filter((href: string) => isNotEmpty(href)) + .take(1) + .do((href: string) => { + const request = new FindByIDRequest(href, scopeID); this.requestService.configure(request); - }, 0); - }); + }); - const [successResponse, errorResponse] = scopeCommunityHrefObs - .flatMap((href: string) => this.responseCache.get(href)) - .map((entry: ResponseCacheEntry) => entry.response) - .share() - .partition((response: RestResponse) => response.isSuccessful); + const [successResponse, errorResponse] = scopeCommunityHrefObs + .flatMap((href: string) => this.responseCache.get(href)) + .map((entry: ResponseCacheEntry) => entry.response) + .share() + .partition((response: RestResponse) => response.isSuccessful); - return Observable.merge( - errorResponse.flatMap((response: DSOSuccessResponse) => - Observable.throw(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))), - successResponse - .flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID, NormalizedCommunity)) - .map((nc: NormalizedCommunity) => nc._links[this.linkName]) - .filter((href) => isNotEmpty(href)) - ).distinctUntilChanged(); + return Observable.merge( + errorResponse.flatMap((response: ErrorResponse) => + Observable.throw(new Error(`The Community with scope ${scopeID} couldn't be retrieved`))), + successResponse + .flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(scopeID, NormalizedCommunity)) + .map((nc: NormalizedCommunity) => nc._links[this.linkName]) + .filter((href) => isNotEmpty(href)) + ).distinctUntilChanged(); + } } } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index ed9d84f04a..b9a729e9d4 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -11,6 +11,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RemoteData } from './remote-data'; import { FindAllOptions, FindAllRequest, FindByIDRequest, RestRequest } from './request.models'; import { RequestService } from './request.service'; +import { URLCombiner } from '../url-combiner/url-combiner'; export abstract class DataService extends HALEndpointService { protected abstract responseCache: ResponseCacheService; @@ -29,11 +30,12 @@ export abstract class DataService public abstract getScopedEndpoint(scope: string): Observable protected getFindAllHref(endpoint, options: FindAllOptions = {}): Observable { - let result; + let result: Observable; const args = []; if (hasValue(options.scopeID)) { - result = this.getScopedEndpoint(options.scopeID); + result = this.getScopedEndpoint(options.scopeID).distinctUntilChanged(); + // result.take(1).subscribe((r) => console.log('result', r)); } else { result = Observable.of(endpoint); } @@ -56,22 +58,22 @@ export abstract class DataService } if (isNotEmpty(args)) { - return result.map((href: string) => `${href}?${args.join('&')}`); + return result.map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString()); } else { return result; } } - findAll(options: FindAllOptions = {}): RemoteData { - const hrefObs = this.getEndpoint() + findAll(options: FindAllOptions = {}): Observable> { + const hrefObs = this.getEndpoint().filter((href: string) => isNotEmpty(href)) .flatMap((endpoint: string) => this.getFindAllHref(endpoint, options)); hrefObs + .filter((href: string) => hasValue(href)) + .take(1) .subscribe((href: string) => { const request = new FindAllRequest(href, options); - setTimeout(() => { - this.requestService.configure(request); - }, 0); + this.requestService.configure(request); }); return this.rdbService.buildList(hrefObs, this.normalizedResourceType); @@ -81,27 +83,24 @@ export abstract class DataService return `${endpoint}/${resourceID}`; } - findById(id: string): RemoteData { + findById(id: string): Observable> { const hrefObs = this.getEndpoint() .map((endpoint: string) => this.getFindByIDHref(endpoint, id)); hrefObs + .filter((href: string) => hasValue(href)) + .take(1) .subscribe((href: string) => { const request = new FindByIDRequest(href, id); - setTimeout(() => { - this.requestService.configure(request); - }, 0); + this.requestService.configure(request); }); return this.rdbService.buildSingle(hrefObs, this.normalizedResourceType); } - findByHref(href: string): RemoteData { - setTimeout(() => { - this.requestService.configure(new RestRequest(href)); - }, 0); + findByHref(href: string): Observable> { + this.requestService.configure(new RestRequest(href)); return this.rdbService.buildSingle(href, this.normalizedResourceType); - // return this.rdbService.buildSingle(href)); } } diff --git a/src/app/core/data/remote-data.ts b/src/app/core/data/remote-data.ts index e3c7a17ef8..a832759632 100644 --- a/src/app/core/data/remote-data.ts +++ b/src/app/core/data/remote-data.ts @@ -14,57 +14,48 @@ export enum RemoteDataState { */ export class RemoteData { constructor( - public self: Observable, - private requestPending: Observable, - private responsePending: Observable, - private isSuccessFul: Observable, - public errorMessage: Observable, - public statusCode: Observable, - public pageInfo: Observable, - public payload: Observable + public self: string, + private requestPending: boolean, + private responsePending: boolean, + private isSuccessFul: boolean, + public errorMessage: string, + public statusCode: string, + public pageInfo: PageInfo, + public payload: T ) { } - get state(): Observable { - return Observable.combineLatest( - this.requestPending, - this.responsePending, - this.isSuccessFul, - (requestPending, responsePending, isSuccessFul) => { - if (requestPending) { - return RemoteDataState.RequestPending - } else if (responsePending) { - return RemoteDataState.ResponsePending - } else if (!isSuccessFul) { - return RemoteDataState.Failed - } else { - return RemoteDataState.Success - } - } - ).distinctUntilChanged(); + get state(): RemoteDataState { + if (this.isSuccessFul === true) { + return RemoteDataState.Success + } else if (this.isSuccessFul === false) { + return RemoteDataState.Failed + } else if (this.requestPending === true) { + return RemoteDataState.RequestPending + } else if (this.responsePending === true || this.isSuccessFul === undefined) { + return RemoteDataState.ResponsePending + } } - get isRequestPending(): Observable { - return this.state.map((state) => state === RemoteDataState.RequestPending).distinctUntilChanged(); + get isRequestPending(): boolean { + return this.state === RemoteDataState.RequestPending; } - get isResponsePending(): Observable { - return this.state.map((state) => state === RemoteDataState.ResponsePending).distinctUntilChanged(); + get isResponsePending(): boolean { + return this.state === RemoteDataState.ResponsePending; } - get isLoading(): Observable { - return this.state.map((state) => { - return state === RemoteDataState.RequestPending - || state === RemoteDataState.ResponsePending - }).distinctUntilChanged(); + get isLoading(): boolean { + return this.state === RemoteDataState.RequestPending + || this.state === RemoteDataState.ResponsePending; } - get hasFailed(): Observable { - return this.state.map((state) => state === RemoteDataState.Failed).distinctUntilChanged(); + get hasFailed(): boolean { + return this.state === RemoteDataState.Failed; } - get hasSucceeded(): Observable { - return this.state.map((state) => state === RemoteDataState.Success).distinctUntilChanged(); + get hasSucceeded(): boolean { + return this.state === RemoteDataState.Success; } } diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 4231f9efbb..6109e7b45c 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -22,6 +22,7 @@ function entryFromHrefSelector(href: string): MemoizedSelector hasValue(re)) + .take(1) + .subscribe((re: RequestEntry) => { + this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((pendingHref: string) => pendingHref !== href) + }); + } } diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index 88fe174ef6..4c8775fcfb 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -31,6 +31,7 @@ import { Item } from '../../core/shared/item.model'; import { MockItem } from '../../shared/mocks/mock-item'; import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; import { BrowseService } from '../browse/browse.service'; +import { PageInfo } from '../shared/page-info.model'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -175,33 +176,17 @@ describe('MetadataService', () => { expect(tagStore.get('description')[0].content).toEqual('This is a dummy item component for testing!'); })); - const mockRemoteData = (mockItem: Item): RemoteData => { - return new RemoteData( - Observable.create((observer) => { - observer.next(''); - }), - Observable.create((observer) => { - observer.next(false); - }), - Observable.create((observer) => { - observer.next(false); - }), - Observable.create((observer) => { - observer.next(true); - }), - Observable.create((observer) => { - observer.next(''); - }), - Observable.create((observer) => { - observer.next(200); - }), - Observable.create((observer) => { - observer.next({}); - }), - Observable.create((observer) => { - observer.next(MockItem); - }) - ); + const mockRemoteData = (mockItem: Item): Observable> => { + return Observable.of(new RemoteData( + '', + false, + false, + true, + '', + '200', + {} as PageInfo, + MockItem + )); } const mockType = (mockItem: Item, type: string): Item => { diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 32b002e721..39fb454ac5 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -25,6 +25,8 @@ import { Item } from '../shared/item.model'; import { Metadatum } from '../shared/metadatum.model'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; +import { BitstreamFormat } from '../shared/bitstream-format.model'; +import { hasValue } from '../../shared/empty.util'; @Injectable() export class MetadataService { @@ -64,13 +66,16 @@ export class MetadataService { }); } - public processRemoteData(remoteData: RemoteData): void { - remoteData.payload.take(1).subscribe((dspaceObject: DSpaceObject) => { - if (!this.initialized) { - this.initialize(dspaceObject); - } - this.currentObject.next(dspaceObject); - }); + public processRemoteData(remoteData: Observable>): void { + remoteData.map((rd: RemoteData) => rd.payload) + .filter((co: CacheableObject) => hasValue(co)) + .take(1) + .subscribe((dspaceObject: DSpaceObject) => { + if (!this.initialized) { + this.initialize(dspaceObject); + } + this.currentObject.next(dspaceObject); + }); } private processRouteChange(routeInfo: any): void { @@ -268,11 +273,14 @@ export class MetadataService { // taking only two, fist one is empty array item.getFiles().take(2).subscribe((bitstreams: Bitstream[]) => { for (const bitstream of bitstreams) { - bitstream.format.payload.take(1).subscribe((format) => { - if (format.mimetype === 'application/pdf') { - this.addMetaTag('citation_pdf_url', bitstream.content); - } - }); + bitstream.format.take(1) + .map((rd: RemoteData) => rd.payload) + .filter((format: BitstreamFormat) => hasValue(format)) + .subscribe((format: BitstreamFormat) => { + if (format.mimetype === 'application/pdf') { + this.addMetaTag('citation_pdf_url', bitstream.content); + } + }); } }); } diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index 0b77a7b032..511c2c5cd2 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -2,6 +2,7 @@ import { DSpaceObject } from './dspace-object.model'; import { RemoteData } from '../data/remote-data'; import { Item } from './item.model'; import { BitstreamFormat } from './bitstream-format.model'; +import { Observable } from 'rxjs/Observable'; export class Bitstream extends DSpaceObject { @@ -23,17 +24,17 @@ export class Bitstream extends DSpaceObject { /** * An array of Bitstream Format of this Bitstream */ - format: RemoteData; + format: Observable>; /** * An array of Items that are direct parents of this Bitstream */ - parents: RemoteData; + parents: Observable>; /** * The Bundle that owns this Bitstream */ - owner: RemoteData; + owner: Observable>; /** * The URL to retrieve this Bitstream's file diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index 798b7c402c..9a8afb2661 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -2,23 +2,24 @@ import { DSpaceObject } from './dspace-object.model'; import { Bitstream } from './bitstream.model'; import { Item } from './item.model'; import { RemoteData } from '../data/remote-data'; +import { Observable } from 'rxjs/Observable'; export class Bundle extends DSpaceObject { /** * The primary bitstream of this Bundle */ - primaryBitstream: RemoteData; + primaryBitstream: Observable>; /** * An array of Items that are direct parents of this Bundle */ - parents: RemoteData; + parents: Observable>; /** * The Item that owns this Bundle */ - owner: RemoteData; + owner: Observable>; - bitstreams: RemoteData + bitstreams: Observable> } diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index 207837ef04..b2f8d90a65 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -2,6 +2,7 @@ import { DSpaceObject } from './dspace-object.model'; import { Bitstream } from './bitstream.model'; import { Item } from './item.model'; import { RemoteData } from '../data/remote-data'; +import { Observable } from 'rxjs/Observable'; export class Collection extends DSpaceObject { @@ -53,18 +54,18 @@ export class Collection extends DSpaceObject { /** * The Bitstream that represents the logo of this Collection */ - logo: RemoteData; + logo: Observable>; /** * An array of Collections that are direct parents of this Collection */ - parents: RemoteData; + parents: Observable>; /** * The Collection that owns this Collection */ - owner: RemoteData; + owner: Observable>; - items: RemoteData; + items: Observable>; } diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index afe9fd734e..c34666b0f0 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -2,6 +2,7 @@ import { DSpaceObject } from './dspace-object.model'; import { Bitstream } from './bitstream.model'; import { Collection } from './collection.model'; import { RemoteData } from '../data/remote-data'; +import { Observable } from 'rxjs/Observable'; export class Community extends DSpaceObject { @@ -45,18 +46,18 @@ export class Community extends DSpaceObject { /** * The Bitstream that represents the logo of this Community */ - logo: RemoteData; + logo: Observable>; /** * An array of Communities that are direct parents of this Community */ - parents: RemoteData; + parents: Observable>; /** * The Community that owns this Community */ - owner: RemoteData; + owner: Observable>; - collections: RemoteData; + collections: Observable>; } diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 8584c179dc..572efecada 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -4,6 +4,7 @@ import { CacheableObject } from '../cache/object-cache.reducer'; import { RemoteData } from '../data/remote-data'; import { ResourceType } from './resource-type'; import { ListableObject } from '../../object-list/listable-object/listable-object.model'; +import { Observable } from 'rxjs/Observable'; /** * An abstract model class for a DSpaceObject. @@ -40,12 +41,12 @@ export abstract class DSpaceObject implements CacheableObject, ListableObject { /** * An array of DSpaceObjects that are direct parents of this DSpaceObject */ - parents: RemoteData; + parents: Observable>; /** * The DSpaceObject that owns this DSpaceObject */ - owner: RemoteData; + owner: Observable>; /** * Find a metadata field by key and language diff --git a/src/app/core/shared/hal-endpoint.service.spec.ts b/src/app/core/shared/hal-endpoint.service.spec.ts index 8b38d39676..f7adc1eccf 100644 --- a/src/app/core/shared/hal-endpoint.service.spec.ts +++ b/src/app/core/shared/hal-endpoint.service.spec.ts @@ -51,13 +51,10 @@ describe('HALEndpointService', () => { ); }); - it('should configure a new RootEndpointRequest', (done) => { + it('should configure a new RootEndpointRequest', () => { (service as any).getEndpointMap(); const expected = new RootEndpointRequest(envConfig); - setTimeout(() => { - expect(requestService.configure).toHaveBeenCalledWith(expected); - done(); - }, 0); + expect(requestService.configure).toHaveBeenCalledWith(expected); }); it('should return an Observable of the endpoint map', () => { diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index bb5ec0134e..61f528d714 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -15,10 +15,9 @@ export abstract class HALEndpointService { protected getEndpointMap(): Observable { const request = new RootEndpointRequest(this.EnvConfig); - setTimeout(() => { - this.requestService.configure(request); - }, 0); + this.requestService.configure(request); return this.responseCache.get(request.href) + // .do((entry: ResponseCacheEntry) => console.log('entry.response', entry.response)) .map((entry: ResponseCacheEntry) => entry.response) .filter((response: RootSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.endpointMap)) .map((response: RootSuccessResponse) => response.endpointMap) diff --git a/src/app/core/shared/item.model.spec.ts b/src/app/core/shared/item.model.spec.ts index 0c89828c9e..1e962f7038 100644 --- a/src/app/core/shared/item.model.spec.ts +++ b/src/app/core/shared/item.model.spec.ts @@ -103,23 +103,15 @@ describe('Item', () => { }); function createRemoteDataObject(object: any) { - const self = Observable.of(''); - const requestPending = Observable.of(false); - const responsePending = Observable.of(false); - const isSuccessful = Observable.of(true); - const errorMessage = Observable.of(undefined); - const statusCode = Observable.of('200'); - const pageInfo = Observable.of(new PageInfo()); - const payload = Observable.of(object); - return new RemoteData( - self, - requestPending, - responsePending, - isSuccessful, - errorMessage, - statusCode, - pageInfo, - payload - ); + return Observable.of(new RemoteData( + '', + false, + false, + true, + undefined, + '200', + new PageInfo(), + object + )); } diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index 16cce9b610..dd60ad9b01 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -4,7 +4,7 @@ import { DSpaceObject } from './dspace-object.model'; import { Collection } from './collection.model'; import { RemoteData } from '../data/remote-data'; import { Bitstream } from './bitstream.model'; -import { isNotEmpty } from '../../shared/empty.util'; +import { hasValue, isNotEmpty } from '../../shared/empty.util'; export class Item extends DSpaceObject { @@ -36,18 +36,18 @@ export class Item extends DSpaceObject { /** * An array of Collections that are direct parents of this Item */ - parents: RemoteData; + parents: Observable>; /** * The Collection that owns this Item */ - owningCollection: RemoteData; + owningCollection: Observable>; - get owner(): RemoteData { + get owner(): Observable> { return this.owningCollection; } - bitstreams: RemoteData; + bitstreams: Observable>; /** * Retrieves the thumbnail of this item @@ -87,9 +87,14 @@ export class Item extends DSpaceObject { * @returns {Observable} the bitstreams with the given bundleName */ getBitstreamsByBundleName(bundleName: string): Observable { - return this.bitstreams.payload.startWith([]) + return this.bitstreams + .map((rd: RemoteData) => rd.payload) + .filter((bitstreams: Bitstream[]) => hasValue(bitstreams)) + .startWith([]) .map((bitstreams) => { - return bitstreams.filter((bitstream) => bitstream.bundleName === bundleName) + return bitstreams + .filter((bitstream) => hasValue(bitstream)) + .filter((bitstream) => bitstream.bundleName === bundleName) }); } diff --git a/src/app/object-list/object-list.component.html b/src/app/object-list/object-list.component.html index 84c800fef4..71f0b0e26c 100644 --- a/src/app/object-list/object-list.component.html +++ b/src/app/object-list/object-list.component.html @@ -1,7 +1,7 @@ -