Merge branch 'response-cache-refactoring' into w2p-54472_Create-community-and-collection-pages

Conflicts:
	src/app/core/auth/auth-response-parsing.service.ts
	src/app/core/auth/auth.service.ts
	src/app/core/auth/models/auth-status.model.ts
	src/app/core/auth/models/normalized-auth-status.model.ts
	src/app/core/auth/server-auth.service.ts
	src/app/core/cache/builders/remote-data-build.service.ts
	src/app/core/data/collection-data.service.ts
	src/app/core/data/comcol-data.service.spec.ts
	src/app/core/data/comcol-data.service.ts
	src/app/core/data/community-data.service.ts
	src/app/core/data/data.service.spec.ts
	src/app/core/data/data.service.ts
	src/app/core/data/dspace-object-data.service.ts
	src/app/core/data/item-data.service.ts
	src/app/core/data/request.models.ts
	src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts
	src/app/core/shared/hal-endpoint.service.ts
	src/app/core/shared/operators.ts
	src/app/shared/testing/auth-request-service-stub.ts
	src/app/shared/testing/auth-service-stub.ts
This commit is contained in:
Kristof De Langhe
2018-10-29 10:26:44 +01:00
324 changed files with 9452 additions and 7948 deletions

13
angular.json Normal file
View File

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

View File

