Merge remote-tracking branch 'community/master' into mydspace-display-entity-type

This commit is contained in:
Ben Bosman
2020-01-16 14:50:58 +01:00
157 changed files with 7303 additions and 1743 deletions

View File

@@ -1,5 +1,5 @@
sudo: required sudo: required
dist: trusty dist: bionic
env: env:
# Install the latest docker-compose version for ci testing. # Install the latest docker-compose version for ci testing.
@@ -12,6 +12,9 @@ env:
DSPACE_REST_NAMESPACE: '/server/api' DSPACE_REST_NAMESPACE: '/server/api'
DSPACE_REST_SSL: false DSPACE_REST_SSL: false
services:
- xvfb
before_install: before_install:
# Docker Compose Install # Docker Compose Install
- curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose - 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: after_script:
- docker-compose -f ./docker/docker-compose-travis.yml down - docker-compose -f ./docker/docker-compose-travis.yml down
addons:
apt:
sources:
- google-chrome
packages:
- dpkg
- google-chrome-stable
language: node_js language: node_js
node_js: node_js:
@@ -53,8 +48,6 @@ cache:
bundler_args: --retry 5 bundler_args: --retry 5
script: script:
# Use Chromium instead of Chrome.
- export CHROME_BIN=chromium-browser
- yarn run build - yarn run build
- yarn run ci - yarn run ci
- cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js - cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js

View File

