diff --git a/.travis.yml b/.travis.yml index 901dee8186..c42923886d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ sudo: required -dist: trusty +dist: bionic env: # Install the latest docker-compose version for ci testing. @@ -12,6 +12,9 @@ env: DSPACE_REST_NAMESPACE: '/server/api' DSPACE_REST_SSL: false +services: + - xvfb + before_install: # Docker Compose Install - curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose @@ -33,14 +36,6 @@ before_script: after_script: - docker-compose -f ./docker/docker-compose-travis.yml down -addons: - apt: - sources: - - google-chrome - packages: - - dpkg - - google-chrome-stable - language: node_js node_js: @@ -53,8 +48,6 @@ cache: bundler_args: --retry 5 script: - # Use Chromium instead of Chrome. - - export CHROME_BIN=chromium-browser - yarn run build - yarn run ci - cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js diff --git a/package.json b/package.json index aaabc0271a..b4a40c0a19 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "node": "8.* || >= 10.*" }, "resolutions": { + "serialize-javascript": ">= 2.1.2", "set-value": ">= 2.0.1" }, "scripts": { @@ -74,38 +75,38 @@ "sync-i18n": "node ./scripts/sync-i18n-files.js" }, "dependencies": { - "@angular/animations": "^6.1.4", - "@angular/cdk": "^6.4.7", - "@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", + "@angular/animations": "^7.2.15", + "@angular/cdk": "7.3.7", + "@angular/cli": "^7.3.5", + "@angular/common": "^7.2.15", + "@angular/core": "^7.2.15", + "@angular/forms": "^7.2.15", + "@angular/http": "^7.2.15", + "@angular/platform-browser": "^7.2.15", + "@angular/platform-browser-dynamic": "^7.2.15", + "@angular/platform-server": "^7.2.15", + "@angular/router": "^7.2.15", "@angularclass/bootloader": "1.0.1", - "@ng-bootstrap/ng-bootstrap": "^2.0.0", - "@ng-dynamic-forms/core": "6.2.0", - "@ng-dynamic-forms/ui-ng-bootstrap": "6.2.0", - "@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", + "@ng-bootstrap/ng-bootstrap": "^4.1.0", + "@ng-dynamic-forms/core": "^7.1.0", + "@ng-dynamic-forms/ui-ng-bootstrap": "^7.1.0", + "@ngrx/effects": "^7.3.0", + "@ngrx/router-store": "^7.3.0", + "@ngrx/store": "^7.3.0", + "@nguniversal/express-engine": "^7.1.1", + "@ngx-translate/core": "11.0.1", + "@ngx-translate/http-loader": "4.0.0", "@nicky-lenaers/ngx-scroll-to": "^1.0.0", "angular-idle-preload": "3.0.0", "angular-sortablejs": "^2.5.0", "angular2-text-mask": "9.0.0", - "angulartics2": "^6.2.0", + "angulartics2": "7.5.2", "body-parser": "1.18.2", "bootstrap": "4.3.1", "cerialize": "0.1.18", "compression": "1.7.1", "cookie-parser": "1.4.3", - "core-js": "^2.5.7", + "core-js": "^2.6.5", "debug-loader": "^0.0.1", "express": "4.16.2", "express-session": "1.15.6", @@ -113,6 +114,7 @@ "file-saver": "^1.3.8", "font-awesome": "4.7.0", "fork-ts-checker-webpack-plugin": "^0.4.10", + "hammerjs": "^2.0.8", "http-server": "0.11.1", "https": "1.0.0", "js-cookie": "2.2.0", @@ -122,18 +124,19 @@ "jwt-decode": "^2.2.0", "methods": "1.1.2", "moment": "^2.22.1", + "moment-range": "^4.0.2", "morgan": "^1.9.1", - "ng-mocks": "^6.2.1", + "ng-mocks": "^7.6.0", "ng2-file-upload": "1.2.1", - "ng2-nouislider": "^1.7.11", + "ng2-nouislider": "^1.8.2", "ngx-bootstrap": "^3.2.0", "ngx-infinite-scroll": "6.0.1", - "ngx-moment": "^3.1.0", + "ngx-moment": "^3.4.0", "ngx-pagination": "3.0.3", "nouislider": "^11.0.0", "pem": "1.13.2", "reflect-metadata": "0.1.12", - "rxjs": "6.2.2", + "rxjs": "6.4.0", "rxjs-spy": "^7.5.1", "sass-resources-loader": "^2.0.0", "sortablejs": "1.7.0", @@ -143,17 +146,18 @@ "url-parse": "^1.4.7", "uuid": "^3.2.1", "webfontloader": "1.6.28", - "webpack-cli": "^3.1.0", - "zone.js": "^0.8.26" + "webpack-cli": "^3.2.0", + "zone.js": "^0.8.29" }, "devDependencies": { - "@angular/compiler": "^6.1.4", - "@angular/compiler-cli": "^6.1.4", + "@angular-devkit/build-angular": "^0.13.5", + "@angular/compiler": "^7.2.15", + "@angular/compiler-cli": "^7.2.15", "@fortawesome/fontawesome-free": "^5.5.0", - "@ngrx/entity": "^6.1.0", - "@ngrx/schematics": "^6.1.0", - "@ngrx/store-devtools": "^6.1.0", - "@ngtools/webpack": "^6.1.5", + "@ngrx/entity": "^7.3.0", + "@ngrx/schematics": "^7.3.0", + "@ngrx/store-devtools": "^7.3.0", + "@ngtools/webpack": "^7.3.9", "@schematics/angular": "^0.7.5", "@types/acorn": "^4.0.3", "@types/cookie-parser": "1.4.1", @@ -162,44 +166,47 @@ "@types/express-serve-static-core": "4.16.0", "@types/file-saver": "^1.3.0", "@types/hammerjs": "2.0.35", - "@types/jasmine": "^2.8.6", + "@types/jasmine": "^3.3.9", "@types/js-cookie": "2.1.0", "@types/json5": "^0.0.30", "@types/lodash": "^4.14.110", "@types/memory-cache": "0.2.0", "@types/mime": "2.0.0", - "@types/node": "^10.9.4", + "@types/node": "^11.11.2", "@types/serve-static": "1.13.2", "@types/uuid": "^3.4.3", "@types/webfontloader": "1.6.29", + "@typescript-eslint/eslint-plugin": "^2.12.0", + "@typescript-eslint/parser": "^2.12.0", "ajv": "^6.1.1", "ajv-keywords": "^3.1.0", "angular2-template-loader": "0.6.2", "autoprefixer": "^9.1.3", "caniuse-lite": "^1.0.30000697", "cli-progress": "^3.3.1", - "codelyzer": "^4.4.4", + "codelyzer": "^5.1.0", "commander": "^3.0.2", - "compression-webpack-plugin": "^1.1.6", - "copy-webpack-plugin": "^4.4.1", + "compression-webpack-plugin": "^3.0.1", + "copy-webpack-plugin": "^5.1.1", "copyfiles": "^2.1.1", "coveralls": "3.0.0", - "css-loader": "1.0.0", + "css-loader": "3.4.0", "cssnano": "^4.1.10", "deep-freeze": "0.0.1", + "eslint": "^6.7.2", "exports-loader": "^0.7.0", - "html-webpack-plugin": "^4.0.0-alpha", + "html-webpack-plugin": "3.2.0", "imports-loader": "0.8.0", "istanbul-instrumenter-loader": "3.0.1", - "jasmine-core": "^3.2.1", + "jasmine-core": "^3.3.0", "jasmine-marbles": "0.3.1", "jasmine-spec-reporter": "4.2.1", - "karma": "3.0.0", + "karma": "4.0.1", "karma-chrome-launcher": "2.2.0", - "karma-cli": "1.0.1", + "karma-cli": "2.0.0", "karma-coverage": "1.1.2", "karma-istanbul-preprocessor": "0.0.2", - "karma-jasmine": "1.1.2", + "karma-jasmine": "2.0.1", "karma-mocha-reporter": "2.2.5", "karma-phantomjs-launcher": "1.0.4", "karma-remap-coverage": "^0.1.5", @@ -223,26 +230,26 @@ "protractor": "^5.4.2", "protractor-istanbul-plugin": "2.0.0", "raw-loader": "0.5.1", - "resolve-url-loader": "^2.3.0", "rimraf": "2.6.2", "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-terser": "^2.0.2", - "sass-loader": "^7.1.0", - "script-ext-html-webpack-plugin": "2.0.1", + "sass-loader": "7.3.1", + "script-ext-html-webpack-plugin": "2.1.4", "source-map": "0.7.3", "source-map-loader": "0.2.4", "string-replace-loader": "^2.1.1", + "terser-webpack-plugin": "^2.3.1", "to-string-loader": "1.1.5", "ts-helpers": "1.1.2", "ts-node": "4.1.0", "tslint": "5.11.0", "typedoc": "^0.9.0", - "typescript": "^2.9.1", + "typescript": "3.1.6", "webdriver-manager": "^12.1.7", - "webpack": "^4.17.1", + "webpack": "^4.29.6", "webpack-bundle-analyzer": "^3.3.2", "webpack-dev-middleware": "3.2.0", "webpack-dev-server": "^3.1.11", diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 299c2afe63..478f183da4 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -244,6 +244,8 @@ "collection.create.head": "Create a Collection", + "collection.create.notifications.success": "Successfully created the Collection", + "collection.create.sub-head": "Create a Collection for Community {{ parent }}", "collection.delete.cancel": "Cancel", @@ -302,6 +304,46 @@ + "collection.edit.logo.label": "Collection logo", + + "collection.edit.logo.notifications.add.error": "Uploading Collection logo failed. Please verify the content before retrying.", + + "collection.edit.logo.notifications.add.success": "Upload Collection logo successful.", + + "collection.edit.logo.notifications.delete.success.title": "Logo deleted", + + "collection.edit.logo.notifications.delete.success.content": "Successfully deleted the collection's logo", + + "collection.edit.logo.notifications.delete.error.title": "Error deleting logo", + + "collection.edit.logo.upload": "Drop a Collection Logo to upload", + + + + "collection.edit.notifications.success": "Successfully edited the Collection", + + "collection.edit.return": "Return", + + + + "collection.edit.tabs.curate.head": "Curate", + + "collection.edit.tabs.curate.title": "Collection Edit - Curate", + + "collection.edit.tabs.metadata.head": "Edit Metadata", + + "collection.edit.tabs.metadata.title": "Collection Edit - Metadata", + + "collection.edit.tabs.roles.head": "Assign Roles", + + "collection.edit.tabs.roles.title": "Collection Edit - Roles", + + "collection.edit.tabs.source.head": "Content Source", + + "collection.edit.tabs.source.title": "Collection Edit - Content Source", + + + "collection.form.abstract": "Short Description", "collection.form.description": "Introductory text (HTML)", @@ -350,6 +392,8 @@ "community.create.head": "Create a Community", + "community.create.notifications.success": "Successfully created the Community", + "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", "community.delete.cancel": "Cancel", @@ -368,6 +412,44 @@ "community.edit.head": "Edit Community", + + + "community.edit.logo.label": "Community logo", + + "community.edit.logo.notifications.add.error": "Uploading Community logo failed. Please verify the content before retrying.", + + "community.edit.logo.notifications.add.success": "Upload Community logo successful.", + + "community.edit.logo.notifications.delete.success.title": "Logo deleted", + + "community.edit.logo.notifications.delete.success.content": "Successfully deleted the community's logo", + + "community.edit.logo.notifications.delete.error.title": "Error deleting logo", + + "community.edit.logo.upload": "Drop a Community Logo to upload", + + + + "community.edit.notifications.success": "Successfully edited the Community", + + "community.edit.return": "Return", + + + + "community.edit.tabs.curate.head": "Curate", + + "community.edit.tabs.curate.title": "Community Edit - Curate", + + "community.edit.tabs.metadata.head": "Edit Metadata", + + "community.edit.tabs.metadata.title": "Community Edit - Metadata", + + "community.edit.tabs.roles.head": "Assign Roles", + + "community.edit.tabs.roles.title": "Community Edit - Roles", + + + "community.form.abstract": "Short Description", "community.form.description": "Introductory text (HTML)", @@ -1484,6 +1566,8 @@ "search.results.no-results-link": "quotes around it", + "search.results.empty": "Your search returned no results.", + "search.sidebar.close": "Back to results", @@ -1557,13 +1641,21 @@ "submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Search for Authors", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Local Authors ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Search for Journals", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Local Journals ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Search for Journal Issues", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Local Journal Issues ({{ count }})", - "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Search for Journal Volumes", + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Local Journal Volumes ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaJournal": "Sherpa Journals ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.sherpaPublisher": "Sherpa Publishers ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.orcidV2": "ORCID ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.lcname": "LC Names ({{ count }})", "submission.sections.describe.relationship-lookup.search-tab.tab-title.Funding Agency": "Search for Funding Agencies", @@ -1597,6 +1689,14 @@ "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Issue": "Selected Issue", + "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results", + "submission.sections.describe.relationship-lookup.name-variant.notification.content": "Would you like to save \"{{ value }}\" as a name variant for this person so you and others can reuse it for future submissions? If you don\'t you can still use it for this submission.", "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant", @@ -1771,7 +1871,7 @@ "uploader.drag-message": "Drag & Drop your files here", - "uploader.or": ", or", + "uploader.or": ", or ", "uploader.processing": "Processing", 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 4a5e301921..674ae739d8 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,7 +1,6 @@ import { MetadataRegistryComponent } from './metadata-registry.component'; import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; 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'; import { By } from '@angular/platform-browser'; @@ -18,6 +17,7 @@ import { NotificationsService } from '../../../shared/notifications/notification import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; import { RestResponse } from '../../../core/cache/response.models'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; +import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; describe('MetadataRegistryComponent', () => { let comp: MetadataRegistryComponent; @@ -101,12 +101,12 @@ describe('MetadataRegistryComponent', () => { it('should start editing the selected schema', async(() => { fixture.whenStable().then(() => { - expect(registryService.editMetadataSchema).toHaveBeenCalledWith(mockSchemasList[0]); + expect(registryService.editMetadataSchema).toHaveBeenCalledWith(mockSchemasList[0] as MetadataSchema); }); })); it('should cancel editing the selected schema when clicked again', async(() => { - spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0])); + spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0] as MetadataSchema)); spyOn(registryService, 'cancelEditMetadataSchema'); row.click(); fixture.detectChanges(); @@ -121,7 +121,7 @@ describe('MetadataRegistryComponent', () => { beforeEach(() => { spyOn(registryService, 'deleteMetadataSchema').and.callThrough(); - spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas)); + spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas as MetadataSchema[])); comp.deleteSchemas(); fixture.detectChanges(); }); 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 e23a9691c4..e0b0ef25a5 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,7 +1,6 @@ import { MetadataSchemaComponent } from './metadata-schema.component'; import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; 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'; import { CommonModule } from '@angular/common'; @@ -22,6 +21,7 @@ import { NotificationsServiceStub } from '../../../shared/testing/notifications- import { RestResponse } from '../../../core/cache/response.models'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; +import { MetadataField } from '../../../core/metadata/metadata-field.model'; describe('MetadataSchemaComponent', () => { let comp: MetadataSchemaComponent; @@ -152,12 +152,12 @@ describe('MetadataSchemaComponent', () => { it('should start editing the selected field', async(() => { fixture.whenStable().then(() => { - expect(registryService.editMetadataField).toHaveBeenCalledWith(mockFieldsList[2]); + expect(registryService.editMetadataField).toHaveBeenCalledWith(mockFieldsList[2] as MetadataField); }); })); it('should cancel editing the selected field when clicked again', async(() => { - spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2])); + spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2] as MetadataField)); spyOn(registryService, 'cancelEditMetadataField'); row.click(); fixture.detectChanges(); @@ -172,7 +172,7 @@ describe('MetadataSchemaComponent', () => { beforeEach(() => { spyOn(registryService, 'deleteMetadataField').and.callThrough(); - spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields)); + spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields as MetadataField[])); comp.deleteFields(); fixture.detectChanges(); }); diff --git a/src/app/+collection-page/collection-form/collection-form.component.ts b/src/app/+collection-page/collection-form/collection-form.component.ts index 21b494f41f..59433e49a0 100644 --- a/src/app/+collection-page/collection-form/collection-form.component.ts +++ b/src/app/+collection-page/collection-form/collection-form.component.ts @@ -1,9 +1,19 @@ import { Component, Input } from '@angular/core'; -import { DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; -import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; +import { + DynamicFormControlModel, + DynamicFormService, + DynamicInputModel, + DynamicTextAreaModel +} from '@ng-dynamic-forms/core'; import { Collection } from '../../core/shared/collection.model'; import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component'; -import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model'; +import { Location } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { AuthService } from '../../core/auth/auth.service'; +import { RequestService } from '../../core/data/request.service'; +import { ObjectCacheService } from '../../core/cache/object-cache.service'; /** * Form used for creating and editing collections @@ -22,7 +32,7 @@ export class CollectionFormComponent extends ComColFormComponent { /** * @type {Collection.type} This is a collection-type form */ - protected type = Collection.type; + type = Collection.type; /** * The dynamic form fields used for creating/editing a collection @@ -65,4 +75,15 @@ export class CollectionFormComponent extends ComColFormComponent { name: 'dc.description.provenance', }), ]; + + public constructor(protected location: Location, + protected formService: DynamicFormService, + protected translate: TranslateService, + protected notificationsService: NotificationsService, + protected authService: AuthService, + protected dsoService: CommunityDataService, + protected requestService: RequestService, + protected objectCache: ObjectCacheService) { + super(location, formService, translate, notificationsService, authService, requestService, objectCache); + } } diff --git a/src/app/+collection-page/collection-page-routing.module.ts b/src/app/+collection-page/collection-page-routing.module.ts index 66c623657d..2df7997e1e 100644 --- a/src/app/+collection-page/collection-page-routing.module.ts +++ b/src/app/+collection-page/collection-page-routing.module.ts @@ -5,7 +5,6 @@ import { CollectionPageComponent } from './collection-page.component'; import { CollectionPageResolver } from './collection-page.resolver'; import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component'; import { CreateCollectionPageGuard } from './create-collection-page/create-collection-page.guard'; import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component'; import { URLCombiner } from '../core/url-combiner/url-combiner'; @@ -39,12 +38,8 @@ const COLLECTION_EDIT_PATH = ':id/edit'; }, { path: COLLECTION_EDIT_PATH, - pathMatch: 'full', - component: EditCollectionPageComponent, - canActivate: [AuthenticatedGuard], - resolve: { - dso: CollectionPageResolver - } + loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule', + canActivate: [AuthenticatedGuard] }, { path: ':id/delete', diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index 12d5c200fd..98552ed40b 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -5,15 +5,17 @@
- - - [alternateText]="'Collection Logo'"> - - + + + + + {{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}
- + diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts index e223b11c65..869a89d5e0 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts @@ -10,6 +10,8 @@ import { CollectionDataService } from '../../core/data/collection-data.service'; import { of as observableOf } from 'rxjs'; import { CommunityDataService } from '../../core/data/community-data.service'; import { CreateCollectionPageComponent } from './create-collection-page.component'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; describe('CreateCollectionPageComponent', () => { let comp: CreateCollectionPageComponent; @@ -27,6 +29,7 @@ describe('CreateCollectionPageComponent', () => { }, { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, { provide: Router, useValue: {} }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 2cab36d285..ae31b94c3d 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -5,6 +5,8 @@ import { Router } from '@angular/router'; import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component'; import { Collection } from '../../core/shared/collection.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; /** * Component that represents the page where a user can create a new Collection @@ -16,13 +18,16 @@ import { CollectionDataService } from '../../core/data/collection-data.service'; }) export class CreateCollectionPageComponent extends CreateComColPageComponent { protected frontendURL = '/collections/'; + protected type = Collection.type; public constructor( protected communityDataService: CommunityDataService, protected collectionDataService: CollectionDataService, protected routeService: RouteService, - protected router: Router + protected router: Router, + protected notificationsService: NotificationsService, + protected translate: TranslateService ) { - super(collectionDataService, communityDataService, routeService, router); + super(collectionDataService, communityDataService, routeService, router, notificationsService, translate); } } diff --git a/src/app/community-list-page/community-list-adapter.ts b/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.html similarity index 100% rename from src/app/community-list-page/community-list-adapter.ts rename to src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.html diff --git a/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.ts b/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.ts new file mode 100644 index 0000000000..d7deaea982 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/collection-curate/collection-curate.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +/** + * Component for managing a collection's curation tasks + */ +@Component({ + selector: 'ds-collection-curate', + templateUrl: './collection-curate.component.html', +}) +export class CollectionCurateComponent { + /* TODO: Implement Collection Edit - Curate */ +} diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html new file mode 100644 index 0000000000..6f3a63790d --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html @@ -0,0 +1,6 @@ + +{{'collection.edit.delete' + | translate}} diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts new file mode 100644 index 0000000000..71cb06394f --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts @@ -0,0 +1,42 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { SharedModule } from '../../../shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { ActivatedRoute } from '@angular/router'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CollectionMetadataComponent } from './collection-metadata.component'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; + +describe('CollectionMetadataComponent', () => { + let comp: CollectionMetadataComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [CollectionMetadataComponent], + providers: [ + { provide: CollectionDataService, useValue: {} }, + { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CollectionMetadataComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('frontendURL', () => { + it('should have the right frontendURL set', () => { + expect((comp as any).frontendURL).toEqual('/collections/'); + }) + }); +}); diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts new file mode 100644 index 0000000000..af2ab7d0a7 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { ComcolMetadataComponent } from '../../../shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; +import { Collection } from '../../../core/shared/collection.model'; +import { CollectionDataService } from '../../../core/data/collection-data.service'; +import { ActivatedRoute, Router } from '@angular/router'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; + +/** + * Component for editing a collection's metadata + */ +@Component({ + selector: 'ds-collection-metadata', + templateUrl: './collection-metadata.component.html', +}) +export class CollectionMetadataComponent extends ComcolMetadataComponent { + protected frontendURL = '/collections/'; + protected type = Collection.type; + + public constructor( + protected collectionDataService: CollectionDataService, + protected router: Router, + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected translate: TranslateService + ) { + super(collectionDataService, router, route, notificationsService, translate); + } +} diff --git a/src/app/shared/mocks/mock-store.ts b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.html similarity index 100% rename from src/app/shared/mocks/mock-store.ts rename to src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.html diff --git a/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts new file mode 100644 index 0000000000..39f72fd2ce --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/collection-roles/collection-roles.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +/** + * Component for managing a collection's roles + */ +@Component({ + selector: 'ds-collection-roles', + templateUrl: './collection-roles.component.html', +}) +export class CollectionRolesComponent { + /* TODO: Implement Collection Edit - Roles */ +} diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts new file mode 100644 index 0000000000..6ec5be884d --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/collection-source/collection-source.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +/** + * Component for managing the content source of the collection + */ +@Component({ + selector: 'ds-collection-source', + templateUrl: './collection-source.component.html', +}) +export class CollectionSourceComponent { + /* TODO: Implement Collection Edit - Content Source */ +} diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html deleted file mode 100644 index c389c681ce..0000000000 --- a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.html +++ /dev/null @@ -1,11 +0,0 @@ -
-
-
- - - {{'collection.edit.delete' - | translate}} -
-
-
diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.scss b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.scss deleted file mode 100644 index 8b13789179..0000000000 --- a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.scss +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.spec.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.spec.ts index 193cb293e4..9f915d2d7a 100644 --- a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.spec.ts +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.spec.ts @@ -13,13 +13,29 @@ describe('EditCollectionPageComponent', () => { let comp: EditCollectionPageComponent; let fixture: ComponentFixture; + const routeStub = { + data: observableOf({ + dso: { payload: {} } + }), + routeConfig: { + children: [] + }, + snapshot: { + firstChild: { + routeConfig: { + path: 'mockUrl' + } + } + } + }; + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], declarations: [EditCollectionPageComponent], providers: [ { provide: CollectionDataService, useValue: {} }, - { provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } }, + { provide: ActivatedRoute, useValue: routeStub }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -31,9 +47,9 @@ describe('EditCollectionPageComponent', () => { fixture.detectChanges(); }); - describe('frontendURL', () => { - it('should have the right frontendURL set', () => { - expect((comp as any).frontendURL).toEqual('/collections/'); + describe('type', () => { + it('should have the right type set', () => { + expect((comp as any).type).toEqual('collection'); }) }); }); diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts index ba70bd26c6..209ce5149a 100644 --- a/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.component.ts @@ -2,24 +2,30 @@ import { Component } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component'; import { Collection } from '../../core/shared/collection.model'; -import { CollectionDataService } from '../../core/data/collection-data.service'; +import { getCollectionPageRoute } from '../collection-page-routing.module'; /** * Component that represents the page where a user can edit an existing Collection */ @Component({ selector: 'ds-edit-collection', - styleUrls: ['./edit-collection-page.component.scss'], - templateUrl: './edit-collection-page.component.html' + templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html' }) export class EditCollectionPageComponent extends EditComColPageComponent { - protected frontendURL = '/collections/'; + type = 'collection'; public constructor( - protected collectionDataService: CollectionDataService, protected router: Router, protected route: ActivatedRoute ) { - super(collectionDataService, router, route); + super(router, route); + } + + /** + * Get the collection page url + * @param collection The collection for which the url is requested + */ + getPageUrl(collection: Collection): string { + return getCollectionPageRoute(collection.id) } } diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.module.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.module.ts new file mode 100644 index 0000000000..f442aae4d6 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.module.ts @@ -0,0 +1,32 @@ +import { NgModule } from '@angular/core'; +import { EditCollectionPageComponent } from './edit-collection-page.component'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../../shared/shared.module'; +import { EditCollectionPageRoutingModule } from './edit-collection-page.routing.module'; +import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component'; +import { CollectionPageModule } from '../collection-page.module'; +import { CollectionRolesComponent } from './collection-roles/collection-roles.component'; +import { CollectionCurateComponent } from './collection-curate/collection-curate.component'; +import { CollectionSourceComponent } from './collection-source/collection-source.component'; + +/** + * Module that contains all components related to the Edit Collection page administrator functionality + */ +@NgModule({ + imports: [ + CommonModule, + SharedModule, + EditCollectionPageRoutingModule, + CollectionPageModule + ], + declarations: [ + EditCollectionPageComponent, + CollectionMetadataComponent, + CollectionRolesComponent, + CollectionCurateComponent, + CollectionSourceComponent + ] +}) +export class EditCollectionPageModule { + +} diff --git a/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts b/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts new file mode 100644 index 0000000000..fcfced9d81 --- /dev/null +++ b/src/app/+collection-page/edit-collection-page/edit-collection-page.routing.module.ts @@ -0,0 +1,61 @@ +import { RouterModule } from '@angular/router'; +import { NgModule } from '@angular/core'; +import { EditCollectionPageComponent } from './edit-collection-page.component'; +import { CollectionPageResolver } from '../collection-page.resolver'; +import { CollectionMetadataComponent } from './collection-metadata/collection-metadata.component'; +import { CollectionRolesComponent } from './collection-roles/collection-roles.component'; +import { CollectionSourceComponent } from './collection-source/collection-source.component'; +import { CollectionCurateComponent } from './collection-curate/collection-curate.component'; + +/** + * Routing module that handles the routing for the Edit Collection page administrator functionality + */ +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + component: EditCollectionPageComponent, + resolve: { + dso: CollectionPageResolver + }, + children: [ + { + path: '', + redirectTo: 'metadata', + pathMatch: 'full' + }, + { + path: 'metadata', + component: CollectionMetadataComponent, + data: { + title: 'collection.edit.tabs.metadata.title', + hideReturnButton: true + } + }, + { + path: 'roles', + component: CollectionRolesComponent, + data: { title: 'collection.edit.tabs.roles.title' } + }, + { + path: 'source', + component: CollectionSourceComponent, + data: { title: 'collection.edit.tabs.source.title' } + }, + { + path: 'curate', + component: CollectionCurateComponent, + data: { title: 'collection.edit.tabs.curate.title' } + } + ] + } + ]) + ], + providers: [ + CollectionPageResolver, + ] +}) +export class EditCollectionPageRoutingModule { + +} diff --git a/src/app/+community-page/community-form/community-form.component.ts b/src/app/+community-page/community-form/community-form.component.ts index 17d601e251..e9bd2f66c8 100644 --- a/src/app/+community-page/community-form/community-form.component.ts +++ b/src/app/+community-page/community-form/community-form.component.ts @@ -1,9 +1,19 @@ import { Component, Input } from '@angular/core'; -import { DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; -import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; +import { + DynamicFormControlModel, + DynamicFormService, + DynamicInputModel, + DynamicTextAreaModel +} from '@ng-dynamic-forms/core'; import { Community } from '../../core/shared/community.model'; -import { ResourceType } from '../../core/shared/resource-type'; import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component'; +import { Location } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { AuthService } from '../../core/auth/auth.service'; +import { RequestService } from '../../core/data/request.service'; +import { ObjectCacheService } from '../../core/cache/object-cache.service'; /** * Form used for creating and editing communities @@ -22,7 +32,7 @@ export class CommunityFormComponent extends ComColFormComponent { /** * @type {Community.type} This is a community-type form */ - protected type = Community.type; + type = Community.type; /** * The dynamic form fields used for creating/editing a community @@ -57,4 +67,15 @@ export class CommunityFormComponent extends ComColFormComponent { name: 'dc.description.tableofcontents', }), ]; + + public constructor(protected location: Location, + protected formService: DynamicFormService, + protected translate: TranslateService, + protected notificationsService: NotificationsService, + protected authService: AuthService, + protected dsoService: CommunityDataService, + protected requestService: RequestService, + protected objectCache: ObjectCacheService) { + super(location, formService, translate, notificationsService, authService, requestService, objectCache); + } } diff --git a/src/app/+community-page/community-page-routing.module.ts b/src/app/+community-page/community-page-routing.module.ts index cecd17ec10..df548e0617 100644 --- a/src/app/+community-page/community-page-routing.module.ts +++ b/src/app/+community-page/community-page-routing.module.ts @@ -5,7 +5,6 @@ import { CommunityPageComponent } from './community-page.component'; import { CommunityPageResolver } from './community-page.resolver'; import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; -import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component'; import { CreateCommunityPageGuard } from './create-community-page/create-community-page.guard'; import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; import { URLCombiner } from '../core/url-combiner/url-combiner'; @@ -38,12 +37,8 @@ const COMMUNITY_EDIT_PATH = ':id/edit'; }, { path: COMMUNITY_EDIT_PATH, - pathMatch: 'full', - component: EditCommunityPageComponent, - canActivate: [AuthenticatedGuard], - resolve: { - dso: CommunityPageResolver - } + loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule', + canActivate: [AuthenticatedGuard] }, { path: ':id/delete', diff --git a/src/app/+community-page/community-page.component.html b/src/app/+community-page/community-page.component.html index 5bd7089e82..dfd1ce93d9 100644 --- a/src/app/+community-page/community-page.component.html +++ b/src/app/+community-page/community-page.component.html @@ -3,12 +3,11 @@
+ + - - - diff --git a/src/app/+community-page/community-page.module.ts b/src/app/+community-page/community-page.module.ts index 8b02471fc2..1228783c3b 100644 --- a/src/app/+community-page/community-page.module.ts +++ b/src/app/+community-page/community-page.module.ts @@ -9,7 +9,6 @@ import { CommunityPageRoutingModule } from './community-page-routing.module'; import { CommunityPageSubCommunityListComponent } from './sub-community-list/community-page-sub-community-list.component'; import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; import { CommunityFormComponent } from './community-form/community-form.component'; -import { EditCommunityPageComponent } from './edit-community-page/edit-community-page.component'; import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; import { StatisticsModule } from '../statistics/statistics.module'; @@ -25,9 +24,11 @@ import { StatisticsModule } from '../statistics/statistics.module'; CommunityPageSubCollectionListComponent, CommunityPageSubCommunityListComponent, CreateCommunityPageComponent, - EditCommunityPageComponent, DeleteCommunityPageComponent, CommunityFormComponent + ], + exports: [ + CommunityFormComponent ] }) diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html index 55a080d2a1..4f75771f6d 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.html +++ b/src/app/+community-page/create-community-page/create-community-page.component.html @@ -7,5 +7,5 @@
- + diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts index dead5a5c3b..d0de8ec71c 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts @@ -10,6 +10,8 @@ import { CollectionDataService } from '../../core/data/collection-data.service'; import { of as observableOf } from 'rxjs'; import { CommunityDataService } from '../../core/data/community-data.service'; import { CreateCommunityPageComponent } from './create-community-page.component'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; describe('CreateCommunityPageComponent', () => { let comp: CreateCommunityPageComponent; @@ -23,6 +25,7 @@ describe('CreateCommunityPageComponent', () => { { provide: CommunityDataService, useValue: { findById: () => observableOf({}) } }, { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, { provide: Router, useValue: {} }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index fd5f18442a..30a2acbb0d 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -4,6 +4,8 @@ import { CommunityDataService } from '../../core/data/community-data.service'; import { RouteService } from '../../core/services/route.service'; import { Router } from '@angular/router'; import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; /** * Component that represents the page where a user can create a new Community @@ -15,12 +17,15 @@ import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comc }) export class CreateCommunityPageComponent extends CreateComColPageComponent { protected frontendURL = '/communities/'; + protected type = Community.type; public constructor( protected communityDataService: CommunityDataService, protected routeService: RouteService, - protected router: Router + protected router: Router, + protected notificationsService: NotificationsService, + protected translate: TranslateService ) { - super(communityDataService, communityDataService, routeService, router); + super(communityDataService, communityDataService, routeService, router, notificationsService, translate); } } diff --git a/src/app/+community-page/edit-community-page/community-curate/community-curate.component.html b/src/app/+community-page/edit-community-page/community-curate/community-curate.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+community-page/edit-community-page/community-curate/community-curate.component.ts b/src/app/+community-page/edit-community-page/community-curate/community-curate.component.ts new file mode 100644 index 0000000000..6151d3fe9a --- /dev/null +++ b/src/app/+community-page/edit-community-page/community-curate/community-curate.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +/** + * Component for managing a community's curation tasks + */ +@Component({ + selector: 'ds-community-curate', + templateUrl: './community-curate.component.html', +}) +export class CommunityCurateComponent { + /* TODO: Implement Community Edit - Curate */ +} diff --git a/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.html b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.html new file mode 100644 index 0000000000..6b441dbabd --- /dev/null +++ b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.html @@ -0,0 +1,6 @@ + +{{'community.edit.delete' + | translate}} diff --git a/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts new file mode 100644 index 0000000000..abeafb4e23 --- /dev/null +++ b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts @@ -0,0 +1,42 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { SharedModule } from '../../../shared/shared.module'; +import { CommonModule } from '@angular/common'; +import { RouterTestingModule } from '@angular/router/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of as observableOf } from 'rxjs/internal/observable/of'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { CommunityMetadataComponent } from './community-metadata.component'; +import { CommunityDataService } from '../../../core/data/community-data.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; + +describe('CommunityMetadataComponent', () => { + let comp: CommunityMetadataComponent; + let fixture: ComponentFixture; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], + declarations: [CommunityMetadataComponent], + providers: [ + { provide: CommunityDataService, useValue: {} }, + { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CommunityMetadataComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + }); + + describe('frontendURL', () => { + it('should have the right frontendURL set', () => { + expect((comp as any).frontendURL).toEqual('/communities/'); + }) + }); +}); diff --git a/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.ts b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.ts new file mode 100644 index 0000000000..c4bb88289f --- /dev/null +++ b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.ts @@ -0,0 +1,29 @@ +import { Component } from '@angular/core'; +import { ComcolMetadataComponent } from '../../../shared/comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; +import { ActivatedRoute, Router } from '@angular/router'; +import { Community } from '../../../core/shared/community.model'; +import { CommunityDataService } from '../../../core/data/community-data.service'; +import { NotificationsService } from '../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; + +/** + * Component for editing a community's metadata + */ +@Component({ + selector: 'ds-community-metadata', + templateUrl: './community-metadata.component.html', +}) +export class CommunityMetadataComponent extends ComcolMetadataComponent { + protected frontendURL = '/communities/'; + protected type = Community.type; + + public constructor( + protected communityDataService: CommunityDataService, + protected router: Router, + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected translate: TranslateService + ) { + super(communityDataService, router, route, notificationsService, translate); + } +} diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.html new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts new file mode 100644 index 0000000000..afa1fe14d1 --- /dev/null +++ b/src/app/+community-page/edit-community-page/community-roles/community-roles.component.ts @@ -0,0 +1,12 @@ +import { Component } from '@angular/core'; + +/** + * Component for managing a community's roles + */ +@Component({ + selector: 'ds-community-roles', + templateUrl: './community-roles.component.html', +}) +export class CommunityRolesComponent { + /* TODO: Implement Community Edit - Roles */ +} diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.html b/src/app/+community-page/edit-community-page/edit-community-page.component.html deleted file mode 100644 index cedb771c14..0000000000 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.html +++ /dev/null @@ -1,12 +0,0 @@ -
-
-
- - - {{'community.edit.delete' - | translate}} -
-
-
diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.scss b/src/app/+community-page/edit-community-page/edit-community-page.component.scss deleted file mode 100644 index 8b13789179..0000000000 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.scss +++ /dev/null @@ -1 +0,0 @@ - diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts b/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts index 54f2133ce7..b61924dd00 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.spec.ts @@ -13,13 +13,29 @@ describe('EditCommunityPageComponent', () => { let comp: EditCommunityPageComponent; let fixture: ComponentFixture; + const routeStub = { + data: observableOf({ + dso: { payload: {} } + }), + routeConfig: { + children: [] + }, + snapshot: { + firstChild: { + routeConfig: { + path: 'mockUrl' + } + } + } + }; + beforeEach(async(() => { TestBed.configureTestingModule({ imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], declarations: [EditCommunityPageComponent], providers: [ { provide: CommunityDataService, useValue: {} }, - { provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } }, + { provide: ActivatedRoute, useValue: routeStub }, ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); @@ -31,9 +47,9 @@ describe('EditCommunityPageComponent', () => { fixture.detectChanges(); }); - describe('frontendURL', () => { - it('should have the right frontendURL set', () => { - expect((comp as any).frontendURL).toEqual('/communities/'); + describe('type', () => { + it('should have the right type set', () => { + expect((comp as any).type).toEqual('community'); }) }); }); diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.ts b/src/app/+community-page/edit-community-page/edit-community-page.component.ts index 9f49ac49dd..c0adfe0ff1 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.ts +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.ts @@ -1,25 +1,31 @@ import { Component } from '@angular/core'; import { Community } from '../../core/shared/community.model'; -import { CommunityDataService } from '../../core/data/community-data.service'; import { ActivatedRoute, Router } from '@angular/router'; import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component'; +import { getCommunityPageRoute } from '../community-page-routing.module'; /** * Component that represents the page where a user can edit an existing Community */ @Component({ selector: 'ds-edit-community', - styleUrls: ['./edit-community-page.component.scss'], - templateUrl: './edit-community-page.component.html' + templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html' }) export class EditCommunityPageComponent extends EditComColPageComponent { - protected frontendURL = '/communities/'; + type = 'community'; public constructor( - protected communityDataService: CommunityDataService, protected router: Router, protected route: ActivatedRoute ) { - super(communityDataService, router, route); + super(router, route); + } + + /** + * Get the community page url + * @param community The community for which the url is requested + */ + getPageUrl(community: Community): string { + return getCommunityPageRoute(community.id) } } diff --git a/src/app/+community-page/edit-community-page/edit-community-page.module.ts b/src/app/+community-page/edit-community-page/edit-community-page.module.ts new file mode 100644 index 0000000000..f9a1e11a14 --- /dev/null +++ b/src/app/+community-page/edit-community-page/edit-community-page.module.ts @@ -0,0 +1,30 @@ +import { NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { SharedModule } from '../../shared/shared.module'; +import { EditCommunityPageRoutingModule } from './edit-community-page.routing.module'; +import { CommunityPageModule } from '../community-page.module'; +import { EditCommunityPageComponent } from './edit-community-page.component'; +import { CommunityCurateComponent } from './community-curate/community-curate.component'; +import { CommunityMetadataComponent } from './community-metadata/community-metadata.component'; +import { CommunityRolesComponent } from './community-roles/community-roles.component'; + +/** + * Module that contains all components related to the Edit Community page administrator functionality + */ +@NgModule({ + imports: [ + CommonModule, + SharedModule, + EditCommunityPageRoutingModule, + CommunityPageModule + ], + declarations: [ + EditCommunityPageComponent, + CommunityCurateComponent, + CommunityMetadataComponent, + CommunityRolesComponent + ] +}) +export class EditCommunityPageModule { + +} diff --git a/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts b/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts new file mode 100644 index 0000000000..1182db2de1 --- /dev/null +++ b/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts @@ -0,0 +1,55 @@ +import { CommunityPageResolver } from '../community-page.resolver'; +import { EditCommunityPageComponent } from './edit-community-page.component'; +import { RouterModule } from '@angular/router'; +import { NgModule } from '@angular/core'; +import { CommunityMetadataComponent } from './community-metadata/community-metadata.component'; +import { CommunityRolesComponent } from './community-roles/community-roles.component'; +import { CommunityCurateComponent } from './community-curate/community-curate.component'; + +/** + * Routing module that handles the routing for the Edit Community page administrator functionality + */ +@NgModule({ + imports: [ + RouterModule.forChild([ + { + path: '', + component: EditCommunityPageComponent, + resolve: { + dso: CommunityPageResolver + }, + children: [ + { + path: '', + redirectTo: 'metadata', + pathMatch: 'full' + }, + { + path: 'metadata', + component: CommunityMetadataComponent, + data: { + title: 'community.edit.tabs.metadata.title', + hideReturnButton: true + } + }, + { + path: 'roles', + component: CommunityRolesComponent, + data: { title: 'community.edit.tabs.roles.title' } + }, + { + path: 'curate', + component: CommunityCurateComponent, + data: { title: 'community.edit.tabs.curate.title' } + } + ] + } + ]) + ], + providers: [ + CommunityPageResolver, + ] +}) +export class EditCommunityPageRoutingModule { + +} diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html index 9156a99b18..bf6ce7fd57 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html @@ -1,14 +1,13 @@

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

- + +
diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts new file mode 100644 index 0000000000..09332dda16 --- /dev/null +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.spec.ts @@ -0,0 +1,182 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { CommunityPageSubCollectionListComponent } from './community-page-sub-collection-list.component'; +import { Community } from '../../core/shared/community.model'; +import { SharedModule } from '../../shared/shared.module'; +import { CollectionDataService } from '../../core/data/collection-data.service'; +import { FindListOptions } from '../../core/data/request.models'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { HostWindowService } from '../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; + +describe('CommunityPageSubCollectionList Component', () => { + let comp: CommunityPageSubCollectionListComponent; + let fixture: ComponentFixture; + let collectionDataServiceStub: any; + let subCollList = []; + + const collections = [Object.assign(new Community(), { + id: '123456789-1', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 1' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-2', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 2' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 3' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-4', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 4' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 5' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 6' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Collection 7' } + ] + } + }) + ]; + + const mockCommunity = Object.assign(new Community(), { + id: '123456789', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'Test title' } + ] + } + }); + + collectionDataServiceStub = { + findByParent(parentUUID: string, options: FindListOptions = {}) { + let currentPage = options.currentPage; + let elementsPerPage = options.elementsPerPage; + if (currentPage === undefined) { + currentPage = 1 + } + elementsPerPage = 5; + const startPageIndex = (currentPage - 1) * elementsPerPage; + let endPageIndex = (currentPage * elementsPerPage); + if (endPageIndex > subCollList.length) { + endPageIndex = subCollList.length; + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subCollList.slice(startPageIndex, endPageIndex))); + + } + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + SharedModule, + RouterTestingModule.withRoutes([]), + NgbModule.forRoot(), + NoopAnimationsModule + ], + declarations: [CommunityPageSubCollectionListComponent], + providers: [ + { provide: CollectionDataService, useValue: collectionDataServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: SelectableListService, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(CommunityPageSubCollectionListComponent); + comp = fixture.componentInstance; + comp.community = mockCommunity; + }); + + it('should display a list of collections', () => { + subCollList = collections; + fixture.detectChanges(); + + const collList = fixture.debugElement.queryAll(By.css('li')); + expect(collList.length).toEqual(5); + expect(collList[0].nativeElement.textContent).toContain('Collection 1'); + expect(collList[1].nativeElement.textContent).toContain('Collection 2'); + expect(collList[2].nativeElement.textContent).toContain('Collection 3'); + expect(collList[3].nativeElement.textContent).toContain('Collection 4'); + expect(collList[4].nativeElement.textContent).toContain('Collection 5'); + }); + + it('should not display the header when list of collections is empty', () => { + subCollList = []; + fixture.detectChanges(); + + const subComHead = fixture.debugElement.queryAll(By.css('h2')); + expect(subComHead.length).toEqual(0); + }); + + it('should update list of collections on pagination change', () => { + subCollList = collections; + fixture.detectChanges(); + + const pagination = Object.create({ + pagination:{ + id: comp.pageId, + currentPage: 2, + pageSize: 5 + }, + sort: { + field: 'dc.title', + direction: 'ASC' + } + }); + comp.onPaginationChange(pagination); + fixture.detectChanges(); + + const collList = fixture.debugElement.queryAll(By.css('li')); + expect(collList.length).toEqual(2); + expect(collList[0].nativeElement.textContent).toContain('Collection 6'); + expect(collList[1].nativeElement.textContent).toContain('Collection 7'); + }); +}); 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 b8a5d60002..64c274444e 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,12 +1,16 @@ import { Component, Input, OnInit } from '@angular/core'; -import { Observable } from 'rxjs'; + +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; import { RemoteData } from '../../core/data/remote-data'; import { Collection } from '../../core/shared/collection.model'; import { Community } from '../../core/shared/community.model'; - import { fadeIn } from '../../shared/animations/fade'; import { PaginatedList } from '../../core/data/paginated-list'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { CollectionDataService } from '../../core/data/collection-data.service'; @Component({ selector: 'ds-community-page-sub-collection-list', @@ -16,9 +20,60 @@ import { PaginatedList } from '../../core/data/paginated-list'; }) export class CommunityPageSubCollectionListComponent implements OnInit { @Input() community: Community; - subCollectionsRDObs: Observable>>; + + /** + * The pagination configuration + */ + config: PaginationComponentOptions; + + /** + * The pagination id + */ + pageId = 'community-collections-pagination'; + + /** + * The sorting configuration + */ + sortConfig: SortOptions; + + /** + * A list of remote data objects of communities' collections + */ + subCollectionsRDObs: BehaviorSubject>> = new BehaviorSubject>>({} as any); + + constructor(private cds: CollectionDataService) {} ngOnInit(): void { - this.subCollectionsRDObs = this.community.collections; + this.config = new PaginationComponentOptions(); + this.config.id = this.pageId; + this.config.pageSize = 5; + this.config.currentPage = 1; + this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); + this.updatePage(); + } + + /** + * Called when one of the pagination settings is changed + * @param event The new pagination data + */ + onPaginationChange(event) { + this.config.currentPage = event.pagination.currentPage; + this.config.pageSize = event.pagination.pageSize; + this.sortConfig.field = event.sort.field; + this.sortConfig.direction = event.sort.direction; + this.updatePage(); + } + + /** + * Update the list of collections + */ + updatePage() { + this.cds.findByParent(this.community.id,{ + currentPage: this.config.currentPage, + elementsPerPage: this.config.pageSize, + sort: { field: this.sortConfig.field, direction: this.sortConfig.direction } + }).pipe(take(1)).subscribe((results) => { + this.subCollectionsRDObs.next(results); + }); } } diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html index 6cd62ba48d..880ea9cc8e 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.html @@ -1,14 +1,13 @@

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