@@ -18,9 +18,17 @@ module.exports = {
// Caching settings
cache: {
// NOTE: how long should objects be cached for by default
msToLive: 15 * 60 * 1000, // 15 minutes
msToLive: {
default: 15 * 60 * 1000, // 15 minutes
exportToZip: 5 * 1000 // 5 seconds
},
// msToLive: 1000, // 15 minutes
control: 'max-age=60' // revalidate browser
control: 'max-age=60', // revalidate browser
autoSync: {
defaultTime: 0,
maxBufferSize: 100,
timePerMethod: {'PATCH': 3} //time in seconds
}
},
// Form settings
form: {

View File

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

277
resources/i18n/cs.json Normal file
View File

@@ -0,0 +1,277 @@
{
"footer": {
"copyright": "copyright © 2002-{{ year }}",
"link.dspace": "software DSpace",
"link.duraspace": "DuraSpace"
},
"collection": {
"page": {
"news": "Novinky",
"license": "Licence",
"browse": {
"recent": {
"head": "Poslední příspěvky"
}
}
}
},
"community": {
"page": {
"news": "Novinky",
"license": "Licence"
},
"sub-collection-list": {
"head": "Kolekce v této komunitě"
}
},
"item": {
"page": {
"author": "Autor",
"abstract": "Abstract",
"date": "Datum",
"uri": "URI",
"files": "Soubory",
"collections": "Kolekce",
"filesection": {
"download": "Stáhnout",
"name": "Název:",
"format": "Formát:",
"size": "Velikost:",
"description": "Popis:"
},
"link": {
"simple": "Minimální záznam",
"full": "Úplný záznam"
}
}
},
"nav": {
"home": "Domů",
"login": "Přihlásit se",
"logout": "Odhlásit se"
},
"pagination": {
"results-per-page": "Výsledků na stránku",
"sort-direction": "Seřazení",
"showing": {
"label": "Zobrazují se záznamy ",
"detail": "{{ range }} z {{ total }}"
}
},
"sorting": {
"score": {
"DESC": "Relevance"
},
"dc.title": {
"ASC": "Název vzestupně",
"DESC": "Název sestupně"
}
},
"title": "DSpace",
"404": {
"help": "Nepodařilo se najít stránku, kterou hledáte. Je možné, že stránka byla přesunuta nebo smazána. Pomocí tlačítka níže můžete přejít na domovskou stránku. ",
"page-not-found": "stránka nenalezena",
"link": {
"home-page": "Přejít na domovskou stránku"
}
},
"home": {
"title": "DSpace Angular :: Domů",
"description": "",
"top-level-communities": {
"head": "Komunity v DSpace",
"help": "Vybráním komunity můžete prohlížet její kolekce."
}
},
"search": {
"title": "DSpace Angular :: Hledat",
"description": "",
"form": {
"search": "Hledat",
"search_dspace": "Hledat v DSpace"
},
"results": {
"head": "Výsledky hledání",
"no-results": "Nebyli nalezeny žádné výsledky"
},
"sidebar": {
"close": "Zpět na výsledky",
"open": "Vyhledávací nástroje",
"results": "výsledky",
"filters": {
"title": "Filtry"
},
"settings": {
"title": "Nastavení",
"sort-by": "Řadit dle",
"rpp": "Výsledků na stránku"
}
},
"view-switch": {
"show-list": "Zobrazit seznam",
"show-grid": "Zobrazit mřížku"
},
"filters": {
"head": "Filtry",
"reset": "Obnovit filtry",
"applied": {
"f.author": "Autor",
"f.dateIssued.min": "Od data",
"f.dateIssued.max": "Do data",
"f.subject": "Předmět",
"f.has_content_in_original_bundle": "Má soubory"
},
"filter": {
"show-more": "Zobrazit více",
"show-less": "Sbalit",
"author": {
"placeholder": "Jméno autora",
"head": "Autor"
},
"scope": {
"placeholder": "Filtr rozsahu",
"head": "Rozsah"
},
"subject": {
"placeholder": "Předmět",
"head": "Předmět"
},
"dateIssued": {
"max": {
"placeholder": "Datum od"
},
"min": {
"placeholder": "Datum do"
},
"head": "Datum"
},
"has_content_in_original_bundle": {
"head": "Má soubory"
}
}
}
},
"browse": {
"title": "Prohlížíte {{ collection }} dle {{ field }} {{ value }}"
},
"admin": {
"registries": {
"metadata": {
"title": "DSpace Angular :: Registr metadat",
"head": "Registr metadat",
"description": "Registr metadat je seznam všech metadatových polí dostupných v repozitáři. Tyto pole mohou být rozdělena do více schémat. DSpace však vyžaduje použití schématu kvalifikový Dublin Core.",
"schemas": {
"table": {
"id": "ID",
"namespace": "Jmenný prostor",
"name": "Název"
},
"no-items": "Žádná schémata metadat."
}
},
"schema": {
"title": "DSpace Angular :: Registr schémat metadat",
"head": "Metadata Schema",
"description": "Toto je schéma metadat pro „{{namespace}}“.",
"fields": {
"head": "Pole schématu metadat",
"table": {
"field": "Pole",
"scopenote": "Poznámka o rozsahu"
},
"no-items": "Žádná metadatová pole."
}
},
"bitstream-formats": {
"title": "DSpace Angular :: Registr formátů souborů",
"head": "Registr formátů souborů",
"description": "Tento seznam formátů souborů poskytuje informace o známých formátech a o úrovni jejich podpory.",
"formats": {
"table": {
"name": "Název",
"mimetype": "Typ MIME",
"supportLevel": {
"head": "Úroveň podpory",
"0": "Neznámá",
"1": "Známá",
"2": "Podpora"
},
"internal": "interní"
},
"no-items": "Žádné formáty souborů."
}
}
}
},
"loading": {
"default": "Načítá se...",
"top-level-communities": "Načítají se komunity nejvyšší úrovně...",
"community": "Načítá se komunita...",
"collection": "Načítá se kolekce...",
"sub-collections": "Načítají se subkolekce...",
"recent-submissions": "Načítají se poslední příspěvky...",
"item": "Načítá se záznam...",
"objects": "Načítá se...",
"search-results": "Načítají se výsledky hledání...",
"browse-by": "Načítají se záznamy..."
},
"error": {
"default": "Chyba",
"top-level-communities": "Chyba během stahování komunit nejvyšší úrovně",
"community": "Chyba během stahování komunity",
"collection": "Chyba během stahování kolekce",
"sub-collections": "Chyba během stahování subkolekcí",
"recent-submissions": "Chyba během stahování posledních příspěvků",
"item": "Chyba během stahování záznamu",
"objects": "Chyba během stahování objektů",
"search-results": "Chyba během stahování výsledků hledání",
"browse-by": "Chyba během stahování záznamů",
"validation": {
"pattern": "Tento vstup je omezen dle vzoru: {{ pattern }}.",
"license": {
"notgranted": "Pro dokončení zaslání Musíte udělit licenci. Pokud v tuto chvíli tuto licenci nemůžete udělit, můžete svou práci uložit a později se k svému příspěveku vrátit nebo jej smazat."
}
}
},
"form": {
"submit": "Odeslat",
"cancel": "Zrušit",
"search": "Hledat",
"remove": "Smazat",
"first-name": "Křestní jméno",
"last-name": "Příjmení",
"loading": "Načítá se...",
"no-results": "Nebyli nalezeny žádné výsledky",
"no-value": "Nebyla zadána hodnota",
"group-collapse": "Sbalit",
"group-expand": "Rozbalit",
"group-collapse-help": "Kliknutím sem sbalíte",
"group-expand-help": "Kliknutím sem rozbalíte a přidáte další prvky"
},
"login": {
"title": "Přihlásit se",
"form": {
"header": "Prosím, přihlaste se do DSpace",
"email": "E-mailová adresa",
"forgot-password": "Zapomněli jste své heslo?",
"new-user": "Nový uživatel? Zaregistrujte se kliknutím sem.",
"password": "Heslo",
"submit": "Přihlásit se"
}
},
"logout": {
"title": "Odhlásit se",
"form": {
"header": "Odhlásit se z DSpace",
"submit": "Odhlásit se"
}
},
"auth": {
"messages": {
"expired": "Vaše relace vypršela. Prosím, znova se přihlaste."
},
"errors": {
"invalid-user": "Neplatná e-mailová adresa nebo heslo."
}
}
}

277
resources/i18n/de.json Normal file
View File

@@ -0,0 +1,277 @@
{
"footer": {
"copyright": "Copyright © 2002-{{ year }}",
"link.dspace": "DSpace Software",
"link.duraspace": "DuraSpace"
},
"collection": {
"page": {
"news": "Neuigkeiten",
"license": "Lizenz",
"browse": {
"recent": {
"head": "Aktuellste Veröffentlichungen"
}
}
}
},
"community": {
"page": {
"news": "Neuigkeiten",
"license": "Lizenz"
},
"sub-collection-list": {
"head": "Sammlungen in diesem Bereich"
}
},
"item": {
"page": {
"author": "Autor",
"abstract": "Kurzfassung",
"date": "Datum",
"uri": "URI",
"files": "Dateien",
"collections": "Sammlungen",
"filesection": {
"download": "Herunterladen",
"name": "Name:",
"format": "Format:",
"size": "Größe:",
"description": "Beschreibung:"
},
"link": {
"simple": "Kurzanzeige",
"full": "Vollanzeige"
}
}
},
"nav": {
"home": "Zur Startseite",
"login": "Anmelden",
"logout": "Abmelden"
},
"pagination": {
"results-per-page": "Ergebnisse pro Seite",
"sort-direction": "Sortiermöglichkeiten",
"showing": {
"label": "Anzeige der Treffer ",
"detail": "{{ range }} bis {{ total }}"
}
},
"sorting": {
"score": {
"DESC": "Relevanz"
},
"dc.title": {
"ASC": "Titel aufsteigend",
"DESC": "Titel absteigend"
}
},
"title": "DSpace",
"404": {
"help": "Die Seite, die Sie aufrufen wollten, konnte nicht gefunden werden. Sie könnte verschoben oder gelöscht worden sein. Mit dem Link unten kommen Sie zurück zur Startseite. ",
"page-not-found": "Seite nicht gefunden",
"link": {
"home-page": "Zurück zur Startseite"
}
},
"home": {
"title": "DSpace Angular :: Startseite",
"description": "",
"top-level-communities": {
"head": "Bereiche in DSpace",
"help": "Wählen Sie einen Bereich, um seine Sammlungen einzusehen."
}
},
"search": {
"title": "DSpace Angular :: Suche",
"description": "",
"form": {
"search": "Suche",
"search_dspace": "DSpace durchsuchen"
},
"results": {
"head": "Suchergebnisse",
"no-results": "Zu dieser Suche gibt es keine Treffer."
},
"sidebar": {
"close": "Zurück zu den Ergebnissen",
"open": "Suchwerkzeuge",
"results": "Ergebnisse",
"filters": {
"title": "Filter"
},
"settings": {
"title": "Einstellungen",
"sort-by": "Sortiere nach",
"rpp": "Treffer pro Seite"
}
},
"view-switch": {
"show-list": "Zeige als Liste",
"show-grid": "Zeige als Raster"
},
"filters": {
"head": "Filter",
"reset": "Filter zurücksetzen",
"applied": {
"f.author": "Autor",
"f.dateIssued.min": "Anfangsdatum",
"f.dateIssued.max": "Enddatum",
"f.subject": "Thema",
"f.has_content_in_original_bundle": "Besitzt Dateien"
},
"filter": {
"show-more": "Zeige mehr",
"show-less": "Zeige weniger",
"author": {
"placeholder": "Autor",
"head": "Autor"
},
"scope": {
"placeholder": "Bereichsfilter",
"head": "Bereich"
},
"subject": {
"placeholder": "Schlagwort",
"head": "Schlagwort"
},
"dateIssued": {
"max": {
"placeholder": "Frühestes Datum"
},
"min": {
"placeholder": "Ältestes Datum"
},
"head": "Datum"
},
"has_content_in_original_bundle": {
"head": "Besitzt Dateien"
}
}
}
},
"browse": {
"title": "Anzeige {{ collection }} nach {{ field }} {{ value }}"
},
"admin": {
"registries": {
"metadata": {
"title": "DSpace Angular :: Metadatenreferenzliste",
"head": "Metadatenreferenzliste",
"description": "Die Metadatenreferenzliste beinhaltet alle Metadatenfelder, die zur Verfügung stehen. Die Felder können in unterschiedlichen Schemata enthalten sein. Nichtsdestotrotz benötigt DSpace mindestens qualifiziertes Dublin Core.",
"schemas": {
"table": {
"id": "ID",
"namespace": "Namensraum",
"name": "Name"
},
"no-items": "Es gbit keine Metadatenschemata."
}
},
"schema": {
"title": "DSpace Angular :: Referenzliste der Metadatenschemata",
"head": "Metadatenschemata",
"description": "Dies ist das Metadatenschema für \"{{namespace}}\".",
"fields": {
"head": "Felder in diesem Schema",
"table": {
"field": "Feld",
"scopenote": "Gültigkeitsbereich"
},
"no-items": "Es gibt keine Felder in diesem Schema."
}
},
"bitstream-formats": {
"title": "DSpace Angular :: Referenzliste der Dateiformate",
"head": "Referenzliste der Dateiformate",
"description": "Diese Liste enhtält die in diesem Repositorium zulässigen Dateiformate und den jeweiligen Unterstützungsgrad.",
"formats": {
"table": {
"name": "Name",
"mimetype": "MIME Type",
"supportLevel": {
"head": "Unterstützungsgrad",
"0": "Unbekannt",
"1": "Bekannt",
"2": "Unterstützt"
},
"internal": "intern"
},
"no-items": "Es gibt keine Formate in dieser Referenzliste."
}
}
}
},
"loading": {
"default": "Am Laden ...",
"top-level-communities": "Die Hauptbereiche werden geladen ...",
"community": "Der Bereich wird geladen ...",
"collection": "Die Sammlung wird geladen ...",
"sub-collections": "Die untergeordneten Sammlungen werden geladen ...",
"recent-submissions": "Die aktuellsten Veröffentlichungen werden geladen ...",
"item": "Die Ressource wird geladen ...",
"objects": "Am Laden ...",
"search-results": "Die Suchergebnisse werden geladen ...",
"browse-by": "Die Ressourcen werden geladen ..."
},
"error": {
"default": "Fehler",
"top-level-communities": "Fehler beim Laden der Hauptbereiche.",
"community": "Fehler beim Laden des Bereiches.",
"collection": "Fehler beim Laden der Sammlung.",
"sub-collections": "Fehler beim Laden der untergeordneten Sammlungen.",
"recent-submissions": "Fehler beim Laden der aktuellsten Veröffentlichungen.",
"item": "Fehler beim Laden der Ressource.",
"objects": "Fehler beim Laden der Objekte.",
"search-results": "Fehler beim Laden der Suchergebnisse.",
"browse-by": "Fehler beim Laden der Ressourcen",
"validation": {
"pattern": "Die Eingabe kann nur folgendes Muster haben: {{ pattern }}.",
"license": {
"notgranted": "Sie müssen der Lizenz zustimmen, um die Ressource einzureichen. Wenn dies zur Zeit nicht geht, können Sie die Einreichung speichern und später wiederaufnehmen oder löschen."
}
}
},
"form": {
"submit": "Los",
"cancel": "Abbrechen",
"search": "Suchen",
"remove": "Löschen",
"first-name": "Vorname",
"last-name": "Nachname",
"loading": "Am Laden ...",
"no-results": "Keine Ergebnisse gefunden",
"no-value": "Kein Wert eingegeben",
"group-collapse": "Weniger",
"group-expand": "Mehr",
"group-collapse-help": "Hier klicken, um die Anzeige zu reduzieren",
"group-expand-help": "Hier klicken, um mehr Elemente anzuzeigen"
},
"login": {
"title": "Einloggen",
"form": {
"header": "Bitte Loggen Sie sich ein.",
"email": "E-Mail-Adresse",
"forgot-password": "Haben Sie Ihr Passwort vergessen?",
"new-user": "Sind Sie neu hier? Klicken Sie hier, um sich zu registrieren.",
"password": "Passwort",
"submit": "Einloggen"
}
},
"logout": {
"title": "Ausloggen",
"form": {
"header": "Ausloggen aus DSpace",
"submit": "Ausloggen"
}
},
"auth": {
"messages": {
"expired": "Ihre Sitzung ist abgelaufen, bitte melden Sie sich erneut an."
},
"errors": {
"invalid-user": "Ungültige E-Mail-Adresse oder Passwort."
}
}
}

View File

@@ -185,6 +185,9 @@
}
}
},
"browse": {
"title": "Browsing {{ collection }} by {{ field }} {{ value }}"
},
"admin": {
"registries": {
"metadata": {
@@ -236,18 +239,19 @@
},
"loading": {
"default": "Loading...",
"top-level-communities": "Loading top level communities...",
"top-level-communities": "Loading top-level communities...",
"community": "Loading community...",
"collection": "Loading collection...",
"sub-collections": "Loading sub-collections...",
"recent-submissions": "Loading recent submissions...",
"item": "Loading item...",
"objects": "Loading...",
"search-results": "Loading search results..."
"search-results": "Loading search results...",
"browse-by": "Loading items..."
},
"error": {
"default": "Error",
"top-level-communities": "Error fetching top level communities",
"top-level-communities": "Error fetching top-level communities",
"community": "Error fetching community",
"collection": "Error fetching collection",
"sub-collections": "Error fetching sub-collections",
@@ -255,6 +259,7 @@
"item": "Error fetching item",
"objects": "Error fetching objects",
"search-results": "Error fetching search results",
"browse-by": "Error fetching items",
"validation": {
"pattern": "This input is restricted by the current pattern: {{ pattern }}.",
"license": {
@@ -275,7 +280,7 @@
"group-collapse": "Collapse",
"group-expand": "Expand",
"group-collapse-help": "Click here to collapse",
"group-expand-help": "Click here to expand and add more element"
"group-expand-help": "Click here to expand and add more elements"
},
"login": {
"title": "Login",
@@ -300,7 +305,7 @@
"expired": "Your session has expired. Please log in again."
},
"errors": {
"invalid-user": "Invalid email or password."
"invalid-user": "Invalid email address or password."
}
}
}

277
resources/i18n/nl.json Normal file
View File

@@ -0,0 +1,277 @@
{
"footer": {
"copyright": "copyright © 2002-{{ year }}",
"link.dspace": "DSpace software",
"link.duraspace": "DuraSpace"
},
"collection": {
"page": {
"news": "Nieuws",
"license": "Licentie",
"browse": {
"recent": {
"head": "Recent toegevoegd"
}
}
}
},
"community": {
"page": {
"news": "Nieuws",
"license": "Licentie"
},
"sub-collection-list": {
"head": "Collecties in deze Community"
}
},
"item": {
"page": {
"author": "Auteur",
"abstract": "Abstract",
"date": "Datum",
"uri": "URI",
"files": "Bestanden",
"collections": "Collecties",
"filesection": {
"download": "Download",
"name": "Naam:",
"format": "Formaat:",
"size": "Grootte:",
"description": "Beschrijving:"
},
"link": {
"simple": "Eenvoudige item weergave",
"full": "Volledige item weergave"
}
}
},
"nav": {
"home": "Home",
"login": "Log In",
"logout": "Log Uit"
},
"pagination": {
"results-per-page": "Resultaten per pagina",
"sort-direction": "Sorteer mogelijkheden",
"showing": {
"label": "Getoonde items ",
"detail": "{{ range }} tot {{ total }}"
}
},
"sorting": {
"score": {
"DESC": "Relevantie"
},
"dc.title": {
"ASC": "Oplopend op titel",
"DESC": "Aflopend op titel"
}
},
"title": "DSpace",
"404": {
"help": "De pagina die u zoekt kan niet gevonden worden. De pagina werd mogelijk verplaatst of verwijderd. U kan onderstaande knop gebruiken om terug naar de homepagina te gaan. ",
"page-not-found": "Pagina niet gevonden",
"link": {
"home-page": "Terug naar de homepagina"
}
},
"home": {
"title": "DSpace Angular :: Home",
"description": "",
"top-level-communities": {
"head": "Communities in DSpace",
"help": "Selecteer een community om diens collecties te verkennen."
}
},
"search": {
"title": "DSpace Angular :: Zoek",
"description": "",
"form": {
"search": "Zoek",
"search_dspace": "Zoek in DSpace"
},
"results": {
"head": "Zoekresultaten",
"no-results": "Er waren geen resultaten voor deze zoekopdracht"
},
"sidebar": {
"close": "Terug naar de resultaten",
"open": "Zoek Tools",
"results": "resultaten",
"filters": {
"title": "Filters"
},
"settings": {
"title": "Instellingen",
"sort-by": "Sorteer volgens",
"rpp": "Resultaten per pagina"
}
},
"view-switch": {
"show-list": "Toon als lijst",
"show-grid": "Toon in raster"
},
"filters": {
"head": "Filters",
"reset": "Filters verwijderen",
"applied": {
"f.author": "Auteur",
"f.dateIssued.min": "Start datum",
"f.dateIssued.max": "Eind datum",
"f.subject": "Sleutelwoord",
"f.has_content_in_original_bundle": "Heeft bestanden"
},
"filter": {
"show-more": "Toon meer",
"show-less": "Inklappen",
"author": {
"placeholder": "Auteursnaam",
"head": "Auteur"
},
"scope": {
"placeholder": "Bereik filter",
"head": "Bereik"
},
"subject": {
"placeholder": "Onderwerp",
"head": "Onderwerp"
},
"dateIssued": {
"max": {
"placeholder": "Vroegste Datum"
},
"min": {
"placeholder": "Laatste Datum"
},
"head": "Datum"
},
"has_content_in_original_bundle": {
"head": "Heeft bestanden"
}
}
}
},
"browse": {
"title": "Verken {{ collection }} volgens {{ field }} {{ value }}"
},
"admin": {
"registries": {
"metadata": {
"title": "DSpace Angular :: Metadata Register",
"head": "Metadata Register",
"description": "Het metadata register omvat de lijst van alle metadata velden die beschikbaar zijn in het systeem. Deze velden kunnen verspreid zijn over verschillende metadata schema's. Het qualified Dublin Core schema (dc) is een verplicht schema en kan niet worden verwijderd.",
"schemas": {
"table": {
"id": "ID",
"namespace": "Naamruimte",
"name": "Naam"
},
"no-items": "Er kunnen geen metadata schema's getoond worden."
}
},
"schema": {
"title": "DSpace Angular :: Metadata Schema Register",
"head": "Metadata Schema",
"description": "Dit is het metadata schema voor \"{{namespace}}\".",
"fields": {
"head": "Schema metadata velden",
"table": {
"field": "Veld",
"scopenote": "Opmerking over bereik"
},
"no-items": "Er kunnen geen metadata velden getoond worden."
}
},
"bitstream-formats": {
"title": "DSpace Angular :: Bitstream Formaat Register",
"head": "Bitstream Formaat Register",
"description": "Deze lijst van Bitstream formaten biedt informatie over de formaten die in deze repository zijn toegelaten en op welke manier ze ondersteund worden. De term Bitstream wordt in DSpace gebruikt om een bestand aan te duiden dat samen met metadata onderdeel uitmaakt van een item. De naam bitstream duidt op het feit dat het bestand achterliggend wordt opgeslaan zonder bestandsextensie.",
"formats": {
"table": {
"name": "Naam",
"mimetype": "MIME Type",
"supportLevel": {
"head": "Ondersteuning",
"0": "Onbekend",
"1": "Gekend",
"2": "Ondersteund"
},
"internal": "intern"
},
"no-items": "Er kunnen geen bitstream formaten getoond worden."
}
}
}
},
"loading": {
"default": "Laden...",
"top-level-communities": "Inladen van de Communities op het hoogste niveau...",
"community": "Community wordt ingeladen...",
"collection": "Collectie wordt ingeladen...",
"sub-collections": "De sub-collecties worden ingeladen...",
"recent-submissions": "Recent toegevoegde items worden ingeladen...",
"item": "Item wordt ingeladen...",
"objects": "Laden...",
"search-results": "Zoekresultaten worden ingeladen...",
"browse-by": "Items worden ingeladen..."
},
"error": {
"default": "Fout",
"top-level-communities": "Fout bij het inladen van communities op het hoogste niveau",
"community": "Fout bij het ophalen van een community",
"collection": "Fout bij het ophalen van een collectie",
"sub-collections": "Fout bij het ophalen van sub-collecties",
"recent-submissions": "Fout bij het ophalen van recent toegevoegde items",
"item": "Fout bij het ophalen van items",
"objects": "Fout bij het ophalen van objecten",
"search-results": "Fout bij het ophalen van zoekresultaten",
"browse-by": "Fout bij het ophalen van items",
"validation": {
"pattern": "Deze invoer is niet toegelaten volgens dit patroon: {{ pattern }}.",
"license": {
"notgranted": "U moet de invoerlicentie goedkeuren om de invoer af te werken. Indien u deze licentie momenteel niet kan of mag goedkeuren, kan u uw werk opslaan en de invoer later afwerken. U kan dit nieuwe item ook verwijderen indien u niet voldoet aan de vereisten van de invoer licentie."
}
}
},
"form": {
"submit": "Verstuur",
"cancel": "Annuleer",
"search": "Zoek",
"remove": "Verwijder",
"first-name": "Voornaam",
"last-name": "Achternaam",
"loading": "Inladen...",
"no-results": "Geen resultaten gevonden",
"no-value": "Geen waarde ingevoerd",
"group-collapse": "Inklappen",
"group-expand": "Uitklappen",
"group-collapse-help": "Klik hier op in te klappen",
"group-expand-help": "Klik hier om uit te klappen en om meer onderdelen toe te voegen"
},
"login": {
"title": "Aanmelden",
"form": {
"header": "Gelieve in te loggen in DSpace",
"email": "Email adres",
"forgot-password": "Bent u uw wachtwoord vergeten?",
"new-user": "Nieuwe gebruiker? Gelieve u hier te registreren",
"password": "Wachtwoord",
"submit": "Aanmelden"
}
},
"logout": {
"title": "Afmelden",
"form": {
"header": "Afmelden in DSpace",
"submit": "Afmelden"
}
},
"auth": {
"messages": {
"expired": "Uw sessie is vervallen. Gelieve opnieuw aan te melden."
},
"errors": {
"invalid-user": "Ongeldig email adres of wachtwoord."
}
}
}

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,16 +1,14 @@
import { MetadataSchemaComponent } from './metadata-schema.component';
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Observable } from 'rxjs/Observable';
import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list';
import { MetadataSchema } from '../../../core/metadata/metadataschema.model';
import { TranslateLoader, TranslateModule } from '@ngx-translate/core';
import { TranslateModule } from '@ngx-translate/core';
import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { By } from '@angular/platform-browser';
import { MockTranslateLoader } from '../../../shared/testing/mock-translate-loader';
import { RegistryService } from '../../../core/registry/registry.service';
import { SharedModule } from '../../../shared/shared.module';
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
@@ -68,15 +66,15 @@ describe('MetadataSchemaComponent', () => {
schema: mockSchemasList[1]
}
];
const mockSchemas = Observable.of(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockSchemasList)));
const mockSchemas = observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockSchemasList)));
const registryServiceStub = {
getMetadataSchemas: () => mockSchemas,
getMetadataFieldsBySchema: (schema: MetadataSchema) => Observable.of(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockFieldsList.filter((value) => value.schema === schema)))),
getMetadataSchemaByName: (schemaName: string) => Observable.of(new RemoteData(false, false, true, undefined, mockSchemasList.filter((value) => value.prefix === schemaName)[0]))
getMetadataFieldsBySchema: (schema: MetadataSchema) => observableOf(new RemoteData(false, false, true, undefined, new PaginatedList(null, mockFieldsList.filter((value) => value.schema === schema)))),
getMetadataSchemaByName: (schemaName: string) => observableOf(new RemoteData(false, false, true, undefined, mockSchemasList.filter((value) => value.prefix === schemaName)[0]))
};
const schemaNameParam = 'mock';
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
params: Observable.of({
params: observableOf({
schemaName: schemaNameParam
})
});

View File

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

View File

@@ -0,0 +1,11 @@
<div class="container">
<div class="browse-by-author w-100 row">
<ds-browse-by class="col-xs-12 w-100"
title="{{'browse.title' | translate:{collection: '', field: 'Author', value: (value)? '&quot;' + value + '&quot;': ''} }}"
[objects$]="(items$ !== undefined)? items$ : authors$"
[currentUrl]="currentUrl"
[paginationConfig]="paginationConfig"
[sortConfig]="sortConfig">
</ds-browse-by>
</div>
</div>

View File