@@ -11,6 +11,7 @@
"node": "8.* || >= 10.*" "node": "8.* || >= 10.*"
}, },
"resolutions": { "resolutions": {
"serialize-javascript": ">= 2.1.2",
"set-value": ">= 2.0.1" "set-value": ">= 2.0.1"
}, },
"scripts": { "scripts": {
@@ -74,38 +75,38 @@
"sync-i18n": "node ./scripts/sync-i18n-files.js" "sync-i18n": "node ./scripts/sync-i18n-files.js"
}, },
"dependencies": { "dependencies": {
"@angular/animations": "^6.1.4", "@angular/animations": "^7.2.15",
"@angular/cdk": "^6.4.7", "@angular/cdk": "7.3.7",
"@angular/cli": "^6.1.5", "@angular/cli": "^7.3.5",
"@angular/common": "^6.1.4", "@angular/common": "^7.2.15",
"@angular/core": "^6.1.4", "@angular/core": "^7.2.15",
"@angular/forms": "^6.1.4", "@angular/forms": "^7.2.15",
"@angular/http": "^6.1.4", "@angular/http": "^7.2.15",
"@angular/platform-browser": "^6.1.4", "@angular/platform-browser": "^7.2.15",
"@angular/platform-browser-dynamic": "^6.1.4", "@angular/platform-browser-dynamic": "^7.2.15",
"@angular/platform-server": "^6.1.4", "@angular/platform-server": "^7.2.15",
"@angular/router": "^6.1.4", "@angular/router": "^7.2.15",
"@angularclass/bootloader": "1.0.1", "@angularclass/bootloader": "1.0.1",
"@ng-bootstrap/ng-bootstrap": "^2.0.0", "@ng-bootstrap/ng-bootstrap": "^4.1.0",
"@ng-dynamic-forms/core": "6.2.0", "@ng-dynamic-forms/core": "^7.1.0",
"@ng-dynamic-forms/ui-ng-bootstrap": "6.2.0", "@ng-dynamic-forms/ui-ng-bootstrap": "^7.1.0",
"@ngrx/effects": "^6.1.0", "@ngrx/effects": "^7.3.0",
"@ngrx/router-store": "^6.1.0", "@ngrx/router-store": "^7.3.0",
"@ngrx/store": "^6.1.0", "@ngrx/store": "^7.3.0",
"@nguniversal/express-engine": "6.1.0", "@nguniversal/express-engine": "^7.1.1",
"@ngx-translate/core": "10.0.2", "@ngx-translate/core": "11.0.1",
"@ngx-translate/http-loader": "3.0.1", "@ngx-translate/http-loader": "4.0.0",
"@nicky-lenaers/ngx-scroll-to": "^1.0.0", "@nicky-lenaers/ngx-scroll-to": "^1.0.0",
"angular-idle-preload": "3.0.0", "angular-idle-preload": "3.0.0",
"angular-sortablejs": "^2.5.0", "angular-sortablejs": "^2.5.0",
"angular2-text-mask": "9.0.0", "angular2-text-mask": "9.0.0",
"angulartics2": "^6.2.0", "angulartics2": "7.5.2",
"body-parser": "1.18.2", "body-parser": "1.18.2",
"bootstrap": "4.3.1", "bootstrap": "4.3.1",
"cerialize": "0.1.18", "cerialize": "0.1.18",
"compression": "1.7.1", "compression": "1.7.1",
"cookie-parser": "1.4.3", "cookie-parser": "1.4.3",
"core-js": "^2.5.7", "core-js": "^2.6.5",
"debug-loader": "^0.0.1", "debug-loader": "^0.0.1",
"express": "4.16.2", "express": "4.16.2",
"express-session": "1.15.6", "express-session": "1.15.6",
@@ -113,6 +114,7 @@
"file-saver": "^1.3.8", "file-saver": "^1.3.8",
"font-awesome": "4.7.0", "font-awesome": "4.7.0",
"fork-ts-checker-webpack-plugin": "^0.4.10", "fork-ts-checker-webpack-plugin": "^0.4.10",
"hammerjs": "^2.0.8",
"http-server": "0.11.1", "http-server": "0.11.1",
"https": "1.0.0", "https": "1.0.0",
"js-cookie": "2.2.0", "js-cookie": "2.2.0",
@@ -122,18 +124,19 @@
"jwt-decode": "^2.2.0", "jwt-decode": "^2.2.0",
"methods": "1.1.2", "methods": "1.1.2",
"moment": "^2.22.1", "moment": "^2.22.1",
"moment-range": "^4.0.2",
"morgan": "^1.9.1", "morgan": "^1.9.1",
"ng-mocks": "^6.2.1", "ng-mocks": "^7.6.0",
"ng2-file-upload": "1.2.1", "ng2-file-upload": "1.2.1",
"ng2-nouislider": "^1.7.11", "ng2-nouislider": "^1.8.2",
"ngx-bootstrap": "^3.2.0", "ngx-bootstrap": "^3.2.0",
"ngx-infinite-scroll": "6.0.1", "ngx-infinite-scroll": "6.0.1",
"ngx-moment": "^3.1.0", "ngx-moment": "^3.4.0",
"ngx-pagination": "3.0.3", "ngx-pagination": "3.0.3",
"nouislider": "^11.0.0", "nouislider": "^11.0.0",
"pem": "1.13.2", "pem": "1.13.2",
"reflect-metadata": "0.1.12", "reflect-metadata": "0.1.12",
"rxjs": "6.2.2", "rxjs": "6.4.0",
"rxjs-spy": "^7.5.1", "rxjs-spy": "^7.5.1",
"sass-resources-loader": "^2.0.0", "sass-resources-loader": "^2.0.0",
"sortablejs": "1.7.0", "sortablejs": "1.7.0",
@@ -143,17 +146,18 @@
"url-parse": "^1.4.7", "url-parse": "^1.4.7",
"uuid": "^3.2.1", "uuid": "^3.2.1",
"webfontloader": "1.6.28", "webfontloader": "1.6.28",
"webpack-cli": "^3.1.0", "webpack-cli": "^3.2.0",
"zone.js": "^0.8.26" "zone.js": "^0.8.29"
}, },
"devDependencies": { "devDependencies": {
"@angular/compiler": "^6.1.4", "@angular-devkit/build-angular": "^0.13.5",
"@angular/compiler-cli": "^6.1.4", "@angular/compiler": "^7.2.15",
"@angular/compiler-cli": "^7.2.15",
"@fortawesome/fontawesome-free": "^5.5.0", "@fortawesome/fontawesome-free": "^5.5.0",
"@ngrx/entity": "^6.1.0", "@ngrx/entity": "^7.3.0",
"@ngrx/schematics": "^6.1.0", "@ngrx/schematics": "^7.3.0",
"@ngrx/store-devtools": "^6.1.0", "@ngrx/store-devtools": "^7.3.0",
"@ngtools/webpack": "^6.1.5", "@ngtools/webpack": "^7.3.9",
"@schematics/angular": "^0.7.5", "@schematics/angular": "^0.7.5",
"@types/acorn": "^4.0.3", "@types/acorn": "^4.0.3",
"@types/cookie-parser": "1.4.1", "@types/cookie-parser": "1.4.1",
@@ -162,44 +166,47 @@
"@types/express-serve-static-core": "4.16.0", "@types/express-serve-static-core": "4.16.0",
"@types/file-saver": "^1.3.0", "@types/file-saver": "^1.3.0",
"@types/hammerjs": "2.0.35", "@types/hammerjs": "2.0.35",
"@types/jasmine": "^2.8.6", "@types/jasmine": "^3.3.9",
"@types/js-cookie": "2.1.0", "@types/js-cookie": "2.1.0",
"@types/json5": "^0.0.30", "@types/json5": "^0.0.30",
"@types/lodash": "^4.14.110", "@types/lodash": "^4.14.110",
"@types/memory-cache": "0.2.0", "@types/memory-cache": "0.2.0",
"@types/mime": "2.0.0", "@types/mime": "2.0.0",
"@types/node": "^10.9.4", "@types/node": "^11.11.2",
"@types/serve-static": "1.13.2", "@types/serve-static": "1.13.2",
"@types/uuid": "^3.4.3", "@types/uuid": "^3.4.3",
"@types/webfontloader": "1.6.29", "@types/webfontloader": "1.6.29",
"@typescript-eslint/eslint-plugin": "^2.12.0",
"@typescript-eslint/parser": "^2.12.0",
"ajv": "^6.1.1", "ajv": "^6.1.1",
"ajv-keywords": "^3.1.0", "ajv-keywords": "^3.1.0",
"angular2-template-loader": "0.6.2", "angular2-template-loader": "0.6.2",
"autoprefixer": "^9.1.3", "autoprefixer": "^9.1.3",
"caniuse-lite": "^1.0.30000697", "caniuse-lite": "^1.0.30000697",
"cli-progress": "^3.3.1", "cli-progress": "^3.3.1",
"codelyzer": "^4.4.4", "codelyzer": "^5.1.0",
"commander": "^3.0.2", "commander": "^3.0.2",
"compression-webpack-plugin": "^1.1.6", "compression-webpack-plugin": "^3.0.1",
"copy-webpack-plugin": "^4.4.1", "copy-webpack-plugin": "^5.1.1",
"copyfiles": "^2.1.1", "copyfiles": "^2.1.1",
"coveralls": "3.0.0", "coveralls": "3.0.0",
"css-loader": "1.0.0", "css-loader": "3.4.0",
"cssnano": "^4.1.10", "cssnano": "^4.1.10",
"deep-freeze": "0.0.1", "deep-freeze": "0.0.1",
"eslint": "^6.7.2",
"exports-loader": "^0.7.0", "exports-loader": "^0.7.0",
"html-webpack-plugin": "^4.0.0-alpha", "html-webpack-plugin": "3.2.0",
"imports-loader": "0.8.0", "imports-loader": "0.8.0",
"istanbul-instrumenter-loader": "3.0.1", "istanbul-instrumenter-loader": "3.0.1",
"jasmine-core": "^3.2.1", "jasmine-core": "^3.3.0",
"jasmine-marbles": "0.3.1", "jasmine-marbles": "0.3.1",
"jasmine-spec-reporter": "4.2.1", "jasmine-spec-reporter": "4.2.1",
"karma": "3.0.0", "karma": "4.0.1",
"karma-chrome-launcher": "2.2.0", "karma-chrome-launcher": "2.2.0",
"karma-cli": "1.0.1", "karma-cli": "2.0.0",
"karma-coverage": "1.1.2", "karma-coverage": "1.1.2",
"karma-istanbul-preprocessor": "0.0.2", "karma-istanbul-preprocessor": "0.0.2",
"karma-jasmine": "1.1.2", "karma-jasmine": "2.0.1",
"karma-mocha-reporter": "2.2.5", "karma-mocha-reporter": "2.2.5",
"karma-phantomjs-launcher": "1.0.4", "karma-phantomjs-launcher": "1.0.4",
"karma-remap-coverage": "^0.1.5", "karma-remap-coverage": "^0.1.5",
@@ -223,26 +230,26 @@
"protractor": "^5.4.2", "protractor": "^5.4.2",
"protractor-istanbul-plugin": "2.0.0", "protractor-istanbul-plugin": "2.0.0",
"raw-loader": "0.5.1", "raw-loader": "0.5.1",
"resolve-url-loader": "^2.3.0",
"rimraf": "2.6.2", "rimraf": "2.6.2",
"rollup": "^0.65.0", "rollup": "^0.65.0",
"rollup-plugin-commonjs": "^9.1.6", "rollup-plugin-commonjs": "^9.1.6",
"rollup-plugin-node-globals": "1.2.1", "rollup-plugin-node-globals": "1.2.1",
"rollup-plugin-node-resolve": "^3.0.3", "rollup-plugin-node-resolve": "^3.0.3",
"rollup-plugin-terser": "^2.0.2", "rollup-plugin-terser": "^2.0.2",
"sass-loader": "^7.1.0", "sass-loader": "7.3.1",
"script-ext-html-webpack-plugin": "2.0.1", "script-ext-html-webpack-plugin": "2.1.4",
"source-map": "0.7.3", "source-map": "0.7.3",
"source-map-loader": "0.2.4", "source-map-loader": "0.2.4",
"string-replace-loader": "^2.1.1", "string-replace-loader": "^2.1.1",
"terser-webpack-plugin": "^2.3.1",
"to-string-loader": "1.1.5", "to-string-loader": "1.1.5",
"ts-helpers": "1.1.2", "ts-helpers": "1.1.2",
"ts-node": "4.1.0", "ts-node": "4.1.0",
"tslint": "5.11.0", "tslint": "5.11.0",
"typedoc": "^0.9.0", "typedoc": "^0.9.0",
"typescript": "^2.9.1", "typescript": "3.1.6",
"webdriver-manager": "^12.1.7", "webdriver-manager": "^12.1.7",
"webpack": "^4.17.1", "webpack": "^4.29.6",
"webpack-bundle-analyzer": "^3.3.2", "webpack-bundle-analyzer": "^3.3.2",
"webpack-dev-middleware": "3.2.0", "webpack-dev-middleware": "3.2.0",
"webpack-dev-server": "^3.1.11", "webpack-dev-server": "^3.1.11",

View File

@@ -244,6 +244,8 @@
"collection.create.head": "Create a Collection", "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.create.sub-head": "Create a Collection for Community {{ parent }}",
"collection.delete.cancel": "Cancel", "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.abstract": "Short Description",
"collection.form.description": "Introductory text (HTML)", "collection.form.description": "Introductory text (HTML)",
@@ -350,6 +392,8 @@
"community.create.head": "Create a Community", "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.create.sub-head": "Create a Sub-Community for Community {{ parent }}",
"community.delete.cancel": "Cancel", "community.delete.cancel": "Cancel",
@@ -368,6 +412,44 @@
"community.edit.head": "Edit Community", "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.abstract": "Short Description",
"community.form.description": "Introductory text (HTML)", "community.form.description": "Introductory text (HTML)",
@@ -1484,6 +1566,8 @@
"search.results.no-results-link": "quotes around it", "search.results.no-results-link": "quotes around it",
"search.results.empty": "Your search returned no results.",
"search.sidebar.close": "Back to 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.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", "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.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.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", "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.drag-message": "Drag & Drop your files here",
"uploader.or": ", or", "uploader.or": ", or ",
"uploader.processing": "Processing", "uploader.processing": "Processing",

View File

@@ -1,7 +1,6 @@
import { MetadataRegistryComponent } from './metadata-registry.component'; import { MetadataRegistryComponent } from './metadata-registry.component';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { By } from '@angular/platform-browser'; 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 { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
import { RestResponse } from '../../../core/cache/response.models'; import { RestResponse } from '../../../core/cache/response.models';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
describe('MetadataRegistryComponent', () => { describe('MetadataRegistryComponent', () => {
let comp: MetadataRegistryComponent; let comp: MetadataRegistryComponent;
@@ -101,12 +101,12 @@ describe('MetadataRegistryComponent', () => {
it('should start editing the selected schema', async(() => { it('should start editing the selected schema', async(() => {
fixture.whenStable().then(() => { 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(() => { 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'); spyOn(registryService, 'cancelEditMetadataSchema');
row.click(); row.click();
fixture.detectChanges(); fixture.detectChanges();
@@ -121,7 +121,7 @@ describe('MetadataRegistryComponent', () => {
beforeEach(() => { beforeEach(() => {
spyOn(registryService, 'deleteMetadataSchema').and.callThrough(); spyOn(registryService, 'deleteMetadataSchema').and.callThrough();
spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas)); spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas as MetadataSchema[]));
comp.deleteSchemas(); comp.deleteSchemas();
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -1,7 +1,6 @@
import { MetadataSchemaComponent } from './metadata-schema.component'; import { MetadataSchemaComponent } from './metadata-schema.component';
import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginatedList } from '../../../core/data/paginated-list';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
@@ -22,6 +21,7 @@ import { NotificationsServiceStub } from '../../../shared/testing/notifications-
import { RestResponse } from '../../../core/cache/response.models'; import { RestResponse } from '../../../core/cache/response.models';
import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils';
import { MetadataField } from '../../../core/metadata/metadata-field.model';
describe('MetadataSchemaComponent', () => { describe('MetadataSchemaComponent', () => {
let comp: MetadataSchemaComponent; let comp: MetadataSchemaComponent;
@@ -152,12 +152,12 @@ describe('MetadataSchemaComponent', () => {
it('should start editing the selected field', async(() => { it('should start editing the selected field', async(() => {
fixture.whenStable().then(() => { 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(() => { 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'); spyOn(registryService, 'cancelEditMetadataField');
row.click(); row.click();
fixture.detectChanges(); fixture.detectChanges();
@@ -172,7 +172,7 @@ describe('MetadataSchemaComponent', () => {
beforeEach(() => { beforeEach(() => {
spyOn(registryService, 'deleteMetadataField').and.callThrough(); spyOn(registryService, 'deleteMetadataField').and.callThrough();
spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields)); spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields as MetadataField[]));
comp.deleteFields(); comp.deleteFields();
fixture.detectChanges(); fixture.detectChanges();
}); });

View File

@@ -1,9 +1,19 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; import {
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; DynamicFormControlModel,
DynamicFormService,
DynamicInputModel,
DynamicTextAreaModel
} from '@ng-dynamic-forms/core';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component'; 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 * Form used for creating and editing collections
@@ -22,7 +32,7 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> {
/** /**
* @type {Collection.type} This is a collection-type form * @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 * The dynamic form fields used for creating/editing a collection
@@ -65,4 +75,15 @@ export class CollectionFormComponent extends ComColFormComponent<Collection> {
name: 'dc.description.provenance', 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);
}
} }

View File

@@ -5,7 +5,6 @@ import { CollectionPageComponent } from './collection-page.component';
import { CollectionPageResolver } from './collection-page.resolver'; import { CollectionPageResolver } from './collection-page.resolver';
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; 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 { CreateCollectionPageGuard } from './create-collection-page/create-collection-page.guard';
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component'; import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
import { URLCombiner } from '../core/url-combiner/url-combiner'; import { URLCombiner } from '../core/url-combiner/url-combiner';
@@ -39,12 +38,8 @@ const COLLECTION_EDIT_PATH = ':id/edit';
}, },
{ {
path: COLLECTION_EDIT_PATH, path: COLLECTION_EDIT_PATH,
pathMatch: 'full', loadChildren: './edit-collection-page/edit-collection-page.module#EditCollectionPageModule',
component: EditCollectionPageComponent, canActivate: [AuthenticatedGuard]
canActivate: [AuthenticatedGuard],
resolve: {
dso: CollectionPageResolver
}
}, },
{ {
path: ':id/delete', path: ':id/delete',

View File

@@ -5,15 +5,17 @@
<div *ngIf="collectionRD?.payload as collection"> <div *ngIf="collectionRD?.payload as collection">
<ds-view-tracker [object]="collection"></ds-view-tracker> <ds-view-tracker [object]="collection"></ds-view-tracker>
<header class="comcol-header border-bottom mb-4 pb-4"> <header class="comcol-header border-bottom mb-4 pb-4">
<!-- Collection logo -->
<ds-comcol-page-logo *ngIf="logoRD$"
[logo]="(logoRD$ | async)?.payload" [alternateText]="'Collection Logo'">
[alternateText]="'Collection Logo'">
</ds-comcol-page-logo>
<!-- Collection Name --> <!-- Collection Name -->
<ds-comcol-page-header <ds-comcol-page-header
[name]="collection.name"> [name]="collection.name">
</ds-comcol-page-header> </ds-comcol-page-header>
<!-- Collection logo -->
<ds-comcol-page-logo *ngIf="logoRD$"
[logo]="(logoRD$ | async)?.payload"
[alternateText]="'Collection Logo'"
[alternateText]="'Collection Logo'">
</ds-comcol-page-logo>
<!-- Handle --> <!-- Handle -->
<ds-comcol-page-handle <ds-comcol-page-handle
[content]="collection.handle" [content]="collection.handle"

View File

@@ -7,7 +7,6 @@ import { CollectionPageComponent } from './collection-page.component';
import { CollectionPageRoutingModule } from './collection-page-routing.module'; import { CollectionPageRoutingModule } from './collection-page-routing.module';
import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component'; import { CreateCollectionPageComponent } from './create-collection-page/create-collection-page.component';
import { CollectionFormComponent } from './collection-form/collection-form.component'; import { CollectionFormComponent } from './collection-form/collection-form.component';
import { EditCollectionPageComponent } from './edit-collection-page/edit-collection-page.component';
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component'; import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component'; import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
import { SearchService } from '../core/shared/search/search.service'; import { SearchService } from '../core/shared/search/search.service';
@@ -23,11 +22,13 @@ import { StatisticsModule } from '../statistics/statistics.module';
declarations: [ declarations: [
CollectionPageComponent, CollectionPageComponent,
CreateCollectionPageComponent, CreateCollectionPageComponent,
EditCollectionPageComponent,
DeleteCollectionPageComponent, DeleteCollectionPageComponent,
CollectionFormComponent, CollectionFormComponent,
CollectionItemMapperComponent CollectionItemMapperComponent
], ],
exports: [
CollectionFormComponent
],
providers: [ providers: [
SearchService, SearchService,
] ]

View File

@@ -4,5 +4,5 @@
<h2 id="sub-header" class="border-bottom pb-2">{{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}</h2> <h2 id="sub-header" class="border-bottom pb-2">{{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}</h2>
</div> </div>
</div> </div>
<ds-collection-form (submitForm)="onSubmit($event)"></ds-collection-form> <ds-collection-form (submitForm)="onSubmit($event)" (finish)="navigateToNewPage()"></ds-collection-form>
</div> </div>

View File

@@ -10,6 +10,8 @@ import { CollectionDataService } from '../../core/data/collection-data.service';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { CreateCollectionPageComponent } from './create-collection-page.component'; import { CreateCollectionPageComponent } from './create-collection-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
describe('CreateCollectionPageComponent', () => { describe('CreateCollectionPageComponent', () => {
let comp: CreateCollectionPageComponent; let comp: CreateCollectionPageComponent;
@@ -27,6 +29,7 @@ describe('CreateCollectionPageComponent', () => {
}, },
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
{ provide: Router, useValue: {} }, { provide: Router, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -5,6 +5,8 @@ import { Router } from '@angular/router';
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component'; import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { CollectionDataService } from '../../core/data/collection-data.service'; 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 * 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<Collection> { export class CreateCollectionPageComponent extends CreateComColPageComponent<Collection> {
protected frontendURL = '/collections/'; protected frontendURL = '/collections/';
protected type = Collection.type;
public constructor( public constructor(
protected communityDataService: CommunityDataService, protected communityDataService: CommunityDataService,
protected collectionDataService: CollectionDataService, protected collectionDataService: CollectionDataService,
protected routeService: RouteService, 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);
} }
} }

View File

@@ -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 */
}

View File

@@ -0,0 +1,6 @@
<ds-collection-form (submitForm)="onSubmit($event)"
[dso]="(dsoRD$ | async)?.payload"
(finish)="navigateToHomePage()"></ds-collection-form>
<a class="btn btn-danger"
[routerLink]="'/collections/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'collection.edit.delete'
| translate}}</a>

View File

@@ -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<CollectionMetadataComponent>;
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/');
})
});
});

View File

@@ -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<Collection> {
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);
}
}

View File

@@ -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 */
}

View File

@@ -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 */
}

View File

@@ -1,11 +0,0 @@
<div class="container">
<div class="row">
<div class="col-12 pb-4">
<h2 id="header" class="border-bottom pb-2">{{ 'collection.edit.head' | translate }}</h2>
<ds-collection-form (submitForm)="onSubmit($event)" [dso]="(dsoRD$ | async)?.payload"></ds-collection-form>
<a class="btn btn-danger"
[routerLink]="'/collections/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'collection.edit.delete'
| translate}}</a>
</div>
</div>
</div>

View File

@@ -13,13 +13,29 @@ describe('EditCollectionPageComponent', () => {
let comp: EditCollectionPageComponent; let comp: EditCollectionPageComponent;
let fixture: ComponentFixture<EditCollectionPageComponent>; let fixture: ComponentFixture<EditCollectionPageComponent>;
const routeStub = {
data: observableOf({
dso: { payload: {} }
}),
routeConfig: {
children: []
},
snapshot: {
firstChild: {
routeConfig: {
path: 'mockUrl'
}
}
}
};
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
declarations: [EditCollectionPageComponent], declarations: [EditCollectionPageComponent],
providers: [ providers: [
{ provide: CollectionDataService, useValue: {} }, { provide: CollectionDataService, useValue: {} },
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } }, { provide: ActivatedRoute, useValue: routeStub },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -31,9 +47,9 @@ describe('EditCollectionPageComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
describe('frontendURL', () => { describe('type', () => {
it('should have the right frontendURL set', () => { it('should have the right type set', () => {
expect((comp as any).frontendURL).toEqual('/collections/'); expect((comp as any).type).toEqual('collection');
}) })
}); });
}); });

View File

@@ -2,24 +2,30 @@ import { Component } from '@angular/core';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component'; import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component';
import { Collection } from '../../core/shared/collection.model'; 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 that represents the page where a user can edit an existing Collection
*/ */
@Component({ @Component({
selector: 'ds-edit-collection', selector: 'ds-edit-collection',
styleUrls: ['./edit-collection-page.component.scss'], templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html'
templateUrl: './edit-collection-page.component.html'
}) })
export class EditCollectionPageComponent extends EditComColPageComponent<Collection> { export class EditCollectionPageComponent extends EditComColPageComponent<Collection> {
protected frontendURL = '/collections/'; type = 'collection';
public constructor( public constructor(
protected collectionDataService: CollectionDataService,
protected router: Router, protected router: Router,
protected route: ActivatedRoute 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)
} }
} }

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -1,9 +1,19 @@
import { Component, Input } from '@angular/core'; import { Component, Input } from '@angular/core';
import { DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; import {
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; DynamicFormControlModel,
DynamicFormService,
DynamicInputModel,
DynamicTextAreaModel
} from '@ng-dynamic-forms/core';
import { Community } from '../../core/shared/community.model'; 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 { 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 * Form used for creating and editing communities
@@ -22,7 +32,7 @@ export class CommunityFormComponent extends ComColFormComponent<Community> {
/** /**
* @type {Community.type} This is a community-type form * @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 * The dynamic form fields used for creating/editing a community
@@ -57,4 +67,15 @@ export class CommunityFormComponent extends ComColFormComponent<Community> {
name: 'dc.description.tableofcontents', 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);
}
} }

View File

@@ -5,7 +5,6 @@ import { CommunityPageComponent } from './community-page.component';
import { CommunityPageResolver } from './community-page.resolver'; import { CommunityPageResolver } from './community-page.resolver';
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
import { AuthenticatedGuard } from '../core/auth/authenticated.guard'; 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 { CreateCommunityPageGuard } from './create-community-page/create-community-page.guard';
import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
import { URLCombiner } from '../core/url-combiner/url-combiner'; import { URLCombiner } from '../core/url-combiner/url-combiner';
@@ -38,12 +37,8 @@ const COMMUNITY_EDIT_PATH = ':id/edit';
}, },
{ {
path: COMMUNITY_EDIT_PATH, path: COMMUNITY_EDIT_PATH,
pathMatch: 'full', loadChildren: './edit-community-page/edit-community-page.module#EditCommunityPageModule',
component: EditCommunityPageComponent, canActivate: [AuthenticatedGuard]
canActivate: [AuthenticatedGuard],
resolve: {
dso: CommunityPageResolver
}
}, },
{ {
path: ':id/delete', path: ':id/delete',

View File

@@ -3,12 +3,11 @@
<div *ngIf="communityRD?.payload; let communityPayload"> <div *ngIf="communityRD?.payload; let communityPayload">
<ds-view-tracker [object]="communityPayload"></ds-view-tracker> <ds-view-tracker [object]="communityPayload"></ds-view-tracker>
<header class="comcol-header border-bottom mb-4 pb-4"> <header class="comcol-header border-bottom mb-4 pb-4">
<!-- Community name -->
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
<!-- Community logo --> <!-- Community logo -->
<ds-comcol-page-logo *ngIf="logoRD$" [logo]="(logoRD$ | async)?.payload" [alternateText]="'Community Logo'"> <ds-comcol-page-logo *ngIf="logoRD$" [logo]="(logoRD$ | async)?.payload" [alternateText]="'Community Logo'">
</ds-comcol-page-logo> </ds-comcol-page-logo>
<!-- Community name -->
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
<!-- Handle --> <!-- Handle -->
<ds-comcol-page-handle [content]="communityPayload.handle" [title]="'community.page.handle'"> <ds-comcol-page-handle [content]="communityPayload.handle" [title]="'community.page.handle'">
</ds-comcol-page-handle> </ds-comcol-page-handle>

View File

@@ -9,7 +9,6 @@ import { CommunityPageRoutingModule } from './community-page-routing.module';
import { CommunityPageSubCommunityListComponent } from './sub-community-list/community-page-sub-community-list.component'; import { CommunityPageSubCommunityListComponent } from './sub-community-list/community-page-sub-community-list.component';
import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component'; import { CreateCommunityPageComponent } from './create-community-page/create-community-page.component';
import { CommunityFormComponent } from './community-form/community-form.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 { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component';
import { StatisticsModule } from '../statistics/statistics.module'; import { StatisticsModule } from '../statistics/statistics.module';
@@ -25,9 +24,11 @@ import { StatisticsModule } from '../statistics/statistics.module';
CommunityPageSubCollectionListComponent, CommunityPageSubCollectionListComponent,
CommunityPageSubCommunityListComponent, CommunityPageSubCommunityListComponent,
CreateCommunityPageComponent, CreateCommunityPageComponent,
EditCommunityPageComponent,
DeleteCommunityPageComponent, DeleteCommunityPageComponent,
CommunityFormComponent CommunityFormComponent
],
exports: [
CommunityFormComponent
] ]
}) })

View File

@@ -7,5 +7,5 @@
</ng-container> </ng-container>
</div> </div>
</div> </div>
<ds-community-form (submitForm)="onSubmit($event)"></ds-community-form> <ds-community-form (submitForm)="onSubmit($event)" (finish)="navigateToNewPage()"></ds-community-form>
</div> </div>

View File

@@ -10,6 +10,8 @@ import { CollectionDataService } from '../../core/data/collection-data.service';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { CreateCommunityPageComponent } from './create-community-page.component'; import { CreateCommunityPageComponent } from './create-community-page.component';
import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
describe('CreateCommunityPageComponent', () => { describe('CreateCommunityPageComponent', () => {
let comp: CreateCommunityPageComponent; let comp: CreateCommunityPageComponent;
@@ -23,6 +25,7 @@ describe('CreateCommunityPageComponent', () => {
{ provide: CommunityDataService, useValue: { findById: () => observableOf({}) } }, { provide: CommunityDataService, useValue: { findById: () => observableOf({}) } },
{ provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } },
{ provide: Router, useValue: {} }, { provide: Router, useValue: {} },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();

View File

@@ -4,6 +4,8 @@ import { CommunityDataService } from '../../core/data/community-data.service';
import { RouteService } from '../../core/services/route.service'; import { RouteService } from '../../core/services/route.service';
import { Router } from '@angular/router'; import { Router } from '@angular/router';
import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component'; 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 * 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<Community> { export class CreateCommunityPageComponent extends CreateComColPageComponent<Community> {
protected frontendURL = '/communities/'; protected frontendURL = '/communities/';
protected type = Community.type;
public constructor( public constructor(
protected communityDataService: CommunityDataService, protected communityDataService: CommunityDataService,
protected routeService: RouteService, 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);
} }
} }

View File

@@ -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 */
}

View File

@@ -0,0 +1,6 @@
<ds-community-form (submitForm)="onSubmit($event)"
[dso]="(dsoRD$ | async)?.payload"
(finish)="navigateToHomePage()"></ds-community-form>
<a class="btn btn-danger"
[routerLink]="'/communities/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'community.edit.delete'
| translate}}</a>

View File

@@ -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<CommunityMetadataComponent>;
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/');
})
});
});

View File

@@ -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<Community> {
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);
}
}

View File

@@ -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 */
}

View File

@@ -1,12 +0,0 @@
<div class="container">
<div class="row">
<div class="col-12 pb-4">
<h2 id="header" class="border-bottom pb-2">{{ 'community.edit.head' | translate }}</h2>
<ds-community-form (submitForm)="onSubmit($event)"
[dso]="(dsoRD$ | async)?.payload"></ds-community-form>
<a class="btn btn-danger"
[routerLink]="'/communities/' + (dsoRD$ | async)?.payload.uuid + '/delete'">{{'community.edit.delete'
| translate}}</a>
</div>
</div>
</div>

View File

@@ -13,13 +13,29 @@ describe('EditCommunityPageComponent', () => {
let comp: EditCommunityPageComponent; let comp: EditCommunityPageComponent;
let fixture: ComponentFixture<EditCommunityPageComponent>; let fixture: ComponentFixture<EditCommunityPageComponent>;
const routeStub = {
data: observableOf({
dso: { payload: {} }
}),
routeConfig: {
children: []
},
snapshot: {
firstChild: {
routeConfig: {
path: 'mockUrl'
}
}
}
};
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
declarations: [EditCommunityPageComponent], declarations: [EditCommunityPageComponent],
providers: [ providers: [
{ provide: CommunityDataService, useValue: {} }, { provide: CommunityDataService, useValue: {} },
{ provide: ActivatedRoute, useValue: { data: observableOf({ dso: { payload: {} } }) } }, { provide: ActivatedRoute, useValue: routeStub },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -31,9 +47,9 @@ describe('EditCommunityPageComponent', () => {
fixture.detectChanges(); fixture.detectChanges();
}); });
describe('frontendURL', () => { describe('type', () => {
it('should have the right frontendURL set', () => { it('should have the right type set', () => {
expect((comp as any).frontendURL).toEqual('/communities/'); expect((comp as any).type).toEqual('community');
}) })
}); });
}); });

View File

@@ -1,25 +1,31 @@
import { Component } from '@angular/core'; import { Component } from '@angular/core';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { CommunityDataService } from '../../core/data/community-data.service';
import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRoute, Router } from '@angular/router';
import { EditComColPageComponent } from '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component'; 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 that represents the page where a user can edit an existing Community
*/ */
@Component({ @Component({
selector: 'ds-edit-community', selector: 'ds-edit-community',
styleUrls: ['./edit-community-page.component.scss'], templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html'
templateUrl: './edit-community-page.component.html'
}) })
export class EditCommunityPageComponent extends EditComColPageComponent<Community> { export class EditCommunityPageComponent extends EditComColPageComponent<Community> {
protected frontendURL = '/communities/'; type = 'community';
public constructor( public constructor(
protected communityDataService: CommunityDataService,
protected router: Router, protected router: Router,
protected route: ActivatedRoute 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)
} }
} }

View File

@@ -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 {
}

View File

@@ -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 {
}

View File

@@ -1,14 +1,13 @@
<ng-container *ngVar="(subCollectionsRDObs | async) as subCollectionsRD"> <ng-container *ngVar="(subCollectionsRDObs | async) as subCollectionsRD">
<div *ngIf="subCollectionsRD?.hasSucceeded && subCollectionsRD?.payload.totalElements > 0" @fadeIn> <div *ngIf="subCollectionsRD?.hasSucceeded && subCollectionsRD?.payload.totalElements > 0" @fadeIn>
<h2>{{'community.sub-collection-list.head' | translate}}</h2> <h2>{{'community.sub-collection-list.head' | translate}}</h2>
<ul> <ds-viewable-collection
<li *ngFor="let collection of subCollectionsRD?.payload.page"> [config]="config"
<p> [sortConfig]="sortConfig"
<span class="lead"><a [routerLink]="['/collections', collection.id]">{{collection.name}}</a></span><br> [objects]="subCollectionsRD"
<span class="text-muted">{{collection.shortDescription}}</span> [hideGear]="false"
</p> (paginationChange)="onPaginationChange($event)">
</li> </ds-viewable-collection>
</ul>
</div> </div>
<ds-error *ngIf="subCollectionsRD?.hasFailed" message="{{'error.sub-collections' | translate}}"></ds-error> <ds-error *ngIf="subCollectionsRD?.hasFailed" message="{{'error.sub-collections' | translate}}"></ds-error>
<ds-loading *ngIf="subCollectionsRD?.isLoading" message="{{'loading.sub-collections' | translate}}"></ds-loading> <ds-loading *ngIf="subCollectionsRD?.isLoading" message="{{'loading.sub-collections' | translate}}"></ds-loading>

View File

@@ -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<CommunityPageSubCollectionListComponent>;
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');
});
});

View File

@@ -1,12 +1,16 @@
import { Component, Input, OnInit } from '@angular/core'; 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 { RemoteData } from '../../core/data/remote-data';
import { Collection } from '../../core/shared/collection.model'; import { Collection } from '../../core/shared/collection.model';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { fadeIn } from '../../shared/animations/fade'; import { fadeIn } from '../../shared/animations/fade';
import { PaginatedList } from '../../core/data/paginated-list'; 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({ @Component({
selector: 'ds-community-page-sub-collection-list', selector: 'ds-community-page-sub-collection-list',
@@ -16,9 +20,60 @@ import { PaginatedList } from '../../core/data/paginated-list';
}) })
export class CommunityPageSubCollectionListComponent implements OnInit { export class CommunityPageSubCollectionListComponent implements OnInit {
@Input() community: Community; @Input() community: Community;
subCollectionsRDObs: Observable<RemoteData<PaginatedList<Collection>>>;
/**
* 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<RemoteData<PaginatedList<Collection>>> = new BehaviorSubject<RemoteData<PaginatedList<Collection>>>({} as any);
constructor(private cds: CollectionDataService) {}
ngOnInit(): void { 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);
});
} }
} }

View File

@@ -1,14 +1,13 @@
<ng-container *ngVar="(subCommunitiesRDObs | async) as subCommunitiesRD"> <ng-container *ngVar="(subCommunitiesRDObs | async) as subCommunitiesRD">
<div *ngIf="subCommunitiesRD?.hasSucceeded && subCommunitiesRD?.payload.totalElements > 0" @fadeIn> <div *ngIf="subCommunitiesRD?.hasSucceeded && subCommunitiesRD?.payload.totalElements > 0" @fadeIn>
<h2>{{'community.sub-community-list.head' | translate}}</h2> <h2>{{'community.sub-community-list.head' | translate}}</h2>
<ul> <ds-viewable-collection
<li *ngFor="let community of subCommunitiesRD?.payload.page"> [config]="config"
<p> [sortConfig]="sortConfig"
<span class="lead"><a [routerLink]="['/communities', community.id]">{{community.name}}</a></span><br> [objects]="subCommunitiesRD"
<span class="text-muted">{{community.shortDescription}}</span> [hideGear]="false"
</p> (paginationChange)="onPaginationChange($event)">
</li> </ds-viewable-collection>
</ul>
</div> </div>
<ds-error *ngIf="subCommunitiesRD?.hasFailed" message="{{'error.sub-communities' | translate}}"></ds-error> <ds-error *ngIf="subCommunitiesRD?.hasFailed" message="{{'error.sub-communities' | translate}}"></ds-error>
<ds-loading *ngIf="subCommunitiesRD?.isLoading" message="{{'loading.sub-communities' | translate}}"></ds-loading> <ds-loading *ngIf="subCommunitiesRD?.isLoading" message="{{'loading.sub-communities' | translate}}"></ds-loading>

View File

@@ -1,21 +1,29 @@
import {async, ComponentFixture, TestBed} from '@angular/core/testing'; import { async, ComponentFixture, TestBed } from '@angular/core/testing';
import {TranslateModule} from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import {NO_ERRORS_SCHEMA} from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import {CommunityPageSubCommunityListComponent} from './community-page-sub-community-list.component'; import { RouterTestingModule } from '@angular/router/testing';
import {Community} from '../../core/shared/community.model'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import {RemoteData} from '../../core/data/remote-data'; import { By } from '@angular/platform-browser';
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';
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 comp: CommunityPageSubCommunityListComponent;
let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>; let fixture: ComponentFixture<CommunityPageSubCommunityListComponent>;
let communityDataServiceStub: any;
let subCommList = [];
const subcommunities = [Object.assign(new Community(), { const subcommunities = [Object.assign(new Community(), {
id: '123456789-1', id: '123456789-1',
@@ -32,34 +40,92 @@ describe('SubCommunityList Component', () => {
{ language: 'en_US', value: 'SubCommunity 2' } { 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: { metadata: {
'dc.title': [ 'dc.title': [
{ language: 'en_US', value: 'Test title' } { language: 'en_US', value: 'Test title' }
] ]
}, }
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), []))
}); });
const mockCommunity = Object.assign(new Community(), { communityDataServiceStub = {
metadata: { findByParent(parentUUID: string, options: FindListOptions = {}) {
'dc.title': [ let currentPage = options.currentPage;
{ language: 'en_US', value: 'Test title' } let elementsPerPage = options.elementsPerPage;
] if (currentPage === undefined) {
}, currentPage = 1
subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subcommunities)) }
}) 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(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), SharedModule, imports: [
TranslateModule.forRoot(),
SharedModule,
RouterTestingModule.withRoutes([]), RouterTestingModule.withRoutes([]),
NoopAnimationsModule], NgbModule.forRoot(),
NoopAnimationsModule
],
declarations: [CommunityPageSubCommunityListComponent], declarations: [CommunityPageSubCommunityListComponent],
providers: [
{ provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
{ provide: SelectableListService, useValue: {} },
],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
})); }));
@@ -67,23 +133,52 @@ describe('SubCommunityList Component', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent); fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
comp.community = mockCommunity;
}); });
it('should display a list of subCommunities', () => { it('should display a list of sub-communities', () => {
comp.community = mockCommunity; subCommList = subcommunities;
fixture.detectChanges(); fixture.detectChanges();
const subComList = fixture.debugElement.queryAll(By.css('li')); 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[0].nativeElement.textContent).toContain('SubCommunity 1');
expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2'); 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', () => { it('should not display the header when list of sub-communities is empty', () => {
comp.community = emptySubCommunitiesCommunity; subCommList = [];
fixture.detectChanges(); fixture.detectChanges();
const subComHead = fixture.debugElement.queryAll(By.css('h2')); const subComHead = fixture.debugElement.queryAll(By.css('h2'));
expect(subComHead.length).toEqual(0); 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');
});
}); });

View File

@@ -1,26 +1,82 @@
import { Component, Input, OnInit } from '@angular/core'; import { Component, Input, OnInit } from '@angular/core';
import { BehaviorSubject } from 'rxjs';
import { take } from 'rxjs/operators';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { fadeIn } from '../../shared/animations/fade'; import { fadeIn } from '../../shared/animations/fade';
import { PaginatedList } from '../../core/data/paginated-list'; 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({ @Component({
selector: 'ds-community-page-sub-community-list', selector: 'ds-community-page-sub-community-list',
styleUrls: ['./community-page-sub-community-list.component.scss'], styleUrls: ['./community-page-sub-community-list.component.scss'],
templateUrl: './community-page-sub-community-list.component.html', templateUrl: './community-page-sub-community-list.component.html',
animations:[fadeIn] animations: [fadeIn]
}) })
/** /**
* Component to render the sub-communities of a Community * Component to render the sub-communities of a Community
*/ */
export class CommunityPageSubCommunityListComponent implements OnInit { export class CommunityPageSubCommunityListComponent implements OnInit {
@Input() community: Community; @Input() community: Community;
subCommunitiesRDObs: Observable<RemoteData<PaginatedList<Community>>>;
/**
* 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<RemoteData<PaginatedList<Community>>> = new BehaviorSubject<RemoteData<PaginatedList<Community>>>({} as any);
constructor(private cds: CommunityDataService) {
}
ngOnInit(): void { 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);
});
} }
} }

View File

@@ -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<TopLevelCommunityListComponent>;
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');
});
});

View File

@@ -1,15 +1,15 @@
import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; 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 { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
import { CommunityDataService } from '../../core/data/community-data.service'; import { CommunityDataService } from '../../core/data/community-data.service';
import { PaginatedList } from '../../core/data/paginated-list'; import { PaginatedList } from '../../core/data/paginated-list';
import { RemoteData } from '../../core/data/remote-data'; import { RemoteData } from '../../core/data/remote-data';
import { Community } from '../../core/shared/community.model'; import { Community } from '../../core/shared/community.model';
import { fadeInOut } from '../../shared/animations/fade'; import { fadeInOut } from '../../shared/animations/fade';
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
import { take } from 'rxjs/operators';
/** /**
* this component renders the Top-Level Community list * this component renders the Top-Level Community list
@@ -33,6 +33,11 @@ export class TopLevelCommunityListComponent implements OnInit {
*/ */
config: PaginationComponentOptions; config: PaginationComponentOptions;
/**
* The pagination id
*/
pageId = 'top-level-pagination';
/** /**
* The sorting configuration * The sorting configuration
*/ */
@@ -40,7 +45,7 @@ export class TopLevelCommunityListComponent implements OnInit {
constructor(private cds: CommunityDataService) { constructor(private cds: CommunityDataService) {
this.config = new PaginationComponentOptions(); this.config = new PaginationComponentOptions();
this.config.id = 'top-level-pagination'; this.config.id = this.pageId;
this.config.pageSize = 5; this.config.pageSize = 5;
this.config.currentPage = 1; this.config.currentPage = 1;
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
@@ -55,10 +60,10 @@ export class TopLevelCommunityListComponent implements OnInit {
* @param event The new pagination data * @param event The new pagination data
*/ */
onPaginationChange(event) { onPaginationChange(event) {
this.config.currentPage = event.page; this.config.currentPage = event.pagination.currentPage;
this.config.pageSize = event.pageSize; this.config.pageSize = event.pagination.pageSize;
this.sortConfig.field = event.sortField; this.sortConfig.field = event.sort.field;
this.sortConfig.direction = event.sortDirection; this.sortConfig.direction = event.sort.direction;
this.updatePage(); this.updatePage();
} }

View File

@@ -33,12 +33,7 @@ export class MyDSpaceNewSubmissionComponent implements OnDestroy, OnInit {
/** /**
* The UploaderOptions object * The UploaderOptions object
*/ */
public uploadFilesOptions: UploaderOptions = { public uploadFilesOptions: UploaderOptions = new UploaderOptions();
url: '',
authToken: null,
disableMultipart: false,
itemAlias: null
};
/** /**
* Subscription to unsubscribe from * Subscription to unsubscribe from

View File

@@ -5,10 +5,10 @@ import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angu
import { pushInOut } from '../shared/animations/push'; import { pushInOut } from '../shared/animations/push';
import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component'; import { SEARCH_CONFIG_SERVICE } from '../+my-dspace-page/my-dspace-page.component';
import { SearchConfigurationService } from '../core/shared/search/search-configuration.service'; import { SearchConfigurationService } from '../core/shared/search/search-configuration.service';
import { Router } from '@angular/router';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { RouteService } from '../core/services/route.service'; import { RouteService } from '../core/services/route.service';
import { SearchService } from '../core/shared/search/search.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. * 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)) { if (hasValue(this.configuration)) {
this.routeService.setParameter('configuration', this.configuration); this.routeService.setParameter('configuration', this.configuration);
} }
if (hasValue(this.fixedFilterQuery)) {
this.routeService.setParameter('fixedFilter', this.fixedFilterQuery);
}
} }
} }

View File

@@ -3,18 +3,28 @@ import { CommonModule } from '@angular/common';
import { CoreModule } from '../core/core.module'; import { CoreModule } from '../core/core.module';
import { SharedModule } from '../shared/shared.module'; import { SharedModule } from '../shared/shared.module';
import { SearchPageRoutingModule } from './search-page-routing.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 { ConfigurationSearchPageComponent } from './configuration-search-page.component';
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard'; 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 { 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 = [ const components = [
SearchPageComponent, SearchPageComponent,
SearchComponent, SearchComponent,
ConfigurationSearchPageComponent, ConfigurationSearchPageComponent,
SearchTrackerComponent SearchTrackerComponent,
]; ];
@NgModule({ @NgModule({
@@ -22,11 +32,18 @@ const components = [
SearchPageRoutingModule, SearchPageRoutingModule,
CommonModule, CommonModule,
SharedModule, SharedModule,
EffectsModule.forFeature(effects),
CoreModule.forRoot(), CoreModule.forRoot(),
StatisticsModule.forRoot(), StatisticsModule.forRoot(),
], ],
providers: [ConfigurationSearchPageGuard],
declarations: components, declarations: components,
providers: [
SidebarService,
SidebarFilterService,
SearchFilterService,
ConfigurationSearchPageGuard,
SearchConfigurationService
],
exports: components exports: components
}) })

View File

@@ -82,6 +82,7 @@ export class AppComponent implements OnInit, AfterViewInit {
} }
} }
angulartics2GoogleAnalytics.startTracking();
angulartics2DSpace.startTracking(); angulartics2DSpace.startTracking();
metadata.listenForRouteChange(); metadata.listenForRouteChange();

View File

@@ -1,5 +1,6 @@
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store'; import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store'; import * as fromRouter from '@ngrx/router-store';
import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer'; import { hostWindowReducer, HostWindowState } from './shared/search/host-window.reducer';
import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer'; import { CommunityListReducer, CommunityListState } from './community-list-page/community-list.reducer';
import { formReducer, FormState } from './shared/form/form.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 { filterReducer, SearchFiltersState } from './shared/search/search-filters/search-filter/search-filter.reducer';
import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers'; import { notificationsReducer, NotificationsState } from './shared/notifications/notifications.reducers';
import { truncatableReducer, TruncatablesState } from './shared/truncatable/truncatable.reducer'; 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 { hasValue } from './shared/empty.util';
import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer'; import { cssVariablesReducer, CSSVariablesState } from './shared/sass-helper/sass-helper.reducer';
import { menusReducer, MenusState } from './shared/menu/menu.reducer'; import { menusReducer, MenusState } from './shared/menu/menu.reducer';
import { historyReducer, HistoryState } from './shared/history/history.reducer'; import {
import { selectableListReducer, SelectableListsState } from './shared/object-list/selectable-list/selectable-list.reducer'; selectableListReducer,
import { bitstreamFormatReducer, BitstreamFormatRegistryState } from './+admin/admin-registries/bitstream-formats/bitstream-format.reducers'; SelectableListsState
} from './shared/object-list/selectable-list/selectable-list.reducer';
import { ObjectSelectionListState, objectSelectionReducer } from './shared/object-select/object-select.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 { export interface AppState {
router: fromRouter.RouterReducerState; router: fromRouter.RouterReducerState;
history: HistoryState;
hostWindow: HostWindowState; hostWindow: HostWindowState;
forms: FormState; forms: FormState;
metadataRegistry: MetadataRegistryState; metadataRegistry: MetadataRegistryState;
bitstreamFormats: BitstreamFormatRegistryState;
notifications: NotificationsState; notifications: NotificationsState;
sidebar: SidebarState; sidebar: SidebarState;
sidebarFilter: SidebarFiltersState; sidebarFilter: SidebarFiltersState;
@@ -40,11 +46,9 @@ export interface AppState {
export const appReducers: ActionReducerMap<AppState> = { export const appReducers: ActionReducerMap<AppState> = {
router: fromRouter.routerReducer, router: fromRouter.routerReducer,
history: historyReducer,
hostWindow: hostWindowReducer, hostWindow: hostWindowReducer,
forms: formReducer, forms: formReducer,
metadataRegistry: metadataRegistryReducer, metadataRegistry: metadataRegistryReducer,
bitstreamFormats: bitstreamFormatReducer,
notifications: notificationsReducer, notifications: notificationsReducer,
sidebar: sidebarReducer, sidebar: sidebarReducer,
sidebarFilter: sidebarFilterReducer, sidebarFilter: sidebarFilterReducer,

View File

@@ -312,7 +312,7 @@ export class CommunityListService {
hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 }) hasColls$ = this.collectionDataService.findByParent(community.uuid, { elementsPerPage: 1 })
.pipe( .pipe(
filter((rd: RemoteData<PaginatedList<Community>>) => rd.hasSucceeded), filter((rd: RemoteData<PaginatedList<Collection>>) => rd.hasSucceeded),
take(1), take(1),
map((results) => results.payload.totalElements > 0), map((results) => results.payload.totalElements > 0),
); );
@@ -320,8 +320,8 @@ export class CommunityListService {
let hasChildren$: Observable<boolean>; let hasChildren$: Observable<boolean>;
hasChildren$ = observableCombineLatest(hasSubcoms$, hasColls$).pipe( hasChildren$ = observableCombineLatest(hasSubcoms$, hasColls$).pipe(
take(1), take(1),
map((result: [boolean]) => { map(([hasSubcoms, hasColls]: [boolean, boolean]) => {
if (result[0] || result[1]) { if (hasSubcoms || hasColls) {
return true; return true;
} else { } else {
return false; return false;

View File

@@ -60,7 +60,8 @@ export class AuthService {
// and is not the login route, clear redirect url and messages // and is not the login route, clear redirect url and messages
const routeUrl$ = this.store.pipe( const routeUrl$ = this.store.pipe(
select(routerStateSelector), 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)), filter((routerState: RouterReducerState) => !this.isLoginRoute(routerState.state.url)),
map((routerState: RouterReducerState) => routerState.state.url) map((routerState: RouterReducerState) => routerState.state.url)
); );

View File

@@ -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<ExternalSourceEntry> {
/**
* 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;
}

View File

@@ -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<ExternalSource> {
/**
* Unique identifier
*/
@autoserialize
id: string;
/**
* The name of this external source
*/
@autoserialize
name: string;
/**
* Is the source hierarchical?
*/
@autoserialize
hierarchical: boolean;
}

View File

@@ -136,6 +136,10 @@ import { SearchConfigurationService } from './shared/search/search-configuration
import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service'; import { SelectableListService } from '../shared/object-list/selectable-list/selectable-list.service';
import { RelationshipTypeService } from './data/relationship-type.service'; import { RelationshipTypeService } from './data/relationship-type.service';
import { SidebarService } from '../shared/sidebar/sidebar.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 * When not in production, endpoint responses can be mocked for testing purposes
@@ -247,6 +251,8 @@ const PROVIDERS = [
SearchConfigurationService, SearchConfigurationService,
SelectableListService, SelectableListService,
RelationshipTypeService, RelationshipTypeService,
ExternalSourceService,
LookupRelationService,
// register AuthInterceptor as HttpInterceptor // register AuthInterceptor as HttpInterceptor
{ {
provide: HTTP_INTERCEPTORS, provide: HTTP_INTERCEPTORS,
@@ -292,7 +298,9 @@ export const normalizedModels =
NormalizedPoolTask, NormalizedPoolTask,
NormalizedRelationship, NormalizedRelationship,
NormalizedRelationshipType, NormalizedRelationshipType,
NormalizedItemType NormalizedItemType,
NormalizedExternalSource,
NormalizedExternalSourceEntry
]; ];
@NgModule({ @NgModule({

View File

@@ -1,7 +1,4 @@
import { import { ActionReducerMap, } from '@ngrx/store';
ActionReducerMap,
createFeatureSelector,
} from '@ngrx/store';
import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer'; import { objectCacheReducer, ObjectCacheState } from './cache/object-cache.reducer';
import { indexReducer, MetaIndexState } from './index/index.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 { authReducer, AuthState } from './auth/auth.reducer';
import { jsonPatchOperationsReducer, JsonPatchOperationsState } from './json-patch/json-patch-operations.reducer'; import { jsonPatchOperationsReducer, JsonPatchOperationsState } from './json-patch/json-patch-operations.reducer';
import { serverSyncBufferReducer, ServerSyncBufferState } from './cache/server-sync-buffer.reducer'; import { serverSyncBufferReducer, ServerSyncBufferState } from './cache/server-sync-buffer.reducer';
import { import { objectUpdatesReducer, ObjectUpdatesState } from './data/object-updates/object-updates.reducer';
objectUpdatesReducer,
ObjectUpdatesState
} from './data/object-updates/object-updates.reducer';
import { routeReducer, RouteState } from './services/route.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 { export interface CoreState {
'bitstreamFormats': BitstreamFormatRegistryState;
'cache/object': ObjectCacheState, 'cache/object': ObjectCacheState,
'cache/syncbuffer': ServerSyncBufferState, 'cache/syncbuffer': ServerSyncBufferState,
'cache/object-updates': ObjectUpdatesState 'cache/object-updates': ObjectUpdatesState
'data/request': RequestState, 'data/request': RequestState,
'history': HistoryState;
'index': MetaIndexState, 'index': MetaIndexState,
'auth': AuthState, 'auth': AuthState,
'json/patch': JsonPatchOperationsState, 'json/patch': JsonPatchOperationsState,
@@ -27,10 +28,12 @@ export interface CoreState {
} }
export const coreReducers: ActionReducerMap<CoreState> = { export const coreReducers: ActionReducerMap<CoreState> = {
'bitstreamFormats': bitstreamFormatReducer,
'cache/object': objectCacheReducer, 'cache/object': objectCacheReducer,
'cache/syncbuffer': serverSyncBufferReducer, 'cache/syncbuffer': serverSyncBufferReducer,
'cache/object-updates': objectUpdatesReducer, 'cache/object-updates': objectUpdatesReducer,
'data/request': requestReducer, 'data/request': requestReducer,
'history': historyReducer,
'index': indexReducer, 'index': indexReducer,
'auth': authReducer, 'auth': authReducer,
'json/patch': jsonPatchOperationsReducer, 'json/patch': jsonPatchOperationsReducer,

View File

@@ -3,7 +3,6 @@ import { RequestEntry } from './request.reducer';
import { RestResponse } from '../cache/response.models'; import { RestResponse } from '../cache/response.models';
import { Observable, of as observableOf } from 'rxjs'; import { Observable, of as observableOf } from 'rxjs';
import { Action, Store } from '@ngrx/store'; import { Action, Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { cold, getTestScheduler, hot } from 'jasmine-marbles'; import { cold, getTestScheduler, hot } from 'jasmine-marbles';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
@@ -19,6 +18,7 @@ import {
BitstreamFormatsRegistrySelectAction BitstreamFormatsRegistrySelectAction
} from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions'; } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { CoreState } from '../core.reducers';
describe('BitstreamFormatDataService', () => { describe('BitstreamFormatDataService', () => {
let service: BitstreamFormatDataService; let service: BitstreamFormatDataService;

View File

@@ -5,7 +5,6 @@ import { RequestService } from './request.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { createSelector, select, Store } from '@ngrx/store'; import { createSelector, select, Store } from '@ngrx/store';
import { CoreState } from '../core.reducers';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { NotificationsService } from '../../shared/notifications/notifications.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 { configureRequest, getResponseFromEntry } from '../shared/operators';
import { distinctUntilChanged } from 'rxjs/internal/operators/distinctUntilChanged'; import { distinctUntilChanged } from 'rxjs/internal/operators/distinctUntilChanged';
import { RestResponse } from '../cache/response.models'; import { RestResponse } from '../cache/response.models';
import { AppState } from '../../app.reducer';
import { BitstreamFormatRegistryState } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.reducers'; import { BitstreamFormatRegistryState } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
import { import {
BitstreamFormatsRegistryDeselectAction, BitstreamFormatsRegistryDeselectAction,
@@ -26,8 +24,9 @@ import {
} from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions'; } from '../../+admin/admin-registries/bitstream-formats/bitstream-format.actions';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { RequestEntry } from './request.reducer'; 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, const selectedBitstreamFormatSelector = createSelector(bitstreamFormatsStateSelector,
(bitstreamFormatRegistryState: BitstreamFormatRegistryState) => bitstreamFormatRegistryState.selectedBitstreamFormats); (bitstreamFormatRegistryState: BitstreamFormatRegistryState) => bitstreamFormatRegistryState.selectedBitstreamFormats);
@@ -55,6 +54,7 @@ export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
/** /**
* Get the endpoint for browsing bitstream formats * Get the endpoint for browsing bitstream formats
* @param {FindListOptions} options * @param {FindListOptions} options
* @param {string} linkPath
* @returns {Observable<string>} * @returns {Observable<string>}
*/ */
getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> { getBrowseEndpoint(options: FindListOptions = {}, linkPath?: string): Observable<string> {
@@ -99,7 +99,7 @@ export class BitstreamFormatDataService extends DataService<BitstreamFormat> {
/** /**
* Create a new BitstreamFormat * Create a new BitstreamFormat
* @param BitstreamFormat * @param {BitstreamFormat} bitstreamFormat
*/ */
public createBitstreamFormat(bitstreamFormat: BitstreamFormat): Observable<RestResponse> { public createBitstreamFormat(bitstreamFormat: BitstreamFormat): Observable<RestResponse> {
const requestId = this.requestService.generateRequestId(); const requestId = this.requestService.generateRequestId();

View File

@@ -1,32 +1,41 @@
import { import {
distinctUntilChanged, distinctUntilChanged,
filter, first, filter, first,map, mergeMap, share, switchMap,
map,
mergeMap,
share,
switchMap,
take, take,
tap tap
} from 'rxjs/operators'; } 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 { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { NormalizedCommunity } from '../cache/models/normalized-community.model'; import { NormalizedCommunity } from '../cache/models/normalized-community.model';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
import { CommunityDataService } from './community-data.service'; import { CommunityDataService } from './community-data.service';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { DeleteRequest, FindListOptions, FindByIDRequest, RestRequest } from './request.models';
import { PaginatedList } from './paginated-list'; import { PaginatedList } from './paginated-list';
import { RemoteData } from './remote-data'; import { RemoteData } from './remote-data';
import { FindListOptions, FindByIDRequest } from './request.models';
import { HALEndpointService } from '../shared/hal-endpoint.service'; 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 { 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<T extends CacheableObject> extends DataService<T> { export abstract class ComColDataService<T extends CacheableObject> extends DataService<T> {
protected abstract cds: CommunityDataService; protected abstract cds: CommunityDataService;
protected abstract objectCache: ObjectCacheService; protected abstract objectCache: ObjectCacheService;
protected abstract halService: HALEndpointService; protected abstract halService: HALEndpointService;
/**
* Linkpath of endpoint to delete the logo
*/
protected logoDeleteLinkpath = 'bitstreams';
/** /**
* Get the scoped endpoint URL by fetching the object with * Get the scoped endpoint URL by fetching the object with
* the given scopeID and returning its HAL link with this * the given scopeID and returning its HAL link with this
@@ -76,4 +85,33 @@ export abstract class ComColDataService<T extends CacheableObject> extends DataS
return this.findList(href$, options); 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<string> {
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<RestResponse> {
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()
);
}
}
} }

View File

@@ -1,12 +1,22 @@
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { Observable } from 'rxjs'; 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 { Store } from '@ngrx/store';
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { CoreState } from '../core.reducers';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { URLCombiner } from '../url-combiner/url-combiner'; import { URLCombiner } from '../url-combiner/url-combiner';
import { PaginatedList } from './paginated-list'; import { PaginatedList } from './paginated-list';
@@ -14,9 +24,9 @@ import { RemoteData } from './remote-data';
import { import {
CreateRequest, CreateRequest,
DeleteByIDRequest, DeleteByIDRequest,
FindByIDRequest,
FindListOptions, FindListOptions,
FindListRequest, FindListRequest,
FindByIDRequest,
GetRequest GetRequest
} from './request.models'; } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
@@ -37,6 +47,7 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec
import { ChangeAnalyzer } from './change-analyzer'; import { ChangeAnalyzer } from './change-analyzer';
import { RestRequestMethod } from './rest-request-method'; import { RestRequestMethod } from './rest-request-method';
import { getMapsToType } from '../cache/builders/build-decorators'; import { getMapsToType } from '../cache/builders/build-decorators';
import { CoreState } from '../core.reducers';
export abstract class DataService<T extends CacheableObject> { export abstract class DataService<T extends CacheableObject> {
protected abstract requestService: RequestService; protected abstract requestService: RequestService;
@@ -238,7 +249,7 @@ export abstract class DataService<T extends CacheableObject> {
*/ */
update(object: T): Observable<RemoteData<T>> { update(object: T): Observable<RemoteData<T>> {
const oldVersion$ = this.objectCache.getObjectBySelfLink(object.self); const oldVersion$ = this.objectCache.getObjectBySelfLink(object.self);
return oldVersion$.pipe(take(1), mergeMap((oldVersion: T) => { return oldVersion$.pipe(take(1), mergeMap((oldVersion: NormalizedObject<T>) => {
const operations = this.comparator.diff(oldVersion, object); const operations = this.comparator.diff(oldVersion, object);
if (isNotEmpty(operations)) { if (isNotEmpty(operations)) {
this.objectCache.addPatch(object.self, operations); this.objectCache.addPatch(object.self, operations);

View File

@@ -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);
});
});
});
});

View File

@@ -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<ExternalSource> {
protected linkPath = 'externalsources';
constructor(
protected requestService: RequestService,
protected rdbService: RemoteDataBuildService,
protected dataBuildService: NormalizedObjectBuildService,
protected store: Store<CoreState>,
protected objectCache: ObjectCacheService,
protected halService: HALEndpointService,
protected notificationsService: NotificationsService,
protected http: HttpClient,
protected comparator: DefaultChangeAnalyzer<ExternalSource>) {
super();
}
/**
* Get the endpoint to browse external sources
* @param options
* @param linkPath
*/
getBrowseEndpoint(options: FindListOptions = {}, linkPath: string = this.linkPath): Observable<string> {
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<string> {
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<RemoteData<PaginatedList<ExternalSourceEntry>>> {
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$);
}
}

View File

@@ -6,22 +6,14 @@ import { CoreState } from '../core.reducers';
import { ItemDataService } from './item-data.service'; import { ItemDataService } from './item-data.service';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models';
DeleteRequest,
FindListOptions,
GetRequest,
MappedCollectionsRequest,
PostRequest,
RestRequest
} from './request.models';
import { ObjectCacheService } from '../cache/object-cache.service'; 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 { RestResponse } from '../cache/response.models';
import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsService } from '../../shared/notifications/notifications.service';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service'; import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
import { HttpClient } from '@angular/common/http'; import { HttpClient } from '@angular/common/http';
import { RequestEntry } from './request.reducer'; import { RequestEntry } from './request.reducer';
import { of as observableOf } from 'rxjs';
import { getMockRequestService } from '../../shared/mocks/mock-request.service'; import { getMockRequestService } from '../../shared/mocks/mock-request.service';
describe('ItemDataService', () => { describe('ItemDataService', () => {
@@ -184,7 +176,7 @@ describe('ItemDataService', () => {
}); });
it('should configure a DELETE request', () => { 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', () => { 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)));
}); });
}); });

View File

@@ -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)
});
});
});
});

View File

@@ -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<RemoteData<PaginatedList<SearchResult<Item>>>> {
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<PaginatedList<SearchResult<DSpaceObject>>>) => rd.isLoading),
concat(subject.pipe(take(1)))
)
) as any
) as Observable<RemoteData<PaginatedList<SearchResult<Item>>>>;
}
/**
* 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<number> {
return this.getLocalResults(relationship, Object.assign(new PaginatedSearchOptions({}), searchOptions, { pagination: this.singleResultOptions }), false).pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload(),
map((results: PaginatedList<SearchResult<Item>>) => 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<number> {
return this.externalSourceService.getExternalSourceEntries(externalSource.id, Object.assign(new PaginatedSearchOptions({}), searchOptions, { pagination: this.singleResultOptions })).pipe(
getAllSucceededRemoteData(),
getRemoteDataPayload(),
map((results: PaginatedList<ExternalSourceEntry>) => results.totalElements),
startWith(0)
);
}
}

View File

@@ -2,7 +2,7 @@ import { Injectable } from '@angular/core';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.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 { configureRequest, getSucceededRemoteData } from '../shared/operators';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { RelationshipType } from '../shared/item-relationships/relationship-type.model'; 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)), map((endpointURL: string) => new FindListRequest(this.requestService.generateRequestId(), endpointURL, options)),
configureRequest(this.requestService), configureRequest(this.requestService),
switchMap(() => this.rdbService.buildList(link$)) switchMap(() => this.rdbService.buildList(link$))
); ) as Observable<RemoteData<PaginatedList<RelationshipType>>>;
} }
/** /**

View File

@@ -3,8 +3,13 @@ import { RequestService } from './request.service';
import { HALEndpointService } from '../shared/hal-endpoint.service'; import { HALEndpointService } from '../shared/hal-endpoint.service';
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util'; import { hasValue, hasValueOperator, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
import { distinctUntilChanged, filter, map, mergeMap, skipWhile, startWith, switchMap, take, tap } from 'rxjs/operators'; import { distinctUntilChanged, filter, map, mergeMap, startWith, switchMap, take, tap } from 'rxjs/operators';
import { configureRequest, getRemoteDataPayload, getResponseFromEntry, getSucceededRemoteData } from '../shared/operators'; import {
configureRequest,
getRemoteDataPayload,
getResponseFromEntry,
getSucceededRemoteData
} from '../shared/operators';
import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models'; import { DeleteRequest, FindListOptions, PostRequest, RestRequest } from './request.models';
import { Observable } from 'rxjs/internal/Observable'; import { Observable } from 'rxjs/internal/Observable';
import { RestResponse } from '../cache/response.models'; import { RestResponse } from '../cache/response.models';
@@ -15,7 +20,11 @@ import { RemoteData } from './remote-data';
import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs'; import { combineLatest, combineLatest as observableCombineLatest } from 'rxjs';
import { PaginatedList } from './paginated-list'; import { PaginatedList } from './paginated-list';
import { ItemDataService } from './item-data.service'; 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 { ObjectCacheService } from '../cache/object-cache.service';
import { DataService } from './data.service'; import { DataService } from './data.service';
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.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 { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
import { AppState, keySelector } from '../../app.reducer'; import { AppState, keySelector } from '../../app.reducer';
import { NameVariantListState } from '../../shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.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; const relationshipListsStateSelector = (state: AppState) => state.relationshipLists;
@@ -117,7 +129,7 @@ export class RelationshipService extends DataService<Relationship> {
getResponseFromEntry(), getResponseFromEntry(),
tap(() => this.removeRelationshipItemsFromCache(item1)), tap(() => this.removeRelationshipItemsFromCache(item1)),
tap(() => this.removeRelationshipItemsFromCache(item2)) tap(() => this.removeRelationshipItemsFromCache(item2))
); ) as Observable<RestResponse>;
} }
/** /**

View File

@@ -21,6 +21,7 @@ import {
} from './request.models'; } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { RequestEntry } from './request.reducer';
describe('RequestService', () => { describe('RequestService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;
@@ -107,7 +108,7 @@ describe('RequestService', () => {
beforeEach(() => { beforeEach(() => {
spyOn(service, 'getByHref').and.returnValue(observableOf({ spyOn(service, 'getByHref').and.returnValue(observableOf({
completed: false completed: false
})) } as RequestEntry))
}); });
it('should return true', () => { it('should return true', () => {
@@ -122,7 +123,7 @@ describe('RequestService', () => {
beforeEach(() => { beforeEach(() => {
spyOn(service, 'getByHref').and.returnValues(observableOf({ spyOn(service, 'getByHref').and.returnValues(observableOf({
completed: true completed: true
})); } as RequestEntry));
}); });
it('should return false', () => { it('should return false', () => {
@@ -432,7 +433,7 @@ describe('RequestService', () => {
let valid; let valid;
const requestEntry = { completed: false }; const requestEntry = { completed: false };
beforeEach(() => { beforeEach(() => {
spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry)); spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry as RequestEntry));
valid = serviceAsAny.isValid(requestEntry); valid = serviceAsAny.isValid(requestEntry);
}); });
it('return an observable emitting false', () => { it('return an observable emitting false', () => {
@@ -444,7 +445,7 @@ describe('RequestService', () => {
let valid; let valid;
const requestEntry = { completed: true, response: { isSuccessful: false } }; const requestEntry = { completed: true, response: { isSuccessful: false } };
beforeEach(() => { beforeEach(() => {
spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry)); spyOn(service, 'getByUUID').and.returnValue(observableOf(requestEntry as RequestEntry));
valid = serviceAsAny.isValid(requestEntry); valid = serviceAsAny.isValid(requestEntry);
}); });
it('return an observable emitting false', () => { it('return an observable emitting false', () => {
@@ -470,7 +471,7 @@ describe('RequestService', () => {
beforeEach(() => { beforeEach(() => {
spyOn(Date.prototype, 'getTime').and.returnValue(now); 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); valid = serviceAsAny.isValid(requestEntry);
}); });
@@ -497,7 +498,7 @@ describe('RequestService', () => {
}; };
beforeEach(() => { beforeEach(() => {
spyOn(Date.prototype, 'getTime').and.returnValue(now); 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); valid = serviceAsAny.isValid(requestEntry);
}); });

View File

@@ -5,8 +5,6 @@ import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { Observable, race as observableRace } from 'rxjs'; import { Observable, race as observableRace } from 'rxjs';
import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators'; import { filter, map, mergeMap, switchMap, take } from 'rxjs/operators';
import { cloneDeep, remove } from 'lodash'; import { cloneDeep, remove } from 'lodash';
import { AppState } from '../../app.reducer';
import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util'; import { hasValue, isEmpty, isNotEmpty } from '../../shared/empty.util';
import { CacheableObject } from '../cache/object-cache.reducer'; import { CacheableObject } from '../cache/object-cache.reducer';
import { ObjectCacheService } from '../cache/object-cache.service'; import { ObjectCacheService } from '../cache/object-cache.service';
@@ -19,7 +17,7 @@ import {
} from '../index/index.selectors'; } from '../index/index.selectors';
import { UUIDService } from '../shared/uuid.service'; import { UUIDService } from '../shared/uuid.service';
import { RequestConfigureAction, RequestExecuteAction, RequestRemoveAction } from './request.actions'; 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 { RequestEntry, RequestState } from './request.reducer';
import { CommitSSBAction } from '../cache/server-sync-buffer.actions'; import { CommitSSBAction } from '../cache/server-sync-buffer.actions';
import { RestRequestMethod } from './rest-request-method'; import { RestRequestMethod } from './rest-request-method';
@@ -52,7 +50,7 @@ const entryFromUUIDSelector = (uuid: string): MemoizedSelector<CoreState, Reques
* @param href Substring that the request's href should contain * @param href Substring that the request's href should contain
*/ */
const uuidsFromHrefSubstringSelector = const uuidsFromHrefSubstringSelector =
(selector: MemoizedSelector<AppState, IndexState>, href: string): MemoizedSelector<AppState, string[]> => createSelector( (selector: MemoizedSelector<CoreState, IndexState>, href: string): MemoizedSelector<CoreState, string[]> => createSelector(
selector, selector,
(state: IndexState) => getUuidsFromHrefSubstring(state, href) (state: IndexState) => getUuidsFromHrefSubstring(state, href)
); );

View File

@@ -1,6 +1,6 @@
import { Action } from '@ngrx/store'; import { Action } from '@ngrx/store';
import { type } from '../ngrx/type'; import { type } from '../../shared/ngrx/type';
export const HistoryActionTypes = { export const HistoryActionTypes = {
ADD_TO_HISTORY: type('dspace/history/ADD_TO_HISTORY'), ADD_TO_HISTORY: type('dspace/history/ADD_TO_HISTORY'),

View File

@@ -0,0 +1,3 @@
import { CoreState } from '../core.reducers';
export const historySelector = (state: CoreState) => state.history;

View File

@@ -1,5 +1,4 @@
import { createSelector, MemoizedSelector } from '@ngrx/store'; import { createSelector, MemoizedSelector } from '@ngrx/store';
import { AppState } from '../../app.reducer';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { CoreState } from '../core.reducers'; import { CoreState } from '../core.reducers';
import { coreSelector } from '../core.selectors'; import { coreSelector } from '../core.selectors';
@@ -11,7 +10,7 @@ import { IndexName, IndexState, MetaIndexState } from './index.reducer';
* @returns * @returns
* a MemoizedSelector to select the MetaIndexState * a MemoizedSelector to select the MetaIndexState
*/ */
export const metaIndexSelector: MemoizedSelector<AppState, MetaIndexState> = createSelector( export const metaIndexSelector: MemoizedSelector<CoreState, MetaIndexState> = createSelector(
coreSelector, coreSelector,
(state: CoreState) => state.index (state: CoreState) => state.index
); );
@@ -23,7 +22,7 @@ export const metaIndexSelector: MemoizedSelector<AppState, MetaIndexState> = cre
* @returns * @returns
* a MemoizedSelector to select the object index * a MemoizedSelector to select the object index
*/ */
export const objectIndexSelector: MemoizedSelector<AppState, IndexState> = createSelector( export const objectIndexSelector: MemoizedSelector<CoreState, IndexState> = createSelector(
metaIndexSelector, metaIndexSelector,
(state: MetaIndexState) => state[IndexName.OBJECT] (state: MetaIndexState) => state[IndexName.OBJECT]
); );
@@ -34,7 +33,7 @@ export const objectIndexSelector: MemoizedSelector<AppState, IndexState> = creat
* @returns * @returns
* a MemoizedSelector to select the request index * a MemoizedSelector to select the request index
*/ */
export const requestIndexSelector: MemoizedSelector<AppState, IndexState> = createSelector( export const requestIndexSelector: MemoizedSelector<CoreState, IndexState> = createSelector(
metaIndexSelector, metaIndexSelector,
(state: MetaIndexState) => state[IndexName.REQUEST] (state: MetaIndexState) => state[IndexName.REQUEST]
); );
@@ -45,7 +44,7 @@ export const requestIndexSelector: MemoizedSelector<AppState, IndexState> = crea
* @returns * @returns
* a MemoizedSelector to select the request UUID mapping * a MemoizedSelector to select the request UUID mapping
*/ */
export const requestUUIDIndexSelector: MemoizedSelector<AppState, IndexState> = createSelector( export const requestUUIDIndexSelector: MemoizedSelector<CoreState, IndexState> = createSelector(
metaIndexSelector, metaIndexSelector,
(state: MetaIndexState) => state[IndexName.UUID_MAPPING] (state: MetaIndexState) => state[IndexName.UUID_MAPPING]
); );
@@ -53,14 +52,13 @@ export const requestUUIDIndexSelector: MemoizedSelector<AppState, IndexState> =
/** /**
* Return the self link of an object in the object-cache based on its UUID * 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 * 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 * @returns
* a MemoizedSelector to select the self link * a MemoizedSelector to select the self link
*/ */
export const selfLinkFromUuidSelector = export const selfLinkFromUuidSelector =
(uuid: string): MemoizedSelector<AppState, string> => createSelector( (uuid: string): MemoizedSelector<CoreState, string> => createSelector(
objectIndexSelector, objectIndexSelector,
(state: IndexState) => hasValue(state) ? state[uuid] : undefined (state: IndexState) => hasValue(state) ? state[uuid] : undefined
); );
@@ -74,7 +72,7 @@ export const selfLinkFromUuidSelector =
* a MemoizedSelector to select the UUID * a MemoizedSelector to select the UUID
*/ */
export const uuidFromHrefSelector = export const uuidFromHrefSelector =
(href: string): MemoizedSelector<AppState, string> => createSelector( (href: string): MemoizedSelector<CoreState, string> => createSelector(
requestIndexSelector, requestIndexSelector,
(state: IndexState) => hasValue(state) ? state[href] : undefined (state: IndexState) => hasValue(state) ? state[href] : undefined
); );
@@ -89,7 +87,7 @@ export const uuidFromHrefSelector =
* a MemoizedSelector to select the UUID of the cached request * a MemoizedSelector to select the UUID of the cached request
*/ */
export const originalRequestUUIDFromRequestUUIDSelector = export const originalRequestUUIDFromRequestUUIDSelector =
(uuid: string): MemoizedSelector<AppState, string> => createSelector( (uuid: string): MemoizedSelector<CoreState, string> => createSelector(
requestUUIDIndexSelector, requestUUIDIndexSelector,
(state: IndexState) => hasValue(state) ? state[uuid] : undefined (state: IndexState) => hasValue(state) ? state[uuid] : undefined
); );

View File

@@ -17,7 +17,6 @@ import {
} from './json-patch-operations.actions'; } from './json-patch-operations.actions';
import { JsonPatchOperationModel } from './json-patch.model'; import { JsonPatchOperationModel } from './json-patch.model';
import { getResponseFromEntry } from '../shared/operators'; import { getResponseFromEntry } from '../shared/operators';
import { ObjectCacheEntry } from '../cache/object-cache.reducer';
/** /**
* An abstract class that provides methods to make JSON Patch requests. * An abstract class that provides methods to make JSON Patch requests.
@@ -88,8 +87,8 @@ export abstract class JsonPatchOperationsService<ResponseDefinitionDomain, Patch
flatMap(() => { flatMap(() => {
const [successResponse$, errorResponse$] = partition((response: RestResponse) => response.isSuccessful)(this.requestService.getByUUID(requestId).pipe( const [successResponse$, errorResponse$] = partition((response: RestResponse) => response.isSuccessful)(this.requestService.getByUUID(requestId).pipe(
getResponseFromEntry(), getResponseFromEntry(),
find((entry: ObjectCacheEntry) => startTransactionTime < entry.timeAdded), find((entry: RestResponse) => startTransactionTime < entry.timeAdded),
map((entry: ObjectCacheEntry) => entry), map((entry: RestResponse) => entry),
)); ));
return observableMerge( return observableMerge(
errorResponse$.pipe( errorResponse$.pipe(

View File

@@ -8,7 +8,7 @@ import { getTestScheduler, hot } from 'jasmine-marbles';
import { RouteService } from './route.service'; import { RouteService } from './route.service';
import { MockRouter } from '../../shared/mocks/mock-router'; import { MockRouter } from '../../shared/mocks/mock-router';
import { TestScheduler } from 'rxjs/testing'; import { TestScheduler } from 'rxjs/testing';
import { AddUrlToHistoryAction } from '../../shared/history/history.actions'; import { AddUrlToHistoryAction } from '../history/history.actions';
describe('RouteService', () => { describe('RouteService', () => {
let scheduler: TestScheduler; let scheduler: TestScheduler;

View File

@@ -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 { Injectable } from '@angular/core';
import { import { ActivatedRoute, NavigationEnd, Params, Router, RouterStateSnapshot, } from '@angular/router';
ActivatedRoute,
NavigationEnd,
Params,
Router,
RouterStateSnapshot,
} from '@angular/router';
import { combineLatest, Observable } from 'rxjs'; import { combineLatest, Observable } from 'rxjs';
import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store';
import { isEqual } from 'lodash'; import { isEqual } from 'lodash';
import { import { AddParameterAction, SetParameterAction, SetParametersAction, SetQueryParametersAction } from './route.actions';
AddParameterAction, import { CoreState } from '../core.reducers';
SetParameterAction, import { coreSelector } from '../core.selectors';
SetParametersAction,
SetQueryParametersAction
} from './route.actions';
import { CoreState } from '../../core/core.reducers';
import { coreSelector } from '../../core/core.selectors';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
import { historySelector } from '../../shared/history/selectors'; import { historySelector } from '../history/selectors';
import { AddUrlToHistoryAction } from '../../shared/history/history.actions'; import { AddUrlToHistoryAction } from '../history/history.actions';
/** /**
* Selector to select all route parameters from the store * 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) { public addParameter(key, value) {
this.store.dispatch(new AddParameterAction(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) { public setParameter(key, value) {
this.store.dispatch(new SetParameterAction(key, value)); this.store.dispatch(new SetParameterAction(key, value));
} }

View File

@@ -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<string | GenericConstructor<ListableObject>> {
return [this.constructor as GenericConstructor<ListableObject>];
}
}

View File

@@ -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;
}

View File

@@ -25,6 +25,7 @@ import { PersonInputSuggestionsComponent } from './submission/item-list-elements
import { NameVariantModalComponent } from './submission/name-variant-modal/name-variant-modal.component'; 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 { 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 { 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 = [ const ENTRY_COMPONENTS = [
OrgUnitComponent, OrgUnitComponent,
@@ -48,7 +49,8 @@ const ENTRY_COMPONENTS = [
PersonInputSuggestionsComponent, PersonInputSuggestionsComponent,
NameVariantModalComponent, NameVariantModalComponent,
OrgUnitSearchResultListSubmissionElementComponent, OrgUnitSearchResultListSubmissionElementComponent,
OrgUnitInputSuggestionsComponent OrgUnitInputSuggestionsComponent,
ExternalSourceEntryListSubmissionElementComponent
]; ];
@NgModule({ @NgModule({

View File

@@ -0,0 +1,2 @@
<div>{{object.display}}</div>
<div *ngIf="uri"><a target="_blank" [href]="uri.value">{{uri.value}}</a></div>

View File

@@ -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<ExternalSourceEntryListSubmissionElementComponent>;
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);
});
});

View File

@@ -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<ExternalSourceEntry> implements OnInit {
/**
* The metadata value for the object's uri
*/
uri: MetadataValue;
ngOnInit(): void {
this.uri = Metadata.first(this.object.metadata, 'dc.identifier.uri');
}
}

View File

@@ -10,7 +10,13 @@ import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap';
templateUrl: './name-variant-modal.component.html', templateUrl: './name-variant-modal.component.html',
styleUrls: ['./name-variant-modal.component.scss'] styleUrls: ['./name-variant-modal.component.scss']
}) })
/**
* The component for the modal to add a name variant to an item
*/
export class NameVariantModalComponent { export class NameVariantModalComponent {
/**
* The name variant
*/
@Input() value: string; @Input() value: string;
constructor(public modal: NgbActiveModal) { constructor(public modal: NgbActiveModal) {

View File

@@ -1,6 +1,6 @@
<ul class="navbar-nav" [ngClass]="{'mr-auto': (isXsOrSm$ | async)}"> <ul class="navbar-nav" [ngClass]="{'mr-auto': (isXsOrSm$ | async)}">
<li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item" (click)="$event.stopPropagation();"> <li *ngIf="!(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item" (click)="$event.stopPropagation();">
<div ngbDropdown placement="bottom-right" class="d-inline-block" @fadeInOut> <div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="#" id="dropdownLogin" (click)="$event.preventDefault()" ngbDropdownToggle class="px-1">{{ 'nav.login' | translate }}</a> <a href="#" id="dropdownLogin" (click)="$event.preventDefault()" ngbDropdownToggle class="px-1">{{ 'nav.login' | translate }}</a>
<div id="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu aria-labelledby="dropdownLogin"> <div id="loginDropdownMenu" [ngClass]="{'pl-3 pr-3': (loading | async)}" ngbDropdownMenu aria-labelledby="dropdownLogin">
<ds-log-in <ds-log-in
@@ -12,7 +12,7 @@
<a id="loginLink" routerLink="/login" routerLinkActive="active" class="px-1" >{{ 'nav.login' | translate }}<span class="sr-only">(current)</span></a> <a id="loginLink" routerLink="/login" routerLinkActive="active" class="px-1" >{{ 'nav.login' | translate }}<span class="sr-only">(current)</span></a>
</li> </li>
<li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item"> <li *ngIf="(isAuthenticated | async) && !(isXsOrSm$ | async) && (showAuth | async)" class="nav-item">
<div ngbDropdown placement="bottom-right" class="d-inline-block" @fadeInOut> <div ngbDropdown display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="#" id="dropdownUser" (click)="$event.preventDefault()" class="px-1" ngbDropdownToggle><i class="fas fa-user-circle fa-lg fa-fw" [title]="'nav.logout' | translate"></i></a> <a href="#" id="dropdownUser" (click)="$event.preventDefault()" class="px-1" ngbDropdownToggle><i class="fas fa-user-circle fa-lg fa-fw" [title]="'nav.logout' | translate"></i></a>
<div id="logoutDropdownMenu" ngbDropdownMenu aria-labelledby="dropdownUser"> <div id="logoutDropdownMenu" ngbDropdownMenu aria-labelledby="dropdownUser">
<ds-user-menu></ds-user-menu> <ds-user-menu></ds-user-menu>

View File

@@ -1,3 +1,38 @@
<div class="container-fluid">
<div class="row">
<div class="col-12 d-inline-block">
<label>{{type.value + '.edit.logo.label' | translate}}</label>
</div>
<ng-container *ngVar="(dso?.logo | async)?.payload as logo">
<div class="col-12 d-inline-block alert" [ngClass]="{'alert-danger': markLogoForDeletion}" id="logo-section">
<div class="row">
<div class="col-8 d-inline-block">
<ds-comcol-page-logo [logo]="logo"></ds-comcol-page-logo>
</div>
<div class="col-4 d-inline-block">
<div *ngIf="logo" class="btn-group btn-group-sm float-right" role="group">
<button *ngIf="!markLogoForDeletion" type="button" class="btn btn-danger" (click)="deleteLogo()">
<i class="fas fa-trash" aria-hidden="true"></i>
</button>
<button *ngIf="markLogoForDeletion" type="button" class="btn btn-warning" (click)="undoDeleteLogo()">
<i class="fas fa-undo" aria-hidden="true"></i>
</button>
</div>
</div>
</div>
</div>
<div *ngIf="!logo" class="col-12 d-inline-block">
<ds-uploader *ngIf="initializedUploaderOptions | async"
[dropMsg]="type.value + '.edit.logo.upload'"
[dropOverDocumentMsg]="type.value + '.edit.logo.upload'"
[enableDragOverDocument]="true"
[uploadFilesOptions]="uploadFilesOptions"
(onCompleteItem)="onCompleteItem()"
(onUploadError)="onUploadError()"></ds-uploader>
</div>
</ng-container>
</div>
</div>
<ds-form *ngIf="formModel" <ds-form *ngIf="formModel"
[formId]="'comcol-form-id'" [formId]="'comcol-form-id'"
[formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form> [formModel]="formModel" (submitForm)="onSubmit()" (cancel)="onCancel()"></ds-form>

View File

@@ -3,14 +3,25 @@ import { TranslateModule } from '@ngx-translate/core';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core'; import { DynamicFormControlModel, DynamicFormService, DynamicInputModel } from '@ng-dynamic-forms/core';
import { FormControl, FormGroup } from '@angular/forms'; import { FormControl, FormGroup } from '@angular/forms';
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
import { Community } from '../../../core/shared/community.model'; import { Community } from '../../../core/shared/community.model';
import { ResourceType } from '../../../core/shared/resource-type';
import { ComColFormComponent } from './comcol-form.component'; import { ComColFormComponent } from './comcol-form.component';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { hasValue } from '../../empty.util'; import { hasValue } from '../../empty.util';
import { VarDirective } from '../../utils/var.directive';
import { NotificationsService } from '../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../testing/notifications-service-stub';
import { AuthService } from '../../../core/auth/auth.service';
import { AuthServiceMock } from '../../mocks/mock-auth.service';
import { of as observableOf } from 'rxjs';
import { RemoteData } from '../../../core/data/remote-data';
import { RestRequestMethod } from '../../../core/data/rest-request-method';
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
import { RequestError } from '../../../core/data/request.models';
import { RequestService } from '../../../core/data/request.service';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
import { By } from '@angular/platform-browser';
describe('ComColFormComponent', () => { describe('ComColFormComponent', () => {
let comp: ComColFormComponent<DSpaceObject>; let comp: ComColFormComponent<DSpaceObject>;
@@ -49,29 +60,47 @@ describe('ComColFormComponent', () => {
}) })
]; ];
const logoEndpoint = 'rest/api/logo/endpoint';
const dsoService = Object.assign({
getLogoEndpoint: () => observableOf(logoEndpoint),
deleteLogo: () => observableOf({})
});
const notificationsService = new NotificationsServiceStub();
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
const locationStub = jasmine.createSpyObj('location', ['back']); const locationStub = jasmine.createSpyObj('location', ['back']);
/* tslint:enable:no-empty */ /* tslint:enable:no-empty */
const requestServiceStub = jasmine.createSpyObj({
removeByHrefSubstring: {}
});
const objectCacheStub = jasmine.createSpyObj({
remove: {}
});
beforeEach(async(() => { beforeEach(async(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule], imports: [TranslateModule.forRoot(), RouterTestingModule],
declarations: [ComColFormComponent], declarations: [ComColFormComponent, VarDirective],
providers: [ providers: [
{ provide: Location, useValue: locationStub }, { provide: Location, useValue: locationStub },
{ provide: DynamicFormService, useValue: formServiceStub } { provide: DynamicFormService, useValue: formServiceStub },
{ provide: NotificationsService, useValue: notificationsService },
{ provide: AuthService, useValue: new AuthServiceMock() },
{ provide: RequestService, useValue: requestServiceStub },
{ provide: ObjectCacheService, useValue: objectCacheStub }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
})); }));
describe('when the dso doesn\'t contain an ID (newly created)', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(ComColFormComponent); initComponent(new Community());
comp = fixture.componentInstance; });
comp.formModel = [];
comp.dso = new Community(); it('should initialize the uploadFilesOptions with a placeholder url', () => {
fixture.detectChanges(); expect(comp.uploadFilesOptions.url.length).toBeGreaterThan(0);
location = (comp as any).location;
}); });
describe('onSubmit', () => { describe('onSubmit', () => {
@@ -94,7 +123,8 @@ describe('ComColFormComponent', () => {
comp.onSubmit(); comp.onSubmit();
expect(comp.submitForm.emit).toHaveBeenCalledWith( expect(comp.submitForm.emit).toHaveBeenCalledWith(
Object.assign( {
dso: Object.assign(
{}, {},
new Community(), new Community(),
{ {
@@ -105,7 +135,10 @@ describe('ComColFormComponent', () => {
}, },
type: Community.type type: Community.type
}, },
) ),
uploader: {} as any,
deleteLogo: false
}
); );
}) })
}); });
@@ -116,4 +149,175 @@ describe('ComColFormComponent', () => {
expect(locationStub.back).toHaveBeenCalled(); expect(locationStub.back).toHaveBeenCalled();
}); });
}); });
describe('onCompleteItem', () => {
beforeEach(() => {
spyOn(comp.finish, 'emit');
comp.onCompleteItem();
});
it('should show a success notification', () => {
expect(notificationsService.success).toHaveBeenCalled();
});
it('should emit finish', () => {
expect(comp.finish.emit).toHaveBeenCalled();
});
it('should remove the object\'s cache', () => {
expect(requestServiceStub.removeByHrefSubstring).toHaveBeenCalled();
expect(objectCacheStub.remove).toHaveBeenCalled();
});
});
describe('onUploadError', () => {
beforeEach(() => {
spyOn(comp.finish, 'emit');
comp.onUploadError();
});
it('should show an error notification', () => {
expect(notificationsService.error).toHaveBeenCalled();
});
it('should emit finish', () => {
expect(comp.finish.emit).toHaveBeenCalled();
});
});
});
describe('when the dso contains an ID (being edited)', () => {
describe('and the dso doesn\'t contain a logo', () => {
beforeEach(() => {
initComponent(Object.assign(new Community(), {
id: 'community-id',
logo: observableOf(new RemoteData(false, false, true, null, undefined))
}));
});
it('should initialize the uploadFilesOptions with the logo\'s endpoint url', () => {
expect(comp.uploadFilesOptions.url).toEqual(logoEndpoint);
});
it('should initialize the uploadFilesOptions with a POST method', () => {
expect(comp.uploadFilesOptions.method).toEqual(RestRequestMethod.POST);
});
});
describe('and the dso contains a logo', () => {
beforeEach(() => {
initComponent(Object.assign(new Community(), {
id: 'community-id',
logo: observableOf(new RemoteData(false, false, true, null, {}))
}));
});
it('should initialize the uploadFilesOptions with the logo\'s endpoint url', () => {
expect(comp.uploadFilesOptions.url).toEqual(logoEndpoint);
});
it('should initialize the uploadFilesOptions with a PUT method', () => {
expect(comp.uploadFilesOptions.method).toEqual(RestRequestMethod.PUT);
});
describe('submit with logo marked for deletion', () => {
beforeEach(() => {
comp.markLogoForDeletion = true;
});
describe('when dsoService.deleteLogo returns a successful response', () => {
const response = new RestResponse(true, 200, 'OK');
beforeEach(() => {
spyOn(dsoService, 'deleteLogo').and.returnValue(observableOf(response));
comp.onSubmit();
});
it('should display a success notification', () => {
expect(notificationsService.success).toHaveBeenCalled();
});
});
describe('when dsoService.deleteLogo returns an error response', () => {
const response = new ErrorResponse(new RequestError('errorMessage'));
beforeEach(() => {
spyOn(dsoService, 'deleteLogo').and.returnValue(observableOf(response));
comp.onSubmit();
});
it('should display an error notification', () => {
expect(notificationsService.error).toHaveBeenCalled();
});
});
});
describe('deleteLogo', () => {
beforeEach(() => {
comp.deleteLogo();
fixture.detectChanges();
});
it('should set markLogoForDeletion to true', () => {
expect(comp.markLogoForDeletion).toEqual(true);
});
it('should mark the logo section with a danger alert', () => {
const logoSection = fixture.debugElement.query(By.css('#logo-section.alert-danger'));
expect(logoSection).toBeTruthy();
});
it('should hide the delete button', () => {
const button = fixture.debugElement.query(By.css('#logo-section .btn-danger'));
expect(button).not.toBeTruthy();
});
it('should show the undo button', () => {
const button = fixture.debugElement.query(By.css('#logo-section .btn-warning'));
expect(button).toBeTruthy();
});
});
describe('undoDeleteLogo', () => {
beforeEach(() => {
comp.markLogoForDeletion = true;
comp.undoDeleteLogo();
fixture.detectChanges();
});
it('should set markLogoForDeletion to false', () => {
expect(comp.markLogoForDeletion).toEqual(false);
});
it('should disable the danger alert on the logo section', () => {
const logoSection = fixture.debugElement.query(By.css('#logo-section.alert-danger'));
expect(logoSection).not.toBeTruthy();
});
it('should show the delete button', () => {
const button = fixture.debugElement.query(By.css('#logo-section .btn-danger'));
expect(button).toBeTruthy();
});
it('should hide the undo button', () => {
const button = fixture.debugElement.query(By.css('#logo-section .btn-warning'));
expect(button).not.toBeTruthy();
});
});
});
});
function initComponent(dso: Community) {
fixture = TestBed.createComponent(ComColFormComponent);
comp = fixture.componentInstance;
comp.formModel = [];
comp.dso = dso;
(comp as any).type = Community.type;
comp.uploaderComponent = Object.assign({
uploader: {}
});
(comp as any).dsoService = dsoService;
fixture.detectChanges();
location = (comp as any).location;
}
}); });

