diff --git a/angular.json b/angular.json new file mode 100644 index 0000000000..336738fd6e --- /dev/null +++ b/angular.json @@ -0,0 +1,13 @@ +{ + "$schema": "./node_modules/@angular/cli/lib/config/schema.json", + "version": 1, + "cli": { + "defaultCollection": "@ngrx/schematics" + }, + "projects": { + "core": { + "root": "", + "projectType": "application" + } + } +} \ No newline at end of file diff --git a/package.json b/package.json index 874f4a6019..91117723d1 100644 --- a/package.json +++ b/package.json @@ -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 --mode production && webpack --env.aot --env.client --mode 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,31 +63,31 @@ "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", + "@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-moment": "^1.9.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.3", "cerialize": "0.1.18", @@ -103,6 +97,7 @@ "express": "4.16.2", "express-session": "1.15.6", "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 +107,117 @@ "methods": "1.1.2", "moment": "^2.22.1", "morgan": "1.9.0", + "ng-mocks": "^6.2.1", "ng2-file-upload": "1.2.1", "ng2-nouislider": "^1.7.11", - "ngx-infinite-scroll": "0.8.2", + "ngx-bootstrap": "^3.0.1", + "ngx-infinite-scroll": "6.0.1", + "ngx-moment": "^3.1.0", "ngx-pagination": "3.0.3", "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" } } diff --git a/rollup.config.js b/rollup.config.js index 8c8700d387..33e3ec3346 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -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() ] } diff --git a/spec-bundle.js b/spec-bundle.js index b9df9bec5e..aa46c35d14 100644 --- a/spec-bundle.js +++ b/spec-bundle.js @@ -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'); diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts index f720c336e5..b6e3b7e989 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts +++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.spec.ts @@ -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 }; diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts index d6c84ac858..6ba4e8146b 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts +++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts @@ -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'; diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts index e3b2e1f2c1..8b72afa083 100644 --- a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts +++ b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts @@ -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'); }); - }); diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts index 15dc6b0d80..c2f70eaa9e 100644 --- a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts +++ b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.ts @@ -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'; diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts index 7e6064ddff..96777116f4 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts @@ -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 }) }); diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts index 2f0bfdeddb..b2cc5129ce 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.ts @@ -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'; diff --git a/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.ts b/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.ts index 1553889741..813ee8a32f 100644 --- a/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.ts +++ b/src/app/+browse-by/+browse-by-author-page/browse-by-author-page.component.ts @@ -1,14 +1,13 @@ + +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 { Observable } from 'rxjs/Observable'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; -import { Subscription } from 'rxjs/Subscription'; import { ActivatedRoute } from '@angular/router'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { Metadatum } from '../../core/shared/metadatum.model'; import { BrowseService } from '../../core/browse/browse.service'; import { BrowseEntry } from '../../core/shared/browse-entry.model'; import { Item } from '../../core/shared/item.model'; @@ -47,7 +46,7 @@ export class BrowseByAuthorPageComponent implements OnInit { sort: this.sortConfig }); this.subs.push( - Observable.combineLatest( + observableCombineLatest( this.route.params, this.route.queryParams, (params, queryParams, ) => { diff --git a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts index 1759264e2a..e9127dbbab 100644 --- a/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts +++ b/src/app/+browse-by/+browse-by-title-page/browse-by-title-page.component.ts @@ -1,13 +1,13 @@ + +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 { Observable } from 'rxjs/Observable'; 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 { Subscription } from 'rxjs/Subscription'; import { ActivatedRoute, PRIMARY_OUTLET, UrlSegmentGroup } from '@angular/router'; import { hasValue } from '../../shared/empty.util'; import { Collection } from '../../core/shared/collection.model'; @@ -45,7 +45,7 @@ export class BrowseByTitlePageComponent implements OnInit { sort: this.sortConfig }); this.subs.push( - Observable.combineLatest( + observableCombineLatest( this.route.params, this.route.queryParams, (params, queryParams, ) => { diff --git a/src/app/+collection-page/collection-page.component.ts b/src/app/+collection-page/collection-page.component.ts index 89567c4a54..59cf83777f 100644 --- a/src/app/+collection-page/collection-page.component.ts +++ b/src/app/+collection-page/collection-page.component.ts @@ -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'; @@ -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) => rd.payload), filter((collection: Collection) => hasValue(collection)), @@ -75,8 +75,8 @@ export class CollectionPageComponent implements OnInit, OnDestroy { pagination: pagination, sort: this.sortConfig }); - })); - + }) + ); } updatePage(searchOptions) { diff --git a/src/app/+collection-page/collection-page.resolver.ts b/src/app/+collection-page/collection-page.resolver.ts index c049901bf2..d4835e2e14 100644 --- a/src/app/+collection-page/collection-page.resolver.ts +++ b/src/app/+collection-page/collection-page.resolver.ts @@ -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'; diff --git a/src/app/+community-page/community-page.component.ts b/src/app/+community-page/community-page.component.ts index 5fea9b01c9..a6e1d5376c 100644 --- a/src/app/+community-page/community-page.component.ts +++ b/src/app/+community-page/community-page.component.ts @@ -1,7 +1,8 @@ +import {mergeMap, filter, map} 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', @@ -35,11 +35,11 @@ export class CommunityPageComponent implements OnInit, OnDestroy { } ngOnInit(): void { - this.communityRD$ = this.route.data.map((data) => data.community); - this.logoRD$ = this.communityRD$ - .map((rd: RemoteData) => 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) => rd.payload), + filter((community: Community) => hasValue(community)), + mergeMap((community: Community) => community.logo)); } ngOnDestroy(): void { diff --git a/src/app/+community-page/community-page.resolver.ts b/src/app/+community-page/community-page.resolver.ts index 917f37a821..a32fe78bc5 100644 --- a/src/app/+community-page/community-page.resolver.ts +++ b/src/app/+community-page/community-page.resolver.ts @@ -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'; diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts index aed2b69a30..b8a5d60002 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.ts @@ -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'; diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts index 1915a8ce64..8e8c83ce5b 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts @@ -1,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'; diff --git a/src/app/+item-page/field-components/collections/collections.component.spec.ts b/src/app/+item-page/field-components/collections/collections.component.spec.ts index 871018a9d8..865ce78a39 100644 --- a/src/app/+item-page/field-components/collections/collections.component.spec.ts +++ b/src/app/+item-page/field-components/collections/collections.component.spec.ts @@ -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(() => { diff --git a/src/app/+item-page/field-components/collections/collections.component.ts b/src/app/+item-page/field-components/collections/collections.component.ts index 83bb0d464d..b33c5fd41b 100644 --- a/src/app/+item-page/field-components/collections/collections.component.ts +++ b/src/app/+item-page/field-components/collections/collections.component.ts @@ -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) => [rd.payload]); + this.collections = this.item.owner.pipe(map((rd: RemoteData) => [rd.payload])); } hasSucceeded() { - return this.item.owner.map((rd: RemoteData) => rd.hasSucceeded); + return this.item.owner.pipe(map((rd: RemoteData) => rd.hasSucceeded)); } } diff --git a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts index 331e979c8f..23d9ef05d0 100644 --- a/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts +++ b/src/app/+item-page/full/field-components/file-section/full-file-section.component.ts @@ -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( diff --git a/src/app/+item-page/full/full-item-page.component.ts b/src/app/+item-page/full/full-item-page.component.ts index dafecd748e..d09ac268ec 100644 --- a/src/app/+item-page/full/full-item-page.component.ts +++ b/src/app/+item-page/full/full-item-page.component.ts @@ -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) => rd.payload) - .filter((item: Item) => hasValue(item)) - .map((item: Item) => item.metadata); + this.metadata$ = this.itemRD$.pipe( + map((rd: RemoteData) => rd.payload), + filter((item: Item) => hasValue(item)), + map((item: Item) => item.metadata),); } } diff --git a/src/app/+item-page/item-page.resolver.ts b/src/app/+item-page/item-page.resolver.ts index c0f4147f47..c0ee6a84ee 100644 --- a/src/app/+item-page/item-page.resolver.ts +++ b/src/app/+item-page/item-page.resolver.ts @@ -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'; diff --git a/src/app/+item-page/simple/field-components/file-section/file-section.component.ts b/src/app/+item-page/simple/field-components/file-section/file-section.component.ts index b42e73940f..8c40d123bf 100644 --- a/src/app/+item-page/simple/field-components/file-section/file-section.component.ts +++ b/src/app/+item-page/simple/field-components/file-section/file-section.component.ts @@ -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'; diff --git a/src/app/+item-page/simple/item-page.component.ts b/src/app/+item-page/simple/item-page.component.ts index 7ff304236d..35162b011f 100644 --- a/src/app/+item-page/simple/item-page.component.ts +++ b/src/app/+item-page/simple/item-page.component.ts @@ -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) => rd.payload) - .filter((item: Item) => hasValue(item)) - .flatMap((item: Item) => item.getThumbnail()); + this.thumbnail$ = this.itemRD$.pipe( + map((rd: RemoteData) => rd.payload), + filter((item: Item) => hasValue(item)), + mergeMap((item: Item) => item.getThumbnail()),); } } diff --git a/src/app/+login-page/login-page.component.spec.ts b/src/app/+login-page/login-page.component.spec.ts index 609cf47794..234435a410 100644 --- a/src/app/+login-page/login-page.component.spec.ts +++ b/src/app/+login-page/login-page.component.spec.ts @@ -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(() => { diff --git a/src/app/+search-page/paginated-search-options.model.spec.ts b/src/app/+search-page/paginated-search-options.model.spec.ts index e8688fd84f..22b3f146b2 100644 --- a/src/app/+search-page/paginated-search-options.model.spec.ts +++ b/src/app/+search-page/paginated-search-options.model.spec.ts @@ -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'; diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts index 49141c2b68..498c41dd6c 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.spec.ts @@ -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 */ diff --git a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts index e0500b555e..4a171a3f3a 100644 --- a/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-facet-filter/search-facet-filter.component.ts @@ -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 = Observable.of([]); + filterSearchResults: Observable = 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} The changed filter parameters */ getRemoveParams(value: string): Observable { - 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} The changed filter parameters */ getAddParams(value: string): Observable { - 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([]); } } diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.spec.ts index 78d40b1cf6..caa5a6febc 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.component.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.spec.ts @@ -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; 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; beforeEach(() => { - filterService.isCollapsed = () => Observable.of(false); + filterService.isCollapsed = () => observableOf(false); isActive = comp.isCollapsed(); }); diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts index bd3c9f7a0c..87f8edc1ea 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.component.ts @@ -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 { diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts index 6d250f6869..156e8d47ea 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.spec.ts @@ -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 */ }; diff --git a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts index 3b7c7b8e86..bf21eab367 100644 --- a/src/app/+search-page/search-filters/search-filter/search-filter.service.ts +++ b/src/app/+search-page/search-filters/search-filter/search-filter.service.ts @@ -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 { 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} Emits the current collapsed state of the given filter, if it's unavailable, return false */ isCollapsed(filterName: string): Observable { - 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} Emits the current page state of the given filter, if it's unavailable, return 1 */ getPage(filterName: string): Observable { - 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 diff --git a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts index 4e555459d6..6f3450e18e 100644 --- a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts +++ b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.spec.ts @@ -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: { diff --git a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts index 61e07b9b53..6cb04c6c1f 100644 --- a/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.ts @@ -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], diff --git a/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.ts b/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.ts index 9e603184e8..bb396a6692 100644 --- a/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.ts +++ b/src/app/+search-page/search-filters/search-filter/search-text-filter/search-text-filter.component.ts @@ -1,6 +1,6 @@ import { animate, state, style, transition, trigger } from '@angular/animations'; import { Component, HostBinding, OnInit } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { FilterType } from '../../../search-service/filter-type.model'; import { facetLoad, diff --git a/src/app/+search-page/search-filters/search-filters.component.spec.ts b/src/app/+search-page/search-filters/search-filters.component.spec.ts index 7f0d4ad748..db21fc8a69 100644 --- a/src/app/+search-page/search-filters/search-filters.component.spec.ts +++ b/src/app/+search-page/search-filters/search-filters.component.spec.ts @@ -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(() => { diff --git a/src/app/+search-page/search-filters/search-filters.component.ts b/src/app/+search-page/search-filters/search-filters.component.ts index 1da4e5860d..7f1eb513ea 100644 --- a/src/app/+search-page/search-filters/search-filters.component.ts +++ b/src/app/+search-page/search-filters/search-filters.component.ts @@ -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,22 +57,23 @@ export class SearchFiltersComponent { * @param {SearchFilterConfig} filter The filter to check for * @returns {Observable} Emits true whenever a given filter config should be shown */ - isActive(filter: SearchFilterConfig): Observable { - return this.filterService.getSelectedValuesForFilter(filter) - .flatMap((isActive) => { + isActive(filterConfig: SearchFilterConfig): Observable { + // console.log(filter.name); + 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),); } } diff --git a/src/app/+search-page/search-labels/search-labels.component.spec.ts b/src/app/+search-page/search-labels/search-labels.component.spec.ts index bf512ed5db..81fa5b5df8 100644 --- a/src/app/+search-page/search-labels/search-labels.component.spec.ts +++ b/src/app/+search-page/search-labels/search-labels.component.spec.ts @@ -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(); }); diff --git a/src/app/+search-page/search-labels/search-labels.component.ts b/src/app/+search-page/search-labels/search-labels.component.ts index 61482f8d8a..08e07cce3d 100644 --- a/src/app/+search-page/search-labels/search-labels.component.ts +++ b/src/app/+search-page/search-labels/search-labels.component.ts @@ -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'; diff --git a/src/app/+search-page/search-options.model.spec.ts b/src/app/+search-page/search-options.model.spec.ts index a624664637..a0ef2b31dd 100644 --- a/src/app/+search-page/search-options.model.spec.ts +++ b/src/app/+search-page/search-options.model.spec.ts @@ -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'; diff --git a/src/app/+search-page/search-page.component.spec.ts b/src/app/+search-page/search-page.component.spec.ts index 3ca18ce4c5..1991cf8f1b 100644 --- a/src/app/+search-page/search-page.component.spec.ts +++ b/src/app/+search-page/search-page.component.spec.ts @@ -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(); }); diff --git a/src/app/+search-page/search-page.component.ts b/src/app/+search-page/search-page.component.ts index da862ee7fc..816e3d67bf 100644 --- a/src/app/+search-page/search-page.component.ts +++ b/src/app/+search-page/search-page.component.ts @@ -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); }); diff --git a/src/app/+search-page/search-service/search-configuration.service.spec.ts b/src/app/+search-page/search-service/search-configuration.service.spec.ts index 9f2e6d5045..af8897c93b 100644 --- a/src/app/+search-page/search-service/search-configuration.service.spec.ts +++ b/src/app/+search-page/search-service/search-configuration.service.spec.ts @@ -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(); diff --git a/src/app/+search-page/search-service/search-configuration.service.ts b/src/app/+search-page/search-service/search-configuration.service.ts index b4c06e83f3..292f26724d 100644 --- a/src/app/+search-page/search-service/search-configuration.service.ts +++ b/src/app/+search-page/search-service/search-configuration.service.ts @@ -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} 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} 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} Emits the current DSpaceObject type as a number */ getCurrentDSOType(): Observable { - 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 { 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 { 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} Emits the current active filters with their values as they are sent to the backend */ getCurrentFilters(): Observable { - 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} Emits the current scope's identifier */ private getScopePart(defaultScope: string): Observable { - return this.getCurrentScope(defaultScope).map((scope) => { + return this.getCurrentScope(defaultScope).pipe(map((scope) => { return { scope } - }); + })); } /** * @returns {Observable} Emits the current query string as a partial SearchOptions object */ private getQueryPart(defaultQuery: string): Observable { - return this.getCurrentQuery(defaultQuery).map((query) => { + return this.getCurrentQuery(defaultQuery).pipe(map((query) => { return { query } - }); + })); } /** * @returns {Observable} Emits the current query string as a partial SearchOptions object */ private getDSOTypePart(): Observable { - return this.getCurrentDSOType().map((dsoType) => { + return this.getCurrentDSOType().pipe(map((dsoType) => { return { dsoType } - }); + })); } /** * @returns {Observable} Emits the current pagination settings as a partial SearchOptions object */ private getPaginationPart(defaultPagination: PaginationComponentOptions): Observable { - return this.getCurrentPagination(defaultPagination).map((pagination) => { + return this.getCurrentPagination(defaultPagination).pipe(map((pagination) => { return { pagination } - }); + })); } /** * @returns {Observable} Emits the current sorting settings as a partial SearchOptions object */ private getSortPart(defaultSort: SortOptions): Observable { - return this.getCurrentSort(defaultSort).map((sort) => { + return this.getCurrentSort(defaultSort).pipe(map((sort) => { return { sort } - }); + })); } /** * @returns {Observable} Emits the current active filters as a partial SearchOptions object */ private getFiltersPart(): Observable { - return this.getCurrentFilters().map((filters) => { + return this.getCurrentFilters().pipe(map((filters) => { return { filters } - }); + })); } } diff --git a/src/app/+search-page/search-service/search.service.spec.ts b/src/app/+search-page/search-service/search.service.spec.ts index 85424a3c20..6bfc9200ec 100644 --- a/src/app/+search-page/search-service/search.service.spec.ts +++ b/src/app/+search-page/search-service/search.service.spec.ts @@ -12,7 +12,7 @@ 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'; @@ -28,6 +28,8 @@ 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 { @@ -56,8 +58,8 @@ describe('SearchService', () => { { provide: RequestService, useValue: getMockRequestService() }, { provide: RemoteDataBuildService, useValue: {} }, { provide: HALEndpointService, useValue: {} }, - { provide: CommunityDataService, useValue: {}}, - { provide: DSpaceObjectDataService, useValue: {}}, + { provide: CommunityDataService, useValue: {} }, + { provide: DSpaceObjectDataService, useValue: {} }, SearchService ], }); @@ -85,13 +87,15 @@ describe('SearchService', () => { const remoteDataBuildService = { toRemoteDataObservable: (requestEntryObs: Observable, responseCacheObs: Observable, payloadObs: Observable) => { - return Observable.combineLatest(requestEntryObs, - responseCacheObs, payloadObs, (req, res, pay) => { + return observableCombineLatest(requestEntryObs, + responseCacheObs, payloadObs).pipe( + map(([req, res, pay]) => { return { req, res, pay }; - }); + }) + ); }, aggregate: (input: Array>>): Observable> => { - return Observable.of(new RemoteData(false, false, true, null, [])); + return observableOf(new RemoteData(false, false, true, null, [])); } }; @@ -113,8 +117,8 @@ describe('SearchService', () => { { 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 ], }); @@ -160,8 +164,8 @@ describe('SearchService', () => { 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)); + (searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry)); /* tslint:disable:no-empty */ searchService.search(searchOptions).subscribe((t) => { }); // subscribe to make sure all methods are called @@ -190,8 +194,8 @@ describe('SearchService', () => { 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)); + (searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry)); /* tslint:disable:no-empty */ searchService.getConfig(null).subscribe((t) => { }); // subscribe to make sure all methods are called @@ -222,8 +226,8 @@ describe('SearchService', () => { 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)); + (searchService as any).responseCache.get.and.returnValue(observableOf(responseEntry)); /* tslint:disable:no-empty */ searchService.getConfig(scope).subscribe((t) => { }); // subscribe to make sure all methods are called diff --git a/src/app/+search-page/search-service/search.service.ts b/src/app/+search-page/search-service/search.service.ts index ac5f7a6169..1503440eb0 100644 --- a/src/app/+search-page/search-service/search.service.ts +++ b/src/app/+search-page/search-service/search.service.ts @@ -1,3 +1,4 @@ +import { of as observableOf, combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { Injectable, OnDestroy } from '@angular/core'; import { ActivatedRoute, @@ -6,7 +7,6 @@ import { Router, UrlSegmentGroup } from '@angular/router'; -import { Observable } from 'rxjs/Observable'; import { flatMap, map, switchMap } from 'rxjs/operators'; import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service'; import { @@ -122,30 +122,33 @@ export class SearchService implements OnDestroy { ); // Create search results again with the correct dso objects linked to each result - const tDomainListObs = Observable.combineLatest(sqrObs, dsoObs, (sqr: SearchQueryResponse, dsos: RemoteData) => { - - return sqr.objects.map((object: NormalizedSearchResult, index: number) => { - let co = DSpaceObject; - if (dsos.payload[index]) { - const constructor: GenericConstructor = dsos.payload[index].constructor as GenericConstructor; - co = getSearchResultFor(constructor); - return Object.assign(new co(), object, { - dspaceObject: dsos.payload[index] - }); - } else { - return undefined; - } - }); - }); + 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 = dsos.payload[index].constructor as GenericConstructor; + co = getSearchResultFor(constructor); + return Object.assign(new co(), object, { + dspaceObject: dsos.payload[index] + }); + } else { + return undefined; + } + }); + }) + ); const pageInfoObs: Observable = responseCacheObs.pipe( map((entry: ResponseCacheEntry) => entry.response), 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); } @@ -244,9 +247,11 @@ export class SearchService implements OnDestroy { 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); } @@ -272,12 +277,14 @@ export class SearchService implements OnDestroy { switchMap((dsoRD: RemoteData) => { 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 +298,13 @@ export class SearchService implements OnDestroy { * @returns {Observable} The current view mode */ getViewMode(): Observable { - 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; } - }); + })); } /** diff --git a/src/app/+search-page/search-settings/search-settings.component.spec.ts b/src/app/+search-page/search-settings/search-settings.component.spec.ts index 5e6dc9b369..b1585c4347 100644 --- a/src/app/+search-page/search-settings/search-settings.component.spec.ts +++ b/src/app/+search-page/search-settings/search-settings.component.spec.ts @@ -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"]')); diff --git a/src/app/+search-page/search-settings/search-settings.component.ts b/src/app/+search-page/search-settings/search-settings.component.ts index 81e2366e39..7fc5645fcc 100644 --- a/src/app/+search-page/search-settings/search-settings.component.ts +++ b/src/app/+search-page/search-settings/search-settings.component.ts @@ -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({ diff --git a/src/app/+search-page/search-sidebar/search-sidebar.effects.spec.ts b/src/app/+search-page/search-sidebar/search-sidebar.effects.spec.ts index 146b1fdcdb..f34f6b72de 100644 --- a/src/app/+search-page/search-sidebar/search-sidebar.effects.spec.ts +++ b/src/app/+search-page/search-sidebar/search-sidebar.effects.spec.ts @@ -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'; diff --git a/src/app/+search-page/search-sidebar/search-sidebar.effects.ts b/src/app/+search-page/search-sidebar/search-sidebar.effects.ts index 758ef2320b..1f5fb0ef60 100644 --- a/src/app/+search-page/search-sidebar/search-sidebar.effects.ts +++ b/src/app/+search-page/search-sidebar/search-sidebar.effects.ts @@ -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) { diff --git a/src/app/+search-page/search-sidebar/search-sidebar.service.spec.ts b/src/app/+search-page/search-sidebar/search-sidebar.service.spec.ts index b6439be4df..0cccf9ea40 100644 --- a/src/app/+search-page/search-sidebar/search-sidebar.service.spec.ts +++ b/src/app/+search-page/search-sidebar/search-sidebar.service.spec.ts @@ -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({ diff --git a/src/app/+search-page/search-sidebar/search-sidebar.service.ts b/src/app/+search-page/search-sidebar/search-sidebar.service.ts index 8cf9339c5c..7185984538 100644 --- a/src/app/+search-page/search-sidebar/search-sidebar.service.ts +++ b/src/app/+search-page/search-sidebar/search-sidebar.service.ts @@ -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, 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} Emits true if the user's screen size is mobile or when the state in the store is currently collapsed */ get isCollapsed(): Observable { - return Observable.combineLatest( + return observableCombineLatest( this.isXsOrSm$, - this.isCollapsedInStore, - (mobile, store) => mobile ? store : true); + this.isCollapsedInStore + ).pipe( + map(([mobile, store]) => mobile ? store : true) + ); } /** diff --git a/src/app/app.component.spec.ts b/src/app/app.component.spec.ts index 0e5af10bcc..c88b999786 100644 --- a/src/app/app.component.spec.ts +++ b/src/app/app.component.spec.ts @@ -92,7 +92,7 @@ describe('App component', () => { let store: Store; beforeEach(() => { - store = fixture.debugElement.injector.get(Store); + store = fixture.debugElement.injector.get(Store) as Store; spyOn(store, 'dispatch'); window.dispatchEvent(new Event('resize')); diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 092c61bdf9..7d4bfe4f33 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -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()); } diff --git a/src/app/app.module.ts b/src/app/app.module.ts index 528c84fd3b..9618dfaca3 100755 --- a/src/app/app.module.ts +++ b/src/app/app.module.ts @@ -45,54 +45,72 @@ export function getMetaReducers(config: GlobalConfig): Array { - const [successResponse, errorResponse] = this.responseCache.get(request.href) - .map((entry: ResponseCacheEntry) => entry.response) + return this.responseCache.get(request.href).pipe( + map((entry: ResponseCacheEntry) => entry.response), // 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 +43,24 @@ export class AuthRequestService { } public postToEndpoint(method: string, body: any, options?: HttpOptions): Observable { - 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 { - 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()); } } diff --git a/src/app/core/auth/auth-response-parsing.service.spec.ts b/src/app/core/auth/auth-response-parsing.service.spec.ts index f6dd87e99a..138d0f1be3 100644 --- a/src/app/core/auth/auth-response-parsing.service.spec.ts +++ b/src/app/core/auth/auth-response-parsing.service.spec.ts @@ -2,20 +2,18 @@ import { AuthStatusResponse } from '../cache/response-cache.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 { getMockStore } from '../../shared/mocks/mock-store'; +import { MockStore } from '../../shared/testing/mock-store'; +import { ObjectCacheState } from '../cache/object-cache.reducer'; describe('AuthResponseParsingService', () => { let service: AuthResponseParsingService; - const EnvConfig = {cache: {msToLive: 1000}} as GlobalConfig; - const store = getMockStore() as Store; - const objectCacheService = new ObjectCacheService(store); + const EnvConfig = { cache: { msToLive: 1000 } } as GlobalConfig; + const store = new MockStore({}); + const objectCacheService = new ObjectCacheService(store as any); beforeEach(() => { service = new AuthResponseParsingService(EnvConfig, objectCacheService); diff --git a/src/app/core/auth/auth.effects.spec.ts b/src/app/core/auth/auth.effects.spec.ts index d75b407516..0dc8abf860 100644 --- a/src/app/core/auth/auth.effects.spec.ts +++ b/src/app/core/auth/auth.effects.spec.ts @@ -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 { @@ -30,16 +29,21 @@ import { EPersonMock } from '../../shared/testing/eperson-mock'; describe('AuthEffects', () => { let authEffects: AuthEffects; let actions: Observable; - const authServiceStub = new AuthServiceStub(); + let authServiceStub; const store: Store = 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, @@ -71,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: { @@ -112,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}}); @@ -138,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}}); @@ -164,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}}); @@ -190,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}}); diff --git a/src/app/core/auth/auth.effects.ts b/src/app/core/auth/auth.effects.ts index 82def702eb..c57fa3f70e 100644 --- a/src/app/core/auth/auth.effects.ts +++ b/src/app/core/auth/auth.effects.ts @@ -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'; @@ -43,112 +43,131 @@ export class AuthEffects { * @method authenticate */ @Effect() - public authenticate$: Observable = 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 = 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 = this.actions$ - .ofType(AuthActionTypes.AUTHENTICATE_SUCCESS) - .do((action: AuthenticationSuccessAction) => this.authService.storeToken(action.payload)) - .map((action: AuthenticationSuccessAction) => new AuthenticatedAction(action.payload)); + public authenticateSuccess$: Observable = 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 = 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 = 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 = this.actions$ - .ofType(AuthActionTypes.AUTHENTICATED_ERROR) - .do((action: LogOutSuccessAction) => this.authService.removeToken()); + @Effect({ dispatch: false }) + public authenticatedError$: Observable = this.actions$.pipe( + ofType(AuthActionTypes.AUTHENTICATED_ERROR), + tap((action: LogOutSuccessAction) => this.authService.removeToken()) + ); @Effect() - public checkToken$: Observable = 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 = 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 = 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 = 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 = 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 = 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 = this.actions$ - .ofType(AuthActionTypes.REFRESH_TOKEN_SUCCESS) - .do((action: RefreshTokenSuccessAction) => this.authService.replaceToken(action.payload)); + @Effect({ dispatch: false }) + public refreshTokenSuccess$: Observable = 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 = 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 = 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 = 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 = 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 = 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 = 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 diff --git a/src/app/core/auth/auth.interceptor.spec.ts b/src/app/core/auth/auth.interceptor.spec.ts index 528c2cfab3..89c9ed1951 100644 --- a/src/app/core/auth/auth.interceptor.spec.ts +++ b/src/app/core/auth/auth.interceptor.spec.ts @@ -4,7 +4,7 @@ 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'; @@ -23,7 +23,7 @@ describe(`AuthInterceptor`, () => { /* tslint:disable:no-empty */ dispatch: {}, /* tslint:enable:no-empty */ - select: Observable.of(true) + select: observableOf(true) }); beforeEach(() => { diff --git a/src/app/core/auth/auth.interceptor.ts b/src/app/core/auth/auth.interceptor.ts index f38abb90bc..dd9e3fb5e7 100644 --- a/src/app/core/auth/auth.interceptor.ts +++ b/src/app/core/auth/auth.interceptor.ts @@ -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'; @@ -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; } } diff --git a/src/app/core/auth/auth.service.spec.ts b/src/app/core/auth/auth.service.spec.ts index c943a815e7..d39c0a4590 100644 --- a/src/app/core/auth/auth.service.spec.ts +++ b/src/app/core/auth/auth.service.spec.ts @@ -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'; @@ -29,41 +28,53 @@ describe('AuthService test', () => { const mockStore: Store = 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(); - describe('', () => { + function init() { + token = new AuthTokenInfo('test_token'); + token.expires = Date.now() + (1000 * 60 * 60); + authenticatedState = { + authenticated: true, + loaded: true, + loading: false, + authToken: token, + user: EPersonMock + }; + authRequest = new AuthRequestServiceStub(); + routeStub = new ActivatedRouteStub(); + } + + beforeEach(() => { + init(); + }); + + describe('', () => { beforeEach(() => { + 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}, + { provide: RemoteDataBuildService, useValue: rdbService }, CookieService, AuthService ], @@ -116,15 +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: RemoteDataBuildService, useValue: rdbService}, + { provide: AuthRequestService, useValue: authRequest }, + { provide: REQUEST, useValue: {} }, + { provide: Router, useValue: routerStub }, + { provide: RemoteDataBuildService, useValue: rdbService }, CookieService ] }).compileComponents(); @@ -136,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, rdbService); + authService = new AuthService({}, window, undefined, authReqService, router, cookieService, store, rdbService); })); it('should return true when user is logged in', () => { @@ -168,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 ] @@ -195,7 +207,7 @@ describe('AuthService test', () => { (state as any).core = Object.create({}); (state as any).core.auth = authenticatedState; }); - authService = new AuthService({}, window, authReqService, router, cookieService, store, rdbService); + authService = new AuthService({}, window, undefined, authReqService, router, cookieService, store, rdbService); storage = (authService as any).storage; spyOn(storage, 'get'); spyOn(storage, 'remove'); diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 1105abe3c7..67f533d4ad 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -1,13 +1,22 @@ -import { Inject, Injectable } from '@angular/core'; +import { Observable, of as observableOf } from 'rxjs'; +import { + distinctUntilChanged, + filter, + first, + map, + startWith, + switchMap, + take, + withLatestFrom +} from 'rxjs/operators'; +import { Inject, Injectable, Optional } from '@angular/core'; import { PRIMARY_OUTLET, Router, UrlSegmentGroup, UrlTree } from '@angular/router'; import { HttpHeaders } from '@angular/common/http'; -import { REQUEST } from '@nguniversal/express-engine/tokens'; +import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens'; import { RouterReducerState } from '@ngrx/router-store'; -import { Store } from '@ngrx/store'; +import { select, Store } from '@ngrx/store'; import { CookieAttributes } from 'js-cookie'; -import { Observable } from 'rxjs/Observable'; -import { map, switchMap, withLatestFrom } from 'rxjs/operators'; import { EPerson } from '../eperson/models/eperson.model'; import { AuthRequestService } from './auth-request.service'; @@ -50,26 +59,30 @@ export class AuthService { constructor(@Inject(REQUEST) protected req: any, @Inject(NativeWindowService) protected _window: NativeWindowRef, protected authRequestService: AuthRequestService, + @Optional() @Inject(RESPONSE) private response: any, protected router: Router, protected storage: CookieService, protected store: Store, protected rdbService: RemoteDataBuildService ) { - this.store.select(isAuthenticated) - .startWith(false) - .subscribe((authenticated: boolean) => this._authenticated = authenticated); + 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(); }); @@ -102,14 +115,14 @@ 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) => { if (status.authenticated) { return status; } else { throw(new Error('Invalid email or password')); } - }) + })) } @@ -118,7 +131,7 @@ export class AuthService { * @returns {Observable} */ public isAuthenticated(): Observable { - return this.store.select(isAuthenticated); + return this.store.pipe(select(isAuthenticated)); } /** @@ -158,9 +171,10 @@ export class AuthService { * Checks if token is present into storage and is not expired */ public hasValidAuthenticationToken(): Observable { - 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); @@ -169,7 +183,8 @@ export class AuthService { } else { throw false; } - }); + }) + ); } /** @@ -181,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')); } - }); + })); } /** @@ -207,7 +222,7 @@ export class AuthService { // 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); } /** @@ -219,14 +234,15 @@ export class AuthService { 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) => { + return this.authRequestService.getRequest('logout', options).pipe( + map((status: AuthStatus) => { if (!status.authenticated) { return true; } else { throw(new Error('auth.errors.invalid-user')); } - }) + })) + } /** @@ -246,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; @@ -259,9 +275,10 @@ export class AuthService { * @returns {boolean} */ public isTokenExpiring(): Observable { - 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 { @@ -269,6 +286,7 @@ export class AuthService { return token.expires - (60 * 5 * 1000) < Date.now(); } }) + ) } /** @@ -328,6 +346,10 @@ export class AuthService { if (this._window.nativeWindow.location) { // Hard redirect to login page, so that all state is definitely lost this._window.nativeWindow.location.href = redirectUrl; + } else if (this.response) { + if (!this.response._headerSent) { + this.response.redirect(302, redirectUrl); + } } else { this.router.navigateByUrl(redirectUrl); } @@ -337,8 +359,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)) { @@ -372,9 +394,9 @@ export class AuthService { getRedirectUrl(): Observable { 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)); } } diff --git a/src/app/core/auth/authenticated.guard.ts b/src/app/core/auth/authenticated.guard.ts index 42c39b403c..b9091a86ad 100644 --- a/src/app/core/auth/authenticated.guard.ts +++ b/src/app/core/auth/authenticated.guard.ts @@ -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 { // 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); diff --git a/src/app/core/auth/models/auth-status.model.ts b/src/app/core/auth/models/auth-status.model.ts index b8ccf9ed6d..37f8d76672 100644 --- a/src/app/core/auth/models/auth-status.model.ts +++ b/src/app/core/auth/models/auth-status.model.ts @@ -2,7 +2,7 @@ import { AuthError } from './auth-error.model'; import { AuthTokenInfo } from './auth-token-info.model'; import { EPerson } from '../../eperson/models/eperson.model'; import { RemoteData } from '../../data/remote-data'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; export class AuthStatus { diff --git a/src/app/core/auth/server-auth.service.ts b/src/app/core/auth/server-auth.service.ts index 9030443a1e..089cbd0ba2 100644 --- a/src/app/core/auth/server-auth.service.ts +++ b/src/app/core/auth/server-auth.service.ts @@ -1,7 +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'; diff --git a/src/app/core/browse/browse.service.spec.ts b/src/app/core/browse/browse.service.spec.ts index daaca1e5b2..d43a26ed4b 100644 --- a/src/app/core/browse/browse.service.spec.ts +++ b/src/app/core/browse/browse.service.spec.ts @@ -1,5 +1,5 @@ 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'; diff --git a/src/app/core/browse/browse.service.ts b/src/app/core/browse/browse.service.ts index 003f92698c..ddce277e7e 100644 --- a/src/app/core/browse/browse.service.ts +++ b/src/app/core/browse/browse.service.ts @@ -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, diff --git a/src/app/core/cache/builders/remote-data-build.service.spec.ts b/src/app/core/cache/builders/remote-data-build.service.spec.ts index bab628a7fe..8aea54102c 100644 --- a/src/app/core/cache/builders/remote-data-build.service.spec.ts +++ b/src/app/core/cache/builders/remote-data-build.service.spec.ts @@ -3,7 +3,7 @@ import { Item } from '../../shared/item.model'; import { PaginatedList } from '../../data/paginated-list'; import { PageInfo } from '../../shared/page-info.model'; import { RemoteData } from '../../data/remote-data'; -import { Observable } from 'rxjs/Observable'; +import { of as observableOf } from 'rxjs'; const pageInfo = new PageInfo(); const array = [ @@ -43,14 +43,14 @@ describe('RemoteDataBuildService', () => { }); it('should return the correct remoteData of a paginatedList when the input is a (remoteData of an) array', () => { - const result = (service as any).toPaginatedList(Observable.of(arrayRD), pageInfo); + const result = (service as any).toPaginatedList(observableOf(arrayRD), pageInfo); result.subscribe((resultRD) => { expect(resultRD).toEqual(expected); }); }); it('should return the correct remoteData of a paginatedList when the input is a (remoteData of a) paginated list', () => { - const result = (service as any).toPaginatedList(Observable.of(paginatedListRD), pageInfo); + const result = (service as any).toPaginatedList(observableOf(paginatedListRD), pageInfo); result.subscribe((resultRD) => { expect(resultRD).toEqual(expected); }); diff --git a/src/app/core/cache/builders/remote-data-build.service.ts b/src/app/core/cache/builders/remote-data-build.service.ts index 13da50cb87..aa622b20c5 100644 --- a/src/app/core/cache/builders/remote-data-build.service.ts +++ b/src/app/core/cache/builders/remote-data-build.service.ts @@ -1,6 +1,11 @@ +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, filter, flatMap, map, startWith, switchMap } from 'rxjs/operators'; +import { distinctUntilChanged, flatMap, map, startWith } from 'rxjs/operators'; import { hasValue, hasValueOperator, isEmpty, isNotEmpty } from '../../../shared/empty.util'; import { PaginatedList } from '../../data/paginated-list'; import { RemoteData } from '../../data/remote-data'; @@ -17,10 +22,10 @@ import { ResponseCacheService } from '../response-cache.service'; import { getMapsTo, getRelationMetadata, getRelationships } from './build-decorators'; import { PageInfo } from '../../shared/page-info.model'; import { + filterSuccessfulResponses, getRequestFromSelflink, getResourceLinksFromResponse, - getResponseFromSelflink, - filterSuccessfulResponses + getResponseFromSelflink } from '../../shared/operators'; @Injectable() @@ -32,24 +37,24 @@ export class RemoteDataBuildService { buildSingle(href$: string | Observable): Observable> { if (typeof href$ === 'string') { - href$ = Observable.of(href$); + href$ = observableOf(href$); } const requestHref$ = href$.pipe(flatMap((href: string) => this.objectCache.getRequestHrefBySelfLink(href))); - const requestEntry$ = Observable.race( + const requestEntry$ = observableRace( href$.pipe(getRequestFromSelflink(this.requestService)), requestHref$.pipe(getRequestFromSelflink(this.requestService)) ); - const responseCache$ = Observable.race( + const responseCache$ = observableRace( href$.pipe(getResponseFromSelflink(this.responseCache)), requestHref$.pipe(getResponseFromSelflink(this.responseCache)) ); // always use self link if that is cached, only if it isn't, get it via the response. const payload$ = - Observable.combineLatest( + observableCombineLatest( href$.pipe( flatMap((href: string) => this.objectCache.getBySelfLink(href)), startWith(undefined) @@ -60,20 +65,20 @@ export class RemoteDataBuildService { 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(normalized); @@ -85,8 +90,8 @@ export class RemoteDataBuildService { } toRemoteDataObservable(requestEntry$: Observable, responseCache$: Observable, payload$: Observable) { - return Observable.combineLatest(requestEntry$, responseCache$.startWith(undefined), payload$, - (reqEntry: RequestEntry, resEntry: ResponseCacheEntry, payload: T) => { + return observableCombineLatest(requestEntry$, responseCache$.pipe(startWith(undefined)), payload$).pipe( + map(([reqEntry, resEntry, payload]) => { const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true; const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false; let isSuccessful: boolean; @@ -105,12 +110,13 @@ export class RemoteDataBuildService { error, payload ); - }); + }) + ); } buildList(href$: string | Observable): Observable>> { if (typeof href$ === 'string') { - href$ = Observable.of(href$); + href$ = observableOf(href$); } const requestEntry$ = href$.pipe(getRequestFromSelflink(this.requestService)); @@ -119,12 +125,12 @@ export class RemoteDataBuildService { const tDomainList$ = responseCache$.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(normalized); }); - }); + })); }), startWith([]), distinctUntilChanged() @@ -142,11 +148,13 @@ 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$); } @@ -204,12 +212,11 @@ export class RemoteDataBuildService { aggregate(input: Array>>): Observable> { 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>) => { + return observableCombineLatest(...input).pipe( + map((arr) => { const requestPending: boolean = arr .map((d: RemoteData) => d.isRequestPending) .every((b: boolean) => b === true); @@ -251,17 +258,19 @@ export class RemoteDataBuildService { error, payload ); - }) + })) } private toPaginatedList(input: Observable>>, pageInfo: PageInfo): Observable>> { - return input.map((rd: RemoteData>) => { - if (Array.isArray(rd.payload)) { - return Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload) }) - } else { - return Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload.page) }); - } - }); + return input.pipe( + map((rd: RemoteData>) => { + if (Array.isArray(rd.payload)) { + return Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload) }) + } else { + return Object.assign(rd, { payload: new PaginatedList(pageInfo, rd.payload.page) }); + } + }) + ); } } diff --git a/src/app/core/cache/object-cache.effects.spec.ts b/src/app/core/cache/object-cache.effects.spec.ts index d0a97a18fd..36502be849 100644 --- a/src/app/core/cache/object-cache.effects.spec.ts +++ b/src/app/core/cache/object-cache.effects.spec.ts @@ -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'; diff --git a/src/app/core/cache/object-cache.effects.ts b/src/app/core/cache/object-cache.effects.ts index 019c792973..2bd8ad0e3c 100644 --- a/src/app/core/cache/object-cache.effects.ts +++ b/src/app/core/cache/object-cache.effects.ts @@ -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) { + } } diff --git a/src/app/core/cache/object-cache.service.spec.ts b/src/app/core/cache/object-cache.service.spec.ts index 80a9121544..38d51f09b3 100644 --- a/src/app/core/cache/object-cache.service.spec.ts +++ b/src/app/core/cache/object-cache.service.spec.ts @@ -1,11 +1,13 @@ import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs/Observable'; +import { of as observableOf } from 'rxjs'; import { ObjectCacheService } from './object-cache.service'; import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; import { CoreState } from '../core.reducers'; import { ResourceType } from '../shared/resource-type'; import { NormalizedItem } from './models/normalized-item.model'; +import { first } from 'rxjs/operators'; +import * as ngrx from '@ngrx/store'; describe('ObjectCacheService', () => { let service: ObjectCacheService; @@ -51,10 +53,14 @@ describe('ObjectCacheService', () => { describe('getBySelfLink', () => { it('should return an observable of the cached object with the specified self link and type', () => { - spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry)); + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf(cacheEntry); + }; + }); // due to the implementation of spyOn above, this subscribe will be synchronous - service.getBySelfLink(selfLink).take(1).subscribe((o) => { + service.getBySelfLink(selfLink).pipe(first()).subscribe((o) => { expect(o.self).toBe(selfLink); // this only works if testObj is an instance of TestClass expect(o instanceof NormalizedItem).toBeTruthy(); @@ -63,7 +69,11 @@ describe('ObjectCacheService', () => { }); it('should not return a cached object that has exceeded its time to live', () => { - spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry)); + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf(invalidCacheEntry); + }; + }); let getObsHasFired = false; const subscription = service.getBySelfLink(selfLink).subscribe((o) => getObsHasFired = true); @@ -76,9 +86,9 @@ describe('ObjectCacheService', () => { it('should return an observable of the array of cached objects with the specified self link and type', () => { const item = new NormalizedItem(); item.self = selfLink; - spyOn(service, 'getBySelfLink').and.returnValue(Observable.of(item)); + spyOn(service, 'getBySelfLink').and.returnValue(observableOf(item)); - service.getList([selfLink, selfLink]).take(1).subscribe((arr) => { + service.getList([selfLink, selfLink]).pipe(first()).subscribe((arr) => { expect(arr[0].self).toBe(selfLink); expect(arr[0] instanceof NormalizedItem).toBeTruthy(); }); @@ -87,19 +97,31 @@ describe('ObjectCacheService', () => { describe('has', () => { it('should return true if the object with the supplied self link is cached and still valid', () => { - spyOn(store, 'select').and.returnValue(Observable.of(cacheEntry)); + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf(cacheEntry); + }; + }); expect(service.hasBySelfLink(selfLink)).toBe(true); }); it("should return false if the object with the supplied self link isn't cached", () => { - spyOn(store, 'select').and.returnValue(Observable.of(undefined)); + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf(undefined); + }; + }); expect(service.hasBySelfLink(selfLink)).toBe(false); }); it('should return false if the object with the supplied self link is cached but has exceeded its time to live', () => { - spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry)); + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf(invalidCacheEntry); + }; + }); expect(service.hasBySelfLink(selfLink)).toBe(false); }); diff --git a/src/app/core/cache/object-cache.service.ts b/src/app/core/cache/object-cache.service.ts index 9344f4d5f0..dbe241ffb3 100644 --- a/src/app/core/cache/object-cache.service.ts +++ b/src/app/core/cache/object-cache.service.ts @@ -1,16 +1,16 @@ -import { Injectable } from '@angular/core'; -import { MemoizedSelector, Store } from '@ngrx/store'; +import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; -import { Observable } from 'rxjs/Observable'; +import { distinctUntilChanged, filter, first, map, mergeMap, take } from 'rxjs/operators'; +import { Injectable } from '@angular/core'; +import { MemoizedSelector, select, Store } from '@ngrx/store'; import { IndexName } from '../index/index.reducer'; -import { ObjectCacheEntry, CacheableObject } from './object-cache.reducer'; +import { CacheableObject, ObjectCacheEntry } from './object-cache.reducer'; import { AddToObjectCacheAction, RemoveFromObjectCacheAction } from './object-cache.actions'; import { hasNoValue } from '../../shared/empty.util'; import { GenericConstructor } from '../shared/generic-constructor'; import { coreSelector, CoreState } from '../core.reducers'; import { pathSelector } from '../shared/selectors'; -import { Item } from '../shared/item.model'; import { NormalizedObjectFactory } from './models/normalized-object-factory'; import { NormalizedObject } from './models/normalized-object.model'; @@ -73,33 +73,40 @@ export class ObjectCacheService { * An observable of the requested object */ getByUUID(uuid: string): Observable { - return this.store.select(selfLinkFromUuidSelector(uuid)) - .flatMap((selfLink: string) => this.getBySelfLink(selfLink)) + return this.store.pipe( + select(selfLinkFromUuidSelector(uuid)), + mergeMap((selfLink: string) => this.getBySelfLink(selfLink) + ) + ) } getBySelfLink(selfLink: string): Observable { - return this.getEntry(selfLink) - .map((entry: ObjectCacheEntry) => { - const type: GenericConstructor= NormalizedObjectFactory.getConstructor(entry.data.type); + return this.getEntry(selfLink).pipe( + map((entry: ObjectCacheEntry) => { + const type: GenericConstructor = NormalizedObjectFactory.getConstructor(entry.data.type); return Object.assign(new type(), entry.data) as T - }); + })); } private getEntry(selfLink: string): Observable { - return this.store.select(entryFromSelfLinkSelector(selfLink)) - .filter((entry) => this.isValid(entry)) - .distinctUntilChanged(); + return this.store.pipe( + select(entryFromSelfLinkSelector(selfLink)), + filter((entry) => this.isValid(entry)), + distinctUntilChanged() + ); } getRequestHrefBySelfLink(selfLink: string): Observable { - return this.getEntry(selfLink) - .map((entry: ObjectCacheEntry) => entry.requestHref) - .distinctUntilChanged(); + return this.getEntry(selfLink).pipe( + map((entry: ObjectCacheEntry) => entry.requestHref), + distinctUntilChanged(),); } getRequestHrefByUUID(uuid: string): Observable { - return this.store.select(selfLinkFromUuidSelector(uuid)) - .flatMap((selfLink: string) => this.getRequestHrefBySelfLink(selfLink)); + return this.store.pipe( + select(selfLinkFromUuidSelector(uuid)), + mergeMap((selfLink: string) => this.getRequestHrefBySelfLink(selfLink)) + ); } /** @@ -122,7 +129,7 @@ export class ObjectCacheService { * @return Observable> */ getList(selfLinks: string[]): Observable { - return Observable.combineLatest( + return observableCombineLatest( selfLinks.map((selfLink: string) => this.getBySelfLink(selfLink)) ); } @@ -139,9 +146,10 @@ export class ObjectCacheService { hasByUUID(uuid: string): boolean { let result: boolean; - this.store.select(selfLinkFromUuidSelector(uuid)) - .take(1) - .subscribe((selfLink: string) => result = this.hasBySelfLink(selfLink)); + this.store.pipe( + select(selfLinkFromUuidSelector(uuid)), + first() + ).subscribe((selfLink: string) => result = this.hasBySelfLink(selfLink)); return result; } @@ -158,9 +166,9 @@ export class ObjectCacheService { hasBySelfLink(selfLink: string): boolean { let result = false; - this.store.select(entryFromSelfLinkSelector(selfLink)) - .take(1) - .subscribe((entry: ObjectCacheEntry) => result = this.isValid(entry)); + this.store.pipe(select(entryFromSelfLinkSelector(selfLink)), + first() + ).subscribe((entry: ObjectCacheEntry) => result = this.isValid(entry)); return result; } diff --git a/src/app/core/cache/response-cache.effects.spec.ts b/src/app/core/cache/response-cache.effects.spec.ts index e58ec536e3..950049bfca 100644 --- a/src/app/core/cache/response-cache.effects.spec.ts +++ b/src/app/core/cache/response-cache.effects.spec.ts @@ -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 { StoreActionTypes } from '../../store.actions'; diff --git a/src/app/core/cache/response-cache.effects.ts b/src/app/core/cache/response-cache.effects.ts index d340750797..5a1e53e20c 100644 --- a/src/app/core/cache/response-cache.effects.ts +++ b/src/app/core/cache/response-cache.effects.ts @@ -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 { ResetResponseCacheTimestampsAction } from './response-cache.actions'; import { StoreActionTypes } from '../../store.actions'; @@ -16,9 +17,11 @@ export class ResponseCacheEffects { * time ago, and will likely need to be revisited later */ @Effect() fixTimestampsOnRehydrate = this.actions$ - .ofType(StoreActionTypes.REHYDRATE) - .map(() => new ResetResponseCacheTimestampsAction(new Date().getTime())); + .pipe(ofType(StoreActionTypes.REHYDRATE), + map(() => new ResetResponseCacheTimestampsAction(new Date().getTime())) + ); - constructor(private actions$: Actions, ) { } + constructor(private actions$: Actions,) { + } } diff --git a/src/app/core/cache/response-cache.service.spec.ts b/src/app/core/cache/response-cache.service.spec.ts index 77838b6eb6..4fcd926343 100644 --- a/src/app/core/cache/response-cache.service.spec.ts +++ b/src/app/core/cache/response-cache.service.spec.ts @@ -1,10 +1,13 @@ import { Store } from '@ngrx/store'; import { ResponseCacheService } from './response-cache.service'; -import { Observable } from 'rxjs/Observable'; +import { of as observableOf } from 'rxjs'; import { CoreState } from '../core.reducers'; import { RestResponse } from './response-cache.models'; import { ResponseCacheEntry } from './response-cache.reducer'; +import { first } from 'rxjs/operators'; +import * as ngrx from '@ngrx/store' +import { cold } from 'jasmine-marbles'; describe('ResponseCacheService', () => { let service: ResponseCacheService; @@ -40,20 +43,23 @@ describe('ResponseCacheService', () => { describe('get', () => { it('should return an observable of the cached request with the specified key', () => { - spyOn(store, 'select').and.callFake((...args: any[]) => { - return Observable.of(validCacheEntry(keys[1])); + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf(validCacheEntry(keys[1])); + }; }); - let testObj: ResponseCacheEntry; - service.get(keys[1]).first().subscribe((entry) => { + service.get(keys[1]).pipe(first()).subscribe((entry) => { testObj = entry; }); expect(testObj.key).toEqual(keys[1]); }); it('should not return a cached request that has exceeded its time to live', () => { - spyOn(store, 'select').and.callFake((...args: any[]) => { - return Observable.of(invalidCacheEntry(keys[1])); + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf(invalidCacheEntry(keys[1])); + }; }); let getObsHasFired = false; @@ -65,17 +71,29 @@ describe('ResponseCacheService', () => { describe('has', () => { it('should return true if the request with the supplied key is cached and still valid', () => { - spyOn(store, 'select').and.returnValue(Observable.of(validCacheEntry(keys[1]))); + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf(validCacheEntry(keys[1])); + }; + }); expect(service.has(keys[1])).toBe(true); }); it('should return false if the request with the supplied key isn\'t cached', () => { - spyOn(store, 'select').and.returnValue(Observable.of(undefined)); + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf(undefined); + }; + }); expect(service.has(keys[1])).toBe(false); }); it('should return false if the request with the supplied key is cached but has exceeded its time to live', () => { - spyOn(store, 'select').and.returnValue(Observable.of(invalidCacheEntry(keys[1]))); + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf(invalidCacheEntry(keys[1])); + }; + }); expect(service.has(keys[1])).toBe(false); }); }); diff --git a/src/app/core/cache/response-cache.service.ts b/src/app/core/cache/response-cache.service.ts index a0e3740094..21430d451c 100644 --- a/src/app/core/cache/response-cache.service.ts +++ b/src/app/core/cache/response-cache.service.ts @@ -1,7 +1,8 @@ +import { filter, take, distinctUntilChanged, first } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { MemoizedSelector, Store } from '@ngrx/store'; +import { MemoizedSelector, select, Store } from '@ngrx/store'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { ResponseCacheEntry } from './response-cache.reducer'; import { hasNoValue } from '../../shared/empty.util'; @@ -21,7 +22,8 @@ function entryFromKeySelector(key: string): MemoizedSelector - ) { } + ) { + } add(key: string, response: RestResponse, msToLive: number): Observable { if (!this.has(key)) { @@ -39,9 +41,11 @@ export class ResponseCacheService { * an observable of the ResponseCacheEntry with the specified key */ get(key: string): Observable { - return this.store.select(entryFromKeySelector(key)) - .filter((entry: ResponseCacheEntry) => this.isValid(entry)) - .distinctUntilChanged() + return this.store.pipe( + select(entryFromKeySelector(key)), + filter((entry: ResponseCacheEntry) => this.isValid(entry)), + distinctUntilChanged() + ) } /** @@ -56,11 +60,11 @@ export class ResponseCacheService { has(key: string): boolean { let result: boolean; - this.store.select(entryFromKeySelector(key)) - .take(1) - .subscribe((entry: ResponseCacheEntry) => { - result = this.isValid(entry); - }); + this.store.pipe(select(entryFromKeySelector(key)), + first() + ).subscribe((entry: ResponseCacheEntry) => { + result = this.isValid(entry); + }); return result; } @@ -70,6 +74,7 @@ export class ResponseCacheService { this.store.dispatch(new ResponseCacheRemoveAction(key)); } } + /** * Check whether a ResponseCacheEntry should still be cached * diff --git a/src/app/core/config/config.service.spec.ts b/src/app/core/config/config.service.spec.ts index 4b05d5c929..46c8fd1859 100644 --- a/src/app/core/config/config.service.spec.ts +++ b/src/app/core/config/config.service.spec.ts @@ -1,5 +1,5 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/Rx'; +import { TestScheduler } from 'rxjs/testing'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { ConfigService } from './config.service'; @@ -56,11 +56,11 @@ describe('ConfigService', () => { } beforeEach(() => { + scheduler = getTestScheduler(); responseCache = initMockResponseCacheService(true); requestService = getMockRequestService(); - service = initTestService(); - scheduler = getTestScheduler(); halService = new HALEndpointServiceStub(configEndpoint); + service = initTestService(); }); describe('getConfigByHref', () => { diff --git a/src/app/core/config/config.service.ts b/src/app/core/config/config.service.ts index bb863ad46f..872bc57c2b 100644 --- a/src/app/core/config/config.service.ts +++ b/src/app/core/config/config.service.ts @@ -1,8 +1,8 @@ -import { Observable } from 'rxjs/Observable'; - +import { Observable, of as observableOf, throwError as observableThrowError, merge as observableMerge } from 'rxjs'; +import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; import { RequestService } from '../data/request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; -import { ConfigSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; +import { ConfigSuccessResponse } from '../cache/response-cache.models'; import { ConfigRequest, FindAllOptions, RestRequest } from '../data/request.models'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; @@ -18,16 +18,17 @@ export abstract class ConfigService { protected abstract halService: HALEndpointService; protected getConfig(request: RestRequest): Observable { - const [successResponse, errorResponse] = this.responseCache.get(request.href) - .map((entry: ResponseCacheEntry) => entry.response) - .partition((response: RestResponse) => response.isSuccessful); - return Observable.merge( - errorResponse.flatMap((response: ErrorResponse) => - Observable.throw(new Error(`Couldn't retrieve the config`))), - successResponse - .filter((response: ConfigSuccessResponse) => isNotEmpty(response) && isNotEmpty(response.configDefinition)) - .map((response: ConfigSuccessResponse) => new ConfigData(response.pageInfo, response.configDefinition)) - .distinctUntilChanged()); + const responses = this.responseCache.get(request.href).pipe(map((entry: ResponseCacheEntry) => entry.response)); + const errorResponses = responses.pipe( + filter((response) => !response.isSuccessful), + mergeMap(() => observableThrowError(new Error(`Couldn't retrieve the config`))) + ); + const successResponses = responses.pipe( + filter((response) => response.isSuccessful && isNotEmpty(response) && isNotEmpty((response as ConfigSuccessResponse).configDefinition)), + map((response: ConfigSuccessResponse) => new ConfigData(response.pageInfo, response.configDefinition)) + ); + return observableMerge(errorResponses, successResponses); + } protected getConfigByNameHref(endpoint, resourceName): string { @@ -65,13 +66,13 @@ export abstract class ConfigService { } public getConfigAll(): Observable { - return this.halService.getEndpoint(this.linkPath) - .filter((href: string) => isNotEmpty(href)) - .distinctUntilChanged() - .map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)) - .do((request: RestRequest) => this.requestService.configure(request)) - .flatMap((request: RestRequest) => this.getConfig(request)) - .distinctUntilChanged(); + return this.halService.getEndpoint(this.linkPath).pipe( + filter((href: string) => isNotEmpty(href)), + distinctUntilChanged(), + map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)), + tap((request: RestRequest) => this.requestService.configure(request)), + mergeMap((request: RestRequest) => this.getConfig(request)), + distinctUntilChanged()); } public getConfigByHref(href: string): Observable { @@ -82,25 +83,26 @@ export abstract class ConfigService { } public getConfigByName(name: string): Observable { - return this.halService.getEndpoint(this.linkPath) - .map((endpoint: string) => this.getConfigByNameHref(endpoint, name)) - .filter((href: string) => isNotEmpty(href)) - .distinctUntilChanged() - .map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)) - .do((request: RestRequest) => this.requestService.configure(request)) - .flatMap((request: RestRequest) => this.getConfig(request)) - .distinctUntilChanged(); + return this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getConfigByNameHref(endpoint, name)), + filter((href: string) => isNotEmpty(href)), + distinctUntilChanged(), + map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)), + tap((request: RestRequest) => this.requestService.configure(request)), + mergeMap((request: RestRequest) => this.getConfig(request)), + distinctUntilChanged()); } public getConfigBySearch(options: FindAllOptions = {}): Observable { - return this.halService.getEndpoint(this.linkPath) - .map((endpoint: string) => this.getConfigSearchHref(endpoint, options)) - .filter((href: string) => isNotEmpty(href)) - .distinctUntilChanged() - .map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)) - .do((request: RestRequest) => this.requestService.configure(request)) - .flatMap((request: RestRequest) => this.getConfig(request)) - .distinctUntilChanged(); + console.log(this.halService.getEndpoint(this.linkPath)); + return this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getConfigSearchHref(endpoint, options)), + filter((href: string) => isNotEmpty(href)), + distinctUntilChanged(), + map((endpointURL: string) => new ConfigRequest(this.requestService.generateRequestId(), endpointURL)), + tap((request: RestRequest) => this.requestService.configure(request)), + mergeMap((request: RestRequest) => this.getConfig(request)), + distinctUntilChanged()); } } diff --git a/src/app/core/data/browse-response-parsing.service.spec.ts b/src/app/core/data/browse-response-parsing.service.spec.ts index b0fbb1f977..2b1703e38f 100644 --- a/src/app/core/data/browse-response-parsing.service.spec.ts +++ b/src/app/core/data/browse-response-parsing.service.spec.ts @@ -10,134 +10,148 @@ describe('BrowseResponseParsingService', () => { beforeEach(() => { service = new BrowseResponseParsingService(); }); + let validRequest; + let validResponse; + let invalidResponse1; + let invalidResponse2; + let invalidResponse3; + let definitions; describe('parse', () => { - const validRequest = new BrowseEndpointRequest('client/b186e8ce-e99c-4183-bc9a-42b4821bdb78', 'https://rest.api/discover/browses'); + beforeEach(() => { + validRequest = new BrowseEndpointRequest('client/b186e8ce-e99c-4183-bc9a-42b4821bdb78', 'https://rest.api/discover/browses'); - const validResponse = { - payload: { - _embedded: { - browses: [{ - metadataBrowse: false, - sortOptions: [{ name: 'title', metadata: 'dc.title' }, { + validResponse = { + payload: { + _embedded: { + browses: [{ + metadataBrowse: false, + sortOptions: [{ name: 'title', metadata: 'dc.title' }, { + name: 'dateissued', + metadata: 'dc.date.issued' + }, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }], + order: 'ASC', + type: 'browse', + metadata: ['dc.date.issued'], + _links: { + self: { href: 'https://rest.api/discover/browses/dateissued' }, + items: { href: 'https://rest.api/discover/browses/dateissued/items' } + } + }, { + metadataBrowse: true, + sortOptions: [{ name: 'title', metadata: 'dc.title' }, { + name: 'dateissued', + metadata: 'dc.date.issued' + }, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }], + order: 'ASC', + type: 'browse', + metadata: ['dc.contributor.*', 'dc.creator'], + _links: { + self: { href: 'https://rest.api/discover/browses/author' }, + entries: { href: 'https://rest.api/discover/browses/author/entries' }, + items: { href: 'https://rest.api/discover/browses/author/items' } + } + }] + }, + _links: { self: { href: 'https://rest.api/discover/browses' } }, + page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } + }, statusCode: '200' + } as DSpaceRESTV2Response; + + invalidResponse1 = { + payload: { + _embedded: { + browse: { + metadataBrowse: false, + sortOptions: [{ name: 'title', metadata: 'dc.title' }, { + name: 'dateissued', + metadata: 'dc.date.issued' + }, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }], + order: 'ASC', + type: 'browse', + metadata: ['dc.date.issued'], + _links: { + self: { href: 'https://rest.api/discover/browses/dateissued' }, + items: { href: 'https://rest.api/discover/browses/dateissued/items' } + } + } + }, + _links: { self: { href: 'https://rest.api/discover/browses' } }, + page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } + }, statusCode: '200' + } as DSpaceRESTV2Response; + + invalidResponse2 = { + payload: { + _links: { self: { href: 'https://rest.api/discover/browses' } }, + page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } + }, statusCode: '200' + } as DSpaceRESTV2Response; + + invalidResponse3 = { + payload: { + _links: { self: { href: 'https://rest.api/discover/browses' } }, + page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } + }, statusCode: '500' + } as DSpaceRESTV2Response; + + definitions = [ + Object.assign(new BrowseDefinition(), { + metadataBrowse: false, + sortOptions: [ + { + name: 'title', + metadata: 'dc.title' + }, + { name: 'dateissued', metadata: 'dc.date.issued' - }, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }], - order: 'ASC', - type: 'browse', - metadata: ['dc.date.issued'], - _links: { - self: { href: 'https://rest.api/discover/browses/dateissued' }, - items: { href: 'https://rest.api/discover/browses/dateissued/items' } + }, + { + name: 'dateaccessioned', + metadata: 'dc.date.accessioned' } - }, { - metadataBrowse: true, - sortOptions: [{ name: 'title', metadata: 'dc.title' }, { + ], + defaultSortOrder: 'ASC', + type: 'browse', + metadataKeys: [ + 'dc.date.issued' + ], + _links: { + self: 'https://rest.api/discover/browses/dateissued', + items: 'https://rest.api/discover/browses/dateissued/items' + } + }), + Object.assign(new BrowseDefinition(), { + metadataBrowse: true, + sortOptions: [ + { + name: 'title', + metadata: 'dc.title' + }, + { name: 'dateissued', metadata: 'dc.date.issued' - }, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }], - order: 'ASC', - type: 'browse', - metadata: ['dc.contributor.*', 'dc.creator'], - _links: { - self: { href: 'https://rest.api/discover/browses/author' }, - entries: { href: 'https://rest.api/discover/browses/author/entries' }, - items: { href: 'https://rest.api/discover/browses/author/items' } - } - }] - }, - _links: { self: { href: 'https://rest.api/discover/browses' } }, - page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } - }, statusCode: '200' - } as DSpaceRESTV2Response; - - const invalidResponse1 = { - payload: { - _embedded: { - browse: { - metadataBrowse: false, - sortOptions: [{ name: 'title', metadata: 'dc.title' }, { - name: 'dateissued', - metadata: 'dc.date.issued' - }, { name: 'dateaccessioned', metadata: 'dc.date.accessioned' }], - order: 'ASC', - type: 'browse', - metadata: ['dc.date.issued'], - _links: { - self: { href: 'https://rest.api/discover/browses/dateissued' }, - items: { href: 'https://rest.api/discover/browses/dateissued/items' } + }, + { + name: 'dateaccessioned', + metadata: 'dc.date.accessioned' } + ], + defaultSortOrder: 'ASC', + type: 'browse', + metadataKeys: [ + 'dc.contributor.*', + 'dc.creator' + ], + _links: { + self: 'https://rest.api/discover/browses/author', + entries: 'https://rest.api/discover/browses/author/entries', + items: 'https://rest.api/discover/browses/author/items' } - }, - _links: { self: { href: 'https://rest.api/discover/browses' } }, - page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } - }, statusCode: '200' - } as DSpaceRESTV2Response; - - const invalidResponse2 = { - payload: { - _links: { self: { href: 'https://rest.api/discover/browses' } }, - page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } - }, statusCode: '200' - } as DSpaceRESTV2Response ; - - const invalidResponse3 = { - payload: { - _links: { self: { href: 'https://rest.api/discover/browses' } }, - page: { size: 20, totalElements: 2, totalPages: 1, number: 0 } - }, statusCode: '500' - } as DSpaceRESTV2Response; - - const definitions = [ - Object.assign(new BrowseDefinition(), { - metadataBrowse: false, - sortOptions: [ - { - name: 'title', - metadata: 'dc.title' - }, - { - name: 'dateissued', - metadata: 'dc.date.issued' - }, - { - name: 'dateaccessioned', - metadata: 'dc.date.accessioned' - } - ], - defaultSortOrder: 'ASC', - type: 'browse', - metadataKeys: [ - 'dc.date.issued' - ], - _links: { } - }), - Object.assign(new BrowseDefinition(), { - metadataBrowse: true, - sortOptions: [ - { - name: 'title', - metadata: 'dc.title' - }, - { - name: 'dateissued', - metadata: 'dc.date.issued' - }, - { - name: 'dateaccessioned', - metadata: 'dc.date.accessioned' - } - ], - defaultSortOrder: 'ASC', - type: 'browse', - metadataKeys: [ - 'dc.contributor.*', - 'dc.creator' - ], - _links: { } - }) - ]; - + }) + ]; + }); it('should return a GenericSuccessResponse if data contains a valid browse endpoint response', () => { const response = service.parse(validRequest, validResponse); expect(response.constructor).toBe(GenericSuccessResponse); diff --git a/src/app/core/data/comcol-data.service.spec.ts b/src/app/core/data/comcol-data.service.spec.ts index 59ae6619d8..dca7caedd4 100644 --- a/src/app/core/data/comcol-data.service.spec.ts +++ b/src/app/core/data/comcol-data.service.spec.ts @@ -1,10 +1,9 @@ import { Store } from '@ngrx/store'; import { cold, getTestScheduler, hot } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/Rx'; +import { TestScheduler } from 'rxjs/testing'; import { GlobalConfig } from '../../../config'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index bb624eda0f..95cfce6c70 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -1,9 +1,8 @@ -import { Observable } from 'rxjs/Observable'; +import { distinctUntilChanged, filter, map, mergeMap, take, tap } from 'rxjs/operators'; +import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs'; import { isEmpty, isNotEmpty } from '../../shared/empty.util'; import { NormalizedCommunity } from '../cache/models/normalized-community.model'; -import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { DSOSuccessResponse, ErrorResponse, RestResponse } from '../cache/response-cache.models'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { CommunityDataService } from './community-data.service'; @@ -12,7 +11,7 @@ import { FindAllOptions, FindByIDRequest } from './request.models'; import { NormalizedObject } from '../cache/models/normalized-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -export abstract class ComColDataService extends DataService { +export abstract class ComColDataService extends DataService { protected abstract cds: CommunityDataService; protected abstract objectCache: ObjectCacheService; protected abstract halService: HALEndpointService; @@ -31,29 +30,30 @@ export abstract class ComColDataService this.cds.getFindByIDHref(endpoint, options.scopeID)) - .filter((href: string) => isNotEmpty(href)) - .take(1) - .do((href: string) => { + const scopeCommunityHrefObs = this.cds.getEndpoint().pipe( + mergeMap((endpoint: string) => this.cds.getFindByIDHref(endpoint, options.scopeID)), + filter((href: string) => isNotEmpty(href)), + take(1), + tap((href: string) => { const request = new FindByIDRequest(this.requestService.generateRequestId(), href, options.scopeID); this.requestService.configure(request); - }); + })); - const [successResponse, errorResponse] = scopeCommunityHrefObs - .flatMap((href: string) => this.responseCache.get(href)) - .map((entry: ResponseCacheEntry) => entry.response) - .share() - .partition((response: RestResponse) => response.isSuccessful); + const responses = scopeCommunityHrefObs.pipe( + mergeMap((href: string) => this.responseCache.get(href)), + map((entry: ResponseCacheEntry) => entry.response)); + const errorResponses = responses.pipe( + filter((response) => !response.isSuccessful), + mergeMap(() => observableThrowError(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`))) + ); + const successResponses = responses.pipe( + filter((response) => response.isSuccessful), + mergeMap(() => this.objectCache.getByUUID(options.scopeID)), + map((nc: NormalizedCommunity) => nc._links[this.linkPath]), + filter((href) => isNotEmpty(href)) + ); - return Observable.merge( - errorResponse.flatMap((response: ErrorResponse) => - Observable.throw(new Error(`The Community with scope ${options.scopeID} couldn't be retrieved`))), - successResponse - .flatMap((response: DSOSuccessResponse) => this.objectCache.getByUUID(options.scopeID)) - .map((nc: NormalizedCommunity) => nc._links[this.linkPath]) - .filter((href) => isNotEmpty(href)) - ).distinctUntilChanged(); + return observableMerge(errorResponses, successResponses).pipe(distinctUntilChanged()); } } } diff --git a/src/app/core/data/community-data.service.ts b/src/app/core/data/community-data.service.ts index 9313823a33..6edd7fc23d 100644 --- a/src/app/core/data/community-data.service.ts +++ b/src/app/core/data/community-data.service.ts @@ -1,3 +1,5 @@ + +import {mergeMap, filter, take} from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; @@ -13,7 +15,7 @@ import { HALEndpointService } from '../shared/hal-endpoint.service'; import { FindAllOptions, FindAllRequest } from './request.models'; import { RemoteData } from './remote-data'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { PaginatedList } from './paginated-list'; @Injectable() @@ -38,11 +40,12 @@ export class CommunityDataService extends ComColDataService>> { - const hrefObs = this.getFindAllHref(options); + const hrefObs = this.halService.getEndpoint(this.topLinkPath).pipe(filter((href: string) => isNotEmpty(href)), + mergeMap((endpoint: string) => this.getFindAllHref(options)),); - hrefObs - .filter((href: string) => hasValue(href)) - .take(1) + hrefObs.pipe( + filter((href: string) => hasValue(href)), + take(1),) .subscribe((href: string) => { const request = new FindAllRequest(this.requestService.generateRequestId(), href, options); this.requestService.configure(request); diff --git a/src/app/core/data/data.service.spec.ts b/src/app/core/data/data.service.spec.ts index 7af06ff62a..d65bad9bbf 100644 --- a/src/app/core/data/data.service.spec.ts +++ b/src/app/core/data/data.service.spec.ts @@ -6,9 +6,10 @@ import { RemoteDataBuildService } from '../cache/builders/remote-data-build.serv import { CoreState } from '../core.reducers'; import { Store } from '@ngrx/store'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { FindAllOptions } from './request.models'; import { SortOptions, SortDirection } from '../cache/models/sort-options.model'; +import { of as observableOf } from 'rxjs'; const endpoint = 'https://rest.api/core'; @@ -17,107 +18,107 @@ class NormalizedTestObject extends NormalizedObject { } class TestService extends DataService { - constructor( - protected responseCache: ResponseCacheService, - protected requestService: RequestService, - protected rdbService: RemoteDataBuildService, - protected store: Store, - protected linkPath: string, - protected halService: HALEndpointService - ) { - super(); - } + constructor( + protected responseCache: ResponseCacheService, + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected store: Store, + protected linkPath: string, + protected halService: HALEndpointService + ) { + super(); + } - public getBrowseEndpoint(options: FindAllOptions): Observable { - return Observable.of(endpoint); - } + public getBrowseEndpoint(options: FindAllOptions): Observable { + return observableOf(endpoint); + } } describe('DataService', () => { - let service: TestService; - let options: FindAllOptions; - const responseCache = {} as ResponseCacheService; - const requestService = {} as RequestService; - const halService = {} as HALEndpointService; - const rdbService = {} as RemoteDataBuildService; - const store = {} as Store; + let service: TestService; + let options: FindAllOptions; + const responseCache = {} as ResponseCacheService; + const requestService = {} as RequestService; + const halService = {} as HALEndpointService; + const rdbService = {} as RemoteDataBuildService; + const store = {} as Store; - function initTestService(): TestService { - return new TestService( - responseCache, - requestService, - rdbService, - store, - endpoint, - halService - ); - } + function initTestService(): TestService { + return new TestService( + responseCache, + requestService, + rdbService, + store, + endpoint, + halService + ); + } - service = initTestService(); + service = initTestService(); - describe('getFindAllHref', () => { + describe('getFindAllHref', () => { - it('should return an observable with the endpoint', () => { - options = {}; + it('should return an observable with the endpoint', () => { + options = {}; - (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(endpoint); - } - ); - }); - - it('should include page in href if currentPage provided in options', () => { - options = { currentPage: 2 }; - const expected = `${endpoint}?page=${options.currentPage - 1}`; - - (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include size in href if elementsPerPage provided in options', () => { - options = { elementsPerPage: 5 }; - const expected = `${endpoint}?size=${options.elementsPerPage}`; - - (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include sort href if SortOptions provided in options', () => { - const sortOptions = new SortOptions('field1', SortDirection.ASC); - options = { sort: sortOptions}; - const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`; - - (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include startsWith in href if startsWith provided in options', () => { - options = { startsWith: 'ab' }; - const expected = `${endpoint}?startsWith=${options.startsWith}`; - - (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(expected); - }); - }); - - it('should include all provided options in href', () => { - const sortOptions = new SortOptions('field1', SortDirection.DESC) - options = { - currentPage: 6, - elementsPerPage: 10, - sort: sortOptions, - startsWith: 'ab' - } - const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` + - `&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`; - - (service as any).getFindAllHref(options).subscribe((value) => { - expect(value).toBe(expected); - }); - }) + (service as any).getFindAllHref(options).subscribe((value) => { + expect(value).toBe(endpoint); + } + ); }); + it('should include page in href if currentPage provided in options', () => { + options = { currentPage: 2 }; + const expected = `${endpoint}?page=${options.currentPage - 1}`; + + (service as any).getFindAllHref(options).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + + it('should include size in href if elementsPerPage provided in options', () => { + options = { elementsPerPage: 5 }; + const expected = `${endpoint}?size=${options.elementsPerPage}`; + + (service as any).getFindAllHref(options).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + + it('should include sort href if SortOptions provided in options', () => { + const sortOptions = new SortOptions('field1', SortDirection.ASC); + options = { sort: sortOptions }; + const expected = `${endpoint}?sort=${sortOptions.field},${sortOptions.direction}`; + + (service as any).getFindAllHref(options).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + + it('should include startsWith in href if startsWith provided in options', () => { + options = { startsWith: 'ab' }; + const expected = `${endpoint}?startsWith=${options.startsWith}`; + + (service as any).getFindAllHref(options).subscribe((value) => { + expect(value).toBe(expected); + }); + }); + + it('should include all provided options in href', () => { + const sortOptions = new SortOptions('field1', SortDirection.DESC) + options = { + currentPage: 6, + elementsPerPage: 10, + sort: sortOptions, + startsWith: 'ab' + } + const expected = `${endpoint}?page=${options.currentPage - 1}&size=${options.elementsPerPage}` + + `&sort=${sortOptions.field},${sortOptions.direction}&startsWith=${options.startsWith}`; + + (service as any).getFindAllHref(options).subscribe((value) => { + expect(value).toBe(expected); + }); + }) + }); + }); diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 3948c3672f..f52f61cea1 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,6 +1,6 @@ -import { filter, take } from 'rxjs/operators'; +import { distinctUntilChanged, filter, take, first, map } from 'rxjs/operators'; +import { of as observableOf, Observable } from 'rxjs'; import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs/Observable'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ResponseCacheService } from '../cache/response-cache.service'; @@ -27,7 +27,7 @@ export abstract class DataService let result: Observable; const args = []; - result = this.getBrowseEndpoint(options).distinctUntilChanged(); + result = this.getBrowseEndpoint(options).pipe(distinctUntilChanged()); if (hasValue(options.currentPage) && typeof options.currentPage === 'number') { /* TODO: this is a temporary fix for the pagination start index (0 or 1) discrepancy between the rest and the frontend respectively */ @@ -47,7 +47,7 @@ export abstract class DataService } if (isNotEmpty(args)) { - return result.map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString()); + return result.pipe(map((href: string) => new URLCombiner(href, `?${args.join('&')}`).toString())); } else { return result; } @@ -72,11 +72,11 @@ export abstract class DataService } findById(id: string): Observable> { - const hrefObs = this.halService.getEndpoint(this.linkPath) - .map((endpoint: string) => this.getFindByIDHref(endpoint, id)); + const hrefObs = this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getFindByIDHref(endpoint, id))); - hrefObs - .first((href: string) => hasValue(href)) + hrefObs.pipe( + first((href: string) => hasValue(href))) .subscribe((href: string) => { const request = new FindByIDRequest(this.requestService.generateRequestId(), href, id); this.requestService.configure(request); diff --git a/src/app/core/data/dspace-object-data.service.spec.ts b/src/app/core/data/dspace-object-data.service.spec.ts index bb2bdc675d..ef767c5d2d 100644 --- a/src/app/core/data/dspace-object-data.service.spec.ts +++ b/src/app/core/data/dspace-object-data.service.spec.ts @@ -1,5 +1,5 @@ import { cold, getTestScheduler } from 'jasmine-marbles'; -import { TestScheduler } from '../../../../node_modules/rxjs'; +import { TestScheduler } from 'rxjs/testing'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { DSpaceObject } from '../shared/dspace-object.model'; import { HALEndpointService } from '../shared/hal-endpoint.service'; diff --git a/src/app/core/data/dspace-object-data.service.ts b/src/app/core/data/dspace-object-data.service.ts index afd9d74f5b..c44f4ce1d1 100644 --- a/src/app/core/data/dspace-object-data.service.ts +++ b/src/app/core/data/dspace-object-data.service.ts @@ -1,6 +1,6 @@ import { Injectable } from '@angular/core'; import { Store } from '@ngrx/store'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedDSpaceObject } from '../cache/models/normalized-dspace-object.model'; import { ResponseCacheService } from '../cache/response-cache.service'; diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 4cf126157a..8e2db15921 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -1,6 +1,6 @@ import { Store } from '@ngrx/store'; import { cold, getTestScheduler } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/Rx'; +import { TestScheduler } from 'rxjs/testing'; import { BrowseService } from '../browse/browse.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { ResponseCacheService } from '../cache/response-cache.service'; diff --git a/src/app/core/data/item-data.service.ts b/src/app/core/data/item-data.service.ts index 90311a6f82..d6c76bff2b 100644 --- a/src/app/core/data/item-data.service.ts +++ b/src/app/core/data/item-data.service.ts @@ -1,7 +1,8 @@ import {Injectable} from '@angular/core'; -import {Store} from '@ngrx/store'; -import {Observable} from 'rxjs/Observable'; +import {distinctUntilChanged, map, filter} from 'rxjs/operators'; +import { Injectable } from '@angular/core';import {Store} from '@ngrx/store'; +import {Observable} from 'rxjs'; import {isNotEmpty, isNotEmptyOperator} from '../../shared/empty.util'; import {BrowseService} from '../browse/browse.service'; import {RemoteDataBuildService} from '../cache/builders/remote-data-build.service'; @@ -15,7 +16,6 @@ import {DataService} from './data.service'; import {RequestService} from './request.service'; import {HALEndpointService} from '../shared/hal-endpoint.service'; import {FindAllOptions, PostRequest, RestRequest} from './request.models'; -import {distinctUntilChanged, map} from 'rxjs/operators'; import {RestResponse} from '../cache/response-cache.models'; import {configureRequest, getResponseFromSelflink} from '../shared/operators'; import {ResponseCacheEntry} from '../cache/response-cache.reducer'; @@ -45,10 +45,10 @@ export class ItemDataService extends DataService { if (options.sort && options.sort.field) { field = options.sort.field; } - return this.bs.getBrowseURLFor(field, this.linkPath) - .filter((href: string) => isNotEmpty(href)) - .map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}`).toString()) - .distinctUntilChanged(); + return this.bs.getBrowseURLFor(field, this.linkPath).pipe( + filter((href: string) => isNotEmpty(href)), + map((href: string) => new URLCombiner(href, `?scope=${options.scopeID}`).toString()), + distinctUntilChanged(),); } public getMoveItemEndpoint(itemId: string, collectionId?: string): Observable { diff --git a/src/app/core/data/request.effects.ts b/src/app/core/data/request.effects.ts index 5fadd316f4..29c9ced472 100644 --- a/src/app/core/data/request.effects.ts +++ b/src/app/core/data/request.effects.ts @@ -1,9 +1,9 @@ + +import {of as observableOf, Observable } from 'rxjs'; import { Inject, Injectable, Injector } from '@angular/core'; import { Request } from '@angular/http'; import { RequestArgs } from '@angular/http/src/interfaces'; import { Actions, Effect, ofType } from '@ngrx/effects'; -// tslint:disable-next-line:import-blacklist -import { Observable } from 'rxjs'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { isNotEmpty } from '../../shared/empty.util'; @@ -30,7 +30,8 @@ export const addToResponseCacheAndCompleteAction = (request: RestRequest, respon @Injectable() export class RequestEffects { - @Effect() execute = this.actions$.ofType(RequestActionTypes.EXECUTE).pipe( + @Effect() execute = this.actions$.pipe( + ofType(RequestActionTypes.EXECUTE), flatMap((action: RequestExecuteAction) => { return this.requestService.getByUUID(action.payload).pipe( take(1) @@ -46,7 +47,7 @@ export class RequestEffects { return this.restApi.request(request.method, request.href, body, request.options).pipe( map((data: DSpaceRESTV2Response) => this.injector.get(request.getResponseParser()).parse(request, data)), addToResponseCacheAndCompleteAction(request, this.responseCache, this.EnvConfig), - catchError((error: RequestError) => Observable.of(new ErrorResponse(error)).pipe( + catchError((error: RequestError) => observableOf(new ErrorResponse(error)).pipe( addToResponseCacheAndCompleteAction(request, this.responseCache, this.EnvConfig) )) ); diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index aa9954f680..39f29a9beb 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -1,16 +1,14 @@ -import { Store } from '@ngrx/store'; -import { cold, hot } from 'jasmine-marbles'; -import { Observable } from 'rxjs/Observable'; -import 'rxjs/add/observable/of'; +import { cold, getTestScheduler, hot } from 'jasmine-marbles'; +import { of as observableOf } from 'rxjs'; import { getMockObjectCacheService } from '../../shared/mocks/mock-object-cache.service'; import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service'; -import { getMockStore } from '../../shared/mocks/mock-store'; import { defaultUUID, getMockUUIDService } from '../../shared/mocks/mock-uuid.service'; import { ObjectCacheService } from '../cache/object-cache.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { CoreState } from '../core.reducers'; import { UUIDService } from '../shared/uuid.service'; import { RequestConfigureAction, RequestExecuteAction } from './request.actions'; +import * as ngrx from '@ngrx/store'; import { DeleteRequest, GetRequest, @@ -18,11 +16,16 @@ import { OptionsRequest, PatchRequest, PostRequest, - PutRequest, RestRequest + PutRequest, + RestRequest } from './request.models'; import { RequestService } from './request.service'; +import { ActionsSubject, Store } from '@ngrx/store'; +import { TestScheduler } from 'rxjs/testing'; +import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; describe('RequestService', () => { + let scheduler: TestScheduler; let service: RequestService; let serviceAsAny: any; let objectCache: ObjectCacheService; @@ -39,19 +42,26 @@ describe('RequestService', () => { const testOptionsRequest = new OptionsRequest(testUUID, testHref); const testHeadRequest = new HeadRequest(testUUID, testHref); const testPatchRequest = new PatchRequest(testUUID, testHref); - + let selectSpy; beforeEach(() => { + scheduler = getTestScheduler(); + objectCache = getMockObjectCacheService(); (objectCache.hasBySelfLink as any).and.returnValue(false); responseCache = getMockResponseCacheService(); (responseCache.has as any).and.returnValue(false); - (responseCache.get as any).and.returnValue(Observable.of(undefined)); + (responseCache.get as any).and.returnValue(observableOf(undefined)); uuidService = getMockUUIDService(); - store = getMockStore(); - (store.select as any).and.returnValue(Observable.of(undefined)); + store = new Store(new BehaviorSubject({}), new ActionsSubject(), null); + selectSpy = spyOnProperty(ngrx, 'select') + selectSpy.and.callFake(() => { + return () => { + return () => cold('a', { a: undefined }); + }; + }); service = new RequestService( objectCache, @@ -74,7 +84,7 @@ describe('RequestService', () => { describe('isPending', () => { describe('before the request is configured', () => { beforeEach(() => { - spyOn(service, 'getByHref').and.returnValue(Observable.of(undefined)); + spyOn(service, 'getByHref').and.returnValue(observableOf(undefined)); }); it('should return false', () => { @@ -87,7 +97,7 @@ describe('RequestService', () => { describe('when the request has been configured but hasn\'t reached the store yet', () => { beforeEach(() => { - spyOn(service, 'getByHref').and.returnValue(Observable.of(undefined)); + spyOn(service, 'getByHref').and.returnValue(observableOf(undefined)); serviceAsAny.requestsOnTheirWayToTheStore = [testHref]; }); @@ -101,7 +111,7 @@ describe('RequestService', () => { describe('when the request has reached the store, before the server responds', () => { beforeEach(() => { - spyOn(service, 'getByHref').and.returnValue(Observable.of({ + spyOn(service, 'getByHref').and.returnValue(observableOf({ completed: false })) }); @@ -116,7 +126,7 @@ describe('RequestService', () => { describe('after the server responds', () => { beforeEach(() => { - spyOn(service, 'getByHref').and.returnValues(Observable.of({ + spyOn(service, 'getByHref').and.returnValues(observableOf({ completed: true })); }); @@ -134,11 +144,15 @@ describe('RequestService', () => { describe('getByUUID', () => { describe('if the request with the specified UUID exists in the store', () => { beforeEach(() => { - (store.select as any).and.returnValues(hot('a', { - a: { - completed: true - } - })); + selectSpy.and.callFake(() => { + return () => { + return () => hot('a', { + a: { + completed: true + } + }); + }; + }); }); it('should return an Observable of the RequestEntry', () => { @@ -155,9 +169,11 @@ describe('RequestService', () => { describe('if the request with the specified UUID doesn\'t exist in the store', () => { beforeEach(() => { - (store.select as any).and.returnValues(hot('a', { - a: undefined - })); + selectSpy.and.callFake(() => { + return () => { + return () => hot('a', { a: undefined }); + }; + }); }); it('should return an Observable of undefined', () => { @@ -175,9 +191,11 @@ describe('RequestService', () => { describe('getByHref', () => { describe('when the request with the specified href exists in the store', () => { beforeEach(() => { - (store.select as any).and.returnValues(hot('a', { - a: testUUID - })); + selectSpy.and.callFake(() => { + return () => { + return () => hot('a', { a: testUUID }); + }; + }); spyOn(service, 'getByUUID').and.returnValue(cold('b', { b: { completed: true @@ -199,9 +217,11 @@ describe('RequestService', () => { describe('when the request with the specified href doesn\'t exist in the store', () => { beforeEach(() => { - (store.select as any).and.returnValues(hot('a', { - a: undefined - })); + selectSpy.and.callFake(() => { + return () => { + return () => hot('a', { a: undefined }); + }; + }); spyOn(service, 'getByUUID').and.returnValue(cold('b', { b: undefined })); @@ -241,7 +261,8 @@ describe('RequestService', () => { }); it('should dispatch the request', () => { - service.configure(request); + scheduler.schedule(() => service.configure(request)); + scheduler.flush(); expect(serviceAsAny.dispatchRequest).toHaveBeenCalledWith(request); }); }); @@ -323,7 +344,7 @@ describe('RequestService', () => { describe('and it\'s a DSOSuccessResponse', () => { beforeEach(() => { - (responseCache.get as any).and.returnValues(Observable.of({ + (responseCache.get as any).and.returnValues(observableOf({ response: { isSuccessful: true, resourceSelfLinks: [ @@ -356,7 +377,7 @@ describe('RequestService', () => { beforeEach(() => { (objectCache.hasBySelfLink as any).and.returnValues(false); (responseCache.has as any).and.returnValues(true); - (responseCache.get as any).and.returnValues(Observable.of({ + (responseCache.get as any).and.returnValues(observableOf({ response: { isSuccessful: true } @@ -398,6 +419,10 @@ describe('RequestService', () => { }); describe('dispatchRequest', () => { + beforeEach(() => { + spyOn(store, 'dispatch'); + }); + it('should dispatch a RequestConfigureAction', () => { const request = testGetRequest; serviceAsAny.dispatchRequest(request); @@ -428,7 +453,11 @@ describe('RequestService', () => { describe('when the request is added to the store', () => { it('should stop tracking the request', () => { - (store.select as any).and.returnValues(Observable.of({ request })); + selectSpy.and.callFake(() => { + return () => { + return () => observableOf({ request }); + }; + }); serviceAsAny.trackRequestsOnTheirWayToTheStore(request); expect(serviceAsAny.requestsOnTheirWayToTheStore.includes(request.href)).toBeFalsy(); }); diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index 12933f83fc..101825e3db 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -1,8 +1,8 @@ +import { Observable, merge as observableMerge } from 'rxjs'; +import { filter, first, map, mergeMap, partition, take } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { createSelector, MemoizedSelector, Store } from '@ngrx/store'; - -import { Observable } from 'rxjs/Observable'; +import { MemoizedSelector, select, Store } from '@ngrx/store'; import { hasValue } from '../../shared/empty.util'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -16,8 +16,7 @@ import { UUIDService } from '../shared/uuid.service'; import { RequestConfigureAction, RequestExecuteAction } from './request.actions'; import { GetRequest, RestRequest, RestRequestMethod } from './request.models'; -import { RequestEntry, RequestState } from './request.reducer'; -import { ResponseCacheRemoveAction } from '../cache/response-cache.actions'; +import { RequestEntry } from './request.reducer'; @Injectable() export class RequestService { @@ -49,8 +48,8 @@ export class RequestService { // then check the store let isPending = false; - this.getByHref(request.href) - .take(1) + this.getByHref(request.href).pipe( + take(1)) .subscribe((re: RequestEntry) => { isPending = (hasValue(re) && !re.completed) }); @@ -59,12 +58,14 @@ export class RequestService { } getByUUID(uuid: string): Observable { - return this.store.select(this.entryFromUUIDSelector(uuid)); + return this.store.pipe(select(this.entryFromUUIDSelector(uuid))); } getByHref(href: string): Observable { - return this.store.select(this.uuidFromHrefSelector(href)) - .flatMap((uuid: string) => this.getByUUID(uuid)); + return this.store.pipe( + select(this.uuidFromHrefSelector(href)), + mergeMap((uuid: string) => this.getByUUID(uuid)) + ); } // TODO to review "overrideRequest" param when https://github.com/DSpace/dspace-angular/issues/217 will be fixed @@ -81,29 +82,23 @@ export class RequestService { private isCachedOrPending(request: GetRequest) { let isCached = this.objectCache.hasBySelfLink(request.href); if (!isCached && this.responseCache.has(request.href)) { - const [successResponse, errorResponse] = this.responseCache.get(request.href) - .take(1) - .map((entry: ResponseCacheEntry) => entry.response) - .share() - .partition((response: RestResponse) => response.isSuccessful); + const responses = this.responseCache.get(request.href).pipe( + take(1), + map((entry: ResponseCacheEntry) => entry.response) + ); - const [dsoSuccessResponse, otherSuccessResponse] = successResponse - .share() - .partition((response: DSOSuccessResponse) => hasValue(response.resourceSelfLinks)); + const errorResponses = responses.pipe(filter((response) => !response.isSuccessful), map(() => true)); // TODO add a configurable number of retries in case of an error. + const dsoSuccessResponses = responses.pipe( + filter((response) => response.isSuccessful && hasValue((response as DSOSuccessResponse).resourceSelfLinks)), + map((response: DSOSuccessResponse) => response.resourceSelfLinks), + map((resourceSelfLinks: string[]) => resourceSelfLinks + .every((selfLink) => this.objectCache.hasBySelfLink(selfLink)) + )); + const otherSuccessResponses = responses.pipe(filter((response) => response.isSuccessful && !hasValue((response as DSOSuccessResponse).resourceSelfLinks)), map(() => true)); - Observable.merge( - errorResponse.map(() => true), // TODO add a configurable number of retries in case of an error. - otherSuccessResponse.map(() => true), - dsoSuccessResponse // a DSOSuccessResponse should only be considered cached if all its resources are cached - .map((response: DSOSuccessResponse) => response.resourceSelfLinks) - .map((resourceSelfLinks: string[]) => resourceSelfLinks - .every((selfLink) => this.objectCache.hasBySelfLink(selfLink)) - ) - ).subscribe((c) => isCached = c); + observableMerge(errorResponses, otherSuccessResponses, dsoSuccessResponses).subscribe((c) => isCached = c); } - const isPending = this.isPending(request); - return isCached || isPending; } @@ -121,11 +116,11 @@ export class RequestService { */ private trackRequestsOnTheirWayToTheStore(request: GetRequest) { this.requestsOnTheirWayToTheStore = [...this.requestsOnTheirWayToTheStore, request.href]; - this.store.select(this.entryFromUUIDSelector(request.href)) - .filter((re: RequestEntry) => hasValue(re)) - .take(1) - .subscribe((re: RequestEntry) => { - this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((pendingHref: string) => pendingHref !== request.href) - }); + this.store.pipe(select(this.entryFromUUIDSelector(request.href)), + filter((re: RequestEntry) => hasValue(re)), + take(1) + ).subscribe((re: RequestEntry) => { + this.requestsOnTheirWayToTheStore = this.requestsOnTheirWayToTheStore.filter((pendingHref: string) => pendingHref !== request.href) + }); } } diff --git a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts index 78c93b8c08..1570613c17 100644 --- a/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts +++ b/src/app/core/dspace-rest-v2/dspace-rest-v2.service.ts @@ -1,7 +1,8 @@ +import {throwError as observableThrowError, Observable } from 'rxjs'; +import {catchError, map} from 'rxjs/operators'; import { Injectable } from '@angular/core'; import { Request } from '@angular/http'; import { HttpClient, HttpHeaders, HttpParams, HttpResponse } from '@angular/common/http' -import { Observable } from 'rxjs/Observable'; import { RestRequestMethod } from '../data/request.models'; import { DSpaceRESTV2Response } from './dspace-rest-v2-response.model'; @@ -36,12 +37,12 @@ export class DSpaceRESTv2Service { * An Observable containing the response from the server */ get(absoluteURL: string): Observable { - return this.http.get(absoluteURL, { observe: 'response' }) - .map((res: HttpResponse) => ({ payload: res.body, statusCode: res.statusText })) - .catch((err) => { + return this.http.get(absoluteURL, { observe: 'response' }).pipe( + map((res: HttpResponse) => ({ payload: res.body, statusCode: res.statusText })), + catchError((err) => { console.log('Error: ', err); - return Observable.throw(err); - }); + return observableThrowError(err); + })); } /** @@ -66,12 +67,12 @@ export class DSpaceRESTv2Service { if (options && options.responseType) { requestOptions.responseType = options.responseType; } - return this.http.request(method, url, requestOptions) - .map((res) => ({ payload: res.body, headers: res.headers, statusCode: res.statusText })) - .catch((err) => { + return this.http.request(method, url, requestOptions).pipe( + map((res) => ({ payload: res.body, headers: res.headers, statusCode: res.statusText })), + catchError((err) => { console.log('Error: ', err); - return Observable.throw(err); - }); + return observableThrowError(err); + })); } } diff --git a/src/app/core/index/index.effects.ts b/src/app/core/index/index.effects.ts index 05ae529c8e..de1ba681a2 100644 --- a/src/app/core/index/index.effects.ts +++ b/src/app/core/index/index.effects.ts @@ -1,5 +1,6 @@ +import { filter, map } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { Effect, Actions } from '@ngrx/effects'; +import { Effect, Actions, ofType } from '@ngrx/effects'; import { ObjectCacheActionTypes, AddToObjectCacheAction, @@ -15,44 +16,52 @@ import { IndexName } from './index.reducer'; export class UUIDIndexEffects { @Effect() addObject$ = this.actions$ - .ofType(ObjectCacheActionTypes.ADD) - .filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid)) - .map((action: AddToObjectCacheAction) => { - return new AddToIndexAction( - IndexName.OBJECT, - action.payload.objectToCache.uuid, - action.payload.objectToCache.self - ); - }); + .pipe( + ofType(ObjectCacheActionTypes.ADD), + filter((action: AddToObjectCacheAction) => hasValue(action.payload.objectToCache.uuid)), + map((action: AddToObjectCacheAction) => { + return new AddToIndexAction( + IndexName.OBJECT, + action.payload.objectToCache.uuid, + action.payload.objectToCache.self + ); + }) + ); @Effect() removeObject$ = this.actions$ - .ofType(ObjectCacheActionTypes.REMOVE) - .map((action: RemoveFromObjectCacheAction) => { - return new RemoveFromIndexByValueAction( - IndexName.OBJECT, - action.payload - ); - }); + .pipe( + ofType(ObjectCacheActionTypes.REMOVE), + map((action: RemoveFromObjectCacheAction) => { + return new RemoveFromIndexByValueAction( + IndexName.OBJECT, + action.payload + ); + }) + ); @Effect() addRequest$ = this.actions$ - .ofType(RequestActionTypes.CONFIGURE) - .filter((action: RequestConfigureAction) => action.payload.method === RestRequestMethod.Get) - .map((action: RequestConfigureAction) => { - return new AddToIndexAction( - IndexName.REQUEST, - action.payload.href, - action.payload.uuid - ); - }); + .pipe( + ofType(RequestActionTypes.CONFIGURE), + filter((action: RequestConfigureAction) => action.payload.method === RestRequestMethod.Get), + map((action: RequestConfigureAction) => { + return new AddToIndexAction( + IndexName.REQUEST, + action.payload.href, + action.payload.uuid + ); + }) + ); // @Effect() removeRequest$ = this.actions$ - // .ofType(ObjectCacheActionTypes.REMOVE) - // .map((action: RemoveFromObjectCacheAction) => { + // .pipe( + // ofType(ObjectCacheActionTypes.REMOVE), + // map((action: RemoveFromObjectCacheAction) => { // return new RemoveFromIndexByValueAction( // IndexName.OBJECT, // action.payload // ); - // }); + // }) + // ) constructor(private actions$: Actions) { diff --git a/src/app/core/integration/integration.service.spec.ts b/src/app/core/integration/integration.service.spec.ts index b7f4e019f7..f7e3769620 100644 --- a/src/app/core/integration/integration.service.spec.ts +++ b/src/app/core/integration/integration.service.spec.ts @@ -1,5 +1,5 @@ import { cold, getTestScheduler } from 'jasmine-marbles'; -import { TestScheduler } from 'rxjs/Rx'; +import { TestScheduler } from 'rxjs/testing'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; diff --git a/src/app/core/integration/integration.service.ts b/src/app/core/integration/integration.service.ts index f1c770336a..3c71ca5f3b 100644 --- a/src/app/core/integration/integration.service.ts +++ b/src/app/core/integration/integration.service.ts @@ -1,7 +1,8 @@ -import { Observable } from 'rxjs/Observable'; +import { Observable, of as observableOf, throwError as observableThrowError } from 'rxjs'; +import { distinctUntilChanged, filter, map, mergeMap, tap } from 'rxjs/operators'; import { RequestService } from '../data/request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; -import { ErrorResponse, IntegrationSuccessResponse, RestResponse } from '../cache/response-cache.models'; +import { IntegrationSuccessResponse } from '../cache/response-cache.models'; import { GetRequest, IntegrationRequest } from '../data/request.models'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; @@ -18,16 +19,18 @@ export abstract class IntegrationService { protected abstract halService: HALEndpointService; protected getData(request: GetRequest): Observable { - const [successResponse, errorResponse] = this.responseCache.get(request.href) - .map((entry: ResponseCacheEntry) => entry.response) - .partition((response: RestResponse) => response.isSuccessful); - return Observable.merge( - errorResponse.flatMap((response: ErrorResponse) => - Observable.throw(new Error(`Couldn't retrieve the integration data`))), - successResponse - .filter((response: IntegrationSuccessResponse) => isNotEmpty(response)) - .map((response: IntegrationSuccessResponse) => new IntegrationData(response.pageInfo, response.dataDefinition)) - .distinctUntilChanged()); + return this.responseCache.get(request.href).pipe( + map((entry: ResponseCacheEntry) => entry.response), + mergeMap((response) => { + if (response.isSuccessful && isNotEmpty(response)) { + const dataResponse = response as IntegrationSuccessResponse; + return observableOf(new IntegrationData(dataResponse.pageInfo, dataResponse.dataDefinition)); + } else if (!response.isSuccessful) { + return observableThrowError(new Error(`Couldn't retrieve the integration data`)); + } + }), + distinctUntilChanged() + ); } protected getIntegrationHref(endpoint, options: IntegrationSearchOptions = new IntegrationSearchOptions()): string { @@ -72,14 +75,14 @@ export abstract class IntegrationService { } public getEntriesByName(options: IntegrationSearchOptions): Observable { - return this.halService.getEndpoint(this.linkPath) - .map((endpoint: string) => this.getIntegrationHref(endpoint, options)) - .filter((href: string) => isNotEmpty(href)) - .distinctUntilChanged() - .map((endpointURL: string) => new IntegrationRequest(this.requestService.generateRequestId(), endpointURL)) - .do((request: GetRequest) => this.requestService.configure(request)) - .flatMap((request: GetRequest) => this.getData(request)) - .distinctUntilChanged(); + return this.halService.getEndpoint(this.linkPath).pipe( + map((endpoint: string) => this.getIntegrationHref(endpoint, options)), + filter((href: string) => isNotEmpty(href)), + distinctUntilChanged(), + map((endpointURL: string) => new IntegrationRequest(this.requestService.generateRequestId(), endpointURL)), + tap((request: GetRequest) => this.requestService.configure(request)), + mergeMap((request: GetRequest) => this.getData(request)), + distinctUntilChanged()); } } diff --git a/src/app/core/metadata/metadata.service.spec.ts b/src/app/core/metadata/metadata.service.spec.ts index c9e159f08f..2456ae9e55 100644 --- a/src/app/core/metadata/metadata.service.spec.ts +++ b/src/app/core/metadata/metadata.service.spec.ts @@ -1,17 +1,15 @@ -import { ComponentFixture, TestBed, async, fakeAsync, inject, tick } from '@angular/core/testing'; +import { ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; -import { Location, CommonModule } from '@angular/common'; +import { CommonModule, Location } from '@angular/common'; import { Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; -import { By, Meta, MetaDefinition, Title } from '@angular/platform-browser'; +import { Meta, MetaDefinition, Title } from '@angular/platform-browser'; import { ActivatedRoute, Router } from '@angular/router'; -import { TranslateModule, TranslateLoader } from '@ngx-translate/core'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { Store, StoreModule } from '@ngrx/store'; - -import { Observable } from 'rxjs/Observable'; -import { RemoteDataError } from '../data/remote-data-error'; +import { Observable, of as observableOf } from 'rxjs'; import { UUIDService } from '../shared/uuid.service'; import { MetadataService } from './metadata.service'; @@ -34,9 +32,7 @@ import { MockItem } from '../../shared/mocks/mock-item'; import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; import { BrowseService } from '../browse/browse.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { PaginatedList } from '../data/paginated-list'; -import { PageInfo } from '../shared/page-info.model'; -import { EmptyError } from 'rxjs/util/EmptyError'; +import { EmptyError } from 'rxjs/internal-compatibility'; /* tslint:disable:max-classes-per-file */ @Component({ @@ -187,7 +183,7 @@ describe('MetadataService', () => { describe('when the item has no bitstreams', () => { beforeEach(() => { - spyOn(MockItem, 'getFiles').and.returnValue(Observable.of([])); + spyOn(MockItem, 'getFiles').and.returnValue(observableOf([])); }); it('processRemoteData should not produce an EmptyError', fakeAsync(() => { @@ -201,7 +197,7 @@ describe('MetadataService', () => { }); const mockRemoteData = (mockItem: Item): Observable> => { - return Observable.of(new RemoteData( + return observableOf(new RemoteData( false, false, true, diff --git a/src/app/core/metadata/metadata.service.ts b/src/app/core/metadata/metadata.service.ts index 89401be06c..3a63be3f55 100644 --- a/src/app/core/metadata/metadata.service.ts +++ b/src/app/core/metadata/metadata.service.ts @@ -1,28 +1,18 @@ -import 'rxjs/add/operator/first' -import 'rxjs/add/operator/take' - +import { catchError, distinctUntilKeyChanged, filter, first, map, take } from 'rxjs/operators'; import { Inject, Injectable } from '@angular/core'; -import { - ActivatedRoute, - Event, - NavigationEnd, - Params, - Router -} from '@angular/router'; +import { ActivatedRoute, NavigationEnd, Router } from '@angular/router'; import { Meta, MetaDefinition, Title } from '@angular/platform-browser'; import { TranslateService } from '@ngx-translate/core'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; -import { Observable } from 'rxjs/Observable'; +import { BehaviorSubject, Observable } from 'rxjs'; import { RemoteData } from '../data/remote-data'; import { Bitstream } from '../shared/bitstream.model'; import { CacheableObject } from '../cache/object-cache.reducer'; import { DSpaceObject } from '../shared/dspace-object.model'; import { Item } from '../shared/item.model'; -import { Metadatum } from '../shared/metadatum.model'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../config'; import { BitstreamFormat } from '../shared/bitstream-format.model'; @@ -55,21 +45,21 @@ export class MetadataService { } public listenForRouteChange(): void { - this.router.events - .filter((event) => event instanceof NavigationEnd) - .map(() => this.router.routerState.root) - .map((route: ActivatedRoute) => { + this.router.events.pipe( + filter((event) => event instanceof NavigationEnd), + map(() => this.router.routerState.root), + map((route: ActivatedRoute) => { route = this.getCurrentRoute(route); return { params: route.params, data: route.data }; - }).subscribe((routeInfo: any) => { - this.processRouteChange(routeInfo); - }); + }),).subscribe((routeInfo: any) => { + this.processRouteChange(routeInfo); + }); } public processRemoteData(remoteData: Observable>): void { - remoteData.map((rd: RemoteData) => rd.payload) - .filter((co: CacheableObject) => hasValue(co)) - .take(1) + remoteData.pipe(map((rd: RemoteData) => rd.payload), + filter((co: CacheableObject) => hasValue(co)), + take(1),) .subscribe((dspaceObject: DSpaceObject) => { if (!this.initialized) { this.initialize(dspaceObject); @@ -83,13 +73,13 @@ export class MetadataService { this.clearMetaTags(); } if (routeInfo.data.value.title) { - this.translate.get(routeInfo.data.value.title).take(1).subscribe((translatedTitle: string) => { + this.translate.get(routeInfo.data.value.title).pipe(take(1)).subscribe((translatedTitle: string) => { this.addMetaTag('title', translatedTitle); this.title.setTitle(translatedTitle); }); } if (routeInfo.data.value.description) { - this.translate.get(routeInfo.data.value.description).take(1).subscribe((translatedDescription: string) => { + this.translate.get(routeInfo.data.value.description).pipe(take(1)).subscribe((translatedDescription: string) => { this.addMetaTag('description', translatedDescription); }); } @@ -97,7 +87,7 @@ export class MetadataService { private initialize(dspaceObject: DSpaceObject): void { this.currentObject = new BehaviorSubject(dspaceObject); - this.currentObject.asObservable().distinctUntilKeyChanged('uuid').subscribe(() => { + this.currentObject.asObservable().pipe(distinctUntilKeyChanged('uuid')).subscribe(() => { this.setMetaTags(); }); this.initialized = true; @@ -270,20 +260,25 @@ export class MetadataService { if (this.currentObject.value instanceof Item) { const item = this.currentObject.value as Item; item.getFiles() - .first((files) => isNotEmpty(files)) - .catch((error) => { console.debug(error); return [] }) + .pipe( + first((files) => isNotEmpty(files)), + catchError((error) => { + console.debug(error); + return [] + })) .subscribe((bitstreams: Bitstream[]) => { for (const bitstream of bitstreams) { - bitstream.format.first() - .map((rd: RemoteData) => rd.payload) - .filter((format: BitstreamFormat) => hasValue(format)) - .subscribe((format: BitstreamFormat) => { - if (format.mimetype === 'application/pdf') { - this.addMetaTag('citation_pdf_url', bitstream.content); - } - }); - } - }); + bitstream.format.pipe( + first(), + map((rd: RemoteData) => rd.payload), + filter((format: BitstreamFormat) => hasValue(format))) + .subscribe((format: BitstreamFormat) => { + if (format.mimetype === 'application/pdf') { + this.addMetaTag('citation_pdf_url', bitstream.content); + } + }); + } + }); } } diff --git a/src/app/core/registry/registry.service.spec.ts b/src/app/core/registry/registry.service.spec.ts index ef1533278d..d0ed1e5cb8 100644 --- a/src/app/core/registry/registry.service.spec.ts +++ b/src/app/core/registry/registry.service.spec.ts @@ -1,4 +1,4 @@ -import { async, TestBed } from '@angular/core/testing'; +import { TestBed } from '@angular/core/testing'; import { RegistryService } from './registry.service'; import { CommonModule } from '@angular/common'; import { ResponseCacheService } from '../cache/response-cache.service'; @@ -6,26 +6,24 @@ import { RequestService } from '../data/request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { Observable } from 'rxjs/Observable'; +import { Observable, of as observableOf, combineLatest as observableCombineLatest } from 'rxjs'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { RequestEntry } from '../data/request.reducer'; import { RemoteData } from '../data/remote-data'; -import { PaginatedList } from '../data/paginated-list'; import { PageInfo } from '../shared/page-info.model'; -import { GetRequest } from '../data/request.models'; -import { URLCombiner } from '../url-combiner/url-combiner'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service'; + import { RegistryBitstreamformatsSuccessResponse, - RegistryMetadatafieldsSuccessResponse, RegistryMetadataschemasSuccessResponse, - SearchSuccessResponse + RegistryMetadatafieldsSuccessResponse, + RegistryMetadataschemasSuccessResponse } from '../cache/response-cache.models'; -import { SearchQueryResponse } from '../../+search-page/search-service/search-query-response.model'; import { Component } from '@angular/core'; import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model'; import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model'; import { RegistryBitstreamformatsResponse } from './registry-bitstreamformats-response.model'; +import { map } from 'rxjs/operators'; @Component({ template: '' }) class DummyComponent { @@ -125,24 +123,25 @@ describe('RegistryService', () => { const endpointWithParams = `${endpoint}?size=${pageInfo.elementsPerPage}&page=${pageInfo.currentPage - 1}`; const halServiceStub = { - getEndpoint: (link: string) => Observable.of(endpoint) + getEndpoint: (link: string) => observableOf(endpoint) }; const rdbStub = { toRemoteDataObservable: (requestEntryObs: Observable, responseCacheObs: Observable, payloadObs: Observable) => { - return Observable.combineLatest(requestEntryObs, - responseCacheObs, payloadObs, (req, res, pay) => { + return observableCombineLatest(requestEntryObs, + responseCacheObs, payloadObs).pipe(map(([req, res, pay]) => { return { req, res, pay }; - }); + }) + ); }, aggregate: (input: Array>>): Observable> => { - return Observable.of(new RemoteData(false, false, true, null, [])); + return observableOf(new RemoteData(false, false, true, null, [])); } }; beforeEach(() => { TestBed.configureTestingModule({ - imports: [ CommonModule ], + imports: [CommonModule], declarations: [ DummyComponent ], @@ -156,16 +155,19 @@ describe('RegistryService', () => { }); registryService = TestBed.get(RegistryService); - spyOn((registryService as any).halService, 'getEndpoint').and.returnValue(Observable.of(endpoint)); + spyOn((registryService as any).halService, 'getEndpoint').and.returnValue(observableOf(endpoint)); }); describe('when requesting metadataschemas', () => { - const queryResponse = Object.assign(new RegistryMetadataschemasResponse(), { metadataschemas: mockSchemasList, page: pageInfo }); + const queryResponse = Object.assign(new RegistryMetadataschemasResponse(), { + metadataschemas: mockSchemasList, + page: pageInfo + }); const response = new RegistryMetadataschemasSuccessResponse(queryResponse, '200', pageInfo); const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); beforeEach(() => { - (registryService as any).responseCache.get.and.returnValue(Observable.of(responseEntry)); + (registryService as any).responseCache.get.and.returnValue(observableOf(responseEntry)); /* tslint:disable:no-empty */ registryService.getMetadataSchemas(pagination).subscribe((value) => { }); @@ -190,12 +192,15 @@ describe('RegistryService', () => { }); describe('when requesting metadataschema by name', () => { - const queryResponse = Object.assign(new RegistryMetadataschemasResponse(), { metadataschemas: mockSchemasList, page: pageInfo }); + const queryResponse = Object.assign(new RegistryMetadataschemasResponse(), { + metadataschemas: mockSchemasList, + page: pageInfo + }); const response = new RegistryMetadataschemasSuccessResponse(queryResponse, '200', pageInfo); const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); beforeEach(() => { - (registryService as any).responseCache.get.and.returnValue(Observable.of(responseEntry)); + (registryService as any).responseCache.get.and.returnValue(observableOf(responseEntry)); /* tslint:disable:no-empty */ registryService.getMetadataSchemaByName(mockSchemasList[0].prefix).subscribe((value) => { }); @@ -220,12 +225,15 @@ describe('RegistryService', () => { }); describe('when requesting metadatafields', () => { - const queryResponse = Object.assign(new RegistryMetadatafieldsResponse(), { metadatafields: mockFieldsList, page: pageInfo }); + const queryResponse = Object.assign(new RegistryMetadatafieldsResponse(), { + metadatafields: mockFieldsList, + page: pageInfo + }); const response = new RegistryMetadatafieldsSuccessResponse(queryResponse, '200', pageInfo); const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); beforeEach(() => { - (registryService as any).responseCache.get.and.returnValue(Observable.of(responseEntry)); + (registryService as any).responseCache.get.and.returnValue(observableOf(responseEntry)); /* tslint:disable:no-empty */ registryService.getMetadataFieldsBySchema(mockSchemasList[0], pagination).subscribe((value) => { }); @@ -250,12 +258,15 @@ describe('RegistryService', () => { }); describe('when requesting bitstreamformats', () => { - const queryResponse = Object.assign(new RegistryBitstreamformatsResponse(), { bitstreamformats: mockFieldsList, page: pageInfo }); + const queryResponse = Object.assign(new RegistryBitstreamformatsResponse(), { + bitstreamformats: mockFieldsList, + page: pageInfo + }); const response = new RegistryBitstreamformatsSuccessResponse(queryResponse, '200', pageInfo); const responseEntry = Object.assign(new ResponseCacheEntry(), { response: response }); beforeEach(() => { - (registryService as any).responseCache.get.and.returnValue(Observable.of(responseEntry)); + (registryService as any).responseCache.get.and.returnValue(observableOf(responseEntry)); /* tslint:disable:no-empty */ registryService.getBitstreamFormats(pagination).subscribe((value) => { }); diff --git a/src/app/core/registry/registry.service.ts b/src/app/core/registry/registry.service.ts index 4359284158..7e7c18f69e 100644 --- a/src/app/core/registry/registry.service.ts +++ b/src/app/core/registry/registry.service.ts @@ -1,5 +1,5 @@ +import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { Injectable } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; import { RemoteData } from '../data/remote-data'; import { PaginatedList } from '../data/paginated-list'; import { PageInfo } from '../shared/page-info.model'; @@ -17,12 +17,11 @@ import { ResponseCacheService } from '../cache/response-cache.service'; import { RegistryMetadataschemasResponse } from './registry-metadataschemas-response.model'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; import { - MetadataschemaSuccessResponse, RegistryBitstreamformatsSuccessResponse, RegistryMetadatafieldsSuccessResponse, + RegistryBitstreamformatsSuccessResponse, + RegistryMetadatafieldsSuccessResponse, RegistryMetadataschemasSuccessResponse } from '../cache/response-cache.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { MetadataschemaParsingService } from '../data/metadataschema-parsing.service'; -import { Res } from 'awesome-typescript-loader/dist/checker/protocol'; import { RegistryMetadatafieldsResponseParsingService } from '../data/registry-metadatafields-response-parsing.service'; import { RegistryMetadatafieldsResponse } from './registry-metadatafields-response.model'; import { isNotEmpty } from '../../shared/empty.util'; @@ -70,9 +69,11 @@ export class RegistryService { map((response: RegistryMetadataschemasSuccessResponse) => response.pageInfo) ); - const payloadObs = Observable.combineLatest(metadataschemasObs, pageInfoObs, (metadataschemas, pageInfo) => { - return new PaginatedList(pageInfo, metadataschemas); - }); + const payloadObs = observableCombineLatest(metadataschemasObs, pageInfoObs).pipe( + map(([metadataschemas, pageInfo]) => { + return new PaginatedList(pageInfo, metadataschemas); + }) + ); return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); } @@ -132,9 +133,11 @@ export class RegistryService { map((response: RegistryMetadatafieldsSuccessResponse) => response.pageInfo) ); - const payloadObs = Observable.combineLatest(metadatafieldsObs, pageInfoObs, (metadatafields, pageInfo) => { - return new PaginatedList(pageInfo, metadatafields); - }); + const payloadObs = observableCombineLatest(metadatafieldsObs, pageInfoObs).pipe( + map(([metadatafields, pageInfo]) => { + return new PaginatedList(pageInfo, metadatafields); + }) + ); return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); } @@ -164,9 +167,11 @@ export class RegistryService { map((response: RegistryBitstreamformatsSuccessResponse) => response.pageInfo) ); - const payloadObs = Observable.combineLatest(bitstreamformatsObs, pageInfoObs, (bitstreamformats, pageInfo) => { - return new PaginatedList(pageInfo, bitstreamformats); - }); + const payloadObs = observableCombineLatest(bitstreamformatsObs, pageInfoObs).pipe( + map(([bitstreamformats, pageInfo]) => { + return new PaginatedList(pageInfo, bitstreamformats); + }) + ); return this.rdb.toRemoteDataObservable(requestEntryObs, responseCacheObs, payloadObs); } diff --git a/src/app/core/shared/bitstream.model.ts b/src/app/core/shared/bitstream.model.ts index 511c2c5cd2..794282e867 100644 --- a/src/app/core/shared/bitstream.model.ts +++ b/src/app/core/shared/bitstream.model.ts @@ -2,7 +2,7 @@ import { DSpaceObject } from './dspace-object.model'; import { RemoteData } from '../data/remote-data'; import { Item } from './item.model'; import { BitstreamFormat } from './bitstream-format.model'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; export class Bitstream extends DSpaceObject { diff --git a/src/app/core/shared/bundle.model.ts b/src/app/core/shared/bundle.model.ts index 9a8afb2661..3f5b5df877 100644 --- a/src/app/core/shared/bundle.model.ts +++ b/src/app/core/shared/bundle.model.ts @@ -2,7 +2,7 @@ import { DSpaceObject } from './dspace-object.model'; import { Bitstream } from './bitstream.model'; import { Item } from './item.model'; import { RemoteData } from '../data/remote-data'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; export class Bundle extends DSpaceObject { /** diff --git a/src/app/core/shared/collection.model.ts b/src/app/core/shared/collection.model.ts index b2f8d90a65..8fdc14bd6e 100644 --- a/src/app/core/shared/collection.model.ts +++ b/src/app/core/shared/collection.model.ts @@ -2,7 +2,7 @@ import { DSpaceObject } from './dspace-object.model'; import { Bitstream } from './bitstream.model'; import { Item } from './item.model'; import { RemoteData } from '../data/remote-data'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; export class Collection extends DSpaceObject { diff --git a/src/app/core/shared/community.model.ts b/src/app/core/shared/community.model.ts index 20bd50f4a9..893a7e0b94 100644 --- a/src/app/core/shared/community.model.ts +++ b/src/app/core/shared/community.model.ts @@ -2,7 +2,7 @@ import { DSpaceObject } from './dspace-object.model'; import { Bitstream } from './bitstream.model'; import { Collection } from './collection.model'; import { RemoteData } from '../data/remote-data'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { PaginatedList } from '../data/paginated-list'; export class Community extends DSpaceObject { diff --git a/src/app/core/shared/dspace-object.model.ts b/src/app/core/shared/dspace-object.model.ts index 042bfc01a0..3a40d142aa 100644 --- a/src/app/core/shared/dspace-object.model.ts +++ b/src/app/core/shared/dspace-object.model.ts @@ -4,7 +4,7 @@ import { CacheableObject } from '../cache/object-cache.reducer'; import { RemoteData } from '../data/remote-data'; import { ResourceType } from './resource-type'; import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { autoserialize } from 'cerialize'; /** diff --git a/src/app/core/shared/hal-endpoint.service.ts b/src/app/core/shared/hal-endpoint.service.ts index 3bedeb9915..d72b9e9a9f 100644 --- a/src/app/core/shared/hal-endpoint.service.ts +++ b/src/app/core/shared/hal-endpoint.service.ts @@ -1,5 +1,5 @@ -import { Observable } from 'rxjs/Observable'; -import { distinctUntilChanged, map, flatMap, startWith, tap } from 'rxjs/operators'; +import {of as observableOf, Observable } from 'rxjs'; +import {filter, distinctUntilChanged, map, flatMap, startWith, tap } from 'rxjs/operators'; import { RequestService } from '../data/request.service'; import { ResponseCacheService } from '../cache/response-cache.service'; import { GlobalConfig } from '../../../config/global-config.interface'; @@ -30,11 +30,11 @@ export class HALEndpointService { private getEndpointMapAt(href): Observable { const request = new EndpointMapRequest(this.requestService.generateRequestId(), href); this.requestService.configure(request); - return this.responseCache.get(request.href) - .map((entry: ResponseCacheEntry) => entry.response) - .filter((response: EndpointMapSuccessResponse) => isNotEmpty(response)) - .map((response: EndpointMapSuccessResponse) => response.endpointMap) - .distinctUntilChanged(); + return this.responseCache.get(request.href).pipe( + map((entry: ResponseCacheEntry) => entry.response), + filter((response: EndpointMapSuccessResponse) => isNotEmpty(response)), + map((response: EndpointMapSuccessResponse) => response.endpointMap), + distinctUntilChanged(),); } public getEndpoint(linkPath: string): Observable { @@ -61,7 +61,7 @@ export class HALEndpointService { }), ]) .reduce((combined, thisElement) => [...combined, ...thisElement], []); - return Observable.of(this.getRootHref()).pipe(...pipeArguments, distinctUntilChanged()); + return observableOf(this.getRootHref()).pipe(...pipeArguments, distinctUntilChanged()); } public isEnabledOnRestApi(linkPath: string): Observable { diff --git a/src/app/core/shared/item.model.spec.ts b/src/app/core/shared/item.model.spec.ts index c020cd3454..2e5388dc4d 100644 --- a/src/app/core/shared/item.model.spec.ts +++ b/src/app/core/shared/item.model.spec.ts @@ -1,10 +1,10 @@ -import { Observable } from 'rxjs/Observable'; +import { Observable, of as observableOf } from 'rxjs'; import { Item } from './item.model'; import { RemoteData } from '../data/remote-data'; import { Bitstream } from './bitstream.model'; import { isEmpty } from '../../shared/empty.util'; -import { PageInfo } from './page-info.model'; +import { first, map } from 'rxjs/operators'; describe('Item', () => { @@ -56,30 +56,30 @@ describe('Item', () => { it('should return the bitstreams related to this item with the specified bundle name', () => { const bitObs: Observable = item.getBitstreamsByBundleName(thumbnailBundleName); - bitObs.take(1).subscribe((bs) => + bitObs.pipe(first()).subscribe((bs) => expect(bs.every((b) => b.name === thumbnailBundleName)).toBeTruthy()); }); it('should return an empty array when no bitstreams with this bundleName exist for this item', () => { const bs: Observable = item.getBitstreamsByBundleName(nonExistingBundleName); - bs.take(1).subscribe((b) => expect(isEmpty(b)).toBeTruthy()); + bs.pipe(first()).subscribe((b) => expect(isEmpty(b)).toBeTruthy()); }); describe('get thumbnail', () => { beforeEach(() => { - spyOn(item, 'getBitstreamsByBundleName').and.returnValue(Observable.of([remoteDataThumbnail])); + spyOn(item, 'getBitstreamsByBundleName').and.returnValue(observableOf([remoteDataThumbnail])); }); it('should return the thumbnail of this item', () => { const path: string = thumbnailPath; const bitstream: Observable = item.getThumbnail(); - bitstream.map((b) => expect(b.content).toBe(path)); + bitstream.pipe(map((b) => expect(b.content).toBe(path))); }); }); describe('get files', () => { beforeEach(() => { - spyOn(item, 'getBitstreamsByBundleName').and.returnValue(Observable.of(bitstreams)); + spyOn(item, 'getBitstreamsByBundleName').and.returnValue(observableOf(bitstreams)); }); it("should return all bitstreams with 'ORIGINAL' as bundleName", () => { @@ -87,7 +87,7 @@ describe('Item', () => { const files: Observable = item.getFiles(); let index = 0; - files.map((f) => expect(f.length).toBe(2)); + files.pipe(map((f) => expect(f.length).toBe(2))); files.subscribe( (array) => array.forEach( (file) => { @@ -103,7 +103,7 @@ describe('Item', () => { }); function createRemoteDataObject(object: any) { - return Observable.of(new RemoteData( + return observableOf(new RemoteData( false, false, true, diff --git a/src/app/core/shared/item.model.ts b/src/app/core/shared/item.model.ts index ad5ce1d8af..69def7b969 100644 --- a/src/app/core/shared/item.model.ts +++ b/src/app/core/shared/item.model.ts @@ -1,5 +1,5 @@ -import { Observable } from 'rxjs/Observable'; -import { filter, map, startWith, tap } from 'rxjs/operators'; +import {map, startWith, filter} from 'rxjs/operators'; +import { Observable } from 'rxjs'; import { DSpaceObject } from './dspace-object.model'; import { Collection } from './collection.model'; @@ -59,9 +59,9 @@ export class Item extends DSpaceObject { // TODO: currently this just picks the first thumbnail // should be adjusted when we have a way to determine // the primary thumbnail from rest - return this.getBitstreamsByBundleName('THUMBNAIL') - .filter((thumbnails) => isNotEmpty(thumbnails)) - .map((thumbnails) => thumbnails[0]) + return this.getBitstreamsByBundleName('THUMBNAIL').pipe( + filter((thumbnails) => isNotEmpty(thumbnails)), + map((thumbnails) => thumbnails[0]),) } /** @@ -69,10 +69,10 @@ export class Item extends DSpaceObject { * @returns {Observable} the primaryBitstream of the 'THUMBNAIL' bundle */ getThumbnailForOriginal(original: Bitstream): Observable { - return this.getBitstreamsByBundleName('THUMBNAIL') - .map((files) => { + return this.getBitstreamsByBundleName('THUMBNAIL').pipe( + map((files) => { return files.find((thumbnail) => thumbnail.name.startsWith(original.name)) - }).startWith(undefined); + }),startWith(undefined),); } /** @@ -89,15 +89,15 @@ export class Item extends DSpaceObject { * @returns {Observable} the bitstreams with the given bundleName */ getBitstreamsByBundleName(bundleName: string): Observable { - return this.bitstreams - .map((rd: RemoteData>) => rd.payload.page) - .filter((bitstreams: Bitstream[]) => hasValue(bitstreams)) - .startWith([]) - .map((bitstreams) => { + return this.bitstreams.pipe( + map((rd: RemoteData>) => rd.payload.page), + filter((bitstreams: Bitstream[]) => hasValue(bitstreams)), + startWith([]), + map((bitstreams) => { return bitstreams .filter((bitstream) => hasValue(bitstream)) .filter((bitstream) => bitstream.bundleName === bundleName) - }); + }),); } } diff --git a/src/app/core/shared/operators.spec.ts b/src/app/core/shared/operators.spec.ts index bb2fc263fd..16bf633705 100644 --- a/src/app/core/shared/operators.spec.ts +++ b/src/app/core/shared/operators.spec.ts @@ -1,5 +1,5 @@ import { cold, getTestScheduler, hot } from 'jasmine-marbles'; -import { TestScheduler } from '../../../../node_modules/rxjs'; +import { TestScheduler } from 'rxjs/testing'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockResponseCacheService } from '../../shared/mocks/mock-response-cache.service'; import { ResponseCacheEntry } from '../cache/response-cache.reducer'; diff --git a/src/app/core/shared/operators.ts b/src/app/core/shared/operators.ts index d340882b8f..9622bb91e1 100644 --- a/src/app/core/shared/operators.ts +++ b/src/app/core/shared/operators.ts @@ -1,4 +1,4 @@ -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { filter, first, flatMap, map, tap } from 'rxjs/operators'; import { hasValueOperator, isNotEmpty } from '../../shared/empty.util'; import { DSOSuccessResponse } from '../cache/response-cache.models'; diff --git a/src/app/header/header.component.spec.ts b/src/app/header/header.component.spec.ts index 9d0dd04e40..fd02621471 100644 --- a/src/app/header/header.component.spec.ts +++ b/src/app/header/header.component.spec.ts @@ -1,10 +1,10 @@ -import { ComponentFixture, TestBed, async } from '@angular/core/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; import { Store, StoreModule } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; -import { Observable } from 'rxjs/Observable'; +import { of as observableOf } from 'rxjs'; import { HeaderComponent } from './header.component'; import { HeaderState } from './header.reducer'; @@ -19,6 +19,7 @@ import { HostWindowServiceStub } from '../shared/testing/host-window-service-stu import { RouterStub } from '../shared/testing/router-stub'; import { Router } from '@angular/router'; import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import * as ngrx from '@ngrx/store'; import { NO_ERRORS_SCHEMA } from '@angular/core'; let comp: HeaderComponent; @@ -52,7 +53,7 @@ describe('HeaderComponent', () => { comp = fixture.componentInstance; - store = fixture.debugElement.injector.get(Store); + store = fixture.debugElement.injector.get(Store) as Store; spyOn(store, 'dispatch'); }); @@ -74,7 +75,11 @@ describe('HeaderComponent', () => { beforeEach(() => { menu = fixture.debugElement.query(By.css('#collapsingNav')).nativeElement; - spyOn(store, 'select').and.returnValue(Observable.of({ navCollapsed: true })); + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf({ navCollapsed: true }) + }; + }); fixture.detectChanges(); }); @@ -89,7 +94,11 @@ describe('HeaderComponent', () => { beforeEach(() => { menu = fixture.debugElement.query(By.css('#collapsingNav')).nativeElement; - spyOn(store, 'select').and.returnValue(Observable.of(false)); + spyOnProperty(ngrx, 'select').and.callFake(() => { + return () => { + return () => observableOf(false) + }; + }); fixture.detectChanges(); }); diff --git a/src/app/header/header.component.ts b/src/app/header/header.component.ts index 93cb329f4f..e1f8da0f9d 100644 --- a/src/app/header/header.component.ts +++ b/src/app/header/header.component.ts @@ -1,6 +1,6 @@ import { Component, OnInit } from '@angular/core'; -import { createSelector, Store } from '@ngrx/store'; -import { Observable } from 'rxjs/Observable'; +import { createSelector, select, Store } from '@ngrx/store'; +import { Observable } from 'rxjs'; import { RouterReducerState } from '@ngrx/router-store'; import { HeaderState } from './header.reducer'; @@ -33,7 +33,7 @@ export class HeaderComponent implements OnInit { ngOnInit(): void { // set loading - this.isNavBarCollapsed = this.store.select(navCollapsedSelector); + this.isNavBarCollapsed = this.store.pipe(select(navCollapsedSelector)); } public toggle(): void { diff --git a/src/app/header/header.effects.spec.ts b/src/app/header/header.effects.spec.ts index e67043dcba..97b428bf8c 100644 --- a/src/app/header/header.effects.spec.ts +++ b/src/app/header/header.effects.spec.ts @@ -2,7 +2,7 @@ import { TestBed } from '@angular/core/testing'; import { HeaderEffects } from './header.effects'; import { HeaderCollapseAction } from './header.actions'; import { HostWindowResizeAction } from '../shared/host-window.actions'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { provideMockActions } from '@ngrx/effects/testing'; import { cold, hot } from 'jasmine-marbles'; import * as fromRouter from '@ngrx/router-store'; diff --git a/src/app/header/header.effects.ts b/src/app/header/header.effects.ts index e1d281958b..cdc018d2d9 100644 --- a/src/app/header/header.effects.ts +++ b/src/app/header/header.effects.ts @@ -1,5 +1,6 @@ +import { map } 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 { HostWindowActionTypes } from '../shared/host-window.actions'; @@ -9,12 +10,16 @@ import { HeaderCollapseAction } from './header.actions'; export class HeaderEffects { @Effect() resize$ = this.actions$ - .ofType(HostWindowActionTypes.RESIZE) - .map(() => new HeaderCollapseAction()); + .pipe( + ofType(HostWindowActionTypes.RESIZE), + map(() => new HeaderCollapseAction()) + ); @Effect() routeChange$ = this.actions$ - .ofType(fromRouter.ROUTER_NAVIGATION) - .map(() => new HeaderCollapseAction()); + .pipe( + ofType(fromRouter.ROUTER_NAVIGATION), + map(() => new HeaderCollapseAction()) + ); constructor(private actions$: Actions) { diff --git a/src/app/shared/animations/slide.ts b/src/app/shared/animations/slide.ts index fa4a451863..ee16f9936f 100644 --- a/src/app/shared/animations/slide.ts +++ b/src/app/shared/animations/slide.ts @@ -1,4 +1,4 @@ -import { animate, state, transition, trigger, style, stagger, query } from '@angular/animations'; +import { animate, state, style, transition, trigger } from '@angular/animations'; export const slide = trigger('slide', [ diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts index e1a82f4a33..5c5dd11d75 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.spec.ts @@ -22,23 +22,28 @@ describe('AuthNavMenuComponent', () => { let deNavMenuItem: DebugElement; let fixture: ComponentFixture; - const notAuthState: AuthState = { - authenticated: false, - loaded: false, - loading: false - }; - const authState: AuthState = { - authenticated: true, - loaded: true, - loading: false, - authToken: new AuthTokenInfo('test_token'), - user: EPersonMock - }; + let notAuthState: AuthState; + let authState: AuthState; + let routerState = { url: '/home' }; - + function init() { + notAuthState = { + authenticated: false, + loaded: false, + loading: false + }; + authState = { + authenticated: true, + loaded: true, + loading: false, + authToken: new AuthTokenInfo('test_token'), + user: EPersonMock + }; + } describe('when is a not mobile view', () => { + beforeEach(async(() => { const window = new HostWindowServiceStub(800); @@ -53,8 +58,13 @@ describe('AuthNavMenuComponent', () => { AuthNavMenuComponent ], providers: [ - {provide: HostWindowService, useValue: window}, - {provide: AuthService, useValue: {setRedirectUrl: () => { /*empty*/ }}} + { provide: HostWindowService, useValue: window }, + { + provide: AuthService, useValue: { + setRedirectUrl: () => { /*empty*/ + } + } + } ], schemas: [ CUSTOM_ELEMENTS_SCHEMA @@ -64,11 +74,14 @@ describe('AuthNavMenuComponent', () => { })); + beforeEach(() => { + init(); + }); describe('when route is /login and user is not authenticated', () => { - routerState = { - url: '/login' - }; beforeEach(inject([Store], (store: Store) => { + routerState = { + url: '/login' + }; store .subscribe((state) => { (state as any).router = Object.create({}); @@ -91,7 +104,9 @@ describe('AuthNavMenuComponent', () => { const navMenuItemSelector = 'li'; deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector)); })); - + afterEach(() => { + fixture.destroy(); + }); it('should not render', () => { expect(component).toBeTruthy(); expect(deNavMenu.nativeElement).toBeDefined(); @@ -101,10 +116,10 @@ describe('AuthNavMenuComponent', () => { }); describe('when route is /logout and user is authenticated', () => { - routerState = { - url: '/logout' - }; beforeEach(inject([Store], (store: Store) => { + routerState = { + url: '/logout' + }; store .subscribe((state) => { (state as any).router = Object.create({}); @@ -128,6 +143,10 @@ describe('AuthNavMenuComponent', () => { deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector)); })); + afterEach(() => { + fixture.destroy(); + }); + it('should not render', () => { expect(component).toBeTruthy(); expect(deNavMenu.nativeElement).toBeDefined(); @@ -166,6 +185,11 @@ describe('AuthNavMenuComponent', () => { deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector)); })); + afterEach(() => { + fixture.destroy(); + component = null; + }); + it('should render login dropdown menu', () => { const loginDropdownMenu = deNavMenuItem.query(By.css('div[id=loginDropdownMenu]')); expect(loginDropdownMenu.nativeElement).toBeDefined(); @@ -200,6 +224,10 @@ describe('AuthNavMenuComponent', () => { deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector)); })); + afterEach(() => { + fixture.destroy(); + component = null; + }); it('should render logout dropdown menu', () => { const logoutDropdownMenu = deNavMenuItem.query(By.css('div[id=logoutDropdownMenu]')); expect(logoutDropdownMenu.nativeElement).toBeDefined(); @@ -223,8 +251,13 @@ describe('AuthNavMenuComponent', () => { AuthNavMenuComponent ], providers: [ - {provide: HostWindowService, useValue: window}, - {provide: AuthService, useValue: {setRedirectUrl: () => { /*empty*/ }}} + { provide: HostWindowService, useValue: window }, + { + provide: AuthService, useValue: { + setRedirectUrl: () => { /*empty*/ + } + } + } ], schemas: [ CUSTOM_ELEMENTS_SCHEMA @@ -234,6 +267,9 @@ describe('AuthNavMenuComponent', () => { })); + beforeEach(() => { + init(); + }); describe('when user is not authenticated', () => { beforeEach(inject([Store], (store: Store) => { @@ -260,6 +296,11 @@ describe('AuthNavMenuComponent', () => { deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector)); })); + afterEach(() => { + fixture.destroy(); + component = null; + }); + it('should render login link', () => { const loginDropdownMenu = deNavMenuItem.query(By.css('a[id=loginLink]')); expect(loginDropdownMenu.nativeElement).toBeDefined(); @@ -291,6 +332,11 @@ describe('AuthNavMenuComponent', () => { deNavMenuItem = deNavMenu.query(By.css(navMenuItemSelector)); })); + afterEach(() => { + fixture.destroy(); + component = null; + }); + it('should render logout link', inject([Store], (store: Store) => { const logoutDropdownMenu = deNavMenuItem.query(By.css('a[id=logoutLink]')); expect(logoutDropdownMenu.nativeElement).toBeDefined(); diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts index c657071987..4361163538 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.ts @@ -1,7 +1,9 @@ +import { of as observableOf, Observable , Subscription } from 'rxjs'; + +import { map, filter } from 'rxjs/operators'; import { Component, OnInit } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; import { RouterReducerState } from '@ngrx/router-store'; -import { Store } from '@ngrx/store'; +import { select, Store } from '@ngrx/store'; import { fadeInOut, fadeOut } from '../animations/fade'; import { HostWindowService } from '../host-window.service'; @@ -14,7 +16,6 @@ import { } from '../../core/auth/selectors'; import { EPerson } from '../../core/eperson/models/eperson.model'; import { AuthService, LOGIN_ROUTE, LOGOUT_ROUTE } from '../../core/auth/auth.service'; -import { Subscription } from 'rxjs/Subscription'; @Component({ selector: 'ds-auth-nav-menu', @@ -37,7 +38,7 @@ export class AuthNavMenuComponent implements OnInit { public isXsOrSm$: Observable; - public showAuth = Observable.of(false); + public showAuth = observableOf(false); public user: Observable; @@ -52,22 +53,24 @@ export class AuthNavMenuComponent implements OnInit { ngOnInit(): void { // set isAuthenticated - this.isAuthenticated = this.store.select(isAuthenticated); + this.isAuthenticated = this.store.pipe(select(isAuthenticated)); // set loading - this.loading = this.store.select(isAuthenticationLoading); + this.loading = this.store.pipe(select(isAuthenticationLoading)); - this.user = this.store.select(getAuthenticatedUser); + this.user = this.store.pipe(select(getAuthenticatedUser)); - this.showAuth = this.store.select(routerStateSelector) - .filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state)) - .map((router: RouterReducerState) => { + this.showAuth = this.store.pipe( + select(routerStateSelector), + filter((router: RouterReducerState) => isNotUndefined(router) && isNotUndefined(router.state)), + map((router: RouterReducerState) => { const url = router.state.url; const show = !router.state.url.startsWith(LOGIN_ROUTE) && !router.state.url.startsWith(LOGOUT_ROUTE); if (show) { this.authService.setRedirectUrl(url); } return show; - }); + }) + ); } } diff --git a/src/app/shared/browse-by/browse-by.component.spec.ts b/src/app/shared/browse-by/browse-by.component.spec.ts index 883d61a221..2417dde7ca 100644 --- a/src/app/shared/browse-by/browse-by.component.spec.ts +++ b/src/app/shared/browse-by/browse-by.component.spec.ts @@ -3,7 +3,7 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { By } from '@angular/platform-browser'; import { NO_ERRORS_SCHEMA } from '@angular/core'; -import { Observable } from 'rxjs/Observable'; +import { of as observableOf } from 'rxjs'; import { SharedModule } from '../shared.module'; describe('BrowseByComponent', () => { @@ -30,7 +30,7 @@ describe('BrowseByComponent', () => { }); it('should display results when objects is not empty', () => { - (comp as any).objects = Observable.of({ + (comp as any).objects = observableOf({ payload: { page: { length: 1 diff --git a/src/app/shared/browse-by/browse-by.component.ts b/src/app/shared/browse-by/browse-by.component.ts index 062b41a440..94cf81f46e 100644 --- a/src/app/shared/browse-by/browse-by.component.ts +++ b/src/app/shared/browse-by/browse-by.component.ts @@ -4,7 +4,7 @@ import { PaginatedList } from '../../core/data/paginated-list'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; import { SortOptions } from '../../core/cache/models/sort-options.model'; import { fadeIn, fadeInOut } from '../animations/fade'; -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { Item } from '../../core/shared/item.model'; import { ListableObject } from '../object-collection/shared/listable-object.model'; diff --git a/src/app/shared/chips/chips.component.spec.ts b/src/app/shared/chips/chips.component.spec.ts index 44092ce7d8..7a9461dfd7 100644 --- a/src/app/shared/chips/chips.component.spec.ts +++ b/src/app/shared/chips/chips.component.spec.ts @@ -1,7 +1,6 @@ // Load the implementations that should be tested import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@angular/core/testing'; -import 'rxjs/add/observable/of'; import { Chips } from './models/chips.model'; import { UploaderService } from '../uploader/uploader.service'; diff --git a/src/app/shared/chips/models/chips.model.ts b/src/app/shared/chips/models/chips.model.ts index e133a416f4..9e6aa653e1 100644 --- a/src/app/shared/chips/models/chips.model.ts +++ b/src/app/shared/chips/models/chips.model.ts @@ -1,5 +1,5 @@ import { findIndex, isEqual, isObject } from 'lodash'; -import { BehaviorSubject } from 'rxjs/BehaviorSubject'; +import { BehaviorSubject } from 'rxjs'; import { ChipsItem, ChipsItemIcon } from './chips-item.model'; import { hasValue, isNotEmpty } from '../../empty.util'; diff --git a/src/app/shared/empty.util.ts b/src/app/shared/empty.util.ts index c1498d11af..d79c520fda 100644 --- a/src/app/shared/empty.util.ts +++ b/src/app/shared/empty.util.ts @@ -1,4 +1,4 @@ -import { Observable } from 'rxjs/Observable'; +import { Observable } from 'rxjs'; import { filter, map } from 'rxjs/operators'; /** diff --git a/src/app/shared/error/error.component.ts b/src/app/shared/error/error.component.ts index 08d06c31d6..6900869183 100644 --- a/src/app/shared/error/error.component.ts +++ b/src/app/shared/error/error.component.ts @@ -2,7 +2,7 @@ import { Component, Input } from '@angular/core'; import { TranslateService } from '@ngx-translate/core'; -import { Subscription } from 'rxjs/Subscription'; +import { Subscription } from 'rxjs'; @Component({ selector: 'ds-error', diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html index db5bc92574..750ef721c2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.html @@ -1,8 +1,8 @@ -
- @@ -12,429 +12,7 @@
- - - -
- -
- - - - -
-
- - - - - - -
- - -
- - -
- -
- - -
- - -
- -
-
- - -
- - -
- - -
- -
- -
- - - - - -
- -
- - - - -
- - -
- - - - - -
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
- {{ message | translate:model.validators }} -
- -
- - - - - - - - - - - - - - - - - - - - - - - - - +
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.spec.ts index 7fc756c470..ca12a7a4b4 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.spec.ts @@ -25,7 +25,7 @@ import { DynamicTextAreaModel, DynamicTimePickerModel } from '@ng-dynamic-forms/core'; -import { DsDynamicFormControlComponent, NGBootstrapFormControlType } from './ds-dynamic-form-control.component'; +import { DsDynamicFormControlComponent } from './ds-dynamic-form-control.component'; import { TranslateModule } from '@ngx-translate/core'; import { SharedModule } from '../../../shared.module'; import { DynamicDsDatePickerModel } from './models/date-picker/date-picker.model'; @@ -39,6 +39,27 @@ import { DynamicTagModel } from './models/tag/dynamic-tag.model'; import { DynamicTypeaheadModel } from './models/typeahead/dynamic-typeahead.model'; import { DynamicQualdropModel } from './models/ds-dynamic-qualdrop.model'; import { DynamicLookupNameModel } from './models/lookup/dynamic-lookup-name.model'; +import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; +import { + DynamicNGBootstrapCalendarComponent, + DynamicNGBootstrapCheckboxComponent, + DynamicNGBootstrapCheckboxGroupComponent, + DynamicNGBootstrapDatePickerComponent, + DynamicNGBootstrapFormArrayComponent, + DynamicNGBootstrapFormGroupComponent, + DynamicNGBootstrapInputComponent, + DynamicNGBootstrapRadioGroupComponent, + DynamicNGBootstrapSelectComponent, + DynamicNGBootstrapTextAreaComponent, + DynamicNGBootstrapTimePickerComponent +} from '@ng-dynamic-forms/ui-ng-bootstrap'; +import { DsDynamicTypeaheadComponent } from './models/typeahead/dynamic-typeahead.component'; +import { DsDynamicScrollableDropdownComponent } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; +import { DsDynamicTagComponent } from './models/tag/dynamic-tag.component'; +import { DsDynamicListComponent } from './models/list/dynamic-list.component'; +import { DsDynamicGroupComponent } from './models/dynamic-group/dynamic-group.components'; +import { DsDatePickerComponent } from './models/date-picker/date-picker.component'; +import { DsDynamicLookupComponent } from './models/lookup/dynamic-lookup.component'; describe('DsDynamicFormControlComponent test suite', () => { @@ -49,27 +70,42 @@ describe('DsDynamicFormControlComponent test suite', () => { scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' }; const formModel = [ - new DynamicCheckboxModel({id: 'checkbox'}), - new DynamicCheckboxGroupModel({id: 'checkboxGroup', group: []}), - new DynamicColorPickerModel({id: 'colorpicker'}), - new DynamicDatePickerModel({id: 'datepicker'}), - new DynamicEditorModel({id: 'editor'}), - new DynamicFileUploadModel({id: 'upload', url: ''}), - new DynamicFormArrayModel({id: 'formArray', groupFactory: () => []}), - new DynamicFormGroupModel({id: 'formGroup', group: []}), - new DynamicInputModel({id: 'input', maxLength: 51}), - new DynamicRadioGroupModel({id: 'radioGroup'}), - new DynamicRatingModel({id: 'rating'}), - new DynamicSelectModel({id: 'select', options: [{value: 'One'}, {value: 'Two'}], value: 'One'}), - new DynamicSliderModel({id: 'slider'}), - new DynamicSwitchModel({id: 'switch'}), - new DynamicTextAreaModel({id: 'textarea'}), - new DynamicTimePickerModel({id: 'timepicker'}), - new DynamicTypeaheadModel({id: 'typeahead'}), - new DynamicScrollableDropdownModel({id: 'scrollableDropdown', authorityOptions: authorityOptions}), - new DynamicTagModel({id: 'tag'}), - new DynamicListCheckboxGroupModel({id: 'checkboxList', authorityOptions: authorityOptions, repeatable: true}), - new DynamicListRadioGroupModel({id: 'radioList', authorityOptions: authorityOptions, repeatable: false}), + new DynamicCheckboxModel({ id: 'checkbox' }), + new DynamicCheckboxGroupModel({ id: 'checkboxGroup', group: [] }), + new DynamicColorPickerModel({ id: 'colorpicker' }), + new DynamicDatePickerModel({ id: 'datepicker' }), + new DynamicEditorModel({ id: 'editor' }), + new DynamicFileUploadModel({ id: 'upload', url: '' }), + new DynamicFormArrayModel({ id: 'formArray', groupFactory: () => [] }), + new DynamicFormGroupModel({ id: 'formGroup', group: [] }), + new DynamicInputModel({ id: 'input', maxLength: 51 }), + new DynamicRadioGroupModel({ id: 'radioGroup' }), + new DynamicRatingModel({ id: 'rating' }), + new DynamicSelectModel({ + id: 'select', + options: [{ value: 'One' }, { value: 'Two' }], + value: 'One' + }), + new DynamicSliderModel({ id: 'slider' }), + new DynamicSwitchModel({ id: 'switch' }), + new DynamicTextAreaModel({ id: 'textarea' }), + new DynamicTimePickerModel({ id: 'timepicker' }), + new DynamicTypeaheadModel({ id: 'typeahead' }), + new DynamicScrollableDropdownModel({ + id: 'scrollableDropdown', + authorityOptions: authorityOptions + }), + new DynamicTagModel({ id: 'tag' }), + new DynamicListCheckboxGroupModel({ + id: 'checkboxList', + authorityOptions: authorityOptions, + repeatable: true + }), + new DynamicListRadioGroupModel({ + id: 'radioList', + authorityOptions: authorityOptions, + repeatable: false + }), new DynamicGroupModel({ id: 'relationGroup', formConfiguration: [], @@ -79,10 +115,10 @@ describe('DsDynamicFormControlComponent test suite', () => { scopeUUID: '', submissionScope: '' }), - new DynamicDsDatePickerModel({id: 'datepicker'}), - new DynamicLookupModel({id: 'lookup'}), - new DynamicLookupNameModel({id: 'lookupName'}), - new DynamicQualdropModel({id: 'combobox', readOnly: false}) + new DynamicDsDatePickerModel({ id: 'datepicker' }), + new DynamicLookupModel({ id: 'lookup' }), + new DynamicLookupNameModel({ id: 'lookupName' }), + new DynamicQualdropModel({ id: 'combobox', readOnly: false }) ]; const testModel = formModel[8]; let formGroup: FormGroup; @@ -93,6 +129,13 @@ describe('DsDynamicFormControlComponent test suite', () => { beforeEach(async(() => { + TestBed.overrideModule(BrowserDynamicTestingModule, { + + set: { + entryComponents: [DynamicNGBootstrapInputComponent] + } + }); + TestBed.configureTestingModule({ imports: [ @@ -102,8 +145,9 @@ describe('DsDynamicFormControlComponent test suite', () => { DynamicFormsCoreModule.forRoot(), SharedModule, TranslateModule.forRoot(), - TextMaskModule, + TextMaskModule ], + providers: [DsDynamicFormControlComponent, DynamicFormService], schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents().then(() => { @@ -128,12 +172,10 @@ describe('DsDynamicFormControlComponent test suite', () => { }); fixture.detectChanges(); - testElement = debugElement.query(By.css(`input[id='${testModel.id}']`)); })); it('should initialize correctly', () => { - expect(component.context).toBeNull(); expect(component.control instanceof FormControl).toBe(true); expect(component.group instanceof FormGroup).toBe(true); @@ -149,15 +191,7 @@ describe('DsDynamicFormControlComponent test suite', () => { expect(component.change).toBeDefined(); expect(component.focus).toBeDefined(); - expect(component.onValueChange).toBeDefined(); - expect(component.onBlur).toBeDefined(); - expect(component.onFocus).toBeDefined(); - - expect(component.isValid).toBe(true); - expect(component.isInvalid).toBe(false); - expect(component.showErrorMessages).toBe(false); - - expect(component.type).toBe(NGBootstrapFormControlType.Input); + expect(component.componentType).toBe(DynamicNGBootstrapInputComponent); }); it('should have an input element', () => { @@ -185,11 +219,11 @@ describe('DsDynamicFormControlComponent test suite', () => { it('should listen to native change event', () => { - spyOn(component, 'onValueChange'); + spyOn(component, 'onChange'); testElement.triggerEventHandler('change', null); - expect(component.onValueChange).toHaveBeenCalled(); + expect(component.onChange).toHaveBeenCalled(); }); it('should update model value when control value changes', () => { @@ -219,63 +253,36 @@ describe('DsDynamicFormControlComponent test suite', () => { expect(component.onModelDisabledUpdates).toHaveBeenCalled(); }); - it('should determine correct form control type', () => { - + it('should map a form control model to a form control component', () => { const testFn = DsDynamicFormControlComponent.getFormControlType; - - expect(testFn(formModel[0])).toEqual(NGBootstrapFormControlType.Checkbox); - - expect(testFn(formModel[1])).toEqual(NGBootstrapFormControlType.CheckboxGroup); - + expect(testFn(formModel[0])).toBe(DynamicNGBootstrapCheckboxComponent); + expect(testFn(formModel[1])).toBe(DynamicNGBootstrapCheckboxGroupComponent); expect(testFn(formModel[2])).toBeNull(); - - expect(testFn(formModel[3])).toEqual(NGBootstrapFormControlType.DatePicker); - + expect(testFn(formModel[3])).toBe(DynamicNGBootstrapDatePickerComponent); (formModel[3] as DynamicDatePickerModel).inline = true; - expect(testFn(formModel[3])).toEqual(NGBootstrapFormControlType.Calendar); - + expect(testFn(formModel[3])).toBe(DynamicNGBootstrapCalendarComponent); expect(testFn(formModel[4])).toBeNull(); - expect(testFn(formModel[5])).toBeNull(); - - expect(testFn(formModel[6])).toEqual(NGBootstrapFormControlType.Array); - - expect(testFn(formModel[7])).toEqual(NGBootstrapFormControlType.Group); - - expect(testFn(formModel[8])).toEqual(NGBootstrapFormControlType.Input); - - expect(testFn(formModel[9])).toEqual(NGBootstrapFormControlType.RadioGroup); - + expect(testFn(formModel[6])).toBe(DynamicNGBootstrapFormArrayComponent); + expect(testFn(formModel[7])).toBe(DynamicNGBootstrapFormGroupComponent); + expect(testFn(formModel[8])).toBe(DynamicNGBootstrapInputComponent); + expect(testFn(formModel[9])).toBe(DynamicNGBootstrapRadioGroupComponent); expect(testFn(formModel[10])).toBeNull(); - - expect(testFn(formModel[11])).toEqual(NGBootstrapFormControlType.Select); - + expect(testFn(formModel[11])).toBe(DynamicNGBootstrapSelectComponent); expect(testFn(formModel[12])).toBeNull(); - expect(testFn(formModel[13])).toBeNull(); - - expect(testFn(formModel[14])).toEqual(NGBootstrapFormControlType.TextArea); - - expect(testFn(formModel[15])).toEqual(NGBootstrapFormControlType.TimePicker); - - expect(testFn(formModel[16])).toEqual(NGBootstrapFormControlType.TypeAhead); - - expect(testFn(formModel[17])).toEqual(NGBootstrapFormControlType.ScrollableDropdown); - - expect(testFn(formModel[18])).toEqual(NGBootstrapFormControlType.Tag); - - expect(testFn(formModel[19])).toEqual(NGBootstrapFormControlType.List); - - expect(testFn(formModel[20])).toEqual(NGBootstrapFormControlType.List); - - expect(testFn(formModel[21])).toEqual(NGBootstrapFormControlType.Relation); - - expect(testFn(formModel[22])).toEqual(NGBootstrapFormControlType.Date); - - expect(testFn(formModel[23])).toEqual(NGBootstrapFormControlType.Lookup); - - expect(testFn(formModel[24])).toEqual(NGBootstrapFormControlType.LookupName); - - expect(testFn(formModel[25])).toEqual(NGBootstrapFormControlType.Group); + expect(testFn(formModel[14])).toBe(DynamicNGBootstrapTextAreaComponent); + expect(testFn(formModel[15])).toBe(DynamicNGBootstrapTimePickerComponent); + expect(testFn(formModel[16])).toBe(DsDynamicTypeaheadComponent); + expect(testFn(formModel[17])).toBe(DsDynamicScrollableDropdownComponent); + expect(testFn(formModel[18])).toBe(DsDynamicTagComponent); + expect(testFn(formModel[19])).toBe(DsDynamicListComponent); + expect(testFn(formModel[20])).toBe(DsDynamicListComponent); + expect(testFn(formModel[21])).toBe(DsDynamicGroupComponent); + expect(testFn(formModel[22])).toBe(DsDatePickerComponent); + expect(testFn(formModel[23])).toBe(DsDynamicLookupComponent); + expect(testFn(formModel[24])).toBe(DsDynamicLookupComponent); + expect(testFn(formModel[25])).toBe(DynamicNGBootstrapFormGroupComponent); }); + }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts index 3a39d22bef..3544bce280 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control.component.ts @@ -1,24 +1,19 @@ import { - ChangeDetectorRef, Component, + ComponentFactoryResolver, ContentChildren, EventEmitter, Input, OnChanges, Output, QueryList, - SimpleChanges + SimpleChanges, + Type, + ViewChild, + ViewContainerRef } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { - DynamicDatePickerModel, - DynamicFormControlComponent, - DynamicFormControlEvent, - DynamicFormControlModel, - DynamicFormLayout, - DynamicFormLayoutService, - DynamicFormValidationService, - DynamicTemplateDirective, DYNAMIC_FORM_CONTROL_TYPE_ARRAY, DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX, DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP, @@ -29,6 +24,14 @@ import { DYNAMIC_FORM_CONTROL_TYPE_SELECT, DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA, DYNAMIC_FORM_CONTROL_TYPE_TIMEPICKER, + DynamicDatePickerModel, + DynamicFormControl, + DynamicFormControlContainerComponent, + DynamicFormControlEvent, + DynamicFormControlModel, DynamicFormLayout, + DynamicFormLayoutService, + DynamicFormValidationService, + DynamicTemplateDirective, } from '@ng-dynamic-forms/core'; import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model'; import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; @@ -40,40 +43,37 @@ import { DynamicListCheckboxGroupModel } from './models/list/dynamic-list-checkb import { DynamicListRadioGroupModel } from './models/list/dynamic-list-radio-group.model'; import { isNotEmpty } from '../../../empty.util'; import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME } from './models/lookup/dynamic-lookup-name.model'; - -export const enum NGBootstrapFormControlType { - - Array = 1, // 'ARRAY', - Calendar = 2, // 'CALENDAR', - Checkbox = 3, // 'CHECKBOX', - CheckboxGroup = 4, // 'CHECKBOX_GROUP', - DatePicker = 5, // 'DATEPICKER', - Group = 6, // 'GROUP', - Input = 7, // 'INPUT', - RadioGroup = 8, // 'RADIO_GROUP', - Select = 9, // 'SELECT', - TextArea = 10, // 'TEXTAREA', - TimePicker = 11, // 'TIMEPICKER' - TypeAhead = 12, // 'TYPEAHEAD' - ScrollableDropdown = 13, // 'SCROLLABLE_DROPDOWN' - Tag = 14, // 'TAG' - List = 15, // 'TYPELIST' - Relation = 16, // 'RELATION' - Date = 17, // 'DATE' - Lookup = 18, // LOOKUP - LookupName = 19, // LOOKUP_NAME -} +import { DsDynamicTagComponent } from './models/tag/dynamic-tag.component'; +import { + DynamicNGBootstrapCalendarComponent, + DynamicNGBootstrapCheckboxComponent, + DynamicNGBootstrapCheckboxGroupComponent, + DynamicNGBootstrapDatePickerComponent, + DynamicNGBootstrapFormArrayComponent, + DynamicNGBootstrapFormGroupComponent, + DynamicNGBootstrapInputComponent, + DynamicNGBootstrapRadioGroupComponent, + DynamicNGBootstrapSelectComponent, + DynamicNGBootstrapTextAreaComponent, + DynamicNGBootstrapTimePickerComponent +} from '@ng-dynamic-forms/ui-ng-bootstrap'; +import { DsDatePickerComponent } from './models/date-picker/date-picker.component'; +import { DsDynamicListComponent } from './models/list/dynamic-list.component'; +import { DsDynamicTypeaheadComponent } from './models/typeahead/dynamic-typeahead.component'; +import { DsDynamicScrollableDropdownComponent } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.component'; +import { DsDynamicGroupComponent } from './models/dynamic-group/dynamic-group.components'; +import { DsDynamicLookupComponent } from './models/lookup/dynamic-lookup.component'; @Component({ selector: 'ds-dynamic-form-control', styleUrls: ['../../form.component.scss', './ds-dynamic-form.component.scss'], templateUrl: './ds-dynamic-form-control.component.html' }) -export class DsDynamicFormControlComponent extends DynamicFormControlComponent implements OnChanges { +export class DsDynamicFormControlComponent extends DynamicFormControlContainerComponent implements OnChanges { - @ContentChildren(DynamicTemplateDirective) contentTemplateList: QueryList; - // tslint:disable-next-line:no-input-rename - @Input('templates') inputTemplateList: QueryList; + @ContentChildren(DynamicTemplateDirective) contentTemplateList: QueryList; + // tslint:disable-next-line:no-input-rename + @Input('templates') inputTemplateList: QueryList; @Input() formId: string; @Input() asBootstrapFormGroup = true; @@ -81,7 +81,7 @@ export class DsDynamicFormControlComponent extends DynamicFormControlComponent i @Input() context: any | null = null; @Input() group: FormGroup; @Input() hasErrorMessaging = false; - @Input() layout: DynamicFormLayout; + @Input() layout = null as DynamicFormLayout; @Input() model: any; /* tslint:disable:no-output-rename */ @@ -89,90 +89,89 @@ export class DsDynamicFormControlComponent extends DynamicFormControlComponent i @Output('dfChange') change: EventEmitter = new EventEmitter(); @Output('dfFocus') focus: EventEmitter = new EventEmitter(); /* tslint:enable:no-output-rename */ + @ViewChild('componentViewContainer', {read: ViewContainerRef}) componentViewContainerRef: ViewContainerRef; - type: NGBootstrapFormControlType | null; + get componentType(): Type | null { + return this.layoutService.getCustomComponentType(this.model) || DsDynamicFormControlComponent.getFormControlType(this.model); + } - static getFormControlType(model: DynamicFormControlModel): NGBootstrapFormControlType | null { + static getFormControlType(model: DynamicFormControlModel): Type | null { switch (model.type) { case DYNAMIC_FORM_CONTROL_TYPE_ARRAY: - return NGBootstrapFormControlType.Array; + return DynamicNGBootstrapFormArrayComponent; case DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX: - return NGBootstrapFormControlType.Checkbox; + return DynamicNGBootstrapCheckboxComponent; case DYNAMIC_FORM_CONTROL_TYPE_CHECKBOX_GROUP: - return (model instanceof DynamicListCheckboxGroupModel) ? NGBootstrapFormControlType.List : NGBootstrapFormControlType.CheckboxGroup; + return (model instanceof DynamicListCheckboxGroupModel) ? DsDynamicListComponent : DynamicNGBootstrapCheckboxGroupComponent; case DYNAMIC_FORM_CONTROL_TYPE_DATEPICKER: const datepickerModel = model as DynamicDatePickerModel; - return datepickerModel.inline ? NGBootstrapFormControlType.Calendar : NGBootstrapFormControlType.DatePicker; + return datepickerModel.inline ? DynamicNGBootstrapCalendarComponent : DynamicNGBootstrapDatePickerComponent; case DYNAMIC_FORM_CONTROL_TYPE_GROUP: - return NGBootstrapFormControlType.Group; + return DynamicNGBootstrapFormGroupComponent; case DYNAMIC_FORM_CONTROL_TYPE_INPUT: - return NGBootstrapFormControlType.Input; + return DynamicNGBootstrapInputComponent; case DYNAMIC_FORM_CONTROL_TYPE_RADIO_GROUP: - return (model instanceof DynamicListRadioGroupModel) ? NGBootstrapFormControlType.List : NGBootstrapFormControlType.RadioGroup; + return (model instanceof DynamicListRadioGroupModel) ? DsDynamicListComponent : DynamicNGBootstrapRadioGroupComponent; case DYNAMIC_FORM_CONTROL_TYPE_SELECT: - return NGBootstrapFormControlType.Select; + return DynamicNGBootstrapSelectComponent; case DYNAMIC_FORM_CONTROL_TYPE_TEXTAREA: - return NGBootstrapFormControlType.TextArea; + return DynamicNGBootstrapTextAreaComponent; case DYNAMIC_FORM_CONTROL_TYPE_TIMEPICKER: - return NGBootstrapFormControlType.TimePicker; + return DynamicNGBootstrapTimePickerComponent; case DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD: - return NGBootstrapFormControlType.TypeAhead; + return DsDynamicTypeaheadComponent; case DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN: - return NGBootstrapFormControlType.ScrollableDropdown; + return DsDynamicScrollableDropdownComponent; case DYNAMIC_FORM_CONTROL_TYPE_TAG: - return NGBootstrapFormControlType.Tag; + return DsDynamicTagComponent; case DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP: - return NGBootstrapFormControlType.Relation; + return DsDynamicGroupComponent; case DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER: - return NGBootstrapFormControlType.Date; + return DsDatePickerComponent; case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP: - return NGBootstrapFormControlType.Lookup; + return DsDynamicLookupComponent; case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME: - return NGBootstrapFormControlType.LookupName; + return DsDynamicLookupComponent; default: return null; } } - constructor(protected changeDetectorRef: ChangeDetectorRef, protected layoutService: DynamicFormLayoutService, + constructor(protected componentFactoryResolver: ComponentFactoryResolver, protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService) { - super(changeDetectorRef, layoutService, validationService); + super(componentFactoryResolver, layoutService, validationService); } ngOnChanges(changes: SimpleChanges) { if (changes) { super.ngOnChanges(changes); } - - if (changes.model) { - this.type = DsDynamicFormControlComponent.getFormControlType(this.model); - } } onChangeLanguage(event) { if (isNotEmpty((this.model as any).value)) { - this.onValueChange(event); + this.onChange(event); } } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts index 7789d910a8..c1b4ca71c8 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form.component.ts @@ -9,13 +9,13 @@ import { } from '@angular/core'; import { FormGroup } from '@angular/forms'; import { - DynamicFormComponent, + DynamicFormComponent, DynamicFormControlContainerComponent, DynamicFormControlEvent, DynamicFormControlModel, - DynamicFormLayout, - DynamicFormLayoutService, - DynamicFormService, - DynamicTemplateDirective, + DynamicFormLayout, + DynamicFormLayoutService, + DynamicFormService, + DynamicTemplateDirective, } from '@ng-dynamic-forms/core'; import { DsDynamicFormControlComponent } from './ds-dynamic-form-control.component'; import { FormBuilderService } from '../form-builder.service'; @@ -29,7 +29,7 @@ export class DsDynamicFormComponent extends DynamicFormComponent { @Input() formId: string; @Input() formGroup: FormGroup; @Input() formModel: DynamicFormControlModel[]; - @Input() formLayout: DynamicFormLayout = null; + @Input() formLayout = null as DynamicFormLayout; /* tslint:disable:no-output-rename */ @Output('dfBlur') blur: EventEmitter = new EventEmitter(); @@ -39,9 +39,10 @@ export class DsDynamicFormComponent extends DynamicFormComponent { @ContentChildren(DynamicTemplateDirective) templates: QueryList; - @ViewChildren(DsDynamicFormControlComponent) components: QueryList; + @ViewChildren(DsDynamicFormControlComponent) components: QueryList; + + constructor(protected formService: FormBuilderService, protected layoutService: DynamicFormLayoutService) { + super(formService, layoutService); + } - constructor(protected formService: FormBuilderService, protected layoutService: DynamicFormLayoutService) { - super(formService, layoutService); - } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts index 02f1415e99..d11dbf664b 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.spec.ts @@ -4,7 +4,7 @@ import { async, ComponentFixture, inject, TestBed, } from '@angular/core/testing import { FormControl, FormGroup } from '@angular/forms'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; -import { DynamicFormValidationService } from '@ng-dynamic-forms/core'; +import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; import { DsDatePickerComponent } from './date-picker.component'; import { DynamicDsDatePickerModel } from './date-picker.model'; @@ -52,10 +52,8 @@ describe('DsDatePickerComponent test suite', () => { providers: [ ChangeDetectorRef, DsDatePickerComponent, - DynamicFormValidationService, - FormBuilderService, - FormComponent, - FormService + {provide: DynamicFormLayoutService, useValue: {}}, + {provide: DynamicFormValidationService, useValue: {}} ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); @@ -70,7 +68,6 @@ describe('DsDatePickerComponent test suite', () => { [bindId]='bindId' [group]='group' [model]='model' - [showErrorMessages]='showErrorMessages' (blur)='onBlur($event)' (change)='onValueChange($event)' (focus)='onFocus($event)'>`; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts index 741d86fab9..2e22f314ed 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.component.ts @@ -1,7 +1,12 @@ -import { Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; -import { FormControl, FormGroup } from '@angular/forms'; +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { FormGroup } from '@angular/forms'; import { DynamicDsDatePickerModel } from './date-picker.model'; -import { hasNoValue, hasValue, isNotEmpty } from '../../../../../empty.util'; +import { hasValue } from '../../../../../empty.util'; +import { + DynamicFormControlComponent, + DynamicFormLayoutService, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; export const DS_DATE_PICKER_SEPARATOR = '-'; @@ -11,11 +16,10 @@ export const DS_DATE_PICKER_SEPARATOR = '-'; templateUrl: './date-picker.component.html', }) -export class DsDatePickerComponent implements OnInit { +export class DsDatePickerComponent extends DynamicFormControlComponent implements OnInit { @Input() bindId = true; @Input() group: FormGroup; @Input() model: DynamicDsDatePickerModel; - @Input() showErrorMessages = false; // @Input() // minDate; // @Input() @@ -49,6 +53,12 @@ export class DsDatePickerComponent implements OnInit { disabledMonth = true; disabledDay = true; + constructor(protected layoutService: DynamicFormLayoutService, + protected validationService: DynamicFormValidationService + ) { + super(layoutService, validationService); + } + ngOnInit() { const now = new Date(); this.initialYear = now.getFullYear(); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts index a75a1d2f1a..214e2d1907 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/date-picker/date-picker.model.ts @@ -1,6 +1,6 @@ import { DynamicDateControlModel, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; import { DynamicDateControlModelConfig } from '@ng-dynamic-forms/core/src/model/dynamic-date-control.model'; -import { Subject } from 'rxjs/Subject'; +import { Subject } from 'rxjs'; export const DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER = 'DATE'; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts index f739c17cf3..860c481820 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts @@ -4,7 +4,7 @@ import { DynamicInputModelConfig, serializable } from '@ng-dynamic-forms/core'; -import { Subject } from 'rxjs/Subject'; +import { Subject } from 'rxjs'; import { LanguageCode } from '../../models/form-field-language-value.model'; import { AuthorityOptions } from '../../../../../core/integration/models/authority-options.model'; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts index bae79cc348..6bd5a604a0 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts @@ -1,6 +1,6 @@ import { DynamicFormControlLayout, DynamicFormGroupModel, DynamicInputModelConfig, serializable } from '@ng-dynamic-forms/core'; import { DsDynamicInputModel, DsDynamicInputModelConfig } from './ds-dynamic-input.model'; -import { Subject } from 'rxjs/Subject'; +import { Subject } from 'rxjs'; import { DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core/src/model/form-group/dynamic-form-group.model'; import { LanguageCode } from '../../models/form-field-language-value.model'; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts index b38ea142f0..b91af8f0c9 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts @@ -6,16 +6,16 @@ import { import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './tag/dynamic-tag.model'; export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig { - notRepeteable: boolean; + notRepeatable: boolean; } export class DynamicRowArrayModel extends DynamicFormArrayModel { - @serializable() notRepeteable = false; + @serializable() notRepeatable = false; isRowArray = true; constructor(config: DynamicRowArrayModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); - this.notRepeteable = config.notRepeteable; + this.notRepeatable = config.notRepeatable; } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts index d1e6f67287..42d8f4b6de 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.component.spec.ts @@ -3,80 +3,88 @@ import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/c import { async, ComponentFixture, inject, TestBed, } from '@angular/core/testing'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; -import { DynamicFormValidationService } from '@ng-dynamic-forms/core'; -import { Store } from '@ngrx/store'; import { TranslateModule } from '@ngx-translate/core'; -import { Observable } from 'rxjs/Observable'; -import 'rxjs/add/observable/of'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { DsDynamicGroupComponent } from './dynamic-group.components'; import { DynamicGroupModel, DynamicGroupModelConfig } from './dynamic-group.model'; -import { FormRowModel, SubmissionFormsModel } from '../../../../../../core/shared/config/config-submission-forms.model'; +import { + FormRowModel, + SubmissionFormsModel +} from '../../../../../../core/shared/config/config-submission-forms.model'; import { FormFieldModel } from '../../../models/form-field.model'; import { FormBuilderService } from '../../../form-builder.service'; import { FormService } from '../../../../form.service'; import { GLOBAL_CONFIG } from '../../../../../../../config'; import { FormComponent } from '../../../../form.component'; -import { AppState } from '../../../../../../app.reducer'; import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; import { Chips } from '../../../../../chips/models/chips.model'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { DsDynamicInputModel } from '../ds-dynamic-input.model'; import { createTestComponent } from '../../../../../testing/utils'; +import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; +import { MockStore } from '../../../../../testing/mock-store'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../../../../../app.reducer'; -export const FORM_GROUP_TEST_MODEL_CONFIG = { - disabled: false, - errorMessages: {required: 'You must specify at least one author.'}, - formConfiguration: [{ - fields: [{ - hints: 'Enter the name of the author.', - input: {type: 'onebox'}, - label: 'Author', - languageCodes: [], - mandatory: 'true', - mandatoryMessage: 'Required field!', - repeatable: false, - selectableMetadata: [{ - authority: 'RPAuthority', - closed: false, - metadata: 'dc.contributor.author' - }], - } as FormFieldModel] - } as FormRowModel, { - fields: [{ - hints: 'Enter the affiliation of the author.', - input: {type: 'onebox'}, - label: 'Affiliation', - languageCodes: [], - mandatory: 'false', - repeatable: false, - selectableMetadata: [{ - authority: 'OUAuthority', - closed: false, - metadata: 'local.contributor.affiliation' - }] - } as FormFieldModel] - } as FormRowModel], - id: 'dc_contributor_author', - label: 'Authors', - mandatoryField: 'dc.contributor.author', - name: 'dc.contributor.author', - placeholder: 'Authors', - readOnly: false, - relationFields: ['local.contributor.affiliation'], - required: true, - scopeUUID: '43fe1f8c-09a6-4fcf-9c78-5d4fed8f2c8f', - submissionScope: undefined, - validators: {required: null} -} as DynamicGroupModelConfig; +export let FORM_GROUP_TEST_MODEL_CONFIG; -export const FORM_GROUP_TEST_GROUP = new FormGroup({ - dc_contributor_author: new FormControl(), -}); +export let FORM_GROUP_TEST_GROUP; -describe('DsDynamicGroupComponent test suite', () => { - const config = { +let config; + +function init() { + FORM_GROUP_TEST_MODEL_CONFIG = { + disabled: false, + errorMessages: { required: 'You must specify at least one author.' }, + formConfiguration: [{ + fields: [{ + hints: 'Enter the name of the author.', + input: { type: 'onebox' }, + label: 'Author', + languageCodes: [], + mandatory: 'true', + mandatoryMessage: 'Required field!', + repeatable: false, + selectableMetadata: [{ + authority: 'RPAuthority', + closed: false, + metadata: 'dc.contributor.author' + }], + } as FormFieldModel] + } as FormRowModel, { + fields: [{ + hints: 'Enter the affiliation of the author.', + input: { type: 'onebox' }, + label: 'Affiliation', + languageCodes: [], + mandatory: 'false', + repeatable: false, + selectableMetadata: [{ + authority: 'OUAuthority', + closed: false, + metadata: 'local.contributor.affiliation' + }] + } as FormFieldModel] + } as FormRowModel], + id: 'dc_contributor_author', + label: 'Authors', + mandatoryField: 'dc.contributor.author', + name: 'dc.contributor.author', + placeholder: 'Authors', + readOnly: false, + relationFields: ['local.contributor.affiliation'], + required: true, + scopeUUID: '43fe1f8c-09a6-4fcf-9c78-5d4fed8f2c8f', + submissionScope: undefined, + validators: { required: null } + } as DynamicGroupModelConfig; + + FORM_GROUP_TEST_GROUP = new FormGroup({ + dc_contributor_author: new FormControl(), + }); + + config = { form: { validatorMap: { required: 'required', @@ -84,6 +92,10 @@ describe('DsDynamicGroupComponent test suite', () => { } } } as any; + +} + +describe('DsDynamicGroupComponent test suite', () => { let testComp: TestComponent; let groupComp: DsDynamicGroupComponent; let testFixture: ComponentFixture; @@ -95,14 +107,11 @@ describe('DsDynamicGroupComponent test suite', () => { let control2: FormControl; let model2: DsDynamicInputModel; - const store: Store = jasmine.createSpyObj('store', { - dispatch: {}, - select: Observable.of(true) - }); - // async beforeEach beforeEach(async(() => { - + init(); + const store = new MockStore(Object.create(null)); + /* TODO make sure these files use mocks instead of real services/components https://github.com/DSpace/dspace-angular/issues/281 */ TestBed.configureTestingModule({ imports: [ BrowserAnimationsModule, @@ -120,10 +129,11 @@ describe('DsDynamicGroupComponent test suite', () => { ChangeDetectorRef, DsDynamicGroupComponent, DynamicFormValidationService, + DynamicFormLayoutService, FormBuilderService, FormComponent, FormService, - {provide: GLOBAL_CONFIG, useValue: config}, + { provide: GLOBAL_CONFIG, useValue: config }, {provide: Store, useValue: store}, ], schemas: [CUSTOM_ELEMENTS_SCHEMA] @@ -138,7 +148,6 @@ describe('DsDynamicGroupComponent test suite', () => { `; @@ -147,6 +156,11 @@ describe('DsDynamicGroupComponent test suite', () => { testComp = testFixture.componentInstance; }); + afterEach(() => { + testFixture.destroy(); + testComp = null; + }); + it('should create DsDynamicGroupComponent', inject([DsDynamicGroupComponent], (app: DsDynamicGroupComponent) => { expect(app).toBeDefined(); @@ -161,9 +175,7 @@ describe('DsDynamicGroupComponent test suite', () => { groupComp.formId = 'testForm'; groupComp.group = FORM_GROUP_TEST_GROUP; groupComp.model = new DynamicGroupModel(FORM_GROUP_TEST_MODEL_CONFIG); - groupComp.showErrorMessages = false; groupFixture.detectChanges(); - control1 = service.getFormControlById('dc_contributor_author', (groupComp as any).formRef.formGroup, groupComp.formModel) as FormControl; model1 = service.findById('dc_contributor_author', groupComp.formModel) as DsDynamicInputModel; control2 = service.getFormControlById('local_contributor_affiliation', (groupComp as any).formRef.formGroup, groupComp.formModel) as FormControl; @@ -178,11 +190,12 @@ describe('DsDynamicGroupComponent test suite', () => { }); it('should init component properly', inject([FormBuilderService], (service: FormBuilderService) => { - const formConfig = {rows: groupComp.model.formConfiguration} as SubmissionFormsModel; + const formConfig = { rows: groupComp.model.formConfiguration } as SubmissionFormsModel; const formModel = service.modelFromConfiguration(formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly); const chips = new Chips([], 'value', 'dc.contributor.author'); - - expect(groupComp.formCollapsed).toEqual(Observable.of(false)); + groupComp.formCollapsed.subscribe((value) => { + expect(value).toEqual(false); + }); expect(groupComp.formModel.length).toEqual(formModel.length); expect(groupComp.chips.getChipsItems()).toEqual(chips.getChipsItems()); })); @@ -203,7 +216,9 @@ describe('DsDynamicGroupComponent test suite', () => { btnEl.click(); expect(groupComp.chips.getChipsItems()).toEqual(modelValue); - expect(groupComp.formCollapsed).toEqual(Observable.of(true)); + groupComp.formCollapsed.subscribe((value) => { + expect(value).toEqual(true); + }) }); it('should clear form inputs', () => { @@ -220,7 +235,9 @@ describe('DsDynamicGroupComponent test suite', () => { expect(control1.value).toBeNull(); expect(control2.value).toBeNull(); - expect(groupComp.formCollapsed).toEqual(Observable.of(false)); + groupComp.formCollapsed.subscribe((value) => { + expect(value).toEqual(false); + }); }); }); @@ -237,7 +254,6 @@ describe('DsDynamicGroupComponent test suite', () => { 'local.contributor.affiliation': new FormFieldMetadataValueObject('test affiliation') }]; groupComp.model.value = modelValue; - groupComp.showErrorMessages = false; groupFixture.detectChanges(); }); @@ -248,11 +264,12 @@ describe('DsDynamicGroupComponent test suite', () => { }); it('should init component properly', inject([FormBuilderService], (service: FormBuilderService) => { - const formConfig = {rows: groupComp.model.formConfiguration} as SubmissionFormsModel; + const formConfig = { rows: groupComp.model.formConfiguration } as SubmissionFormsModel; const formModel = service.modelFromConfiguration(formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly); const chips = new Chips(modelValue, 'value', 'dc.contributor.author'); - - expect(groupComp.formCollapsed).toEqual(Observable.of(true)); + groupComp.formCollapsed.subscribe((value) => { + expect(value).toEqual(true); + }) expect(groupComp.formModel.length).toEqual(formModel.length); expect(groupComp.chips.getChipsItems()).toEqual(chips.getChipsItems()); })); @@ -280,7 +297,9 @@ describe('DsDynamicGroupComponent test suite', () => { groupFixture.detectChanges(); expect(groupComp.chips.getChipsItems()).toEqual(modelValue); - expect(groupComp.formCollapsed).toEqual(Observable.of(true)); + groupComp.formCollapsed.subscribe((value) => { + expect(value).toEqual(true); + }) })); it('should delete existing chips item', () => { @@ -292,7 +311,9 @@ describe('DsDynamicGroupComponent test suite', () => { btnEl.click(); expect(groupComp.chips.getChipsItems()).toEqual([]); - expect(groupComp.formCollapsed).toEqual(Observable.of(false)); + groupComp.formCollapsed.subscribe((value) => { + expect(value).toEqual(false); + }) }); }); }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts index a55e7aff9d..40e337588a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/dynamic-group/dynamic-group.components.ts @@ -1,3 +1,4 @@ +import { of as observableOf, Subscription } from 'rxjs'; import { ChangeDetectorRef, Component, @@ -9,9 +10,14 @@ import { Output, ViewChild } from '@angular/core'; - -import { Observable } from 'rxjs/Observable'; -import { DynamicFormControlModel, DynamicFormGroupModel, DynamicInputModel } from '@ng-dynamic-forms/core'; +import { + DynamicFormControlComponent, + DynamicFormControlModel, + DynamicFormGroupModel, + DynamicFormLayoutService, + DynamicFormValidationService, + DynamicInputModel +} from '@ng-dynamic-forms/core'; import { isEqual } from 'lodash'; import { DynamicGroupModel, PLACEHOLDER_PARENT_METADATA } from './dynamic-group.model'; @@ -26,10 +32,7 @@ import { ChipsItem } from '../../../../../chips/models/chips-item.model'; import { GlobalConfig } from '../../../../../../../config/global-config.interface'; import { GLOBAL_CONFIG } from '../../../../../../../config'; import { FormGroup } from '@angular/forms'; -import { Subscription } from 'rxjs/Subscription'; import { hasOnlyEmptyProperties } from '../../../../../object.util'; -import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; -import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model'; @Component({ selector: 'ds-dynamic-group', @@ -37,19 +40,18 @@ import { AuthorityValueModel } from '../../../../../../core/integration/models/a templateUrl: './dynamic-group.component.html', animations: [shrinkInOut] }) -export class DsDynamicGroupComponent implements OnDestroy, OnInit { +export class DsDynamicGroupComponent extends DynamicFormControlComponent implements OnDestroy, OnInit { @Input() formId: string; @Input() group: FormGroup; @Input() model: DynamicGroupModel; - @Input() showErrorMessages = false; @Output() blur: EventEmitter = new EventEmitter(); @Output() change: EventEmitter = new EventEmitter(); @Output() focus: EventEmitter = new EventEmitter(); public chips: Chips; - public formCollapsed = Observable.of(false); + public formCollapsed = observableOf(false); public formModel: DynamicFormControlModel[]; public editMode = false; @@ -61,13 +63,18 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, private formBuilderService: FormBuilderService, private formService: FormService, - private cdr: ChangeDetectorRef) { + private cdr: ChangeDetectorRef, + protected layoutService: DynamicFormLayoutService, + protected validationService: DynamicFormValidationService + ) { + super(layoutService, validationService); + } ngOnInit() { - const config = {rows: this.model.formConfiguration} as SubmissionFormsModel; + const config = { rows: this.model.formConfiguration } as SubmissionFormsModel; if (!this.model.isEmpty()) { - this.formCollapsed = Observable.of(true); + this.formCollapsed = observableOf(true); } this.model.valueUpdates.subscribe((value: any[]) => { if ((isNotEmpty(value) && !(value.length === 1 && hasOnlyEmptyProperties(value[0])))) { @@ -75,7 +82,7 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { } else { this.expandForm(); } - // this.formCollapsed = (isNotEmpty(value) && !(value.length === 1 && hasOnlyEmptyProperties(value[0]))) ? Observable.of(true) : Observable.of(false); + // this.formCollapsed = (isNotEmpty(value) && !(value.length === 1 && hasOnlyEmptyProperties(value[0]))) ? observableOf(true) : observableOf(false); }); this.formId = this.formService.getUniqueId(this.model.id); @@ -152,12 +159,12 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { } collapseForm() { - this.formCollapsed = Observable.of(true); + this.formCollapsed = observableOf(true); this.clear(); } expandForm() { - this.formCollapsed = Observable.of(false); + this.formCollapsed = observableOf(false); } clear() { @@ -168,7 +175,7 @@ export class DsDynamicGroupComponent implements OnDestroy, OnInit { } this.resetForm(); if (!this.model.isEmpty()) { - this.formCollapsed = Observable.of(true); + this.formCollapsed = observableOf(true); } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts index 5fdc530ebd..ed9db8b1a5 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list-checkbox-group.model.ts @@ -1,4 +1,4 @@ -import { Subject } from 'rxjs/Subject'; +import { Subject } from 'rxjs'; import { DynamicCheckboxGroupModel, DynamicFormControlLayout, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts index 6a765eba4a..adfd087033 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.spec.ts @@ -9,7 +9,12 @@ import { DsDynamicListComponent } from './dynamic-list.component'; import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model'; import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; import { FormBuilderService } from '../../../form-builder.service'; -import { DynamicFormControlLayout, DynamicFormsCoreModule, DynamicFormValidationService } from '@ng-dynamic-forms/core'; +import { + DynamicFormControlLayout, + DynamicFormLayoutService, + DynamicFormsCoreModule, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { AuthorityService } from '../../../../../../core/integration/authority.service'; import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub'; @@ -90,12 +95,13 @@ describe('DsDynamicListComponent test suite', () => { TestComponent, ], // declare the test component providers: [ - AuthorityService, ChangeDetectorRef, DsDynamicListComponent, DynamicFormValidationService, FormBuilderService, {provide: AuthorityService, useValue: authorityServiceStub}, + {provide: DynamicFormLayoutService, useValue: {}}, + {provide: DynamicFormValidationService, useValue: {}} ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); @@ -110,7 +116,6 @@ describe('DsDynamicListComponent test suite', () => { [bindId]="bindId" [group]="group" [model]="model" - [showErrorMessages]="showErrorMessages" (blur)="onBlur($event)" (change)="onValueChange($event)" (focus)="onFocus($event)">`; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts index ec0d3e343a..dc808f4759 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/list/dynamic-list.component.ts @@ -7,7 +7,11 @@ import { IntegrationSearchOptions } from '../../../../../../core/integration/mod import { hasValue, isNotEmpty } from '../../../../../empty.util'; import { DynamicListCheckboxGroupModel } from './dynamic-list-checkbox-group.model'; import { FormBuilderService } from '../../../form-builder.service'; -import { DynamicCheckboxModel } from '@ng-dynamic-forms/core'; +import { + DynamicCheckboxModel, + DynamicFormControlComponent, DynamicFormLayoutService, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model'; import { DynamicListRadioGroupModel } from './dynamic-list-radio-group.model'; import { IntegrationData } from '../../../../../../core/integration/integration-data'; @@ -25,11 +29,10 @@ export interface ListItem { templateUrl: './dynamic-list.component.html' }) -export class DsDynamicListComponent implements OnInit { +export class DsDynamicListComponent extends DynamicFormControlComponent implements OnInit { @Input() bindId = true; @Input() group: FormGroup; @Input() model: DynamicListCheckboxGroupModel | DynamicListRadioGroupModel; - @Input() showErrorMessages = false; @Output() blur: EventEmitter = new EventEmitter(); @Output() change: EventEmitter = new EventEmitter(); @@ -41,7 +44,11 @@ export class DsDynamicListComponent implements OnInit { constructor(private authorityService: AuthorityService, private cdr: ChangeDetectorRef, - private formBuilderService: FormBuilderService) { + private formBuilderService: FormBuilderService, + protected layoutService: DynamicFormLayoutService, + protected validationService: DynamicFormValidationService + ) { + super(layoutService, validationService); } ngOnInit() { @@ -99,7 +106,7 @@ export class DsDynamicListComponent implements OnInit { const value = option.id || option.value; const checked: boolean = isNotEmpty(findKey( this.model.value, - {value: option.value})); + (v) => v.value === option.value)); const item: ListItem = { id: value, @@ -110,7 +117,10 @@ export class DsDynamicListComponent implements OnInit { if (this.model.repeatable) { this.formBuilderService.addFormGroupControl(listGroup, (this.model as DynamicListCheckboxGroupModel), new DynamicCheckboxModel(item)); } else { - (this.model as DynamicListRadioGroupModel).options.push({label: item.label, value: option}); + (this.model as DynamicListRadioGroupModel).options.push({ + label: item.label, + value: option + }); } tempList.push(item); itemsPerGroup++; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts index ce45453bee..62e9191893 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts @@ -6,7 +6,11 @@ import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@ang import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; -import { DynamicFormsCoreModule, DynamicFormValidationService } from '@ng-dynamic-forms/core'; +import { + DynamicFormLayoutService, + DynamicFormsCoreModule, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { AuthorityService } from '../../../../../../core/integration/authority.service'; import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub'; @@ -14,16 +18,13 @@ import { DsDynamicLookupComponent } from './dynamic-lookup.component'; import { DynamicLookupModel } from './dynamic-lookup.model'; import { InfiniteScrollModule } from 'ngx-infinite-scroll'; import { TranslateModule } from '@ngx-translate/core'; -import { FormBuilderService } from '../../../form-builder.service'; -import { FormService } from '../../../../form.service'; -import { FormComponent } from '../../../../form.component'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { By } from '@angular/platform-browser'; import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model'; -import { DynamicLookupNameModel } from './dynamic-lookup-name.model'; import { createTestComponent } from '../../../../../testing/utils'; +import { DynamicLookupNameModel } from './dynamic-lookup-name.model'; -export const LOOKUP_TEST_MODEL_CONFIG = { +let LOOKUP_TEST_MODEL_CONFIG = { authorityOptions: { closed: false, metadata: 'lookup', @@ -31,7 +32,7 @@ export const LOOKUP_TEST_MODEL_CONFIG = { scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' } as AuthorityOptions, disabled: false, - errorMessages: {required: 'Required field.'}, + errorMessages: { required: 'Required field.' }, id: 'lookup', label: 'Author', maxOptions: 10, @@ -41,11 +42,11 @@ export const LOOKUP_TEST_MODEL_CONFIG = { required: true, repeatable: true, separator: ',', - validators: {required: null}, + validators: { required: null }, value: undefined }; -export const LOOKUP_NAME_TEST_MODEL_CONFIG = { +let LOOKUP_NAME_TEST_MODEL_CONFIG = { authorityOptions: { closed: false, metadata: 'lookup-name', @@ -53,7 +54,7 @@ export const LOOKUP_NAME_TEST_MODEL_CONFIG = { scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' } as AuthorityOptions, disabled: false, - errorMessages: {required: 'Required field.'}, + errorMessages: { required: 'Required field.' }, id: 'lookupName', label: 'Author', maxOptions: 10, @@ -63,16 +64,67 @@ export const LOOKUP_NAME_TEST_MODEL_CONFIG = { required: true, repeatable: true, separator: ',', - validators: {required: null}, + validators: { required: null }, value: undefined }; -export const LOOKUP_TEST_GROUP = new FormGroup({ +let LOOKUP_TEST_GROUP = new FormGroup({ lookup: new FormControl(), lookupName: new FormControl() }); describe('Dynamic Lookup component', () => { + function init() { + LOOKUP_TEST_MODEL_CONFIG = { + authorityOptions: { + closed: false, + metadata: 'lookup', + name: 'RPAuthority', + scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + } as AuthorityOptions, + disabled: false, + errorMessages: { required: 'Required field.' }, + id: 'lookup', + label: 'Author', + maxOptions: 10, + name: 'lookup', + placeholder: 'Author', + readOnly: false, + required: true, + repeatable: true, + separator: ',', + validators: { required: null }, + value: undefined + }; + + LOOKUP_NAME_TEST_MODEL_CONFIG = { + authorityOptions: { + closed: false, + metadata: 'lookup-name', + name: 'RPAuthority', + scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + } as AuthorityOptions, + disabled: false, + errorMessages: { required: 'Required field.' }, + id: 'lookupName', + label: 'Author', + maxOptions: 10, + name: 'lookupName', + placeholder: 'Author', + readOnly: false, + required: true, + repeatable: true, + separator: ',', + validators: { required: null }, + value: undefined + }; + + LOOKUP_TEST_GROUP = new FormGroup({ + lookup: new FormControl(), + lookupName: new FormControl() + }); + + } let testComp: TestComponent; let lookupComp: DsDynamicLookupComponent; @@ -80,11 +132,11 @@ describe('Dynamic Lookup component', () => { let lookupFixture: ComponentFixture; let html; - const authorityServiceStub = new AuthorityServiceStub(); - + let authorityServiceStub; // async beforeEach beforeEach(async(() => { - + const authorityService = new AuthorityServiceStub(); + authorityServiceStub = authorityService; TestBed.configureTestingModule({ imports: [ DynamicFormsCoreModule, @@ -102,18 +154,19 @@ describe('Dynamic Lookup component', () => { providers: [ ChangeDetectorRef, DsDynamicLookupComponent, - DynamicFormValidationService, - FormBuilderService, - FormComponent, - FormService, - {provide: AuthorityService, useValue: authorityServiceStub}, + { provide: AuthorityService, useValue: authorityService }, + { provide: DynamicFormLayoutService, useValue: {} }, + { provide: DynamicFormValidationService, useValue: {} } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); - })); - describe('', () => { + beforeEach(() => { + init(); + }); + + describe('DynamicLookUpComponent', () => { // synchronous beforeEach beforeEach(() => { html = ` @@ -121,7 +174,6 @@ describe('Dynamic Lookup component', () => { [bindId]="bindId" [group]="group" [model]="model" - [showErrorMessages]="showErrorMessages" (blur)="onBlur($event)" (change)="onValueChange($event)" (focus)="onFocus($event)">`; @@ -129,200 +181,236 @@ describe('Dynamic Lookup component', () => { testFixture = createTestComponent(html, TestComponent) as ComponentFixture; testComp = testFixture.componentInstance; }); - + afterEach(() => { + testFixture.destroy(); + testComp = null; + }); it('should create DsDynamicLookupComponent', inject([DsDynamicLookupComponent], (app: DsDynamicLookupComponent) => { expect(app).toBeDefined(); })); - }); - describe('when model is DynamicLookupModel', () => { + describe('when model is DynamicLookupModel', () => { - describe('', () => { - beforeEach(() => { + describe('', () => { + beforeEach(() => { + + lookupFixture = TestBed.createComponent(DsDynamicLookupComponent); + lookupComp = lookupFixture.componentInstance; // FormComponent test instance + lookupComp.group = LOOKUP_TEST_GROUP; + lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG); + lookupFixture.detectChanges(); + }); + afterEach(() => { + lookupFixture.destroy(); + lookupComp = null; + }); + it('should render only an input element', () => { + const de = lookupFixture.debugElement.queryAll(By.css('input.form-control')); + expect(de.length).toBe(1); + }); - lookupFixture = TestBed.createComponent(DsDynamicLookupComponent); - lookupComp = lookupFixture.componentInstance; // FormComponent test instance - lookupComp.group = LOOKUP_TEST_GROUP; - lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG); - lookupFixture.detectChanges(); }); - it('should render only an input element', () => { - const de = lookupFixture.debugElement.queryAll(By.css('input.form-control')); - expect(de.length).toBe(1); + describe('and init model value is empty', () => { + beforeEach(() => { + lookupFixture = TestBed.createComponent(DsDynamicLookupComponent); + lookupComp = lookupFixture.componentInstance; // FormComponent test instance + lookupComp.group = LOOKUP_TEST_GROUP; + lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG); + lookupFixture.detectChanges(); + }); + + afterEach(() => { + lookupFixture.destroy(); + lookupComp = null; + }); + + it('should init component properly', () => { + expect(lookupComp.firstInputValue).toBe(''); + }); + + it('should return search results', fakeAsync(() => { + const de = lookupFixture.debugElement.queryAll(By.css('button')); + const btnEl = de[0].nativeElement; + const results$ = authorityServiceStub.getEntriesByName({} as any); + + lookupComp.firstInputValue = 'test'; + lookupFixture.detectChanges(); + + btnEl.click(); + tick(); + lookupFixture.detectChanges(); + results$.subscribe((results) => { + expect(lookupComp.optionsList).toEqual(results.payload); + }); + + })); + + it('should select a results entry properly', fakeAsync(() => { + let de = lookupFixture.debugElement.queryAll(By.css('button')); + const btnEl = de[0].nativeElement; + const selectedValue = Object.assign(new AuthorityValueModel(), { + id: 1, + display: 'one', + value: 1 + }); + spyOn(lookupComp.change, 'emit'); + lookupComp.firstInputValue = 'test'; + lookupFixture.detectChanges(); + btnEl.click(); + tick(); + lookupFixture.detectChanges(); + de = lookupFixture.debugElement.queryAll(By.css('button.dropdown-item')); + const entryEl = de[0].nativeElement; + entryEl.click(); + lookupFixture.detectChanges(); + expect(lookupComp.firstInputValue).toEqual('one'); + expect(lookupComp.model.value).toEqual(selectedValue); + expect(lookupComp.change.emit).toHaveBeenCalled(); + })); + + it('should set model.value on input type when AuthorityOptions.closed is false', fakeAsync(() => { + lookupComp.firstInputValue = 'test'; + lookupFixture.detectChanges(); + + lookupComp.onInput(new Event('input')); + expect(lookupComp.model.value).toEqual(new FormFieldMetadataValueObject('test')) + + })); + + it('should not set model.value on input type when AuthorityOptions.closed is true', () => { + lookupComp.model.authorityOptions.closed = true; + lookupComp.firstInputValue = 'test'; + lookupFixture.detectChanges(); + + lookupComp.onInput(new Event('input')); + expect(lookupComp.model.value).not.toBeDefined(); + + }); }); - }); + describe('and init model value is not empty', () => { + beforeEach(() => { - describe('and init model value is empty', () => { - beforeEach(() => { - - lookupFixture = TestBed.createComponent(DsDynamicLookupComponent); - lookupComp = lookupFixture.componentInstance; // FormComponent test instance - lookupComp.group = LOOKUP_TEST_GROUP; - lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG); - lookupFixture.detectChanges(); - }); - - it('should init component properly', () => { - expect(lookupComp.firstInputValue).toBe(''); - }); - - it('should return search results', fakeAsync(() => { - const de = lookupFixture.debugElement.queryAll(By.css('button')); - const btnEl = de[0].nativeElement; - const results$ = authorityServiceStub.getEntriesByName({} as any); - - lookupComp.firstInputValue = 'test'; - lookupFixture.detectChanges(); - - btnEl.click(); - tick(); - lookupFixture.detectChanges(); - results$.subscribe((results) => { - expect(lookupComp.optionsList).toEqual(results.payload); - }) - - })); - - it('should select a results entry properly', fakeAsync(() => { - let de = lookupFixture.debugElement.queryAll(By.css('button')); - const btnEl = de[0].nativeElement; - const selectedValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'one', value: 1}); - spyOn(lookupComp.change, 'emit'); - - lookupComp.firstInputValue = 'test'; - lookupFixture.detectChanges(); - btnEl.click(); - tick(); - lookupFixture.detectChanges(); - de = lookupFixture.debugElement.queryAll(By.css('button.dropdown-item')); - const entryEl = de[0].nativeElement; - entryEl.click(); - - expect(lookupComp.firstInputValue).toEqual('one'); - expect(lookupComp.model.value).toEqual(selectedValue); - expect(lookupComp.change.emit).toHaveBeenCalled(); - })); - - it('should set model.value on input type when AuthorityOptions.closed is false', fakeAsync(() => { - lookupComp.firstInputValue = 'test'; - lookupFixture.detectChanges(); - - lookupComp.onInput(new Event('input')); - expect(lookupComp.model.value).toEqual(new FormFieldMetadataValueObject('test')) - - })); - - it('should not set model.value on input type when AuthorityOptions.closed is true', () => { - lookupComp.model.authorityOptions.closed = true; - lookupComp.firstInputValue = 'test'; - lookupFixture.detectChanges(); - - lookupComp.onInput(new Event('input')); - expect(lookupComp.model.value).not.toBeDefined(); + lookupFixture = TestBed.createComponent(DsDynamicLookupComponent); + lookupComp = lookupFixture.componentInstance; // FormComponent test instance + lookupComp.group = LOOKUP_TEST_GROUP; + lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG); + lookupComp.model.value = new FormFieldMetadataValueObject('test', null, 'test001'); + lookupFixture.detectChanges(); + // spyOn(store, 'dispatch'); + }); + afterEach(() => { + lookupFixture.destroy(); + lookupComp = null; + }); + it('should init component properly', () => { + expect(lookupComp.firstInputValue).toBe('test'); + }); }); }); - describe('and init model value is not empty', () => { - beforeEach(() => { + describe('when model is DynamicLookupNameModel', () => { - lookupFixture = TestBed.createComponent(DsDynamicLookupComponent); - lookupComp = lookupFixture.componentInstance; // FormComponent test instance - lookupComp.group = LOOKUP_TEST_GROUP; - lookupComp.model = new DynamicLookupModel(LOOKUP_TEST_MODEL_CONFIG); - lookupComp.model.value = new FormFieldMetadataValueObject('test', null, 'test001'); - lookupFixture.detectChanges(); + describe('', () => { + beforeEach(() => { + + lookupFixture = TestBed.createComponent(DsDynamicLookupComponent); + lookupComp = lookupFixture.componentInstance; // FormComponent test instance + lookupComp.group = LOOKUP_TEST_GROUP; + lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG); + lookupFixture.detectChanges(); + + // spyOn(store, 'dispatch'); + }); + afterEach(() => { + lookupFixture.destroy(); + lookupComp = null; + }); + it('should render two input element', () => { + const de = lookupFixture.debugElement.queryAll(By.css('input.form-control')); + expect(de.length).toBe(2); + }); - // spyOn(store, 'dispatch'); }); - it('should init component properly', () => { - expect(lookupComp.firstInputValue).toBe('test') + describe('and init model value is empty', () => { + + beforeEach(() => { + + lookupFixture = TestBed.createComponent(DsDynamicLookupComponent); + lookupComp = lookupFixture.componentInstance; // FormComponent test instance + lookupComp.group = LOOKUP_TEST_GROUP; + lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG); + lookupFixture.detectChanges(); + }); + + afterEach(() => { + lookupFixture.destroy(); + lookupComp = null; + }); + + it('should select a results entry properly', fakeAsync(() => { + const payload = [ + Object.assign(new AuthorityValueModel(), { + id: 1, + display: 'Name, Lastname', + value: 1 + }), + Object.assign(new AuthorityValueModel(), { + id: 2, + display: 'NameTwo, LastnameTwo', + value: 2 + }), + ]; + let de = lookupFixture.debugElement.queryAll(By.css('button')); + const btnEl = de[0].nativeElement; + const selectedValue = Object.assign(new AuthorityValueModel(), { + id: 1, + display: 'Name, Lastname', + value: 1 + }); + spyOn(lookupComp.change, 'emit'); + authorityServiceStub.setNewPayload(payload); + lookupComp.firstInputValue = 'test'; + lookupFixture.detectChanges(); + btnEl.click(); + tick(); + lookupFixture.detectChanges(); + de = lookupFixture.debugElement.queryAll(By.css('button.dropdown-item')); + const entryEl = de[0].nativeElement; + entryEl.click(); + + expect(lookupComp.firstInputValue).toEqual('Name'); + expect(lookupComp.secondInputValue).toEqual('Lastname'); + expect(lookupComp.model.value).toEqual(selectedValue); + expect(lookupComp.change.emit).toHaveBeenCalled(); + })); + }); + + describe('and init model value is not empty', () => { + beforeEach(() => { + + lookupFixture = TestBed.createComponent(DsDynamicLookupComponent); + lookupComp = lookupFixture.componentInstance; // FormComponent test instance + lookupComp.group = LOOKUP_TEST_GROUP; + lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG); + lookupComp.model.value = new FormFieldMetadataValueObject('Name, Lastname', null, 'test001'); + lookupFixture.detectChanges(); + + }); + afterEach(() => { + lookupFixture.destroy(); + lookupComp = null; + }); + it('should init component properly', () => { + expect(lookupComp.firstInputValue).toBe('Name'); + expect(lookupComp.secondInputValue).toBe('Lastname'); + }); }); }); }); - - describe('when model is DynamicLookupNameModel', () => { - - describe('', () => { - beforeEach(() => { - - lookupFixture = TestBed.createComponent(DsDynamicLookupComponent); - lookupComp = lookupFixture.componentInstance; // FormComponent test instance - lookupComp.group = LOOKUP_TEST_GROUP; - lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG); - lookupFixture.detectChanges(); - - // spyOn(store, 'dispatch'); - }); - - it('should render two input element', () => { - const de = lookupFixture.debugElement.queryAll(By.css('input.form-control')); - expect(de.length).toBe(2); - }); - - }); - - describe('and init model value is empty', () => { - - beforeEach(() => { - - lookupFixture = TestBed.createComponent(DsDynamicLookupComponent); - lookupComp = lookupFixture.componentInstance; // FormComponent test instance - lookupComp.group = LOOKUP_TEST_GROUP; - lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG); - lookupFixture.detectChanges(); - }); - - it('should select a results entry properly', fakeAsync(() => { - const payload = [ - Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1}), - Object.assign(new AuthorityValueModel(), {id: 2, display: 'NameTwo, LastnameTwo', value: 2}), - ]; - let de = lookupFixture.debugElement.queryAll(By.css('button')); - const btnEl = de[0].nativeElement; - const selectedValue = Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1}); - - spyOn(lookupComp.change, 'emit'); - authorityServiceStub.setNewPayload(payload); - lookupComp.firstInputValue = 'test'; - lookupFixture.detectChanges(); - btnEl.click(); - tick(); - lookupFixture.detectChanges(); - de = lookupFixture.debugElement.queryAll(By.css('button.dropdown-item')); - const entryEl = de[0].nativeElement; - entryEl.click(); - - expect(lookupComp.firstInputValue).toEqual('Name'); - expect(lookupComp.secondInputValue).toEqual('Lastname'); - expect(lookupComp.model.value).toEqual(selectedValue); - expect(lookupComp.change.emit).toHaveBeenCalled(); - })); - - }); - - describe('and init model value is not empty', () => { - beforeEach(() => { - - lookupFixture = TestBed.createComponent(DsDynamicLookupComponent); - lookupComp = lookupFixture.componentInstance; // FormComponent test instance - lookupComp.group = LOOKUP_TEST_GROUP; - lookupComp.model = new DynamicLookupNameModel(LOOKUP_NAME_TEST_MODEL_CONFIG); - lookupComp.model.value = new FormFieldMetadataValueObject('Name, Lastname', null, 'test001'); - lookupFixture.detectChanges(); - - }); - - it('should init component properly', () => { - expect(lookupComp.firstInputValue).toBe('Name'); - expect(lookupComp.secondInputValue).toBe('Lastname'); - }); - }); - - }); }); // declare a test component @@ -337,7 +425,4 @@ class TestComponent { inputLookupModelConfig = LOOKUP_TEST_MODEL_CONFIG; model = new DynamicLookupModel(this.inputLookupModelConfig); - - showErrorMessages = false; - } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts index 4e88e9c78e..2a2ee64e9e 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.ts @@ -1,3 +1,5 @@ + +import {distinctUntilChanged} from 'rxjs/operators'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; import { FormGroup } from '@angular/forms'; @@ -7,21 +9,25 @@ import { IntegrationSearchOptions } from '../../../../../../core/integration/mod import { hasValue, isEmpty, isNotEmpty, isNull, isUndefined } from '../../../../../empty.util'; import { IntegrationData } from '../../../../../../core/integration/integration-data'; import { PageInfo } from '../../../../../../core/shared/page-info.model'; -import { Subscription } from 'rxjs/Subscription'; +import { Subscription } from 'rxjs'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model'; import { DynamicLookupNameModel } from './dynamic-lookup-name.model'; +import { + DynamicFormControlComponent, + DynamicFormLayoutService, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; @Component({ selector: 'ds-dynamic-lookup', styleUrls: ['./dynamic-lookup.component.scss'], templateUrl: './dynamic-lookup.component.html' }) -export class DsDynamicLookupComponent implements OnDestroy, OnInit { +export class DsDynamicLookupComponent extends DynamicFormControlComponent implements OnDestroy, OnInit { @Input() bindId = true; @Input() group: FormGroup; @Input() model: DynamicLookupModel | DynamicLookupNameModel; - @Input() showErrorMessages = false; @Output() blur: EventEmitter = new EventEmitter(); @Output() change: EventEmitter = new EventEmitter(); @@ -37,7 +43,11 @@ export class DsDynamicLookupComponent implements OnDestroy, OnInit { protected sub: Subscription; constructor(private authorityService: AuthorityService, - private cdr: ChangeDetectorRef) { + private cdr: ChangeDetectorRef, + protected layoutService: DynamicFormLayoutService, + protected validationService: DynamicFormValidationService + ) { + super(layoutService, validationService); } ngOnInit() { @@ -137,8 +147,8 @@ export class DsDynamicLookupComponent implements OnDestroy, OnInit { this.searchOptions.query = this.getCurrentValue(); this.loading = true; - this.authorityService.getEntriesByName(this.searchOptions) - .distinctUntilChanged() + this.authorityService.getEntriesByName(this.searchOptions).pipe( + distinctUntilChanged()) .subscribe((object: IntegrationData) => { this.optionsList = object.payload; this.pageInfo = object.pageInfo; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts index 49cdb5d890..6902530956 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts @@ -6,7 +6,11 @@ import { async, ComponentFixture, fakeAsync, inject, TestBed, tick, } from '@ang import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; -import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core'; +import { + DynamicFormLayoutService, + DynamicFormsCoreModule, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { AuthorityService } from '../../../../../../core/integration/authority.service'; import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub'; @@ -77,6 +81,8 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => { ChangeDetectorRef, DsDynamicScrollableDropdownComponent, {provide: AuthorityService, useValue: authorityServiceStub}, + {provide: DynamicFormLayoutService, useValue: {}}, + {provide: DynamicFormValidationService, useValue: {}} ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); @@ -90,7 +96,6 @@ describe('Dynamic Dynamic Scrollable Dropdown component', () => { `; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts index 1c8bf15f1a..02468f9fbf 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.ts @@ -1,3 +1,5 @@ + +import {tap} from 'rxjs/operators'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output, ViewChild } from '@angular/core'; import { FormGroup } from '@angular/forms'; @@ -9,17 +11,21 @@ import { IntegrationSearchOptions } from '../../../../../../core/integration/mod import { IntegrationData } from '../../../../../../core/integration/integration-data'; import { AuthorityValueModel } from '../../../../../../core/integration/models/authority-value.model'; import { NgbDropdown } from '@ng-bootstrap/ng-bootstrap'; +import { + DynamicFormControlComponent, + DynamicFormLayoutService, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; @Component({ selector: 'ds-dynamic-scrollable-dropdown', styleUrls: ['./dynamic-scrollable-dropdown.component.scss'], templateUrl: './dynamic-scrollable-dropdown.component.html' }) -export class DsDynamicScrollableDropdownComponent implements OnInit { +export class DsDynamicScrollableDropdownComponent extends DynamicFormControlComponent implements OnInit { @Input() bindId = true; @Input() group: FormGroup; @Input() model: DynamicScrollableDropdownModel; - @Input() showErrorMessages = false; @Output() blur: EventEmitter = new EventEmitter(); @Output() change: EventEmitter = new EventEmitter(); @@ -31,7 +37,13 @@ export class DsDynamicScrollableDropdownComponent implements OnInit { protected searchOptions: IntegrationSearchOptions; - constructor(private authorityService: AuthorityService, private cdr: ChangeDetectorRef) {} + constructor(private authorityService: AuthorityService, + private cdr: ChangeDetectorRef, + protected layoutService: DynamicFormLayoutService, + protected validationService: DynamicFormValidationService + ) { + super(layoutService, validationService); + } ngOnInit() { this.searchOptions = new IntegrationSearchOptions( @@ -66,8 +78,8 @@ export class DsDynamicScrollableDropdownComponent implements OnInit { if (!this.loading && this.pageInfo.currentPage <= this.pageInfo.totalPages) { this.loading = true; this.searchOptions.currentPage++; - this.authorityService.getEntriesByName(this.searchOptions) - .do(() => this.loading = false) + this.authorityService.getEntriesByName(this.searchOptions).pipe( + tap(() => this.loading = false)) .subscribe((object: IntegrationData) => { this.optionsList = this.optionsList.concat(object.payload); this.pageInfo = object.pageInfo; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts index 24959f4be4..9eaa23c004 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.spec.ts @@ -2,12 +2,15 @@ import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { async, ComponentFixture, fakeAsync, flush, inject, TestBed, } from '@angular/core/testing'; +import { of as observableOf } from 'rxjs'; -import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core'; +import { + DynamicFormLayoutService, + DynamicFormsCoreModule, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { NgbModule, NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; -import { Observable } from 'rxjs/Observable'; -import 'rxjs/add/observable/of' import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; import { AuthorityService } from '../../../../../../core/integration/authority.service'; @@ -34,27 +37,32 @@ function createKeyUpEvent(key: number) { return event; } -export const TAG_TEST_GROUP = new FormGroup({ - tag: new FormControl(), -}); +let TAG_TEST_GROUP; +let TAG_TEST_MODEL_CONFIG; -export const TAG_TEST_MODEL_CONFIG = { - authorityOptions: { - closed: false, - metadata: 'tag', - name: 'common_iso_languages', - scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' - } as AuthorityOptions, - disabled: false, - id: 'tag', - label: 'Keywords', - minChars: 3, - name: 'tag', - placeholder: 'Keywords', - readOnly: false, - required: false, - repeatable: false -}; +function init() { + TAG_TEST_GROUP = new FormGroup({ + tag: new FormControl(), + }); + + TAG_TEST_MODEL_CONFIG = { + authorityOptions: { + closed: false, + metadata: 'tag', + name: 'common_iso_languages', + scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + } as AuthorityOptions, + disabled: false, + id: 'tag', + label: 'Keywords', + minChars: 3, + name: 'tag', + placeholder: 'Keywords', + readOnly: false, + required: false, + repeatable: false + }; +} describe('DsDynamicTagComponent test suite', () => { @@ -69,7 +77,7 @@ describe('DsDynamicTagComponent test suite', () => { // async beforeEach beforeEach(async(() => { const authorityServiceStub = new AuthorityServiceStub(); - + init(); TestBed.configureTestingModule({ imports: [ DynamicFormsCoreModule, @@ -85,8 +93,10 @@ describe('DsDynamicTagComponent test suite', () => { providers: [ ChangeDetectorRef, DsDynamicTagComponent, - {provide: AuthorityService, useValue: authorityServiceStub}, - {provide: GLOBAL_CONFIG, useValue: {} as GlobalConfig}, + { provide: AuthorityService, useValue: authorityServiceStub }, + { provide: GLOBAL_CONFIG, useValue: {} as GlobalConfig }, + { provide: DynamicFormLayoutService, useValue: {} }, + { provide: DynamicFormValidationService, useValue: {} } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); @@ -100,7 +110,6 @@ describe('DsDynamicTagComponent test suite', () => { `; @@ -108,14 +117,16 @@ describe('DsDynamicTagComponent test suite', () => { testFixture = createTestComponent(html, TestComponent) as ComponentFixture; testComp = testFixture.componentInstance; }); - + afterEach(() => { + testFixture.destroy(); + }); it('should create DsDynamicTagComponent', inject([DsDynamicTagComponent], (app: DsDynamicTagComponent) => { expect(app).toBeDefined(); })); }); - describe('when authorityOptions are setted', () => { + describe('when authorityOptions are set', () => { describe('and init model value is empty', () => { beforeEach(() => { @@ -134,23 +145,28 @@ describe('DsDynamicTagComponent test suite', () => { it('should init component properly', () => { chips = new Chips([], 'display'); expect(tagComp.chips.getChipsItems()).toEqual(chips.getChipsItems()); + expect(tagComp.searchOptions).toBeDefined(); }); it('should search when 3+ characters typed', fakeAsync(() => { spyOn((tagComp as any).authorityService, 'getEntriesByName').and.callThrough(); - tagComp.search(Observable.of('test')).subscribe(() => { + tagComp.search(observableOf('test')).subscribe(() => { expect((tagComp as any).authorityService.getEntriesByName).toHaveBeenCalled(); }); })); it('should select a results entry properly', fakeAsync(() => { modelValue = [ - Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1}) + Object.assign(new AuthorityValueModel(), { id: 1, display: 'Name, Lastname', value: 1 }) ]; const event: NgbTypeaheadSelectItemEvent = { - item: Object.assign(new AuthorityValueModel(), {id: 1, display: 'Name, Lastname', value: 1}), + item: Object.assign(new AuthorityValueModel(), { + id: 1, + display: 'Name, Lastname', + value: 1 + }), preventDefault: () => { return; } @@ -225,7 +241,7 @@ describe('DsDynamicTagComponent test suite', () => { }); - describe('when authorityOptions are not setted', () => { + describe('when authorityOptions are not set', () => { describe('and init model value is empty', () => { beforeEach(() => { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts index ac23e665d0..b8ef84d48d 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/tag/dynamic-tag.component.ts @@ -1,7 +1,8 @@ +import {of as observableOf, Observable } from 'rxjs'; + +import {catchError, debounceTime, distinctUntilChanged, tap, switchMap, map, merge} from 'rxjs/operators'; import { ChangeDetectorRef, Component, EventEmitter, Inject, Input, OnInit, Output } from '@angular/core'; import { FormGroup } from '@angular/forms'; - -import { Observable } from 'rxjs/Observable'; import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; import { AuthorityService } from '../../../../../../core/integration/authority.service'; @@ -12,17 +13,21 @@ import { hasValue, isNotEmpty } from '../../../../../empty.util'; import { isEqual } from 'lodash'; import { GlobalConfig } from '../../../../../../../config/global-config.interface'; import { GLOBAL_CONFIG } from '../../../../../../../config'; +import { + DynamicFormControlComponent, + DynamicFormLayoutService, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; @Component({ selector: 'ds-dynamic-tag', styleUrls: ['./dynamic-tag.component.scss'], templateUrl: './dynamic-tag.component.html' }) -export class DsDynamicTagComponent implements OnInit { +export class DsDynamicTagComponent extends DynamicFormControlComponent implements OnInit { @Input() bindId = true; @Input() group: FormGroup; @Input() model: DynamicTagModel; - @Input() showErrorMessages = false; @Output() blur: EventEmitter = new EventEmitter(); @Output() change: EventEmitter = new EventEmitter(); @@ -40,41 +45,46 @@ export class DsDynamicTagComponent implements OnInit { formatter = (x: { display: string }) => x.display; search = (text$: Observable) => - text$ - .debounceTime(300) - .distinctUntilChanged() - .do(() => this.changeSearchingStatus(true)) - .switchMap((term) => { + text$.pipe( + debounceTime(300), + distinctUntilChanged(), + tap(() => this.changeSearchingStatus(true)), + switchMap((term) => { if (term === '' || term.length < this.model.minChars) { - return Observable.of({list: []}); + return observableOf({list: []}); } else { this.searchOptions.query = term; - return this.authorityService.getEntriesByName(this.searchOptions) - .map((authorities) => { + return this.authorityService.getEntriesByName(this.searchOptions).pipe( + map((authorities) => { // @TODO Pagination for authority is not working, to refactor when it will be fixed return { list: authorities.payload, pageInfo: authorities.pageInfo }; - }) - .do(() => this.searchFailed = false) - .catch(() => { + }), + tap(() => this.searchFailed = false), + catchError(() => { this.searchFailed = true; - return Observable.of({list: []}); - }); + return observableOf({list: []}); + }),); } - }) - .map((results) => results.list) - .do(() => this.changeSearchingStatus(false)) - .merge(this.hideSearchingWhenUnsubscribed); + }), + map((results) => results.list), + tap(() => this.changeSearchingStatus(false)), + merge(this.hideSearchingWhenUnsubscribed),); constructor(@Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, private authorityService: AuthorityService, - private cdr: ChangeDetectorRef) { + private cdr: ChangeDetectorRef, + protected layoutService: DynamicFormLayoutService, + protected validationService: DynamicFormValidationService + ) { + super(layoutService, validationService); } ngOnInit() { this.hasAuthority = this.model.authorityOptions && hasValue(this.model.authorityOptions.name); + if (this.hasAuthority) { this.searchOptions = new IntegrationSearchOptions( this.model.authorityOptions.scope, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.spec.ts index 2ed145b03a..c950f8f4ef 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.spec.ts @@ -3,12 +3,14 @@ import { ChangeDetectorRef, Component, CUSTOM_ELEMENTS_SCHEMA } from '@angular/c import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { async, ComponentFixture, fakeAsync, inject, TestBed, } from '@angular/core/testing'; import { By } from '@angular/platform-browser'; - -import { Observable } from 'rxjs/Observable'; -import 'rxjs/add/observable/of'; +import { of as observableOf } from 'rxjs'; import { AuthorityOptions } from '../../../../../../core/integration/models/authority-options.model'; -import { DynamicFormsCoreModule } from '@ng-dynamic-forms/core'; +import { + DynamicFormLayoutService, + DynamicFormsCoreModule, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; import { DynamicFormsNGBootstrapUIModule } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { AuthorityService } from '../../../../../../core/integration/authority.service'; import { AuthorityServiceStub } from '../../../../../testing/authority-service-stub'; @@ -20,29 +22,34 @@ import { DynamicTypeaheadModel } from './dynamic-typeahead.model'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; import { createTestComponent } from '../../../../../testing/utils'; -export const TYPEAHEAD_TEST_GROUP = new FormGroup({ - typeahead: new FormControl(), -}); +export let TYPEAHEAD_TEST_GROUP; -export const TYPEAHEAD_TEST_MODEL_CONFIG = { - authorityOptions: { - closed: false, - metadata: 'typeahead', - name: 'EVENTAuthority', - scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' - } as AuthorityOptions, - disabled: false, - id: 'typeahead', - label: 'Conference', - minChars: 3, - name: 'typeahead', - placeholder: 'Conference', - readOnly: false, - required: false, - repeatable: false, - value: undefined -}; +export let TYPEAHEAD_TEST_MODEL_CONFIG; +function init() { + TYPEAHEAD_TEST_GROUP = new FormGroup({ + typeahead: new FormControl(), + }); + + TYPEAHEAD_TEST_MODEL_CONFIG = { + authorityOptions: { + closed: false, + metadata: 'typeahead', + name: 'EVENTAuthority', + scope: 'c1c16450-d56f-41bc-bb81-27f1d1eb5c23' + } as AuthorityOptions, + disabled: false, + id: 'typeahead', + label: 'Conference', + minChars: 3, + name: 'typeahead', + placeholder: 'Conference', + readOnly: false, + required: false, + repeatable: false, + value: undefined + }; +} describe('DsDynamicTypeaheadComponent test suite', () => { let testComp: TestComponent; @@ -54,7 +61,7 @@ describe('DsDynamicTypeaheadComponent test suite', () => { // async beforeEach beforeEach(async(() => { const authorityServiceStub = new AuthorityServiceStub(); - + init() TestBed.configureTestingModule({ imports: [ DynamicFormsCoreModule, @@ -70,8 +77,9 @@ describe('DsDynamicTypeaheadComponent test suite', () => { providers: [ ChangeDetectorRef, DsDynamicTypeaheadComponent, - {provide: AuthorityService, useValue: authorityServiceStub}, - {provide: GLOBAL_CONFIG, useValue: {} as GlobalConfig}, + { provide: AuthorityService, useValue: authorityServiceStub }, + { provide: DynamicFormLayoutService, useValue: {} }, + { provide: DynamicFormValidationService, useValue: {} } ], schemas: [CUSTOM_ELEMENTS_SCHEMA] }); @@ -85,7 +93,6 @@ describe('DsDynamicTypeaheadComponent test suite', () => { `; @@ -94,6 +101,9 @@ describe('DsDynamicTypeaheadComponent test suite', () => { testComp = testFixture.componentInstance; }); + afterEach(() => { + testFixture.destroy(); + }); it('should create DsDynamicTypeaheadComponent', inject([DsDynamicTypeaheadComponent], (app: DsDynamicTypeaheadComponent) => { expect(app).toBeDefined(); @@ -123,7 +133,7 @@ describe('DsDynamicTypeaheadComponent test suite', () => { it('should search when 3+ characters typed', fakeAsync(() => { spyOn((typeaheadComp as any).authorityService, 'getEntriesByName').and.callThrough(); - typeaheadComp.search(Observable.of('test')).subscribe(() => { + typeaheadComp.search(observableOf('test')).subscribe(() => { expect((typeaheadComp as any).authorityService.getEntriesByName).toHaveBeenCalled(); }); @@ -219,6 +229,4 @@ class TestComponent { model = new DynamicTypeaheadModel(TYPEAHEAD_TEST_MODEL_CONFIG); - showErrorMessages = false; - } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts index dade5d037a..58f8030bcc 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/typeahead/dynamic-typeahead.component.ts @@ -1,7 +1,9 @@ + +import {of as observableOf, Observable } from 'rxjs'; + +import {distinctUntilChanged, switchMap, tap, filter, catchError, debounceTime, merge, map} from 'rxjs/operators'; import { ChangeDetectorRef, Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { FormGroup } from '@angular/forms'; - -import { Observable } from 'rxjs/Observable'; import { NgbTypeaheadSelectItemEvent } from '@ng-bootstrap/ng-bootstrap'; import { AuthorityService } from '../../../../../../core/integration/authority.service'; @@ -9,17 +11,21 @@ import { DynamicTypeaheadModel } from './dynamic-typeahead.model'; import { IntegrationSearchOptions } from '../../../../../../core/integration/models/integration-options.model'; import { isEmpty, isNotEmpty } from '../../../../../empty.util'; import { FormFieldMetadataValueObject } from '../../../models/form-field-metadata-value.model'; +import { + DynamicFormControlComponent, + DynamicFormLayoutService, + DynamicFormValidationService +} from '@ng-dynamic-forms/core'; @Component({ selector: 'ds-dynamic-typeahead', styleUrls: ['./dynamic-typeahead.component.scss'], templateUrl: './dynamic-typeahead.component.html' }) -export class DsDynamicTypeaheadComponent implements OnInit { +export class DsDynamicTypeaheadComponent extends DynamicFormControlComponent implements OnInit { @Input() bindId = true; @Input() group: FormGroup; @Input() model: DynamicTypeaheadModel; - @Input() showErrorMessages = false; @Output() blur: EventEmitter = new EventEmitter(); @Output() change: EventEmitter = new EventEmitter(); @@ -37,35 +43,40 @@ export class DsDynamicTypeaheadComponent implements OnInit { }; search = (text$: Observable) => - text$ - .debounceTime(300) - .distinctUntilChanged() - .do(() => this.changeSearchingStatus(true)) - .switchMap((term) => { + text$.pipe( + debounceTime(300), + distinctUntilChanged(), + tap(() => this.changeSearchingStatus(true)), + switchMap((term) => { if (term === '' || term.length < this.model.minChars) { - return Observable.of({list: []}); + return observableOf({list: []}); } else { this.searchOptions.query = term; - return this.authorityService.getEntriesByName(this.searchOptions) - .map((authorities) => { + return this.authorityService.getEntriesByName(this.searchOptions).pipe( + map((authorities) => { // @TODO Pagination for authority is not working, to refactor when it will be fixed return { list: authorities.payload, pageInfo: authorities.pageInfo }; - }) - .do(() => this.searchFailed = false) - .catch(() => { + }), + tap(() => this.searchFailed = false), + catchError(() => { this.searchFailed = true; - return Observable.of({list: []}); - }); + return observableOf({list: []}); + }),); } - }) - .map((results) => results.list) - .do(() => this.changeSearchingStatus(false)) - .merge(this.hideSearchingWhenUnsubscribed); + }), + map((results) => results.list), + tap(() => this.changeSearchingStatus(false)), + merge(this.hideSearchingWhenUnsubscribed),); - constructor(private authorityService: AuthorityService, private cdr: ChangeDetectorRef) { + constructor(private authorityService: AuthorityService, + private cdr: ChangeDetectorRef, + protected layoutService: DynamicFormLayoutService, + protected validationService: DynamicFormValidationService + ) { + super(layoutService, validationService); } ngOnInit() { @@ -74,8 +85,8 @@ export class DsDynamicTypeaheadComponent implements OnInit { this.model.authorityOptions.scope, this.model.authorityOptions.name, this.model.authorityOptions.metadata); - this.group.get(this.model.id).valueChanges - .filter((value) => this.currentValue !== value) + this.group.get(this.model.id).valueChanges.pipe( + filter((value) => this.currentValue !== value)) .subscribe((value) => { this.currentValue = value; }); diff --git a/src/app/shared/form/builder/form-builder.service.spec.ts b/src/app/shared/form/builder/form-builder.service.spec.ts index 12f51166b5..5266afabfd 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -13,13 +13,10 @@ import { DynamicColorPickerModel, DynamicDatePickerModel, DynamicEditorModel, - DynamicFileUploadModel, DynamicFormArrayGroupModel, + DynamicFileUploadModel, DynamicFormArrayModel, DynamicFormControlModel, - DynamicFormControlValue, - DynamicFormGroupModel, - DynamicFormService, - DynamicFormValidationService, + DynamicFormGroupModel, DynamicFormValidationService, DynamicFormValueControlModel, DynamicInputModel, DynamicRadioGroupModel, @@ -41,7 +38,10 @@ import { DynamicTypeaheadModel } from './ds-dynamic-form-ui/models/typeahead/dyn import { DynamicListRadioGroupModel } from './ds-dynamic-form-ui/models/list/dynamic-list-radio-group.model'; import { AuthorityOptions } from '../../../core/integration/models/authority-options.model'; import { FormFieldModel } from './models/form-field.model'; -import { FormRowModel, SubmissionFormsModel } from '../../../core/shared/config/config-submission-forms.model'; +import { + FormRowModel, + SubmissionFormsModel +} from '../../../core/shared/config/config-submission-forms.model'; import { FormBuilderService } from './form-builder.service'; import { DynamicRowGroupModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; import { DsDynamicInputModel } from './ds-dynamic-form-ui/models/ds-dynamic-input.model'; @@ -69,9 +69,8 @@ describe('FormBuilderService test suite', () => { TestBed.configureTestingModule({ imports: [ReactiveFormsModule], providers: [ - FormBuilderService, - DynamicFormService, - DynamicFormValidationService, + {provide: FormBuilderService, useClass: FormBuilderService}, + {provide: DynamicFormValidationService, useValue: {}}, {provide: NG_VALIDATORS, useValue: testValidator, multi: true}, {provide: NG_ASYNC_VALIDATORS, useValue: testAsyncValidator, multi: true} ] @@ -255,7 +254,7 @@ describe('FormBuilderService test suite', () => { { id: 'testFormRowArray', initialCount: 5, - notRepeteable: false, + notRepeatable: false, groupFactory: () => { return [ new DynamicInputModel({id: 'testFormRowArrayGroupInput'}) @@ -761,8 +760,8 @@ describe('FormBuilderService test suite', () => { (formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 1'); (formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 2'); - (model.get(index).get(0) as DynamicFormValueControlModel).valueUpdates.next('next test value 1'); - (model.get(index + step).get(0) as DynamicFormValueControlModel).valueUpdates.next('next test value 2'); + (model.get(index).get(0) as DynamicFormValueControlModel).valueUpdates.next('next test value 1'); + (model.get(index + step).get(0) as DynamicFormValueControlModel).valueUpdates.next('next test value 2'); service.moveFormArrayGroup(index, step, formArray, model); @@ -771,8 +770,8 @@ describe('FormBuilderService test suite', () => { expect((formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 2'); expect((formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 1'); - expect((model.get(index).get(0) as DynamicFormValueControlModel).value).toEqual('next test value 2'); - expect((model.get(index + step).get(0) as DynamicFormValueControlModel).value).toEqual('next test value 1'); + expect((model.get(index).get(0) as DynamicFormValueControlModel).value).toEqual('next test value 2'); + expect((model.get(index + step).get(0) as DynamicFormValueControlModel).value).toEqual('next test value 1'); }); it('should move down a form array group', () => { @@ -785,8 +784,8 @@ describe('FormBuilderService test suite', () => { (formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 1'); (formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.setValue('next test value 2'); - (model.get(index).get(0) as DynamicFormValueControlModel).valueUpdates.next('next test value 1'); - (model.get(index + step).get(0) as DynamicFormValueControlModel).valueUpdates.next('next test value 2'); + (model.get(index).get(0) as DynamicFormValueControlModel).valueUpdates.next('next test value 1'); + (model.get(index + step).get(0) as DynamicFormValueControlModel).valueUpdates.next('next test value 2'); service.moveFormArrayGroup(index, step, formArray, model); @@ -795,8 +794,8 @@ describe('FormBuilderService test suite', () => { expect((formArray.at(index) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 2'); expect((formArray.at(index + step) as FormGroup).controls.testFormArrayGroupInput.value).toEqual('next test value 1'); - expect((model.get(index).get(0) as DynamicFormValueControlModel).value).toEqual('next test value 2'); - expect((model.get(index + step).get(0) as DynamicFormValueControlModel).value).toEqual('next test value 1'); + expect((model.get(index).get(0) as DynamicFormValueControlModel).value).toEqual('next test value 2'); + expect((model.get(index + step).get(0) as DynamicFormValueControlModel).value).toEqual('next test value 1'); }); it('should throw when form array group is to be moved out of bounds', () => { diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index f37b3868f3..3286b3fdbb 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -35,7 +35,7 @@ export abstract class FieldParser { id: uniqueId() + '_array', label: this.configData.label, initialCount: this.getInitArrayIndex(), - notRepeteable: !this.configData.repeatable, + notRepeatable: !this.configData.repeatable, groupFactory: () => { let model; if ((arrayCounter === 0)) { diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 1b5f2ef72f..958c9a6c73 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -14,7 +14,7 @@ -