@@ -0,0 +1,107 @@
import {combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs';
import { Component, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { PaginatedList } from '../../core/data/paginated-list';
import { ItemDataService } from '../../core/data/item-data.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { ActivatedRoute } from '@angular/router';
import { hasValue, isNotEmpty } from '../../shared/empty.util';
import { BrowseService } from '../../core/browse/browse.service';
import { BrowseEntry } from '../../core/shared/browse-entry.model';
import { Item } from '../../core/shared/item.model';
@Component({
selector: 'ds-browse-by-author-page',
styleUrls: ['./browse-by-author-page.component.scss'],
templateUrl: './browse-by-author-page.component.html'
})
/**
* Component for browsing (items) by author (dc.contributor.author)
*/
export class BrowseByAuthorPageComponent implements OnInit {
authors$: Observable<RemoteData<PaginatedList<BrowseEntry>>>;
items$: Observable<RemoteData<PaginatedList<Item>>>;
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'browse-by-author-pagination',
currentPage: 1,
pageSize: 20
});
sortConfig: SortOptions = new SortOptions('dc.contributor.author', SortDirection.ASC);
subs: Subscription[] = [];
currentUrl: string;
value = '';
public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute, private browseService: BrowseService) {
}
ngOnInit(): void {
this.currentUrl = this.route.snapshot.pathFromRoot
.map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
.join('/');
this.updatePage({
pagination: this.paginationConfig,
sort: this.sortConfig
});
this.subs.push(
observableCombineLatest(
this.route.params,
this.route.queryParams,
(params, queryParams, ) => {
return Object.assign({}, params, queryParams);
})
.subscribe((params) => {
const page = +params.page || this.paginationConfig.currentPage;
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
const sortDirection = params.sortDirection || this.sortConfig.direction;
const sortField = params.sortField || this.sortConfig.field;
this.value = +params.value || params.value || '';
const pagination = Object.assign({},
this.paginationConfig,
{ currentPage: page, pageSize: pageSize }
);
const sort = Object.assign({},
this.sortConfig,
{ direction: sortDirection, field: sortField }
);
const searchOptions = {
pagination: pagination,
sort: sort
};
if (isNotEmpty(this.value)) {
this.updatePageWithItems(searchOptions, this.value);
} else {
this.updatePage(searchOptions);
}
}));
}
/**
* Updates the current page with searchOptions
* @param searchOptions Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
*/
updatePage(searchOptions) {
this.authors$ = this.browseService.getBrowseEntriesFor('author', searchOptions);
this.items$ = undefined;
}
/**
* Updates the current page with searchOptions and display items linked to author
* @param searchOptions Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
* @param author The author's name for displaying items
*/
updatePageWithItems(searchOptions, author: string) {
this.items$ = this.browseService.getBrowseItemsFor('author', author, searchOptions);
}
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -0,0 +1,11 @@
<div class="container">
<div class="browse-by-title w-100 row">
<ds-browse-by class="col-xs-12 w-100"
title="{{'browse.title' | translate:{collection: '', field: 'Title', value: ''} }}"
[objects$]="items$"
[currentUrl]="currentUrl"
[paginationConfig]="paginationConfig"
[sortConfig]="sortConfig">
</ds-browse-by>
</div>
</div>

View File