View File

@@ -1,17 +1,34 @@
import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; import { Component, EventEmitter, Input, OnDestroy, OnInit, Output, ViewChild } from '@angular/core';
import { Location } from '@angular/common'; import { Location } from '@angular/common';
import { import {
DynamicFormControlModel,
DynamicFormService, DynamicFormService,
DynamicInputModel DynamicInputModel
} from '@ng-dynamic-forms/core'; } from '@ng-dynamic-forms/core';
import { FormGroup } from '@angular/forms'; import { FormGroup } from '@angular/forms';
import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model';
import { TranslateService } from '@ngx-translate/core'; import { TranslateService } from '@ngx-translate/core';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.models'; import { MetadataMap, MetadataValue } from '../../../core/shared/metadata.models';
import { ResourceType } from '../../../core/shared/resource-type'; import { ResourceType } from '../../../core/shared/resource-type';
import { isNotEmpty } from '../../empty.util'; import { hasValue, isNotEmpty } from '../../empty.util';
import { UploaderOptions } from '../../uploader/uploader-options.model';
import { NotificationsService } from '../../notifications/notifications.service';
import { ComColDataService } from '../../../core/data/comcol-data.service';
import { Subscription } from 'rxjs/internal/Subscription';
import { AuthService } from '../../../core/auth/auth.service';
import { Community } from '../../../core/shared/community.model'; import { Community } from '../../../core/shared/community.model';
import { Collection } from '../../../core/shared/collection.model';
import { UploaderComponent } from '../../uploader/uploader.component';
import { FileUploader } from 'ng2-file-upload';
import { ErrorResponse, RestResponse } from '../../../core/cache/response.models';
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
import { RemoteData } from '../../../core/data/remote-data';
import { Bitstream } from '../../../core/shared/bitstream.model';
import { combineLatest as observableCombineLatest } from 'rxjs';
import { RestRequestMethod } from '../../../core/data/rest-request-method';
import { RequestService } from '../../../core/data/request.service';
import { ObjectCacheService } from '../../../core/cache/object-cache.service';
/** /**
* A form for creating and editing Communities or Collections * A form for creating and editing Communities or Collections
@@ -21,7 +38,13 @@ import { Community } from '../../../core/shared/community.model';
styleUrls: ['./comcol-form.component.scss'], styleUrls: ['./comcol-form.component.scss'],
templateUrl: './comcol-form.component.html' templateUrl: './comcol-form.component.html'
}) })
export class ComColFormComponent<T extends DSpaceObject> implements OnInit { export class ComColFormComponent<T extends DSpaceObject> implements OnInit, OnDestroy {
/**
* The logo uploader component
*/
@ViewChild(UploaderComponent) uploaderComponent: UploaderComponent;
/** /**
* DSpaceObject that the form represents * DSpaceObject that the form represents
*/ */
@@ -30,7 +53,7 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
/** /**
* Type of DSpaceObject that the form represents * Type of DSpaceObject that the form represents
*/ */
protected type: ResourceType; type: ResourceType;
/** /**
* @type {string} Key prefix used to generate form labels * @type {string} Key prefix used to generate form labels
@@ -53,14 +76,56 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
formGroup: FormGroup; formGroup: FormGroup;
/** /**
* Emits DSO when the form is submitted * The uploader configuration options
* @type {EventEmitter<any>} * @type {UploaderOptions}
*/ */
@Output() submitForm: EventEmitter<any> = new EventEmitter(); uploadFilesOptions: UploaderOptions = Object.assign(new UploaderOptions(), {
autoUpload: false
});
public constructor(private location: Location, /**
private formService: DynamicFormService, * Emits DSO and Uploader when the form is submitted
private translate: TranslateService) { */
@Output() submitForm: EventEmitter<{
dso: T,
uploader: FileUploader,
deleteLogo: boolean
}> = new EventEmitter();
/**
* Fires an event when the logo has finished uploading (with or without errors) or was removed
*/
@Output() finish: EventEmitter<any> = new EventEmitter();
/**
* Observable keeping track whether or not the uploader has finished initializing
* Used to start rendering the uploader component
*/
initializedUploaderOptions = new BehaviorSubject(false);
/**
* Is the logo marked to be deleted?
*/
markLogoForDeletion = false;
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
protected subs: Subscription[] = [];
/**
* The service used to fetch from or send data to
*/
protected dsoService: ComColDataService<Community | Collection>;
public constructor(protected location: Location,
protected formService: DynamicFormService,
protected translate: TranslateService,
protected notificationsService: NotificationsService,
protected authService: AuthService,
protected requestService: RequestService,
protected objectCache: ObjectCacheService) {
} }
ngOnInit(): void { ngOnInit(): void {
@@ -76,13 +141,56 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
.subscribe(() => { .subscribe(() => {
this.updateFieldTranslations(); this.updateFieldTranslations();
}); });
if (hasValue(this.dso.id)) {
this.subs.push(
observableCombineLatest(
this.dsoService.getLogoEndpoint(this.dso.id),
(this.dso as any).logo
).subscribe(([href, logoRD]: [string, RemoteData<Bitstream>]) => {
this.uploadFilesOptions.url = href;
this.uploadFilesOptions.authToken = this.authService.buildAuthHeader();
// If the object already contains a logo, send out a PUT request instead of POST for setting a new logo
if (hasValue(logoRD.payload)) {
this.uploadFilesOptions.method = RestRequestMethod.PUT;
}
this.initializedUploaderOptions.next(true);
})
);
} else {
// Set a placeholder URL to not break the uploader component. This will be replaced once the object is created.
this.uploadFilesOptions.url = 'placeholder';
this.uploadFilesOptions.authToken = this.authService.buildAuthHeader();
this.initializedUploaderOptions.next(true);
}
} }
/** /**
* Checks which new fields were added and sends the updated version of the DSO to the parent component * Checks which new fields were added and sends the updated version of the DSO to the parent component
*/ */
onSubmit() { onSubmit() {
const formMetadata = new Object() as MetadataMap; if (this.markLogoForDeletion && hasValue(this.dso.id)) {
this.dsoService.deleteLogo(this.dso).subscribe((response: RestResponse) => {
if (response.isSuccessful) {
this.notificationsService.success(
this.translate.get(this.type.value + '.edit.logo.notifications.delete.success.title'),
this.translate.get(this.type.value + '.edit.logo.notifications.delete.success.content')
);
} else {
const errorResponse = response as ErrorResponse;
this.notificationsService.error(
this.translate.get(this.type.value + '.edit.logo.notifications.delete.error.title'),
errorResponse.errorMessage
);
}
(this.dso as any).logo = undefined;
this.uploadFilesOptions.method = RestRequestMethod.POST;
this.refreshCache();
this.finish.emit();
});
}
const formMetadata = {} as MetadataMap;
this.formModel.forEach((fieldModel: DynamicInputModel) => { this.formModel.forEach((fieldModel: DynamicInputModel) => {
const value: MetadataValue = { const value: MetadataValue = {
value: fieldModel.value as string, value: fieldModel.value as string,
@@ -102,7 +210,11 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
}, },
type: Community.type type: Community.type
}); });
this.submitForm.emit(updatedDSO); this.submitForm.emit({
dso: updatedDSO,
uploader: hasValue(this.uploaderComponent) ? this.uploaderComponent.uploader : undefined,
deleteLogo: this.markLogoForDeletion
});
} }
/** /**
@@ -122,7 +234,59 @@ export class ComColFormComponent<T extends DSpaceObject> implements OnInit {
); );
} }
/**
* Mark the logo to be deleted
* Send out a delete request to remove the logo from the community/collection and display notifications
*/
deleteLogo() {
this.markLogoForDeletion = true;
}
/**
* Undo marking the logo to be deleted
*/
undoDeleteLogo() {
this.markLogoForDeletion = false;
}
/**
* Refresh the object's cache to ensure the latest version
*/
private refreshCache() {
this.requestService.removeByHrefSubstring(this.dso.self);
this.objectCache.remove(this.dso.self);
}
/**
* The request was successful, display a success notification
*/
public onCompleteItem() {
this.refreshCache();
this.notificationsService.success(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.success'));
this.finish.emit();
}
/**
* The request was unsuccessful, display an error notification
*/
public onUploadError() {
this.notificationsService.error(null, this.translate.get(this.type.value + '.edit.logo.notifications.add.error'));
this.finish.emit();
}
/**
* Cancel the form and return to the previous page
*/
onCancel() { onCancel() {
this.location.back(); this.location.back();
} }
/**
* Unsubscribe from open subscriptions
*/
ngOnDestroy(): void {
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
}
} }