- + +
diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts index 2feaa3afa6..41502e7bd4 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -1,21 +1,29 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {TranslateModule} from '@ngx-translate/core'; -import {NO_ERRORS_SCHEMA} from '@angular/core'; -import {CommunityPageSubCommunityListComponent} from './community-page-sub-community-list.component'; -import {Community} from '../../core/shared/community.model'; -import {RemoteData} from '../../core/data/remote-data'; -import {PaginatedList} from '../../core/data/paginated-list'; -import {PageInfo} from '../../core/shared/page-info.model'; -import {SharedModule} from '../../shared/shared.module'; -import {RouterTestingModule} from '@angular/router/testing'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; -import {By} from '@angular/platform-browser'; -import {of as observableOf, Observable } from 'rxjs'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { By } from '@angular/platform-browser'; -describe('SubCommunityList Component', () => { +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; +import { Community } from '../../core/shared/community.model'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { SharedModule } from '../../shared/shared.module'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { FindListOptions } from '../../core/data/request.models'; +import { HostWindowService } from '../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; + +describe('CommunityPageSubCommunityListComponent Component', () => { let comp: CommunityPageSubCommunityListComponent; let fixture: ComponentFixture; + let communityDataServiceStub: any; + let subCommList = []; const subcommunities = [Object.assign(new Community(), { id: '123456789-1', @@ -32,34 +40,92 @@ describe('SubCommunityList Component', () => { { language: 'en_US', value: 'SubCommunity 2' } ] } + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 3' } + ] + } + }), + Object.assign(new Community(), { + id: '12345678942', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 4' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 5' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 6' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 7' } + ] + } }) ]; - const emptySubCommunitiesCommunity = Object.assign(new Community(), { + const mockCommunity = Object.assign(new Community(), { + id: '123456789', metadata: { 'dc.title': [ { language: 'en_US', value: 'Test title' } ] - }, - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + } }); - const mockCommunity = Object.assign(new Community(), { - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Test title' } - ] - }, - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subcommunities)) - }) - ; + communityDataServiceStub = { + findByParent(parentUUID: string, options: FindListOptions = {}) { + let currentPage = options.currentPage; + let elementsPerPage = options.elementsPerPage; + if (currentPage === undefined) { + currentPage = 1 + } + elementsPerPage = 5; + + const startPageIndex = (currentPage - 1) * elementsPerPage; + let endPageIndex = (currentPage * elementsPerPage); + if (endPageIndex > subCommList.length) { + endPageIndex = subCommList.length; + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subCommList.slice(startPageIndex, endPageIndex))); + + } + }; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, + imports: [ + TranslateModule.forRoot(), + SharedModule, RouterTestingModule.withRoutes([]), - NoopAnimationsModule], + NgbModule.forRoot(), + NoopAnimationsModule + ], declarations: [CommunityPageSubCommunityListComponent], + providers: [ + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: SelectableListService, useValue: {} }, + ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); @@ -67,23 +133,52 @@ describe('SubCommunityList Component', () => { beforeEach(() => { fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent); comp = fixture.componentInstance; + comp.community = mockCommunity; + }); - it('should display a list of subCommunities', () => { - comp.community = mockCommunity; + it('should display a list of sub-communities', () => { + subCommList = subcommunities; fixture.detectChanges(); const subComList = fixture.debugElement.queryAll(By.css('li')); - expect(subComList.length).toEqual(2); + expect(subComList.length).toEqual(5); expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1'); expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2'); + expect(subComList[2].nativeElement.textContent).toContain('SubCommunity 3'); + expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4'); + expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5'); }); - it('should not display the header when subCommunities are empty', () => { - comp.community = emptySubCommunitiesCommunity; + it('should not display the header when list of sub-communities is empty', () => { + subCommList = []; fixture.detectChanges(); const subComHead = fixture.debugElement.queryAll(By.css('h2')); expect(subComHead.length).toEqual(0); }); + + it('should update list of sub-communities on pagination change', () => { + subCommList = subcommunities; + fixture.detectChanges(); + + const pagination = Object.create({ + pagination:{ + id: comp.pageId, + currentPage: 2, + pageSize: 5 + }, + sort: { + field: 'dc.title', + direction: 'ASC' + } + }); + comp.onPaginationChange(pagination); + fixture.detectChanges(); + + const collList = fixture.debugElement.queryAll(By.css('li')); + expect(collList.length).toEqual(2); + expect(collList[0].nativeElement.textContent).toContain('SubCommunity 6'); + expect(collList[1].nativeElement.textContent).toContain('SubCommunity 7'); + }); }); diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts index 91f6d7bac1..1bd664021e 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts @@ -1,26 +1,82 @@ import { Component, Input, OnInit } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; + import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; - import { fadeIn } from '../../shared/animations/fade'; import { PaginatedList } from '../../core/data/paginated-list'; -import {Observable} from 'rxjs'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { CommunityDataService } from '../../core/data/community-data.service'; @Component({ selector: 'ds-community-page-sub-community-list', styleUrls: ['./community-page-sub-community-list.component.scss'], templateUrl: './community-page-sub-community-list.component.html', - animations:[fadeIn] + animations: [fadeIn] }) /** * Component to render the sub-communities of a Community */ export class CommunityPageSubCommunityListComponent implements OnInit { @Input() community: Community; - subCommunitiesRDObs: Observable>>; + + /** + * The pagination configuration + */ + config: PaginationComponentOptions; + + /** + * The pagination id + */ + pageId = 'community-subCommunities-pagination'; + + /** + * The sorting configuration + */ + sortConfig: SortOptions; + + /** + * A list of remote data objects of communities' collections + */ + subCommunitiesRDObs: BehaviorSubject>> = new BehaviorSubject>>({} as any); + + constructor(private cds: CommunityDataService) { + } ngOnInit(): void { - this.subCommunitiesRDObs = this.community.subcommunities; + this.config = new PaginationComponentOptions(); + this.config.id = this.pageId; + this.config.pageSize = 5; + this.config.currentPage = 1; + this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); + this.updatePage(); + } + + /** + * Called when one of the pagination settings is changed + * @param event The new pagination data + */ + onPaginationChange(event) { + this.config.currentPage = event.pagination.currentPage; + this.config.pageSize = event.pagination.pageSize; + this.sortConfig.field = event.sort.field; + this.sortConfig.direction = event.sort.direction; + this.updatePage(); + } + + /** + * Update the list of sub-communities + */ + updatePage() { + this.cds.findByParent(this.community.id, { + currentPage: this.config.currentPage, + elementsPerPage: this.config.pageSize, + sort: { field: this.sortConfig.field, direction: this.sortConfig.direction } + }).pipe(take(1)).subscribe((results) => { + this.subCommunitiesRDObs.next(results); + }); } } diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts new file mode 100644 index 0000000000..fa164fe624 --- /dev/null +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts @@ -0,0 +1,161 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { By } from '@angular/platform-browser'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { TopLevelCommunityListComponent } from './top-level-community-list.component'; +import { Community } from '../../core/shared/community.model'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { SharedModule } from '../../shared/shared.module'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { FindListOptions } from '../../core/data/request.models'; +import { HostWindowService } from '../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; + +describe('TopLevelCommunityList Component', () => { + let comp: TopLevelCommunityListComponent; + let fixture: ComponentFixture; + let communityDataServiceStub: any; + + const topCommList = [Object.assign(new Community(), { + id: '123456789-1', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 1' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-2', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 2' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 3' } + ] + } + }), + Object.assign(new Community(), { + id: '12345678942', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 4' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 5' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 6' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 7' } + ] + } + }) + ]; + + communityDataServiceStub = { + findTop(options: FindListOptions = {}) { + let currentPage = options.currentPage; + let elementsPerPage = options.elementsPerPage; + if (currentPage === undefined) { + currentPage = 1 + } + elementsPerPage = 5; + + const startPageIndex = (currentPage - 1) * elementsPerPage; + let endPageIndex = (currentPage * elementsPerPage); + if (endPageIndex > topCommList.length) { + endPageIndex = topCommList.length; + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), topCommList.slice(startPageIndex, endPageIndex))); + + } + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + SharedModule, + RouterTestingModule.withRoutes([]), + NgbModule.forRoot(), + NoopAnimationsModule + ], + declarations: [TopLevelCommunityListComponent], + providers: [ + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: SelectableListService, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TopLevelCommunityListComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + + }); + + it('should display a list of top-communities', () => { + const subComList = fixture.debugElement.queryAll(By.css('li')); + + expect(subComList.length).toEqual(5); + expect(subComList[0].nativeElement.textContent).toContain('TopCommunity 1'); + expect(subComList[1].nativeElement.textContent).toContain('TopCommunity 2'); + expect(subComList[2].nativeElement.textContent).toContain('TopCommunity 3'); + expect(subComList[3].nativeElement.textContent).toContain('TopCommunity 4'); + expect(subComList[4].nativeElement.textContent).toContain('TopCommunity 5'); + }); + + it('should update list of top-communities on pagination change', () => { + const pagination = Object.create({ + pagination:{ + id: comp.pageId, + currentPage: 2, + pageSize: 5 + }, + sort: { + field: 'dc.title', + direction: 'ASC' + } + }); + comp.onPaginationChange(pagination); + fixture.detectChanges(); + + const collList = fixture.debugElement.queryAll(By.css('li')); + expect(collList.length).toEqual(2); + expect(collList[0].nativeElement.textContent).toContain('TopCommunity 6'); + expect(collList[1].nativeElement.textContent).toContain('TopCommunity 7'); + }); +}); 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 1115d785a3..02c3cb54a0 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,15 +1,15 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; + +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; + 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'; - import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; - import { fadeInOut } from '../../shared/animations/fade'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { take } from 'rxjs/operators'; /** * this component renders the Top-Level Community list @@ -33,6 +33,11 @@ export class TopLevelCommunityListComponent implements OnInit { */ config: PaginationComponentOptions; + /** + * The pagination id + */ + pageId = 'top-level-pagination'; + /** * The sorting configuration */ @@ -40,7 +45,7 @@ export class TopLevelCommunityListComponent implements OnInit { constructor(private cds: CommunityDataService) { this.config = new PaginationComponentOptions(); - this.config.id = 'top-level-pagination'; + this.config.id = this.pageId; this.config.pageSize = 5; this.config.currentPage = 1; this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); @@ -55,10 +60,10 @@ export class TopLevelCommunityListComponent implements OnInit { * @param event The new pagination data */ onPaginationChange(event) { - this.config.currentPage = event.page; - this.config.pageSize = event.pageSize; - this.sortConfig.field = event.sortField; - this.sortConfig.direction = event.sortDirection; + this.config.currentPage = event.pagination.currentPage; + this.config.pageSize = event.pagination.pageSize; + this.sortConfig.field = event.sort.field; + this.sortConfig.direction = event.sort.direction; this.updatePage(); } diff --git a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts index 1d5564a295..81d66bb5f7 100644 --- a/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts +++ b/src/app/+my-dspace-page/my-dspace-new-submission/my-dspace-new-submission.component.ts @@ -33,12 +33,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit { /** * The UploaderOptions object */ - public uploadFilesOptions: UploaderOptions = { - url: '', - authToken: null, - disableMultipart: false, - itemAlias: null - }; + public uploadFilesOptions: UploaderOptions = new UploaderOptions(); /** * Subscription to unsubscribe from diff --git a/src/app/+search-page/configuration-search-page.component.ts b/src/app/+search-page/configuration-search-page.component.ts index 33d99a9cd2..2cde216c05 100644 --- a/src/app/+search-page/configuration-search-page.component.ts +++ b/src/app/+search-page/configuration-search-page.component.ts @@ -5,10 +5,10 @@ import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angu import { pushInOut } from '../shared/animations/push'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; -import { Router } from '@angular/router'; import { hasValue } from '../shared/empty.util'; import { RouteService } from '../core/services/route.service'; import { SearchService } from '../core/shared/search/search.service'; +import { Router } from '@angular/router'; /** * This component renders a search page using a configuration as input. @@ -61,5 +61,8 @@ export class ConfigurationSearchPageComponent extends SearchComponent implements if (hasValue(this.configuration)) { this.routeService.setParameter('configuration', this.configuration); } + if (hasValue(this.fixedFilterQuery)) { + this.routeService.setParameter('fixedFilter', this.fixedFilterQuery); + } } } diff --git a/src/app/+search-page/search-page.module.ts b/src/app/+search-page/search-page.module.ts index e5ce670013..ca6cbf9e5c 100644 --- a/src/app/+search-page/search-page.module.ts +++ b/src/app/+search-page/search-page.module.ts @@ -3,18 +3,28 @@ import { CommonModule } from '@angular/common'; import { CoreModule } from '../core/core.module'; import { SharedModule } from '../shared/shared.module'; import { SearchPageRoutingModule } from './search-page-routing.module'; -import { SearchPageComponent } from './search-page.component'; +import { SearchComponent } from './search.component'; +import { SidebarService } from '../shared/sidebar/sidebar.service'; +import { SidebarEffects } from '../shared/sidebar/sidebar-effects.service'; +import { EffectsModule } from '@ngrx/effects'; import { ConfigurationSearchPageComponent } from './configuration-search-page.component'; import { ConfigurationSearchPageGuard } from './configuration-search-page.guard'; -import { SearchTrackerComponent } from './search-tracker.component'; +import { SearchPageComponent } from './search-page.component'; +import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service'; import { StatisticsModule } from '../statistics/statistics.module'; -import { SearchComponent } from './search.component'; +import { SearchTrackerComponent } from './search-tracker.component'; +import { SearchFilterService } from '../core/shared/search/search-filter.service'; +import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; + +const effects = [ + SidebarEffects +]; const components = [ SearchPageComponent, SearchComponent, ConfigurationSearchPageComponent, - SearchTrackerComponent + SearchTrackerComponent, ]; @NgModule({ @@ -22,11 +32,18 @@ const components = [ SearchPageRoutingModule, CommonModule, SharedModule, + EffectsModule.forFeature(effects), CoreModule.forRoot(), StatisticsModule.forRoot(), ], - providers: [ConfigurationSearchPageGuard], declarations: components, + providers: [ + SidebarService, + SidebarFilterService, + SearchFilterService, + ConfigurationSearchPageGuard, + SearchConfigurationService + ], exports: components }) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 1d0d765130..1f3da086c2 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -82,6 +82,7 @@ export class AppComponent implements OnInit, AfterViewInit { } } + angulartics2GoogleAnalytics.startTracking(); angulartics2DSpace.startTracking(); metadata.listenForRouteChange(); diff --git a/src/app/app.reducer.ts b/src/app/app.reducer.ts index ad9247799b..837fb9befd 100644 --- a/src/app/app.reducer.ts +++ b/src/app/app.reducer.ts @@ -1,5 +1,6 @@ import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store'; import * as fromRouter from '@ngrx/router-store'; + import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer'; import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer'; import { formReducer, FormState } from './shared/form/form.reducer'; @@ -8,23 +9,28 @@ import { sidebarFilterReducer, SidebarFiltersState } from './shared/sidebar/filt import { filterReducer, SearchFiltersState } from './shared/search/search-filters/search-filter/search-filter.reducer'; import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers'; import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; -import { metadataRegistryReducer, MetadataRegistryState } from './+admin/admin-registries/metadata-registry/metadata-registry.reducers'; +import { + metadataRegistryReducer, + MetadataRegistryState +} from './+admin/admin-registries/metadata-registry/metadata-registry.reducers'; import { hasValue } from './shared/empty.util'; import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer'; import { menusReducer, MenusState } from './shared/menu/menu.reducer'; -import { historyReducer, HistoryState } from './shared/history/history.reducer'; -import { selectableListReducer, SelectableListsState } from './shared/object-list/selectable-list/selectable-list.reducer'; -import { bitstreamFormatReducer, BitstreamFormatRegistryState } from './+admin/admin-registries/bitstream-formats/bitstream-format.reducers'; +import { + selectableListReducer, + SelectableListsState +} from './shared/object-list/selectable-list/selectable-list.reducer'; import { ObjectSelectionListState, objectSelectionReducer } from './shared/object-select/object-select.reducer'; -import { NameVariantListsState, nameVariantReducer } from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer'; +import { + NameVariantListsState, + nameVariantReducer +} from './shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer'; export interface AppState { router: fromRouter.RouterReducerState; - history: HistoryState; hostWindow: HostWindowState; forms: FormState; metadataRegistry: MetadataRegistryState; - bitstreamFormats: BitstreamFormatRegistryState; notifications: NotificationsState; sidebar: SidebarState; sidebarFilter: SidebarFiltersState; @@ -40,11 +46,9 @@ export interface AppState { export const appReducers: ActionReducerMap = { router: fromRouter.routerReducer, - history: historyReducer, hostWindow: hostWindowReducer, forms: formReducer, metadataRegistry: metadataRegistryReducer, - bitstreamFormats: bitstreamFormatReducer, notifications: notificationsReducer, sidebar: sidebarReducer, sidebarFilter: sidebarFilterReducer, diff --git a/src/app/community-list-page/community-list-service.ts b/src/app/community-list-page/community-list-service.ts index a25dbd2689..be04887e71 100644 --- a/src/app/community-list-page/community-list-service.ts +++ b/src/app/community-list-page/community-list-service.ts @@ -312,7 +312,7 @@ export class CommunityListService { hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 }) .pipe( - filter((rd: RemoteData>) => rd.hasSucceeded), + filter((rd: RemoteData>) => rd.hasSucceeded), take(1), map((results) => results.payload.totalElements > 0), ); @@ -320,8 +320,8 @@ export class CommunityListService { let hasChildren$: Observable; hasChildren$ = observableCombineLatest(hasSubcoms$, hasColls$).pipe( take(1), - map((result: [boolean]) => { - if (result[0] || result[1]) { + map(([hasSubcoms, hasColls]: [boolean, boolean]) => { + if (hasSubcoms || hasColls) { return true; } else { return false; diff --git a/src/app/core/auth/auth.service.ts b/src/app/core/auth/auth.service.ts index 94b1e9e6ff..a536313521 100644 --- a/src/app/core/auth/auth.service.ts +++ b/src/app/core/auth/auth.service.ts @@ -60,7 +60,8 @@ export class AuthService { // and is not the login route, clear redirect url and messages const routeUrl$ = this.store.pipe( select(routerStateSelector), - filter((routerState: RouterReducerState) => isNotUndefined(routerState) && isNotUndefined(routerState.state)), + filter((routerState: RouterReducerState) => isNotUndefined(routerState) + && isNotUndefined(routerState.state) && isNotEmpty(routerState.state.url)), filter((routerState: RouterReducerState) => !this.isLoginRoute(routerState.state.url)), map((routerState: RouterReducerState) => routerState.state.url) ); diff --git a/src/app/core/cache/models/normalized-external-source-entry.model.ts b/src/app/core/cache/models/normalized-external-source-entry.model.ts new file mode 100644 index 0000000000..e8e3c695c3 --- /dev/null +++ b/src/app/core/cache/models/normalized-external-source-entry.model.ts @@ -0,0 +1,36 @@ +import { autoserialize, autoserializeAs, inheritSerialization } from 'cerialize'; +import { NormalizedObject } from './normalized-object.model'; +import { ExternalSourceEntry } from '../../shared/external-source-entry.model'; +import { mapsTo } from '../builders/build-decorators'; +import { MetadataMap, MetadataMapSerializer } from '../../shared/metadata.models'; + +/** + * Normalized model class for an external source entry + */ +@mapsTo(ExternalSourceEntry) +@inheritSerialization(NormalizedObject) +export class NormalizedExternalSourceEntry extends NormalizedObject { + /** + * Unique identifier + */ + @autoserialize + id: string; + + /** + * The value to display + */ + @autoserialize + display: string; + + /** + * The value to store the entry with + */ + @autoserialize + value: string; + + /** + * Metadata of the entry + */ + @autoserializeAs(MetadataMapSerializer) + metadata: MetadataMap; +} diff --git a/src/app/core/cache/models/normalized-external-source.model.ts b/src/app/core/cache/models/normalized-external-source.model.ts new file mode 100644 index 0000000000..fd9a42fb72 --- /dev/null +++ b/src/app/core/cache/models/normalized-external-source.model.ts @@ -0,0 +1,29 @@ +import { autoserialize, inheritSerialization } from 'cerialize'; +import { NormalizedObject } from './normalized-object.model'; +import { ExternalSource } from '../../shared/external-source.model'; +import { mapsTo } from '../builders/build-decorators'; + +/** + * Normalized model class for an external source + */ +@mapsTo(ExternalSource) +@inheritSerialization(NormalizedObject) +export class NormalizedExternalSource extends NormalizedObject { + /** + * Unique identifier + */ + @autoserialize + id: string; + + /** + * The name of this external source + */ + @autoserialize + name: string; + + /** + * Is the source hierarchical? + */ + @autoserialize + hierarchical: boolean; +} diff --git a/src/app/core/core.module.ts b/src/app/core/core.module.ts index 4fdef02357..efd83d33d5 100644 --- a/src/app/core/core.module.ts +++ b/src/app/core/core.module.ts @@ -136,6 +136,10 @@ import { SearchConfigurationService } from './shared/search/search-configuration import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service'; import { RelationshipTypeService } from './data/relationship-type.service'; import { SidebarService } from '../shared/sidebar/sidebar.service'; +import { NormalizedExternalSource } from './cache/models/normalized-external-source.model'; +import { NormalizedExternalSourceEntry } from './cache/models/normalized-external-source-entry.model'; +import { ExternalSourceService } from './data/external-source.service'; +import { LookupRelationService } from './data/lookup-relation.service'; /** * When not in production, endpoint responses can be mocked for testing purposes @@ -247,6 +251,8 @@ const PROVIDERS = [ SearchConfigurationService, SelectableListService, RelationshipTypeService, + ExternalSourceService, + LookupRelationService, // register AuthInterceptor as HttpInterceptor { provide: HTTP_INTERCEPTORS, @@ -292,7 +298,9 @@ export const normalizedModels = NormalizedPoolTask, NormalizedRelationship, NormalizedRelationshipType, - NormalizedItemType + NormalizedItemType, + NormalizedExternalSource, + NormalizedExternalSourceEntry ]; @NgModule({ diff --git a/src/app/core/core.reducers.ts b/src/app/core/core.reducers.ts index 4fcf36f9cc..78391eee3f 100644 --- a/src/app/core/core.reducers.ts +++ b/src/app/core/core.reducers.ts @@ -1,7 +1,4 @@ -import { - ActionReducerMap, - createFeatureSelector, -} from '@ngrx/store'; +import { ActionReducerMap, } from '@ngrx/store'; import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer'; import { indexReducer, MetaIndexState } from './index/index.reducer'; @@ -9,17 +6,21 @@ import { requestReducer, RequestState } from './data/request.reducer'; import { authReducer, AuthState } from './auth/auth.reducer'; import { jsonPatchOperationsReducer, JsonPatchOperationsState } from './json-patch/json-patch-operations.reducer'; import { serverSyncBufferReducer, ServerSyncBufferState } from './cache/server-sync-buffer.reducer'; -import { - objectUpdatesReducer, - ObjectUpdatesState -} from './data/object-updates/object-updates.reducer'; +import { objectUpdatesReducer, ObjectUpdatesState } from './data/object-updates/object-updates.reducer'; import { routeReducer, RouteState } from './services/route.reducer'; +import { + bitstreamFormatReducer, + BitstreamFormatRegistryState +} from '../+admin/admin-registries/bitstream-formats/bitstream-format.reducers'; +import { historyReducer, HistoryState } from './history/history.reducer'; export interface CoreState { + 'bitstreamFormats': BitstreamFormatRegistryState; 'cache/object': ObjectCacheState, 'cache/syncbuffer': ServerSyncBufferState, 'cache/object-updates': ObjectUpdatesState 'data/request': RequestState, + 'history': HistoryState; 'index': MetaIndexState, 'auth': AuthState, 'json/patch': JsonPatchOperationsState, @@ -27,10 +28,12 @@ export interface CoreState { } export const coreReducers: ActionReducerMap = { + 'bitstreamFormats': bitstreamFormatReducer, 'cache/object': objectCacheReducer, 'cache/syncbuffer': serverSyncBufferReducer, 'cache/object-updates': objectUpdatesReducer, 'data/request': requestReducer, + 'history': historyReducer, 'index': indexReducer, 'auth': authReducer, 'json/patch': jsonPatchOperationsReducer, diff --git a/src/app/core/data/bitstream-format-data.service.spec.ts b/src/app/core/data/bitstream-format-data.service.spec.ts index f3ce478236..c626fcd6e2 100644 --- a/src/app/core/data/bitstream-format-data.service.spec.ts +++ b/src/app/core/data/bitstream-format-data.service.spec.ts @@ -3,7 +3,6 @@ import { RequestEntry } from './request.reducer'; import { RestResponse } from '../cache/response.models'; import { Observable, of as observableOf } from 'rxjs'; import { Action, Store } from '@ngrx/store'; -import { CoreState } from '../core.reducers'; import { ObjectCacheService } from '../cache/object-cache.service'; import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { HALEndpointService } from '../shared/hal-endpoint.service'; @@ -19,6 +18,7 @@ import { BitstreamFormatsRegistrySelectAction } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions'; import { TestScheduler } from 'rxjs/testing'; +import { CoreState } from '../core.reducers'; describe('BitstreamFormatDataService', () => { let service: BitstreamFormatDataService; diff --git a/src/app/core/data/bitstream-format-data.service.ts b/src/app/core/data/bitstream-format-data.service.ts index 7255ed3663..b5c2b708dc 100644 --- a/src/app/core/data/bitstream-format-data.service.ts +++ b/src/app/core/data/bitstream-format-data.service.ts @@ -5,7 +5,6 @@ import { RequestService } from './request.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { createSelector, select, Store } from '@ngrx/store'; -import { CoreState } from '../core.reducers'; import { ObjectCacheService } from '../cache/object-cache.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { NotificationsService } from '../../shared/notifications/notifications.service'; @@ -17,7 +16,6 @@ import { find, map, tap } from 'rxjs/operators'; import { configureRequest, getResponseFromEntry } from '../shared/operators'; import { distinctUntilChanged } from 'rxjs/internal/operators/distinctUntilChanged'; import { RestResponse } from '../cache/response.models'; -import { AppState } from '../../app.reducer'; import { BitstreamFormatRegistryState } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.reducers'; import { BitstreamFormatsRegistryDeselectAction, @@ -26,8 +24,9 @@ import { } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions'; import { hasValue } from '../../shared/empty.util'; import { RequestEntry } from './request.reducer'; +import { CoreState } from '../core.reducers'; -const bitstreamFormatsStateSelector = (state: AppState) => state.bitstreamFormats; +const bitstreamFormatsStateSelector = (state: CoreState) => state.bitstreamFormats; const selectedBitstreamFormatSelector = createSelector(bitstreamFormatsStateSelector, (bitstreamFormatRegistryState: BitstreamFormatRegistryState) => bitstreamFormatRegistryState.selectedBitstreamFormats); @@ -55,6 +54,7 @@ export class BitstreamFormatDataService extends DataService { /** * Get the endpoint for browsing bitstream formats * @param {FindListOptions} options + * @param {string} linkPath * @returns {Observable} */ getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable { @@ -99,7 +99,7 @@ export class BitstreamFormatDataService extends DataService { /** * Create a new BitstreamFormat - * @param BitstreamFormat + * @param {BitstreamFormat} bitstreamFormat */ public createBitstreamFormat(bitstreamFormat: BitstreamFormat): Observable { const requestId = this.requestService.generateRequestId(); diff --git a/src/app/core/data/comcol-data.service.ts b/src/app/core/data/comcol-data.service.ts index 867ee24fc1..2ce0362a4e 100644 --- a/src/app/core/data/comcol-data.service.ts +++ b/src/app/core/data/comcol-data.service.ts @@ -1,32 +1,41 @@ import { distinctUntilChanged, - filter, first, - map, - mergeMap, - share, - switchMap, + filter, first,map, mergeMap, share, switchMap, take, tap } from 'rxjs/operators'; -import { merge as observableMerge, Observable, throwError as observableThrowError } from 'rxjs'; +import { merge as observableMerge, Observable, throwError as observableThrowError, combineLatest as observableCombineLatest } from 'rxjs'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { ObjectCacheService } from '../cache/object-cache.service'; import { CommunityDataService } from './community-data.service'; import { DataService } from './data.service'; +import { DeleteRequest, FindListOptions, FindByIDRequest, RestRequest } from './request.models'; import { PaginatedList } from './paginated-list'; import { RemoteData } from './remote-data'; -import { FindListOptions, FindByIDRequest } from './request.models'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { getResponseFromEntry } from '../shared/operators'; +import { + configureRequest, + getRemoteDataPayload, + getResponseFromEntry, + getSucceededRemoteData +} from '../shared/operators'; import { CacheableObject } from '../cache/object-cache.reducer'; +import { RestResponse } from '../cache/response.models'; +import { Bitstream } from '../shared/bitstream.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; export abstract class ComColDataService extends DataService { protected abstract cds: CommunityDataService; protected abstract objectCache: ObjectCacheService; protected abstract halService: HALEndpointService; + /** + * Linkpath of endpoint to delete the logo + */ + protected logoDeleteLinkpath = 'bitstreams'; + /** * Get the scoped endpoint URL by fetching the object with * the given scopeID and returning its HAL link with this @@ -76,4 +85,33 @@ export abstract class ComColDataService extends DataS return this.findList(href$, options); } + /** + * Get the endpoint for the community or collection's logo + * @param id The community or collection's ID + */ + public getLogoEndpoint(id: string): Observable { + return this.halService.getEndpoint(this.linkPath).pipe( + switchMap((href: string) => this.halService.getEndpoint('logo', `${href}/${id}`)) + ) + } + + /** + * Delete the logo from the community or collection + * @param dso The object to delete the logo from + */ + public deleteLogo(dso: DSpaceObject): Observable { + const logo$ = (dso as any).logo; + if (hasValue(logo$)) { + return observableCombineLatest( + logo$.pipe(getSucceededRemoteData(), getRemoteDataPayload(), take(1)), + this.halService.getEndpoint(this.logoDeleteLinkpath) + ).pipe( + map(([logo, href]: [Bitstream, string]) => `${href}/${logo.id}`), + map((href: string) => new DeleteRequest(this.requestService.generateRequestId(), href)), + configureRequest(this.requestService), + switchMap((restRequest: RestRequest) => this.requestService.getByUUID(restRequest.uuid)), + getResponseFromEntry() + ); + } + } } diff --git a/src/app/core/data/data.service.ts b/src/app/core/data/data.service.ts index 997ccec49f..f1d76b47fd 100644 --- a/src/app/core/data/data.service.ts +++ b/src/app/core/data/data.service.ts @@ -1,12 +1,22 @@ import { HttpClient } from '@angular/common/http'; import { Observable } from 'rxjs'; -import { distinctUntilChanged, filter, find, first, map, mergeMap, skipWhile, switchMap, take, tap } from 'rxjs/operators'; +import { + distinctUntilChanged, + filter, + find, + first, + map, + mergeMap, + skipWhile, + switchMap, + take, + tap +} from 'rxjs/operators'; import { Store } from '@ngrx/store'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { CoreState } from '../core.reducers'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { URLCombiner } from '../url-combiner/url-combiner'; import { PaginatedList } from './paginated-list'; @@ -14,9 +24,9 @@ import { RemoteData } from './remote-data'; import { CreateRequest, DeleteByIDRequest, + FindByIDRequest, FindListOptions, FindListRequest, - FindByIDRequest, GetRequest } from './request.models'; import { RequestService } from './request.service'; @@ -37,6 +47,7 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec import { ChangeAnalyzer } from './change-analyzer'; import { RestRequestMethod } from './rest-request-method'; import { getMapsToType } from '../cache/builders/build-decorators'; +import { CoreState } from '../core.reducers'; export abstract class DataService { protected abstract requestService: RequestService; @@ -238,7 +249,7 @@ export abstract class DataService { */ update(object: T): Observable> { const oldVersion$ = this.objectCache.getObjectBySelfLink(object.self); - return oldVersion$.pipe(take(1), mergeMap((oldVersion: T) => { + return oldVersion$.pipe(take(1), mergeMap((oldVersion: NormalizedObject) => { const operations = this.comparator.diff(oldVersion, object); if (isNotEmpty(operations)) { this.objectCache.addPatch(object.self, operations); diff --git a/src/app/core/data/external-source.service.spec.ts b/src/app/core/data/external-source.service.spec.ts new file mode 100644 index 0000000000..77a2a85dfd --- /dev/null +++ b/src/app/core/data/external-source.service.spec.ts @@ -0,0 +1,76 @@ +import { ExternalSourceService } from './external-source.service'; +import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { of as observableOf } from 'rxjs'; +import { GetRequest } from './request.models'; + +describe('ExternalSourceService', () => { + let service: ExternalSourceService; + + let requestService; + let rdbService; + let halService; + + const entries = [ + Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0001', + display: 'John Doe', + value: 'John, Doe', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0001' + } + ] + } + }), + Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0002', + display: 'Sampson Megan', + value: 'Sampson, Megan', + metadata: { + 'dc.identifier.uri': [ + { + value: 'https://orcid.org/0001-0001-0001-0002' + } + ] + } + }) + ]; + + function init() { + requestService = jasmine.createSpyObj('requestService', { + generateRequestId: 'request-uuid', + configure: {} + }); + rdbService = jasmine.createSpyObj('rdbService', { + buildList: createSuccessfulRemoteDataObject$(createPaginatedList(entries)) + }); + halService = jasmine.createSpyObj('halService', { + getEndpoint: observableOf('external-sources-REST-endpoint') + }); + service = new ExternalSourceService(requestService, rdbService, undefined, undefined, undefined, halService, undefined, undefined, undefined); + } + + beforeEach(() => { + init(); + }); + + describe('getExternalSourceEntries', () => { + let result; + + beforeEach(() => { + result = service.getExternalSourceEntries('test'); + }); + + it('should configure a GetRequest', () => { + expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest)); + }); + + it('should return the entries', () => { + result.subscribe((resultRD) => { + expect(resultRD.payload.page).toBe(entries); + }); + }); + }); +}); diff --git a/src/app/core/data/external-source.service.ts b/src/app/core/data/external-source.service.ts new file mode 100644 index 0000000000..c32c13a20f --- /dev/null +++ b/src/app/core/data/external-source.service.ts @@ -0,0 +1,85 @@ +import { Injectable } from '@angular/core'; +import { DataService } from './data.service'; +import { ExternalSource } from '../shared/external-source.model'; +import { RequestService } from './request.service'; +import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; +import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; +import { Store } from '@ngrx/store'; +import { CoreState } from '../core.reducers'; +import { ObjectCacheService } from '../cache/object-cache.service'; +import { HALEndpointService } from '../shared/hal-endpoint.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { HttpClient } from '@angular/common/http'; +import { FindListOptions, GetRequest } from './request.models'; +import { Observable } from 'rxjs/internal/Observable'; +import { distinctUntilChanged, map, switchMap } from 'rxjs/operators'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { hasValue, isNotEmptyOperator } from '../../shared/empty.util'; +import { configureRequest } from '../shared/operators'; +import { RemoteData } from './remote-data'; +import { PaginatedList } from './paginated-list'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; +import { DefaultChangeAnalyzer } from './default-change-analyzer.service'; + +/** + * A service handling all external source requests + */ +@Injectable() +export class ExternalSourceService extends DataService { + protected linkPath = 'externalsources'; + + constructor( + protected requestService: RequestService, + protected rdbService: RemoteDataBuildService, + protected dataBuildService: NormalizedObjectBuildService, + protected store: Store, + protected objectCache: ObjectCacheService, + protected halService: HALEndpointService, + protected notificationsService: NotificationsService, + protected http: HttpClient, + protected comparator: DefaultChangeAnalyzer) { + super(); + } + + /** + * Get the endpoint to browse external sources + * @param options + * @param linkPath + */ + getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable { + return this.halService.getEndpoint(linkPath); + } + + /** + * Get the endpoint for an external source's entries + * @param externalSourceId The id of the external source to fetch entries for + */ + getEntriesEndpoint(externalSourceId: string): Observable { + return this.getBrowseEndpoint().pipe( + map((href) => this.getIDHref(href, externalSourceId)), + switchMap((href) => this.halService.getEndpoint('entries', href)) + ); + } + + /** + * Get the entries for an external source + * @param externalSourceId The id of the external source to fetch entries for + * @param searchOptions The search options to limit results to + */ + getExternalSourceEntries(externalSourceId: string, searchOptions?: PaginatedSearchOptions): Observable>> { + const requestUuid = this.requestService.generateRequestId(); + + const href$ = this.getEntriesEndpoint(externalSourceId).pipe( + isNotEmptyOperator(), + distinctUntilChanged(), + map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint) + ); + + href$.pipe( + map((endpoint: string) => new GetRequest(requestUuid, endpoint)), + configureRequest(this.requestService) + ).subscribe(); + + return this.rdbService.buildList(href$); + } +} diff --git a/src/app/core/data/item-data.service.spec.ts b/src/app/core/data/item-data.service.spec.ts index 44c5f48cfe..6f2719f374 100644 --- a/src/app/core/data/item-data.service.spec.ts +++ b/src/app/core/data/item-data.service.spec.ts @@ -6,22 +6,14 @@ import { CoreState } from '../core.reducers'; import { ItemDataService } from './item-data.service'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; -import { - DeleteRequest, - FindListOptions, - GetRequest, - MappedCollectionsRequest, - PostRequest, - RestRequest -} from './request.models'; +import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models'; import { ObjectCacheService } from '../cache/object-cache.service'; -import { Observable } from 'rxjs'; +import { Observable, of as observableOf } from 'rxjs'; import { RestResponse } from '../cache/response.models'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { HttpClient } from '@angular/common/http'; import { RequestEntry } from './request.reducer'; -import { of as observableOf } from 'rxjs'; import { getMockRequestService } from '../../shared/mocks/mock-request.service'; describe('ItemDataService', () => { @@ -184,7 +176,7 @@ describe('ItemDataService', () => { }); it('should configure a DELETE request', () => { - result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(DeleteRequest), undefined)); + result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(DeleteRequest))); }); }); @@ -198,7 +190,7 @@ describe('ItemDataService', () => { }); it('should configure a POST request', () => { - result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest), undefined)); + result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest))); }); }); diff --git a/src/app/core/data/lookup-relation.service.spec.ts b/src/app/core/data/lookup-relation.service.spec.ts new file mode 100644 index 0000000000..321fd8d218 --- /dev/null +++ b/src/app/core/data/lookup-relation.service.spec.ts @@ -0,0 +1,116 @@ +import { LookupRelationService } from './lookup-relation.service'; +import { ExternalSourceService } from './external-source.service'; +import { SearchService } from '../shared/search/search.service'; +import { createPaginatedList, createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { PaginatedList } from './paginated-list'; +import { PageInfo } from '../shared/page-info.model'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; +import { SearchResult } from '../../shared/search/search-result.model'; +import { Item } from '../shared/item.model'; +import { skip, take } from 'rxjs/operators'; +import { ExternalSource } from '../shared/external-source.model'; + +describe('LookupRelationService', () => { + let service: LookupRelationService; + let externalSourceService: ExternalSourceService; + let searchService: SearchService; + + const totalExternal = 8; + const optionsWithQuery = new PaginatedSearchOptions({ query: 'test-query' }); + const relationship = Object.assign(new RelationshipOptions(), { + filter: 'test-filter', + configuration: 'test-configuration' + }); + const localResults = [ + Object.assign(new SearchResult(), { + indexableObject: Object.assign(new Item(), { + uuid: 'test-item-uuid', + handle: 'test-item-handle' + }) + }) + ]; + const externalSource = Object.assign(new ExternalSource(), { + id: 'orcidV2', + name: 'orcidV2', + hierarchical: false + }); + + function init() { + externalSourceService = jasmine.createSpyObj('externalSourceService', { + getExternalSourceEntries: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo({ elementsPerPage: 1, totalElements: totalExternal, totalPages: totalExternal, currentPage: 1 }), [{}])) + }); + searchService = jasmine.createSpyObj('searchService', { + search: createSuccessfulRemoteDataObject$(createPaginatedList(localResults)) + }); + service = new LookupRelationService(externalSourceService, searchService); + } + + beforeEach(() => { + init(); + }); + + describe('getLocalResults', () => { + let result; + + beforeEach(() => { + result = service.getLocalResults(relationship, optionsWithQuery); + }); + + it('should return the local results', () => { + result.subscribe((resultsRD) => { + expect(resultsRD.payload.page).toBe(localResults); + }); + }); + + it('should set the searchConfig to contain a fixedFilter and configuration', () => { + expect(service.searchConfig).toEqual(Object.assign(new PaginatedSearchOptions({}), optionsWithQuery, + { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration } + )); + }); + }); + + describe('getTotalLocalResults', () => { + let result; + + beforeEach(() => { + result = service.getTotalLocalResults(relationship, optionsWithQuery); + }); + + it('should start with 0', () => { + result.pipe(take(1)).subscribe((amount) => { + expect(amount).toEqual(0) + }); + }); + + it('should return the correct total amount', () => { + result.pipe(skip(1)).subscribe((amount) => { + expect(amount).toEqual(localResults.length) + }); + }); + + it('should not set searchConfig', () => { + expect(service.searchConfig).toBeUndefined(); + }); + }); + + describe('getTotalExternalResults', () => { + let result; + + beforeEach(() => { + result = service.getTotalExternalResults(externalSource, optionsWithQuery); + }); + + it('should start with 0', () => { + result.pipe(take(1)).subscribe((amount) => { + expect(amount).toEqual(0) + }); + }); + + it('should return the correct total amount', () => { + result.pipe(skip(1)).subscribe((amount) => { + expect(amount).toEqual(totalExternal) + }); + }); + }); +}); diff --git a/src/app/core/data/lookup-relation.service.ts b/src/app/core/data/lookup-relation.service.ts new file mode 100644 index 0000000000..ad977e42dc --- /dev/null +++ b/src/app/core/data/lookup-relation.service.ts @@ -0,0 +1,94 @@ +import { ExternalSourceService } from './external-source.service'; +import { SearchService } from '../shared/search/search.service'; +import { concat, map, multicast, startWith, take, takeWhile } from 'rxjs/operators'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { ReplaySubject } from 'rxjs/internal/ReplaySubject'; +import { RemoteData } from './remote-data'; +import { PaginatedList } from './paginated-list'; +import { SearchResult } from '../../shared/search/search-result.model'; +import { DSpaceObject } from '../shared/dspace-object.model'; +import { RelationshipOptions } from '../../shared/form/builder/models/relationship-options.model'; +import { Observable } from 'rxjs/internal/Observable'; +import { Item } from '../shared/item.model'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { getAllSucceededRemoteData, getRemoteDataPayload } from '../shared/operators'; +import { Injectable } from '@angular/core'; +import { ExternalSource } from '../shared/external-source.model'; +import { ExternalSourceEntry } from '../shared/external-source-entry.model'; + +/** + * A service for retrieving local and external entries information during a relation lookup + */ +@Injectable() +export class LookupRelationService { + /** + * The search config last used for retrieving local results + */ + public searchConfig: PaginatedSearchOptions; + + /** + * Pagination options for retrieving exactly one result + */ + private singleResultOptions = Object.assign(new PaginationComponentOptions(), { + id: 'single-result-options', + pageSize: 1 + }); + + constructor(protected externalSourceService: ExternalSourceService, + protected searchService: SearchService) { + } + + /** + * Retrieve the available local entries for a relationship + * @param relationship Relationship options + * @param searchOptions Search options to filter results + * @param setSearchConfig Optionally choose if we should store the used search config in a local variable (defaults to true) + */ + getLocalResults(relationship: RelationshipOptions, searchOptions: PaginatedSearchOptions, setSearchConfig = true): Observable>>> { + const newConfig = Object.assign(new PaginatedSearchOptions({}), searchOptions, + { fixedFilter: relationship.filter, configuration: relationship.searchConfiguration } + ); + if (setSearchConfig) { + this.searchConfig = newConfig; + } + return this.searchService.search(newConfig).pipe( + /* Make sure to only listen to the first x results, until loading is finished */ + /* TODO: in Rxjs 6.4.0 and up, we can replace this with takeWhile(predicate, true) - see https://stackoverflow.com/a/44644237 */ + multicast( + () => new ReplaySubject(1), + (subject) => subject.pipe( + takeWhile((rd: RemoteData>>) => rd.isLoading), + concat(subject.pipe(take(1))) + ) + ) as any + ) as Observable>>>; + } + + /** + * Calculate the total local entries available for the given relationship + * @param relationship Relationship options + * @param searchOptions Search options to filter results + */ + getTotalLocalResults(relationship: RelationshipOptions, searchOptions: PaginatedSearchOptions): Observable { + return this.getLocalResults(relationship, Object.assign(new PaginatedSearchOptions({}), searchOptions, { pagination: this.singleResultOptions }), false).pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + map((results: PaginatedList>) => results.totalElements), + startWith(0) + ); + } + + /** + * Calculate the total external entries available for a given external source + * @param externalSource External Source + * @param searchOptions Search options to filter results + */ + getTotalExternalResults(externalSource: ExternalSource, searchOptions: PaginatedSearchOptions): Observable { + return this.externalSourceService.getExternalSourceEntries(externalSource.id, Object.assign(new PaginatedSearchOptions({}), searchOptions, { pagination: this.singleResultOptions })).pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + map((results: PaginatedList) => results.totalElements), + startWith(0) + ); + } +} diff --git a/src/app/core/data/relationship-type.service.ts b/src/app/core/data/relationship-type.service.ts index 627fc4863f..7978373b08 100644 --- a/src/app/core/data/relationship-type.service.ts +++ b/src/app/core/data/relationship-type.service.ts @@ -2,7 +2,7 @@ import { Injectable } from '@angular/core'; import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; -import { filter, find, map, switchMap, tap } from 'rxjs/operators'; +import { filter, find, map, switchMap } from 'rxjs/operators'; import { configureRequest, getSucceededRemoteData } from '../shared/operators'; import { Observable } from 'rxjs/internal/Observable'; import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; @@ -42,7 +42,7 @@ export class RelationshipTypeService { map((endpointURL: string) => new FindListRequest(this.requestService.generateRequestId(), endpointURL, options)), configureRequest(this.requestService), switchMap(() => this.rdbService.buildList(link$)) - ); + ) as Observable>>; } /** diff --git a/src/app/core/data/relationship.service.ts b/src/app/core/data/relationship.service.ts index d624238bb8..b73657ce2c 100644 --- a/src/app/core/data/relationship.service.ts +++ b/src/app/core/data/relationship.service.ts @@ -3,8 +3,13 @@ import { RequestService } from './request.service'; import { HALEndpointService } from '../shared/hal-endpoint.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; -import { distinctUntilChanged, filter, map, mergeMap, skipWhile, startWith, switchMap, take, tap } from 'rxjs/operators'; -import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators'; +import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators'; +import { + configureRequest, + getRemoteDataPayload, + getResponseFromEntry, + getSucceededRemoteData +} from '../shared/operators'; import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models'; import { Observable } from 'rxjs/internal/Observable'; import { RestResponse } from '../cache/response.models'; @@ -15,7 +20,11 @@ import { RemoteData } from './remote-data'; import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs'; import { PaginatedList } from './paginated-list'; import { ItemDataService } from './item-data.service'; -import { compareArraysUsingIds, paginatedRelationsToItems, relationsToItems } from '../../+item-page/simple/item-types/shared/item-relationships-utils'; +import { + compareArraysUsingIds, + paginatedRelationsToItems, + relationsToItems +} from '../../+item-page/simple/item-types/shared/item-relationships-utils'; import { ObjectCacheService } from '../cache/object-cache.service'; import { DataService } from './data.service'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; @@ -28,7 +37,10 @@ import { SearchParam } from '../cache/models/search-param.model'; import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service'; import { AppState, keySelector } from '../../app.reducer'; import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer'; -import { RemoveNameVariantAction, SetNameVariantAction } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions'; +import { + RemoveNameVariantAction, + SetNameVariantAction +} from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions'; const relationshipListsStateSelector = (state: AppState) => state.relationshipLists; @@ -117,7 +129,7 @@ export class RelationshipService extends DataService { getResponseFromEntry(), tap(() => this.removeRelationshipItemsFromCache(item1)), tap(() => this.removeRelationshipItemsFromCache(item2)) - ); + ) as Observable; } /** diff --git a/src/app/core/data/request.service.spec.ts b/src/app/core/data/request.service.spec.ts index 5807666d66..01560380c2 100644 --- a/src/app/core/data/request.service.spec.ts +++ b/src/app/core/data/request.service.spec.ts @@ -21,6 +21,7 @@ import { } from './request.models'; import { RequestService } from './request.service'; import { TestScheduler } from 'rxjs/testing'; +import { RequestEntry } from './request.reducer'; describe('RequestService', () => { let scheduler: TestScheduler; @@ -107,7 +108,7 @@ describe('RequestService', () => { beforeEach(() => { spyOn(service, 'getByHref').and.returnValue(observableOf({ completed: false - })) + } as RequestEntry)) }); it('should return true', () => { @@ -122,7 +123,7 @@ describe('RequestService', () => { beforeEach(() => { spyOn(service, 'getByHref').and.returnValues(observableOf({ completed: true - })); + } as RequestEntry)); }); it('should return false', () => { @@ -432,7 +433,7 @@ describe('RequestService', () => { let valid; const requestEntry = { completed: false }; beforeEach(() => { - spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry)); + spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry as RequestEntry)); valid = serviceAsAny.isValid(requestEntry); }); it('return an observable emitting false', () => { @@ -444,7 +445,7 @@ describe('RequestService', () => { let valid; const requestEntry = { completed: true, response: { isSuccessful: false } }; beforeEach(() => { - spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry)); + spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry as RequestEntry)); valid = serviceAsAny.isValid(requestEntry); }); it('return an observable emitting false', () => { @@ -470,7 +471,7 @@ describe('RequestService', () => { beforeEach(() => { spyOn(Date.prototype, 'getTime').and.returnValue(now); - spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry)); + spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry as RequestEntry)); valid = serviceAsAny.isValid(requestEntry); }); @@ -497,7 +498,7 @@ describe('RequestService', () => { }; beforeEach(() => { spyOn(Date.prototype, 'getTime').and.returnValue(now); - spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry)); + spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry as RequestEntry)); valid = serviceAsAny.isValid(requestEntry); }); diff --git a/src/app/core/data/request.service.ts b/src/app/core/data/request.service.ts index af9bf54cb8..b811a75549 100644 --- a/src/app/core/data/request.service.ts +++ b/src/app/core/data/request.service.ts @@ -5,8 +5,6 @@ import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { Observable, race as observableRace } from 'rxjs'; import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; import { cloneDeep, remove } from 'lodash'; - -import { AppState } from '../../app.reducer'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { CacheableObject } from '../cache/object-cache.reducer'; import { ObjectCacheService } from '../cache/object-cache.service'; @@ -19,7 +17,7 @@ import { } from '../index/index.selectors'; import { UUIDService } from '../shared/uuid.service'; import { RequestConfigureAction, RequestExecuteAction, RequestRemoveAction } from './request.actions'; -import { GetRequest, RestRequest, SubmissionRequest } from './request.models'; +import { GetRequest, RestRequest } from './request.models'; import { RequestEntry, RequestState } from './request.reducer'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; import { RestRequestMethod } from './rest-request-method'; @@ -52,7 +50,7 @@ const entryFromUUIDSelector = (uuid: string): MemoizedSelector, href: string): MemoizedSelector => createSelector( + (selector: MemoizedSelector, href: string): MemoizedSelector => createSelector( selector, (state: IndexState) => getUuidsFromHrefSubstring(state, href) ); diff --git a/src/app/shared/history/history.actions.ts b/src/app/core/history/history.actions.ts similarity index 91% rename from src/app/shared/history/history.actions.ts rename to src/app/core/history/history.actions.ts index ad970bd350..d4e621cc1c 100644 --- a/src/app/shared/history/history.actions.ts +++ b/src/app/core/history/history.actions.ts @@ -1,6 +1,6 @@ import { Action } from '@ngrx/store'; -import { type } from '../ngrx/type'; +import { type } from '../../shared/ngrx/type'; export const HistoryActionTypes = { ADD_TO_HISTORY: type('dspace/history/ADD_TO_HISTORY'), diff --git a/src/app/shared/history/history.reducer.spec.ts b/src/app/core/history/history.reducer.spec.ts similarity index 100% rename from src/app/shared/history/history.reducer.spec.ts rename to src/app/core/history/history.reducer.spec.ts diff --git a/src/app/shared/history/history.reducer.ts b/src/app/core/history/history.reducer.ts similarity index 100% rename from src/app/shared/history/history.reducer.ts rename to src/app/core/history/history.reducer.ts diff --git a/src/app/core/history/selectors.ts b/src/app/core/history/selectors.ts new file mode 100644 index 0000000000..a04d3839b1 --- /dev/null +++ b/src/app/core/history/selectors.ts @@ -0,0 +1,3 @@ +import { CoreState } from '../core.reducers'; + +export const historySelector = (state: CoreState) => state.history; diff --git a/src/app/core/index/index.selectors.ts b/src/app/core/index/index.selectors.ts index a6d7b7e7b0..de4adab09b 100644 --- a/src/app/core/index/index.selectors.ts +++ b/src/app/core/index/index.selectors.ts @@ -1,5 +1,4 @@ import { createSelector, MemoizedSelector } from '@ngrx/store'; -import { AppState } from '../../app.reducer'; import { hasValue } from '../../shared/empty.util'; import { CoreState } from '../core.reducers'; import { coreSelector } from '../core.selectors'; @@ -11,7 +10,7 @@ import { IndexName, IndexState, MetaIndexState } from './index.reducer'; * @returns * a MemoizedSelector to select the MetaIndexState */ -export const metaIndexSelector: MemoizedSelector = createSelector( +export const metaIndexSelector: MemoizedSelector = createSelector( coreSelector, (state: CoreState) => state.index ); @@ -23,7 +22,7 @@ export const metaIndexSelector: MemoizedSelector = cre * @returns * a MemoizedSelector to select the object index */ -export const objectIndexSelector: MemoizedSelector = createSelector( +export const objectIndexSelector: MemoizedSelector = createSelector( metaIndexSelector, (state: MetaIndexState) => state[IndexName.OBJECT] ); @@ -34,7 +33,7 @@ export const objectIndexSelector: MemoizedSelector = creat * @returns * a MemoizedSelector to select the request index */ -export const requestIndexSelector: MemoizedSelector = createSelector( +export const requestIndexSelector: MemoizedSelector = createSelector( metaIndexSelector, (state: MetaIndexState) => state[IndexName.REQUEST] ); @@ -45,7 +44,7 @@ export const requestIndexSelector: MemoizedSelector = crea * @returns * a MemoizedSelector to select the request UUID mapping */ -export const requestUUIDIndexSelector: MemoizedSelector = createSelector( +export const requestUUIDIndexSelector: MemoizedSelector = createSelector( metaIndexSelector, (state: MetaIndexState) => state[IndexName.UUID_MAPPING] ); @@ -53,14 +52,13 @@ export const requestUUIDIndexSelector: MemoizedSelector = /** * Return the self link of an object in the object-cache based on its UUID * - * @param id + * @param uuid * the UUID for which you want to find the matching self link - * @param identifierType the type of index, used to select index from state * @returns * a MemoizedSelector to select the self link */ export const selfLinkFromUuidSelector = - (uuid: string): MemoizedSelector => createSelector( + (uuid: string): MemoizedSelector => createSelector( objectIndexSelector, (state: IndexState) => hasValue(state) ? state[uuid] : undefined ); @@ -74,7 +72,7 @@ export const selfLinkFromUuidSelector = * a MemoizedSelector to select the UUID */ export const uuidFromHrefSelector = - (href: string): MemoizedSelector => createSelector( + (href: string): MemoizedSelector => createSelector( requestIndexSelector, (state: IndexState) => hasValue(state) ? state[href] : undefined ); @@ -89,7 +87,7 @@ export const uuidFromHrefSelector = * a MemoizedSelector to select the UUID of the cached request */ export const originalRequestUUIDFromRequestUUIDSelector = - (uuid: string): MemoizedSelector => createSelector( + (uuid: string): MemoizedSelector => createSelector( requestUUIDIndexSelector, (state: IndexState) => hasValue(state) ? state[uuid] : undefined ); diff --git a/src/app/core/json-patch/json-patch-operations.service.ts b/src/app/core/json-patch/json-patch-operations.service.ts index 90eaf87a0e..c1b37bf904 100644 --- a/src/app/core/json-patch/json-patch-operations.service.ts +++ b/src/app/core/json-patch/json-patch-operations.service.ts @@ -17,7 +17,6 @@ import { } from './json-patch-operations.actions'; import { JsonPatchOperationModel } from './json-patch.model'; import { getResponseFromEntry } from '../shared/operators'; -import { ObjectCacheEntry } from '../cache/object-cache.reducer'; /** * An abstract class that provides methods to make JSON Patch requests. @@ -88,8 +87,8 @@ export abstract class JsonPatchOperationsService { const [successResponse$, errorResponse$] = partition((response: RestResponse) => response.isSuccessful)(this.requestService.getByUUID(requestId).pipe( getResponseFromEntry(), - find((entry: ObjectCacheEntry) => startTransactionTime < entry.timeAdded), - map((entry: ObjectCacheEntry) => entry), + find((entry: RestResponse) => startTransactionTime < entry.timeAdded), + map((entry: RestResponse) => entry), )); return observableMerge( errorResponse$.pipe( diff --git a/src/app/core/services/route.service.spec.ts b/src/app/core/services/route.service.spec.ts index ae31f28384..525329d50f 100644 --- a/src/app/core/services/route.service.spec.ts +++ b/src/app/core/services/route.service.spec.ts @@ -8,7 +8,7 @@ import { getTestScheduler, hot } from 'jasmine-marbles'; import { RouteService } from './route.service'; import { MockRouter } from '../../shared/mocks/mock-router'; import { TestScheduler } from 'rxjs/testing'; -import { AddUrlToHistoryAction } from '../../shared/history/history.actions'; +import { AddUrlToHistoryAction } from '../history/history.actions'; describe('RouteService', () => { let scheduler: TestScheduler; diff --git a/src/app/core/services/route.service.ts b/src/app/core/services/route.service.ts index b29c491cb0..661f4acf94 100644 --- a/src/app/core/services/route.service.ts +++ b/src/app/core/services/route.service.ts @@ -1,28 +1,17 @@ -import { distinctUntilChanged, filter, map, take, tap } from 'rxjs/operators'; +import { distinctUntilChanged, filter, map, take } from 'rxjs/operators'; import { Injectable } from '@angular/core'; -import { - ActivatedRoute, - NavigationEnd, - Params, - Router, - RouterStateSnapshot, -} from '@angular/router'; +import { ActivatedRoute, NavigationEnd, Params, Router, RouterStateSnapshot, } from '@angular/router'; import { combineLatest, Observable } from 'rxjs'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { isEqual } from 'lodash'; -import { - AddParameterAction, - SetParameterAction, - SetParametersAction, - SetQueryParametersAction -} from './route.actions'; -import { CoreState } from '../../core/core.reducers'; -import { coreSelector } from '../../core/core.selectors'; +import { AddParameterAction, SetParameterAction, SetParametersAction, SetQueryParametersAction } from './route.actions'; +import { CoreState } from '../core.reducers'; +import { coreSelector } from '../core.selectors'; import { hasValue } from '../../shared/empty.util'; -import { historySelector } from '../../shared/history/selectors'; -import { AddUrlToHistoryAction } from '../../shared/history/history.actions'; +import { historySelector } from '../history/selectors'; +import { AddUrlToHistoryAction } from '../history/history.actions'; /** * Selector to select all route parameters from the store @@ -187,10 +176,20 @@ export class RouteService { ); } + /** + * Add a parameter to the current route + * @param key The parameter name + * @param value The parameter value + */ public addParameter(key, value) { this.store.dispatch(new AddParameterAction(key, value)); } + /** + * Set a parameter in the current route (overriding the previous value) + * @param key The parameter name + * @param value The parameter value + */ public setParameter(key, value) { this.store.dispatch(new SetParameterAction(key, value)); } diff --git a/src/app/core/shared/external-source-entry.model.ts b/src/app/core/shared/external-source-entry.model.ts new file mode 100644 index 0000000000..be52f96b07 --- /dev/null +++ b/src/app/core/shared/external-source-entry.model.ts @@ -0,0 +1,43 @@ +import { MetadataMap } from './metadata.models'; +import { ResourceType } from './resource-type'; +import { ListableObject } from '../../shared/object-collection/shared/listable-object.model'; +import { GenericConstructor } from './generic-constructor'; + +/** + * Model class for a single entry from an external source + */ +export class ExternalSourceEntry extends ListableObject { + static type = new ResourceType('externalSourceEntry'); + + /** + * Unique identifier + */ + id: string; + + /** + * The value to display + */ + display: string; + + /** + * The value to store the entry with + */ + value: string; + + /** + * Metadata of the entry + */ + metadata: MetadataMap; + + /** + * The link to the rest endpoint where this External Source Entry can be found + */ + self: string; + + /** + * Method that returns as which type of object this object should be rendered + */ + getRenderTypes(): Array> { + return [this.constructor as GenericConstructor]; + } +} diff --git a/src/app/core/shared/external-source.model.ts b/src/app/core/shared/external-source.model.ts new file mode 100644 index 0000000000..a158f18f5d --- /dev/null +++ b/src/app/core/shared/external-source.model.ts @@ -0,0 +1,29 @@ +import { ResourceType } from './resource-type'; +import { CacheableObject } from '../cache/object-cache.reducer'; + +/** + * Model class for an external source + */ +export class ExternalSource extends CacheableObject { + static type = new ResourceType('externalsource'); + + /** + * Unique identifier + */ + id: string; + + /** + * The name of this external source + */ + name: string; + + /** + * Is the source hierarchical? + */ + hierarchical: boolean; + + /** + * The link to the rest endpoint where this External Source can be found + */ + self: string; +} diff --git a/src/app/entity-groups/research-entities/research-entities.module.ts b/src/app/entity-groups/research-entities/research-entities.module.ts index 86c2a375da..cef3b4539b 100644 --- a/src/app/entity-groups/research-entities/research-entities.module.ts +++ b/src/app/entity-groups/research-entities/research-entities.module.ts @@ -25,6 +25,7 @@ import { PersonInputSuggestionsComponent } from './submission/item-list-elements import { NameVariantModalComponent } from './submission/name-variant-modal/name-variant-modal.component'; import { OrgUnitInputSuggestionsComponent } from './submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component'; import { OrgUnitSearchResultListSubmissionElementComponent } from './submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component'; +import { ExternalSourceEntryListSubmissionElementComponent } from './submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component'; const ENTRY_COMPONENTS = [ OrgUnitComponent, @@ -48,7 +49,8 @@ const ENTRY_COMPONENTS = [ PersonInputSuggestionsComponent, NameVariantModalComponent, OrgUnitSearchResultListSubmissionElementComponent, - OrgUnitInputSuggestionsComponent + OrgUnitInputSuggestionsComponent, + ExternalSourceEntryListSubmissionElementComponent ]; @NgModule({ diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html new file mode 100644 index 0000000000..55b8f38a5e --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html @@ -0,0 +1,2 @@ +
{{object.display}}
+ diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts new file mode 100644 index 0000000000..fa153b8c5e --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts @@ -0,0 +1,47 @@ +import { ExternalSourceEntryListSubmissionElementComponent } from './external-source-entry-list-submission-element.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ExternalSourceEntry } from '../../../../../core/shared/external-source-entry.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('ExternalSourceEntryListSubmissionElementComponent', () => { + let component: ExternalSourceEntryListSubmissionElementComponent; + let fixture: ComponentFixture; + + const uri = 'https://orcid.org/0001-0001-0001-0001'; + const entry = Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0001', + display: 'John Doe', + value: 'John, Doe', + metadata: { + 'dc.identifier.uri': [ + { + value: uri + } + ] + } + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ExternalSourceEntryListSubmissionElementComponent], + imports: [TranslateModule.forRoot()], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExternalSourceEntryListSubmissionElementComponent); + component = fixture.componentInstance; + component.object = entry; + fixture.detectChanges(); + }); + + it('should display the entry\'s display value', () => { + expect(fixture.nativeElement.textContent).toContain(entry.display); + }); + + it('should display the entry\'s uri', () => { + expect(fixture.nativeElement.textContent).toContain(uri); + }); +}); diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts new file mode 100644 index 0000000000..c0512b4995 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts @@ -0,0 +1,28 @@ +import { AbstractListableElementComponent } from '../../../../../shared/object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { ExternalSourceEntry } from '../../../../../core/shared/external-source-entry.model'; +import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { Context } from '../../../../../core/shared/context.model'; +import { Component, OnInit } from '@angular/core'; +import { Metadata } from '../../../../../core/shared/metadata.utils'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; + +@listableObjectComponent(ExternalSourceEntry, ViewMode.ListElement, Context.SubmissionModal) +@Component({ + selector: 'ds-external-source-entry-list-submission-element', + styleUrls: ['./external-source-entry-list-submission-element.component.scss'], + templateUrl: './external-source-entry-list-submission-element.component.html' +}) +/** + * The component for displaying a list element of an external source entry + */ +export class ExternalSourceEntryListSubmissionElementComponent extends AbstractListableElementComponent implements OnInit { + /** + * The metadata value for the object's uri + */ + uri: MetadataValue; + + ngOnInit(): void { + this.uri = Metadata.first(this.object.metadata, 'dc.identifier.uri'); + } +} diff --git a/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts index 75817d786a..eb6f7d01ac 100644 --- a/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts +++ b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts @@ -10,7 +10,13 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; templateUrl: './name-variant-modal.component.html', styleUrls: ['./name-variant-modal.component.scss'] }) +/** + * The component for the modal to add a name variant to an item + */ export class NameVariantModalComponent { + /** + * The name variant + */ @Input() value: string; constructor(public modal: NgbActiveModal) { diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html index 4df07880d8..86de30c23e 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html @@ -1,6 +1,6 @@