@@ -0,0 +1,92 @@
import {combineLatest as observableCombineLatest, Observable , Subscription } from 'rxjs';
import { Component, OnInit } from '@angular/core';
import { RemoteData } from '../../core/data/remote-data';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { PaginatedList } from '../../core/data/paginated-list';
import { ItemDataService } from '../../core/data/item-data.service';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { Item } from '../../core/shared/item.model';
import { ActivatedRoute, PRIMARY_OUTLET, UrlSegmentGroup } from '@angular/router';
import { hasValue } from '../../shared/empty.util';
import { Collection } from '../../core/shared/collection.model';
@Component({
selector: 'ds-browse-by-title-page',
styleUrls: ['./browse-by-title-page.component.scss'],
templateUrl: './browse-by-title-page.component.html'
})
/**
* Component for browsing items by title (dc.title)
*/
export class BrowseByTitlePageComponent implements OnInit {
items$: Observable<RemoteData<PaginatedList<Item>>>;
paginationConfig: PaginationComponentOptions = Object.assign(new PaginationComponentOptions(), {
id: 'browse-by-title-pagination',
currentPage: 1,
pageSize: 20
});
sortConfig: SortOptions = new SortOptions('dc.title', SortDirection.ASC);
subs: Subscription[] = [];
currentUrl: string;
public constructor(private itemDataService: ItemDataService, private route: ActivatedRoute) {
}
ngOnInit(): void {
this.currentUrl = this.route.snapshot.pathFromRoot
.map((snapshot) => (snapshot.routeConfig) ? snapshot.routeConfig.path : '')
.join('/');
this.updatePage({
pagination: this.paginationConfig,
sort: this.sortConfig
});
this.subs.push(
observableCombineLatest(
this.route.params,
this.route.queryParams,
(params, queryParams, ) => {
return Object.assign({}, params, queryParams);
})
.subscribe((params) => {
const page = +params.page || this.paginationConfig.currentPage;
const pageSize = +params.pageSize || this.paginationConfig.pageSize;
const sortDirection = params.sortDirection || this.sortConfig.direction;
const sortField = params.sortField || this.sortConfig.field;
const pagination = Object.assign({},
this.paginationConfig,
{ currentPage: page, pageSize: pageSize }
);
const sort = Object.assign({},
this.sortConfig,
{ direction: sortDirection, field: sortField }
);
this.updatePage({
pagination: pagination,
sort: sort
});
}));
}
/**
* Updates the current page with searchOptions
* @param searchOptions Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
*/
updatePage(searchOptions) {
this.items$ = this.itemDataService.findAll({
currentPage: searchOptions.pagination.currentPage,
elementsPerPage: searchOptions.pagination.pageSize,
sort: searchOptions.sort
});
}
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

@@ -0,0 +1,16 @@
import { RouterModule } from '@angular/router';
import { NgModule } from '@angular/core';
import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component';
import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
@NgModule({
imports: [
RouterModule.forChild([
{ path: 'title', component: BrowseByTitlePageComponent },
{ path: 'author', component: BrowseByAuthorPageComponent }
])
]
})
export class BrowseByRoutingModule {
}

View File

@@ -0,0 +1,27 @@
import { NgModule } from '@angular/core';
import { CommonModule } from '@angular/common';
import { BrowseByTitlePageComponent } from './+browse-by-title-page/browse-by-title-page.component';
import { ItemDataService } from '../core/data/item-data.service';
import { SharedModule } from '../shared/shared.module';
import { BrowseByRoutingModule } from './browse-by-routing.module';
import { BrowseByAuthorPageComponent } from './+browse-by-author-page/browse-by-author-page.component';
import { BrowseService } from '../core/browse/browse.service';
@NgModule({
imports: [
BrowseByRoutingModule,
CommonModule,
SharedModule
],
declarations: [
BrowseByTitlePageComponent,
BrowseByAuthorPageComponent
],
providers: [
ItemDataService,
BrowseService
]
})
export class BrowseByModule {
}

View File

@@ -1,8 +1,6 @@
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Subscription } from 'rxjs/Subscription';
import { Observable, Subscription } from 'rxjs';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { CollectionDataService } from '../core/data/collection-data.service';
import { PaginatedList } from '../core/data/paginated-list';
@@ -17,7 +15,7 @@ import { Item } from '../core/shared/item.model';
import { fadeIn, fadeInOut } from '../shared/animations/fade';
import { hasValue, isNotEmpty } from '../shared/empty.util';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { filter, flatMap, map } from 'rxjs/operators';
import { filter, flatMap, map, tap } from 'rxjs/operators';
import { SearchService } from '../+search-page/search-service/search.service';
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
import { toDSpaceObjectListRD } from '../core/shared/operators';
@@ -56,7 +54,9 @@ export class CollectionPageComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.collectionRD$ = this.route.data.map((data) => data.collection);
this.collectionRD$ = this.route.data.pipe(
map((data) => data.collection)
);
this.logoRD$ = this.collectionRD$.pipe(
map((rd: RemoteData<Collection>) => rd.payload),
filter((collection: Collection) => hasValue(collection)),

View File

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

View File

@@ -1,23 +1,14 @@
import { Component } from '@angular/core';
import { Community } from '../../core/shared/community.model';
import { ComColDataService } from '../../core/data/comcol-data.service';
import { NormalizedCommunity } from '../../core/cache/models/normalized-community.model';
import { CommunityDataService } from '../../core/data/community-data.service';
import { CollectionDataService } from '../../core/data/collection-data.service';
import { Collection } from '../../core/shared/collection.model';
import { RouteService } from '../../shared/services/route.service';
import { Router } from '@angular/router';
import { DSOSuccessResponse, ErrorResponse } from '../../core/cache/response-cache.models';
import { Observable } from 'rxjs/Observable';
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
import { first, flatMap, map, take } from 'rxjs/operators';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { RemoteData } from '../../core/data/remote-data';
import { hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { HttpEvent } from '@angular/common/http';
import { getSucceededRemoteData } from '../../core/shared/operators';
import { ObjectCacheService } from '../../core/cache/object-cache.service';
import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model';
import { isNotEmpty } from '../../shared/empty.util';
@Component({
selector: 'ds-create-collection',

View File

@@ -24,9 +24,11 @@
[content]="communityPayload.copyrightText"
[hasInnerHtml]="true">
</ds-comcol-page-content>
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
<ds-community-page-sub-collection-list
[community]="communityPayload"></ds-community-page-sub-collection-list>
</div>
</div>
<ds-error *ngIf="communityRD?.hasFailed" message="{{'error.community' | translate}}"></ds-error>
<ds-loading *ngIf="communityRD?.isLoading" message="{{'loading.community' | translate}}"></ds-loading>
<ds-loading *ngIf="communityRD?.isLoading"
message="{{'loading.community' | translate}}"></ds-loading>
</div>

View File

@@ -1,7 +1,8 @@
import { mergeMap, filter, map, first, tap } from 'rxjs/operators';
import { ChangeDetectionStrategy, Component, OnDestroy, OnInit } from '@angular/core';
import { ActivatedRoute, Params } from '@angular/router';
import { ActivatedRoute } from '@angular/router';
import { Subscription } from 'rxjs/Subscription';
import { Subscription, Observable } from 'rxjs';
import { CommunityDataService } from '../core/data/community-data.service';
import { RemoteData } from '../core/data/remote-data';
import { Bitstream } from '../core/shared/bitstream.model';
@@ -12,7 +13,6 @@ import { MetadataService } from '../core/metadata/metadata.service';
import { fadeInOut } from '../shared/animations/fade';
import { hasValue } from '../shared/empty.util';
import { Observable } from 'rxjs/Observable';
@Component({
selector: 'ds-community-page',
@@ -24,6 +24,8 @@ import { Observable } from 'rxjs/Observable';
export class CommunityPageComponent implements OnInit, OnDestroy {
communityRD$: Observable<RemoteData<Community>>;
logoRD$: Observable<RemoteData<Bitstream>>;
private subs: Subscription[] = [];
constructor(
@@ -35,15 +37,19 @@ export class CommunityPageComponent implements OnInit, OnDestroy {
}
ngOnInit(): void {
this.communityRD$ = this.route.data.map((data) => data.community);
this.logoRD$ = this.communityRD$
.map((rd: RemoteData<Community>) => rd.payload)
.filter((community: Community) => hasValue(community))
.flatMap((community: Community) => community.logo);
this.communityRD$ = this.route.data.pipe(map((data) => data.community));
this.logoRD$ = this.communityRD$.pipe(
map((rd: RemoteData<Community>) => rd.payload),
filter((community: Community) => hasValue(community)),
mergeMap((community: Community) => community.logo));
}
ngOnDestroy(): void {
this.subs.filter((sub) => hasValue(sub)).forEach((sub) => sub.unsubscribe());
}
}

View File

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

View File

@@ -1,8 +1,7 @@
import { Component } from '@angular/core';
import { Community } from '../../core/shared/community.model';
import { CommunityDataService } from '../../core/data/community-data.service';
import { ErrorResponse } from '../../core/cache/response-cache.models';
import { Observable } from 'rxjs/Observable';
import { Observable } from 'rxjs';
import { RouteService } from '../../shared/services/route.service';
import { Router } from '@angular/router';
import { RemoteData } from '../../core/data/remote-data';

View File

@@ -1,5 +1,5 @@
import { Component, Input, OnInit } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Observable } from 'rxjs';
import { RemoteData } from '../../core/data/remote-data';
import { Collection } from '../../core/shared/collection.model';

View File

@@ -1,5 +1,5 @@
import { ChangeDetectionStrategy, Component } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Observable } from 'rxjs';
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { CommunityDataService } from '../../core/data/community-data.service';
import { PaginatedList } from '../../core/data/paginated-list';
@@ -17,6 +17,7 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
changeDetection: ChangeDetectionStrategy.OnPush,
animations: [fadeInOut]
})
export class TopLevelCommunityListComponent {
communitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
config: PaginationComponentOptions;

View File

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

View File

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

View File

@@ -1,6 +1,8 @@
<div class="simple-view-element">
<span *ngIf="content.children.length != 0">
<h5 class="simple-view-element-header" *ngIf="label">{{ label }}</h5>
<div class="simple-view-element-body">
<ng-content></ng-content>
</div>
</span>
<div #content class="simple-view-element-body">
<ng-content></ng-content>
</div>
</div>

View File

@@ -0,0 +1,54 @@
import { ComponentFixture, TestBed, async } from '@angular/core/testing';
import { By } from '@angular/platform-browser';
import { Component, DebugElement } from '@angular/core';
import { MetadataFieldWrapperComponent } from './metadata-field-wrapper.component';
@Component({
selector: 'ds-component-with-content',
template: '<ds-metadata-field-wrapper [label]="\'test label\'">\n' +
' <div class="my content">\n' +
' </div>\n' +
'</ds-metadata-field-wrapper>'
})
class ContentComponent {}
describe('MetadataFieldWrapperComponent', () => {
let component: MetadataFieldWrapperComponent;
let fixture: ComponentFixture<MetadataFieldWrapperComponent>;
beforeEach(async(() => {
TestBed.configureTestingModule({
declarations: [MetadataFieldWrapperComponent, ContentComponent]
}).compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(MetadataFieldWrapperComponent);
component = fixture.componentInstance;
});
const wrapperSelector = '.simple-view-element';
const labelSelector = '.simple-view-element-header';
it('should create', () => {
expect(component).toBeDefined();
});
it('should not show a label when there is no content', () => {
component.label = 'test label';
fixture.detectChanges();
const debugLabel = fixture.debugElement.query(By.css(labelSelector));
expect(debugLabel).toBeNull();
});
it('should show a label when there is content', () => {
const parentFixture = TestBed.createComponent(ContentComponent);
parentFixture.detectChanges();
const parentComponent = parentFixture.componentInstance;
const parentNative = parentFixture.nativeElement;
const nativeLabel = parentNative.querySelector(labelSelector);
expect(nativeLabel.textContent).toContain('test label');
});
});

View File

@@ -1,4 +1,5 @@
import { Component, Input } from '@angular/core';
import { Metadatum } from '../../../core/shared/metadatum.model';
/**
* This component renders the configured 'values' into the ds-metadata-field-wrapper component.
@@ -11,7 +12,7 @@ import { Component, Input } from '@angular/core';
})
export class MetadataValuesComponent {
@Input() values: any;
@Input() values: Metadatum[];
@Input() separator: string;

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,7 +1,9 @@
import {mergeMap, filter, map} from 'rxjs/operators';
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core';
import { ActivatedRoute } from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { Observable } from 'rxjs';
import { ItemDataService } from '../../core/data/item-data.service';
import { RemoteData } from '../../core/data/remote-data';
import { Bitstream } from '../../core/shared/bitstream.model';
@@ -44,11 +46,11 @@ export class ItemPageComponent implements OnInit {
}
ngOnInit(): void {
this.itemRD$ = this.route.data.map((data) => data.item);
this.itemRD$ = this.route.data.pipe(map((data) => data.item));
this.metadataService.processRemoteData(this.itemRD$);
this.thumbnail$ = this.itemRD$
.map((rd: RemoteData<Item>) => rd.payload)
.filter((item: Item) => hasValue(item))
.flatMap((item: Item) => item.getThumbnail());
this.thumbnail$ = this.itemRD$.pipe(
map((rd: RemoteData<Item>) => rd.payload),
filter((item: Item) => hasValue(item)),
mergeMap((item: Item) => item.getThumbnail()),);
}
}

View File

@@ -3,8 +3,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import { Store } from '@ngrx/store';
import { TranslateModule } from '@ngx-translate/core';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of';
import { of as observableOf } from 'rxjs';
import { LoginPageComponent } from './login-page.component';
@@ -16,7 +15,7 @@ describe('LoginPageComponent', () => {
/* tslint:disable:no-empty */
dispatch: {},
/* tslint:enable:no-empty */
select: Observable.of(true)
select: observableOf(true)
});
beforeEach(async(() => {

View File

@@ -1,4 +1,3 @@
import 'rxjs/add/observable/of';
import { SortDirection, SortOptions } from '../core/cache/models/sort-options.model';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { PaginatedSearchOptions } from './paginated-search-options.model';

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -8,26 +8,25 @@ import { SearchService } from './search.service';
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
import { ActivatedRoute, Router, UrlTree } from '@angular/router';
import { RequestService } from '../../core/data/request.service';
import { ResponseCacheService } from '../../core/cache/response-cache.service';
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
import { RouterStub } from '../../shared/testing/router-stub';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { Observable } from 'rxjs/Observable';
import { Observable, combineLatest as observableCombineLatest } from 'rxjs';
import { PaginatedSearchOptions } from '../paginated-search-options.model';
import { RemoteData } from '../../core/data/remote-data';
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
import { RequestEntry } from '../../core/data/request.reducer';
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
import {
FacetConfigSuccessResponse,
SearchSuccessResponse
} from '../../core/cache/response-cache.models';
} from '../../core/cache/response.models';
import { SearchQueryResponse } from './search-query-response.model';
import { SearchFilterConfig } from './search-filter-config.model';
import { CommunityDataService } from '../../core/data/community-data.service';
import { ViewMode } from '../../core/shared/view-mode.model';
import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service';
import { of as observableOf } from 'rxjs';
import { map } from 'rxjs/operators';
@Component({ template: '' })
class DummyComponent {
@@ -52,12 +51,11 @@ describe('SearchService', () => {
providers: [
{ provide: Router, useValue: router },
{ provide: ActivatedRoute, useValue: route },
{ provide: ResponseCacheService, useValue: getMockResponseCacheService() },
{ provide: RequestService, useValue: getMockRequestService() },
{ provide: RemoteDataBuildService, useValue: {} },
{ provide: HALEndpointService, useValue: {} },
{ provide: CommunityDataService, useValue: {}},
{ provide: DSpaceObjectDataService, useValue: {}},
{ provide: CommunityDataService, useValue: {} },
{ provide: DSpaceObjectDataService, useValue: {} },
SearchService
],
});
@@ -84,14 +82,15 @@ describe('SearchService', () => {
};
const remoteDataBuildService = {
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, responseCacheObs: Observable<ResponseCacheEntry>, payloadObs: Observable<any>) => {
return Observable.combineLatest(requestEntryObs,
responseCacheObs, payloadObs, (req, res, pay) => {
return { req, res, pay };
});
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, payloadObs: Observable<any>) => {
return observableCombineLatest(requestEntryObs, payloadObs).pipe(
map(([req, pay]) => {
return { req, pay };
})
);
},
aggregate: (input: Array<Observable<RemoteData<any>>>): Observable<RemoteData<any[]>> => {
return Observable.of(new RemoteData(false, false, true, null, []));
return observableOf(new RemoteData(false, false, true, null, []));
}
};
@@ -109,12 +108,11 @@ describe('SearchService', () => {
providers: [
{ provide: Router, useValue: router },
{ provide: ActivatedRoute, useValue: route },
{ provide: ResponseCacheService, useValue: getMockResponseCacheService() },
{ provide: RequestService, useValue: getMockRequestService() },
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
{ provide: HALEndpointService, useValue: halService },
{ provide: CommunityDataService, useValue: {}},
{ provide: DSpaceObjectDataService, useValue: {}},
{ provide: CommunityDataService, useValue: {} },
{ provide: DSpaceObjectDataService, useValue: {} },
SearchService
],
});
@@ -158,10 +156,8 @@ describe('SearchService', () => {
const searchOptions = new PaginatedSearchOptions({});
const queryResponse = Object.assign(new SearchQueryResponse(), { objects: [] });
const response = new SearchSuccessResponse(queryResponse, '200');
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
beforeEach(() => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint));
(searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry));
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
/* tslint:disable:no-empty */
searchService.search(searchOptions).subscribe((t) => {
}); // subscribe to make sure all methods are called
@@ -179,19 +175,14 @@ describe('SearchService', () => {
it('should call getByHref on the request service with the correct request url', () => {
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint);
});
it('should call get on the request service with the correct request url', () => {
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(endPoint);
});
});
describe('when getConfig is called without a scope', () => {
const endPoint = 'http://endpoint.com/test/config';
const filterConfig = [new SearchFilterConfig()];
const response = new FacetConfigSuccessResponse(filterConfig, '200');
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
beforeEach(() => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint));
(searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry));
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
/* tslint:disable:no-empty */
searchService.getConfig(null).subscribe((t) => {
}); // subscribe to make sure all methods are called
@@ -209,9 +200,6 @@ describe('SearchService', () => {
it('should call getByHref on the request service with the correct request url', () => {
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(endPoint);
});
it('should call get on the request service with the correct request url', () => {
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(endPoint);
});
});
describe('when getConfig is called with a scope', () => {
@@ -220,10 +208,8 @@ describe('SearchService', () => {
const requestUrl = endPoint + '?scope=' + scope;
const filterConfig = [new SearchFilterConfig()];
const response = new FacetConfigSuccessResponse(filterConfig, '200');
const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response });
beforeEach(() => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endPoint));
(searchService as any).responseCache.get.and.returnValue(Observable.of(responseEntry));
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
/* tslint:disable:no-empty */
searchService.getConfig(scope).subscribe((t) => {
}); // subscribe to make sure all methods are called
@@ -241,9 +227,6 @@ describe('SearchService', () => {
it('should call getByHref on the request service with the correct request url', () => {
expect((searchService as any).requestService.getByHref).toHaveBeenCalledWith(requestUrl);
});
it('should call get on the request service with the correct request url', () => {
expect((searchService as any).responseCache.get).toHaveBeenCalledWith(requestUrl);
});
});
});
});

View File

@@ -1,3 +1,4 @@
import { combineLatest as observableCombineLatest, Observable, of as observableOf } from 'rxjs';
import { Injectable, OnDestroy } from '@angular/core';
import {
ActivatedRoute,
@@ -6,16 +7,13 @@ import {
Router,
UrlSegmentGroup
} from '@angular/router';
import { Observable } from 'rxjs/Observable';
import { flatMap, map, switchMap } from 'rxjs/operators';
import { map, switchMap, tap } from 'rxjs/operators';
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
import {
FacetConfigSuccessResponse,
FacetValueSuccessResponse,
SearchSuccessResponse
} from '../../core/cache/response-cache.models';
import { ResponseCacheEntry } from '../../core/cache/response-cache.reducer';
import { ResponseCacheService } from '../../core/cache/response-cache.service';
} from '../../core/cache/response.models';
import { PaginatedList } from '../../core/data/paginated-list';
import { ResponseParsingService } from '../../core/data/parsing.service';
import { RemoteData } from '../../core/data/remote-data';
@@ -24,7 +22,11 @@ import { RequestService } from '../../core/data/request.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { GenericConstructor } from '../../core/shared/generic-constructor';
import { HALEndpointService } from '../../core/shared/hal-endpoint.service';
import { configureRequest, getSucceededRemoteData } from '../../core/shared/operators';
import {
configureRequest,
getResponseFromEntry,
getSucceededRemoteData
} from '../../core/shared/operators';
import { URLCombiner } from '../../core/url-combiner/url-combiner';
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { NormalizedSearchResult } from '../normalized-search-result.model';
@@ -68,7 +70,6 @@ export class SearchService implements OnDestroy {
constructor(private router: Router,
private route: ActivatedRoute,
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
private rdb: RemoteDataBuildService,
private halService: HALEndpointService,
@@ -98,16 +99,12 @@ export class SearchService implements OnDestroy {
configureRequest(this.requestService)
);
const requestEntryObs = requestObs.pipe(
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
);
const responseCacheObs = requestObs.pipe(
flatMap((request: RestRequest) => this.responseCache.get(request.href))
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
);
// get search results from response cache
const sqrObs: Observable<SearchQueryResponse> = responseCacheObs.pipe(
map((entry: ResponseCacheEntry) => entry.response),
const sqrObs: Observable<SearchQueryResponse> = requestEntryObs.pipe(
getResponseFromEntry(),
map((response: SearchSuccessResponse) => response.results)
);
@@ -115,39 +112,43 @@ export class SearchService implements OnDestroy {
// Turn list of observable remote data DSO's into observable remote data object with list of DSO
const dsoObs: Observable<RemoteData<DSpaceObject[]>> = sqrObs.pipe(
map((sqr: SearchQueryResponse) => {
return sqr.objects.map((nsr: NormalizedSearchResult) =>
this.rdb.buildSingle(nsr.dspaceObject));
return sqr.objects.map((nsr: NormalizedSearchResult) => {
return this.rdb.buildSingle(nsr.dspaceObject);
})
}),
flatMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input))
switchMap((input: Array<Observable<RemoteData<DSpaceObject>>>) => this.rdb.aggregate(input)),
);
// Create search results again with the correct dso objects linked to each result
const tDomainListObs = Observable.combineLatest(sqrObs, dsoObs, (sqr: SearchQueryResponse, dsos: RemoteData<DSpaceObject[]>) => {
const tDomainListObs = observableCombineLatest(sqrObs, dsoObs).pipe(
map(([sqr, dsos]) => {
return sqr.objects.map((object: NormalizedSearchResult, index: number) => {
let co = DSpaceObject;
if (dsos.payload[index]) {
const constructor: GenericConstructor<ListableObject> = dsos.payload[index].constructor as GenericConstructor<ListableObject>;
co = getSearchResultFor(constructor);
return Object.assign(new co(), object, {
dspaceObject: dsos.payload[index]
});
} else {
return undefined;
}
});
})
);
return sqr.objects.map((object: NormalizedSearchResult, index: number) => {
let co = DSpaceObject;
if (dsos.payload[index]) {
const constructor: GenericConstructor<ListableObject> = dsos.payload[index].constructor as GenericConstructor<ListableObject>;
co = getSearchResultFor(constructor);
return Object.assign(new co(), object, {
dspaceObject: dsos.payload[index]
});
} else {
return undefined;
}
});
});
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
map((entry: ResponseCacheEntry) => entry.response),
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
getResponseFromEntry(),
map((response: FacetValueSuccessResponse) => response.pageInfo)
);
const payloadObs = Observable.combineLatest(tDomainListObs, pageInfoObs, (tDomainList, pageInfo) => {
return new PaginatedList(pageInfo, tDomainList);
});
const payloadObs = observableCombineLatest(tDomainListObs, pageInfoObs).pipe(
map(([tDomainList, pageInfo]) => {
return new PaginatedList(pageInfo, tDomainList);
})
);
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
}
/**
@@ -179,21 +180,17 @@ export class SearchService implements OnDestroy {
);
const requestEntryObs = requestObs.pipe(
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
);
const responseCacheObs = requestObs.pipe(
flatMap((request: RestRequest) => this.responseCache.get(request.href))
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
);
// get search results from response cache
const facetConfigObs: Observable<SearchFilterConfig[]> = responseCacheObs.pipe(
map((entry: ResponseCacheEntry) => entry.response),
const facetConfigObs: Observable<SearchFilterConfig[]> = requestEntryObs.pipe(
getResponseFromEntry(),
map((response: FacetConfigSuccessResponse) =>
response.results.map((result: any) => Object.assign(new SearchFilterConfig(), result)))
);
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, facetConfigObs);
return this.rdb.toRemoteDataObservable(requestEntryObs, facetConfigObs);
}
/**
@@ -226,29 +223,27 @@ export class SearchService implements OnDestroy {
);
const requestEntryObs = requestObs.pipe(
flatMap((request: RestRequest) => this.requestService.getByHref(request.href))
);
const responseCacheObs = requestObs.pipe(
flatMap((request: RestRequest) => this.responseCache.get(request.href))
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
);
// get search results from response cache
const facetValueObs: Observable<FacetValue[]> = responseCacheObs.pipe(
map((entry: ResponseCacheEntry) => entry.response),
const facetValueObs: Observable<FacetValue[]> = requestEntryObs.pipe(
getResponseFromEntry(),
map((response: FacetValueSuccessResponse) => response.results)
);
const pageInfoObs: Observable<PageInfo> = responseCacheObs.pipe(
map((entry: ResponseCacheEntry) => entry.response),
const pageInfoObs: Observable<PageInfo> = requestEntryObs.pipe(
getResponseFromEntry(),
map((response: FacetValueSuccessResponse) => response.pageInfo)
);
const payloadObs = Observable.combineLatest(facetValueObs, pageInfoObs, (facetValue, pageInfo) => {
return new PaginatedList(pageInfo, facetValue);
});
const payloadObs = observableCombineLatest(facetValueObs, pageInfoObs).pipe(
map(([facetValue, pageInfo]) => {
return new PaginatedList(pageInfo, facetValue);
})
);
return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs);
return this.rdb.toRemoteDataObservable(requestEntryObs, payloadObs);
}
/**
@@ -272,12 +267,14 @@ export class SearchService implements OnDestroy {
switchMap((dsoRD: RemoteData<DSpaceObject>) => {
if (dsoRD.payload.type === ResourceType.Community) {
const community: Community = dsoRD.payload as Community;
return Observable.combineLatest(community.subcommunities, community.collections, (subCommunities, collections) => {
/*if this is a community, we also need to show the direct children*/
return [community, ...subCommunities.payload.page, ...collections.payload.page]
})
return observableCombineLatest(community.subcommunities, community.collections).pipe(
map(([subCommunities, collections]) => {
/*if this is a community, we also need to show the direct children*/
return [community, ...subCommunities.payload.page, ...collections.payload.page]
})
);
} else {
return Observable.of([dsoRD.payload]);
return observableOf([dsoRD.payload]);
}
}
));
@@ -291,13 +288,13 @@ export class SearchService implements OnDestroy {
* @returns {Observable<ViewMode>} The current view mode
*/
getViewMode(): Observable<ViewMode> {
return this.route.queryParams.map((params) => {
return this.route.queryParams.pipe(map((params) => {
if (isNotEmpty(params.view) && hasValue(params.view)) {
return params.view;
} else {
return ViewMode.List;
}
});
}));
}
/**

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -12,6 +12,7 @@ import { PageNotFoundComponent } from './pagenotfound/pagenotfound.component';
{ path: 'collections', loadChildren: './+collection-page/collection-page.module#CollectionPageModule' },
{ path: 'items', loadChildren: './+item-page/item-page.module#ItemPageModule' },
{ path: 'search', loadChildren: './+search-page/search-page.module#SearchPageModule' },
{ path: 'browse', loadChildren: './+browse-by/browse-by.module#BrowseByModule' },
{ path: 'admin', loadChildren: './+admin/admin.module#AdminModule' },
{ path: 'login', loadChildren: './+login-page/login-page.module#LoginPageModule' },
{ path: 'logout', loadChildren: './+logout-page/logout-page.module#LogoutPageModule' },

View File

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

View File

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

View File

@@ -7,7 +7,6 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
import { EffectsModule } from '@ngrx/effects';
import { RouterStateSerializer, StoreRouterConnectingModule } from '@ngrx/router-store';
import { META_REDUCERS, MetaReducer, StoreModule } from '@ngrx/store';
import { StoreDevtoolsModule } from '@ngrx/store-devtools';
import { TranslateModule } from '@ngx-translate/core';
@@ -47,10 +46,6 @@ export function getMetaReducers(config: GlobalConfig): Array<MetaReducer<AppStat
const DEV_MODULES: any[] = [];
if (!ENV_CONFIG.production) {
DEV_MODULES.push(StoreDevtoolsModule.instrument({ maxAge: 500 }));
}
@NgModule({
imports: [
CommonModule,

View File

@@ -1,14 +1,15 @@
import { AuthType } from './auth-type';
import { GenericConstructor } from '../shared/generic-constructor';
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model';
import { NormalizedEpersonModel } from '../eperson/models/NormalizedEperson.model';
import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model';
import { NormalizedObject } from '../cache/models/normalized-object.model';
import { EPerson } from '../eperson/models/eperson.model';
export class AuthObjectFactory {
public static getConstructor(type): GenericConstructor<NormalizedDSpaceObject> {
public static getConstructor(type): GenericConstructor<NormalizedObject> {
switch (type) {
case AuthType.Eperson: {
return NormalizedEpersonModel
case AuthType.EPerson: {
return NormalizedEPerson
}
case AuthType.Status: {

View File

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

View File

@@ -1,20 +1,18 @@
import { AuthStatusResponse } from '../cache/response-cache.models';
import { AuthStatusResponse } from '../cache/response.models';
import { ObjectCacheService } from '../cache/object-cache.service';
import { GlobalConfig } from '../../../config/global-config.interface';
import { Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { AuthStatus } from './models/auth-status.model';
import { AuthResponseParsingService } from './auth-response-parsing.service';
import { AuthGetRequest, AuthPostRequest } from '../data/request.models';
import { MockStore } from '../../shared/testing/mock-store';
import { ObjectCacheState } from '../cache/object-cache.reducer';
describe('ConfigResponseParsingService', () => {
describe('AuthResponseParsingService', () => {
let service: AuthResponseParsingService;
const EnvConfig = {} as GlobalConfig;
const store = {} as Store<CoreState>;
const objectCacheService = new ObjectCacheService(store);
const EnvConfig = { cache: { msToLive: 1000 } } as any;
const store = new MockStore<ObjectCacheState>({});
const objectCacheService = new ObjectCacheService(store as any);
beforeEach(() => {
service = new AuthResponseParsingService(EnvConfig, objectCacheService);
@@ -86,13 +84,19 @@ describe('ConfigResponseParsingService', () => {
type: 'eperson',
uuid: '4dc70ab5-cd73-492f-b007-3179d2d9296b',
_links: {
self: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b'
self: {
href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b'
}
}
}
},
_links: {
eperson: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b',
self: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status'
eperson: {
href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/eperson/epersons/4dc70ab5-cd73-492f-b007-3179d2d9296b'
},
self: {
href: 'https://hasselt-dspace.dev01.4science.it/dspace-spring-rest/api/authn/status'
}
}
},
statusCode: '200'

View File

@@ -2,7 +2,7 @@ import { Inject, Injectable } from '@angular/core';
import { AuthObjectFactory } from './auth-object-factory';
import { BaseResponseParsingService } from '../data/base-response-parsing.service';
import { AuthStatusResponse, RestResponse } from '../cache/response-cache.models';
import { AuthStatusResponse, RestResponse } from '../cache/response.models';
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
import { GLOBAL_CONFIG } from '../../../config';
import { GlobalConfig } from '../../../config/global-config.interface';
@@ -12,22 +12,22 @@ import { ResponseParsingService } from '../data/parsing.service';
import { RestRequest } from '../data/request.models';
import { AuthType } from './auth-type';
import { AuthStatus } from './models/auth-status.model';
import { NormalizedAuthStatus } from './models/normalized-auth-status.model';
@Injectable()
export class AuthResponseParsingService extends BaseResponseParsingService implements ResponseParsingService {
protected objectFactory = AuthObjectFactory;
protected toCache = false;
protected toCache = true;
constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig,
protected objectCache: ObjectCacheService,) {
protected objectCache: ObjectCacheService) {
super();
}
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
if (isNotEmpty(data.payload) && isNotEmpty(data.payload._links) && (data.statusCode === '200' || data.statusCode === 'OK')) {
const response: AuthStatus = this.process<AuthStatus, AuthType>(data.payload, request.href);
response.eperson = data.payload._embedded.eperson;
const response = this.process<NormalizedAuthStatus, AuthType>(data.payload, request.href);
return new AuthStatusResponse(response, data.statusCode);
} else {
return new AuthStatusResponse(data.payload as AuthStatus, data.statusCode);

View File

@@ -1,4 +1,4 @@
export enum AuthType {
Eperson = 'eperson',
EPerson = 'eperson',
Status = 'status'
}

View File

@@ -5,7 +5,7 @@ import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type';
// import models
import { Eperson } from '../eperson/models/eperson.model';
import { EPerson } from '../eperson/models/eperson.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
export const AuthActionTypes = {
@@ -76,10 +76,10 @@ export class AuthenticatedSuccessAction implements Action {
payload: {
authenticated: boolean;
authToken: AuthTokenInfo;
user: Eperson
user: EPerson
};
constructor(authenticated: boolean, authToken: AuthTokenInfo, user: Eperson) {
constructor(authenticated: boolean, authToken: AuthTokenInfo, user: EPerson) {
this.payload = { authenticated, authToken, user };
}
}
@@ -250,9 +250,9 @@ export class RefreshTokenErrorAction implements Action {
*/
export class RegistrationAction implements Action {
public type: string = AuthActionTypes.REGISTRATION;
payload: Eperson;
payload: EPerson;
constructor(user: Eperson) {
constructor(user: EPerson) {
this.payload = user;
}
}
@@ -278,9 +278,9 @@ export class RegistrationErrorAction implements Action {
*/
export class RegistrationSuccessAction implements Action {
public type: string = AuthActionTypes.REGISTRATION_SUCCESS;
payload: Eperson;
payload: EPerson;
constructor(user: Eperson) {
constructor(user: EPerson) {
this.payload = user;
}
}

View File

@@ -4,8 +4,7 @@ import { provideMockActions } from '@ngrx/effects/testing';
import { Store } from '@ngrx/store';
import { cold, hot } from 'jasmine-marbles';
import { Observable } from 'rxjs/Observable';
import 'rxjs/add/observable/of'
import { Observable, of as observableOf, throwError as observableThrow } from 'rxjs';
import { AuthEffects } from './auth.effects';
import {
@@ -25,22 +24,26 @@ import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
import { AuthService } from './auth.service';
import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer';
import { EpersonMock } from '../../shared/testing/eperson-mock';
import { EPersonMock } from '../../shared/testing/eperson-mock';
describe('AuthEffects', () => {
let authEffects: AuthEffects;
let actions: Observable<any>;
const authServiceStub = new AuthServiceStub();
let authServiceStub;
const store: Store<TruncatablesState> = jasmine.createSpyObj('store', {
/* tslint:disable:no-empty */
dispatch: {},
/* tslint:enable:no-empty */
select: Observable.of(true)
select: observableOf(true)
});
const token = authServiceStub.getToken();
let token;
function init() {
authServiceStub = new AuthServiceStub();
token = authServiceStub.getToken();
}
beforeEach(() => {
init();
TestBed.configureTestingModule({
providers: [
AuthEffects,
@@ -72,7 +75,7 @@ describe('AuthEffects', () => {
describe('when credentials are wrong', () => {
it('should return a AUTHENTICATE_ERROR action in response to a AUTHENTICATE action', () => {
spyOn((authEffects as any).authService, 'authenticate').and.returnValue(Observable.throw(new Error('Message Error test')));
spyOn((authEffects as any).authService, 'authenticate').and.returnValue(observableThrow(new Error('Message Error test')));
actions = hot('--a-', {
a: {
@@ -105,7 +108,7 @@ describe('AuthEffects', () => {
it('should return a AUTHENTICATED_SUCCESS action in response to a AUTHENTICATED action', () => {
actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}});
const expected = cold('--b-', {b: new AuthenticatedSuccessAction(true, token, EpersonMock)});
const expected = cold('--b-', {b: new AuthenticatedSuccessAction(true, token, EPersonMock)});
expect(authEffects.authenticated$).toBeObservable(expected);
});
@@ -113,7 +116,7 @@ describe('AuthEffects', () => {
describe('when token is not valid', () => {
it('should return a AUTHENTICATED_ERROR action in response to a AUTHENTICATED action', () => {
spyOn((authEffects as any).authService, 'authenticatedUser').and.returnValue(Observable.throw(new Error('Message Error test')));
spyOn((authEffects as any).authService, 'authenticatedUser').and.returnValue(observableThrow(new Error('Message Error test')));
actions = hot('--a-', {a: {type: AuthActionTypes.AUTHENTICATED, payload: token}});
@@ -139,7 +142,7 @@ describe('AuthEffects', () => {
describe('when check token failed', () => {
it('should return a CHECK_AUTHENTICATION_TOKEN_ERROR action in response to a CHECK_AUTHENTICATION_TOKEN action', () => {
spyOn((authEffects as any).authService, 'hasValidAuthenticationToken').and.returnValue(Observable.throw(''));
spyOn((authEffects as any).authService, 'hasValidAuthenticationToken').and.returnValue(observableThrow(''));
actions = hot('--a-', {a: {type: AuthActionTypes.CHECK_AUTHENTICATION_TOKEN, payload: token}});
@@ -165,7 +168,7 @@ describe('AuthEffects', () => {
describe('when refresh token failed', () => {
it('should return a REFRESH_TOKEN_ERROR action in response to a REFRESH_TOKEN action', () => {
spyOn((authEffects as any).authService, 'refreshAuthenticationToken').and.returnValue(Observable.throw(''));
spyOn((authEffects as any).authService, 'refreshAuthenticationToken').and.returnValue(observableThrow(''));
actions = hot('--a-', {a: {type: AuthActionTypes.REFRESH_TOKEN, payload: token}});
@@ -191,7 +194,7 @@ describe('AuthEffects', () => {
describe('when refresh token failed', () => {
it('should return a REFRESH_TOKEN_ERROR action in response to a LOG_OUT action', () => {
spyOn((authEffects as any).authService, 'logout').and.returnValue(Observable.throw(new Error('Message Error test')));
spyOn((authEffects as any).authService, 'logout').and.returnValue(observableThrow(new Error('Message Error test')));
actions = hot('--a-', {a: {type: AuthActionTypes.LOG_OUT, payload: token}});

View File

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

View File

@@ -4,15 +4,15 @@ import { HTTP_INTERCEPTORS } from '@angular/common/http';
import { Router } from '@angular/router';
import { Store } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { of as observableOf } from 'rxjs';
import { AuthInterceptor } from './auth.interceptor';
import { AuthService } from './auth.service';
import { DSpaceRESTv2Service } from '../dspace-rest-v2/dspace-rest-v2.service';
import { RestRequestMethod } from '../data/request.models';
import { RouterStub } from '../../shared/testing/router-stub';
import { TruncatablesState } from '../../shared/truncatable/truncatable.reducer';
import { AuthServiceStub } from '../../shared/testing/auth-service-stub';
import { RestRequestMethod } from '../data/rest-request-method';
describe(`AuthInterceptor`, () => {
let service: DSpaceRESTv2Service;
@@ -23,7 +23,7 @@ describe(`AuthInterceptor`, () => {
/* tslint:disable:no-empty */
dispatch: {},
/* tslint:enable:no-empty */
select: Observable.of(true)
select: observableOf(true)
});
beforeEach(() => {
@@ -49,7 +49,7 @@ describe(`AuthInterceptor`, () => {
describe('when has a valid token', () => {
it('should not add an Authorization header when were sending a HTTP request to \'authn\' endpoint', () => {
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/authn/login', 'password=password&user=user').subscribe((response) => {
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/authn/login', 'password=password&user=user').subscribe((response) => {
expect(response).toBeTruthy();
});
@@ -60,7 +60,7 @@ describe(`AuthInterceptor`, () => {
});
it('should add an Authorization header when were sending a HTTP request to \'authn\' endpoint', () => {
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'test').subscribe((response) => {
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'test').subscribe((response) => {
expect(response).toBeTruthy();
});
@@ -85,11 +85,11 @@ describe(`AuthInterceptor`, () => {
it('should redirect to login', () => {
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user').subscribe((response) => {
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user').subscribe((response) => {
expect(response).toBeTruthy();
});
service.request(RestRequestMethod.Post, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user');
service.request(RestRequestMethod.POST, 'dspace-spring-rest/api/submission/workspaceitems', 'password=password&user=user');
httpMock.expectNone('dspace-spring-rest/api/submission/workspaceitems');
});

View File

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

View File

@@ -21,7 +21,7 @@ import {
SetRedirectUrlAction
} from './auth.actions';
import { AuthTokenInfo } from './models/auth-token-info.model';
import { EpersonMock } from '../../shared/testing/eperson-mock';
import { EPersonMock } from '../../shared/testing/eperson-mock';
describe('authReducer', () => {
@@ -107,7 +107,7 @@ describe('authReducer', () => {
loading: true,
info: undefined
};
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EpersonMock);
const action = new AuthenticatedSuccessAction(true, mockTokenInfo, EPersonMock);
const newState = authReducer(initialState, action);
state = {
authenticated: true,
@@ -116,7 +116,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock
user: EPersonMock
};
expect(newState).toEqual(state);
});
@@ -182,7 +182,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock
user: EPersonMock
};
const action = new LogOutAction();
@@ -199,7 +199,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock
user: EPersonMock
};
const action = new LogOutSuccessAction();
@@ -225,7 +225,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock
user: EPersonMock
};
const action = new LogOutErrorAction(mockError);
@@ -237,7 +237,7 @@ describe('authReducer', () => {
error: 'Test error message',
loading: false,
info: undefined,
user: EpersonMock
user: EPersonMock
};
expect(newState).toEqual(state);
});
@@ -250,7 +250,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock
user: EPersonMock
};
const newTokenInfo = new AuthTokenInfo('Refreshed token');
const action = new RefreshTokenAction(newTokenInfo);
@@ -262,7 +262,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock,
user: EPersonMock,
refreshing: true
};
expect(newState).toEqual(state);
@@ -276,7 +276,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock,
user: EPersonMock,
refreshing: true
};
const newTokenInfo = new AuthTokenInfo('Refreshed token');
@@ -289,7 +289,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock,
user: EPersonMock,
refreshing: false
};
expect(newState).toEqual(state);
@@ -303,7 +303,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock,
user: EPersonMock,
refreshing: true
};
const action = new RefreshTokenErrorAction();
@@ -329,7 +329,7 @@ describe('authReducer', () => {
error: undefined,
loading: false,
info: undefined,
user: EpersonMock
user: EPersonMock
};
state = {

View File

@@ -12,7 +12,7 @@ import {
SetRedirectUrlAction
} from './auth.actions';
// import models
import { Eperson } from '../eperson/models/eperson.model';
import { EPerson } from '../eperson/models/eperson.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
/**
@@ -46,7 +46,7 @@ export interface AuthState {
refreshing?: boolean;
// the authenticated user
user?: Eperson;
user?: EPerson;
}
/**

View File

@@ -3,9 +3,8 @@ import { CommonModule } from '@angular/common';
import { ActivatedRoute, Router } from '@angular/router';
import { Store, StoreModule } from '@ngrx/store';
import { Observable } from 'rxjs/Observable';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import 'rxjs/add/observable/of';
import { of as observableOf } from 'rxjs';
import { authReducer, AuthState } from './auth.reducer';
import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service';
@@ -18,49 +17,64 @@ import { AuthRequestServiceStub } from '../../shared/testing/auth-request-servic
import { AuthRequestService } from './auth-request.service';
import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo } from './models/auth-token-info.model';
import { Eperson } from '../eperson/models/eperson.model';
import { EpersonMock } from '../../shared/testing/eperson-mock';
import { EPerson } from '../eperson/models/eperson.model';
import { EPersonMock } from '../../shared/testing/eperson-mock';
import { AppState } from '../../app.reducer';
import { ClientCookieService } from '../../shared/services/client-cookie.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
describe('AuthService test', () => {
const mockStore: Store<AuthState> = jasmine.createSpyObj('store', {
dispatch: {},
select: Observable.of(true)
pipe: observableOf(true)
});
let authService: AuthService;
const authRequest = new AuthRequestServiceStub();
let authRequest;
const window = new NativeWindowRef();
const routerStub = new RouterStub();
const routeStub = new ActivatedRouteStub();
let routeStub;
let storage: CookieService;
const token: AuthTokenInfo = new AuthTokenInfo('test_token');
token.expires = Date.now() + (1000 * 60 * 60);
let authenticatedState = {
authenticated: true,
loaded: true,
loading: false,
authToken: token,
user: EpersonMock
};
let token: AuthTokenInfo;
let authenticatedState;
const rdbService = getMockRemoteDataBuildService();
function init() {
token = new AuthTokenInfo('test_token');
token.expires = Date.now() + (1000 * 60 * 60);
authenticatedState = {
authenticated: true,
loaded: true,
loading: false,
authToken: token,
user: EPersonMock
};
authRequest = new AuthRequestServiceStub();
routeStub = new ActivatedRouteStub();
}
beforeEach(() => {
init();
});
describe('', () => {
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
StoreModule.forRoot({authReducer}),
StoreModule.forRoot({ authReducer }),
],
declarations: [],
providers: [
{provide: AuthRequestService, useValue: authRequest},
{provide: NativeWindowService, useValue: window},
{provide: REQUEST, useValue: {}},
{provide: Router, useValue: routerStub},
{provide: ActivatedRoute, useValue: routeStub},
{ provide: AuthRequestService, useValue: authRequest },
{ provide: NativeWindowService, useValue: window },
{ provide: REQUEST, useValue: {} },
{ provide: Router, useValue: routerStub },
{ provide: ActivatedRoute, useValue: routeStub },
{provide: Store, useValue: mockStore},
{ provide: RemoteDataBuildService, useValue: rdbService },
CookieService,
AuthService
],
@@ -79,7 +93,7 @@ describe('AuthService test', () => {
});
it('should return the authenticated user object when user token is valid', () => {
authService.authenticatedUser(new AuthTokenInfo('test_token')).subscribe((user: Eperson) => {
authService.authenticatedUser(new AuthTokenInfo('test_token')).subscribe((user: EPerson) => {
expect(user).toBeDefined();
});
});
@@ -113,14 +127,16 @@ describe('AuthService test', () => {
describe('', () => {
beforeEach(async(() => {
init();
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({authReducer})
StoreModule.forRoot({ authReducer })
],
providers: [
{provide: AuthRequestService, useValue: authRequest},
{provide: REQUEST, useValue: {}},
{provide: Router, useValue: routerStub},
{ provide: AuthRequestService, useValue: authRequest },
{ provide: REQUEST, useValue: {} },
{ provide: Router, useValue: routerStub },
{ provide: RemoteDataBuildService, useValue: rdbService },
CookieService
]
}).compileComponents();
@@ -132,7 +148,7 @@ describe('AuthService test', () => {
(state as any).core = Object.create({});
(state as any).core.auth = authenticatedState;
});
authService = new AuthService({}, window, authReqService, router, cookieService, store);
authService = new AuthService({}, window, authReqService, router, cookieService, store, rdbService);
}));
it('should return true when user is logged in', () => {
@@ -164,12 +180,12 @@ describe('AuthService test', () => {
beforeEach(async(() => {
TestBed.configureTestingModule({
imports: [
StoreModule.forRoot({authReducer})
StoreModule.forRoot({ authReducer })
],
providers: [
{provide: AuthRequestService, useValue: authRequest},
{provide: REQUEST, useValue: {}},
{provide: Router, useValue: routerStub},
{ provide: AuthRequestService, useValue: authRequest },
{ provide: REQUEST, useValue: {} },
{ provide: Router, useValue: routerStub },
ClientCookieService,
CookieService
]
@@ -184,14 +200,14 @@ describe('AuthService test', () => {
loaded: true,
loading: false,
authToken: expiredToken,
user: EpersonMock
user: EPersonMock
};
store
.subscribe((state) => {
(state as any).core = Object.create({});
(state as any).core.auth = authenticatedState;
});
authService = new AuthService({}, window, authReqService, router, cookieService, store);
authService = new AuthService({}, window, authReqService, router, cookieService, store, rdbService);
storage = (authService as any).storage;
spyOn(storage, 'get');
spyOn(storage, 'remove');

View File

@@ -1,15 +1,24 @@
import { Observable, of as observableOf } from 'rxjs';
import {
distinctUntilChanged,
filter,
first,
map,
startWith,
switchMap,
take,
withLatestFrom
} from 'rxjs/operators';
import { Inject, Injectable } from '@angular/core';
import { PRIMARY_OUTLET, Router, UrlSegmentGroup, UrlTree } from '@angular/router';
import { HttpHeaders } from '@angular/common/http';
import { REQUEST } from '@nguniversal/express-engine/tokens';
import { RouterReducerState } from '@ngrx/router-store';
import { Store } from '@ngrx/store';
import { select, Store } from '@ngrx/store';
import { CookieAttributes } from 'js-cookie';
import { Observable } from 'rxjs/Observable';
import { map, withLatestFrom } from 'rxjs/operators';
import { Eperson } from '../eperson/models/eperson.model';
import { EPerson } from '../eperson/models/eperson.model';
import { AuthRequestService } from './auth-request.service';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
@@ -17,11 +26,18 @@ import { AuthStatus } from './models/auth-status.model';
import { AuthTokenInfo, TOKENITEM } from './models/auth-token-info.model';
import { isEmpty, isNotEmpty, isNotNull, isNotUndefined } from '../../shared/empty.util';
import { CookieService } from '../../shared/services/cookie.service';
import { getAuthenticationToken, getRedirectUrl, isAuthenticated, isTokenRefreshing } from './selectors';
import {
getAuthenticationToken,
getRedirectUrl,
isAuthenticated,
isTokenRefreshing
} from './selectors';
import { AppState, routerStateSelector } from '../../app.reducer';
import { ResetAuthenticationMessagesAction, SetRedirectUrlAction } from './auth.actions';
import { NativeWindowRef, NativeWindowService } from '../../shared/services/window.service';
import { Base64EncodeUrl } from '../../shared/utils/encode-decode.util';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model';
export const LOGIN_ROUTE = '/login';
export const LOGOUT_ROUTE = '/logout';
@@ -45,22 +61,27 @@ export class AuthService {
protected authRequestService: AuthRequestService,
protected router: Router,
protected storage: CookieService,
protected store: Store<AppState>) {
this.store.select(isAuthenticated)
.startWith(false)
.subscribe((authenticated: boolean) => this._authenticated = authenticated);
protected store: Store<AppState>,
protected rdbService: RemoteDataBuildService
) {
this.store.pipe(
select(isAuthenticated),
startWith(false)
).subscribe((authenticated: boolean) => this._authenticated = authenticated);
// If current route is different from the one setted in authentication guard
// and is not the login route, clear redirect url and messages
const routeUrl$ = this.store.select(routerStateSelector)
.filter((routerState: RouterReducerState) => isNotUndefined(routerState) && isNotUndefined(routerState.state))
.filter((routerState: RouterReducerState) => !this.isLoginRoute(routerState.state.url))
.map((routerState: RouterReducerState) => routerState.state.url);
const redirectUrl$ = this.store.select(getRedirectUrl).distinctUntilChanged();
const routeUrl$ = this.store.pipe(
select(routerStateSelector),
filter((routerState: RouterReducerState) => isNotUndefined(routerState) && isNotUndefined(routerState.state)),
filter((routerState: RouterReducerState) => !this.isLoginRoute(routerState.state.url)),
map((routerState: RouterReducerState) => routerState.state.url)
);
const redirectUrl$ = this.store.pipe(select(getRedirectUrl), distinctUntilChanged());
routeUrl$.pipe(
withLatestFrom(redirectUrl$),
map(([routeUrl, redirectUrl]) => [routeUrl, redirectUrl])
).filter(([routeUrl, redirectUrl]) => isNotEmpty(redirectUrl) && (routeUrl !== redirectUrl))
).pipe(filter(([routeUrl, redirectUrl]) => isNotEmpty(redirectUrl) && (routeUrl !== redirectUrl)))
.subscribe(() => {
this.clearRedirectUrl();
});
@@ -93,14 +114,15 @@ export class AuthService {
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
options.headers = headers;
return this.authRequestService.postToEndpoint('login', body, options)
.map((status: AuthStatus) => {
return this.authRequestService.postToEndpoint('login', body, options).pipe(
map((status: AuthStatus) => {
console.log('yey response');
if (status.authenticated) {
return status;
} else {
throw(new Error('Invalid email or password'));
}
})
}))
}
@@ -109,28 +131,33 @@ export class AuthService {
* @returns {Observable<boolean>}
*/
public isAuthenticated(): Observable<boolean> {
return this.store.select(isAuthenticated);
return this.store.pipe(select(isAuthenticated));
}
/**
* Returns the authenticated user
* @returns {User}
*/
public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> {
public authenticatedUser(token: AuthTokenInfo): Observable<EPerson> {
// Determine if the user has an existing auth session on the server
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
headers = headers.append('Accept', 'application/json');
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
options.headers = headers;
return this.authRequestService.getRequest('status', options)
.map((status: AuthStatus) => {
return this.authRequestService.getRequest('status', options).pipe(
switchMap((status: AuthStatus) => {
if (status.authenticated) {
return status.eperson;
// TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole...
// Review when https://jira.duraspace.org/browse/DS-4006 is fixed
// See https://github.com/DSpace/dspace-angular/issues/292
const person$ = this.rdbService.buildSingle<NormalizedEPerson, EPerson>(status.eperson.toString());
return person$.pipe(map((eperson) => eperson.payload));
} else {
throw(new Error('Not authenticated'));
}
});
}))
}
/**
@@ -144,9 +171,10 @@ export class AuthService {
* Checks if token is present into storage and is not expired
*/
public hasValidAuthenticationToken(): Observable<AuthTokenInfo> {
return this.store.select(getAuthenticationToken)
.take(1)
.map((authTokenInfo: AuthTokenInfo) => {
return this.store.pipe(
select(getAuthenticationToken),
take(1),
map((authTokenInfo: AuthTokenInfo) => {
let token: AuthTokenInfo;
// Retrieve authentication token info and check if is valid
token = isNotEmpty(authTokenInfo) ? authTokenInfo : this.storage.get(TOKENITEM);
@@ -155,7 +183,8 @@ export class AuthService {
} else {
throw false;
}
});
})
);
}
/**
@@ -167,14 +196,14 @@ export class AuthService {
headers = headers.append('Accept', 'application/json');
headers = headers.append('Authorization', `Bearer ${token.accessToken}`);
options.headers = headers;
return this.authRequestService.postToEndpoint('login', {}, options)
.map((status: AuthStatus) => {
return this.authRequestService.postToEndpoint('login', {}, options).pipe(
map((status: AuthStatus) => {
if (status.authenticated) {
return status.token;
} else {
throw(new Error('Not authenticated'));
}
});
}));
}
/**
@@ -188,12 +217,12 @@ export class AuthService {
* Create a new user
* @returns {User}
*/
public create(user: Eperson): Observable<Eperson> {
public create(user: EPerson): Observable<EPerson> {
// Normally you would do an HTTP request to POST the user
// details and then return the new user object
// but, let's just return the new user for this example.
// this._authenticated = true;
return Observable.of(user);
return observableOf(user);
}
/**
@@ -204,15 +233,15 @@ export class AuthService {
// Send a request that sign end the session
let headers = new HttpHeaders();
headers = headers.append('Content-Type', 'application/x-www-form-urlencoded');
const options: HttpOptions = Object.create({headers, responseType: 'text'});
return this.authRequestService.getRequest('logout', options)
.map((status: AuthStatus) => {
const options: HttpOptions = Object.create({ headers, responseType: 'text' });
return this.authRequestService.getRequest('logout', options).pipe(
map((status: AuthStatus) => {
if (!status.authenticated) {
return true;
} else {
throw(new Error('auth.errors.invalid-user'));
}
})
}))
}
@@ -233,7 +262,7 @@ export class AuthService {
*/
public getToken(): AuthTokenInfo {
let token: AuthTokenInfo;
this.store.select(getAuthenticationToken)
this.store.pipe(select(getAuthenticationToken))
.subscribe((authTokenInfo: AuthTokenInfo) => {
// Retrieve authentication token info and check if is valid
token = authTokenInfo || null;
@@ -246,9 +275,10 @@ export class AuthService {
* @returns {boolean}
*/
public isTokenExpiring(): Observable<boolean> {
return this.store.select(isTokenRefreshing)
.first()
.map((isRefreshing: boolean) => {
return this.store.pipe(
select(isTokenRefreshing),
first(),
map((isRefreshing: boolean) => {
if (this.isTokenExpired() || isRefreshing) {
return false;
} else {
@@ -256,6 +286,7 @@ export class AuthService {
return token.expires - (60 * 5 * 1000) < Date.now();
}
})
)
}
/**
@@ -279,7 +310,7 @@ export class AuthService {
// Set the cookie expire date
const expires = new Date(expireDate);
const options: CookieAttributes = {expires: expires};
const options: CookieAttributes = { expires: expires };
// Save cookie with the token
return this.storage.set(TOKENITEM, token, options);
@@ -324,8 +355,8 @@ export class AuthService {
* Redirect to the route navigated before the login
*/
public redirectToPreviousUrl() {
this.getRedirectUrl()
.first()
this.getRedirectUrl().pipe(
first())
.subscribe((redirectUrl) => {
if (isNotEmpty(redirectUrl)) {
this.clearRedirectUrl();
@@ -337,8 +368,12 @@ export class AuthService {
this.router.navigated = false;
const url = decodeURIComponent(redirectUrl);
this.router.navigateByUrl(url);
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
// this._window.nativeWindow.location.href = url;
} else {
this.router.navigate(['/']);
/* TODO Reenable hard redirect when REST API can handle x-forwarded-for, see https://github.com/DSpace/DSpace/pull/2207 */
// this._window.nativeWindow.location.href = '/';
}
})
@@ -359,9 +394,9 @@ export class AuthService {
getRedirectUrl(): Observable<string> {
const redirectUrl = this.storage.get(REDIRECT_COOKIE);
if (isNotEmpty(redirectUrl)) {
return Observable.of(redirectUrl);
return observableOf(redirectUrl);
} else {
return this.store.select(getRedirectUrl);
return this.store.pipe(select(getRedirectUrl));
}
}
@@ -374,7 +409,7 @@ export class AuthService {
// Set the cookie expire date
const expires = new Date(expireDate);
const options: CookieAttributes = {expires: expires};
const options: CookieAttributes = { expires: expires };
this.storage.set(REDIRECT_COOKIE, url, options);
this.store.dispatch(new SetRedirectUrlAction(isNotUndefined(url) ? url : ''));
}

View File

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

View File

@@ -1,7 +1,8 @@
import { AuthError } from './auth-error.model';
import { AuthTokenInfo } from './auth-token-info.model';
import { DSpaceObject } from '../../shared/dspace-object.model';
import { Eperson } from '../../eperson/models/eperson.model';
import { EPerson } from '../../eperson/models/eperson.model';
import { RemoteData } from '../../data/remote-data';
import { Observable } from 'rxjs';
export class AuthStatus {
@@ -13,7 +14,7 @@ export class AuthStatus {
error?: AuthError;
eperson: Eperson;
eperson: Observable<RemoteData<EPerson>>;
token?: AuthTokenInfo;

View File

@@ -1,12 +1,18 @@
import { AuthStatus } from './auth-status.model';
import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize';
import { mapsTo } from '../../cache/builders/build-decorators';
import { NormalizedDSpaceObject } from '../../cache/models/normalized-dspace-object.model';
import { Eperson } from '../../eperson/models/eperson.model';
import { mapsTo, relationship } from '../../cache/builders/build-decorators';
import { ResourceType } from '../../shared/resource-type';
import { NormalizedObject } from '../../cache/models/normalized-object.model';
import { IDToUUIDSerializer } from '../../cache/id-to-uuid-serializer';
@mapsTo(AuthStatus)
@inheritSerialization(NormalizedDSpaceObject)
export class NormalizedAuthStatus extends NormalizedDSpaceObject {
@inheritSerialization(NormalizedObject)
export class NormalizedAuthStatus extends NormalizedObject {
@autoserialize
id: string;
@autoserializeAs(new IDToUUIDSerializer('auth-status'), 'id')
uuid: string;
/**
* True if REST API is up and running, should never return false
@@ -20,7 +26,7 @@ export class NormalizedAuthStatus extends NormalizedDSpaceObject {
@autoserialize
authenticated: boolean;
@autoserializeAs(Eperson)
eperson: Eperson;
@relationship(ResourceType.EPerson, false)
@autoserialize
eperson: string;
}

View File

@@ -1,6 +1,7 @@
import { first, map, switchMap } from 'rxjs/operators';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Observable } from 'rxjs';
import { HttpHeaders } from '@angular/common/http';
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { AuthStatus } from './models/auth-status.model';
@@ -8,7 +9,8 @@ import { isNotEmpty } from '../../shared/empty.util';
import { AuthService } from './auth.service';
import { AuthTokenInfo } from './models/auth-token-info.model';
import { CheckAuthenticationTokenAction } from './auth.actions';
import { Eperson } from '../eperson/models/eperson.model';
import { EPerson } from '../eperson/models/eperson.model';
import { NormalizedEPerson } from '../eperson/models/normalized-eperson.model';
/**
* The auth service.
@@ -20,7 +22,7 @@ export class ServerAuthService extends AuthService {
* Returns the authenticated user
* @returns {User}
*/
public authenticatedUser(token: AuthTokenInfo): Observable<Eperson> {
public authenticatedUser(token: AuthTokenInfo): Observable<EPerson> {
// Determine if the user has an existing auth session on the server
const options: HttpOptions = Object.create({});
let headers = new HttpHeaders();
@@ -32,14 +34,21 @@ export class ServerAuthService extends AuthService {
headers = headers.append('X-Forwarded-For', clientIp);
options.headers = headers;
return this.authRequestService.getRequest('status', options)
.map((status: AuthStatus) => {
return this.authRequestService.getRequest('status', options).pipe(
switchMap((status: AuthStatus) => {
if (status.authenticated) {
return status.eperson;
// TODO this should be cleaned up, AuthStatus could be parsed by the RemoteDataService as a whole...
const person$ = this.rdbService.buildSingle<NormalizedEPerson, EPerson>(status.eperson.toString());
// person$.subscribe(() => console.log('test'));
return person$.pipe(
map((eperson) => eperson.payload)
);
} else {
throw(new Error('Not authenticated'));
}
});
}));
}
/**
@@ -53,8 +62,8 @@ export class ServerAuthService extends AuthService {
* Redirect to the route navigated before the login
*/
public redirectToPreviousUrl() {
this.getRedirectUrl()
.first()
this.getRedirectUrl().pipe(
first())
.subscribe((redirectUrl) => {
if (isNotEmpty(redirectUrl)) {
// override the route reuse strategy

View File

@@ -1,20 +1,19 @@
import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { TestScheduler } from 'rxjs/Rx';
import { TestScheduler } from 'rxjs/testing';
import { getMockRemoteDataBuildService } from '../../shared/mocks/mock-remote-data-build.service';
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { ResponseCacheService } from '../cache/response-cache.service';
import { BrowseEndpointRequest, BrowseEntriesRequest } from '../data/request.models';
import { BrowseEndpointRequest, BrowseEntriesRequest, BrowseItemsRequest } from '../data/request.models';
import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseService } from './browse.service';
import { RequestEntry } from '../data/request.reducer';
import { of as observableOf } from 'rxjs';
describe('BrowseService', () => {
let scheduler: TestScheduler;
let service: BrowseService;
let responseCache: ResponseCacheService;
let requestService: RequestService;
let rdbService: RemoteDataBuildService;
@@ -79,22 +78,14 @@ describe('BrowseService', () => {
})
];
function initMockResponseCacheService(isSuccessful: boolean) {
const rcs = getMockResponseCacheService();
(rcs.get as any).and.returnValue(cold('b-', {
b: {
response: {
isSuccessful,
payload: browseDefinitions,
}
}
}));
return rcs;
}
const getRequestEntry$ = (successful: boolean) => {
return observableOf({
response: { isSuccessful: successful, payload: browseDefinitions } as any
} as RequestEntry)
};
function initTestService() {
return new BrowseService(
responseCache,
requestService,
halService,
rdbService
@@ -108,8 +99,7 @@ describe('BrowseService', () => {
describe('getBrowseDefinitions', () => {
beforeEach(() => {
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService();
requestService = getMockRequestService(getRequestEntry$(true));
rdbService = getMockRemoteDataBuildService();
service = initTestService();
spyOn(halService, 'getEndpoint').and
@@ -143,10 +133,11 @@ describe('BrowseService', () => {
});
describe('getBrowseEntriesFor', () => {
describe('getBrowseEntriesFor and getBrowseItemsFor', () => {
const mockAuthorName = 'Donald Smith';
beforeEach(() => {
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService();
requestService = getMockRequestService(getRequestEntry$(true));
rdbService = getMockRemoteDataBuildService();
service = initTestService();
spyOn(service, 'getBrowseDefinitions').and
@@ -156,7 +147,7 @@ describe('BrowseService', () => {
spyOn(rdbService, 'toRemoteDataObservable').and.callThrough();
});
describe('when called with a valid browse definition id', () => {
describe('when getBrowseEntriesFor is called with a valid browse definition id', () => {
it('should configure a new BrowseEntriesRequest', () => {
const expected = new BrowseEntriesRequest(requestService.generateRequestId(), browseDefinitions[1]._links.entries);
@@ -175,7 +166,26 @@ describe('BrowseService', () => {
});
describe('when called with an invalid browse definition id', () => {
describe('when getBrowseItemsFor is called with a valid browse definition id', () => {
it('should configure a new BrowseItemsRequest', () => {
const expected = new BrowseItemsRequest(requestService.generateRequestId(), browseDefinitions[1]._links.items + '?filterValue=' + mockAuthorName);
scheduler.schedule(() => service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName).subscribe());
scheduler.flush();
expect(requestService.configure).toHaveBeenCalledWith(expected);
});
it('should call RemoteDataBuildService to create the RemoteData Observable', () => {
service.getBrowseItemsFor(browseDefinitions[1].id, mockAuthorName);
expect(rdbService.toRemoteDataObservable).toHaveBeenCalled();
});
});
describe('when getBrowseEntriesFor is called with an invalid browse definition id', () => {
it('should throw an Error', () => {
const definitionID = 'invalidID';
@@ -184,14 +194,23 @@ describe('BrowseService', () => {
expect(service.getBrowseEntriesFor(definitionID)).toBeObservable(expected);
});
});
describe('when getBrowseItemsFor is called with an invalid browse definition id', () => {
it('should throw an Error', () => {
const definitionID = 'invalidID';
const expected = cold('--#-', undefined, new Error(`No metadata browse definition could be found for id '${definitionID}'`))
expect(service.getBrowseItemsFor(definitionID, mockAuthorName)).toBeObservable(expected);
});
});
});
describe('getBrowseURLFor', () => {
describe('if getBrowseDefinitions fires', () => {
beforeEach(() => {
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService();
requestService = getMockRequestService(getRequestEntry$(true));
rdbService = getMockRemoteDataBuildService();
service = initTestService();
spyOn(service, 'getBrowseDefinitions').and
@@ -246,8 +265,7 @@ describe('BrowseService', () => {
describe('if getBrowseDefinitions doesn\'t fire', () => {
it('should return undefined', () => {
responseCache = initMockResponseCacheService(true);
requestService = getMockRequestService();
requestService = getMockRequestService(getRequestEntry$(true));
rdbService = getMockRemoteDataBuildService();
service = initTestService();
spyOn(service, 'getBrowseDefinitions').and

View File

@@ -1,5 +1,5 @@
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { Observable } from 'rxjs';
import { distinctUntilChanged, map, startWith } from 'rxjs/operators';
import {
ensureArrayHasValue,
@@ -11,12 +11,15 @@ import {
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { SortOptions } from '../cache/models/sort-options.model';
import { GenericSuccessResponse } from '../cache/response-cache.models';
import { ResponseCacheEntry } from '../cache/response-cache.reducer';
import { ResponseCacheService } from '../cache/response-cache.service';
import { GenericSuccessResponse } from '../cache/response.models';
import { PaginatedList } from '../data/paginated-list';
import { RemoteData } from '../data/remote-data';
import { BrowseEndpointRequest, BrowseEntriesRequest, RestRequest } from '../data/request.models';
import {
BrowseEndpointRequest,
BrowseEntriesRequest,
BrowseItemsRequest,
RestRequest
} from '../data/request.models';
import { RequestService } from '../data/request.service';
import { BrowseDefinition } from '../shared/browse-definition.model';
import { BrowseEntry } from '../shared/browse-entry.model';
@@ -24,11 +27,13 @@ import { HALEndpointService } from '../shared/hal-endpoint.service';
import {
configureRequest,
filterSuccessfulResponses,
getBrowseDefinitionLinks,
getRemoteDataPayload,
getRequestFromSelflink,
getResponseFromSelflink
getRequestFromSelflink
} from '../shared/operators';
import { URLCombiner } from '../url-combiner/url-combiner';
import { Item } from '../shared/item.model';
import { DSpaceObject } from '../shared/dspace-object.model';
@Injectable()
export class BrowseService {
@@ -48,7 +53,6 @@ export class BrowseService {
}
constructor(
protected responseCache: ResponseCacheService,
protected requestService: RequestService,
protected halService: HALEndpointService,
private rdb: RemoteDataBuildService,
@@ -65,16 +69,16 @@ export class BrowseService {
const href$ = request$.pipe(map((request: RestRequest) => request.href));
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
const payload$ = responseCache$.pipe(
const payload$ = requestEntry$.pipe(
filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => entry.response),
map((response: GenericSuccessResponse<BrowseDefinition[]>) => response.payload),
ensureArrayHasValue(),
map((definitions: BrowseDefinition[]) => definitions
.map((definition: BrowseDefinition) => Object.assign(new BrowseDefinition(), definition))),
distinctUntilChanged()
);
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
}
getBrowseEntriesFor(definitionID: string, options: {
@@ -82,17 +86,7 @@ export class BrowseService {
sort?: SortOptions;
} = {}): Observable<RemoteData<PaginatedList<BrowseEntry>>> {
const request$ = this.getBrowseDefinitions().pipe(
getRemoteDataPayload(),
map((browseDefinitions: BrowseDefinition[]) => browseDefinitions
.find((def: BrowseDefinition) => def.id === definitionID && def.metadataBrowse === true)
),
map((def: BrowseDefinition) => {
if (isNotEmpty(def)) {
return def._links;
} else {
throw new Error(`No metadata browse definition could be found for id '${definitionID}'`);
}
}),
getBrowseDefinitionLinks(definitionID),
hasValueOperator(),
map((_links: any) => _links.entries),
hasValueOperator(),
@@ -118,16 +112,72 @@ export class BrowseService {
const href$ = request$.pipe(map((request: RestRequest) => request.href));
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
const payload$ = responseCache$.pipe(
const payload$ = requestEntry$.pipe(
filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => entry.response),
map((response: GenericSuccessResponse<BrowseEntry[]>) => new PaginatedList(response.pageInfo, response.payload)),
map((list: PaginatedList<BrowseEntry>) => Object.assign(list, {
page: list.page ? list.page.map((entry: BrowseEntry) => Object.assign(new BrowseEntry(), entry)) : list.page
})),
distinctUntilChanged()
);
return this.rdb.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
}
/**
* Get all items linked to a certain metadata value
* @param {string} definitionID definition ID to define the metadata-field (e.g. author)
* @param {string} filterValue metadata value to filter by (e.g. author's name)
* @param options Options to narrow down your search:
* { pagination: PaginationComponentOptions,
* sort: SortOptions }
* @returns {Observable<RemoteData<PaginatedList<Item>>>}
*/
getBrowseItemsFor(definitionID: string, filterValue: string, options: {
pagination?: PaginationComponentOptions;
sort?: SortOptions;
} = {}): Observable<RemoteData<PaginatedList<Item>>> {
const request$ = this.getBrowseDefinitions().pipe(
getBrowseDefinitionLinks(definitionID),
hasValueOperator(),
map((_links: any) => _links.items),
hasValueOperator(),
map((href: string) => {
const args = [];
if (isNotEmpty(options.sort)) {
args.push(`sort=${options.sort.field},${options.sort.direction}`);
}
if (isNotEmpty(options.pagination)) {
args.push(`page=${options.pagination.currentPage - 1}`);
args.push(`size=${options.pagination.pageSize}`);
}
if (isNotEmpty(filterValue)) {
args.push(`filterValue=${filterValue}`);
}
if (isNotEmpty(args)) {
href = new URLCombiner(href, `?${args.join('&')}`).toString();
}
return href;
}),
map((endpointURL: string) => new BrowseItemsRequest(this.requestService.generateRequestId(), endpointURL)),
configureRequest(this.requestService)
);
const href$ = request$.pipe(map((request: RestRequest) => request.href));
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
const payload$ = requestEntry$.pipe(
filterSuccessfulResponses(),
map((response: GenericSuccessResponse<Item[]>) => new PaginatedList(response.pageInfo, response.payload)),
map((list: PaginatedList<Item>) => Object.assign(list, {
page: list.page ? list.page.map((item: DSpaceObject) => Object.assign(new Item(), item)) : list.page
})),
distinctUntilChanged()
);
return this.rdb.toRemoteDataObservable(requestEntry$, payload$);
}
getBrowseURLFor(metadatumKey: string, linkPath: string): Observable<string> {

View File

@@ -1,6 +1,19 @@
import {
combineLatest as observableCombineLatest,
Observable,
of as observableOf,
race as observableRace
} from 'rxjs';
import { Injectable } from '@angular/core';
import { Observable } from 'rxjs/Observable';
import { distinctUntilChanged, flatMap, map, startWith } from 'rxjs/operators';
import {
distinctUntilChanged,
first,
flatMap,
map,
startWith,
switchMap,
takeUntil, tap
} from 'rxjs/operators';
import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util';
import { PaginatedList } from '../../data/paginated-list';
import { RemoteData } from '../../data/remote-data';
@@ -11,72 +24,63 @@ import { RequestService } from '../../data/request.service';
import { NormalizedObject } from '../models/normalized-object.model';
import { ObjectCacheService } from '../object-cache.service';
import { DSOSuccessResponse, ErrorResponse } from '../response-cache.models';
import { ResponseCacheEntry } from '../response-cache.reducer';
import { ResponseCacheService } from '../response-cache.service';
import { DSOSuccessResponse, ErrorResponse } from '../response.models';
import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators';
import { PageInfo } from '../../shared/page-info.model';
import {
filterSuccessfulResponses,
getRequestFromSelflink,
getResourceLinksFromResponse,
getResponseFromSelflink,
filterSuccessfulResponses
getResourceLinksFromResponse
} from '../../shared/operators';
import { ReplaySubject } from 'rxjs/ReplaySubject';
@Injectable()
export class RemoteDataBuildService {
constructor(protected objectCache: ObjectCacheService,
protected responseCache: ResponseCacheService,
protected requestService: RequestService) {
}
buildSingle<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<TDomain>> {
if (typeof href$ === 'string') {
href$ = Observable.of(href$);
href$ = observableOf(href$);
}
href$ = href$.multicast(new ReplaySubject(1)).refCount();
const requestHref$ = href$.pipe(flatMap((href: string) =>
this.objectCache.getRequestHrefBySelfLink(href)));
const requestEntry$ = Observable.race(
href$.pipe(getRequestFromSelflink(this.requestService)),
requestHref$.pipe(getRequestFromSelflink(this.requestService))
const requestHref$ = href$.pipe(
switchMap((href: string) =>
this.objectCache.getRequestHrefBySelfLink(href)),
);
const responseCache$ = Observable.race(
href$.pipe(getResponseFromSelflink(this.responseCache)),
requestHref$.pipe(getResponseFromSelflink(this.responseCache))
const requestEntry$ = observableRace(
href$.pipe(getRequestFromSelflink(this.requestService)),
requestHref$.pipe(getRequestFromSelflink(this.requestService)),
).pipe(
first()
);
// always use self link if that is cached, only if it isn't, get it via the response.
const payload$ =
Observable.combineLatest(
observableCombineLatest(
href$.pipe(
flatMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)),
startWith(undefined)
),
responseCache$.pipe(
switchMap((href: string) => this.objectCache.getBySelfLink<TNormalized>(href)),
startWith(undefined)),
requestEntry$.pipe(
getResourceLinksFromResponse(),
flatMap((resourceSelfLinks: string[]) => {
switchMap((resourceSelfLinks: string[]) => {
if (isNotEmpty(resourceSelfLinks)) {
return this.objectCache.getBySelfLink(resourceSelfLinks[0]);
} else {
return Observable.of(undefined);
return observableOf(undefined);
}
}),
distinctUntilChanged(),
startWith(undefined)
),
(fromSelfLink, fromResponse) => {
)
).pipe(
map(([fromSelfLink, fromResponse]) => {
if (hasValue(fromSelfLink)) {
return fromSelfLink;
} else {
return fromResponse;
}
}
).pipe(
}),
hasValueOperator(),
map((normalized: TNormalized) => {
return this.build<TNormalized, TDomain>(normalized);
@@ -84,21 +88,21 @@ export class RemoteDataBuildService {
startWith(undefined),
distinctUntilChanged()
);
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
return this.toRemoteDataObservable(requestEntry$, payload$);
}
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, responseCache$: Observable<ResponseCacheEntry>, payload$: Observable<T>) {
return Observable.combineLatest(requestEntry$, responseCache$.startWith(undefined), payload$,
(reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => {
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, payload$: Observable<T>) {
return observableCombineLatest(requestEntry$, payload$).pipe(
map(([reqEntry, payload]) => {
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
let isSuccessful: boolean;
let error: RemoteDataError;
if (hasValue(resEntry) && hasValue(resEntry.response)) {
isSuccessful = resEntry.response.isSuccessful;
const errorMessage = isSuccessful === false ? (resEntry.response as ErrorResponse).errorMessage : undefined;
if (hasValue(reqEntry) && hasValue(reqEntry.response)) {
isSuccessful = reqEntry.response.isSuccessful;
const errorMessage = isSuccessful === false ? (reqEntry.response as ErrorResponse).errorMessage : undefined;
if (hasValue(errorMessage)) {
error = new RemoteDataError(resEntry.response.statusCode, errorMessage);
error = new RemoteDataError(reqEntry.response.statusCode, errorMessage);
}
}
return new RemoteData(
@@ -108,37 +112,34 @@ export class RemoteDataBuildService {
error,
payload
);
});
})
);
}
buildList<TNormalized extends NormalizedObject, TDomain>(href$: string | Observable<string>): Observable<RemoteData<PaginatedList<TDomain>>> {
if (typeof href$ === 'string') {
href$ = Observable.of(href$);
href$ = observableOf(href$);
}
href$ = href$.shareReplay();
const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService));
const responseCache$ = href$.pipe(getResponseFromSelflink(this.responseCache));
const tDomainList$ = responseCache$.pipe(
const tDomainList$ = requestEntry$.pipe(
getResourceLinksFromResponse(),
flatMap((resourceUUIDs: string[]) => {
return this.objectCache.getList(resourceUUIDs)
.map((normList: TNormalized[]) => {
return this.objectCache.getList(resourceUUIDs).pipe(
map((normList: TNormalized[]) => {
return normList.map((normalized: TNormalized) => {
return this.build<TNormalized, TDomain>(normalized);
});
});
}));
}),
startWith([]),
distinctUntilChanged()
distinctUntilChanged(),
);
const pageInfo$ = responseCache$.pipe(
const pageInfo$ = requestEntry$.pipe(
filterSuccessfulResponses(),
map((entry: ResponseCacheEntry) => {
if (hasValue((entry.response as DSOSuccessResponse).pageInfo)) {
const resPageInfo = (entry.response as DSOSuccessResponse).pageInfo;
map((response: DSOSuccessResponse) => {
if (hasValue((response as DSOSuccessResponse).pageInfo)) {
const resPageInfo = (response as DSOSuccessResponse).pageInfo;
if (isNotEmpty(resPageInfo) && resPageInfo.currentPage >= 0) {
return Object.assign({}, resPageInfo, { currentPage: resPageInfo.currentPage + 1 });
} else {
@@ -146,18 +147,19 @@ export class RemoteDataBuildService {
}
}
})
);
);
const payload$ = Observable.combineLatest(tDomainList$, pageInfo$, (tDomainList, pageInfo) => {
return new PaginatedList(pageInfo, tDomainList);
});
const payload$ = observableCombineLatest(tDomainList$, pageInfo$).pipe(
map(([tDomainList, pageInfo]) => {
return new PaginatedList(pageInfo, tDomainList);
})
);
return this.toRemoteDataObservable(requestEntry$, responseCache$, payload$);
return this.toRemoteDataObservable(requestEntry$, payload$);
}
build<TNormalized, TDomain>(normalized: TNormalized): TDomain {
const links: any = {};
const relationships = getRelationships(normalized.constructor) || [];
relationships.forEach((relationship: string) => {
@@ -200,7 +202,6 @@ export class RemoteDataBuildService {
}
}
});
const domainModel = getMapsTo(normalized.constructor);
return Object.assign(new domainModel(), normalized, links);
}
@@ -208,12 +209,11 @@ export class RemoteDataBuildService {
aggregate<T>(input: Array<Observable<RemoteData<T>>>): Observable<RemoteData<T[]>> {
if (isEmpty(input)) {
return Observable.of(new RemoteData(false, false, true, null, []));
return observableOf(new RemoteData(false, false, true, null, []));
}
return Observable.combineLatest(
...input,
(...arr: Array<RemoteData<T>>) => {
return observableCombineLatest(...input).pipe(
map((arr) => {
const requestPending: boolean = arr
.map((d: RemoteData<T>) => d.isRequestPending)
.every((b: boolean) => b === true);
@@ -255,11 +255,11 @@ export class RemoteDataBuildService {
error,
payload
);
})
}))
}
aggregatePaginatedList<T>(input: Observable<RemoteData<T[]>>, pageInfo: PageInfo): Observable<RemoteData<PaginatedList<T>>> {
return input.map((rd) => Object.assign(rd, {payload: new PaginatedList(pageInfo, rd.payload)}));
return input.pipe(map((rd) => Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload) })));
}
}

View File

@@ -8,6 +8,8 @@ import { ResourceType } from '../../shared/resource-type';
import { NormalizedObject } from './normalized-object.model';
import { NormalizedBitstreamFormat } from './normalized-bitstream-format.model';
import { NormalizedResourcePolicy } from './normalized-resource-policy.model';
import { NormalizedEPerson } from '../../eperson/models/normalized-eperson.model';
import { NormalizedGroup } from '../../eperson/models/normalized-group.model';
export class NormalizedObjectFactory {
public static getConstructor(type: ResourceType): GenericConstructor<NormalizedObject> {
@@ -33,6 +35,12 @@ export class NormalizedObjectFactory {
case ResourceType.ResourcePolicy: {
return NormalizedResourcePolicy
}
case ResourceType.EPerson: {
return NormalizedEPerson
}
case ResourceType.Group: {
return NormalizedGroup
}
default: {
return undefined;
}

View File

@@ -2,6 +2,7 @@ import { Action } from '@ngrx/store';
import { type } from '../../shared/ngrx/type';
import { CacheableObject } from './object-cache.reducer';
import { Operation } from 'fast-json-patch';
/**
* The list of ObjectCacheAction type definitions
@@ -9,7 +10,9 @@ import { CacheableObject } from './object-cache.reducer';
export const ObjectCacheActionTypes = {
ADD: type('dspace/core/cache/object/ADD'),
REMOVE: type('dspace/core/cache/object/REMOVE'),
RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS')
RESET_TIMESTAMPS: type('dspace/core/cache/object/RESET_TIMESTAMPS'),
ADD_PATCH: type('dspace/core/cache/object/ADD_PATCH'),
APPLY_PATCH: type('dspace/core/cache/object/APPLY_PATCH')
};
/* tslint:disable:max-classes-per-file */
@@ -54,11 +57,11 @@ export class RemoveFromObjectCacheAction implements Action {
/**
* Create a new RemoveFromObjectCacheAction
*
* @param uuid
* the UUID of the object to remove
* @param href
* the unique href of the object to remove
*/
constructor(uuid: string) {
this.payload = uuid;
constructor(href: string) {
this.payload = href;
}
}
@@ -79,6 +82,48 @@ export class ResetObjectCacheTimestampsAction implements Action {
this.payload = newTimestamp;
}
}
/**
* An ngrx action to add new operations to a specified cached object
*/
export class AddPatchObjectCacheAction implements Action {
type = ObjectCacheActionTypes.ADD_PATCH;
payload: {
href: string,
operations: Operation[]
};
/**
* Create a new AddPatchObjectCacheAction
*
* @param href
* the unique href of the object that should be updated
* @param operations
* the list of operations to add
*/
constructor(href: string, operations: Operation[]) {
this.payload = { href, operations };
}
}
/**
* An ngrx action to apply all existing operations to a specified cached object
*/
export class ApplyPatchObjectCacheAction implements Action {
type = ObjectCacheActionTypes.APPLY_PATCH;
payload: string;
/**
* Create a new ApplyPatchObjectCacheAction
*
* @param href
* the unique href of the object that should be updated
*/
constructor(href: string) {
this.payload = href;
}
}
/* tslint:enable:max-classes-per-file */
/**
@@ -87,4 +132,6 @@ export class ResetObjectCacheTimestampsAction implements Action {
export type ObjectCacheAction
= AddToObjectCacheAction
| RemoveFromObjectCacheAction
| ResetObjectCacheTimestampsAction;
| ResetObjectCacheTimestampsAction
| AddPatchObjectCacheAction
| ApplyPatchObjectCacheAction;

View File

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

View File

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

View File

@@ -2,8 +2,11 @@ import * as deepFreeze from 'deep-freeze';
import { objectCacheReducer } from './object-cache.reducer';
import {
AddPatchObjectCacheAction,
AddToObjectCacheAction,
RemoveFromObjectCacheAction, ResetObjectCacheTimestampsAction
ApplyPatchObjectCacheAction,
RemoveFromObjectCacheAction,
ResetObjectCacheTimestampsAction
} from './object-cache.actions';
class NullAction extends RemoveFromObjectCacheAction {
@@ -26,7 +29,9 @@ describe('objectCacheReducer', () => {
},
timeAdded: new Date().getTime(),
msToLive: 900000,
requestHref: selfLink1
requestHref: selfLink1,
patches: [],
isDirty: false
},
[selfLink2]: {
data: {
@@ -35,7 +40,9 @@ describe('objectCacheReducer', () => {
},
timeAdded: new Date().getTime(),
msToLive: 900000,
requestHref: selfLink2
requestHref: selfLink2,
patches: [],
isDirty: false
}
};
deepFreeze(testState);
@@ -132,4 +139,16 @@ describe('objectCacheReducer', () => {
objectCacheReducer(testState, action);
});
it('should perform the ADD_PATCH action without affecting the previous state', () => {
const action = new AddPatchObjectCacheAction(selfLink1, [{ op: 'replace', path: '/name', value: 'random string' }]);
// testState has already been frozen above
objectCacheReducer(testState, action);
});
it('should perform the APPLY_PATCH action without affecting the previous state', () => {
const action = new ApplyPatchObjectCacheAction(selfLink1);
// testState has already been frozen above
objectCacheReducer(testState, action);
});
});

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