View File

@@ -11,11 +11,13 @@ import { RouterTestingModule } from '@angular/router/testing';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model';
import { CreateComColPageComponent } from './create-comcol-page.component'; import { CreateComColPageComponent } from './create-comcol-page.component';
import { DataService } from '../../../core/data/data.service';
import { import {
createFailedRemoteDataObject$, createFailedRemoteDataObject$,
createSuccessfulRemoteDataObject$ createSuccessfulRemoteDataObject$
} from '../../testing/utils'; } from '../../testing/utils';
import { ComColDataService } from '../../../core/data/comcol-data.service';
import { NotificationsService } from '../../notifications/notifications.service';
import { NotificationsServiceStub } from '../../testing/notifications-service-stub';
describe('CreateComColPageComponent', () => { describe('CreateComColPageComponent', () => {
let comp: CreateComColPageComponent<DSpaceObject>; let comp: CreateComColPageComponent<DSpaceObject>;
@@ -31,6 +33,8 @@ describe('CreateComColPageComponent', () => {
let routeServiceStub; let routeServiceStub;
let routerStub; let routerStub;
const logoEndpoint = 'rest/api/logo/endpoint';
function initializeVars() { function initializeVars() {
community = Object.assign(new Community(), { community = Object.assign(new Community(), {
uuid: 'a20da287-e174-466a-9926-f66b9300d347', uuid: 'a20da287-e174-466a-9926-f66b9300d347',
@@ -56,8 +60,8 @@ describe('CreateComColPageComponent', () => {
value: community.name value: community.name
}] }]
})), })),
create: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity) create: (com, uuid?) => createSuccessfulRemoteDataObject$(newCommunity),
getLogoEndpoint: () => observableOf(logoEndpoint)
}; };
routeServiceStub = { routeServiceStub = {
@@ -74,10 +78,11 @@ describe('CreateComColPageComponent', () => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule], imports: [TranslateModule.forRoot(), SharedModule, CommonModule, RouterTestingModule],
providers: [ providers: [
{ provide: DataService, useValue: communityDataServiceStub }, { provide: ComColDataService, useValue: communityDataServiceStub },
{ provide: CommunityDataService, useValue: communityDataServiceStub }, { provide: CommunityDataService, useValue: communityDataServiceStub },
{ provide: RouteService, useValue: routeServiceStub }, { provide: RouteService, useValue: routeServiceStub },
{ provide: Router, useValue: routerStub }, { provide: Router, useValue: routerStub },
{ provide: NotificationsService, useValue: new NotificationsServiceStub() }
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA]
}).compileComponents(); }).compileComponents();
@@ -86,6 +91,7 @@ describe('CreateComColPageComponent', () => {
beforeEach(() => { beforeEach(() => {
fixture = TestBed.createComponent(CreateComColPageComponent); fixture = TestBed.createComponent(CreateComColPageComponent);
comp = fixture.componentInstance; comp = fixture.componentInstance;
(comp as any).type = Community.type;
fixture.detectChanges(); fixture.detectChanges();
dsoDataService = (comp as any).dsoDataService; dsoDataService = (comp as any).dsoDataService;
communityDataService = (comp as any).communityDataService; communityDataService = (comp as any).communityDataService;
@@ -95,14 +101,28 @@ describe('CreateComColPageComponent', () => {
describe('onSubmit', () => { describe('onSubmit', () => {
let data; let data;
describe('with an empty queue in the uploader', () => {
beforeEach(() => { beforeEach(() => {
data = Object.assign(new Community(), { data = {
dso: Object.assign(new Community(), {
metadata: [{ metadata: [{
key: 'dc.title', key: 'dc.title',
value: 'test' value: 'test'
}] }]
}),
uploader: {
options: {
url: ''
},
queue: [],
/* tslint:disable:no-empty */
uploadAll: () => {}
/* tslint:enable:no-empty */
}
};
}); });
});
it('should navigate when successful', () => { it('should navigate when successful', () => {
spyOn(router, 'navigate'); spyOn(router, 'navigate');
comp.onSubmit(data); comp.onSubmit(data);
@@ -118,4 +138,49 @@ describe('CreateComColPageComponent', () => {
expect(router.navigate).not.toHaveBeenCalled(); expect(router.navigate).not.toHaveBeenCalled();
}); });
}); });
describe('with at least one item in the uploader\'s queue', () => {
beforeEach(() => {
data = {
dso: Object.assign(new Community(), {
metadata: [{
key: 'dc.title',
value: 'test'
}]
}),
uploader: {
options: {
url: ''
},
queue: [
{}
],
/* tslint:disable:no-empty */
uploadAll: () => {}
/* tslint:enable:no-empty */
}
};
});
it('should not navigate', () => {
spyOn(router, 'navigate');
comp.onSubmit(data);
fixture.detectChanges();
expect(router.navigate).not.toHaveBeenCalled();
});
it('should set the uploader\'s url to the logo\'s endpoint', () => {
comp.onSubmit(data);
fixture.detectChanges();
expect(data.uploader.options.url).toEqual(logoEndpoint);
});
it('should call the uploader\'s uploadAll', () => {
spyOn(data.uploader, 'uploadAll');
comp.onSubmit(data);
fixture.detectChanges();
expect(data.uploader.uploadAll).toHaveBeenCalled();
});
});
});
}); });

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