diff --git a/.travis.yml b/.travis.yml index 901dee8186..c42923886d 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,5 +1,5 @@ sudo: required -dist: trusty +dist: bionic env: # Install the latest docker-compose version for ci testing. @@ -12,6 +12,9 @@ env: DSPACE_REST_NAMESPACE: '/server/api' DSPACE_REST_SSL: false +services: + - xvfb + before_install: # Docker Compose Install - curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose @@ -33,14 +36,6 @@ before_script: after_script: - docker-compose -f ./docker/docker-compose-travis.yml down -addons: - apt: - sources: - - google-chrome - packages: - - dpkg - - google-chrome-stable - language: node_js node_js: @@ -53,8 +48,6 @@ cache: bundler_args: --retry 5 script: - # Use Chromium instead of Chrome. - - export CHROME_BIN=chromium-browser - yarn run build - yarn run ci - cat coverage/lcov.info | ./node_modules/coveralls/bin/coveralls.js diff --git a/e2e/app.po.ts b/e2e/app.po.ts index 54b5b55af3..c76bef118f 100644 --- a/e2e/app.po.ts +++ b/e2e/app.po.ts @@ -2,7 +2,8 @@ import { browser, element, by } from 'protractor'; export class ProtractorPage { navigateTo() { - return browser.get('/'); + return browser.get('/') + .then(() => browser.waitForAngular()); } getPageTitleText() { diff --git a/e2e/search-navbar/search-navbar.e2e-spec.ts b/e2e/search-navbar/search-navbar.e2e-spec.ts new file mode 100644 index 0000000000..b60f71919d --- /dev/null +++ b/e2e/search-navbar/search-navbar.e2e-spec.ts @@ -0,0 +1,46 @@ +import { ProtractorPage } from './search-navbar.po'; +import { browser } from 'protractor'; + +describe('protractor SearchNavbar', () => { + let page: ProtractorPage; + let queryString: string; + + beforeEach(() => { + page = new ProtractorPage(); + queryString = 'the test query'; + }); + + it('should go to search page with correct query if submitted (from home)', () => { + page.navigateToHome(); + return checkIfSearchWorks(); + }); + + it('should go to search page with correct query if submitted (from search)', () => { + page.navigateToSearch(); + return checkIfSearchWorks(); + }); + + it('check if can submit search box with pressing button', () => { + page.navigateToHome(); + page.expandAndFocusSearchBox(); + page.setCurrentQuery(queryString); + page.submitNavbarSearchForm(); + browser.wait(() => { + return browser.getCurrentUrl().then((url: string) => { + return url.indexOf('query=' + encodeURI(queryString)) !== -1; + }); + }); + }); + + function checkIfSearchWorks(): boolean { + page.setCurrentQuery(queryString); + page.submitByPressingEnter(); + browser.wait(() => { + return browser.getCurrentUrl().then((url: string) => { + return url.indexOf('query=' + encodeURI(queryString)) !== -1; + }); + }); + return false; + } + +}); diff --git a/e2e/search-navbar/search-navbar.po.ts b/e2e/search-navbar/search-navbar.po.ts new file mode 100644 index 0000000000..17112ab468 --- /dev/null +++ b/e2e/search-navbar/search-navbar.po.ts @@ -0,0 +1,40 @@ +import { browser, element, by, protractor } from 'protractor'; +import { promise } from 'selenium-webdriver'; + +export class ProtractorPage { + HOME = '/home'; + SEARCH = '/search'; + + navigateToHome() { + return browser.get(this.HOME); + } + + navigateToSearch() { + return browser.get(this.SEARCH); + } + + getCurrentQuery(): promise.Promise { + return element(by.css('#search-navbar-container form input')).getAttribute('value'); + } + + expandAndFocusSearchBox() { + element(by.css('#search-navbar-container form a')).click(); + } + + setCurrentQuery(query: string) { + element(by.css('#search-navbar-container form input[name="query"]')).sendKeys(query); + } + + submitNavbarSearchForm() { + element(by.css('#search-navbar-container form .submit-icon')).click(); + } + + submitByPressingEnter() { + element(by.css('#search-navbar-container form input[name="query"]')).sendKeys(protractor.Key.ENTER); + } + + submitByPressingEnter() { + element(by.css('#search-navbar-container form input[name="query"]')).sendKeys(protractor.Key.ENTER); + } + +} diff --git a/e2e/search-page/search-page.po.ts b/e2e/search-page/search-page.po.ts index fde3e68bf8..51bf86453b 100644 --- a/e2e/search-page/search-page.po.ts +++ b/e2e/search-page/search-page.po.ts @@ -1,4 +1,4 @@ -import { browser, element, by, protractor } from 'protractor'; +import { browser, by, element, protractor } from 'protractor'; import { promise } from 'selenium-webdriver'; export class ProtractorPage { @@ -27,15 +27,15 @@ export class ProtractorPage { } setCurrentScope(scope: string) { - element(by.css('option[value="' + scope + '"]')).click(); + element(by.css('#search-form option[value="' + scope + '"]')).click(); } setCurrentQuery(query: string) { - element(by.css('input[name="query"]')).sendKeys(query); + element(by.css('#search-form input[name="query"]')).sendKeys(query); } submitSearchForm() { - element(by.css('button.search-button')).click(); + element(by.css('#search-form button.search-button')).click(); } getRandomScopeOption(): promise.Promise { diff --git a/karma.conf.js b/karma.conf.js index 456c2ecd99..f40c8b2166 100644 --- a/karma.conf.js +++ b/karma.conf.js @@ -15,7 +15,11 @@ module.exports = function (config) { }; var configuration = { - + client: { + jasmine: { + random: false + } + }, // base path that will be used to resolve all patterns (e.g. files, exclude) basePath: '', diff --git a/package.json b/package.json index 3a54b941dd..b4a40c0a19 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,7 @@ "node": "8.* || >= 10.*" }, "resolutions": { + "serialize-javascript": ">= 2.1.2", "set-value": ">= 2.0.1" }, "scripts": { @@ -74,37 +75,38 @@ "sync-i18n": "node ./scripts/sync-i18n-files.js" }, "dependencies": { - "@angular/animations": "^6.1.4", - "@angular/cli": "^6.1.5", - "@angular/common": "^6.1.4", - "@angular/core": "^6.1.4", - "@angular/forms": "^6.1.4", - "@angular/http": "^6.1.4", - "@angular/platform-browser": "^6.1.4", - "@angular/platform-browser-dynamic": "^6.1.4", - "@angular/platform-server": "^6.1.4", - "@angular/router": "^6.1.4", + "@angular/animations": "^7.2.15", + "@angular/cdk": "7.3.7", + "@angular/cli": "^7.3.5", + "@angular/common": "^7.2.15", + "@angular/core": "^7.2.15", + "@angular/forms": "^7.2.15", + "@angular/http": "^7.2.15", + "@angular/platform-browser": "^7.2.15", + "@angular/platform-browser-dynamic": "^7.2.15", + "@angular/platform-server": "^7.2.15", + "@angular/router": "^7.2.15", "@angularclass/bootloader": "1.0.1", - "@ng-bootstrap/ng-bootstrap": "^2.0.0", - "@ng-dynamic-forms/core": "6.2.0", - "@ng-dynamic-forms/ui-ng-bootstrap": "6.2.0", - "@ngrx/effects": "^6.1.0", - "@ngrx/router-store": "^6.1.0", - "@ngrx/store": "^6.1.0", - "@nguniversal/express-engine": "6.1.0", - "@ngx-translate/core": "10.0.2", - "@ngx-translate/http-loader": "3.0.1", + "@ng-bootstrap/ng-bootstrap": "^4.1.0", + "@ng-dynamic-forms/core": "^7.1.0", + "@ng-dynamic-forms/ui-ng-bootstrap": "^7.1.0", + "@ngrx/effects": "^7.3.0", + "@ngrx/router-store": "^7.3.0", + "@ngrx/store": "^7.3.0", + "@nguniversal/express-engine": "^7.1.1", + "@ngx-translate/core": "11.0.1", + "@ngx-translate/http-loader": "4.0.0", "@nicky-lenaers/ngx-scroll-to": "^1.0.0", "angular-idle-preload": "3.0.0", "angular-sortablejs": "^2.5.0", "angular2-text-mask": "9.0.0", - "angulartics2": "^6.2.0", + "angulartics2": "7.5.2", "body-parser": "1.18.2", "bootstrap": "4.3.1", "cerialize": "0.1.18", "compression": "1.7.1", "cookie-parser": "1.4.3", - "core-js": "^2.5.7", + "core-js": "^2.6.5", "debug-loader": "^0.0.1", "express": "4.16.2", "express-session": "1.15.6", @@ -112,6 +114,7 @@ "file-saver": "^1.3.8", "font-awesome": "4.7.0", "fork-ts-checker-webpack-plugin": "^0.4.10", + "hammerjs": "^2.0.8", "http-server": "0.11.1", "https": "1.0.0", "js-cookie": "2.2.0", @@ -121,37 +124,40 @@ "jwt-decode": "^2.2.0", "methods": "1.1.2", "moment": "^2.22.1", + "moment-range": "^4.0.2", "morgan": "^1.9.1", - "ng-mocks": "^6.2.1", + "ng-mocks": "^7.6.0", "ng2-file-upload": "1.2.1", - "ng2-nouislider": "^1.7.11", + "ng2-nouislider": "^1.8.2", "ngx-bootstrap": "^3.2.0", "ngx-infinite-scroll": "6.0.1", - "ngx-moment": "^3.1.0", + "ngx-moment": "^3.4.0", "ngx-pagination": "3.0.3", "nouislider": "^11.0.0", "pem": "1.13.2", "reflect-metadata": "0.1.12", - "rxjs": "6.2.2", + "rxjs": "6.4.0", "rxjs-spy": "^7.5.1", "sass-resources-loader": "^2.0.0", "sortablejs": "1.7.0", "text-mask-core": "5.0.1", "ts-loader": "^5.2.1", "ts-md5": "^1.2.4", + "url-parse": "^1.4.7", "uuid": "^3.2.1", "webfontloader": "1.6.28", - "webpack-cli": "^3.1.0", - "zone.js": "^0.8.26" + "webpack-cli": "^3.2.0", + "zone.js": "^0.8.29" }, "devDependencies": { - "@angular/compiler": "^6.1.4", - "@angular/compiler-cli": "^6.1.4", + "@angular-devkit/build-angular": "^0.13.5", + "@angular/compiler": "^7.2.15", + "@angular/compiler-cli": "^7.2.15", "@fortawesome/fontawesome-free": "^5.5.0", - "@ngrx/entity": "^6.1.0", - "@ngrx/schematics": "^6.1.0", - "@ngrx/store-devtools": "^6.1.0", - "@ngtools/webpack": "^6.1.5", + "@ngrx/entity": "^7.3.0", + "@ngrx/schematics": "^7.3.0", + "@ngrx/store-devtools": "^7.3.0", + "@ngtools/webpack": "^7.3.9", "@schematics/angular": "^0.7.5", "@types/acorn": "^4.0.3", "@types/cookie-parser": "1.4.1", @@ -160,44 +166,47 @@ "@types/express-serve-static-core": "4.16.0", "@types/file-saver": "^1.3.0", "@types/hammerjs": "2.0.35", - "@types/jasmine": "^2.8.6", + "@types/jasmine": "^3.3.9", "@types/js-cookie": "2.1.0", "@types/json5": "^0.0.30", "@types/lodash": "^4.14.110", "@types/memory-cache": "0.2.0", "@types/mime": "2.0.0", - "@types/node": "^10.9.4", + "@types/node": "^11.11.2", "@types/serve-static": "1.13.2", "@types/uuid": "^3.4.3", "@types/webfontloader": "1.6.29", + "@typescript-eslint/eslint-plugin": "^2.12.0", + "@typescript-eslint/parser": "^2.12.0", "ajv": "^6.1.1", "ajv-keywords": "^3.1.0", "angular2-template-loader": "0.6.2", "autoprefixer": "^9.1.3", "caniuse-lite": "^1.0.30000697", "cli-progress": "^3.3.1", - "codelyzer": "^4.4.4", + "codelyzer": "^5.1.0", "commander": "^3.0.2", - "compression-webpack-plugin": "^1.1.6", - "copy-webpack-plugin": "^4.4.1", + "compression-webpack-plugin": "^3.0.1", + "copy-webpack-plugin": "^5.1.1", "copyfiles": "^2.1.1", "coveralls": "3.0.0", - "css-loader": "1.0.0", + "css-loader": "3.4.0", "cssnano": "^4.1.10", "deep-freeze": "0.0.1", + "eslint": "^6.7.2", "exports-loader": "^0.7.0", - "html-webpack-plugin": "^4.0.0-alpha", + "html-webpack-plugin": "3.2.0", "imports-loader": "0.8.0", "istanbul-instrumenter-loader": "3.0.1", - "jasmine-core": "^3.2.1", + "jasmine-core": "^3.3.0", "jasmine-marbles": "0.3.1", "jasmine-spec-reporter": "4.2.1", - "karma": "3.0.0", + "karma": "4.0.1", "karma-chrome-launcher": "2.2.0", - "karma-cli": "1.0.1", + "karma-cli": "2.0.0", "karma-coverage": "1.1.2", "karma-istanbul-preprocessor": "0.0.2", - "karma-jasmine": "1.1.2", + "karma-jasmine": "2.0.1", "karma-mocha-reporter": "2.2.5", "karma-phantomjs-launcher": "1.0.4", "karma-remap-coverage": "^0.1.5", @@ -221,26 +230,26 @@ "protractor": "^5.4.2", "protractor-istanbul-plugin": "2.0.0", "raw-loader": "0.5.1", - "resolve-url-loader": "^2.3.0", "rimraf": "2.6.2", "rollup": "^0.65.0", "rollup-plugin-commonjs": "^9.1.6", "rollup-plugin-node-globals": "1.2.1", "rollup-plugin-node-resolve": "^3.0.3", "rollup-plugin-terser": "^2.0.2", - "sass-loader": "7.1.0", - "script-ext-html-webpack-plugin": "2.0.1", + "sass-loader": "7.3.1", + "script-ext-html-webpack-plugin": "2.1.4", "source-map": "0.7.3", "source-map-loader": "0.2.4", "string-replace-loader": "^2.1.1", + "terser-webpack-plugin": "^2.3.1", "to-string-loader": "1.1.5", "ts-helpers": "1.1.2", "ts-node": "4.1.0", "tslint": "5.11.0", "typedoc": "^0.9.0", - "typescript": "^2.9.1", + "typescript": "3.1.6", "webdriver-manager": "^12.1.7", - "webpack": "^4.17.1", + "webpack": "^4.29.6", "webpack-bundle-analyzer": "^3.3.2", "webpack-dev-middleware": "3.2.0", "webpack-dev-server": "^3.1.11", diff --git a/protractor.conf.js b/protractor.conf.js index 2949702a0a..6570c9f7c3 100644 --- a/protractor.conf.js +++ b/protractor.conf.js @@ -5,7 +5,7 @@ var SpecReporter = require('jasmine-spec-reporter').SpecReporter; exports.config = { - allScriptsTimeout: 11000, + allScriptsTimeout: 600000, // ----------------------------------------------------------------- // Uncomment to run tests using a remote Selenium server //seleniumAddress: 'http://selenium.address:4444/wd/hub', @@ -73,7 +73,7 @@ exports.config = { framework: 'jasmine', jasmineNodeOpts: { showColors: true, - defaultTimeoutInterval: 30000, + defaultTimeoutInterval: 600000, print: function () {} }, useAllAngular2AppRoots: true, diff --git a/resources/fonts/README.md b/resources/fonts/README.md new file mode 100644 index 0000000000..e4817b8572 --- /dev/null +++ b/resources/fonts/README.md @@ -0,0 +1,3 @@ +# Supported font formats + +DSpace supports EOT, TTF, OTF, SVG, WOFF and WOFF2 fonts. diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index 2a6165e14c..86d93044ac 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -244,6 +244,8 @@ "collection.create.head": "Create a Collection", + "collection.create.notifications.success": "Successfully created the Collection", + "collection.create.sub-head": "Create a Collection for Community {{ parent }}", "collection.delete.cancel": "Cancel", @@ -264,6 +266,62 @@ "collection.edit.head": "Edit Collection", + + + "collection.edit.item-mapper.cancel": "Cancel", + + "collection.edit.item-mapper.collection": "Collection: \"{{name}}\"", + + "collection.edit.item-mapper.confirm": "Map selected items", + + "collection.edit.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", + + "collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections", + + "collection.edit.item-mapper.no-search": "Please enter a query to search", + + "collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.", + + "collection.edit.item-mapper.notifications.map.error.head": "Mapping errors", + + "collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.", + + "collection.edit.item-mapper.notifications.map.success.head": "Mapping completed", + + "collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.", + + "collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors", + + "collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", + + "collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed", + + "collection.edit.item-mapper.remove": "Remove selected item mappings", + + "collection.edit.item-mapper.tabs.browse": "Browse mapped items", + + "collection.edit.item-mapper.tabs.map": "Map new items", + + + + "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", @@ -304,42 +362,6 @@ - "collection.edit.item-mapper.cancel": "Cancel", - - "collection.edit.item-mapper.collection": "Collection: \"{{name}}\"", - - "collection.edit.item-mapper.confirm": "Map selected items", - - "collection.edit.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.", - - "collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections", - - "collection.edit.item-mapper.no-search": "Please enter a query to search", - - "collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.", - - "collection.edit.item-mapper.notifications.map.error.head": "Mapping errors", - - "collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.", - - "collection.edit.item-mapper.notifications.map.success.head": "Mapping completed", - - "collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.", - - "collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors", - - "collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", - - "collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed", - - "collection.edit.item-mapper.remove": "Remove selected item mappings", - - "collection.edit.item-mapper.tabs.browse": "Browse mapped items", - - "collection.edit.item-mapper.tabs.map": "Map new items", - - - "collection.form.abstract": "Short Description", "collection.form.description": "Introductory text (HTML)", @@ -378,8 +400,18 @@ + "communityList.tabTitle": "DSpace - Community List", + + "communityList.title": "List of Communities", + + "communityList.showMore": "Show More", + + + "community.create.head": "Create a Community", + "community.create.notifications.success": "Successfully created the Community", + "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", "community.delete.cancel": "Cancel", @@ -398,8 +430,30 @@ "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", @@ -412,6 +466,8 @@ "community.edit.tabs.roles.title": "Community Edit - Roles", + + "community.form.abstract": "Short Description", "community.form.description": "Introductory text (HTML)", @@ -503,6 +559,9 @@ "footer.link.duraspace": "DuraSpace", + "form.add": "Add", + + "form.add-help": "Click here to add the current entry and to add another one", "form.cancel": "Cancel", @@ -528,6 +587,10 @@ "form.loading": "Loading...", + "form.lookup": "Lookup", + + "form.lookup-help": "Click here to look up an existing relation", + "form.no-results": "No results found", "form.no-value": "No value entered", @@ -768,7 +831,7 @@ "item.edit.tabs.relationships.head": "Item Relationships", - "item.edit.tabs.relationships.title": "Item Edit - Relationships", + "item.edit.tabs.relationships.title": "Item Edit - Relationships", "item.edit.tabs.status.buttons.authorizations.button": "Authorizations...", @@ -866,9 +929,17 @@ "item.page.person.search.title": "Articles by this author", - "item.page.related-items.view-more": "View more", + "item.page.related-items.view-more": "Show {{ amount }} more", - "item.page.related-items.view-less": "View less", + "item.page.related-items.view-less": "Hide last {{ amount }}", + + "item.page.relationships.isAuthorOfPublication": "Publications", + + "item.page.relationships.isJournalOfPublication": "Publications", + + "item.page.relationships.isOrgUnitOfPerson": "Authors", + + "item.page.relationships.isOrgUnitOfProject": "Research Projects", "item.page.subject": "Keywords", @@ -1320,6 +1391,8 @@ "project.page.titleprefix": "Research Project: ", + "project.search.results.head": "Project Search Results", + "publication.listelement.badge": "Publication", @@ -1395,6 +1468,9 @@ "search.filters.applied.f.subject": "Subject", "search.filters.applied.f.submitter": "Submitter", + "search.filters.applied.f.jobTitle": "Job Title", + "search.filters.applied.f.birthDate.max": "End birth date", + "search.filters.applied.f.birthDate.min": "Start birth date", @@ -1508,6 +1584,8 @@ "search.results.no-results-link": "quotes around it", + "search.results.empty": "Your search returned no results.", + "search.sidebar.close": "Back to results", @@ -1563,6 +1641,85 @@ "submission.general.save-later": "Save for later", + "submission.sections.describe.relationship-lookup.close": "Close", + + "submission.sections.describe.relationship-lookup.search-tab.deselect-all": "Deselect all", + + "submission.sections.describe.relationship-lookup.search-tab.deselect-page": "Deselect page", + + "submission.sections.describe.relationship-lookup.search-tab.loading": "Loading...", + + "submission.sections.describe.relationship-lookup.search-tab.placeholder": "Search query", + + "submission.sections.describe.relationship-lookup.search-tab.search": "Go", + + "submission.sections.describe.relationship-lookup.search-tab.select-all": "Select all", + + "submission.sections.describe.relationship-lookup.search-tab.select-page": "Select page", + + "submission.sections.describe.relationship-lookup.selected": "Selected {{ size }} items", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Author": "Local Authors ({{ count }})", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Local Journals ({{ count }})", + + "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": "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": "Search for Funding", + + "submission.sections.describe.relationship-lookup.selection-tab.tab-title": "Current Selection ({{ count }})", + + "submission.sections.describe.relationship-lookup.title.Journal Issue": "Journal Issues", + + "submission.sections.describe.relationship-lookup.title.Journal Volume": "Journal Volumes", + + "submission.sections.describe.relationship-lookup.title.Journal": "Journals", + + "submission.sections.describe.relationship-lookup.title.Author": "Authors", + + "submission.sections.describe.relationship-lookup.title.Funding Agency": "Funding Agency", + + "submission.sections.describe.relationship-lookup.title.Funding": "Funding", + + "submission.sections.describe.relationship-lookup.search-tab.toggle-dropdown": "Toggle dropdown", + + "submission.sections.describe.relationship-lookup.selection-tab.settings": "Settings", + + "submission.sections.describe.relationship-lookup.selection-tab.no-selection": "Your selection is currently empty.", + + "submission.sections.describe.relationship-lookup.selection-tab.title.Author": "Selected Authors", + + "submission.sections.describe.relationship-lookup.selection-tab.title.Journal": "Selected Journals", + + "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Volume": "Selected Journal Volume", + + "submission.sections.describe.relationship-lookup.selection-tab.title.Journal Issue": "Selected Issue", + + "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaJournal": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.sherpaPublisher": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.orcidV2": "Search Results", + + "submission.sections.describe.relationship-lookup.selection-tab.title.lcname": "Search Results", + + "submission.sections.describe.relationship-lookup.name-variant.notification.content": "Would you like to save \"{{ value }}\" as a name variant for this person so you and others can reuse it for future submissions? If you don\'t you can still use it for this submission.", + + "submission.sections.describe.relationship-lookup.name-variant.notification.confirm": "Save a new name variant", + + "submission.sections.describe.relationship-lookup.name-variant.notification.decline": "Use only for this submission", "submission.sections.general.add-more": "Add more", @@ -1732,7 +1889,7 @@ "uploader.drag-message": "Drag & Drop your files here", - "uploader.or": ", or", + "uploader.or": ", or ", "uploader.processing": "Processing", diff --git a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts index cb7aa1ef91..ec4003c108 100644 --- a/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts +++ b/src/app/+admin/admin-registries/bitstream-formats/bitstream-formats.component.ts @@ -5,7 +5,7 @@ import { PaginatedList } from '../../../core/data/paginated-list'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { BitstreamFormat } from '../../../core/shared/bitstream-format.model'; import { BitstreamFormatDataService } from '../../../core/data/bitstream-format-data.service'; -import { FindAllOptions } from '../../../core/data/request.models'; +import { FindListOptions } from '../../../core/data/request.models'; import { map, switchMap, take } from 'rxjs/operators'; import { hasValue } from '../../../shared/empty.util'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; @@ -35,7 +35,7 @@ export class BitstreamFormatsComponent implements OnInit { * The current pagination configuration for the page used by the FindAll method * Currently simply renders all bitstream formats */ - config: FindAllOptions = Object.assign(new FindAllOptions(), { + config: FindListOptions = Object.assign(new FindListOptions(), { elementsPerPage: 20 }); @@ -145,7 +145,7 @@ export class BitstreamFormatsComponent implements OnInit { * @param event The page change event */ onPageChange(event) { - this.config = Object.assign(new FindAllOptions(), this.config, { + this.config = Object.assign(new FindListOptions(), this.config, { currentPage: event, }); this.pageConfig.currentPage = event; diff --git a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts index 4a5e301921..674ae739d8 100644 --- a/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts +++ b/src/app/+admin/admin-registries/metadata-registry/metadata-registry.component.spec.ts @@ -1,7 +1,6 @@ import { MetadataRegistryComponent } from './metadata-registry.component'; import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { of as observableOf } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; import { TranslateModule } from '@ngx-translate/core'; import { By } from '@angular/platform-browser'; @@ -18,6 +17,7 @@ import { NotificationsService } from '../../../shared/notifications/notification import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; import { RestResponse } from '../../../core/cache/response.models'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; +import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; describe('MetadataRegistryComponent', () => { let comp: MetadataRegistryComponent; @@ -101,12 +101,12 @@ describe('MetadataRegistryComponent', () => { it('should start editing the selected schema', async(() => { fixture.whenStable().then(() => { - expect(registryService.editMetadataSchema).toHaveBeenCalledWith(mockSchemasList[0]); + expect(registryService.editMetadataSchema).toHaveBeenCalledWith(mockSchemasList[0] as MetadataSchema); }); })); it('should cancel editing the selected schema when clicked again', async(() => { - spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0])); + spyOn(registryService, 'getActiveMetadataSchema').and.returnValue(observableOf(mockSchemasList[0] as MetadataSchema)); spyOn(registryService, 'cancelEditMetadataSchema'); row.click(); fixture.detectChanges(); @@ -121,7 +121,7 @@ describe('MetadataRegistryComponent', () => { beforeEach(() => { spyOn(registryService, 'deleteMetadataSchema').and.callThrough(); - spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas)); + spyOn(registryService, 'getSelectedMetadataSchemas').and.returnValue(observableOf(selectedSchemas as MetadataSchema[])); comp.deleteSchemas(); fixture.detectChanges(); }); diff --git a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts index e23a9691c4..e0b0ef25a5 100644 --- a/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts +++ b/src/app/+admin/admin-registries/metadata-schema/metadata-schema.component.spec.ts @@ -1,7 +1,6 @@ import { MetadataSchemaComponent } from './metadata-schema.component'; import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; import { of as observableOf } from 'rxjs'; -import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; import { TranslateModule } from '@ngx-translate/core'; import { CommonModule } from '@angular/common'; @@ -22,6 +21,7 @@ import { NotificationsServiceStub } from '../../../shared/testing/notifications- import { RestResponse } from '../../../core/cache/response.models'; import { MetadataSchema } from '../../../core/metadata/metadata-schema.model'; import { createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; +import { MetadataField } from '../../../core/metadata/metadata-field.model'; describe('MetadataSchemaComponent', () => { let comp: MetadataSchemaComponent; @@ -152,12 +152,12 @@ describe('MetadataSchemaComponent', () => { it('should start editing the selected field', async(() => { fixture.whenStable().then(() => { - expect(registryService.editMetadataField).toHaveBeenCalledWith(mockFieldsList[2]); + expect(registryService.editMetadataField).toHaveBeenCalledWith(mockFieldsList[2] as MetadataField); }); })); it('should cancel editing the selected field when clicked again', async(() => { - spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2])); + spyOn(registryService, 'getActiveMetadataField').and.returnValue(observableOf(mockFieldsList[2] as MetadataField)); spyOn(registryService, 'cancelEditMetadataField'); row.click(); fixture.detectChanges(); @@ -172,7 +172,7 @@ describe('MetadataSchemaComponent', () => { beforeEach(() => { spyOn(registryService, 'deleteMetadataField').and.callThrough(); - spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields)); + spyOn(registryService, 'getSelectedMetadataFields').and.returnValue(observableOf(selectedFields as MetadataField[])); comp.deleteFields(); fixture.detectChanges(); }); diff --git a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index 3ad1bd4272..72eb306bf1 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -13,11 +13,11 @@ import { combineLatest as combineLatestObservable } from 'rxjs'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { OnClickMenuItemModel } from '../../shared/menu/menu-item/models/onclick.model'; import { CreateCommunityParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-community-parent-selector/create-community-parent-selector.component'; -import { CreateItemParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; import { CreateCollectionParentSelectorComponent } from '../../shared/dso-selector/modal-wrappers/create-collection-parent-selector/create-collection-parent-selector.component'; import { EditItemSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-item-selector/edit-item-selector.component'; import { EditCommunitySelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-community-selector/edit-community-selector.component'; import { EditCollectionSelectorComponent } from '../../shared/dso-selector/modal-wrappers/edit-collection-selector/edit-collection-selector.component'; +import {CreateItemParentSelectorComponent} from '../../shared/dso-selector/modal-wrappers/create-item-parent-selector/create-item-parent-selector.component'; /** * Component representing the admin sidebar @@ -138,18 +138,18 @@ export class AdminSidebarComponent extends MenuComponent implements OnInit { parentID: 'new', active: false, visible: true, - // model: { - // type: MenuItemType.ONCLICK, - // text: 'menu.section.new_item', - // function: () => { - // this.modalService.open(CreateItemParentSelectorComponent); - // } - // } as OnClickMenuItemModel, model: { - type: MenuItemType.LINK, + type: MenuItemType.ONCLICK, text: 'menu.section.new_item', - link: '/submit' - } as LinkMenuItemModel, + function: () => { + this.modalService.open(CreateItemParentSelectorComponent); + } + } as OnClickMenuItemModel, + // model: { + // type: MenuItemType.LINK, + // text: 'menu.section.new_item', + // link: '/submit' + // } as LinkMenuItemModel, }, { id: 'new_item_version', diff --git a/src/app/+collection-page/collection-form/collection-form.component.ts b/src/app/+collection-page/collection-form/collection-form.component.ts index 21b494f41f..59433e49a0 100644 --- a/src/app/+collection-page/collection-form/collection-form.component.ts +++ b/src/app/+collection-page/collection-form/collection-form.component.ts @@ -1,9 +1,19 @@ import { Component, Input } from '@angular/core'; -import { DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; -import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; +import { + DynamicFormControlModel, + DynamicFormService, + DynamicInputModel, + DynamicTextAreaModel +} from '@ng-dynamic-forms/core'; import { Collection } from '../../core/shared/collection.model'; import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component'; -import { NormalizedCollection } from '../../core/cache/models/normalized-collection.model'; +import { Location } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { AuthService } from '../../core/auth/auth.service'; +import { RequestService } from '../../core/data/request.service'; +import { ObjectCacheService } from '../../core/cache/object-cache.service'; /** * Form used for creating and editing collections @@ -22,7 +32,7 @@ export class CollectionFormComponent extends ComColFormComponent { /** * @type {Collection.type} This is a collection-type form */ - protected type = Collection.type; + type = Collection.type; /** * The dynamic form fields used for creating/editing a collection @@ -65,4 +75,15 @@ export class CollectionFormComponent extends ComColFormComponent { name: 'dc.description.provenance', }), ]; + + public constructor(protected location: Location, + protected formService: DynamicFormService, + protected translate: TranslateService, + protected notificationsService: NotificationsService, + protected authService: AuthService, + protected dsoService: CommunityDataService, + protected requestService: RequestService, + protected objectCache: ObjectCacheService) { + super(location, formService, translate, notificationsService, authService, requestService, objectCache); + } } diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts index 0bbfb30821..62a8d8dabb 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.spec.ts @@ -1,29 +1,23 @@ import { CollectionItemMapperComponent } from './collection-item-mapper.component'; -import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { CommonModule } from '@angular/common'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { SearchFormComponent } from '../../shared/search-form/search-form.component'; -import { SearchPageModule } from '../../+search-page/search-page.module'; -import { ObjectCollectionComponent } from '../../shared/object-collection/object-collection.component'; import { ActivatedRoute, Router } from '@angular/router'; import { ActivatedRouteStub } from '../../shared/testing/active-router-stub'; import { RouterStub } from '../../shared/testing/router-stub'; -import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service'; -import { SearchService } from '../../+search-page/search-service/search.service'; import { SearchServiceStub } from '../../shared/testing/search-service-stub'; import { NotificationsService } from '../../shared/notifications/notifications.service'; import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; import { ItemDataService } from '../../core/data/item-data.service'; import { FormsModule } from '@angular/forms'; -import { SharedModule } from '../../shared/shared.module'; import { Collection } from '../../core/shared/collection.model'; import { RemoteData } from '../../core/data/remote-data'; -import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; -import { EventEmitter, NgModule } from '@angular/core'; +import { EventEmitter } from '@angular/core'; import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; import { By } from '@angular/platform-browser'; @@ -36,13 +30,14 @@ import { ItemSelectComponent } from '../../shared/object-select/item-select/item import { ObjectSelectService } from '../../shared/object-select/object-select.service'; import { ObjectSelectServiceStub } from '../../shared/testing/object-select-service-stub'; import { VarDirective } from '../../shared/utils/var.directive'; -import { Observable } from 'rxjs/internal/Observable'; import { of as observableOf, of } from 'rxjs/internal/observable/of'; import { RestResponse } from '../../core/cache/response.models'; -import { SearchFixedFilterService } from '../../+search-page/search-filters/search-filter/search-fixed-filter.service'; import { RouteService } from '../../core/services/route.service'; import { ErrorComponent } from '../../shared/error/error.component'; import { LoadingComponent } from '../../shared/loading/loading.component'; +import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; +import { SearchService } from '../../core/shared/search/search.service'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; describe('CollectionItemMapperComponent', () => { let comp: CollectionItemMapperComponent; @@ -135,7 +130,6 @@ describe('CollectionItemMapperComponent', () => { { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, { provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() }, { provide: RouteService, useValue: routeServiceStub }, - { provide: SearchFixedFilterService, useValue: fixedFilterServiceStub } ] }).compileComponents(); })); diff --git a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts index 750578cc35..5c67a78401 100644 --- a/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts +++ b/src/app/+collection-page/collection-item-mapper/collection-item-mapper.component.ts @@ -5,12 +5,9 @@ import { fadeIn, fadeInOut } from '../../shared/animations/fade'; import { ActivatedRoute, Router } from '@angular/router'; import { RemoteData } from '../../core/data/remote-data'; import { Collection } from '../../core/shared/collection.model'; -import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service'; -import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model'; import { PaginatedList } from '../../core/data/paginated-list'; -import { map, startWith, switchMap, take, tap } from 'rxjs/operators'; +import { map, startWith, switchMap, take } from 'rxjs/operators'; import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators'; -import { SearchService } from '../../+search-page/search-service/search.service'; import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model'; import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; @@ -22,6 +19,9 @@ import { isNotEmpty } from '../../shared/empty.util'; import { RestResponse } from '../../core/cache/response.models'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component'; +import { SearchConfigurationService } from '../../core/shared/search/search-configuration.service'; +import { PaginatedSearchOptions } from '../../shared/search/paginated-search-options.model'; +import { SearchService } from '../../core/shared/search/search.service'; @Component({ selector: 'ds-collection-item-mapper', diff --git a/src/app/+collection-page/collection-page.component.html b/src/app/+collection-page/collection-page.component.html index 436cd351a0..98552ed40b 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -3,16 +3,19 @@ *ngVar="(collectionRD$ | async) as collectionRD">
+
- - - [alternateText]="'Collection Logo'"> - - + + + + + {{'collection.create.sub-head' | translate:{ parent: (parentRD$| async)?.payload.name } }}
- + diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts index e223b11c65..869a89d5e0 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.spec.ts @@ -10,6 +10,8 @@ import { CollectionDataService } from '../../core/data/collection-data.service'; import { of as observableOf } from 'rxjs'; import { CommunityDataService } from '../../core/data/community-data.service'; import { CreateCollectionPageComponent } from './create-collection-page.component'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; describe('CreateCollectionPageComponent', () => { let comp: CreateCollectionPageComponent; @@ -27,6 +29,7 @@ describe('CreateCollectionPageComponent', () => { }, { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, { provide: Router, useValue: {} }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts index 2cab36d285..ae31b94c3d 100644 --- a/src/app/+collection-page/create-collection-page/create-collection-page.component.ts +++ b/src/app/+collection-page/create-collection-page/create-collection-page.component.ts @@ -5,6 +5,8 @@ import { Router } from '@angular/router'; import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component'; import { Collection } from '../../core/shared/collection.model'; import { CollectionDataService } from '../../core/data/collection-data.service'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; /** * Component that represents the page where a user can create a new Collection @@ -16,13 +18,16 @@ import { CollectionDataService } from '../../core/data/collection-data.service'; }) export class CreateCollectionPageComponent extends CreateComColPageComponent { protected frontendURL = '/collections/'; + protected type = Collection.type; public constructor( protected communityDataService: CommunityDataService, protected collectionDataService: CollectionDataService, protected routeService: RouteService, - protected router: Router + protected router: Router, + protected notificationsService: NotificationsService, + protected translate: TranslateService ) { - super(collectionDataService, communityDataService, routeService, router); + super(collectionDataService, communityDataService, routeService, router, notificationsService, translate); } } diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html index d627f7e8ef..b4eaf46bfb 100644 --- a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.html @@ -16,7 +16,9 @@ - + {{'collection.edit.delete' | translate}} diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts index 2bd932b7d2..a4336201fc 100644 --- a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.spec.ts @@ -8,10 +8,10 @@ import { ActivatedRoute, Router } 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 { Item } from '../../../core/shared/item.model'; import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../../../shared/testing/utils'; import { ItemTemplateDataService } from '../../../core/data/item-template-data.service'; -import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { Collection } from '../../../core/shared/collection.model'; describe('CollectionMetadataComponent', () => { diff --git a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts index a46c4189bb..72ebbb8508 100644 --- a/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts +++ b/src/app/+collection-page/edit-collection-page/collection-metadata/collection-metadata.component.ts @@ -22,6 +22,7 @@ import { TranslateService } from '@ngx-translate/core'; }) export class CollectionMetadataComponent extends ComcolMetadataComponent { protected frontendURL = '/collections/'; + protected type = Collection.type; /** * The collection's item template @@ -36,7 +37,7 @@ export class CollectionMetadataComponent extends ComcolMetadataComponent { - protected type = 'collection'; + type = 'collection'; public constructor( protected router: Router, diff --git a/src/app/+community-page/community-form/community-form.component.ts b/src/app/+community-page/community-form/community-form.component.ts index 17d601e251..e9bd2f66c8 100644 --- a/src/app/+community-page/community-form/community-form.component.ts +++ b/src/app/+community-page/community-form/community-form.component.ts @@ -1,9 +1,19 @@ import { Component, Input } from '@angular/core'; -import { DynamicInputModel, DynamicTextAreaModel } from '@ng-dynamic-forms/core'; -import { DynamicFormControlModel } from '@ng-dynamic-forms/core/src/model/dynamic-form-control.model'; +import { + DynamicFormControlModel, + DynamicFormService, + DynamicInputModel, + DynamicTextAreaModel +} from '@ng-dynamic-forms/core'; import { Community } from '../../core/shared/community.model'; -import { ResourceType } from '../../core/shared/resource-type'; import { ComColFormComponent } from '../../shared/comcol-forms/comcol-form/comcol-form.component'; +import { Location } from '@angular/common'; +import { TranslateService } from '@ngx-translate/core'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { AuthService } from '../../core/auth/auth.service'; +import { RequestService } from '../../core/data/request.service'; +import { ObjectCacheService } from '../../core/cache/object-cache.service'; /** * Form used for creating and editing communities @@ -22,7 +32,7 @@ export class CommunityFormComponent extends ComColFormComponent { /** * @type {Community.type} This is a community-type form */ - protected type = Community.type; + type = Community.type; /** * The dynamic form fields used for creating/editing a community @@ -57,4 +67,15 @@ export class CommunityFormComponent extends ComColFormComponent { name: 'dc.description.tableofcontents', }), ]; + + public constructor(protected location: Location, + protected formService: DynamicFormService, + protected translate: TranslateService, + protected notificationsService: NotificationsService, + protected authService: AuthService, + protected dsoService: CommunityDataService, + protected requestService: RequestService, + protected objectCache: ObjectCacheService) { + super(location, formService, translate, notificationsService, authService, requestService, objectCache); + } } diff --git a/src/app/+community-page/community-page.component.html b/src/app/+community-page/community-page.component.html index 05d0bd1d0e..dfd1ce93d9 100644 --- a/src/app/+community-page/community-page.component.html +++ b/src/app/+community-page/community-page.component.html @@ -1,13 +1,13 @@
+
+ + - - - diff --git a/src/app/+community-page/community-page.module.ts b/src/app/+community-page/community-page.module.ts index 534c96989e..1228783c3b 100644 --- a/src/app/+community-page/community-page.module.ts +++ b/src/app/+community-page/community-page.module.ts @@ -6,16 +6,18 @@ import { SharedModule } from '../shared/shared.module'; import { CommunityPageComponent } from './community-page.component'; import { CommunityPageSubCollectionListComponent } from './sub-collection-list/community-page-sub-collection-list.component'; 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 { CommunityFormComponent } from './community-form/community-form.component'; import { DeleteCommunityPageComponent } from './delete-community-page/delete-community-page.component'; +import { StatisticsModule } from '../statistics/statistics.module'; @NgModule({ imports: [ CommonModule, SharedModule, - CommunityPageRoutingModule + CommunityPageRoutingModule, + StatisticsModule.forRoot() ], declarations: [ CommunityPageComponent, diff --git a/src/app/+community-page/create-community-page/create-community-page.component.html b/src/app/+community-page/create-community-page/create-community-page.component.html index 55a080d2a1..4f75771f6d 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.html +++ b/src/app/+community-page/create-community-page/create-community-page.component.html @@ -7,5 +7,5 @@
- +
diff --git a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts index dead5a5c3b..d0de8ec71c 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.spec.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.spec.ts @@ -10,6 +10,8 @@ import { CollectionDataService } from '../../core/data/collection-data.service'; import { of as observableOf } from 'rxjs'; import { CommunityDataService } from '../../core/data/community-data.service'; import { CreateCommunityPageComponent } from './create-community-page.component'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub'; describe('CreateCommunityPageComponent', () => { let comp: CreateCommunityPageComponent; @@ -23,6 +25,7 @@ describe('CreateCommunityPageComponent', () => { { provide: CommunityDataService, useValue: { findById: () => observableOf({}) } }, { provide: RouteService, useValue: { getQueryParameterValue: () => observableOf('1234') } }, { provide: Router, useValue: {} }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/+community-page/create-community-page/create-community-page.component.ts b/src/app/+community-page/create-community-page/create-community-page.component.ts index fd5f18442a..30a2acbb0d 100644 --- a/src/app/+community-page/create-community-page/create-community-page.component.ts +++ b/src/app/+community-page/create-community-page/create-community-page.component.ts @@ -4,6 +4,8 @@ import { CommunityDataService } from '../../core/data/community-data.service'; import { RouteService } from '../../core/services/route.service'; import { Router } from '@angular/router'; import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comcol-page/create-comcol-page.component'; +import { NotificationsService } from '../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; /** * Component that represents the page where a user can create a new Community @@ -15,12 +17,15 @@ import { CreateComColPageComponent } from '../../shared/comcol-forms/create-comc }) export class CreateCommunityPageComponent extends CreateComColPageComponent { protected frontendURL = '/communities/'; + protected type = Community.type; public constructor( protected communityDataService: CommunityDataService, protected routeService: RouteService, - protected router: Router + protected router: Router, + protected notificationsService: NotificationsService, + protected translate: TranslateService ) { - super(communityDataService, communityDataService, routeService, router); + super(communityDataService, communityDataService, routeService, router, notificationsService, translate); } } diff --git a/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.html b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.html index 9a59be9067..6b441dbabd 100644 --- a/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.html +++ b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.html @@ -1,4 +1,6 @@ - + {{'community.edit.delete' | translate}} diff --git a/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts index 52ee73bfab..abeafb4e23 100644 --- a/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts +++ b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.spec.ts @@ -8,6 +8,8 @@ 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; @@ -20,6 +22,7 @@ describe('CommunityMetadataComponent', () => { providers: [ { provide: CommunityDataService, useValue: {} }, { provide: ActivatedRoute, useValue: { parent: { data: observableOf({ dso: { payload: {} } }) } } }, + { provide: NotificationsService, useValue: new NotificationsServiceStub() } ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); diff --git a/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.ts b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.ts index c57c74175d..c4bb88289f 100644 --- a/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.ts +++ b/src/app/+community-page/edit-community-page/community-metadata/community-metadata.component.ts @@ -3,6 +3,8 @@ import { ComcolMetadataComponent } from '../../../shared/comcol-forms/edit-comco 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 @@ -13,12 +15,15 @@ import { CommunityDataService } from '../../../core/data/community-data.service' }) export class CommunityMetadataComponent extends ComcolMetadataComponent { protected frontendURL = '/communities/'; + protected type = Community.type; public constructor( protected communityDataService: CommunityDataService, protected router: Router, - protected route: ActivatedRoute + protected route: ActivatedRoute, + protected notificationsService: NotificationsService, + protected translate: TranslateService ) { - super(communityDataService, router, route); + super(communityDataService, router, route, notificationsService, translate); } } diff --git a/src/app/+community-page/edit-community-page/edit-community-page.component.ts b/src/app/+community-page/edit-community-page/edit-community-page.component.ts index a8d4d32b7d..c0adfe0ff1 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.component.ts +++ b/src/app/+community-page/edit-community-page/edit-community-page.component.ts @@ -12,7 +12,7 @@ import { getCommunityPageRoute } from '../community-page-routing.module'; templateUrl: '../../shared/comcol-forms/edit-comcol-page/edit-comcol-page.component.html' }) export class EditCommunityPageComponent extends EditComColPageComponent { - protected type = 'community'; + type = 'community'; public constructor( protected router: Router, diff --git a/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts b/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts index 527b3c018f..1182db2de1 100644 --- a/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts +++ b/src/app/+community-page/edit-community-page/edit-community-page.routing.module.ts @@ -27,7 +27,10 @@ import { CommunityCurateComponent } from './community-curate/community-curate.co { path: 'metadata', component: CommunityMetadataComponent, - data: { title: 'community.edit.tabs.metadata.title' } + data: { + title: 'community.edit.tabs.metadata.title', + hideReturnButton: true + } }, { path: 'roles', diff --git a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html index 9156a99b18..bf6ce7fd57 100644 --- a/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html +++ b/src/app/+community-page/sub-collection-list/community-page-sub-collection-list.component.html @@ -1,14 +1,13 @@

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

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

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

- + +
diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts index 2feaa3afa6..41502e7bd4 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.spec.ts @@ -1,21 +1,29 @@ -import {async, ComponentFixture, TestBed} from '@angular/core/testing'; -import {TranslateModule} from '@ngx-translate/core'; -import {NO_ERRORS_SCHEMA} from '@angular/core'; -import {CommunityPageSubCommunityListComponent} from './community-page-sub-community-list.component'; -import {Community} from '../../core/shared/community.model'; -import {RemoteData} from '../../core/data/remote-data'; -import {PaginatedList} from '../../core/data/paginated-list'; -import {PageInfo} from '../../core/shared/page-info.model'; -import {SharedModule} from '../../shared/shared.module'; -import {RouterTestingModule} from '@angular/router/testing'; -import {NoopAnimationsModule} from '@angular/platform-browser/animations'; -import {By} from '@angular/platform-browser'; -import {of as observableOf, Observable } from 'rxjs'; -import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { By } from '@angular/platform-browser'; -describe('SubCommunityList Component', () => { +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { CommunityPageSubCommunityListComponent } from './community-page-sub-community-list.component'; +import { Community } from '../../core/shared/community.model'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { SharedModule } from '../../shared/shared.module'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { FindListOptions } from '../../core/data/request.models'; +import { HostWindowService } from '../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; + +describe('CommunityPageSubCommunityListComponent Component', () => { let comp: CommunityPageSubCommunityListComponent; let fixture: ComponentFixture; + let communityDataServiceStub: any; + let subCommList = []; const subcommunities = [Object.assign(new Community(), { id: '123456789-1', @@ -32,34 +40,92 @@ describe('SubCommunityList Component', () => { { language: 'en_US', value: 'SubCommunity 2' } ] } + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 3' } + ] + } + }), + Object.assign(new Community(), { + id: '12345678942', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 4' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 5' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 6' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'SubCommunity 7' } + ] + } }) ]; - const emptySubCommunitiesCommunity = Object.assign(new Community(), { + const mockCommunity = Object.assign(new Community(), { + id: '123456789', metadata: { 'dc.title': [ { language: 'en_US', value: 'Test title' } ] - }, - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), [])) + } }); - const mockCommunity = Object.assign(new Community(), { - metadata: { - 'dc.title': [ - { language: 'en_US', value: 'Test title' } - ] - }, - subcommunities: createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subcommunities)) - }) - ; + communityDataServiceStub = { + findByParent(parentUUID: string, options: FindListOptions = {}) { + let currentPage = options.currentPage; + let elementsPerPage = options.elementsPerPage; + if (currentPage === undefined) { + currentPage = 1 + } + elementsPerPage = 5; + + const startPageIndex = (currentPage - 1) * elementsPerPage; + let endPageIndex = (currentPage * elementsPerPage); + if (endPageIndex > subCommList.length) { + endPageIndex = subCommList.length; + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), subCommList.slice(startPageIndex, endPageIndex))); + + } + }; beforeEach(async(() => { TestBed.configureTestingModule({ - imports: [TranslateModule.forRoot(), SharedModule, + imports: [ + TranslateModule.forRoot(), + SharedModule, RouterTestingModule.withRoutes([]), - NoopAnimationsModule], + NgbModule.forRoot(), + NoopAnimationsModule + ], declarations: [CommunityPageSubCommunityListComponent], + providers: [ + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: SelectableListService, useValue: {} }, + ], schemas: [NO_ERRORS_SCHEMA] }).compileComponents(); })); @@ -67,23 +133,52 @@ describe('SubCommunityList Component', () => { beforeEach(() => { fixture = TestBed.createComponent(CommunityPageSubCommunityListComponent); comp = fixture.componentInstance; + comp.community = mockCommunity; + }); - it('should display a list of subCommunities', () => { - comp.community = mockCommunity; + it('should display a list of sub-communities', () => { + subCommList = subcommunities; fixture.detectChanges(); const subComList = fixture.debugElement.queryAll(By.css('li')); - expect(subComList.length).toEqual(2); + expect(subComList.length).toEqual(5); expect(subComList[0].nativeElement.textContent).toContain('SubCommunity 1'); expect(subComList[1].nativeElement.textContent).toContain('SubCommunity 2'); + expect(subComList[2].nativeElement.textContent).toContain('SubCommunity 3'); + expect(subComList[3].nativeElement.textContent).toContain('SubCommunity 4'); + expect(subComList[4].nativeElement.textContent).toContain('SubCommunity 5'); }); - it('should not display the header when subCommunities are empty', () => { - comp.community = emptySubCommunitiesCommunity; + it('should not display the header when list of sub-communities is empty', () => { + subCommList = []; fixture.detectChanges(); const subComHead = fixture.debugElement.queryAll(By.css('h2')); expect(subComHead.length).toEqual(0); }); + + it('should update list of sub-communities on pagination change', () => { + subCommList = subcommunities; + fixture.detectChanges(); + + const pagination = Object.create({ + pagination:{ + id: comp.pageId, + currentPage: 2, + pageSize: 5 + }, + sort: { + field: 'dc.title', + direction: 'ASC' + } + }); + comp.onPaginationChange(pagination); + fixture.detectChanges(); + + const collList = fixture.debugElement.queryAll(By.css('li')); + expect(collList.length).toEqual(2); + expect(collList[0].nativeElement.textContent).toContain('SubCommunity 6'); + expect(collList[1].nativeElement.textContent).toContain('SubCommunity 7'); + }); }); diff --git a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts index 91f6d7bac1..1bd664021e 100644 --- a/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts +++ b/src/app/+community-page/sub-community-list/community-page-sub-community-list.component.ts @@ -1,26 +1,82 @@ import { Component, Input, OnInit } from '@angular/core'; +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; + import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; - import { fadeIn } from '../../shared/animations/fade'; import { PaginatedList } from '../../core/data/paginated-list'; -import {Observable} from 'rxjs'; +import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; +import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; +import { CommunityDataService } from '../../core/data/community-data.service'; @Component({ selector: 'ds-community-page-sub-community-list', styleUrls: ['./community-page-sub-community-list.component.scss'], templateUrl: './community-page-sub-community-list.component.html', - animations:[fadeIn] + animations: [fadeIn] }) /** * Component to render the sub-communities of a Community */ export class CommunityPageSubCommunityListComponent implements OnInit { @Input() community: Community; - subCommunitiesRDObs: Observable>>; + + /** + * The pagination configuration + */ + config: PaginationComponentOptions; + + /** + * The pagination id + */ + pageId = 'community-subCommunities-pagination'; + + /** + * The sorting configuration + */ + sortConfig: SortOptions; + + /** + * A list of remote data objects of communities' collections + */ + subCommunitiesRDObs: BehaviorSubject>> = new BehaviorSubject>>({} as any); + + constructor(private cds: CommunityDataService) { + } ngOnInit(): void { - this.subCommunitiesRDObs = this.community.subcommunities; + this.config = new PaginationComponentOptions(); + this.config.id = this.pageId; + this.config.pageSize = 5; + this.config.currentPage = 1; + this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); + this.updatePage(); + } + + /** + * Called when one of the pagination settings is changed + * @param event The new pagination data + */ + onPaginationChange(event) { + this.config.currentPage = event.pagination.currentPage; + this.config.pageSize = event.pagination.pageSize; + this.sortConfig.field = event.sort.field; + this.sortConfig.direction = event.sort.direction; + this.updatePage(); + } + + /** + * Update the list of sub-communities + */ + updatePage() { + this.cds.findByParent(this.community.id, { + currentPage: this.config.currentPage, + elementsPerPage: this.config.pageSize, + sort: { field: this.sortConfig.field, direction: this.sortConfig.direction } + }).pipe(take(1)).subscribe((results) => { + this.subCommunitiesRDObs.next(results); + }); } } diff --git a/src/app/+home-page/home-page-routing.module.ts b/src/app/+home-page/home-page-routing.module.ts index d7dcc18f49..78da529906 100644 --- a/src/app/+home-page/home-page-routing.module.ts +++ b/src/app/+home-page/home-page-routing.module.ts @@ -2,12 +2,25 @@ import { NgModule } from '@angular/core'; import { RouterModule } from '@angular/router'; import { HomePageComponent } from './home-page.component'; +import { HomePageResolver } from './home-page.resolver'; @NgModule({ imports: [ RouterModule.forChild([ - { path: '', component: HomePageComponent, pathMatch: 'full', data: { title: 'home.title' } } + { + path: '', + component: HomePageComponent, + pathMatch: 'full', + data: {title: 'home.title'}, + resolve: { + site: HomePageResolver + } + } ]) + ], + providers: [ + HomePageResolver ] }) -export class HomePageRoutingModule { } +export class HomePageRoutingModule { +} diff --git a/src/app/+home-page/home-page.component.html b/src/app/+home-page/home-page.component.html index 39ba479033..5515df595b 100644 --- a/src/app/+home-page/home-page.component.html +++ b/src/app/+home-page/home-page.component.html @@ -1,5 +1,8 @@
+ + +
diff --git a/src/app/+home-page/home-page.component.ts b/src/app/+home-page/home-page.component.ts index 902a0e820d..65caa01430 100644 --- a/src/app/+home-page/home-page.component.ts +++ b/src/app/+home-page/home-page.component.ts @@ -1,9 +1,26 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; +import { map } from 'rxjs/operators'; +import { ActivatedRoute } from '@angular/router'; +import { Observable } from 'rxjs'; +import { Site } from '../core/shared/site.model'; @Component({ selector: 'ds-home-page', styleUrls: ['./home-page.component.scss'], templateUrl: './home-page.component.html' }) -export class HomePageComponent { +export class HomePageComponent implements OnInit { + + site$: Observable; + + constructor( + private route: ActivatedRoute, + ) { + } + + ngOnInit(): void { + this.site$ = this.route.data.pipe( + map((data) => data.site as Site), + ); + } } diff --git a/src/app/+home-page/home-page.module.ts b/src/app/+home-page/home-page.module.ts index c0c082b36c..51e978bbfe 100644 --- a/src/app/+home-page/home-page.module.ts +++ b/src/app/+home-page/home-page.module.ts @@ -6,12 +6,14 @@ import { HomePageRoutingModule } from './home-page-routing.module'; import { HomePageComponent } from './home-page.component'; import { TopLevelCommunityListComponent } from './top-level-community-list/top-level-community-list.component'; +import { StatisticsModule } from '../statistics/statistics.module'; @NgModule({ imports: [ CommonModule, SharedModule, - HomePageRoutingModule + HomePageRoutingModule, + StatisticsModule.forRoot() ], declarations: [ HomePageComponent, diff --git a/src/app/+home-page/home-page.resolver.ts b/src/app/+home-page/home-page.resolver.ts new file mode 100644 index 0000000000..6b63a4e782 --- /dev/null +++ b/src/app/+home-page/home-page.resolver.ts @@ -0,0 +1,25 @@ +import { Injectable } from '@angular/core'; +import { ActivatedRouteSnapshot, Resolve, RouterStateSnapshot } from '@angular/router'; +import { SiteDataService } from '../core/data/site-data.service'; +import { Site } from '../core/shared/site.model'; +import { Observable } from 'rxjs'; +import { take } from 'rxjs/operators'; + +/** + * The class that resolve the Site object for a route + */ +@Injectable() +export class HomePageResolver implements Resolve { + constructor(private siteService: SiteDataService) { + } + + /** + * Method for resolving a site object + * @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot + * @param {RouterStateSnapshot} state The current RouterStateSnapshot + * @returns Observable Emits the found Site object, or an error if something went wrong + */ + resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable | Promise | Site { + return this.siteService.find().pipe(take(1)); + } +} diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts new file mode 100644 index 0000000000..fa164fe624 --- /dev/null +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.spec.ts @@ -0,0 +1,161 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; +import { By } from '@angular/platform-browser'; + +import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; + +import { TopLevelCommunityListComponent } from './top-level-community-list.component'; +import { Community } from '../../core/shared/community.model'; +import { PaginatedList } from '../../core/data/paginated-list'; +import { PageInfo } from '../../core/shared/page-info.model'; +import { SharedModule } from '../../shared/shared.module'; +import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils'; +import { FindListOptions } from '../../core/data/request.models'; +import { HostWindowService } from '../../shared/host-window.service'; +import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub'; +import { CommunityDataService } from '../../core/data/community-data.service'; +import { SelectableListService } from '../../shared/object-list/selectable-list/selectable-list.service'; + +describe('TopLevelCommunityList Component', () => { + let comp: TopLevelCommunityListComponent; + let fixture: ComponentFixture; + let communityDataServiceStub: any; + + const topCommList = [Object.assign(new Community(), { + id: '123456789-1', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 1' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-2', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 2' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-3', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 3' } + ] + } + }), + Object.assign(new Community(), { + id: '12345678942', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 4' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-5', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 5' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-6', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 6' } + ] + } + }), + Object.assign(new Community(), { + id: '123456789-7', + metadata: { + 'dc.title': [ + { language: 'en_US', value: 'TopCommunity 7' } + ] + } + }) + ]; + + communityDataServiceStub = { + findTop(options: FindListOptions = {}) { + let currentPage = options.currentPage; + let elementsPerPage = options.elementsPerPage; + if (currentPage === undefined) { + currentPage = 1 + } + elementsPerPage = 5; + + const startPageIndex = (currentPage - 1) * elementsPerPage; + let endPageIndex = (currentPage * elementsPerPage); + if (endPageIndex > topCommList.length) { + endPageIndex = topCommList.length; + } + return createSuccessfulRemoteDataObject$(new PaginatedList(new PageInfo(), topCommList.slice(startPageIndex, endPageIndex))); + + } + }; + + beforeEach(async(() => { + TestBed.configureTestingModule({ + imports: [ + TranslateModule.forRoot(), + SharedModule, + RouterTestingModule.withRoutes([]), + NgbModule.forRoot(), + NoopAnimationsModule + ], + declarations: [TopLevelCommunityListComponent], + providers: [ + { provide: CommunityDataService, useValue: communityDataServiceStub }, + { provide: HostWindowService, useValue: new HostWindowServiceStub(0) }, + { provide: SelectableListService, useValue: {} }, + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(TopLevelCommunityListComponent); + comp = fixture.componentInstance; + fixture.detectChanges(); + + }); + + it('should display a list of top-communities', () => { + const subComList = fixture.debugElement.queryAll(By.css('li')); + + expect(subComList.length).toEqual(5); + expect(subComList[0].nativeElement.textContent).toContain('TopCommunity 1'); + expect(subComList[1].nativeElement.textContent).toContain('TopCommunity 2'); + expect(subComList[2].nativeElement.textContent).toContain('TopCommunity 3'); + expect(subComList[3].nativeElement.textContent).toContain('TopCommunity 4'); + expect(subComList[4].nativeElement.textContent).toContain('TopCommunity 5'); + }); + + it('should update list of top-communities on pagination change', () => { + const pagination = Object.create({ + pagination:{ + id: comp.pageId, + currentPage: 2, + pageSize: 5 + }, + sort: { + field: 'dc.title', + direction: 'ASC' + } + }); + comp.onPaginationChange(pagination); + fixture.detectChanges(); + + const collList = fixture.debugElement.queryAll(By.css('li')); + expect(collList.length).toEqual(2); + expect(collList[0].nativeElement.textContent).toContain('TopCommunity 6'); + expect(collList[1].nativeElement.textContent).toContain('TopCommunity 7'); + }); +}); diff --git a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts index 1115d785a3..02c3cb54a0 100644 --- a/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts +++ b/src/app/+home-page/top-level-community-list/top-level-community-list.component.ts @@ -1,15 +1,15 @@ import { ChangeDetectionStrategy, Component, OnInit } from '@angular/core'; -import { BehaviorSubject, Observable } from 'rxjs'; + +import { BehaviorSubject } from 'rxjs'; +import { take } from 'rxjs/operators'; + import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model'; import { CommunityDataService } from '../../core/data/community-data.service'; import { PaginatedList } from '../../core/data/paginated-list'; - import { RemoteData } from '../../core/data/remote-data'; import { Community } from '../../core/shared/community.model'; - import { fadeInOut } from '../../shared/animations/fade'; import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model'; -import { take } from 'rxjs/operators'; /** * this component renders the Top-Level Community list @@ -33,6 +33,11 @@ export class TopLevelCommunityListComponent implements OnInit { */ config: PaginationComponentOptions; + /** + * The pagination id + */ + pageId = 'top-level-pagination'; + /** * The sorting configuration */ @@ -40,7 +45,7 @@ export class TopLevelCommunityListComponent implements OnInit { constructor(private cds: CommunityDataService) { this.config = new PaginationComponentOptions(); - this.config.id = 'top-level-pagination'; + this.config.id = this.pageId; this.config.pageSize = 5; this.config.currentPage = 1; this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); @@ -55,10 +60,10 @@ export class TopLevelCommunityListComponent implements OnInit { * @param event The new pagination data */ onPaginationChange(event) { - this.config.currentPage = event.page; - this.config.pageSize = event.pageSize; - this.sortConfig.field = event.sortField; - this.sortConfig.direction = event.sortDirection; + this.config.currentPage = event.pagination.currentPage; + this.config.pageSize = event.pagination.pageSize; + this.sortConfig.field = event.sort.field; + this.sortConfig.direction = event.sort.direction; this.updatePage(); } diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts index ed9351d5d2..c8740c35b2 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.spec.ts @@ -1,15 +1,12 @@ -import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { RouterTestingModule } from '@angular/router/testing'; import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { CommonModule } from '@angular/common'; import { ItemCollectionMapperComponent } from './item-collection-mapper.component'; import { ActivatedRoute, Router } from '@angular/router'; -import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service'; -import { SearchService } from '../../../+search-page/search-service/search.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { ItemDataService } from '../../../core/data/item-data.service'; import { RemoteData } from '../../../core/data/remote-data'; -import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model'; import { RouterStub } from '../../../shared/testing/router-stub'; @@ -19,7 +16,6 @@ import { SearchServiceStub } from '../../../shared/testing/search-service-stub'; import { PaginatedList } from '../../../core/data/paginated-list'; import { PageInfo } from '../../../core/shared/page-info.model'; import { FormsModule } from '@angular/forms'; -import { SharedModule } from '../../../shared/shared.module'; import { TranslateModule, TranslateService } from '@ngx-translate/core'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; import { HostWindowService } from '../../../shared/host-window.service'; @@ -28,7 +24,6 @@ import { By } from '@angular/platform-browser'; import { Item } from '../../../core/shared/item.model'; import { ObjectSelectService } from '../../../shared/object-select/object-select.service'; import { ObjectSelectServiceStub } from '../../../shared/testing/object-select-service-stub'; -import { Observable } from 'rxjs/internal/Observable'; import { of } from 'rxjs/internal/observable/of'; import { RestResponse } from '../../../core/cache/response.models'; import { CollectionSelectComponent } from '../../../shared/object-select/collection-select/collection-select.component'; @@ -39,6 +34,9 @@ import { SearchFormComponent } from '../../../shared/search-form/search-form.com import { Collection } from '../../../core/shared/collection.model'; import { ErrorComponent } from '../../../shared/error/error.component'; import { LoadingComponent } from '../../../shared/loading/loading.component'; +import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; +import { SearchService } from '../../../core/shared/search/search.service'; +import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; describe('ItemCollectionMapperComponent', () => { let comp: ItemCollectionMapperComponent; diff --git a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts index 97b8164a6e..5494d5ab5f 100644 --- a/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts +++ b/src/app/+item-page/edit-item-page/item-collection-mapper/item-collection-mapper.component.ts @@ -2,15 +2,12 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core'; import { fadeIn, fadeInOut } from '../../../shared/animations/fade'; -import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; import { Collection } from '../../../core/shared/collection.model'; import { Item } from '../../../core/shared/item.model'; import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators'; import { ActivatedRoute, Router } from '@angular/router'; -import { SearchService } from '../../../+search-page/search-service/search.service'; -import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service'; import { map, startWith, switchMap, take } from 'rxjs/operators'; import { ItemDataService } from '../../../core/data/item-data.service'; import { TranslateService } from '@ngx-translate/core'; @@ -19,6 +16,9 @@ import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model' import { isNotEmpty } from '../../../shared/empty.util'; import { RestResponse } from '../../../core/cache/response.models'; import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject'; +import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; +import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; +import { SearchService } from '../../../core/shared/search/search.service'; @Component({ selector: 'ds-item-collection-mapper', diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts index e73b4b6f9a..aa84b160a0 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.spec.ts @@ -9,7 +9,6 @@ import { ActivatedRoute, Router } from '@angular/router'; import { ItemMoveComponent } from './item-move.component'; import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; -import { SearchService } from '../../../+search-page/search-service/search.service'; import { of as observableOf } from 'rxjs'; import { FormsModule } from '@angular/forms'; import { ItemDataService } from '../../../core/data/item-data.service'; @@ -18,6 +17,7 @@ import { PaginatedList } from '../../../core/data/paginated-list'; import { CUSTOM_ELEMENTS_SCHEMA } from '@angular/core'; import { RestResponse } from '../../../core/cache/response.models'; import { Collection } from '../../../core/shared/collection.model'; +import { SearchService } from '../../../core/shared/search/search.service'; describe('ItemMoveComponent', () => { let comp: ItemMoveComponent; diff --git a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts index 113ee97b3f..4db7cf94da 100644 --- a/src/app/+item-page/edit-item-page/item-move/item-move.component.ts +++ b/src/app/+item-page/edit-item-page/item-move/item-move.component.ts @@ -1,12 +1,9 @@ import { Component, OnInit } from '@angular/core'; -import { SearchService } from '../../../+search-page/search-service/search.service'; import { first, map } from 'rxjs/operators'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; -import { SearchOptions } from '../../../+search-page/search-options.model'; import { RemoteData } from '../../../core/data/remote-data'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { PaginatedList } from '../../../core/data/paginated-list'; -import { SearchResult } from '../../../+search-page/search-result.model'; import { Item } from '../../../core/shared/item.model'; import { ActivatedRoute, Router } from '@angular/router'; import { NotificationsService } from '../../../shared/notifications/notifications.service'; @@ -17,9 +14,10 @@ import { getItemEditPath } from '../../item-page-routing.module'; import { Observable, of as observableOf } from 'rxjs'; import { RestResponse } from '../../../core/cache/response.models'; import { Collection } from '../../../core/shared/collection.model'; -import { tap } from 'rxjs/internal/operators/tap'; import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model'; -import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model'; +import { SearchService } from '../../../core/shared/search/search.service'; +import { PaginatedSearchOptions } from '../../../shared/search/paginated-search-options.model'; +import { SearchResult } from '../../../shared/search/search-result.model'; @Component({ selector: 'ds-item-move', diff --git a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts index 302ebf68a7..ee9d2cda27 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/edit-relationship/edit-relationship.component.ts @@ -70,5 +70,4 @@ export class EditRelationshipComponent implements OnChanges { canUndo(): boolean { return this.fieldUpdate.changeType >= 0; } - } diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts index 1c2080aa6a..0e473149e9 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts @@ -157,7 +157,9 @@ describe('ItemRelationshipsComponent', () => { getRelatedItemsByLabel: observableOf([author1, author2]), getItemRelationshipsArray: observableOf(relationships), deleteRelationship: observableOf(new RestResponse(true, 200, 'OK')), - getItemResolvedRelatedItemsAndRelationships: observableCombineLatest(observableOf([author1, author2]), observableOf([item, item]), observableOf(relationships)) + getItemResolvedRelatedItemsAndRelationships: observableCombineLatest(observableOf([author1, author2]), observableOf([item, item]), observableOf(relationships)), + getRelationshipsByRelatedItemIds: observableOf(relationships), + getRelationshipTypeLabelsByItem: observableOf([relationshipType.leftwardType]) } ); diff --git a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts index e8f34bc70e..42ebc5563e 100644 --- a/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts +++ b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.ts @@ -2,8 +2,8 @@ import { ChangeDetectorRef, Component, Inject, OnDestroy } from '@angular/core'; import { Item } from '../../../core/shared/item.model'; import { FieldUpdate, FieldUpdates } from '../../../core/data/object-updates/object-updates.reducer'; import { Observable } from 'rxjs/internal/Observable'; -import { filter, map, switchMap, take, tap } from 'rxjs/operators'; -import { combineLatest as observableCombineLatest, zip as observableZip } from 'rxjs'; +import { filter, flatMap, map, switchMap, take, tap } from 'rxjs/operators'; +import { zip as observableZip } from 'rxjs'; import { AbstractItemUpdateComponent } from '../abstract-item-update/abstract-item-update.component'; import { ItemDataService } from '../../../core/data/item-data.service'; import { ObjectUpdatesService } from '../../../core/data/object-updates/object-updates.service'; @@ -21,7 +21,6 @@ import { ObjectCacheService } from '../../../core/cache/object-cache.service'; import { getSucceededRemoteData } from '../../../core/shared/operators'; import { RequestService } from '../../../core/data/request.service'; import { Subscription } from 'rxjs/internal/Subscription'; -import { getRelationsByRelatedItemIds } from '../../simple/item-types/shared/item-relationships-utils'; @Component({ selector: 'ds-item-relationships', @@ -65,7 +64,7 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl */ ngOnInit(): void { super.ngOnInit(); - this.relationLabels$ = this.relationshipService.getItemRelationshipLabels(this.item); + this.relationLabels$ = this.relationshipService.getRelationshipTypeLabelsByItem(this.item); this.initializeItemUpdate(); } @@ -113,8 +112,9 @@ export class ItemRelationshipsComponent extends AbstractItemUpdateComponent impl ); // Get all the relationships that should be removed const removedRelationships$ = removedItemIds$.pipe( - getRelationsByRelatedItemIds(this.item, this.relationshipService) + flatMap((uuids) => this.relationshipService.getRelationshipsByRelatedItemIds(this.item, uuids)) ); + // const removedRelationships$ = removedItemIds$.pipe(flatMap((uuids: string[]) => this.relationshipService.getRelationshipsByRelatedItemIds(this.item, uuids))); // Request a delete for every relationship found in the observable created above removedRelationships$.pipe( take(1), diff --git a/src/app/+item-page/full/full-item-page.component.html b/src/app/+item-page/full/full-item-page.component.html index 7aec57da0c..c453df6bff 100644 --- a/src/app/+item-page/full/full-item-page.component.html +++ b/src/app/+item-page/full/full-item-page.component.html @@ -1,6 +1,7 @@
+
- - + +
diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts index a475e16637..39d7d9ccce 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.spec.ts @@ -13,6 +13,7 @@ import { isNotEmpty } from '../../../../shared/empty.util'; import { JournalComponent } from './journal.component'; import { GenericItemPageFieldComponent } from '../../../../+item-page/simple/field-components/specific-field/generic/generic-item-page-field.component'; import { createSuccessfulRemoteDataObject$ } from '../../../../shared/testing/utils'; +import { RelationshipService } from '../../../../core/data/relationship.service'; let comp: JournalComponent; let fixture: ComponentFixture; @@ -53,7 +54,8 @@ describe('JournalComponent', () => { declarations: [JournalComponent, GenericItemPageFieldComponent, TruncatePipe], providers: [ {provide: ItemDataService, useValue: {}}, - {provide: TruncatableService, useValue: {}} + {provide: TruncatableService, useValue: {}}, + {provide: RelationshipService, useValue: {}} ], schemas: [NO_ERRORS_SCHEMA] diff --git a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.ts b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.ts index dbbeb81662..605bd52238 100644 --- a/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.ts +++ b/src/app/entity-groups/journal-entities/item-pages/journal/journal.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; -import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ViewMode } from '../../../../core/shared/view-mode.model'; +import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; @listableObjectComponent('Journal', ViewMode.StandalonePage) @Component({ diff --git a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html index 4d97868b58..1b23d567f5 100644 --- a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html +++ b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html @@ -24,16 +24,6 @@
- - - -
+
+ + +
diff --git a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.ts b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.ts index d9d4461bfa..6df2d87503 100644 --- a/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.ts +++ b/src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; -import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ViewMode } from '../../../../core/shared/view-mode.model'; +import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; @listableObjectComponent('OrgUnit', ViewMode.StandalonePage) @Component({ diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.html b/src/app/entity-groups/research-entities/item-pages/person/person.component.html index ff675ab057..97a3cf416e 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.html +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.html @@ -53,8 +53,11 @@
- - + +
diff --git a/src/app/entity-groups/research-entities/item-pages/person/person.component.ts b/src/app/entity-groups/research-entities/item-pages/person/person.component.ts index 15c7184702..9972736b95 100644 --- a/src/app/entity-groups/research-entities/item-pages/person/person.component.ts +++ b/src/app/entity-groups/research-entities/item-pages/person/person.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; -import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ViewMode } from '../../../../core/shared/view-mode.model'; +import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; @listableObjectComponent('Person', ViewMode.StandalonePage) @Component({ diff --git a/src/app/entity-groups/research-entities/item-pages/project/project.component.ts b/src/app/entity-groups/research-entities/item-pages/project/project.component.ts index 8ac424af5b..4e432e869e 100644 --- a/src/app/entity-groups/research-entities/item-pages/project/project.component.ts +++ b/src/app/entity-groups/research-entities/item-pages/project/project.component.ts @@ -1,7 +1,7 @@ import { Component } from '@angular/core'; import { ItemComponent } from '../../../../+item-page/simple/item-types/shared/item.component'; -import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; import { ViewMode } from '../../../../core/shared/view-mode.model'; +import { listableObjectComponent } from '../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; @listableObjectComponent('Project', ViewMode.StandalonePage) @Component({ diff --git a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts index f3d0a28fda..867b5890eb 100644 --- a/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts +++ b/src/app/entity-groups/research-entities/metadata-representations/person/person-item-metadata-list-element.component.ts @@ -1,4 +1,4 @@ -import { Component } from '@angular/core'; +import { Component, OnInit } from '@angular/core'; import { metadataRepresentationComponent } from '../../../../shared/metadata-representation/metadata-representation.decorator'; import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model'; import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component'; diff --git a/src/app/entity-groups/research-entities/research-entities.module.ts b/src/app/entity-groups/research-entities/research-entities.module.ts index 8829318f34..cef3b4539b 100644 --- a/src/app/entity-groups/research-entities/research-entities.module.ts +++ b/src/app/entity-groups/research-entities/research-entities.module.ts @@ -20,6 +20,12 @@ import { OrgUnitSearchResultGridElementComponent } from './item-grid-elements/se import { ProjectSearchResultGridElementComponent } from './item-grid-elements/search-result-grid-elements/project/project-search-result-grid-element.component'; import { PersonItemMetadataListElementComponent } from './metadata-representations/person/person-item-metadata-list-element.component'; import { OrgUnitItemMetadataListElementComponent } from './metadata-representations/org-unit/org-unit-item-metadata-list-element.component'; +import { PersonSearchResultListSubmissionElementComponent } from './submission/item-list-elements/person/person-search-result-list-submission-element.component'; +import { PersonInputSuggestionsComponent } from './submission/item-list-elements/person/person-suggestions/person-input-suggestions.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 { OrgUnitSearchResultListSubmissionElementComponent } from './submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component'; +import { ExternalSourceEntryListSubmissionElementComponent } from './submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component'; const ENTRY_COMPONENTS = [ OrgUnitComponent, @@ -38,7 +44,13 @@ const ENTRY_COMPONENTS = [ ProjectSearchResultListElementComponent, PersonSearchResultGridElementComponent, OrgUnitSearchResultGridElementComponent, - ProjectSearchResultGridElementComponent + ProjectSearchResultGridElementComponent, + PersonSearchResultListSubmissionElementComponent, + PersonInputSuggestionsComponent, + NameVariantModalComponent, + OrgUnitSearchResultListSubmissionElementComponent, + OrgUnitInputSuggestionsComponent, + ExternalSourceEntryListSubmissionElementComponent ]; @NgModule({ @@ -49,7 +61,7 @@ const ENTRY_COMPONENTS = [ ItemPageModule ], declarations: [ - ...ENTRY_COMPONENTS + ...ENTRY_COMPONENTS, ], entryComponents: [ ...ENTRY_COMPONENTS diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html new file mode 100644 index 0000000000..55b8f38a5e --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.html @@ -0,0 +1,2 @@ +
{{object.display}}
+ diff --git a/src/app/+search-page/search-filters/search-filters.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.scss similarity index 100% rename from src/app/+search-page/search-filters/search-filters.component.scss rename to src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.scss diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts new file mode 100644 index 0000000000..fa153b8c5e --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.spec.ts @@ -0,0 +1,47 @@ +import { ExternalSourceEntryListSubmissionElementComponent } from './external-source-entry-list-submission-element.component'; +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ExternalSourceEntry } from '../../../../../core/shared/external-source-entry.model'; +import { TranslateModule } from '@ngx-translate/core'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; + +describe('ExternalSourceEntryListSubmissionElementComponent', () => { + let component: ExternalSourceEntryListSubmissionElementComponent; + let fixture: ComponentFixture; + + const uri = 'https://orcid.org/0001-0001-0001-0001'; + const entry = Object.assign(new ExternalSourceEntry(), { + id: '0001-0001-0001-0001', + display: 'John Doe', + value: 'John, Doe', + metadata: { + 'dc.identifier.uri': [ + { + value: uri + } + ] + } + }); + + beforeEach(async(() => { + TestBed.configureTestingModule({ + declarations: [ExternalSourceEntryListSubmissionElementComponent], + imports: [TranslateModule.forRoot()], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(ExternalSourceEntryListSubmissionElementComponent); + component = fixture.componentInstance; + component.object = entry; + fixture.detectChanges(); + }); + + it('should display the entry\'s display value', () => { + expect(fixture.nativeElement.textContent).toContain(entry.display); + }); + + it('should display the entry\'s uri', () => { + expect(fixture.nativeElement.textContent).toContain(uri); + }); +}); diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts new file mode 100644 index 0000000000..c0512b4995 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/external-source-entry/external-source-entry-list-submission-element.component.ts @@ -0,0 +1,28 @@ +import { AbstractListableElementComponent } from '../../../../../shared/object-collection/shared/object-collection-element/abstract-listable-element.component'; +import { ExternalSourceEntry } from '../../../../../core/shared/external-source-entry.model'; +import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { Context } from '../../../../../core/shared/context.model'; +import { Component, OnInit } from '@angular/core'; +import { Metadata } from '../../../../../core/shared/metadata.utils'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; + +@listableObjectComponent(ExternalSourceEntry, ViewMode.ListElement, Context.SubmissionModal) +@Component({ + selector: 'ds-external-source-entry-list-submission-element', + styleUrls: ['./external-source-entry-list-submission-element.component.scss'], + templateUrl: './external-source-entry-list-submission-element.component.html' +}) +/** + * The component for displaying a list element of an external source entry + */ +export class ExternalSourceEntryListSubmissionElementComponent extends AbstractListableElementComponent implements OnInit { + /** + * The metadata value for the object's uri + */ + uri: MetadataValue; + + ngOnInit(): void { + this.uri = Metadata.first(this.object.metadata, 'dc.identifier.uri'); + } +} diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html new file mode 100644 index 0000000000..b0fa714371 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.html @@ -0,0 +1,19 @@ +
+
+ +
+
+ + + + , + + + + + +
+
diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.scss new file mode 100644 index 0000000000..8fc6d2138d --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.scss @@ -0,0 +1,7 @@ +@import '../../../../../../styles/variables'; + +$submission-relationship-thumbnail-width: 80px; + +.person-thumbnail { + width: $submission-relationship-thumbnail-width; +} \ No newline at end of file diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.spec.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.spec.ts new file mode 100644 index 0000000000..2a77b64f43 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.spec.ts @@ -0,0 +1,154 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { of as observableOf } from 'rxjs'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { OrgUnitSearchResultListSubmissionElementComponent } from './org-unit-search-result-list-submission-element.component'; +import { Item } from '../../../../../core/shared/item.model'; +import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { RelationshipService } from '../../../../../core/data/relationship.service'; +import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ItemDataService } from '../../../../../core/data/item-data.service'; +import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service'; +import { Store } from '@ngrx/store'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { PaginatedList } from '../../../../../core/data/paginated-list'; + +let personListElementComponent: OrgUnitSearchResultListSubmissionElementComponent; +let fixture: ComponentFixture; + +let mockItemWithMetadata: ItemSearchResult; +let mockItemWithoutMetadata: ItemSearchResult; + +let nameVariant; +let mockRelationshipService; + +function init() { + mockItemWithMetadata = Object.assign( + new ItemSearchResult(), + { + indexableObject: Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(new PaginatedList(undefined, [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'organization.address.addressLocality': [ + { + language: 'en_US', + value: 'Europe' + } + ], + 'organization.address.addressCountry': [ + { + language: 'en_US', + value: 'Belgium' + } + ] + } + }) + }); + mockItemWithoutMetadata = Object.assign( + new ItemSearchResult(), + { + indexableObject: Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(new PaginatedList(undefined, [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } + }) + }); + + nameVariant = 'Doe J.'; + mockRelationshipService = { + getNameVariant: () => observableOf(nameVariant) + }; +} + +describe('OrgUnitSearchResultListSubmissionElementComponent', () => { + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + declarations: [OrgUnitSearchResultListSubmissionElementComponent, TruncatePipe], + providers: [ + { provide: TruncatableService, useValue: {} }, + { provide: RelationshipService, useValue: mockRelationshipService }, + { provide: NotificationsService, useValue: {} }, + { provide: TranslateService, useValue: {} }, + { provide: NgbModal, useValue: {} }, + { provide: ItemDataService, useValue: {} }, + { provide: SelectableListService, useValue: {} }, + { provide: Store, useValue: {} } + ], + + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(OrgUnitSearchResultListSubmissionElementComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(OrgUnitSearchResultListSubmissionElementComponent); + personListElementComponent = fixture.componentInstance; + + })); + + describe('When the item has a address locality span', () => { + beforeEach(() => { + personListElementComponent.object = mockItemWithMetadata; + fixture.detectChanges(); + }); + + it('should show the address locality span', () => { + const jobTitleField = fixture.debugElement.query(By.css('span.item-list-address-locality')); + expect(jobTitleField).not.toBeNull(); + }); + }); + + describe('When the item has no address locality', () => { + beforeEach(() => { + personListElementComponent.object = mockItemWithoutMetadata; + fixture.detectChanges(); + }); + + it('should not show the address locality span', () => { + const jobTitleField = fixture.debugElement.query(By.css('span.item-list-address-locality')); + expect(jobTitleField).toBeNull(); + }); + }); + + describe('When the item has a address country span', () => { + beforeEach(() => { + personListElementComponent.object = mockItemWithMetadata; + fixture.detectChanges(); + }); + + it('should show the address country span', () => { + const jobTitleField = fixture.debugElement.query(By.css('span.item-list-address-country')); + expect(jobTitleField).not.toBeNull(); + }); + }); + + describe('When the item has no address country', () => { + beforeEach(() => { + personListElementComponent.object = mockItemWithoutMetadata; + fixture.detectChanges(); + }); + + it('should not show the address country span', () => { + const jobTitleField = fixture.debugElement.query(By.css('span.item-list-address-country')); + expect(jobTitleField).toBeNull(); + }); + }); +}); diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts new file mode 100644 index 0000000000..cbddb8d6f9 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-search-result-list-submission-element.component.ts @@ -0,0 +1,98 @@ +import { Component, OnInit } from '@angular/core'; +import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { Item } from '../../../../../core/shared/item.model'; +import { Context } from '../../../../../core/shared/context.model'; +import { RelationshipService } from '../../../../../core/data/relationship.service'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { take } from 'rxjs/operators'; +import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; +import { ItemDataService } from '../../../../../core/data/item-data.service'; +import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service'; +import { NameVariantModalComponent } from '../../name-variant-modal/name-variant-modal.component'; + +@listableObjectComponent('OrgUnitSearchResult', ViewMode.ListElement, Context.SubmissionModal) +@Component({ + selector: 'ds-person-search-result-list-submission-element', + styleUrls: ['./org-unit-search-result-list-submission-element.component.scss'], + templateUrl: './org-unit-search-result-list-submission-element.component.html' +}) + +/** + * The component for displaying a list element for an item search result of the type OrgUnit + */ +export class OrgUnitSearchResultListSubmissionElementComponent extends SearchResultListElementComponent implements OnInit { + allSuggestions: string[]; + selectedName: string; + alternativeField = 'dc.title.alternative'; + + constructor(protected truncatableService: TruncatableService, + private relationshipService: RelationshipService, + private notificationsService: NotificationsService, + private translateService: TranslateService, + private modalService: NgbModal, + private itemDataService: ItemDataService, + private selectableListService: SelectableListService) { + super(truncatableService); + } + + ngOnInit() { + super.ngOnInit(); + const defaultValue = this.firstMetadataValue('organization.legalName'); + const alternatives = this.allMetadataValues(this.alternativeField); + this.allSuggestions = [defaultValue, ...alternatives]; + + this.relationshipService.getNameVariant(this.listID, this.dso.uuid) + .pipe(take(1)) + .subscribe((nameVariant: string) => { + this.selectedName = nameVariant || defaultValue; + } + ); + } + + select(value) { + this.selectableListService.isObjectSelected(this.listID, this.object) + .pipe(take(1)) + .subscribe((selected) => { + if (!selected) { + this.selectableListService.selectSingle(this.listID, this.object); + } + }); + this.relationshipService.setNameVariant(this.listID, this.dso.uuid, value); + } + + selectCustom(value) { + if (!this.allSuggestions.includes(value)) { + this.openModal(value) + .then(() => { + + const newName: MetadataValue = new MetadataValue(); + newName.value = value; + + const existingNames: MetadataValue[] = this.dso.metadata[this.alternativeField] || []; + const alternativeNames = { [this.alternativeField]: [...existingNames, newName] }; + const updatedItem = + Object.assign({}, this.dso, { + metadata: { + ...this.dso.metadata, + ...alternativeNames + }, + }); + this.itemDataService.update(updatedItem).pipe(take(1)).subscribe(); + }) + } + this.select(value); + } + + openModal(value): Promise { + const modalRef = this.modalService.open(NameVariantModalComponent, { centered: true }); + const modalComp = modalRef.componentInstance; + modalComp.value = value; + return modalRef.result; + } +} diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.html new file mode 100644 index 0000000000..e177b2b561 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.html @@ -0,0 +1,24 @@ +
+ + + +
\ No newline at end of file diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss new file mode 100644 index 0000000000..8301e12c5f --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.scss @@ -0,0 +1,18 @@ +form { + z-index: 1; + &:before { + position: absolute; + font-weight: 900; + font-family: "Font Awesome 5 Free"; + content: "\f0d7"; + top: 7px; + right: 0; + height: 20px; + width: 20px; + z-index: -1; + } + + input.suggestion_input { + background: transparent; + } +} \ No newline at end of file diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.spec.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.spec.ts new file mode 100644 index 0000000000..34b89cc8aa --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.spec.ts @@ -0,0 +1,64 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { OrgUnitInputSuggestionsComponent } from './org-unit-input-suggestions.component'; +import { FormsModule } from '@angular/forms'; + +let component: OrgUnitInputSuggestionsComponent; +let fixture: ComponentFixture; + +let suggestions: string[]; +let testValue; + +function init() { + suggestions = ['test', 'suggestion', 'example'] + testValue = 'bla'; +} + +describe('OrgUnitInputSuggestionsComponent', () => { + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + declarations: [OrgUnitInputSuggestionsComponent], + imports: [ + FormsModule, + ], + providers: [ + ], + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(OrgUnitInputSuggestionsComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(OrgUnitInputSuggestionsComponent); + component = fixture.componentInstance; + component.suggestions = suggestions; + fixture.detectChanges(); + })); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('When the component is initialized', () => { + it('should set the value to the first value of the suggestions', () => { + expect(component.value).toEqual('test'); + }); + }); + + describe('When onSubmit is called', () => { + it('should set the value to parameter of the method', () => { + component.onSubmit(testValue); + expect(component.value).toEqual(testValue); + }); + }); + + describe('When onClickSuggestion is called', () => { + it('should set the value to parameter of the method', () => { + component.onClickSuggestion(testValue); + expect(component.value).toEqual(testValue); + }); + }); + +}); diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.ts new file mode 100644 index 0000000000..c868281e00 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/org-unit/org-unit-suggestions/org-unit-input-suggestions.component.ts @@ -0,0 +1,48 @@ +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { InputSuggestionsComponent } from '../../../../../../shared/input-suggestions/input-suggestions.component'; + +@Component({ + selector: 'ds-org-unit-input-suggestions', + styleUrls: ['./org-unit-input-suggestions.component.scss', './../../../../../../shared/input-suggestions/input-suggestions.component.scss'], + templateUrl: './org-unit-input-suggestions.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + // Usage of forwardRef necessary https://github.com/angular/angular.io/issues/1151 + // tslint:disable-next-line:no-forward-ref + useExisting: forwardRef(() => OrgUnitInputSuggestionsComponent), + multi: true + } + ] +}) + +/** + * Component representing a form with a autocomplete functionality + */ +export class OrgUnitInputSuggestionsComponent extends InputSuggestionsComponent implements OnInit { + /** + * The suggestions that should be shown + */ + @Input() suggestions: string[] = []; + + ngOnInit() { + if (this.suggestions.length > 0) { + this.value = this.suggestions[0] + } + } + + onSubmit(data) { + this.value = data; + this.submitSuggestion.emit(data); + } + + onClickSuggestion(data) { + this.value = data; + this.clickSuggestion.emit(data); + this.close(); + this.blockReopen = true; + this.queryInput.nativeElement.focus(); + return false; + } +} diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html new file mode 100644 index 0000000000..df93c2f4f3 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.html @@ -0,0 +1,16 @@ +
+
+ +
+
+ + + + + + + + +
+
diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.scss new file mode 100644 index 0000000000..8fc6d2138d --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.scss @@ -0,0 +1,7 @@ +@import '../../../../../../styles/variables'; + +$submission-relationship-thumbnail-width: 80px; + +.person-thumbnail { + width: $submission-relationship-thumbnail-width; +} \ No newline at end of file diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts new file mode 100644 index 0000000000..a21f0ec075 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.spec.ts @@ -0,0 +1,124 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { ChangeDetectionStrategy, NO_ERRORS_SCHEMA } from '@angular/core'; +import { By } from '@angular/platform-browser'; +import { of as observableOf } from 'rxjs'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { PersonSearchResultListSubmissionElementComponent } from './person-search-result-list-submission-element.component'; +import { Item } from '../../../../../core/shared/item.model'; +import { TruncatePipe } from '../../../../../shared/utils/truncate.pipe'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { RelationshipService } from '../../../../../core/data/relationship.service'; +import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { ItemDataService } from '../../../../../core/data/item-data.service'; +import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service'; +import { Store } from '@ngrx/store'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../shared/testing/utils'; +import { PaginatedList } from '../../../../../core/data/paginated-list'; + +let personListElementComponent: PersonSearchResultListSubmissionElementComponent; +let fixture: ComponentFixture; + +let mockItemWithMetadata: ItemSearchResult; +let mockItemWithoutMetadata: ItemSearchResult; + +let nameVariant; +let mockRelationshipService; + +function init() { + mockItemWithMetadata = Object.assign( + new ItemSearchResult(), + { + indexableObject: Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(new PaginatedList(undefined, [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ], + 'person.jobTitle': [ + { + language: 'en_US', + value: 'Developer' + } + ] + } + }) + }); + mockItemWithoutMetadata = Object.assign( + new ItemSearchResult(), + { + indexableObject: Object.assign(new Item(), { + bundles: createSuccessfulRemoteDataObject$(new PaginatedList(undefined, [])), + metadata: { + 'dc.title': [ + { + language: 'en_US', + value: 'This is just another title' + } + ] + } + }) + }); + + nameVariant = 'Doe J.'; + mockRelationshipService = { + getNameVariant: () => observableOf(nameVariant) + }; +} + +describe('PersonSearchResultListElementSubmissionComponent', () => { + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + declarations: [PersonSearchResultListSubmissionElementComponent, TruncatePipe], + providers: [ + { provide: TruncatableService, useValue: {} }, + { provide: RelationshipService, useValue: mockRelationshipService }, + { provide: NotificationsService, useValue: {} }, + { provide: TranslateService, useValue: {} }, + { provide: NgbModal, useValue: {} }, + { provide: ItemDataService, useValue: {} }, + { provide: SelectableListService, useValue: {} }, + { provide: Store, useValue: {}} + ], + + schemas: [NO_ERRORS_SCHEMA] + }).overrideComponent(PersonSearchResultListSubmissionElementComponent, { + set: { changeDetection: ChangeDetectionStrategy.Default } + }).compileComponents(); + })); + + beforeEach(async(() => { + fixture = TestBed.createComponent(PersonSearchResultListSubmissionElementComponent); + personListElementComponent = fixture.componentInstance; + + })); + + describe('When the item has a job title', () => { + beforeEach(() => { + personListElementComponent.object = mockItemWithMetadata; + fixture.detectChanges(); + }); + + it('should show the job title span', () => { + const jobTitleField = fixture.debugElement.query(By.css('span.item-list-job-title')); + expect(jobTitleField).not.toBeNull(); + }); + }); + + describe('When the item has no job title', () => { + beforeEach(() => { + personListElementComponent.object = mockItemWithoutMetadata; + fixture.detectChanges(); + }); + + it('should not show the job title span', () => { + const jobTitleField = fixture.debugElement.query(By.css('span.item-list-job-title')); + expect(jobTitleField).toBeNull(); + }); + }); +}); diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts new file mode 100644 index 0000000000..37fd77649b --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-search-result-list-submission-element.component.ts @@ -0,0 +1,98 @@ +import { Component, OnInit } from '@angular/core'; +import { SearchResultListElementComponent } from '../../../../../shared/object-list/search-result-list-element/search-result-list-element.component'; +import { ItemSearchResult } from '../../../../../shared/object-collection/shared/item-search-result.model'; +import { listableObjectComponent } from '../../../../../shared/object-collection/shared/listable-object/listable-object.decorator'; +import { ViewMode } from '../../../../../core/shared/view-mode.model'; +import { Item } from '../../../../../core/shared/item.model'; +import { Context } from '../../../../../core/shared/context.model'; +import { RelationshipService } from '../../../../../core/data/relationship.service'; +import { TruncatableService } from '../../../../../shared/truncatable/truncatable.service'; +import { take } from 'rxjs/operators'; +import { NotificationsService } from '../../../../../shared/notifications/notifications.service'; +import { TranslateService } from '@ngx-translate/core'; +import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; +import { NameVariantModalComponent } from '../../name-variant-modal/name-variant-modal.component'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; +import { ItemDataService } from '../../../../../core/data/item-data.service'; +import { SelectableListService } from '../../../../../shared/object-list/selectable-list/selectable-list.service'; + +@listableObjectComponent('PersonSearchResult', ViewMode.ListElement, Context.SubmissionModal) +@Component({ + selector: 'ds-person-search-result-list-submission-element', + styleUrls: ['./person-search-result-list-submission-element.component.scss'], + templateUrl: './person-search-result-list-submission-element.component.html' +}) + +/** + * The component for displaying a list element for an item search result of the type Person + */ +export class PersonSearchResultListSubmissionElementComponent extends SearchResultListElementComponent implements OnInit { + allSuggestions: string[]; + selectedName: string; + alternativeField = 'dc.title.alternative'; + + constructor(protected truncatableService: TruncatableService, + private relationshipService: RelationshipService, + private notificationsService: NotificationsService, + private translateService: TranslateService, + private modalService: NgbModal, + private itemDataService: ItemDataService, + private selectableListService: SelectableListService) { + super(truncatableService); + } + + ngOnInit() { + super.ngOnInit(); + const defaultValue = this.firstMetadataValue('person.familyName') + ', ' + this.firstMetadataValue('person.givenName'); + const alternatives = this.allMetadataValues(this.alternativeField); + this.allSuggestions = [defaultValue, ...alternatives]; + + this.relationshipService.getNameVariant(this.listID, this.dso.uuid) + .pipe(take(1)) + .subscribe((nameVariant: string) => { + this.selectedName = nameVariant || defaultValue; + } + ); + } + + select(value) { + this.selectableListService.isObjectSelected(this.listID, this.object) + .pipe(take(1)) + .subscribe((selected) => { + if (!selected) { + this.selectableListService.selectSingle(this.listID, this.object); + } + }); + this.relationshipService.setNameVariant(this.listID, this.dso.uuid, value); + } + + selectCustom(value) { + if (!this.allSuggestions.includes(value)) { + this.openModal(value) + .then(() => { + + const newName: MetadataValue = new MetadataValue(); + newName.value = value; + + const existingNames: MetadataValue[] = this.dso.metadata[this.alternativeField] || []; + const alternativeNames = { [this.alternativeField]: [...existingNames, newName] }; + const updatedItem = + Object.assign({}, this.dso, { + metadata: { + ...this.dso.metadata, + ...alternativeNames + }, + }); + this.itemDataService.update(updatedItem).pipe(take(1)).subscribe(); + }) + } + this.select(value); + } + + openModal(value): Promise { + const modalRef = this.modalService.open(NameVariantModalComponent, { centered: true }); + const modalComp = modalRef.componentInstance; + modalComp.value = value; + return modalRef.result; + } +} diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.html b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.html new file mode 100644 index 0000000000..e177b2b561 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.html @@ -0,0 +1,24 @@ +
+ + + +
\ No newline at end of file diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss new file mode 100644 index 0000000000..8301e12c5f --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.scss @@ -0,0 +1,18 @@ +form { + z-index: 1; + &:before { + position: absolute; + font-weight: 900; + font-family: "Font Awesome 5 Free"; + content: "\f0d7"; + top: 7px; + right: 0; + height: 20px; + width: 20px; + z-index: -1; + } + + input.suggestion_input { + background: transparent; + } +} \ No newline at end of file diff --git a/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.ts b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.ts new file mode 100644 index 0000000000..a1802ce1a7 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/item-list-elements/person/person-suggestions/person-input-suggestions.component.ts @@ -0,0 +1,48 @@ +import { Component, forwardRef, Input, OnInit } from '@angular/core'; +import { NG_VALUE_ACCESSOR } from '@angular/forms'; +import { InputSuggestionsComponent } from '../../../../../../shared/input-suggestions/input-suggestions.component'; + +@Component({ + selector: 'ds-person-input-suggestions', + styleUrls: ['./person-input-suggestions.component.scss', './../../../../../../shared/input-suggestions/input-suggestions.component.scss'], + templateUrl: './person-input-suggestions.component.html', + providers: [ + { + provide: NG_VALUE_ACCESSOR, + // Usage of forwardRef necessary https://github.com/angular/angular.io/issues/1151 + // tslint:disable-next-line:no-forward-ref + useExisting: forwardRef(() => PersonInputSuggestionsComponent), + multi: true + } + ] +}) + +/** + * Component representing a form with a autocomplete functionality + */ +export class PersonInputSuggestionsComponent extends InputSuggestionsComponent implements OnInit { + /** + * The suggestions that should be shown + */ + @Input() suggestions: string[] = []; + + ngOnInit() { + if (this.suggestions.length > 0) { + this.value = this.suggestions[0] + } + } + + onSubmit(data) { + this.value = data; + this.submitSuggestion.emit(data); + } + + onClickSuggestion(data) { + this.value = data; + this.clickSuggestion.emit(data); + this.close(); + this.blockReopen = true; + this.queryInput.nativeElement.focus(); + return false; + } +} diff --git a/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.html b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.html new file mode 100644 index 0000000000..13ae884ccb --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.html @@ -0,0 +1,13 @@ + + + diff --git a/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.scss b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.scss similarity index 100% rename from src/app/+search-page/search-switch-configuration/search-switch-configuration.component.scss rename to src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.scss diff --git a/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.spec.ts b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.spec.ts new file mode 100644 index 0000000000..b5043ea2d6 --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.spec.ts @@ -0,0 +1,53 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; + +import { NameVariantModalComponent } from './name-variant-modal.component'; +import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { TranslateModule } from '@ngx-translate/core'; +import { By } from '@angular/platform-browser'; + +describe('NameVariantModalComponent', () => { + let component: NameVariantModalComponent; + let fixture: ComponentFixture; + let debugElement; + let modal; + + function init() { + modal = jasmine.createSpyObj('modal', ['close', 'dismiss']); + } + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + declarations: [NameVariantModalComponent], + imports: [NgbModule.forRoot(), TranslateModule.forRoot()], + providers: [{ provide: NgbActiveModal, useValue: modal }] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(NameVariantModalComponent); + component = fixture.componentInstance; + debugElement = fixture.debugElement; + fixture.detectChanges(); + + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('when close button is clicked, dismiss should be called on the modal', () => { + debugElement.query(By.css('button.close')).triggerEventHandler('click', {}); + expect(modal.dismiss).toHaveBeenCalled(); + }); + + it('when confirm button is clicked, close should be called on the modal', () => { + debugElement.query(By.css('button.confirm-button')).triggerEventHandler('click', {}); + expect(modal.close).toHaveBeenCalled(); + }); + + it('when decline button is clicked, dismiss should be called on the modal', () => { + debugElement.query(By.css('button.decline-button')).triggerEventHandler('click', {}); + expect(modal.dismiss).toHaveBeenCalled(); + }); +}); diff --git a/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts new file mode 100644 index 0000000000..eb6f7d01ac --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts @@ -0,0 +1,24 @@ +import { Component, Input } from '@angular/core'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; + +/** + * This component a pop up for when the user selects a custom name variant during submission for a relationship$ + * The user can either choose to decline or accept to save the name variant as a metadata in the entity + */ +@Component({ + selector: 'ds-name-variant-modal', + templateUrl: './name-variant-modal.component.html', + styleUrls: ['./name-variant-modal.component.scss'] +}) +/** + * The component for the modal to add a name variant to an item + */ +export class NameVariantModalComponent { + /** + * The name variant + */ + @Input() value: string; + + constructor(public modal: NgbActiveModal) { + } +} diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index a03fd01c53..58f7cb1ecf 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -1,20 +1,20 @@
-
- - - +
+ + + - -
+ +
diff --git a/src/app/navbar/navbar.component.ts b/src/app/navbar/navbar.component.ts index 4c7c3cd030..b2ba10fb98 100644 --- a/src/app/navbar/navbar.component.ts +++ b/src/app/navbar/navbar.component.ts @@ -53,17 +53,18 @@ export class NavbarComponent extends MenuComponent implements OnInit { } as TextMenuItemModel, index: 0 }, - // { - // id: 'browse_global_communities_and_collections', - // parentID: 'browse_global', - // active: false, - // visible: true, - // model: { - // type: MenuItemType.LINK, - // text: 'menu.section.browse_global_communities_and_collections', - // link: '#' - // } as LinkMenuItemModel, - // }, + /* Communities & Collections tree */ + { + id: `browse_global_communities_and_collections`, + parentID: 'browse_global', + active: false, + visible: true, + model: { + type: MenuItemType.LINK, + text: `menu.section.browse_global_communities_and_collections`, + link: `/community-list` + } as LinkMenuItemModel + }, /* Statistics */ { diff --git a/src/app/search-navbar/search-navbar.component.html b/src/app/search-navbar/search-navbar.component.html new file mode 100644 index 0000000000..13d792c80f --- /dev/null +++ b/src/app/search-navbar/search-navbar.component.html @@ -0,0 +1,12 @@ +
+
+
+ + + + +
+
+
diff --git a/src/app/search-navbar/search-navbar.component.scss b/src/app/search-navbar/search-navbar.component.scss new file mode 100644 index 0000000000..3606c47afc --- /dev/null +++ b/src/app/search-navbar/search-navbar.component.scss @@ -0,0 +1,25 @@ +input[type="text"] { + margin-top: -0.5 * $font-size-base; + + &:focus { + background-color: rgba(255, 255, 255, 0.5) !important; + } + + &.collapsed { + opacity: 0; + } +} + +a.submit-icon { + cursor: pointer; +} + + + +@media screen and (max-width: map-get($grid-breakpoints, sm)) { + #query:focus { + max-width: 250px !important; + width: 40vw !important; + } +} + diff --git a/src/app/search-navbar/search-navbar.component.spec.ts b/src/app/search-navbar/search-navbar.component.spec.ts new file mode 100644 index 0000000000..2a03acd2a2 --- /dev/null +++ b/src/app/search-navbar/search-navbar.component.spec.ts @@ -0,0 +1,121 @@ +import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing'; +import { FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { By } from '@angular/platform-browser'; +import { BrowserAnimationsModule } from '@angular/platform-browser/animations'; +import { Router } from '@angular/router'; +import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; +import { SearchService } from '../core/shared/search/search.service'; +import { MockTranslateLoader } from '../shared/mocks/mock-translate-loader'; + +import { SearchNavbarComponent } from './search-navbar.component'; + +describe('SearchNavbarComponent', () => { + let component: SearchNavbarComponent; + let fixture: ComponentFixture; + let mockSearchService: any; + let router: Router; + let routerStub; + + beforeEach(async(() => { + mockSearchService = { + getSearchLink() { + return '/search'; + } + }; + + routerStub = { + navigate: (commands) => commands + }; + TestBed.configureTestingModule({ + imports: [ + FormsModule, + ReactiveFormsModule, + BrowserAnimationsModule, + TranslateModule.forRoot({ + loader: { + provide: TranslateLoader, + useClass: MockTranslateLoader + } + })], + declarations: [SearchNavbarComponent], + providers: [ + { provide: SearchService, useValue: mockSearchService }, + { provide: Router, useValue: routerStub } + ] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(SearchNavbarComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + router = (component as any).router; + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('when you click on search icon', () => { + beforeEach(fakeAsync(() => { + spyOn(component, 'expand').and.callThrough(); + spyOn(component, 'onSubmit').and.callThrough(); + spyOn(router, 'navigate').and.callThrough(); + const searchIcon = fixture.debugElement.query(By.css('#search-navbar-container form .submit-icon')); + searchIcon.triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + fixture.detectChanges(); + })); + + it('input expands', () => { + expect(component.expand).toHaveBeenCalled(); + }); + + describe('empty query', () => { + describe('press submit button', () => { + beforeEach(fakeAsync(() => { + const searchIcon = fixture.debugElement.query(By.css('#search-navbar-container form .submit-icon')); + searchIcon.triggerEventHandler('click', { + preventDefault: () => {/**/ + } + }); + tick(); + fixture.detectChanges(); + })); + it('to search page with empty query', () => { + expect(component.onSubmit).toHaveBeenCalledWith({ query: '' }); + expect(router.navigate).toHaveBeenCalled(); + }); + }); + }); + + describe('fill in some query', () => { + let searchInput; + beforeEach(async () => { + await fixture.whenStable(); + fixture.detectChanges(); + searchInput = fixture.debugElement.query(By.css('#search-navbar-container form input')); + searchInput.nativeElement.value = 'test'; + searchInput.nativeElement.dispatchEvent(new Event('input')); + fixture.detectChanges(); + }); + describe('press submit button', () => { + beforeEach(fakeAsync(() => { + const searchIcon = fixture.debugElement.query(By.css('#search-navbar-container form .submit-icon')); + searchIcon.triggerEventHandler('click', null); + tick(); + fixture.detectChanges(); + })); + it('to search page with query', async () => { + expect(component.onSubmit).toHaveBeenCalledWith({ query: 'test' }); + expect(router.navigate).toHaveBeenCalled(); + }); + }); + }) + + }); +}); diff --git a/src/app/search-navbar/search-navbar.component.ts b/src/app/search-navbar/search-navbar.component.ts new file mode 100644 index 0000000000..1bedfb73ef --- /dev/null +++ b/src/app/search-navbar/search-navbar.component.ts @@ -0,0 +1,71 @@ +import { Component, ElementRef, ViewChild } from '@angular/core'; +import { FormBuilder } from '@angular/forms'; +import { Router } from '@angular/router'; +import { SearchService } from '../core/shared/search/search.service'; +import { expandSearchInput } from '../shared/animations/slide'; + +/** + * The search box in the header that expands on focus and collapses on focus out + */ +@Component({ + selector: 'ds-search-navbar', + templateUrl: './search-navbar.component.html', + styleUrls: ['./search-navbar.component.scss'], + animations: [expandSearchInput] +}) +export class SearchNavbarComponent { + + // The search form + searchForm; + // Whether or not the search bar is expanded, boolean for html ngIf, string fo AngularAnimation state change + searchExpanded = false; + isExpanded = 'collapsed'; + + // Search input field + @ViewChild('searchInput') searchField: ElementRef; + + constructor(private formBuilder: FormBuilder, private router: Router, private searchService: SearchService) { + this.searchForm = this.formBuilder.group(({ + query: '', + })); + } + + /** + * Expands search bar by angular animation, see expandSearchInput + */ + expand() { + this.searchExpanded = true; + this.isExpanded = 'expanded'; + this.editSearch(); + } + + /** + * Collapses & blurs search bar by angular animation, see expandSearchInput + */ + collapse() { + this.searchField.nativeElement.blur(); + this.searchExpanded = false; + this.isExpanded = 'collapsed'; + } + + /** + * Focuses on input search bar so search can be edited + */ + editSearch(): void { + this.searchField.nativeElement.focus(); + } + + /** + * Submits the search (on enter or on search icon click) + * @param data Data for the searchForm, containing the search query + */ + onSubmit(data: any) { + this.collapse(); + const linkToNavigateTo = this.searchService.getSearchLink().split('/'); + this.searchForm.reset(); + this.router.navigate(linkToNavigateTo, { + queryParams: Object.assign({}, { page: 1 }, data), + queryParamsHandling: 'merge' + }); + } +} diff --git a/src/app/shared/animations/slide.ts b/src/app/shared/animations/slide.ts index 38bfaaddca..7928a25659 100644 --- a/src/app/shared/animations/slide.ts +++ b/src/app/shared/animations/slide.ts @@ -1,13 +1,4 @@ -import { - animate, - animateChild, - group, - query, - state, - style, - transition, - trigger -} from '@angular/animations'; +import { animate, animateChild, group, query, state, style, transition, trigger } from '@angular/animations'; export const slide = trigger('slide', [ state('expanded', style({ height: '*' })), @@ -70,3 +61,30 @@ export const slideSidebarPadding = trigger('slideSidebarPadding', [ transition('hidden <=> expanded', [animate('200ms')]), transition('shown <=> expanded', [animate('200ms')]), ]); + +export const expandSearchInput = trigger('toggleAnimation', [ + state('collapsed', style({ + width: '30px', + opacity: '0' + })), + state('expanded', style({ + width: '250px', + opacity: '1' + })), + transition('* => collapsed', group([ + animate('300ms ease-in-out', style({ + width: '30px' + })), + animate('300ms ease-in', style({ + opacity: '0' + })) + ])), + transition('* => expanded', group([ + animate('300ms ease-out', style({ + opacity: '1' + })), + animate('300ms ease-in-out', style({ + width: '250px' + })) + ])) +]); diff --git a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html index 4df07880d8..86de30c23e 100644 --- a/src/app/shared/auth-nav-menu/auth-nav-menu.component.html +++ b/src/app/shared/auth-nav-menu/auth-nav-menu.component.html @@ -1,6 +1,6 @@