diff --git a/README.md b/README.md index a9f2b0861b..27e61afe76 100644 --- a/README.md +++ b/README.md @@ -3,13 +3,12 @@ dspace-angular ============== -> The next UI for DSpace, based on Angular Universal. +> The next UI for DSpace 7, based on Angular Universal. -This project is currently in pre-alpha. +This project is currently under active development. For more information on the DSpace 7 release see the [DSpace 7.0 Release Status wiki page](https://wiki.lyrasis.org/display/DSPACE/DSpace+Release+7.0+Status) -You can find additional information on the [wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+-+Angular+UI) or [the project board (waffle.io)](https://waffle.io/DSpace/dspace-angular). +You can find additional information on the DSpace 7 Angular UI on the [wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+-+Angular+UI+Development). -If you're looking for the 2016 Angular 2 DSpace UI prototype, you can find it [here](https://github.com/DSpace-Labs/angular2-ui-prototype) Quick start ----------- @@ -32,8 +31,6 @@ yarn start Then go to [http://localhost:3000](http://localhost:3000) in your browser -NOTE: currently there's not much to see at that URL. We really do need your help. If you're interested in jumping in, and you've made it this far, please look at the [the project board (waffle.io)](https://waffle.io/DSpace/dspace-angular), grab a card, and get to work. Thanks! - Not sure where to start? watch the training videos linked in the [Introduction to the technology](#introduction-to-the-technology) section below. Table of Contents @@ -42,24 +39,27 @@ Table of Contents - [Introduction to the technology](#introduction-to-the-technology) - [Requirements](#requirements) - [Installing](#installing) -- [Configuring](#configuring) + - [Configuring](#configuring) - [Running the app](#running-the-app) -- [Running in production mode](#running-in-production-mode) + - [Running in production mode](#running-in-production-mode) + - [Deploy](#deploy) + - [Running the application with Docker](#running-the-application-with-docker) - [Cleaning](#cleaning) - [Testing](#testing) + - [Test a Pull Request](#test-a-pull-request) - [Documentation](#documentation) - [Other commands](#other-commands) - [Recommended Editors/IDEs](#recommended-editorsides) - [Collaborating](#collaborating) - [File Structure](#file-structure) -- [3rd Party Library Installation](#3rd-party-library-installation) +- [Managing Dependencies (via yarn)](#managing-dependencies-via-yarn) - [Frequently asked questions](#frequently-asked-questions) - [License](#license) Introduction to the technology ------------------------------ -You can find more information on the technologies used in this project (Angular 2, Typescript, Angular Universal, RxJS, etc) on the [DuraSpace wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+UI+Technology+Stack) +You can find more information on the technologies used in this project (Angular.io, Typescript, Angular Universal, RxJS, etc) on the [LYRASIS wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+UI+Technology+Stack) Requirements ------------ @@ -75,8 +75,7 @@ Installing - `yarn run global` to install the required global dependencies - `yarn install` to install the local dependencies -Configuring ------------ +### Configuring Default configuration file is located in `config/` folder. @@ -98,8 +97,7 @@ Running the app After you have installed all dependencies you can now run the app. Run `yarn run watch` to start a local server which will watch for changes, rebuild the code, and reload the server for you. You can visit it at `http://localhost:3000`. -Running in production mode --------------------------- +### Running in production mode When building for production we're using Ahead of Time (AoT) compilation. With AoT, the browser downloads a pre-compiled version of the application, so it can render the application immediately, without waiting to compile the app first. The compiler is roughly half the size of Angular itself, so omitting it dramatically reduces the application payload. @@ -117,6 +115,19 @@ yarn run build:prod This will build the application and put the result in the `dist` folder +### Deploy +```bash +# deploy production in standalone pm2 container +yarn run deploy + +# remove production from standalone pm2 container +yarn run undeploy +``` + +### Running the application with Docker +See [Docker Runtime Options](docker/README.md) + + Cleaning -------- @@ -131,10 +142,6 @@ yarn run clean:prod yarn run clean:dist ``` -Running the application with Docker ------------------------------------ -See [Docker Runtime Options](docker/README.md) - Testing ------- @@ -189,21 +196,14 @@ To run all the tests (e.g.: to run tests with Continuous Integration software) y Documentation -------------- +See [`./docs`](docs) for further documentation. + +### Building code documentation + To build the code documentation we use [TYPEDOC](http://typedoc.org). TYPEDOC is a documentation generator for TypeScript projects. It extracts informations from properly formatted comments that can be written within the code files. Follow the instructions [here](http://typedoc.org/guides/doccomments/) to know how to make those comments. Run:`yarn run docs` to produce the documentation that will be available in the 'doc' folder. -Deploy ------- - -```bash -# deploy production in standalone pm2 container -yarn run deploy - -# remove production from standalone pm2 container -yarn run undeploy -``` - Other commands -------------- @@ -229,7 +229,7 @@ To get the most out of TypeScript, you'll need a TypeScript-aware editor. We've Collaborating ------------- -See [the guide on the wiki](https://wiki.duraspace.org/display/DSPACE/DSpace+7+-+Angular+2+UI#DSpace7-Angular2UI-Howtocontribute) +See [the guide on the wiki](https://wiki.lyrasis.org/display/DSPACE/DSpace+7+-+Angular+UI+Development#DSpace7-AngularUIDevelopment-Howtocontribute) File Structure -------------- @@ -335,10 +335,20 @@ dspace-angular └── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock) ``` -3rd Party Library Installation ------------------------------- +Managing Dependencies (via yarn) +------------- -Install your library via `yarn add lib-name --save` and import it in your code. `--save` will add it to `package.json`. +This project makes use of [`yarn`](https://yarnpkg.com/en/) to ensure that the exact same dependency versions are used every time you install it. + +* `yarn` creates a [`yarn.lock`](https://yarnpkg.com/en/docs/yarn-lock) to track those versions. That file is updated automatically by whenever dependencies are added/updated/removed via yarn. +* **Adding new dependencies**: To install/add a new dependency (third party library), use [`yarn add`](https://yarnpkg.com/en/docs/cli/add). For example: `yarn add some-lib`. + * If you are adding a new build tool dependency (to `devDependencies`), use `yarn add some-lib --dev` +* **Upgrading existing dependencies**: To upgrade existing dependencies, you can use [`yarn upgrade`](https://yarnpkg.com/en/docs/cli/upgrade). For example: `yarn upgrade some-lib` or `yarn upgrade some-lib@version` +* **Removing dependencies**: If a dependency is no longer needed, or replaced, use [`yarn remove`](https://yarnpkg.com/en/docs/cli/remove) to remove it. + +As you can see above, using `yarn` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `yarn` to keep dependencies updated / in sync.* + +### Adding Typings for libraries If the library does not include typings, you can install them using yarn: @@ -370,24 +380,6 @@ If you're importing a module that uses CommonJS you need to import as import * as _ from 'lodash'; ``` -Managing Dependencies (via yarn) -------------- - -This project makes use of [`yarn`](https://yarnpkg.com/en/) to ensure that the exact same dependency versions are used every time you install it. - -* `yarn` creates a [`yarn.lock`](https://yarnpkg.com/en/docs/yarn-lock) to track those versions. That file is updated automatically by whenever dependencies are added/updated/removed via yarn. -* **Adding new dependencies**: To install/add a new dependency (third party library), use [`yarn add`](https://yarnpkg.com/en/docs/cli/add). For example: `yarn add some-lib`. - * If you are adding a new build tool dependency (to `devDependencies`), use `yarn add some-lib --dev` -* **Upgrading existing dependencies**: To upgrade existing dependencies, you can use [`yarn upgrade`](https://yarnpkg.com/en/docs/cli/upgrade). For example: `yarn upgrade some-lib` or `yarn upgrade some-lib@version` -* **Removing dependencies**: If a dependency is no longer needed, or replaced, use [`yarn remove`](https://yarnpkg.com/en/docs/cli/remove) to remove it. - -As you can see above, using `yarn` commandline tools means that you should never need to modify the `package.json` manually. *We recommend always using `yarn` to keep dependencies updated / in sync.* - -Further Documentation ---------------------- - -See [`./docs`](docs) for further documentation. - Frequently asked questions -------------------------- @@ -411,5 +403,4 @@ Frequently asked questions License ------- - -http://www.dspace.org/license +This project's source code is made available under the DSpace BSD License: http://www.dspace.org/license diff --git a/config/environment.default.js b/config/environment.default.js index 7fb573039a..58193d31bc 100644 --- a/config/environment.default.js +++ b/config/environment.default.js @@ -141,6 +141,10 @@ module.exports = { code: 'nl', label: 'Nederlands', active: false, + }, { + code: 'pt', + label: 'Português', + active: true, }], // Browse-By Pages browseBy: { 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/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..aaabc0271a 100644 --- a/package.json +++ b/package.json @@ -75,6 +75,7 @@ }, "dependencies": { "@angular/animations": "^6.1.4", + "@angular/cdk": "^6.4.7", "@angular/cli": "^6.1.5", "@angular/common": "^6.1.4", "@angular/core": "^6.1.4", @@ -139,6 +140,7 @@ "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", @@ -228,7 +230,7 @@ "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", + "sass-loader": "^7.1.0", "script-ext-html-webpack-plugin": "2.0.1", "source-map": "0.7.3", "source-map-loader": "0.2.4", 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/cs.json5 b/resources/i18n/cs.json5 index 5e787b5cc9..2dbe7c55fb 100644 --- a/resources/i18n/cs.json5 +++ b/resources/i18n/cs.json5 @@ -1125,9 +1125,9 @@ // TODO New key - Add a translation "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", - // "item.edit.move.error": "An error occured when attempting to move the item", + // "item.edit.move.error": "An error occurred when attempting to move the item", // TODO New key - Add a translation - "item.edit.move.error": "An error occured when attempting to move the item", + "item.edit.move.error": "An error occurred when attempting to move the item", // "item.edit.move.head": "Move item: {{id}}", // TODO New key - Add a translation @@ -1153,9 +1153,9 @@ // TODO New key - Add a translation "item.edit.move.search.placeholder": "Enter a search query to look for collections", - // "item.edit.move.success": "The item has been moved succesfully", + // "item.edit.move.success": "The item has been moved successfully", // TODO New key - Add a translation - "item.edit.move.success": "The item has been moved succesfully", + "item.edit.move.success": "The item has been moved successfully", // "item.edit.move.title": "Move item", // TODO New key - Add a translation @@ -2911,9 +2911,9 @@ // TODO New key - Add a translation "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", - // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", + // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", // TODO New key - Add a translation - "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", + "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", // "submission.sections.upload.no-entry": "No", // TODO New key - Add a translation diff --git a/resources/i18n/de.json5 b/resources/i18n/de.json5 index fb56921fa4..ade65d7052 100644 --- a/resources/i18n/de.json5 +++ b/resources/i18n/de.json5 @@ -587,7 +587,7 @@ "item.edit.move.cancel": "Abbrechen", // "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", - // "item.edit.move.error": "An error occured when attempting to move the item", + // "item.edit.move.error": "An error occurred when attempting to move the item", "item.edit.move.error": "Ein Fehler ist beim Verschieben der Ressource aufgetreten", // "item.edit.move.head": "Move item: {{id}}", "item.edit.move.head": "Ressource verschieben: {{id}}", @@ -601,7 +601,7 @@ "item.edit.move.processing": "Verschieben...", // "item.edit.move.search.placeholder": "Enter a search query to look for collections", "item.edit.move.search.placeholder": "Geben Sie einen Begriff ein, um nach Sammlungen zu suchen", - // "item.edit.move.success": "The item has been moved succesfully", + // "item.edit.move.success": "The item has been moved successfully", "item.edit.move.success": "Die Ressource wurde erfolgreich verschoben", // "item.edit.move.title": "Move item", "item.edit.move.title": "Ressource verschieben", @@ -1515,7 +1515,7 @@ "submission.sections.upload.header.policy.default.nolist": "In diese Sammlung {{collectionName}} hochgeladene Dateien werden für folgende(n) Gruppe(n) zugänglich sein:", // "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", "submission.sections.upload.header.policy.default.withlist": "Bitte beachten Sie, dass in diese Sammlung {{collectionName}} hochgeladene Dateien zugüglich zu dem, was für einzelne Dateien entschieden wurde, für folgende Gruppe(n) zugänglich sein:", - // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", + // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", "submission.sections.upload.info": "Hier finden Sie alle Dateien, die aktuell zur Ressource gehören. Sie können die Metadaten und Zugriffsrechte bearbeiten oder weitere Dateien hinzufügen, indem Sie sie einfach irgenwo auf diese Seite ziehen.", // "submission.sections.upload.no-entry": "No", "submission.sections.upload.no-entry": "Kein Eintrag", diff --git a/resources/i18n/en.json5 b/resources/i18n/en.json5 index f3a1076914..b2f300897b 100644 --- a/resources/i18n/en.json5 +++ b/resources/i18n/en.json5 @@ -400,6 +400,14 @@ + "communityList.tabTitle": "DSpace - Community List", + + "communityList.title": "List of Communities", + + "communityList.showMore": "Show More", + + + "community.create.head": "Create a Community", "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", @@ -525,6 +533,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", @@ -550,6 +561,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", @@ -688,7 +703,7 @@ "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", - "item.edit.move.error": "An error occured when attempting to move the item", + "item.edit.move.error": "An error occurred when attempting to move the item", "item.edit.move.head": "Move item: {{id}}", @@ -702,7 +717,7 @@ "item.edit.move.search.placeholder": "Enter a search query to look for collections", - "item.edit.move.success": "The item has been moved succesfully", + "item.edit.move.success": "The item has been moved successfully", "item.edit.move.title": "Move item", @@ -790,7 +805,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...", @@ -888,9 +903,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", @@ -1344,6 +1367,8 @@ "project.page.titleprefix": "Research Project: ", + "project.search.results.head": "Project Search Results", + "publication.listelement.badge": "Publication", @@ -1419,6 +1444,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", @@ -1587,6 +1615,69 @@ "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": "Search for Authors", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal": "Search for Journals", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Issue": "Search for Journal Issues", + + "submission.sections.describe.relationship-lookup.search-tab.tab-title.Journal Volume": "Search for Journal Volumes", + + "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.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", @@ -1668,7 +1759,7 @@ "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", - "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", + "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", "submission.sections.upload.no-entry": "No", diff --git a/resources/i18n/es.json5 b/resources/i18n/es.json5 index 99c5168d3e..230af002b6 100644 --- a/resources/i18n/es.json5 +++ b/resources/i18n/es.json5 @@ -591,7 +591,7 @@ "item.edit.move.cancel": "Cancelar", // "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", "item.edit.move.description": "Seleccione la colección a la que desea mover este ítem. Para reducir la lista de colecciones mostradas, puede ingresar una consulta de búsqueda en el cuadro.", -// "item.edit.move.error": "An error occured when attempting to move the item", +// "item.edit.move.error": "An error occurred when attempting to move the item", "item.edit.move.error": "Ha ocurrido un error cuando ha intentado mover el ítem", // "item.edit.move.head": "Move item: {{id}}", "item.edit.move.head": "Mover el ítem: {{id}}", @@ -605,7 +605,7 @@ "item.edit.move.processing": "Moviendo...", // "item.edit.move.search.placeholder": "Enter a search query to look for collections", "item.edit.move.search.placeholder": "Ingrese una consulta para buscar colecciones", -// "item.edit.move.success": "The item has been moved succesfully", +// "item.edit.move.success": "The item has been moved successfully", "item.edit.move.success": "El ítem ha sido movido exitosamente", // "item.edit.move.title": "Move item", "item.edit.move.title": "Mover ítem", @@ -1521,7 +1521,7 @@ "submission.sections.upload.header.policy.default.nolist": "Archivos subidos a la colección {{collectionName}} serán accesibles según el siguiente grupo(s):", // "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", "submission.sections.upload.header.policy.default.withlist": "Tenga en cuenta que los archivos cargados en la colección {{collectionName}} serán accesibles, además de lo que se decida explísitamente para el único archivo con el siguiente grupo(s):", -// "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", +// "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", "submission.sections.upload.info": "Aquí encontrará todos los archivos aque actualmente están en el ítem.. Puede actualizar los metadatos del archivo y las condiciones de acceso o cargar los archivos adicionales simplemente arrastrándolos y soltándolos en cualquier parte de la página", // "submission.sections.upload.no-entry": "No", "submission.sections.upload.no-entry": "No", diff --git a/resources/i18n/nl.json5 b/resources/i18n/nl.json5 index 81f8ec4ada..3eb7b7c7c3 100644 --- a/resources/i18n/nl.json5 +++ b/resources/i18n/nl.json5 @@ -1122,9 +1122,9 @@ // TODO New key - Add a translation "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", - // "item.edit.move.error": "An error occured when attempting to move the item", + // "item.edit.move.error": "An error occurred when attempting to move the item", // TODO New key - Add a translation - "item.edit.move.error": "An error occured when attempting to move the item", + "item.edit.move.error": "An error occurred when attempting to move the item", // "item.edit.move.head": "Move item: {{id}}", // TODO New key - Add a translation @@ -1150,9 +1150,9 @@ // TODO New key - Add a translation "item.edit.move.search.placeholder": "Enter a search query to look for collections", - // "item.edit.move.success": "The item has been moved succesfully", + // "item.edit.move.success": "The item has been moved successfully", // TODO New key - Add a translation - "item.edit.move.success": "The item has been moved succesfully", + "item.edit.move.success": "The item has been moved successfully", // "item.edit.move.title": "Move item", // TODO New key - Add a translation @@ -2908,9 +2908,9 @@ // TODO New key - Add a translation "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", - // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", + // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", // TODO New key - Add a translation - "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", + "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", // "submission.sections.upload.no-entry": "No", // TODO New key - Add a translation diff --git a/resources/i18n/pt.json5 b/resources/i18n/pt.json5 index 398c57e6b2..15f0608520 100644 --- a/resources/i18n/pt.json5 +++ b/resources/i18n/pt.json5 @@ -1,958 +1,728 @@ { // "404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ", - // TODO New key - Add a translation - "404.help": "We can't find the page you're looking for. The page may have been moved or deleted. You can use the button below to get back to the home page. ", + "404.help": "Não pudemos encontrar a página pela qual procura. A página pode ter sido movida ou apagada. Você pode utilizar o botão abaixo para voltar a página inicial. ", // "404.link.home-page": "Take me to the home page", - // TODO New key - Add a translation - "404.link.home-page": "Take me to the home page", + "404.link.home-page": "Leve-me a página inicial", // "404.page-not-found": "page not found", - // TODO New key - Add a translation - "404.page-not-found": "page not found", + "404.page-not-found": "página não encontrada", // "admin.registries.bitstream-formats.create.failure.content": "An error occurred while creating the new bitstream format.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.failure.content": "An error occurred while creating the new bitstream format.", + "admin.registries.bitstream-formats.create.failure.content": "Um erro ocorreu durante a criação do novo formato de bitstream.", // "admin.registries.bitstream-formats.create.failure.head": "Failure", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.failure.head": "Failure", + "admin.registries.bitstream-formats.create.failure.head": "Falha", // "admin.registries.bitstream-formats.create.head": "Create Bitstream format", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.head": "Create Bitstream format", + "admin.registries.bitstream-formats.create.head": "Criar formato de Bitstream", // "admin.registries.bitstream-formats.create.new": "Add a new bitstream format", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.new": "Add a new bitstream format", + "admin.registries.bitstream-formats.create.new": "Adicionar um novo formato de bitstream", // "admin.registries.bitstream-formats.create.success.content": "The new bitstream format was successfully created.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.success.content": "The new bitstream format was successfully created.", + "admin.registries.bitstream-formats.create.success.content": "O novo formato de bitstream foi criado com sucesso.", // "admin.registries.bitstream-formats.create.success.head": "Success", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.create.success.head": "Success", + "admin.registries.bitstream-formats.create.success.head": "Sucesso", // "admin.registries.bitstream-formats.delete.failure.amount": "Failed to remove {{ amount }} format(s)", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.delete.failure.amount": "Failed to remove {{ amount }} format(s)", + "admin.registries.bitstream-formats.delete.failure.amount": "Falha ao remover {{ amount }} formato(s)", // "admin.registries.bitstream-formats.delete.failure.head": "Failure", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.delete.failure.head": "Failure", + "admin.registries.bitstream-formats.delete.failure.head": "Falha", // "admin.registries.bitstream-formats.delete.success.amount": "Successfully removed {{ amount }} format(s)", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.delete.success.amount": "Successfully removed {{ amount }} format(s)", + "admin.registries.bitstream-formats.delete.success.amount": "Removeu {{ amount }} formato(s) com sucesso", // "admin.registries.bitstream-formats.delete.success.head": "Success", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.delete.success.head": "Success", + "admin.registries.bitstream-formats.delete.success.head": "Sucesso", // "admin.registries.bitstream-formats.description": "This list of bitstream formats provides information about known formats and their support level.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.description": "This list of bitstream formats provides information about known formats and their support level.", + "admin.registries.bitstream-formats.description": "Esta lista de formatos de bitstream provê informações sobre formatos conhecidos e seus níveis de suporte.", // "admin.registries.bitstream-formats.edit.description.hint": "", - // TODO New key - Add a translation "admin.registries.bitstream-formats.edit.description.hint": "", // "admin.registries.bitstream-formats.edit.description.label": "Description", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.description.label": "Description", + "admin.registries.bitstream-formats.edit.description.label": "Descrição", // "admin.registries.bitstream-formats.edit.extensions.hint": "Extensions are file extensions that are used to automatically identify the format of uploaded files. You can enter several extensions for each format.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.extensions.hint": "Extensions are file extensions that are used to automatically identify the format of uploaded files. You can enter several extensions for each format.", + "admin.registries.bitstream-formats.edit.extensions.hint": "Extensões são extensões de arquivo que são usadas para identificar automaticamente o formato dos arquivo enviados. Você pode informar várias extensões para cada formato.", // "admin.registries.bitstream-formats.edit.extensions.label": "File extensions", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.extensions.label": "File extensions", + "admin.registries.bitstream-formats.edit.extensions.label": "Extensões de arquivo", // "admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extenstion without the dot", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.extensions.placeholder": "Enter a file extenstion without the dot", + "admin.registries.bitstream-formats.edit.extensions.placeholder": "Informe uma extenção e arquivo sem o ponto", // "admin.registries.bitstream-formats.edit.failure.content": "An error occurred while editing the bitstream format.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.failure.content": "An error occurred while editing the bitstream format.", + "admin.registries.bitstream-formats.edit.failure.content": "Ocorreu um erro ao editar o formato de bitstream.", // "admin.registries.bitstream-formats.edit.failure.head": "Failure", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.failure.head": "Failure", + "admin.registries.bitstream-formats.edit.failure.head": "Falha", // "admin.registries.bitstream-formats.edit.head": "Bitstream format: {{ format }}", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.head": "Bitstream format: {{ format }}", + "admin.registries.bitstream-formats.edit.head": "Formato de bitstream: {{ format }}", - // "admin.registries.bitstream-formats.edit.internal.hint": "Formats marked as internal are are hidden from the user, and used for administrative purposes.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.internal.hint": "Formats marked as internal are are hidden from the user, and used for administrative purposes.", + // "admin.registries.bitstream-formats.edit.internal.hint": "Formats marked as internal are hidden from the user, and used for administrative purposes.", + "admin.registries.bitstream-formats.edit.internal.hint": "Formatos marcados como interno são ocultos para o usuário, e utilizados por motivos administrativos.", // "admin.registries.bitstream-formats.edit.internal.label": "Internal", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.internal.label": "Internal", + "admin.registries.bitstream-formats.edit.internal.label": "Interno", // "admin.registries.bitstream-formats.edit.mimetype.hint": "The MIME type associated with this format, does not have to be unique.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.mimetype.hint": "The MIME type associated with this format, does not have to be unique.", + "admin.registries.bitstream-formats.edit.mimetype.hint": "O MIME type associado à este formato não tem que ser único.", // "admin.registries.bitstream-formats.edit.mimetype.label": "MIME Type", - // TODO New key - Add a translation "admin.registries.bitstream-formats.edit.mimetype.label": "MIME Type", // "admin.registries.bitstream-formats.edit.shortDescription.hint": "A unique name for this format, (e.g. Microsoft Word XP or Microsoft Word 2000)", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.shortDescription.hint": "A unique name for this format, (e.g. Microsoft Word XP or Microsoft Word 2000)", + "admin.registries.bitstream-formats.edit.shortDescription.hint": "Um nome único para este formato (exemplo. Microsoft Word XP ou Microsoft Word 2000)", // "admin.registries.bitstream-formats.edit.shortDescription.label": "Name", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.shortDescription.label": "Name", + "admin.registries.bitstream-formats.edit.shortDescription.label": "Nome", // "admin.registries.bitstream-formats.edit.success.content": "The bitstream format was successfully edited.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.success.content": "The bitstream format was successfully edited.", + "admin.registries.bitstream-formats.edit.success.content": "O formato de bitstream foi editedo com sucesso.", // "admin.registries.bitstream-formats.edit.success.head": "Success", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.success.head": "Success", + "admin.registries.bitstream-formats.edit.success.head": "Sucesso", // "admin.registries.bitstream-formats.edit.supportLevel.hint": "The level of support your institution pledges for this format.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.supportLevel.hint": "The level of support your institution pledges for this format.", + "admin.registries.bitstream-formats.edit.supportLevel.hint": "O nível de suporte que a sua instituição promete para este formato.", // "admin.registries.bitstream-formats.edit.supportLevel.label": "Support level", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.edit.supportLevel.label": "Support level", + "admin.registries.bitstream-formats.edit.supportLevel.label": "Nível de suporte", // "admin.registries.bitstream-formats.head": "Bitstream Format Registry", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.head": "Bitstream Format Registry", + "admin.registries.bitstream-formats.head": "Registro de Formato de Bitstream", // "admin.registries.bitstream-formats.no-items": "No bitstream formats to show.", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.no-items": "No bitstream formats to show.", + "admin.registries.bitstream-formats.no-items": "Nenhum formato de bitstream para exibir.", // "admin.registries.bitstream-formats.table.delete": "Delete selected", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.delete": "Delete selected", + "admin.registries.bitstream-formats.table.delete": "Apagar selecionado(s)", // "admin.registries.bitstream-formats.table.deselect-all": "Deselect all", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.deselect-all": "Deselect all", + "admin.registries.bitstream-formats.table.deselect-all": "Desselecionar todos", // "admin.registries.bitstream-formats.table.internal": "internal", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.internal": "internal", + "admin.registries.bitstream-formats.table.internal": "Interno", // "admin.registries.bitstream-formats.table.mimetype": "MIME Type", - // TODO New key - Add a translation "admin.registries.bitstream-formats.table.mimetype": "MIME Type", // "admin.registries.bitstream-formats.table.name": "Name", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.name": "Name", + "admin.registries.bitstream-formats.table.name": "Nome", // "admin.registries.bitstream-formats.table.return": "Return", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.return": "Return", + "admin.registries.bitstream-formats.table.return": "Voltar", // "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Known", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Known", + "admin.registries.bitstream-formats.table.supportLevel.KNOWN": "Conhecido", // "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Supported", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Supported", + "admin.registries.bitstream-formats.table.supportLevel.SUPPORTED": "Com suporte", // "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Unknown", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Unknown", + "admin.registries.bitstream-formats.table.supportLevel.UNKNOWN": "Desconhecido", // "admin.registries.bitstream-formats.table.supportLevel.head": "Support Level", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.table.supportLevel.head": "Support Level", + "admin.registries.bitstream-formats.table.supportLevel.head": "Nível de Suporte", // "admin.registries.bitstream-formats.title": "DSpace Angular :: Bitstream Format Registry", - // TODO New key - Add a translation - "admin.registries.bitstream-formats.title": "DSpace Angular :: Bitstream Format Registry", + "admin.registries.bitstream-formats.title": "DSpace Angular :: Registro de Formato de Bitstream", // "admin.registries.metadata.description": "The metadata registry maintains a list of all metadata fields available in the repository. These fields may be divided amongst multiple schemas. However, DSpace requires the qualified Dublin Core schema.", - // TODO New key - Add a translation - "admin.registries.metadata.description": "The metadata registry maintains a list of all metadata fields available in the repository. These fields may be divided amongst multiple schemas. However, DSpace requires the qualified Dublin Core schema.", + "admin.registries.metadata.description": "O registro de metadados mantém a lista de todos os campos de metadados disponíveis no repositório. Estes campos podêm ser divididos em multiplos esquemas. Entretanto, o DSpace requer esquemas de Dublin Core qualificados.", // "admin.registries.metadata.form.create": "Create metadata schema", - // TODO New key - Add a translation - "admin.registries.metadata.form.create": "Create metadata schema", + "admin.registries.metadata.form.create": "Criar esquema de metadados", // "admin.registries.metadata.form.edit": "Edit metadata schema", - // TODO New key - Add a translation - "admin.registries.metadata.form.edit": "Edit metadata schema", + "admin.registries.metadata.form.edit": "Editar esquema de metadados", // "admin.registries.metadata.form.name": "Name", - // TODO New key - Add a translation - "admin.registries.metadata.form.name": "Name", + "admin.registries.metadata.form.name": "Nome", // "admin.registries.metadata.form.namespace": "Namespace", - // TODO New key - Add a translation "admin.registries.metadata.form.namespace": "Namespace", // "admin.registries.metadata.head": "Metadata Registry", - // TODO New key - Add a translation - "admin.registries.metadata.head": "Metadata Registry", + "admin.registries.metadata.head": "Registro de Metadados", // "admin.registries.metadata.schemas.no-items": "No metadata schemas to show.", - // TODO New key - Add a translation - "admin.registries.metadata.schemas.no-items": "No metadata schemas to show.", + "admin.registries.metadata.schemas.no-items": "Nenhum esquema de metadados a mostrar.", // "admin.registries.metadata.schemas.table.delete": "Delete selected", - // TODO New key - Add a translation - "admin.registries.metadata.schemas.table.delete": "Delete selected", + "admin.registries.metadata.schemas.table.delete": "Apagar selecionado(s)", // "admin.registries.metadata.schemas.table.id": "ID", - // TODO New key - Add a translation "admin.registries.metadata.schemas.table.id": "ID", // "admin.registries.metadata.schemas.table.name": "Name", - // TODO New key - Add a translation - "admin.registries.metadata.schemas.table.name": "Name", + "admin.registries.metadata.schemas.table.name": "Nome", // "admin.registries.metadata.schemas.table.namespace": "Namespace", - // TODO New key - Add a translation "admin.registries.metadata.schemas.table.namespace": "Namespace", // "admin.registries.metadata.title": "DSpace Angular :: Metadata Registry", - // TODO New key - Add a translation - "admin.registries.metadata.title": "DSpace Angular :: Metadata Registry", + "admin.registries.metadata.title": "DSpace Angular :: Registro de Metadados", // "admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".", - // TODO New key - Add a translation - "admin.registries.schema.description": "This is the metadata schema for \"{{namespace}}\".", + "admin.registries.schema.description": "Este é o esquema de metadados para \"{{namespace}}\".", // "admin.registries.schema.fields.head": "Schema metadata fields", - // TODO New key - Add a translation - "admin.registries.schema.fields.head": "Schema metadata fields", + "admin.registries.schema.fields.head": "Campos do esquema de metadados", // "admin.registries.schema.fields.no-items": "No metadata fields to show.", - // TODO New key - Add a translation - "admin.registries.schema.fields.no-items": "No metadata fields to show.", + "admin.registries.schema.fields.no-items": "Nenhum campo de metadado a exibir.", // "admin.registries.schema.fields.table.delete": "Delete selected", - // TODO New key - Add a translation - "admin.registries.schema.fields.table.delete": "Delete selected", + "admin.registries.schema.fields.table.delete": "Apagar selecionado(s)", // "admin.registries.schema.fields.table.field": "Field", - // TODO New key - Add a translation - "admin.registries.schema.fields.table.field": "Field", + "admin.registries.schema.fields.table.field": "Campo", // "admin.registries.schema.fields.table.scopenote": "Scope Note", - // TODO New key - Add a translation - "admin.registries.schema.fields.table.scopenote": "Scope Note", + "admin.registries.schema.fields.table.scopenote": "Nota de escopo", // "admin.registries.schema.form.create": "Create metadata field", - // TODO New key - Add a translation - "admin.registries.schema.form.create": "Create metadata field", + "admin.registries.schema.form.create": "Criar campo de metadado", // "admin.registries.schema.form.edit": "Edit metadata field", - // TODO New key - Add a translation - "admin.registries.schema.form.edit": "Edit metadata field", + "admin.registries.schema.form.edit": "Editar campo de metadado", // "admin.registries.schema.form.element": "Element", - // TODO New key - Add a translation - "admin.registries.schema.form.element": "Element", + "admin.registries.schema.form.element": "Elemento", // "admin.registries.schema.form.qualifier": "Qualifier", - // TODO New key - Add a translation - "admin.registries.schema.form.qualifier": "Qualifier", + "admin.registries.schema.form.qualifier": "Qualificador", // "admin.registries.schema.form.scopenote": "Scope Note", - // TODO New key - Add a translation - "admin.registries.schema.form.scopenote": "Scope Note", + "admin.registries.schema.form.scopenote": "Nota de Escopo", // "admin.registries.schema.head": "Metadata Schema", - // TODO New key - Add a translation - "admin.registries.schema.head": "Metadata Schema", + "admin.registries.schema.head": "Esquema de Metadados", // "admin.registries.schema.notification.created": "Successfully created metadata schema \"{{prefix}}\"", - // TODO New key - Add a translation - "admin.registries.schema.notification.created": "Successfully created metadata schema \"{{prefix}}\"", + "admin.registries.schema.notification.created": "Criou o esquema de metadados \"{{prefix}}\" com sucesso", // "admin.registries.schema.notification.deleted.failure": "Failed to delete {{amount}} metadata schemas", - // TODO New key - Add a translation - "admin.registries.schema.notification.deleted.failure": "Failed to delete {{amount}} metadata schemas", + "admin.registries.schema.notification.deleted.failure": "Falhou ao apagar {{amount}} esquema(s) de metadados", // "admin.registries.schema.notification.deleted.success": "Successfully deleted {{amount}} metadata schemas", - // TODO New key - Add a translation - "admin.registries.schema.notification.deleted.success": "Successfully deleted {{amount}} metadata schemas", + "admin.registries.schema.notification.deleted.success": "Apagou {{amount}} esquema(s) de metadados com sucesso", // "admin.registries.schema.notification.edited": "Successfully edited metadata schema \"{{prefix}}\"", - // TODO New key - Add a translation - "admin.registries.schema.notification.edited": "Successfully edited metadata schema \"{{prefix}}\"", + "admin.registries.schema.notification.edited": "Editou o esquema de metadados \"{{prefix}}\" com sucesso", // "admin.registries.schema.notification.failure": "Error", - // TODO New key - Add a translation - "admin.registries.schema.notification.failure": "Error", + "admin.registries.schema.notification.failure": "Erro", // "admin.registries.schema.notification.field.created": "Successfully created metadata field \"{{field}}\"", - // TODO New key - Add a translation - "admin.registries.schema.notification.field.created": "Successfully created metadata field \"{{field}}\"", + "admin.registries.schema.notification.field.created": "Criou o campo de medado \"{{field}}\" com sucesso", // "admin.registries.schema.notification.field.deleted.failure": "Failed to delete {{amount}} metadata fields", - // TODO New key - Add a translation - "admin.registries.schema.notification.field.deleted.failure": "Failed to delete {{amount}} metadata fields", + "admin.registries.schema.notification.field.deleted.failure": "Falhou ao apagar {{amount}} campo(s) de metadados", // "admin.registries.schema.notification.field.deleted.success": "Successfully deleted {{amount}} metadata fields", - // TODO New key - Add a translation - "admin.registries.schema.notification.field.deleted.success": "Successfully deleted {{amount}} metadata fields", + "admin.registries.schema.notification.field.deleted.success": "Apagou {{amount}} campo(s) de metadados com sucesso", // "admin.registries.schema.notification.field.edited": "Successfully edited metadata field \"{{field}}\"", - // TODO New key - Add a translation - "admin.registries.schema.notification.field.edited": "Successfully edited metadata field \"{{field}}\"", + "admin.registries.schema.notification.field.edited": "Editou o campo de metadodo \"{{field}}\" com sucesso", // "admin.registries.schema.notification.success": "Success", - // TODO New key - Add a translation - "admin.registries.schema.notification.success": "Success", + "admin.registries.schema.notification.success": "Sucesso", // "admin.registries.schema.return": "Return", - // TODO New key - Add a translation - "admin.registries.schema.return": "Return", + "admin.registries.schema.return": "Voltar", // "admin.registries.schema.title": "DSpace Angular :: Metadata Schema Registry", - // TODO New key - Add a translation - "admin.registries.schema.title": "DSpace Angular :: Metadata Schema Registry", + "admin.registries.schema.title": "DSpace Angular :: Registro de Esquema de Metadados", // "auth.errors.invalid-user": "Invalid email address or password.", - // TODO New key - Add a translation - "auth.errors.invalid-user": "Invalid email address or password.", + "auth.errors.invalid-user": "Endereço de email ou senha inválidos.", // "auth.messages.expired": "Your session has expired. Please log in again.", - // TODO New key - Add a translation - "auth.messages.expired": "Your session has expired. Please log in again.", + "auth.messages.expired": "Sua sessão expirou. Por favor entre novamente.", // "browse.comcol.by.author": "By Author", - // TODO New key - Add a translation - "browse.comcol.by.author": "By Author", + "browse.comcol.by.author": "Por Autor", // "browse.comcol.by.dateissued": "By Issue Date", - // TODO New key - Add a translation - "browse.comcol.by.dateissued": "By Issue Date", + "browse.comcol.by.dateissued": "Por Data de Publicação", // "browse.comcol.by.subject": "By Subject", - // TODO New key - Add a translation - "browse.comcol.by.subject": "By Subject", + "browse.comcol.by.subject": "Por Assunto", // "browse.comcol.by.title": "By Title", - // TODO New key - Add a translation - "browse.comcol.by.title": "By Title", + "browse.comcol.by.title": "Por Título", // "browse.comcol.head": "Browse", - // TODO New key - Add a translation - "browse.comcol.head": "Browse", + "browse.comcol.head": "Navegar", // "browse.empty": "No items to show.", - // TODO New key - Add a translation - "browse.empty": "No items to show.", + "browse.empty": "Sem itens a exibir.", // "browse.metadata.author": "Author", - // TODO New key - Add a translation - "browse.metadata.author": "Author", + "browse.metadata.author": "Autor", // "browse.metadata.dateissued": "Issue Date", - // TODO New key - Add a translation - "browse.metadata.dateissued": "Issue Date", + "browse.metadata.dateissued": "Data de Publicação", // "browse.metadata.subject": "Subject", - // TODO New key - Add a translation - "browse.metadata.subject": "Subject", + "browse.metadata.subject": "Assunto", // "browse.metadata.title": "Title", - // TODO New key - Add a translation - "browse.metadata.title": "Title", + "browse.metadata.title": "Título", // "browse.startsWith.choose_start": "(Choose start)", - // TODO New key - Add a translation - "browse.startsWith.choose_start": "(Choose start)", + "browse.startsWith.choose_start": "(Escolha o início)", // "browse.startsWith.choose_year": "(Choose year)", - // TODO New key - Add a translation - "browse.startsWith.choose_year": "(Choose year)", + "browse.startsWith.choose_year": "(Escolha o ano)", // "browse.startsWith.jump": "Jump to a point in the index:", - // TODO New key - Add a translation - "browse.startsWith.jump": "Jump to a point in the index:", + "browse.startsWith.jump": "Pular para um ponto do índice:", // "browse.startsWith.months.april": "April", - // TODO New key - Add a translation - "browse.startsWith.months.april": "April", + "browse.startsWith.months.april": "Abril", // "browse.startsWith.months.august": "August", - // TODO New key - Add a translation - "browse.startsWith.months.august": "August", + "browse.startsWith.months.august": "Agosto", // "browse.startsWith.months.december": "December", - // TODO New key - Add a translation - "browse.startsWith.months.december": "December", + "browse.startsWith.months.december": "Dezembro", // "browse.startsWith.months.february": "February", - // TODO New key - Add a translation - "browse.startsWith.months.february": "February", + "browse.startsWith.months.february": "Fevereiro", // "browse.startsWith.months.january": "January", - // TODO New key - Add a translation - "browse.startsWith.months.january": "January", + "browse.startsWith.months.january": "Janeiro", // "browse.startsWith.months.july": "July", - // TODO New key - Add a translation - "browse.startsWith.months.july": "July", + "browse.startsWith.months.july": "Julho", // "browse.startsWith.months.june": "June", - // TODO New key - Add a translation - "browse.startsWith.months.june": "June", + "browse.startsWith.months.june": "Junho", // "browse.startsWith.months.march": "March", - // TODO New key - Add a translation - "browse.startsWith.months.march": "March", + "browse.startsWith.months.march": "Março", // "browse.startsWith.months.may": "May", - // TODO New key - Add a translation - "browse.startsWith.months.may": "May", + "browse.startsWith.months.may": "Maio", // "browse.startsWith.months.none": "(Choose month)", - // TODO New key - Add a translation - "browse.startsWith.months.none": "(Choose month)", + "browse.startsWith.months.none": "(escolha o mês)", // "browse.startsWith.months.november": "November", - // TODO New key - Add a translation - "browse.startsWith.months.november": "November", + "browse.startsWith.months.november": "Novembro", // "browse.startsWith.months.october": "October", - // TODO New key - Add a translation - "browse.startsWith.months.october": "October", + "browse.startsWith.months.october": "Outubro", // "browse.startsWith.months.september": "September", - // TODO New key - Add a translation - "browse.startsWith.months.september": "September", + "browse.startsWith.months.september": "Setembro", // "browse.startsWith.submit": "Go", - // TODO New key - Add a translation - "browse.startsWith.submit": "Go", + "browse.startsWith.submit": "Ir", // "browse.startsWith.type_date": "Or type in a date (year-month):", - // TODO New key - Add a translation - "browse.startsWith.type_date": "Or type in a date (year-month):", + "browse.startsWith.type_date": "Ou informe uma data (ano-mês):", // "browse.startsWith.type_text": "Or enter first few letters:", - // TODO New key - Add a translation - "browse.startsWith.type_text": "Or enter first few letters:", + "browse.startsWith.type_text": "Ou informe as primeiras letras:", // "browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}", - // TODO New key - Add a translation - "browse.title": "Browsing {{ collection }} by {{ field }} {{ value }}", + "browse.title": "Navegando {{ collection }} por {{ field }} {{ value }}", // "chips.remove": "Remove chip", - // TODO New key - Add a translation - "chips.remove": "Remove chip", + "chips.remove": "Remover chip", // "collection.create.head": "Create a Collection", - // TODO New key - Add a translation - "collection.create.head": "Create a Collection", + "collection.create.head": "Criar uma coleção", // "collection.create.sub-head": "Create a Collection for Community {{ parent }}", - // TODO New key - Add a translation - "collection.create.sub-head": "Create a Collection for Community {{ parent }}", + "collection.create.sub-head": "Criar uma Coleção na Comunidade {{ parent }}", // "collection.delete.cancel": "Cancel", - // TODO New key - Add a translation - "collection.delete.cancel": "Cancel", + "collection.delete.cancel": "Cancelar", // "collection.delete.confirm": "Confirm", - // TODO New key - Add a translation - "collection.delete.confirm": "Confirm", + "collection.delete.confirm": "Confirmar", // "collection.delete.head": "Delete Collection", - // TODO New key - Add a translation - "collection.delete.head": "Delete Collection", + "collection.delete.head": "Apagar Coleção", // "collection.delete.notification.fail": "Collection could not be deleted", - // TODO New key - Add a translation - "collection.delete.notification.fail": "Collection could not be deleted", + "collection.delete.notification.fail": "Coleção não pôde ser apagada", // "collection.delete.notification.success": "Successfully deleted collection", - // TODO New key - Add a translation - "collection.delete.notification.success": "Successfully deleted collection", + "collection.delete.notification.success": "Apagou a coleção com sucesso", // "collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"", - // TODO New key - Add a translation - "collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"", + "collection.delete.text": "Você tem certeza que deseja apagar a coleção \"{{ dso }}?\"", // "collection.edit.delete": "Delete this collection", - // TODO New key - Add a translation - "collection.edit.delete": "Delete this collection", + "collection.edit.delete": "Apagar esta coleção", // "collection.edit.head": "Edit Collection", - // TODO New key - Add a translation - "collection.edit.head": "Edit Collection", + "collection.edit.head": "Editar Coleção", // "collection.edit.item-mapper.cancel": "Cancel", - // TODO New key - Add a translation - "collection.edit.item-mapper.cancel": "Cancel", + "collection.edit.item-mapper.cancel": "Cancelar", // "collection.edit.item-mapper.collection": "Collection: \"{{name}}\"", - // TODO New key - Add a translation - "collection.edit.item-mapper.collection": "Collection: \"{{name}}\"", + "collection.edit.item-mapper.collection": "Coleção: \"{{name}}\"", // "collection.edit.item-mapper.confirm": "Map selected items", - // TODO New key - Add a translation - "collection.edit.item-mapper.confirm": "Map selected items", + "collection.edit.item-mapper.confirm": "Mapear itens selecionados", // "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.", - // TODO New key - Add a translation - "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.description": "Esta é a ferramenta de mapeação de itens que permite administradores de coleções a mapear itens de outras coleções nesta. VoCẽ pode busca-los em outras coleções para mapeá-los, ou navegar na lista dos itens atualmente mapeados.", // "collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections", - // TODO New key - Add a translation - "collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections", + "collection.edit.item-mapper.head": "Mapeador de Itens - Mapear itens em Outras Coleções", // "collection.edit.item-mapper.no-search": "Please enter a query to search", - // TODO New key - Add a translation - "collection.edit.item-mapper.no-search": "Please enter a query to search", + "collection.edit.item-mapper.no-search": "Por favor informe uma consulta para buscar", // "collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.", + "collection.edit.item-mapper.notifications.map.error.content": "Ocorreu erros ao mapear {{amount}} itens.", // "collection.edit.item-mapper.notifications.map.error.head": "Mapping errors", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.map.error.head": "Mapping errors", + "collection.edit.item-mapper.notifications.map.error.head": "Erros de mapeamento", // "collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.", + "collection.edit.item-mapper.notifications.map.success.content": "Mapeou {{amount}} itens com sucesso.", // "collection.edit.item-mapper.notifications.map.success.head": "Mapping completed", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.map.success.head": "Mapping completed", + "collection.edit.item-mapper.notifications.map.success.head": "Mapeamento completo", // "collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.", + "collection.edit.item-mapper.notifications.unmap.error.content": "Ocorreram erros ao tentar remover os mapeamentos de {{amount}} item(ns).", // "collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors", + "collection.edit.item-mapper.notifications.unmap.error.head": "Erros de remoção de mapeamento", // "collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.", + "collection.edit.item-mapper.notifications.unmap.success.content": "Removeu os mapeamentps de {{amount}} item(ns) com sucesso.", // "collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed", - // TODO New key - Add a translation - "collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed", + "collection.edit.item-mapper.notifications.unmap.success.head": "Remoção de mapeamentos completa", // "collection.edit.item-mapper.remove": "Remove selected item mappings", - // TODO New key - Add a translation - "collection.edit.item-mapper.remove": "Remove selected item mappings", + "collection.edit.item-mapper.remove": "Remover mapeamentos selecionados", // "collection.edit.item-mapper.tabs.browse": "Browse mapped items", - // TODO New key - Add a translation - "collection.edit.item-mapper.tabs.browse": "Browse mapped items", + "collection.edit.item-mapper.tabs.browse": "Navegar por itens mapeados", // "collection.edit.item-mapper.tabs.map": "Map new items", - // TODO New key - Add a translation - "collection.edit.item-mapper.tabs.map": "Map new items", + "collection.edit.item-mapper.tabs.map": "Mapear novos itens", // "collection.form.abstract": "Short Description", - // TODO New key - Add a translation - "collection.form.abstract": "Short Description", + "collection.form.abstract": "Descrição curta", // "collection.form.description": "Introductory text (HTML)", - // TODO New key - Add a translation - "collection.form.description": "Introductory text (HTML)", + "collection.form.description": "Texto introdutório (HTML)", // "collection.form.errors.title.required": "Please enter a collection name", - // TODO New key - Add a translation - "collection.form.errors.title.required": "Please enter a collection name", + "collection.form.errors.title.required": "Por favor informe um nome de coleção", // "collection.form.license": "License", - // TODO New key - Add a translation - "collection.form.license": "License", + "collection.form.license": "Licença", // "collection.form.provenance": "Provenance", - // TODO New key - Add a translation - "collection.form.provenance": "Provenance", + "collection.form.provenance": "Proveniência", // "collection.form.rights": "Copyright text (HTML)", - // TODO New key - Add a translation - "collection.form.rights": "Copyright text (HTML)", + "collection.form.rights": "Texto de direito de cópia (HTML)", // "collection.form.tableofcontents": "News (HTML)", - // TODO New key - Add a translation - "collection.form.tableofcontents": "News (HTML)", + "collection.form.tableofcontents": "Notícias (HTML)", // "collection.form.title": "Name", - // TODO New key - Add a translation - "collection.form.title": "Name", + "collection.form.title": "Nome", // "collection.page.browse.recent.head": "Recent Submissions", - // TODO New key - Add a translation - "collection.page.browse.recent.head": "Recent Submissions", + "collection.page.browse.recent.head": "Submissões Recentes", // "collection.page.browse.recent.empty": "No items to show", - // TODO New key - Add a translation - "collection.page.browse.recent.empty": "No items to show", + "collection.page.browse.recent.empty": "Nenhum item a exibir", // "collection.page.handle": "Permanent URI for this collection", - // TODO New key - Add a translation - "collection.page.handle": "Permanent URI for this collection", + "collection.page.handle": "URI Permanente para esta coleção", // "collection.page.license": "License", - // TODO New key - Add a translation - "collection.page.license": "License", + "collection.page.license": "Licença", // "collection.page.news": "News", - // TODO New key - Add a translation - "collection.page.news": "News", + "collection.page.news": "Notícias", // "collection.select.confirm": "Confirm selected", - // TODO New key - Add a translation - "collection.select.confirm": "Confirm selected", + "collection.select.confirm": "Confirmar seleção", // "collection.select.empty": "No collections to show", - // TODO New key - Add a translation - "collection.select.empty": "No collections to show", + "collection.select.empty": "Nenhuma coleção a mostrar", // "collection.select.table.title": "Title", - // TODO New key - Add a translation - "collection.select.table.title": "Title", + "collection.select.table.title": "Título", // "community.create.head": "Create a Community", - // TODO New key - Add a translation - "community.create.head": "Create a Community", + "community.create.head": "Criar uma comunidade", // "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", - // TODO New key - Add a translation - "community.create.sub-head": "Create a Sub-Community for Community {{ parent }}", + "community.create.sub-head": "Criar uma Sub-Comunidade para Comunidade {{ parent }}", // "community.delete.cancel": "Cancel", - // TODO New key - Add a translation - "community.delete.cancel": "Cancel", + "community.delete.cancel": "Cancelar", // "community.delete.confirm": "Confirm", - // TODO New key - Add a translation - "community.delete.confirm": "Confirm", + "community.delete.confirm": "Confirmar", // "community.delete.head": "Delete Community", - // TODO New key - Add a translation - "community.delete.head": "Delete Community", + "community.delete.head": "Apagar Comunidade", // "community.delete.notification.fail": "Community could not be deleted", - // TODO New key - Add a translation - "community.delete.notification.fail": "Community could not be deleted", + "community.delete.notification.fail": "Comunidade não pôde ser apagada", // "community.delete.notification.success": "Successfully deleted community", - // TODO New key - Add a translation - "community.delete.notification.success": "Successfully deleted community", + "community.delete.notification.success": "Comunidade apagada com sucesso", // "community.delete.text": "Are you sure you want to delete community \"{{ dso }}\"", - // TODO New key - Add a translation - "community.delete.text": "Are you sure you want to delete community \"{{ dso }}\"", + "community.delete.text": "Você tem certeza que quer apagar a comunidade \"{{ dso }}\"?", // "community.edit.delete": "Delete this community", - // TODO New key - Add a translation - "community.edit.delete": "Delete this community", + "community.edit.delete": "Apagar esta comunidade", // "community.edit.head": "Edit Community", - // TODO New key - Add a translation - "community.edit.head": "Edit Community", + "community.edit.head": "Editar Comunidade", // "community.form.abstract": "Short Description", - // TODO New key - Add a translation - "community.form.abstract": "Short Description", + "community.form.abstract": "Descrição curta", // "community.form.description": "Introductory text (HTML)", - // TODO New key - Add a translation - "community.form.description": "Introductory text (HTML)", + "community.form.description": "Texto introdutório (HTML)", // "community.form.errors.title.required": "Please enter a community name", - // TODO New key - Add a translation - "community.form.errors.title.required": "Please enter a community name", + "community.form.errors.title.required": "Por favor informe um nome para comunidade", // "community.form.rights": "Copyright text (HTML)", - // TODO New key - Add a translation - "community.form.rights": "Copyright text (HTML)", + "community.form.rights": "Texto de direito de cópia (HTML)", // "community.form.tableofcontents": "News (HTML)", - // TODO New key - Add a translation - "community.form.tableofcontents": "News (HTML)", + "community.form.tableofcontents": "Notícias (HTML)", // "community.form.title": "Name", - // TODO New key - Add a translation - "community.form.title": "Name", + "community.form.title": "Nome", // "community.page.handle": "Permanent URI for this community", - // TODO New key - Add a translation - "community.page.handle": "Permanent URI for this community", + "community.page.handle": "URI Permanente desta comunidade", // "community.page.license": "License", - // TODO New key - Add a translation - "community.page.license": "License", + "community.page.license": "Licença", // "community.page.news": "News", - // TODO New key - Add a translation - "community.page.news": "News", + "community.page.news": "Notícias", // "community.all-lists.head": "Subcommunities and Collections", - // TODO New key - Add a translation - "community.all-lists.head": "Subcommunities and Collections", + "community.all-lists.head": "Sub-Comunidade e Coleções", // "community.sub-collection-list.head": "Collections of this Community", - // TODO New key - Add a translation - "community.sub-collection-list.head": "Collections of this Community", + "community.sub-collection-list.head": "Coleções desta Comunidade", // "community.sub-community-list.head": "Communities of this Community", - // TODO New key - Add a translation - "community.sub-community-list.head": "Communities of this Community", + "community.sub-community-list.head": "Comunidades desta Comunidade", // "dso-selector.create.collection.head": "New collection", - // TODO New key - Add a translation - "dso-selector.create.collection.head": "New collection", + "dso-selector.create.collection.head": "Nova coleção", // "dso-selector.create.community.head": "New community", - // TODO New key - Add a translation - "dso-selector.create.community.head": "New community", + "dso-selector.create.community.head": "Nova comunidade", // "dso-selector.create.community.sub-level": "Create a new community in", - // TODO New key - Add a translation - "dso-selector.create.community.sub-level": "Create a new community in", + "dso-selector.create.community.sub-level": "Criar uma nova coleção em", // "dso-selector.create.community.top-level": "Create a new top-level community", - // TODO New key - Add a translation - "dso-selector.create.community.top-level": "Create a new top-level community", + "dso-selector.create.community.top-level": "Criar uma nova comunidade no nível superior", // "dso-selector.create.item.head": "New item", - // TODO New key - Add a translation - "dso-selector.create.item.head": "New item", + "dso-selector.create.item.head": "Novo item", // "dso-selector.edit.collection.head": "Edit collection", - // TODO New key - Add a translation - "dso-selector.edit.collection.head": "Edit collection", + "dso-selector.edit.collection.head": "Editar coleção", // "dso-selector.edit.community.head": "Edit community", - // TODO New key - Add a translation - "dso-selector.edit.community.head": "Edit community", + "dso-selector.edit.community.head": "Editar comunidade", // "dso-selector.edit.item.head": "Edit item", - // TODO New key - Add a translation - "dso-selector.edit.item.head": "Edit item", + "dso-selector.edit.item.head": "Editar item", // "dso-selector.no-results": "No {{ type }} found", - // TODO New key - Add a translation - "dso-selector.no-results": "No {{ type }} found", + "dso-selector.no-results": "Nenhum(a) {{ type }} encontrado(a)", // "dso-selector.placeholder": "Search for a {{ type }}", - // TODO New key - Add a translation - "dso-selector.placeholder": "Search for a {{ type }}", + "dso-selector.placeholder": "Buscar por um(a) {{ type }}", // "error.browse-by": "Error fetching items", - // TODO New key - Add a translation - "error.browse-by": "Error fetching items", + "error.browse-by": "Erro ao carregar itens", // "error.collection": "Error fetching collection", - // TODO New key - Add a translation - "error.collection": "Error fetching collection", + "error.collection": "Erro ao carregar coleção", // "error.collections": "Error fetching collections", - // TODO New key - Add a translation - "error.collections": "Error fetching collections", + "error.collections": "Erro ao carregar coleções", // "error.community": "Error fetching community", - // TODO New key - Add a translation - "error.community": "Error fetching community", + "error.community": "Erro ao carregar comunidade", // "error.identifier": "No item found for the identifier", - // TODO New key - Add a translation - "error.identifier": "No item found for the identifier", + "error.identifier": "Nenhum item encontrado para o identificador", // "error.default": "Error", - // TODO New key - Add a translation - "error.default": "Error", + "error.default": "Erro", // "error.item": "Error fetching item", - // TODO New key - Add a translation - "error.item": "Error fetching item", + "error.item": "Erro ao carregar item", // "error.items": "Error fetching items", - // TODO New key - Add a translation - "error.items": "Error fetching items", + "error.items": "Erro ao carregar itens", // "error.objects": "Error fetching objects", - // TODO New key - Add a translation - "error.objects": "Error fetching objects", + "error.objects": "Erro ao carregar objetos", // "error.recent-submissions": "Error fetching recent submissions", - // TODO New key - Add a translation - "error.recent-submissions": "Error fetching recent submissions", + "error.recent-submissions": "Erro ao carregar as submissões recentes", // "error.search-results": "Error fetching search results", - // TODO New key - Add a translation - "error.search-results": "Error fetching search results", + "error.search-results": "Erro ao carregar os resultados de busca", // "error.sub-collections": "Error fetching sub-collections", - // TODO New key - Add a translation - "error.sub-collections": "Error fetching sub-collections", + "error.sub-collections": "Erro ao carregar sub-coleções", // "error.sub-communities": "Error fetching sub-communities", - // TODO New key - Add a translation - "error.sub-communities": "Error fetching sub-communities", + "error.sub-communities": "Erro ao carregar sub-comunidade", // "error.submission.sections.init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below :

", - // TODO New key - Add a translation - "error.submission.sections.init-form-error": "An error occurred during section initialize, please check your input-form configuration. Details are below :

", + "error.submission.sections.init-form-error": "Ocorreu um erro durante a seção de inicialização, por favor verifique sua configuração de input-form. Detalhes estão abaixo :

", // "error.top-level-communities": "Error fetching top-level communities", - // TODO New key - Add a translation - "error.top-level-communities": "Error fetching top-level communities", + "error.top-level-communities": "Erro ao carregar as comunidade de nível superior", // "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", - // TODO New key - Add a translation - "error.validation.license.notgranted": "You must grant this license to complete your submission. If you are unable to grant this license at this time you may save your work and return later or remove the submission.", + "error.validation.license.notgranted": "Você deve concordar com esta licença para completar sua submissão. Se vocẽ não estiver de acordo com esta licença neste momento você pode salvar seu trabalho para continuar depois ou remover a submissão.", // "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", - // TODO New key - Add a translation - "error.validation.pattern": "This input is restricted by the current pattern: {{ pattern }}.", + "error.validation.pattern": "Este campo está restrito ao seguinte padrão: {{ pattern }}.", // "footer.copyright": "copyright © 2002-{{ year }}", - // TODO New key - Add a translation "footer.copyright": "copyright © 2002-{{ year }}", // "footer.link.dspace": "DSpace software", - // TODO New key - Add a translation "footer.link.dspace": "DSpace software", // "footer.link.duraspace": "DuraSpace", - // TODO New key - Add a translation "footer.link.duraspace": "DuraSpace", // "form.cancel": "Cancel", - // TODO New key - Add a translation - "form.cancel": "Cancel", + "form.cancel": "Cancelar", // "form.clear": "Clear", - // TODO New key - Add a translation - "form.clear": "Clear", + "form.clear": "Limpar", // "form.clear-help": "Click here to remove the selected value", - // TODO New key - Add a translation - "form.clear-help": "Click here to remove the selected value", + "form.clear-help": "Clique aqui para apagar o valor selecionado", // "form.edit": "Edit", - // TODO New key - Add a translation - "form.edit": "Edit", + "form.edit": "Editar", // "form.edit-help": "Click here to edit the selected value", - // TODO New key - Add a translation - "form.edit-help": "Click here to edit the selected value", + "form.edit-help": "Clique aqui para editar o valor selecionado", // "form.first-name": "First name", - // TODO New key - Add a translation - "form.first-name": "First name", + "form.first-name": "Primeiro nome", // "form.group-collapse": "Collapse", - // TODO New key - Add a translation - "form.group-collapse": "Collapse", + "form.group-collapse": "Esconder", // "form.group-collapse-help": "Click here to collapse", - // TODO New key - Add a translation - "form.group-collapse-help": "Click here to collapse", + "form.group-collapse-help": "Clique aqui para esconder", // "form.group-expand": "Expand", - // TODO New key - Add a translation - "form.group-expand": "Expand", + "form.group-expand": "Expandir", // "form.group-expand-help": "Click here to expand and add more elements", - // TODO New key - Add a translation - "form.group-expand-help": "Click here to expand and add more elements", + "form.group-expand-help": "Clique aqui para expandir e adicionar mais elementos", // "form.last-name": "Last name", - // TODO New key - Add a translation - "form.last-name": "Last name", + "form.last-name": "Último nome", // "form.loading": "Loading...", - // TODO New key - Add a translation - "form.loading": "Loading...", + "form.loading": "Carregando...", // "form.no-results": "No results found", - // TODO New key - Add a translation - "form.no-results": "No results found", + "form.no-results": "Nenhum resultado encontrado", // "form.no-value": "No value entered", - // TODO New key - Add a translation - "form.no-value": "No value entered", + "form.no-value": "Nenhum valor informado", // "form.other-information": {}, - // TODO New key - Add a translation "form.other-information": {}, // "form.remove": "Remove", - // TODO New key - Add a translation - "form.remove": "Remove", + "form.remove": "Apagar", // "form.save": "Save", - // TODO New key - Add a translation - "form.save": "Save", + "form.save": "Salvar", // "form.save-help": "Save changes", - // TODO New key - Add a translation - "form.save-help": "Save changes", + "form.save-help": "Salvar alterações", // "form.search": "Search", - // TODO New key - Add a translation - "form.search": "Search", + "form.search": "Buscar", // "form.search-help": "Click here to looking for an existing correspondence", - // TODO New key - Add a translation - "form.search-help": "Click here to looking for an existing correspondence", + "form.search-help": "Clique aqui para procurar por uma correspondência existente", // "form.submit": "Submit", - // TODO New key - Add a translation - "form.submit": "Submit", + "form.submit": "Submeter", @@ -961,2258 +731,1725 @@ "home.description": "", // "home.title": "DSpace Angular :: Home", - // TODO New key - Add a translation - "home.title": "DSpace Angular :: Home", + "home.title": "DSpace Angular :: Início", // "home.top-level-communities.head": "Communities in DSpace", - // TODO New key - Add a translation - "home.top-level-communities.head": "Communities in DSpace", + "home.top-level-communities.head": "Comunidades no DSpace", // "home.top-level-communities.help": "Select a community to browse its collections.", - // TODO New key - Add a translation - "home.top-level-communities.help": "Select a community to browse its collections.", + "home.top-level-communities.help": "Selecione uma comunidade para navegar por suas coleções", // "item.edit.delete.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.delete.cancel": "Cancel", + "item.edit.delete.cancel": "Cancelar", // "item.edit.delete.confirm": "Delete", - // TODO New key - Add a translation - "item.edit.delete.confirm": "Delete", + "item.edit.delete.confirm": "Apagar", // "item.edit.delete.description": "Are you sure this item should be completely deleted? Caution: At present, no tombstone would be left.", - // TODO New key - Add a translation - "item.edit.delete.description": "Are you sure this item should be completely deleted? Caution: At present, no tombstone would be left.", + "item.edit.delete.description": "Você tem certeza que deseja apagar completamento este item? Atenção: No momento, nenhum vestígio restará.", // "item.edit.delete.error": "An error occurred while deleting the item", - // TODO New key - Add a translation - "item.edit.delete.error": "An error occurred while deleting the item", + "item.edit.delete.error": "Ocorreu um erro ao apagar o item", // "item.edit.delete.header": "Delete item: {{ id }}", - // TODO New key - Add a translation - "item.edit.delete.header": "Delete item: {{ id }}", + "item.edit.delete.header": "Apagar item: {{ id }}", // "item.edit.delete.success": "The item has been deleted", - // TODO New key - Add a translation - "item.edit.delete.success": "The item has been deleted", + "item.edit.delete.success": "O item foi apagado", // "item.edit.head": "Edit Item", - // TODO New key - Add a translation - "item.edit.head": "Edit Item", + "item.edit.head": "Editar Item", // "item.edit.item-mapper.buttons.add": "Map item to selected collections", - // TODO New key - Add a translation - "item.edit.item-mapper.buttons.add": "Map item to selected collections", + "item.edit.item-mapper.buttons.add": "Mapear item na(s) coleção(ões) seleciona(s)", // "item.edit.item-mapper.buttons.remove": "Remove item's mapping for selected collections", - // TODO New key - Add a translation - "item.edit.item-mapper.buttons.remove": "Remove item's mapping for selected collections", + "item.edit.item-mapper.buttons.remove": "Remover mapeamento(s) do item da(s) coleção(ões) seleciona(s)", // "item.edit.item-mapper.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.item-mapper.cancel": "Cancel", + "item.edit.item-mapper.cancel": "Cancelar", // "item.edit.item-mapper.description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", - // TODO New key - Add a translation - "item.edit.item-mapper.description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.", + "item.edit.item-mapper.description": "Essa é a ferramenta de mapeamento de itens que permite que os administradores mapeiem esse item para outras coleções. Você pode procurar coleções e mapeá-las ou navegar na lista de coleções para as quais o item está atualmente mapeado.", // "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", - // TODO New key - Add a translation - "item.edit.item-mapper.head": "Item Mapper - Map Item to Collections", + "item.edit.item-mapper.head": "Mapeador de Item - Mapear Itens em Coleções", // "item.edit.item-mapper.item": "Item: \"{{name}}\"", - // TODO New key - Add a translation "item.edit.item-mapper.item": "Item: \"{{name}}\"", // "item.edit.item-mapper.no-search": "Please enter a query to search", - // TODO New key - Add a translation - "item.edit.item-mapper.no-search": "Please enter a query to search", + "item.edit.item-mapper.no-search": "Por favor informe uma consulta para buscar", // "item.edit.item-mapper.notifications.add.error.content": "Errors occurred for mapping of item to {{amount}} collections.", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.add.error.content": "Errors occurred for mapping of item to {{amount}} collections.", + "item.edit.item-mapper.notifications.add.error.content": "Ocorreram erros ao mapear o item em {{amount}} coleções.", // "item.edit.item-mapper.notifications.add.error.head": "Mapping errors", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.add.error.head": "Mapping errors", + "item.edit.item-mapper.notifications.add.error.head": "Erros de mapeamento", // "item.edit.item-mapper.notifications.add.success.content": "Successfully mapped item to {{amount}} collections.", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.add.success.content": "Successfully mapped item to {{amount}} collections.", + "item.edit.item-mapper.notifications.add.success.content": "Mapeou o item em {{amount}} coleções com sucesso.", // "item.edit.item-mapper.notifications.add.success.head": "Mapping completed", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.add.success.head": "Mapping completed", + "item.edit.item-mapper.notifications.add.success.head": "Mapeamento complesto", // "item.edit.item-mapper.notifications.remove.error.content": "Errors occurred for the removal of the mapping to {{amount}} collections.", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.remove.error.content": "Errors occurred for the removal of the mapping to {{amount}} collections.", + "item.edit.item-mapper.notifications.remove.error.content": "Ocorreram erros ao remover mapeamento do item em {{amount}} coleções.", // "item.edit.item-mapper.notifications.remove.error.head": "Removal of mapping errors", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.remove.error.head": "Removal of mapping errors", + "item.edit.item-mapper.notifications.remove.error.head": "Erros de remoção de mapeamento", // "item.edit.item-mapper.notifications.remove.success.content": "Successfully removed mapping of item to {{amount}} collections.", - // TODO New key - Add a translation "item.edit.item-mapper.notifications.remove.success.content": "Successfully removed mapping of item to {{amount}} collections.", // "item.edit.item-mapper.notifications.remove.success.head": "Removal of mapping completed", - // TODO New key - Add a translation - "item.edit.item-mapper.notifications.remove.success.head": "Removal of mapping completed", + "item.edit.item-mapper.notifications.remove.success.head": "Completou a remoção de mapeamento", // "item.edit.item-mapper.tabs.browse": "Browse mapped collections", - // TODO New key - Add a translation - "item.edit.item-mapper.tabs.browse": "Browse mapped collections", + "item.edit.item-mapper.tabs.browse": "Navegar nas coleções mapeadas", // "item.edit.item-mapper.tabs.map": "Map new collections", - // TODO New key - Add a translation - "item.edit.item-mapper.tabs.map": "Map new collections", + "item.edit.item-mapper.tabs.map": "Mapear novas coleções", // "item.edit.metadata.add-button": "Add", - // TODO New key - Add a translation - "item.edit.metadata.add-button": "Add", + "item.edit.metadata.add-button": "Adicionar", // "item.edit.metadata.discard-button": "Discard", - // TODO New key - Add a translation - "item.edit.metadata.discard-button": "Discard", + "item.edit.metadata.discard-button": "Descartar", // "item.edit.metadata.edit.buttons.edit": "Edit", - // TODO New key - Add a translation - "item.edit.metadata.edit.buttons.edit": "Edit", + "item.edit.metadata.edit.buttons.edit": "Editar", // "item.edit.metadata.edit.buttons.remove": "Remove", - // TODO New key - Add a translation - "item.edit.metadata.edit.buttons.remove": "Remove", + "item.edit.metadata.edit.buttons.remove": "Apagar", // "item.edit.metadata.edit.buttons.undo": "Undo changes", - // TODO New key - Add a translation - "item.edit.metadata.edit.buttons.undo": "Undo changes", + "item.edit.metadata.edit.buttons.undo": "Desfazer alterações", // "item.edit.metadata.edit.buttons.unedit": "Stop editing", - // TODO New key - Add a translation - "item.edit.metadata.edit.buttons.unedit": "Stop editing", + "item.edit.metadata.edit.buttons.unedit": "Parar edição", // "item.edit.metadata.headers.edit": "Edit", - // TODO New key - Add a translation - "item.edit.metadata.headers.edit": "Edit", + "item.edit.metadata.headers.edit": "Editar", // "item.edit.metadata.headers.field": "Field", - // TODO New key - Add a translation - "item.edit.metadata.headers.field": "Field", + "item.edit.metadata.headers.field": "Campo", // "item.edit.metadata.headers.language": "Lang", - // TODO New key - Add a translation - "item.edit.metadata.headers.language": "Lang", + "item.edit.metadata.headers.language": "Idioma", // "item.edit.metadata.headers.value": "Value", - // TODO New key - Add a translation - "item.edit.metadata.headers.value": "Value", + "item.edit.metadata.headers.value": "Valor", // "item.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field", - // TODO New key - Add a translation - "item.edit.metadata.metadatafield.invalid": "Please choose a valid metadata field", + "item.edit.metadata.metadatafield.invalid": "Por favor escolha um campo de metadados válido", // "item.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - // TODO New key - Add a translation - "item.edit.metadata.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", + "item.edit.metadata.notifications.discarded.content": "Suas alterações foram descartadas. Para restabelecer suas alterações, clique no botão 'Desfazer'", // "item.edit.metadata.notifications.discarded.title": "Changed discarded", - // TODO New key - Add a translation - "item.edit.metadata.notifications.discarded.title": "Changed discarded", + "item.edit.metadata.notifications.discarded.title": "Mudança descartada", // "item.edit.metadata.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", - // TODO New key - Add a translation - "item.edit.metadata.notifications.invalid.content": "Your changes were not saved. Please make sure all fields are valid before you save.", + "item.edit.metadata.notifications.invalid.content": "Suas alterações não foram salvas. Verifique se todos os campos são válidos antes de salvar.", // "item.edit.metadata.notifications.invalid.title": "Metadata invalid", - // TODO New key - Add a translation - "item.edit.metadata.notifications.invalid.title": "Metadata invalid", + "item.edit.metadata.notifications.invalid.title": "Metadado inválido", // "item.edit.metadata.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", - // TODO New key - Add a translation - "item.edit.metadata.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", + "item.edit.metadata.notifications.outdated.content": "O item em que você está trabalhando foi alterado por outro usuário. Suas alterações atuais são descartadas para evitar conflitos", // "item.edit.metadata.notifications.outdated.title": "Changed outdated", - // TODO New key - Add a translation - "item.edit.metadata.notifications.outdated.title": "Changed outdated", + "item.edit.metadata.notifications.outdated.title": "Alteração desatualizada", // "item.edit.metadata.notifications.saved.content": "Your changes to this item's metadata were saved.", - // TODO New key - Add a translation - "item.edit.metadata.notifications.saved.content": "Your changes to this item's metadata were saved.", + "item.edit.metadata.notifications.saved.content": "Suas alterações nos metadados deste item foram salvas.", // "item.edit.metadata.notifications.saved.title": "Metadata saved", - // TODO New key - Add a translation - "item.edit.metadata.notifications.saved.title": "Metadata saved", + "item.edit.metadata.notifications.saved.title": "Metadados salvos", // "item.edit.metadata.reinstate-button": "Undo", - // TODO New key - Add a translation - "item.edit.metadata.reinstate-button": "Undo", + "item.edit.metadata.reinstate-button": "Desfazer", // "item.edit.metadata.save-button": "Save", - // TODO New key - Add a translation - "item.edit.metadata.save-button": "Save", + "item.edit.metadata.save-button": "Salvar", // "item.edit.modify.overview.field": "Field", - // TODO New key - Add a translation - "item.edit.modify.overview.field": "Field", + "item.edit.modify.overview.field": "Campo", // "item.edit.modify.overview.language": "Language", - // TODO New key - Add a translation - "item.edit.modify.overview.language": "Language", + "item.edit.modify.overview.language": "Idioma", // "item.edit.modify.overview.value": "Value", - // TODO New key - Add a translation - "item.edit.modify.overview.value": "Value", + "item.edit.modify.overview.value": "Valor", // "item.edit.move.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.move.cancel": "Cancel", + "item.edit.move.cancel": "Cancelar", // "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", - // TODO New key - Add a translation - "item.edit.move.description": "Select the collection you wish to move this item to. To narrow down the list of displayed collections, you can enter a search query in the box.", + "item.edit.move.description": "Selecione a coleção para a qual você deseja mover este item. Para restringir a lista de coleções exibidas, você pode inserir uma consulta de pesquisa na caixa.", - // "item.edit.move.error": "An error occured when attempting to move the item", - // TODO New key - Add a translation - "item.edit.move.error": "An error occured when attempting to move the item", + // "item.edit.move.error": "An error occurred when attempting to move the item", + "item.edit.move.error": "Ocorreu um erro ao tentar mover o item", // "item.edit.move.head": "Move item: {{id}}", - // TODO New key - Add a translation - "item.edit.move.head": "Move item: {{id}}", + "item.edit.move.head": "Mover item: {{id}}", // "item.edit.move.inheritpolicies.checkbox": "Inherit policies", - // TODO New key - Add a translation - "item.edit.move.inheritpolicies.checkbox": "Inherit policies", + "item.edit.move.inheritpolicies.checkbox": "Herdar politicas", // "item.edit.move.inheritpolicies.description": "Inherit the default policies of the destination collection", - // TODO New key - Add a translation - "item.edit.move.inheritpolicies.description": "Inherit the default policies of the destination collection", + "item.edit.move.inheritpolicies.description": "Herdar as politicas padrões da coleção destino", // "item.edit.move.move": "Move", - // TODO New key - Add a translation - "item.edit.move.move": "Move", + "item.edit.move.move": "Mover", // "item.edit.move.processing": "Moving...", - // TODO New key - Add a translation - "item.edit.move.processing": "Moving...", + "item.edit.move.processing": "Movendo...", // "item.edit.move.search.placeholder": "Enter a search query to look for collections", - // TODO New key - Add a translation - "item.edit.move.search.placeholder": "Enter a search query to look for collections", + "item.edit.move.search.placeholder": "nsira uma consulta para procurar coleções", - // "item.edit.move.success": "The item has been moved succesfully", - // TODO New key - Add a translation - "item.edit.move.success": "The item has been moved succesfully", + // "item.edit.move.success": "The item has been moved successfully", + "item.edit.move.success": "O item foi movido com sucesso", // "item.edit.move.title": "Move item", - // TODO New key - Add a translation - "item.edit.move.title": "Move item", + "item.edit.move.title": "Mover item", // "item.edit.private.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.private.cancel": "Cancel", + "item.edit.private.cancel": "Cancelar", // "item.edit.private.confirm": "Make it Private", - // TODO New key - Add a translation - "item.edit.private.confirm": "Make it Private", + "item.edit.private.confirm": "Tornar Privado", // "item.edit.private.description": "Are you sure this item should be made private in the archive?", - // TODO New key - Add a translation - "item.edit.private.description": "Are you sure this item should be made private in the archive?", + "item.edit.private.description": "Tem certeza de que este item deve ser tornado privado no arquivo?", // "item.edit.private.error": "An error occurred while making the item private", - // TODO New key - Add a translation - "item.edit.private.error": "An error occurred while making the item private", + "item.edit.private.error": "Ocorreu um erro ao tornar o item privado", // "item.edit.private.header": "Make item private: {{ id }}", - // TODO New key - Add a translation - "item.edit.private.header": "Make item private: {{ id }}", + "item.edit.private.header": "Tornar privado o item: {{ id }}", // "item.edit.private.success": "The item is now private", - // TODO New key - Add a translation - "item.edit.private.success": "The item is now private", + "item.edit.private.success": "O item agora é privado", // "item.edit.public.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.public.cancel": "Cancel", + "item.edit.public.cancel": "Cancelar", // "item.edit.public.confirm": "Make it Public", - // TODO New key - Add a translation - "item.edit.public.confirm": "Make it Public", + "item.edit.public.confirm": "Tornar público", // "item.edit.public.description": "Are you sure this item should be made public in the archive?", - // TODO New key - Add a translation - "item.edit.public.description": "Are you sure this item should be made public in the archive?", + "item.edit.public.description": "Você tem certeza que deseja tornar este item público no arquivo?", // "item.edit.public.error": "An error occurred while making the item public", - // TODO New key - Add a translation - "item.edit.public.error": "An error occurred while making the item public", + "item.edit.public.error": "Ocorreu um erro ao tornar o item público", // "item.edit.public.header": "Make item public: {{ id }}", - // TODO New key - Add a translation - "item.edit.public.header": "Make item public: {{ id }}", + "item.edit.public.header": "Tornar público o item: {{ id }}", // "item.edit.public.success": "The item is now public", - // TODO New key - Add a translation - "item.edit.public.success": "The item is now public", + "item.edit.public.success": "O item agora é público", // "item.edit.reinstate.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.reinstate.cancel": "Cancel", + "item.edit.reinstate.cancel": "Cancelar", // "item.edit.reinstate.confirm": "Reinstate", - // TODO New key - Add a translation - "item.edit.reinstate.confirm": "Reinstate", + "item.edit.reinstate.confirm": "Restabelecer", // "item.edit.reinstate.description": "Are you sure this item should be reinstated to the archive?", - // TODO New key - Add a translation - "item.edit.reinstate.description": "Are you sure this item should be reinstated to the archive?", + "item.edit.reinstate.description": "Tem certeza de que este item deve ser restabelecido no arquivo?", // "item.edit.reinstate.error": "An error occurred while reinstating the item", - // TODO New key - Add a translation - "item.edit.reinstate.error": "An error occurred while reinstating the item", + "item.edit.reinstate.error": "Ocorreu um erro ao restabelecer o item", // "item.edit.reinstate.header": "Reinstate item: {{ id }}", - // TODO New key - Add a translation - "item.edit.reinstate.header": "Reinstate item: {{ id }}", + "item.edit.reinstate.header": "Restabelecer item: {{ id }}", // "item.edit.reinstate.success": "The item was reinstated successfully", - // TODO New key - Add a translation - "item.edit.reinstate.success": "The item was reinstated successfully", + "item.edit.reinstate.success": "O item foi restabelecido com sucesso", // "item.edit.relationships.discard-button": "Discard", - // TODO New key - Add a translation - "item.edit.relationships.discard-button": "Discard", + "item.edit.relationships.discard-button": "Descartar", // "item.edit.relationships.edit.buttons.remove": "Remove", - // TODO New key - Add a translation - "item.edit.relationships.edit.buttons.remove": "Remove", + "item.edit.relationships.edit.buttons.remove": "Apagar", // "item.edit.relationships.edit.buttons.undo": "Undo changes", - // TODO New key - Add a translation - "item.edit.relationships.edit.buttons.undo": "Undo changes", + "item.edit.relationships.edit.buttons.undo": "Desfazer alterações", // "item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", - // TODO New key - Add a translation - "item.edit.relationships.notifications.discarded.content": "Your changes were discarded. To reinstate your changes click the 'Undo' button", + "item.edit.relationships.notifications.discarded.content": "Suas alterações foram descartadas. Para restabelecer suas alterações, clique no botão 'Desfazer'", // "item.edit.relationships.notifications.discarded.title": "Changes discarded", - // TODO New key - Add a translation - "item.edit.relationships.notifications.discarded.title": "Changes discarded", + "item.edit.relationships.notifications.discarded.title": "Alterações descartadas", // "item.edit.relationships.notifications.failed.title": "Error deleting relationship", - // TODO New key - Add a translation - "item.edit.relationships.notifications.failed.title": "Error deleting relationship", + "item.edit.relationships.notifications.failed.title": "Erro ao apagar relacionamento", // "item.edit.relationships.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", - // TODO New key - Add a translation - "item.edit.relationships.notifications.outdated.content": "The item you're currently working on has been changed by another user. Your current changes are discarded to prevent conflicts", + "item.edit.relationships.notifications.outdated.content": "O item em que você está trabalhando foi alterado por outro usuário. Suas alterações atuais são descartadas para evitar conflitos", // "item.edit.relationships.notifications.outdated.title": "Changes outdated", - // TODO New key - Add a translation - "item.edit.relationships.notifications.outdated.title": "Changes outdated", + "item.edit.relationships.notifications.outdated.title": "Alterações desatualizadas", // "item.edit.relationships.notifications.saved.content": "Your changes to this item's relationships were saved.", - // TODO New key - Add a translation - "item.edit.relationships.notifications.saved.content": "Your changes to this item's relationships were saved.", + "item.edit.relationships.notifications.saved.content": "Suas alterações nos relacionamentos deste item foram salvas.", // "item.edit.relationships.notifications.saved.title": "Relationships saved", - // TODO New key - Add a translation - "item.edit.relationships.notifications.saved.title": "Relationships saved", + "item.edit.relationships.notifications.saved.title": "Relacionamentos salvos", // "item.edit.relationships.reinstate-button": "Undo", - // TODO New key - Add a translation - "item.edit.relationships.reinstate-button": "Undo", + "item.edit.relationships.reinstate-button": "Desfazer", // "item.edit.relationships.save-button": "Save", - // TODO New key - Add a translation - "item.edit.relationships.save-button": "Save", + "item.edit.relationships.save-button": "Salvar", // "item.edit.tabs.bitstreams.head": "Item Bitstreams", - // TODO New key - Add a translation - "item.edit.tabs.bitstreams.head": "Item Bitstreams", + "item.edit.tabs.bitstreams.head": "Bitstreams do Item", // "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", - // TODO New key - Add a translation - "item.edit.tabs.bitstreams.title": "Item Edit - Bitstreams", + "item.edit.tabs.bitstreams.title": "Editar Item - Bitstreams", // "item.edit.tabs.curate.head": "Curate", - // TODO New key - Add a translation - "item.edit.tabs.curate.head": "Curate", + "item.edit.tabs.curate.head": "Curadoria", // "item.edit.tabs.curate.title": "Item Edit - Curate", - // TODO New key - Add a translation - "item.edit.tabs.curate.title": "Item Edit - Curate", + "item.edit.tabs.curate.title": "Editar Item - Curadoria", // "item.edit.tabs.metadata.head": "Item Metadata", - // TODO New key - Add a translation - "item.edit.tabs.metadata.head": "Item Metadata", + "item.edit.tabs.metadata.head": "Metadados do Item", // "item.edit.tabs.metadata.title": "Item Edit - Metadata", - // TODO New key - Add a translation - "item.edit.tabs.metadata.title": "Item Edit - Metadata", + "item.edit.tabs.metadata.title": "Editar Item - Metadados", // "item.edit.tabs.relationships.head": "Item Relationships", - // TODO New key - Add a translation - "item.edit.tabs.relationships.head": "Item Relationships", + "item.edit.tabs.relationships.head": "Relacionamentos do Item", // "item.edit.tabs.relationships.title": "Item Edit - Relationships", - // TODO New key - Add a translation - "item.edit.tabs.relationships.title": "Item Edit - Relationships", + "item.edit.tabs.relationships.title": "Editar Item - Relacionamentos", // "item.edit.tabs.status.buttons.authorizations.button": "Authorizations...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.authorizations.button": "Authorizations...", + "item.edit.tabs.status.buttons.authorizations.button": "Autorizações...", // "item.edit.tabs.status.buttons.authorizations.label": "Edit item's authorization policies", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.authorizations.label": "Edit item's authorization policies", + "item.edit.tabs.status.buttons.authorizations.label": "Editar politicas de autorizações de item", // "item.edit.tabs.status.buttons.delete.button": "Permanently delete", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.delete.button": "Permanently delete", + "item.edit.tabs.status.buttons.delete.button": "Apagar permanentemente", // "item.edit.tabs.status.buttons.delete.label": "Completely expunge item", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.delete.label": "Completely expunge item", + "item.edit.tabs.status.buttons.delete.label": "Eliminar completamente o item", // "item.edit.tabs.status.buttons.mappedCollections.button": "Mapped collections", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.mappedCollections.button": "Mapped collections", + "item.edit.tabs.status.buttons.mappedCollections.button": "Coleções mapeadas", // "item.edit.tabs.status.buttons.mappedCollections.label": "Manage mapped collections", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.mappedCollections.label": "Manage mapped collections", + "item.edit.tabs.status.buttons.mappedCollections.label": "Gerenciar coleções mapeadas", // "item.edit.tabs.status.buttons.move.button": "Move...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.move.button": "Move...", + "item.edit.tabs.status.buttons.move.button": "Mover...", // "item.edit.tabs.status.buttons.move.label": "Move item to another collection", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.move.label": "Move item to another collection", + "item.edit.tabs.status.buttons.move.label": "Mover item para outra coleção", // "item.edit.tabs.status.buttons.private.button": "Make it private...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.private.button": "Make it private...", + "item.edit.tabs.status.buttons.private.button": "Tornar privado o item...", // "item.edit.tabs.status.buttons.private.label": "Make item private", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.private.label": "Make item private", + "item.edit.tabs.status.buttons.private.label": "Tornar privado o item", // "item.edit.tabs.status.buttons.public.button": "Make it public...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.public.button": "Make it public...", + "item.edit.tabs.status.buttons.public.button": "Tornar público o item...", // "item.edit.tabs.status.buttons.public.label": "Make item public", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.public.label": "Make item public", + "item.edit.tabs.status.buttons.public.label": "Tornar público o item", // "item.edit.tabs.status.buttons.reinstate.button": "Reinstate...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.reinstate.button": "Reinstate...", + "item.edit.tabs.status.buttons.reinstate.button": "Restabelecer...", // "item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.reinstate.label": "Reinstate item into the repository", + "item.edit.tabs.status.buttons.reinstate.label": "Restabelecer item no repositório", // "item.edit.tabs.status.buttons.withdraw.button": "Withdraw...", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.withdraw.button": "Withdraw...", + "item.edit.tabs.status.buttons.withdraw.button": "Retirar...", // "item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository", - // TODO New key - Add a translation - "item.edit.tabs.status.buttons.withdraw.label": "Withdraw item from the repository", + "item.edit.tabs.status.buttons.withdraw.label": "Retirar item do repositório", // "item.edit.tabs.status.description": "Welcome to the item management page. From here you can withdraw, reinstate, move or delete the item. You may also update or add new metadata / bitstreams on the other tabs.", - // TODO New key - Add a translation - "item.edit.tabs.status.description": "Welcome to the item management page. From here you can withdraw, reinstate, move or delete the item. You may also update or add new metadata / bitstreams on the other tabs.", + "item.edit.tabs.status.description": "Bem-vindo à página de gerenciamento de itens. A partir daqui, você pode retirar, restabelecer, mover ou apagar o item. Você também pode atualizar ou adicionar novos metadados / bitstream nas outras guias.", // "item.edit.tabs.status.head": "Item Status", - // TODO New key - Add a translation - "item.edit.tabs.status.head": "Item Status", + "item.edit.tabs.status.head": "Estado do Item", // "item.edit.tabs.status.labels.handle": "Handle", - // TODO New key - Add a translation "item.edit.tabs.status.labels.handle": "Handle", // "item.edit.tabs.status.labels.id": "Item Internal ID", - // TODO New key - Add a translation - "item.edit.tabs.status.labels.id": "Item Internal ID", + "item.edit.tabs.status.labels.id": "ID Interno do Item", // "item.edit.tabs.status.labels.itemPage": "Item Page", - // TODO New key - Add a translation - "item.edit.tabs.status.labels.itemPage": "Item Page", + "item.edit.tabs.status.labels.itemPage": "Página do Item", // "item.edit.tabs.status.labels.lastModified": "Last Modified", - // TODO New key - Add a translation - "item.edit.tabs.status.labels.lastModified": "Last Modified", + "item.edit.tabs.status.labels.lastModified": "Ultima alteração", // "item.edit.tabs.status.title": "Item Edit - Status", - // TODO New key - Add a translation - "item.edit.tabs.status.title": "Item Edit - Status", + "item.edit.tabs.status.title": "Editar Item - Estado", // "item.edit.tabs.view.head": "View Item", - // TODO New key - Add a translation - "item.edit.tabs.view.head": "View Item", + "item.edit.tabs.view.head": "Visualizar Item", // "item.edit.tabs.view.title": "Item Edit - View", - // TODO New key - Add a translation - "item.edit.tabs.view.title": "Item Edit - View", + "item.edit.tabs.view.title": "Editar Item - Visualizar", // "item.edit.withdraw.cancel": "Cancel", - // TODO New key - Add a translation - "item.edit.withdraw.cancel": "Cancel", + "item.edit.withdraw.cancel": "Cancelar", // "item.edit.withdraw.confirm": "Withdraw", - // TODO New key - Add a translation - "item.edit.withdraw.confirm": "Withdraw", + "item.edit.withdraw.confirm": "Retirar", // "item.edit.withdraw.description": "Are you sure this item should be withdrawn from the archive?", - // TODO New key - Add a translation - "item.edit.withdraw.description": "Are you sure this item should be withdrawn from the archive?", + "item.edit.withdraw.description": "Tem certeza de que este item deve ser retirado do arquivo?", // "item.edit.withdraw.error": "An error occurred while withdrawing the item", - // TODO New key - Add a translation - "item.edit.withdraw.error": "An error occurred while withdrawing the item", + "item.edit.withdraw.error": "Ocorreu um erro ao retirar o item", // "item.edit.withdraw.header": "Withdraw item: {{ id }}", - // TODO New key - Add a translation - "item.edit.withdraw.header": "Withdraw item: {{ id }}", + "item.edit.withdraw.header": "Retirar item: {{ id }}", // "item.edit.withdraw.success": "The item was withdrawn successfully", - // TODO New key - Add a translation - "item.edit.withdraw.success": "The item was withdrawn successfully", + "item.edit.withdraw.success": "O item foi retirado com sucesso", // "item.page.abstract": "Abstract", - // TODO New key - Add a translation - "item.page.abstract": "Abstract", + "item.page.abstract": "Resumo", // "item.page.author": "Authors", - // TODO New key - Add a translation - "item.page.author": "Authors", + "item.page.author": "Autores", // "item.page.citation": "Citation", - // TODO New key - Add a translation - "item.page.citation": "Citation", + "item.page.citation": "Citação", // "item.page.collections": "Collections", - // TODO New key - Add a translation - "item.page.collections": "Collections", + "item.page.collections": "Coleções", // "item.page.date": "Date", - // TODO New key - Add a translation - "item.page.date": "Date", + "item.page.date": "Data", // "item.page.files": "Files", - // TODO New key - Add a translation - "item.page.files": "Files", + "item.page.files": "Arquivos", // "item.page.filesection.description": "Description:", - // TODO New key - Add a translation - "item.page.filesection.description": "Description:", + "item.page.filesection.description": "Descrição:", // "item.page.filesection.download": "Download", - // TODO New key - Add a translation - "item.page.filesection.download": "Download", + "item.page.filesection.download": "Baixar", // "item.page.filesection.format": "Format:", - // TODO New key - Add a translation - "item.page.filesection.format": "Format:", + "item.page.filesection.format": "Formato:", // "item.page.filesection.name": "Name:", - // TODO New key - Add a translation - "item.page.filesection.name": "Name:", + "item.page.filesection.name": "Nome:", // "item.page.filesection.size": "Size:", - // TODO New key - Add a translation - "item.page.filesection.size": "Size:", + "item.page.filesection.size": "Tamanho:", // "item.page.journal.search.title": "Articles in this journal", - // TODO New key - Add a translation "item.page.journal.search.title": "Articles in this journal", // "item.page.link.full": "Full item page", - // TODO New key - Add a translation - "item.page.link.full": "Full item page", + "item.page.link.full": "Página do item completo", // "item.page.link.simple": "Simple item page", - // TODO New key - Add a translation - "item.page.link.simple": "Simple item page", + "item.page.link.simple": "Página do item simplificado", // "item.page.person.search.title": "Articles by this author", - // TODO New key - Add a translation - "item.page.person.search.title": "Articles by this author", + "item.page.person.search.title": "Artigos deste autor", // "item.page.related-items.view-more": "View more", - // TODO New key - Add a translation - "item.page.related-items.view-more": "View more", + "item.page.related-items.view-more": "Mostrar mais", // "item.page.related-items.view-less": "View less", - // TODO New key - Add a translation - "item.page.related-items.view-less": "View less", + "item.page.related-items.view-less": "Mostrar menos", // "item.page.subject": "Keywords", - // TODO New key - Add a translation - "item.page.subject": "Keywords", + "item.page.subject": "Palavras-chave", // "item.page.uri": "URI", - // TODO New key - Add a translation "item.page.uri": "URI", // "item.select.confirm": "Confirm selected", - // TODO New key - Add a translation - "item.select.confirm": "Confirm selected", + "item.select.confirm": "Confirmar seleção", // "item.select.empty": "No items to show", - // TODO New key - Add a translation - "item.select.empty": "No items to show", + "item.select.empty": "Nenhum itme a mostrar", // "item.select.table.author": "Author", - // TODO New key - Add a translation - "item.select.table.author": "Author", + "item.select.table.author": "Autor", // "item.select.table.collection": "Collection", - // TODO New key - Add a translation - "item.select.table.collection": "Collection", + "item.select.table.collection": "Coleção", // "item.select.table.title": "Title", - // TODO New key - Add a translation - "item.select.table.title": "Title", + "item.select.table.title": "Título", // "journal.listelement.badge": "Journal", - // TODO New key - Add a translation - "journal.listelement.badge": "Journal", + "journal.listelement.badge": "Periódico", // "journal.page.description": "Description", - // TODO New key - Add a translation - "journal.page.description": "Description", + "journal.page.description": "Descrição", // "journal.page.editor": "Editor-in-Chief", - // TODO New key - Add a translation - "journal.page.editor": "Editor-in-Chief", + "journal.page.editor": "Editor Chefe", // "journal.page.issn": "ISSN", - // TODO New key - Add a translation "journal.page.issn": "ISSN", // "journal.page.publisher": "Publisher", - // TODO New key - Add a translation - "journal.page.publisher": "Publisher", + "journal.page.publisher": "Editora", // "journal.page.titleprefix": "Journal: ", - // TODO New key - Add a translation - "journal.page.titleprefix": "Journal: ", + "journal.page.titleprefix": "Periódico: ", // "journal.search.results.head": "Journal Search Results", - // TODO New key - Add a translation - "journal.search.results.head": "Journal Search Results", + "journal.search.results.head": "Resultado da Busca de Periódicos", // "journal.search.title": "DSpace Angular :: Journal Search", - // TODO New key - Add a translation - "journal.search.title": "DSpace Angular :: Journal Search", + "journal.search.title": "DSpace Angular :: Busca de Periódicos", // "journalissue.listelement.badge": "Journal Issue", - // TODO New key - Add a translation - "journalissue.listelement.badge": "Journal Issue", + "journalissue.listelement.badge": "Fascículo", // "journalissue.page.description": "Description", - // TODO New key - Add a translation - "journalissue.page.description": "Description", + "journalissue.page.description": "Descrição", // "journalissue.page.issuedate": "Issue Date", - // TODO New key - Add a translation - "journalissue.page.issuedate": "Issue Date", + "journalissue.page.issuedate": "Data de Publicação", // "journalissue.page.journal-issn": "Journal ISSN", - // TODO New key - Add a translation - "journalissue.page.journal-issn": "Journal ISSN", + "journalissue.page.journal-issn": "ISSN do Periódico", // "journalissue.page.journal-title": "Journal Title", - // TODO New key - Add a translation - "journalissue.page.journal-title": "Journal Title", + "journalissue.page.journal-title": "Título do Periódico", // "journalissue.page.keyword": "Keywords", - // TODO New key - Add a translation - "journalissue.page.keyword": "Keywords", + "journalissue.page.keyword": "Palavras-chave", // "journalissue.page.number": "Number", - // TODO New key - Add a translation - "journalissue.page.number": "Number", + "journalissue.page.number": "Número", // "journalissue.page.titleprefix": "Journal Issue: ", - // TODO New key - Add a translation - "journalissue.page.titleprefix": "Journal Issue: ", + "journalissue.page.titleprefix": "Fascículo: ", // "journalvolume.listelement.badge": "Journal Volume", - // TODO New key - Add a translation - "journalvolume.listelement.badge": "Journal Volume", + "journalvolume.listelement.badge": "Volume do Periódico", // "journalvolume.page.description": "Description", - // TODO New key - Add a translation - "journalvolume.page.description": "Description", + "journalvolume.page.description": "Descrição", // "journalvolume.page.issuedate": "Issue Date", - // TODO New key - Add a translation - "journalvolume.page.issuedate": "Issue Date", + "journalvolume.page.issuedate": "Data de Publicação", // "journalvolume.page.titleprefix": "Journal Volume: ", - // TODO New key - Add a translation - "journalvolume.page.titleprefix": "Journal Volume: ", + "journalvolume.page.titleprefix": "Volume do Periódico: ", // "journalvolume.page.volume": "Volume", - // TODO New key - Add a translation "journalvolume.page.volume": "Volume", // "loading.browse-by": "Loading items...", - // TODO New key - Add a translation - "loading.browse-by": "Loading items...", + "loading.browse-by": "Carregando itens...", // "loading.browse-by-page": "Loading page...", - // TODO New key - Add a translation - "loading.browse-by-page": "Loading page...", + "loading.browse-by-page": "Carregando página...", // "loading.collection": "Loading collection...", - // TODO New key - Add a translation - "loading.collection": "Loading collection...", + "loading.collection": "Carregando coleção...", // "loading.collections": "Loading collections...", - // TODO New key - Add a translation - "loading.collections": "Loading collections...", + "loading.collections": "Carregando coleções...", // "loading.community": "Loading community...", - // TODO New key - Add a translation - "loading.community": "Loading community...", + "loading.community": "Carregando comunidade...", // "loading.default": "Loading...", - // TODO New key - Add a translation - "loading.default": "Loading...", + "loading.default": "Carregando...", // "loading.item": "Loading item...", - // TODO New key - Add a translation - "loading.item": "Loading item...", + "loading.item": "Carregando item...", // "loading.items": "Loading items...", - // TODO New key - Add a translation - "loading.items": "Loading items...", + "loading.items": "Carregando itens...", // "loading.mydspace-results": "Loading items...", - // TODO New key - Add a translation - "loading.mydspace-results": "Loading items...", + "loading.mydspace-results": "Carregando itens...", // "loading.objects": "Loading...", - // TODO New key - Add a translation - "loading.objects": "Loading...", + "loading.objects": "Carregando...", // "loading.recent-submissions": "Loading recent submissions...", - // TODO New key - Add a translation - "loading.recent-submissions": "Loading recent submissions...", + "loading.recent-submissions": "Carregando submissões recentes...", // "loading.search-results": "Loading search results...", - // TODO New key - Add a translation - "loading.search-results": "Loading search results...", + "loading.search-results": "Carregando resultados de busca...", // "loading.sub-collections": "Loading sub-collections...", - // TODO New key - Add a translation - "loading.sub-collections": "Loading sub-collections...", + "loading.sub-collections": "Carregando sub-coleções...", // "loading.sub-communities": "Loading sub-communities...", - // TODO New key - Add a translation - "loading.sub-communities": "Loading sub-communities...", + "loading.sub-communities": "Carregando sub-comunidades...", // "loading.top-level-communities": "Loading top-level communities...", - // TODO New key - Add a translation - "loading.top-level-communities": "Loading top-level communities...", + "loading.top-level-communities": "Carregando comunidades de nível superior...", // "login.form.email": "Email address", - // TODO New key - Add a translation - "login.form.email": "Email address", + "login.form.email": "Endereço de email", // "login.form.forgot-password": "Have you forgotten your password?", - // TODO New key - Add a translation - "login.form.forgot-password": "Have you forgotten your password?", + "login.form.forgot-password": "Esqueceu sua senha?", // "login.form.header": "Please log in to DSpace", - // TODO New key - Add a translation - "login.form.header": "Please log in to DSpace", + "login.form.header": "Por favor entre no DSpace", // "login.form.new-user": "New user? Click here to register.", - // TODO New key - Add a translation - "login.form.new-user": "New user? Click here to register.", + "login.form.new-user": "Novo usuário? Clique aqui para cadastrar.", // "login.form.password": "Password", - // TODO New key - Add a translation - "login.form.password": "Password", + "login.form.password": "Senha", // "login.form.submit": "Log in", - // TODO New key - Add a translation - "login.form.submit": "Log in", + "login.form.submit": "Entrar", // "login.title": "Login", - // TODO New key - Add a translation - "login.title": "Login", + "login.title": "Entrar", // "logout.form.header": "Log out from DSpace", - // TODO New key - Add a translation - "logout.form.header": "Log out from DSpace", + "logout.form.header": "Sair do DSpace", // "logout.form.submit": "Log out", - // TODO New key - Add a translation - "logout.form.submit": "Log out", + "logout.form.submit": "Sair", // "logout.title": "Logout", - // TODO New key - Add a translation - "logout.title": "Logout", + "logout.title": "Sair", // "menu.header.admin": "Admin", - // TODO New key - Add a translation - "menu.header.admin": "Admin", + "menu.header.admin": "Administração", // "menu.header.image.logo": "Repository logo", - // TODO New key - Add a translation - "menu.header.image.logo": "Repository logo", + "menu.header.image.logo": "Logo do repositório", // "menu.section.access_control": "Access Control", - // TODO New key - Add a translation - "menu.section.access_control": "Access Control", + "menu.section.access_control": "Controle de Acesso", // "menu.section.access_control_authorizations": "Authorizations", - // TODO New key - Add a translation - "menu.section.access_control_authorizations": "Authorizations", + "menu.section.access_control_authorizations": "Autorizações", // "menu.section.access_control_groups": "Groups", - // TODO New key - Add a translation - "menu.section.access_control_groups": "Groups", + "menu.section.access_control_groups": "Grupos", // "menu.section.access_control_people": "People", - // TODO New key - Add a translation - "menu.section.access_control_people": "People", + "menu.section.access_control_people": "Pessoas", // "menu.section.browse_community": "This Community", - // TODO New key - Add a translation - "menu.section.browse_community": "This Community", + "menu.section.browse_community": "Esta Comunidade", // "menu.section.browse_community_by_author": "By Author", - // TODO New key - Add a translation - "menu.section.browse_community_by_author": "By Author", + "menu.section.browse_community_by_author": "Por Autor", // "menu.section.browse_community_by_issue_date": "By Issue Date", - // TODO New key - Add a translation - "menu.section.browse_community_by_issue_date": "By Issue Date", + "menu.section.browse_community_by_issue_date": "Por Data de Publicação", // "menu.section.browse_community_by_title": "By Title", - // TODO New key - Add a translation - "menu.section.browse_community_by_title": "By Title", + "menu.section.browse_community_by_title": "Por Título", // "menu.section.browse_global": "All of DSpace", - // TODO New key - Add a translation - "menu.section.browse_global": "All of DSpace", + "menu.section.browse_global": "Tudo no DSpace", // "menu.section.browse_global_by_author": "By Author", - // TODO New key - Add a translation - "menu.section.browse_global_by_author": "By Author", + "menu.section.browse_global_by_author": "Por Autor", // "menu.section.browse_global_by_dateissued": "By Issue Date", - // TODO New key - Add a translation - "menu.section.browse_global_by_dateissued": "By Issue Date", + "menu.section.browse_global_by_dateissued": "Por Data de Publicação", // "menu.section.browse_global_by_subject": "By Subject", - // TODO New key - Add a translation - "menu.section.browse_global_by_subject": "By Subject", + "menu.section.browse_global_by_subject": "Por Assunto", // "menu.section.browse_global_by_title": "By Title", - // TODO New key - Add a translation - "menu.section.browse_global_by_title": "By Title", + "menu.section.browse_global_by_title": "Por Título", // "menu.section.browse_global_communities_and_collections": "Communities & Collections", - // TODO New key - Add a translation - "menu.section.browse_global_communities_and_collections": "Communities & Collections", + "menu.section.browse_global_communities_and_collections": "Comunidades e Coleções", // "menu.section.control_panel": "Control Panel", - // TODO New key - Add a translation - "menu.section.control_panel": "Control Panel", + "menu.section.control_panel": "Painel de Controle", // "menu.section.curation_task": "Curation Task", - // TODO New key - Add a translation - "menu.section.curation_task": "Curation Task", + "menu.section.curation_task": "Tarefas de Curadoria", // "menu.section.edit": "Edit", - // TODO New key - Add a translation - "menu.section.edit": "Edit", + "menu.section.edit": "Editar", // "menu.section.edit_collection": "Collection", - // TODO New key - Add a translation - "menu.section.edit_collection": "Collection", + "menu.section.edit_collection": "Coleção", // "menu.section.edit_community": "Community", - // TODO New key - Add a translation - "menu.section.edit_community": "Community", + "menu.section.edit_community": "Comunidade", // "menu.section.edit_item": "Item", - // TODO New key - Add a translation "menu.section.edit_item": "Item", // "menu.section.export": "Export", - // TODO New key - Add a translation - "menu.section.export": "Export", + "menu.section.export": "Exportar", // "menu.section.export_collection": "Collection", - // TODO New key - Add a translation - "menu.section.export_collection": "Collection", + "menu.section.export_collection": "Coleção", // "menu.section.export_community": "Community", - // TODO New key - Add a translation - "menu.section.export_community": "Community", + "menu.section.export_community": "Comunidade", // "menu.section.export_item": "Item", - // TODO New key - Add a translation "menu.section.export_item": "Item", // "menu.section.export_metadata": "Metadata", - // TODO New key - Add a translation - "menu.section.export_metadata": "Metadata", + "menu.section.export_metadata": "Metadados", // "menu.section.find": "Find", - // TODO New key - Add a translation - "menu.section.find": "Find", + "menu.section.find": "Localizar", // "menu.section.find_items": "Items", - // TODO New key - Add a translation - "menu.section.find_items": "Items", + "menu.section.find_items": "Itens", // "menu.section.find_private_items": "Private Items", - // TODO New key - Add a translation - "menu.section.find_private_items": "Private Items", + "menu.section.find_private_items": "Itens Privados", // "menu.section.find_withdrawn_items": "Withdrawn Items", - // TODO New key - Add a translation - "menu.section.find_withdrawn_items": "Withdrawn Items", + "menu.section.find_withdrawn_items": "Itens Removidos", // "menu.section.icon.access_control": "Access Control menu section", - // TODO New key - Add a translation - "menu.section.icon.access_control": "Access Control menu section", + "menu.section.icon.access_control": "Seção do menu Controle de Acesso", // "menu.section.icon.control_panel": "Control Panel menu section", - // TODO New key - Add a translation - "menu.section.icon.control_panel": "Control Panel menu section", + "menu.section.icon.control_panel": "Seção do menu Painel de Controle", // "menu.section.icon.curation_task": "Curation Task menu section", - // TODO New key - Add a translation - "menu.section.icon.curation_task": "Curation Task menu section", + "menu.section.icon.curation_task": "Seção do menu Tarefas de Curadoria", // "menu.section.icon.edit": "Edit menu section", - // TODO New key - Add a translation - "menu.section.icon.edit": "Edit menu section", + "menu.section.icon.edit": "Seção do menu Editar", // "menu.section.icon.export": "Export menu section", - // TODO New key - Add a translation - "menu.section.icon.export": "Export menu section", + "menu.section.icon.export": "Seção do menu Exportar", // "menu.section.icon.find": "Find menu section", - // TODO New key - Add a translation - "menu.section.icon.find": "Find menu section", + "menu.section.icon.find": "Seção do menu Buscar", // "menu.section.icon.import": "Import menu section", - // TODO New key - Add a translation - "menu.section.icon.import": "Import menu section", + "menu.section.icon.import": "Seção do menu Importar", // "menu.section.icon.new": "New menu section", - // TODO New key - Add a translation - "menu.section.icon.new": "New menu section", + "menu.section.icon.new": "Seção do menu Novo", // "menu.section.icon.pin": "Pin sidebar", - // TODO New key - Add a translation - "menu.section.icon.pin": "Pin sidebar", + "menu.section.icon.pin": "Fixar barra lateral", // "menu.section.icon.registries": "Registries menu section", - // TODO New key - Add a translation - "menu.section.icon.registries": "Registries menu section", + "menu.section.icon.registries": "Seção do menu Registros", // "menu.section.icon.statistics_task": "Statistics Task menu section", - // TODO New key - Add a translation - "menu.section.icon.statistics_task": "Statistics Task menu section", + "menu.section.icon.statistics_task": "Seção do menu Tarefas de Estatísticas", // "menu.section.icon.unpin": "Unpin sidebar", - // TODO New key - Add a translation - "menu.section.icon.unpin": "Unpin sidebar", + "menu.section.icon.unpin": "Soltar barra lateral", // "menu.section.import": "Import", - // TODO New key - Add a translation - "menu.section.import": "Import", + "menu.section.import": "Importar", // "menu.section.import_batch": "Batch Import (ZIP)", - // TODO New key - Add a translation - "menu.section.import_batch": "Batch Import (ZIP)", + "menu.section.import_batch": "Importação em Lote (ZIP)", // "menu.section.import_metadata": "Metadata", - // TODO New key - Add a translation - "menu.section.import_metadata": "Metadata", + "menu.section.import_metadata": "Metadados", // "menu.section.new": "New", - // TODO New key - Add a translation - "menu.section.new": "New", + "menu.section.new": "Nova", // "menu.section.new_collection": "Collection", - // TODO New key - Add a translation - "menu.section.new_collection": "Collection", + "menu.section.new_collection": "Coleção", // "menu.section.new_community": "Community", - // TODO New key - Add a translation - "menu.section.new_community": "Community", + "menu.section.new_community": "Comunidade", // "menu.section.new_item": "Item", - // TODO New key - Add a translation "menu.section.new_item": "Item", // "menu.section.new_item_version": "Item Version", - // TODO New key - Add a translation - "menu.section.new_item_version": "Item Version", + "menu.section.new_item_version": "Versão do Item", // "menu.section.pin": "Pin sidebar", - // TODO New key - Add a translation - "menu.section.pin": "Pin sidebar", + "menu.section.pin": "Fixar barra lateral", // "menu.section.unpin": "Unpin sidebar", - // TODO New key - Add a translation - "menu.section.unpin": "Unpin sidebar", + "menu.section.unpin": "Soltar barra lateral", // "menu.section.registries": "Registries", - // TODO New key - Add a translation - "menu.section.registries": "Registries", + "menu.section.registries": "Registros", // "menu.section.registries_format": "Format", - // TODO New key - Add a translation - "menu.section.registries_format": "Format", + "menu.section.registries_format": "Formatos", // "menu.section.registries_metadata": "Metadata", - // TODO New key - Add a translation - "menu.section.registries_metadata": "Metadata", + "menu.section.registries_metadata": "Metadados", // "menu.section.statistics": "Statistics", - // TODO New key - Add a translation - "menu.section.statistics": "Statistics", + "menu.section.statistics": "Estatísticas", // "menu.section.statistics_task": "Statistics Task", - // TODO New key - Add a translation - "menu.section.statistics_task": "Statistics Task", + "menu.section.statistics_task": "Tarefas de Estatísticas", // "menu.section.toggle.access_control": "Toggle Access Control section", - // TODO New key - Add a translation - "menu.section.toggle.access_control": "Toggle Access Control section", + "menu.section.toggle.access_control": "Alternar Seção Controle de Acesso", // "menu.section.toggle.control_panel": "Toggle Control Panel section", - // TODO New key - Add a translation - "menu.section.toggle.control_panel": "Toggle Control Panel section", + "menu.section.toggle.control_panel": "Alternar Seção Painel de COntrole", // "menu.section.toggle.curation_task": "Toggle Curation Task section", - // TODO New key - Add a translation - "menu.section.toggle.curation_task": "Toggle Curation Task section", + "menu.section.toggle.curation_task": "Alternar Seção Tarefas de Curadoria", // "menu.section.toggle.edit": "Toggle Edit section", - // TODO New key - Add a translation - "menu.section.toggle.edit": "Toggle Edit section", + "menu.section.toggle.edit": "Alternar Seção Editar", // "menu.section.toggle.export": "Toggle Export section", - // TODO New key - Add a translation - "menu.section.toggle.export": "Toggle Export section", + "menu.section.toggle.export": "Alternar Seção Exportar", // "menu.section.toggle.find": "Toggle Find section", - // TODO New key - Add a translation - "menu.section.toggle.find": "Toggle Find section", + "menu.section.toggle.find": "Alternar Seção Pesquisa", // "menu.section.toggle.import": "Toggle Import section", - // TODO New key - Add a translation - "menu.section.toggle.import": "Toggle Import section", + "menu.section.toggle.import": "Alternar Seção Importar", // "menu.section.toggle.new": "Toggle New section", - // TODO New key - Add a translation - "menu.section.toggle.new": "Toggle New section", + "menu.section.toggle.new": "Alternar Nova Seção", // "menu.section.toggle.registries": "Toggle Registries section", - // TODO New key - Add a translation - "menu.section.toggle.registries": "Toggle Registries section", + "menu.section.toggle.registries": "Alternar Seção Registros", // "menu.section.toggle.statistics_task": "Toggle Statistics Task section", - // TODO New key - Add a translation - "menu.section.toggle.statistics_task": "Toggle Statistics Task section", + "menu.section.toggle.statistics_task": "Alternar Seção Tarefas de Estatísticas", // "mydspace.description": "", - // TODO New key - Add a translation "mydspace.description": "", // "mydspace.general.text-here": "HERE", - // TODO New key - Add a translation - "mydspace.general.text-here": "HERE", + "mydspace.general.text-here": "AQUI", // "mydspace.messages.controller-help": "Select this option to send a message to item's submitter.", - // TODO New key - Add a translation - "mydspace.messages.controller-help": "Select this option to send a message to item's submitter.", + "mydspace.messages.controller-help": "Selecione esta opção para enviar uma mensagem para o submetedor do item.", // "mydspace.messages.description-placeholder": "Insert your message here...", - // TODO New key - Add a translation - "mydspace.messages.description-placeholder": "Insert your message here...", + "mydspace.messages.description-placeholder": "Insira sua mensagem aqui...", // "mydspace.messages.hide-msg": "Hide message", - // TODO New key - Add a translation - "mydspace.messages.hide-msg": "Hide message", + "mydspace.messages.hide-msg": "Ocultar mensagem", // "mydspace.messages.mark-as-read": "Mark as read", - // TODO New key - Add a translation - "mydspace.messages.mark-as-read": "Mark as read", + "mydspace.messages.mark-as-read": "Marcar como lida", // "mydspace.messages.mark-as-unread": "Mark as unread", - // TODO New key - Add a translation - "mydspace.messages.mark-as-unread": "Mark as unread", + "mydspace.messages.mark-as-unread": "Marcar como não lida", // "mydspace.messages.no-content": "No content.", - // TODO New key - Add a translation - "mydspace.messages.no-content": "No content.", + "mydspace.messages.no-content": "Sem conteúdo", // "mydspace.messages.no-messages": "No messages yet.", - // TODO New key - Add a translation - "mydspace.messages.no-messages": "No messages yet.", + "mydspace.messages.no-messages": "Nenhuma mensagem ainda", // "mydspace.messages.send-btn": "Send", - // TODO New key - Add a translation - "mydspace.messages.send-btn": "Send", + "mydspace.messages.send-btn": "Enviar", // "mydspace.messages.show-msg": "Show message", - // TODO New key - Add a translation - "mydspace.messages.show-msg": "Show message", + "mydspace.messages.show-msg": "Mostrar mensagem", // "mydspace.messages.subject-placeholder": "Subject...", - // TODO New key - Add a translation - "mydspace.messages.subject-placeholder": "Subject...", + "mydspace.messages.subject-placeholder": "Assunto...", // "mydspace.messages.submitter-help": "Select this option to send a message to controller.", - // TODO New key - Add a translation - "mydspace.messages.submitter-help": "Select this option to send a message to controller.", + "mydspace.messages.submitter-help": "Selecione esta opção para enviar uma mensagem ao controlador.", // "mydspace.messages.title": "Messages", - // TODO New key - Add a translation - "mydspace.messages.title": "Messages", + "mydspace.messages.title": "Mensagens", // "mydspace.messages.to": "To", - // TODO New key - Add a translation - "mydspace.messages.to": "To", + "mydspace.messages.to": "Para", // "mydspace.new-submission": "New submission", - // TODO New key - Add a translation - "mydspace.new-submission": "New submission", + "mydspace.new-submission": "Nova submissão", // "mydspace.results.head": "Your submissions", - // TODO New key - Add a translation - "mydspace.results.head": "Your submissions", + "mydspace.results.head": "Minhas submissões", // "mydspace.results.no-abstract": "No Abstract", - // TODO New key - Add a translation - "mydspace.results.no-abstract": "No Abstract", + "mydspace.results.no-abstract": "Sem Resumo", // "mydspace.results.no-authors": "No Authors", - // TODO New key - Add a translation - "mydspace.results.no-authors": "No Authors", + "mydspace.results.no-authors": "Sem Autores", // "mydspace.results.no-collections": "No Collections", - // TODO New key - Add a translation - "mydspace.results.no-collections": "No Collections", + "mydspace.results.no-collections": "Sem Coleções", // "mydspace.results.no-date": "No Date", - // TODO New key - Add a translation - "mydspace.results.no-date": "No Date", + "mydspace.results.no-date": "Sem Data", // "mydspace.results.no-files": "No Files", - // TODO New key - Add a translation - "mydspace.results.no-files": "No Files", + "mydspace.results.no-files": "Sem arquivos", // "mydspace.results.no-results": "There were no items to show", - // TODO New key - Add a translation - "mydspace.results.no-results": "There were no items to show", + "mydspace.results.no-results": "Não havia itens a mostrar", // "mydspace.results.no-title": "No title", - // TODO New key - Add a translation - "mydspace.results.no-title": "No title", + "mydspace.results.no-title": "Sem título", // "mydspace.results.no-uri": "No Uri", - // TODO New key - Add a translation - "mydspace.results.no-uri": "No Uri", + "mydspace.results.no-uri": "Sem Uri", // "mydspace.show.workflow": "All tasks", - // TODO New key - Add a translation - "mydspace.show.workflow": "All tasks", + "mydspace.show.workflow": "Todas as tarefas", // "mydspace.show.workspace": "Your Submissions", - // TODO New key - Add a translation - "mydspace.show.workspace": "Your Submissions", + "mydspace.show.workspace": "Minhas Submissões", // "mydspace.status.archived": "Archived", - // TODO New key - Add a translation - "mydspace.status.archived": "Archived", + "mydspace.status.archived": "Arquivado", // "mydspace.status.validation": "Validation", - // TODO New key - Add a translation - "mydspace.status.validation": "Validation", + "mydspace.status.validation": "Validação", // "mydspace.status.waiting-for-controller": "Waiting for controller", - // TODO New key - Add a translation - "mydspace.status.waiting-for-controller": "Waiting for controller", + "mydspace.status.waiting-for-controller": "Esperando pelo controlador", // "mydspace.status.workflow": "Workflow", - // TODO New key - Add a translation - "mydspace.status.workflow": "Workflow", + "mydspace.status.workflow": "Fluxo de trabalho", // "mydspace.status.workspace": "Workspace", - // TODO New key - Add a translation - "mydspace.status.workspace": "Workspace", + "mydspace.status.workspace": "Espaço de trabalho", // "mydspace.title": "MyDSpace", - // TODO New key - Add a translation "mydspace.title": "MyDSpace", // "mydspace.upload.upload-failed": "Error creating new workspace. Please verify the content uploaded before retry.", - // TODO New key - Add a translation - "mydspace.upload.upload-failed": "Error creating new workspace. Please verify the content uploaded before retry.", + "mydspace.upload.upload-failed": "Erro ao criar novo espaço de trabalho. Por favor verifique o conteúdo enviado antes de tentar novamente.", // "mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.", - // TODO New key - Add a translation - "mydspace.upload.upload-multiple-successful": "{{qty}} new workspace items created.", + "mydspace.upload.upload-multiple-successful": "{{qty}} novo(s) item(ns) de espaço de trabalho criados.", // "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.", - // TODO New key - Add a translation - "mydspace.upload.upload-successful": "New workspace item created. Click {{here}} for edit it.", + "mydspace.upload.upload-successful": "Novo item de espaço de trabalho criado. Clique {{here}} para o editar.", // "mydspace.view-btn": "View", - // TODO New key - Add a translation - "mydspace.view-btn": "View", + "mydspace.view-btn": "Ver", // "nav.browse.header": "All of DSpace", - // TODO New key - Add a translation - "nav.browse.header": "All of DSpace", + "nav.browse.header": "Tudo no DSpace", // "nav.community-browse.header": "By Community", - // TODO New key - Add a translation - "nav.community-browse.header": "By Community", + "nav.community-browse.header": "Por Comunidade", // "nav.language": "Language switch", - // TODO New key - Add a translation - "nav.language": "Language switch", + "nav.language": "Seletor de idioma", // "nav.login": "Log In", - // TODO New key - Add a translation - "nav.login": "Log In", + "nav.login": "Entrar", // "nav.logout": "Log Out", - // TODO New key - Add a translation - "nav.logout": "Log Out", + "nav.logout": "Sair", // "nav.mydspace": "MyDSpace", - // TODO New key - Add a translation "nav.mydspace": "MyDSpace", // "nav.search": "Search", - // TODO New key - Add a translation - "nav.search": "Search", + "nav.search": "Buscar", // "nav.statistics.header": "Statistics", - // TODO New key - Add a translation - "nav.statistics.header": "Statistics", + "nav.statistics.header": "Estatísticas", // "orgunit.listelement.badge": "Organizational Unit", - // TODO New key - Add a translation - "orgunit.listelement.badge": "Organizational Unit", + "orgunit.listelement.badge": "Unidade Organizacional", // "orgunit.page.city": "City", - // TODO New key - Add a translation - "orgunit.page.city": "City", + "orgunit.page.city": "Cidade", // "orgunit.page.country": "Country", - // TODO New key - Add a translation - "orgunit.page.country": "Country", + "orgunit.page.country": "País", // "orgunit.page.dateestablished": "Date established", - // TODO New key - Add a translation - "orgunit.page.dateestablished": "Date established", + "orgunit.page.dateestablished": "Data estabelecida", // "orgunit.page.description": "Description", - // TODO New key - Add a translation - "orgunit.page.description": "Description", + "orgunit.page.description": "Descrição", // "orgunit.page.id": "ID", - // TODO New key - Add a translation "orgunit.page.id": "ID", // "orgunit.page.titleprefix": "Organizational Unit: ", - // TODO New key - Add a translation - "orgunit.page.titleprefix": "Organizational Unit: ", + "orgunit.page.titleprefix": "Unidade Organizacional: ", // "pagination.results-per-page": "Results Per Page", - // TODO New key - Add a translation - "pagination.results-per-page": "Results Per Page", + "pagination.results-per-page": "Resultados por página", // "pagination.showing.detail": "{{ range }} of {{ total }}", - // TODO New key - Add a translation - "pagination.showing.detail": "{{ range }} of {{ total }}", + "pagination.showing.detail": "{{ range }} de {{ total }}", // "pagination.showing.label": "Now showing ", - // TODO New key - Add a translation - "pagination.showing.label": "Now showing ", + "pagination.showing.label": "Agora exibindo ", // "pagination.sort-direction": "Sort Options", - // TODO New key - Add a translation - "pagination.sort-direction": "Sort Options", + "pagination.sort-direction": "Opções de Ordenação", // "person.listelement.badge": "Person", - // TODO New key - Add a translation - "person.listelement.badge": "Person", + "person.listelement.badge": "Pessoa", // "person.page.birthdate": "Birth Date", - // TODO New key - Add a translation - "person.page.birthdate": "Birth Date", + "person.page.birthdate": "Data de nascimento", // "person.page.email": "Email Address", - // TODO New key - Add a translation - "person.page.email": "Email Address", + "person.page.email": "Endereço de Email", // "person.page.firstname": "First Name", - // TODO New key - Add a translation - "person.page.firstname": "First Name", + "person.page.firstname": "Primeiro Nome", // "person.page.jobtitle": "Job Title", - // TODO New key - Add a translation - "person.page.jobtitle": "Job Title", + "person.page.jobtitle": "Cargo", // "person.page.lastname": "Last Name", - // TODO New key - Add a translation - "person.page.lastname": "Last Name", + "person.page.lastname": "Último Nome", // "person.page.link.full": "Show all metadata", - // TODO New key - Add a translation - "person.page.link.full": "Show all metadata", + "person.page.link.full": "Mostrar todos os metadados", // "person.page.orcid": "ORCID", - // TODO New key - Add a translation "person.page.orcid": "ORCID", // "person.page.staffid": "Staff ID", - // TODO New key - Add a translation - "person.page.staffid": "Staff ID", + "person.page.staffid": "ID de Equipe", // "person.page.titleprefix": "Person: ", - // TODO New key - Add a translation - "person.page.titleprefix": "Person: ", + "person.page.titleprefix": "Pessoa: ", // "person.search.results.head": "Person Search Results", - // TODO New key - Add a translation - "person.search.results.head": "Person Search Results", + "person.search.results.head": "Resultado da Busca de Pessoa", // "person.search.title": "DSpace Angular :: Person Search", - // TODO New key - Add a translation - "person.search.title": "DSpace Angular :: Person Search", + "person.search.title": "DSpace Angular :: Buscar Pessoa", // "project.listelement.badge": "Research Project", - // TODO New key - Add a translation - "project.listelement.badge": "Research Project", + "project.listelement.badge": "Projeto de Pesquisa", // "project.page.contributor": "Contributors", - // TODO New key - Add a translation - "project.page.contributor": "Contributors", + "project.page.contributor": "Contribuidores", // "project.page.description": "Description", - // TODO New key - Add a translation - "project.page.description": "Description", + "project.page.description": "Descrição", // "project.page.expectedcompletion": "Expected Completion", - // TODO New key - Add a translation - "project.page.expectedcompletion": "Expected Completion", + "project.page.expectedcompletion": "Conclusão esperada", // "project.page.funder": "Funders", - // TODO New key - Add a translation - "project.page.funder": "Funders", + "project.page.funder": "Financiadores", // "project.page.id": "ID", - // TODO New key - Add a translation "project.page.id": "ID", // "project.page.keyword": "Keywords", - // TODO New key - Add a translation - "project.page.keyword": "Keywords", + "project.page.keyword": "Palavras-chave", // "project.page.status": "Status", - // TODO New key - Add a translation - "project.page.status": "Status", + "project.page.status": "Estado", // "project.page.titleprefix": "Research Project: ", - // TODO New key - Add a translation - "project.page.titleprefix": "Research Project: ", + "project.page.titleprefix": "Projeto de Pesquisa: ", // "publication.listelement.badge": "Publication", - // TODO New key - Add a translation - "publication.listelement.badge": "Publication", + "publication.listelement.badge": "Publicação", // "publication.page.description": "Description", - // TODO New key - Add a translation - "publication.page.description": "Description", + "publication.page.description": "Descrição", // "publication.page.journal-issn": "Journal ISSN", - // TODO New key - Add a translation - "publication.page.journal-issn": "Journal ISSN", + "publication.page.journal-issn": "ISSN do Periódico", // "publication.page.journal-title": "Journal Title", - // TODO New key - Add a translation - "publication.page.journal-title": "Journal Title", + "publication.page.journal-title": "Título do Periódico", // "publication.page.publisher": "Publisher", - // TODO New key - Add a translation - "publication.page.publisher": "Publisher", + "publication.page.publisher": "Editora", // "publication.page.titleprefix": "Publication: ", - // TODO New key - Add a translation - "publication.page.titleprefix": "Publication: ", + "publication.page.titleprefix": "Publicação: ", // "publication.page.volume-title": "Volume Title", - // TODO New key - Add a translation - "publication.page.volume-title": "Volume Title", + "publication.page.volume-title": "Título do Volume", // "publication.search.results.head": "Publication Search Results", - // TODO New key - Add a translation - "publication.search.results.head": "Publication Search Results", + "publication.search.results.head": "Resultados da Busca de Publicação", // "publication.search.title": "DSpace Angular :: Publication Search", - // TODO New key - Add a translation - "publication.search.title": "DSpace Angular :: Publication Search", + "publication.search.title": "DSpace Angular :: Busca de Publicações", // "relationships.isAuthorOf": "Authors", - // TODO New key - Add a translation - "relationships.isAuthorOf": "Authors", + "relationships.isAuthorOf": "Autores", // "relationships.isIssueOf": "Journal Issues", - // TODO New key - Add a translation - "relationships.isIssueOf": "Journal Issues", + "relationships.isIssueOf": "Fascículo", // "relationships.isJournalIssueOf": "Journal Issue", - // TODO New key - Add a translation - "relationships.isJournalIssueOf": "Journal Issue", + "relationships.isJournalIssueOf": "Fascículo", // "relationships.isJournalOf": "Journals", - // TODO New key - Add a translation - "relationships.isJournalOf": "Journals", + "relationships.isJournalOf": "Periódicos", // "relationships.isOrgUnitOf": "Organizational Units", - // TODO New key - Add a translation - "relationships.isOrgUnitOf": "Organizational Units", + "relationships.isOrgUnitOf": "Unidades Organizacionais", // "relationships.isPersonOf": "Authors", - // TODO New key - Add a translation - "relationships.isPersonOf": "Authors", + "relationships.isPersonOf": "Autores", // "relationships.isProjectOf": "Research Projects", - // TODO New key - Add a translation - "relationships.isProjectOf": "Research Projects", + "relationships.isProjectOf": "Projetos de Pesquisa", // "relationships.isPublicationOf": "Publications", - // TODO New key - Add a translation - "relationships.isPublicationOf": "Publications", + "relationships.isPublicationOf": "Publicações", // "relationships.isPublicationOfJournalIssue": "Articles", - // TODO New key - Add a translation - "relationships.isPublicationOfJournalIssue": "Articles", + "relationships.isPublicationOfJournalIssue": "Artigos", // "relationships.isSingleJournalOf": "Journal", - // TODO New key - Add a translation - "relationships.isSingleJournalOf": "Journal", + "relationships.isSingleJournalOf": "Periódico", // "relationships.isSingleVolumeOf": "Journal Volume", - // TODO New key - Add a translation - "relationships.isSingleVolumeOf": "Journal Volume", + "relationships.isSingleVolumeOf": "Volume do Periódico", // "relationships.isVolumeOf": "Journal Volumes", - // TODO New key - Add a translation - "relationships.isVolumeOf": "Journal Volumes", + "relationships.isVolumeOf": "Volumes do Periódico", // "search.description": "", - // TODO New key - Add a translation "search.description": "", // "search.switch-configuration.title": "Show", - // TODO New key - Add a translation - "search.switch-configuration.title": "Show", + "search.switch-configuration.title": "Mostrar", // "search.title": "DSpace Angular :: Search", - // TODO New key - Add a translation - "search.title": "DSpace Angular :: Search", + "search.title": "DSpace Angular :: Busca", // "search.filters.applied.f.author": "Author", - // TODO New key - Add a translation - "search.filters.applied.f.author": "Author", + "search.filters.applied.f.author": "Autor", // "search.filters.applied.f.dateIssued.max": "End date", - // TODO New key - Add a translation - "search.filters.applied.f.dateIssued.max": "End date", + "search.filters.applied.f.dateIssued.max": "Data final", // "search.filters.applied.f.dateIssued.min": "Start date", - // TODO New key - Add a translation - "search.filters.applied.f.dateIssued.min": "Start date", + "search.filters.applied.f.dateIssued.min": "Data inicial", // "search.filters.applied.f.dateSubmitted": "Date submitted", - // TODO New key - Add a translation - "search.filters.applied.f.dateSubmitted": "Date submitted", + "search.filters.applied.f.dateSubmitted": "Data de submissão", // "search.filters.applied.f.entityType": "Item Type", - // TODO New key - Add a translation - "search.filters.applied.f.entityType": "Item Type", + "search.filters.applied.f.entityType": "Tipo de Item", // "search.filters.applied.f.has_content_in_original_bundle": "Has files", - // TODO New key - Add a translation - "search.filters.applied.f.has_content_in_original_bundle": "Has files", + "search.filters.applied.f.has_content_in_original_bundle": "Tem arquivos", // "search.filters.applied.f.itemtype": "Type", - // TODO New key - Add a translation - "search.filters.applied.f.itemtype": "Type", + "search.filters.applied.f.itemtype": "Tipo", // "search.filters.applied.f.namedresourcetype": "Status", - // TODO New key - Add a translation - "search.filters.applied.f.namedresourcetype": "Status", + "search.filters.applied.f.namedresourcetype": "Estado", // "search.filters.applied.f.subject": "Subject", - // TODO New key - Add a translation - "search.filters.applied.f.subject": "Subject", + "search.filters.applied.f.subject": "Assunto", // "search.filters.applied.f.submitter": "Submitter", - // TODO New key - Add a translation - "search.filters.applied.f.submitter": "Submitter", + "search.filters.applied.f.submitter": "Submetedor", // "search.filters.filter.author.head": "Author", - // TODO New key - Add a translation - "search.filters.filter.author.head": "Author", + "search.filters.filter.author.head": "Autor", // "search.filters.filter.author.placeholder": "Author name", - // TODO New key - Add a translation - "search.filters.filter.author.placeholder": "Author name", + "search.filters.filter.author.placeholder": "Nome do autor", // "search.filters.filter.birthDate.head": "Birth Date", - // TODO New key - Add a translation - "search.filters.filter.birthDate.head": "Birth Date", + "search.filters.filter.birthDate.head": "Data de nascimento", // "search.filters.filter.birthDate.placeholder": "Birth Date", - // TODO New key - Add a translation - "search.filters.filter.birthDate.placeholder": "Birth Date", + "search.filters.filter.birthDate.placeholder": "Data de nascimento", // "search.filters.filter.creativeDatePublished.head": "Date Published", - // TODO New key - Add a translation - "search.filters.filter.creativeDatePublished.head": "Date Published", + "search.filters.filter.creativeDatePublished.head": "Data de publicação", // "search.filters.filter.creativeDatePublished.placeholder": "Date Published", - // TODO New key - Add a translation - "search.filters.filter.creativeDatePublished.placeholder": "Date Published", + "search.filters.filter.creativeDatePublished.placeholder": "Data de publicação", // "search.filters.filter.creativeWorkEditor.head": "Editor", - // TODO New key - Add a translation "search.filters.filter.creativeWorkEditor.head": "Editor", // "search.filters.filter.creativeWorkEditor.placeholder": "Editor", - // TODO New key - Add a translation "search.filters.filter.creativeWorkEditor.placeholder": "Editor", // "search.filters.filter.creativeWorkKeywords.head": "Subject", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkKeywords.head": "Subject", + "search.filters.filter.creativeWorkKeywords.head": "Assunto", // "search.filters.filter.creativeWorkKeywords.placeholder": "Subject", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkKeywords.placeholder": "Subject", + "search.filters.filter.creativeWorkKeywords.placeholder": "Assunto", // "search.filters.filter.creativeWorkPublisher.head": "Publisher", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkPublisher.head": "Publisher", + "search.filters.filter.creativeWorkPublisher.head": "Editora", // "search.filters.filter.creativeWorkPublisher.placeholder": "Publisher", - // TODO New key - Add a translation - "search.filters.filter.creativeWorkPublisher.placeholder": "Publisher", + "search.filters.filter.creativeWorkPublisher.placeholder": "Editora", // "search.filters.filter.dateIssued.head": "Date", - // TODO New key - Add a translation - "search.filters.filter.dateIssued.head": "Date", + "search.filters.filter.dateIssued.head": "Data", // "search.filters.filter.dateIssued.max.placeholder": "Minimum Date", - // TODO New key - Add a translation - "search.filters.filter.dateIssued.max.placeholder": "Minimum Date", + "search.filters.filter.dateIssued.max.placeholder": "Data Mínima", // "search.filters.filter.dateIssued.min.placeholder": "Maximum Date", - // TODO New key - Add a translation - "search.filters.filter.dateIssued.min.placeholder": "Maximum Date", + "search.filters.filter.dateIssued.min.placeholder": "Data Máxima", // "search.filters.filter.dateSubmitted.head": "Date submitted", - // TODO New key - Add a translation - "search.filters.filter.dateSubmitted.head": "Date submitted", + "search.filters.filter.dateSubmitted.head": "Data de submissão", // "search.filters.filter.dateSubmitted.placeholder": "Date submitted", - // TODO New key - Add a translation - "search.filters.filter.dateSubmitted.placeholder": "Date submitted", + "search.filters.filter.dateSubmitted.placeholder": "Data de submissão", // "search.filters.filter.entityType.head": "Item Type", - // TODO New key - Add a translation - "search.filters.filter.entityType.head": "Item Type", + "search.filters.filter.entityType.head": "Tipo de Item", // "search.filters.filter.entityType.placeholder": "Item Type", - // TODO New key - Add a translation - "search.filters.filter.entityType.placeholder": "Item Type", + "search.filters.filter.entityType.placeholder": "Tipo de Item", // "search.filters.filter.has_content_in_original_bundle.head": "Has files", - // TODO New key - Add a translation - "search.filters.filter.has_content_in_original_bundle.head": "Has files", + "search.filters.filter.has_content_in_original_bundle.head": "Tem arquivos", // "search.filters.filter.itemtype.head": "Type", - // TODO New key - Add a translation - "search.filters.filter.itemtype.head": "Type", + "search.filters.filter.itemtype.head": "Tipo", // "search.filters.filter.itemtype.placeholder": "Type", - // TODO New key - Add a translation - "search.filters.filter.itemtype.placeholder": "Type", + "search.filters.filter.itemtype.placeholder": "Tipo", // "search.filters.filter.jobTitle.head": "Job Title", - // TODO New key - Add a translation - "search.filters.filter.jobTitle.head": "Job Title", + "search.filters.filter.jobTitle.head": "Cargo", // "search.filters.filter.jobTitle.placeholder": "Job Title", - // TODO New key - Add a translation - "search.filters.filter.jobTitle.placeholder": "Job Title", + "search.filters.filter.jobTitle.placeholder": "Cargo", // "search.filters.filter.knowsLanguage.head": "Known language", - // TODO New key - Add a translation - "search.filters.filter.knowsLanguage.head": "Known language", + "search.filters.filter.knowsLanguage.head": "Idioma conhecido", // "search.filters.filter.knowsLanguage.placeholder": "Known language", - // TODO New key - Add a translation - "search.filters.filter.knowsLanguage.placeholder": "Known language", + "search.filters.filter.knowsLanguage.placeholder": "Idioma conhecido", // "search.filters.filter.namedresourcetype.head": "Status", - // TODO New key - Add a translation - "search.filters.filter.namedresourcetype.head": "Status", + "search.filters.filter.namedresourcetype.head": "Estado", // "search.filters.filter.namedresourcetype.placeholder": "Status", - // TODO New key - Add a translation - "search.filters.filter.namedresourcetype.placeholder": "Status", + "search.filters.filter.namedresourcetype.placeholder": "Estado", // "search.filters.filter.objectpeople.head": "People", - // TODO New key - Add a translation - "search.filters.filter.objectpeople.head": "People", + "search.filters.filter.objectpeople.head": "Pessoas", // "search.filters.filter.objectpeople.placeholder": "People", - // TODO New key - Add a translation - "search.filters.filter.objectpeople.placeholder": "People", + "search.filters.filter.objectpeople.placeholder": "Pessoas", // "search.filters.filter.organizationAddressCountry.head": "Country", - // TODO New key - Add a translation - "search.filters.filter.organizationAddressCountry.head": "Country", + "search.filters.filter.organizationAddressCountry.head": "País", // "search.filters.filter.organizationAddressCountry.placeholder": "Country", - // TODO New key - Add a translation - "search.filters.filter.organizationAddressCountry.placeholder": "Country", + "search.filters.filter.organizationAddressCountry.placeholder": "País", // "search.filters.filter.organizationAddressLocality.head": "City", - // TODO New key - Add a translation - "search.filters.filter.organizationAddressLocality.head": "City", + "search.filters.filter.organizationAddressLocality.head": "Cidade", // "search.filters.filter.organizationAddressLocality.placeholder": "City", - // TODO New key - Add a translation - "search.filters.filter.organizationAddressLocality.placeholder": "City", + "search.filters.filter.organizationAddressLocality.placeholder": "Cidade", // "search.filters.filter.organizationFoundingDate.head": "Date Founded", - // TODO New key - Add a translation - "search.filters.filter.organizationFoundingDate.head": "Date Founded", + "search.filters.filter.organizationFoundingDate.head": "Data de Fundação", // "search.filters.filter.organizationFoundingDate.placeholder": "Date Founded", - // TODO New key - Add a translation - "search.filters.filter.organizationFoundingDate.placeholder": "Date Founded", + "search.filters.filter.organizationFoundingDate.placeholder": "Data de Fundação", // "search.filters.filter.scope.head": "Scope", - // TODO New key - Add a translation - "search.filters.filter.scope.head": "Scope", + "search.filters.filter.scope.head": "Escopo", // "search.filters.filter.scope.placeholder": "Scope filter", - // TODO New key - Add a translation - "search.filters.filter.scope.placeholder": "Scope filter", + "search.filters.filter.scope.placeholder": "Filtrar escopo", // "search.filters.filter.show-less": "Collapse", - // TODO New key - Add a translation - "search.filters.filter.show-less": "Collapse", + "search.filters.filter.show-less": "Mostrar menos", // "search.filters.filter.show-more": "Show more", - // TODO New key - Add a translation - "search.filters.filter.show-more": "Show more", + "search.filters.filter.show-more": "Mostrar mais", // "search.filters.filter.subject.head": "Subject", - // TODO New key - Add a translation - "search.filters.filter.subject.head": "Subject", + "search.filters.filter.subject.head": "Assunto", // "search.filters.filter.subject.placeholder": "Subject", - // TODO New key - Add a translation - "search.filters.filter.subject.placeholder": "Subject", + "search.filters.filter.subject.placeholder": "Assunto", // "search.filters.filter.submitter.head": "Submitter", - // TODO New key - Add a translation - "search.filters.filter.submitter.head": "Submitter", + "search.filters.filter.submitter.head": "Submetedor", // "search.filters.filter.submitter.placeholder": "Submitter", - // TODO New key - Add a translation - "search.filters.filter.submitter.placeholder": "Submitter", + "search.filters.filter.submitter.placeholder": "Submetedor", // "search.filters.head": "Filters", - // TODO New key - Add a translation - "search.filters.head": "Filters", + "search.filters.head": "Filtros", // "search.filters.reset": "Reset filters", - // TODO New key - Add a translation - "search.filters.reset": "Reset filters", + "search.filters.reset": "Limpar filtros", // "search.form.search": "Search", - // TODO New key - Add a translation - "search.form.search": "Search", + "search.form.search": "Buscar", // "search.form.search_dspace": "Search DSpace", - // TODO New key - Add a translation - "search.form.search_dspace": "Search DSpace", + "search.form.search_dspace": "Buscar no DSpace", // "search.form.search_mydspace": "Search MyDSpace", - // TODO New key - Add a translation - "search.form.search_mydspace": "Search MyDSpace", + "search.form.search_mydspace": "Buscar no MyDSpace", // "search.results.head": "Search Results", - // TODO New key - Add a translation - "search.results.head": "Search Results", + "search.results.head": "Resultados de Busca", // "search.results.no-results": "Your search returned no results. Having trouble finding what you're looking for? Try putting", - // TODO New key - Add a translation - "search.results.no-results": "Your search returned no results. Having trouble finding what you're looking for? Try putting", + "search.results.no-results": "Sua busca não trouxe resultados. Tendo problema em localizar o que está buscando? Tente", // "search.results.no-results-link": "quotes around it", - // TODO New key - Add a translation - "search.results.no-results-link": "quotes around it", + "search.results.no-results-link": "envolver entre aspas", // "search.sidebar.close": "Back to results", - // TODO New key - Add a translation - "search.sidebar.close": "Back to results", + "search.sidebar.close": "Voltar para os resultados", // "search.sidebar.filters.title": "Filters", - // TODO New key - Add a translation - "search.sidebar.filters.title": "Filters", + "search.sidebar.filters.title": "Filtros", // "search.sidebar.open": "Search Tools", - // TODO New key - Add a translation - "search.sidebar.open": "Search Tools", + "search.sidebar.open": "Ferramentas de busca", // "search.sidebar.results": "results", - // TODO New key - Add a translation - "search.sidebar.results": "results", + "search.sidebar.results": "resultados", // "search.sidebar.settings.rpp": "Results per page", - // TODO New key - Add a translation - "search.sidebar.settings.rpp": "Results per page", + "search.sidebar.settings.rpp": "Resultados por página", // "search.sidebar.settings.sort-by": "Sort By", - // TODO New key - Add a translation - "search.sidebar.settings.sort-by": "Sort By", + "search.sidebar.settings.sort-by": "Ordenar por", // "search.sidebar.settings.title": "Settings", - // TODO New key - Add a translation - "search.sidebar.settings.title": "Settings", + "search.sidebar.settings.title": "Configurações", // "search.view-switch.show-detail": "Show detail", - // TODO New key - Add a translation - "search.view-switch.show-detail": "Show detail", + "search.view-switch.show-detail": "Mostrar detalhes", // "search.view-switch.show-grid": "Show as grid", - // TODO New key - Add a translation - "search.view-switch.show-grid": "Show as grid", + "search.view-switch.show-grid": "Mostrar como grade", // "search.view-switch.show-list": "Show as list", - // TODO New key - Add a translation - "search.view-switch.show-list": "Show as list", + "search.view-switch.show-list": "Mostrar como lista", // "sorting.dc.title.ASC": "Title Ascending", - // TODO New key - Add a translation - "sorting.dc.title.ASC": "Title Ascending", + "sorting.dc.title.ASC": "Título Ascendente", // "sorting.dc.title.DESC": "Title Descending", - // TODO New key - Add a translation - "sorting.dc.title.DESC": "Title Descending", + "sorting.dc.title.DESC": "Título Descendente", // "sorting.score.DESC": "Relevance", - // TODO New key - Add a translation - "sorting.score.DESC": "Relevance", + "sorting.score.DESC": "Relevância", // "submission.edit.title": "Edit Submission", - // TODO New key - Add a translation - "submission.edit.title": "Edit Submission", + "submission.edit.title": "Editar Submissão", // "submission.general.cannot_submit": "You have not the privilege to make a new submission.", - // TODO New key - Add a translation - "submission.general.cannot_submit": "You have not the privilege to make a new submission.", + "submission.general.cannot_submit": "Você mão tem privilégios para fazer uma nova submissão.", // "submission.general.deposit": "Deposit", - // TODO New key - Add a translation - "submission.general.deposit": "Deposit", + "submission.general.deposit": "Depositar", // "submission.general.discard.confirm.cancel": "Cancel", - // TODO New key - Add a translation - "submission.general.discard.confirm.cancel": "Cancel", + "submission.general.discard.confirm.cancel": "Cancelar", // "submission.general.discard.confirm.info": "This operation can't be undone. Are you sure?", - // TODO New key - Add a translation - "submission.general.discard.confirm.info": "This operation can't be undone. Are you sure?", + "submission.general.discard.confirm.info": "Esta operação não pode ser desfeita. Tem certeza?", // "submission.general.discard.confirm.submit": "Yes, I'm sure", - // TODO New key - Add a translation - "submission.general.discard.confirm.submit": "Yes, I'm sure", + "submission.general.discard.confirm.submit": "Sim, tenho certeza", // "submission.general.discard.confirm.title": "Discard submission", - // TODO New key - Add a translation - "submission.general.discard.confirm.title": "Discard submission", + "submission.general.discard.confirm.title": "Descartar submissão", // "submission.general.discard.submit": "Discard", - // TODO New key - Add a translation - "submission.general.discard.submit": "Discard", + "submission.general.discard.submit": "Descartar", // "submission.general.save": "Save", - // TODO New key - Add a translation - "submission.general.save": "Save", + "submission.general.save": "Salvar", // "submission.general.save-later": "Save for later", - // TODO New key - Add a translation - "submission.general.save-later": "Save for later", + "submission.general.save-later": "Salvar para continuar depois", // "submission.sections.general.add-more": "Add more", - // TODO New key - Add a translation - "submission.sections.general.add-more": "Add more", + "submission.sections.general.add-more": "Adicionar mais", // "submission.sections.general.collection": "Collection", - // TODO New key - Add a translation - "submission.sections.general.collection": "Collection", + "submission.sections.general.collection": "Coleção", // "submission.sections.general.deposit_error_notice": "There was an issue when submitting the item, please try again later.", - // TODO New key - Add a translation - "submission.sections.general.deposit_error_notice": "There was an issue when submitting the item, please try again later.", + "submission.sections.general.deposit_error_notice": "Houve um problema durante a submissão do item, por favor tente novamente mais tarde.", // "submission.sections.general.deposit_success_notice": "Submission deposited successfully.", - // TODO New key - Add a translation - "submission.sections.general.deposit_success_notice": "Submission deposited successfully.", + "submission.sections.general.deposit_success_notice": "Submissão depositada com sucesso.", // "submission.sections.general.discard_error_notice": "There was an issue when discarding the item, please try again later.", - // TODO New key - Add a translation - "submission.sections.general.discard_error_notice": "There was an issue when discarding the item, please try again later.", + "submission.sections.general.discard_error_notice": "Houve um problema ao descartar o item, por favor tente novamente mais tarde.", // "submission.sections.general.discard_success_notice": "Submission discarded successfully.", - // TODO New key - Add a translation - "submission.sections.general.discard_success_notice": "Submission discarded successfully.", + "submission.sections.general.discard_success_notice": "Submissão descartada com sucesso.", // "submission.sections.general.metadata-extracted": "New metadata have been extracted and added to the {{sectionId}} section.", - // TODO New key - Add a translation - "submission.sections.general.metadata-extracted": "New metadata have been extracted and added to the {{sectionId}} section.", + "submission.sections.general.metadata-extracted": "Novos metadados foram extraídos e adicionados a seção {{sectionId}}.", // "submission.sections.general.metadata-extracted-new-section": "New {{sectionId}} section has been added to submission.", - // TODO New key - Add a translation - "submission.sections.general.metadata-extracted-new-section": "New {{sectionId}} section has been added to submission.", + "submission.sections.general.metadata-extracted-new-section": "Nova seção {{sectionId}} foi adicionada a dubmissão.", // "submission.sections.general.no-collection": "No collection found", - // TODO New key - Add a translation - "submission.sections.general.no-collection": "No collection found", + "submission.sections.general.no-collection": "Nenhuma coleção encontrada", // "submission.sections.general.no-sections": "No options available", - // TODO New key - Add a translation - "submission.sections.general.no-sections": "No options available", + "submission.sections.general.no-sections": "Sem opções disponíveis", // "submission.sections.general.save_error_notice": "There was an issue when saving the item, please try again later.", - // TODO New key - Add a translation - "submission.sections.general.save_error_notice": "There was an issue when saving the item, please try again later.", + "submission.sections.general.save_error_notice": "Houve um problema ao salvar o item, por favor tente novamente mais tarde.", // "submission.sections.general.save_success_notice": "Submission saved successfully.", - // TODO New key - Add a translation - "submission.sections.general.save_success_notice": "Submission saved successfully.", + "submission.sections.general.save_success_notice": "Submissão salva com sucesso.", // "submission.sections.general.search-collection": "Search for a collection", - // TODO New key - Add a translation - "submission.sections.general.search-collection": "Search for a collection", + "submission.sections.general.search-collection": "Buscar uma coleção", // "submission.sections.general.sections_not_valid": "There are incomplete sections.", - // TODO New key - Add a translation - "submission.sections.general.sections_not_valid": "There are incomplete sections.", + "submission.sections.general.sections_not_valid": "Há seções incompletas.", // "submission.sections.submit.progressbar.cclicense": "Creative commons license", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.cclicense": "Creative commons license", + "submission.sections.submit.progressbar.cclicense": "Licença creative commons", // "submission.sections.submit.progressbar.describe.recycle": "Recycle", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.describe.recycle": "Recycle", + "submission.sections.submit.progressbar.describe.recycle": "Reciclar", // "submission.sections.submit.progressbar.describe.stepcustom": "Describe", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.describe.stepcustom": "Describe", + "submission.sections.submit.progressbar.describe.stepcustom": "Descrever", // "submission.sections.submit.progressbar.describe.stepone": "Describe", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.describe.stepone": "Describe", + "submission.sections.submit.progressbar.describe.stepone": "Descrever", // "submission.sections.submit.progressbar.describe.steptwo": "Describe", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.describe.steptwo": "Describe", + "submission.sections.submit.progressbar.describe.steptwo": "Descrever", // "submission.sections.submit.progressbar.detect-duplicate": "Potential duplicates", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.detect-duplicate": "Potential duplicates", + "submission.sections.submit.progressbar.detect-duplicate": "Duplicados em potencial", // "submission.sections.submit.progressbar.license": "Deposit license", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.license": "Deposit license", + "submission.sections.submit.progressbar.license": "Depositar licença", // "submission.sections.submit.progressbar.upload": "Upload files", - // TODO New key - Add a translation - "submission.sections.submit.progressbar.upload": "Upload files", + "submission.sections.submit.progressbar.upload": "Enviar arquivos", // "submission.sections.upload.delete.confirm.cancel": "Cancel", - // TODO New key - Add a translation - "submission.sections.upload.delete.confirm.cancel": "Cancel", + "submission.sections.upload.delete.confirm.cancel": "Cancelar", // "submission.sections.upload.delete.confirm.info": "This operation can't be undone. Are you sure?", - // TODO New key - Add a translation - "submission.sections.upload.delete.confirm.info": "This operation can't be undone. Are you sure?", + "submission.sections.upload.delete.confirm.info": "Esta operação é irreversível. Tem certeza?", // "submission.sections.upload.delete.confirm.submit": "Yes, I'm sure", - // TODO New key - Add a translation - "submission.sections.upload.delete.confirm.submit": "Yes, I'm sure", + "submission.sections.upload.delete.confirm.submit": "Sim, tenho certeza", // "submission.sections.upload.delete.confirm.title": "Delete bitstream", - // TODO New key - Add a translation - "submission.sections.upload.delete.confirm.title": "Delete bitstream", + "submission.sections.upload.delete.confirm.title": "Remover bitstream", // "submission.sections.upload.delete.submit": "Delete", - // TODO New key - Add a translation - "submission.sections.upload.delete.submit": "Delete", + "submission.sections.upload.delete.submit": "Remover", // "submission.sections.upload.drop-message": "Drop files to attach them to the item", - // TODO New key - Add a translation - "submission.sections.upload.drop-message": "Drop files to attach them to the item", + "submission.sections.upload.drop-message": "Arraste arquivos para anexá-los ao item", // "submission.sections.upload.form.access-condition-label": "Access condition type", - // TODO New key - Add a translation - "submission.sections.upload.form.access-condition-label": "Access condition type", + "submission.sections.upload.form.access-condition-label": "Tipo de condição de acesso", // "submission.sections.upload.form.date-required": "Date is required.", - // TODO New key - Add a translation - "submission.sections.upload.form.date-required": "Date is required.", + "submission.sections.upload.form.date-required": "Data necessária.", // "submission.sections.upload.form.from-label": "Access grant from", - // TODO New key - Add a translation - "submission.sections.upload.form.from-label": "Access grant from", + "submission.sections.upload.form.from-label": "Acesso permitido a partir de", // "submission.sections.upload.form.from-placeholder": "From", - // TODO New key - Add a translation - "submission.sections.upload.form.from-placeholder": "From", + "submission.sections.upload.form.from-placeholder": "De", // "submission.sections.upload.form.group-label": "Group", - // TODO New key - Add a translation - "submission.sections.upload.form.group-label": "Group", + "submission.sections.upload.form.group-label": "Grupo", // "submission.sections.upload.form.group-required": "Group is required.", - // TODO New key - Add a translation - "submission.sections.upload.form.group-required": "Group is required.", + "submission.sections.upload.form.group-required": "Grupo é necessário.", // "submission.sections.upload.form.until-label": "Access grant until", - // TODO New key - Add a translation - "submission.sections.upload.form.until-label": "Access grant until", + "submission.sections.upload.form.until-label": "Acesso permitido até", // "submission.sections.upload.form.until-placeholder": "Until", - // TODO New key - Add a translation - "submission.sections.upload.form.until-placeholder": "Until", + "submission.sections.upload.form.until-placeholder": "Até", // "submission.sections.upload.header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):", - // TODO New key - Add a translation - "submission.sections.upload.header.policy.default.nolist": "Uploaded files in the {{collectionName}} collection will be accessible according to the following group(s):", + "submission.sections.upload.header.policy.default.nolist": "Arquivos enviados na coleção {{collectionName}} serão acessiveis de acordo com o(s) seguinte(s) grupo(s):", // "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", - // TODO New key - Add a translation - "submission.sections.upload.header.policy.default.withlist": "Please note that uploaded files in the {{collectionName}} collection will be accessible, in addition to what is explicitly decided for the single file, with the following group(s):", + "submission.sections.upload.header.policy.default.withlist": "Por favor note que arquivos enviados a coleção {{collectionName}} serão acessíveis, de acordo com o que está explicitamente definido no arquivo, no(s) seguinte(s) grupo(s):", - // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", - // TODO New key - Add a translation - "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the fle metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", + // "submission.sections.upload.info": "Here you will find all the files currently in the item. You can update the file metadata and access conditions or upload additional files just dragging & dropping them everywhere in the page", + "submission.sections.upload.info": "Aqui voCẽ encontra todos os arquivos que estão atualmente no item. Você pode atualizar os metadados do arquivo e condições de acesso ou enviar arquivos adicionais apenas arrastando os arquivos em qualquer lugar da página", // "submission.sections.upload.no-entry": "No", - // TODO New key - Add a translation - "submission.sections.upload.no-entry": "No", + "submission.sections.upload.no-entry": "Não", // "submission.sections.upload.no-file-uploaded": "No file uploaded yet.", - // TODO New key - Add a translation - "submission.sections.upload.no-file-uploaded": "No file uploaded yet.", + "submission.sections.upload.no-file-uploaded": "Nenhum arquivo enviado ainda.", // "submission.sections.upload.save-metadata": "Save metadata", - // TODO New key - Add a translation - "submission.sections.upload.save-metadata": "Save metadata", + "submission.sections.upload.save-metadata": "Salvar metadados", // "submission.sections.upload.undo": "Cancel", - // TODO New key - Add a translation - "submission.sections.upload.undo": "Cancel", + "submission.sections.upload.undo": "Cancelar", // "submission.sections.upload.upload-failed": "Upload failed", - // TODO New key - Add a translation - "submission.sections.upload.upload-failed": "Upload failed", + "submission.sections.upload.upload-failed": "Falha no envio", // "submission.sections.upload.upload-successful": "Upload successful", - // TODO New key - Add a translation - "submission.sections.upload.upload-successful": "Upload successful", + "submission.sections.upload.upload-successful": "Enviado com sucesso", // "submission.submit.title": "Submission", - // TODO New key - Add a translation - "submission.submit.title": "Submission", + "submission.submit.title": "Submissão", // "submission.workflow.generic.delete": "Delete", - // TODO New key - Add a translation - "submission.workflow.generic.delete": "Delete", + "submission.workflow.generic.delete": "Apagar", // "submission.workflow.generic.delete-help": "If you would to discard this item, select \"Delete\". You will then be asked to confirm it.", - // TODO New key - Add a translation - "submission.workflow.generic.delete-help": "If you would to discard this item, select \"Delete\". You will then be asked to confirm it.", + "submission.workflow.generic.delete-help": "Se você gostaria de descartar este item, selecione \"Apagar\". Você será questionado para confirmar.", // "submission.workflow.generic.edit": "Edit", - // TODO New key - Add a translation - "submission.workflow.generic.edit": "Edit", + "submission.workflow.generic.edit": "Editar", // "submission.workflow.generic.edit-help": "Select this option to change the item's metadata.", - // TODO New key - Add a translation - "submission.workflow.generic.edit-help": "Select this option to change the item's metadata.", + "submission.workflow.generic.edit-help": "Selecione esta opção para modificar os metadados do item.", // "submission.workflow.generic.view": "View", - // TODO New key - Add a translation - "submission.workflow.generic.view": "View", + "submission.workflow.generic.view": "Visualizar", // "submission.workflow.generic.view-help": "Select this option to view the item's metadata.", - // TODO New key - Add a translation - "submission.workflow.generic.view-help": "Select this option to view the item's metadata.", + "submission.workflow.generic.view-help": "Selecione esta opção para ver o metadados do item.", // "submission.workflow.tasks.claimed.approve": "Approve", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.approve": "Approve", + "submission.workflow.tasks.claimed.approve": "Aprovar", // "submission.workflow.tasks.claimed.approve_help": "If you have reviewed the item and it is suitable for inclusion in the collection, select \"Approve\".", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.approve_help": "If you have reviewed the item and it is suitable for inclusion in the collection, select \"Approve\".", + "submission.workflow.tasks.claimed.approve_help": "Se você revisou o item e este está adequado para inclusão na coleção, selecione \"Aprovar\".", // "submission.workflow.tasks.claimed.edit": "Edit", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.edit": "Edit", + "submission.workflow.tasks.claimed.edit": "Editar", // "submission.workflow.tasks.claimed.edit_help": "Select this option to change the item's metadata.", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.edit_help": "Select this option to change the item's metadata.", + "submission.workflow.tasks.claimed.edit_help": "Selecione esta opção para modificar os metadados do item.", // "submission.workflow.tasks.claimed.reject.reason.info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.reason.info": "Please enter your reason for rejecting the submission into the box below, indicating whether the submitter may fix a problem and resubmit.", + "submission.workflow.tasks.claimed.reject.reason.info": "Por favor informe o motivo pela rejeição da submissão na caixa abaixo, indicando se o submetedor pode corrigir um problema e reenviar.", // "submission.workflow.tasks.claimed.reject.reason.placeholder": "Describe the reason of reject", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.reason.placeholder": "Describe the reason of reject", + "submission.workflow.tasks.claimed.reject.reason.placeholder": "Descreva o motivo da rejeição", // "submission.workflow.tasks.claimed.reject.reason.submit": "Reject item", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.reason.submit": "Reject item", + "submission.workflow.tasks.claimed.reject.reason.submit": "Rejeitar item", // "submission.workflow.tasks.claimed.reject.reason.title": "Reason", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.reason.title": "Reason", + "submission.workflow.tasks.claimed.reject.reason.title": "Motivo", // "submission.workflow.tasks.claimed.reject.submit": "Reject", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject.submit": "Reject", + "submission.workflow.tasks.claimed.reject.submit": "Rejeitar", // "submission.workflow.tasks.claimed.reject_help": "If you have reviewed the item and found it is not suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.reject_help": "If you have reviewed the item and found it is not suitable for inclusion in the collection, select \"Reject\". You will then be asked to enter a message indicating why the item is unsuitable, and whether the submitter should change something and resubmit.", + "submission.workflow.tasks.claimed.reject_help": "Se você revisou o item e achou que ele não é adequado para inclusão na coleção, selecione \"Rejeitar\". Você será questionado a informar uma mensagem indicando porque o item está inadequado e se o submetedor deve modificar algo e reenviar.", // "submission.workflow.tasks.claimed.return": "Return to pool", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.return": "Return to pool", + "submission.workflow.tasks.claimed.return": "Retornar para o conjunto", // "submission.workflow.tasks.claimed.return_help": "Return the task to the pool so that another user may perform the task.", - // TODO New key - Add a translation - "submission.workflow.tasks.claimed.return_help": "Return the task to the pool so that another user may perform the task.", + "submission.workflow.tasks.claimed.return_help": "Retornar a tarefa para o conjunto para que outra pessoa possa a fazer.", // "submission.workflow.tasks.generic.error": "Error occurred during operation...", - // TODO New key - Add a translation - "submission.workflow.tasks.generic.error": "Error occurred during operation...", + "submission.workflow.tasks.generic.error": "Ocorreu um erro durante a operação...", // "submission.workflow.tasks.generic.processing": "Processing...", - // TODO New key - Add a translation - "submission.workflow.tasks.generic.processing": "Processing...", + "submission.workflow.tasks.generic.processing": "Processando...", // "submission.workflow.tasks.generic.submitter": "Submitter", - // TODO New key - Add a translation - "submission.workflow.tasks.generic.submitter": "Submitter", + "submission.workflow.tasks.generic.submitter": "Submetedor", // "submission.workflow.tasks.generic.success": "Operation successful", - // TODO New key - Add a translation - "submission.workflow.tasks.generic.success": "Operation successful", + "submission.workflow.tasks.generic.success": "Operação realizada com sucesso", // "submission.workflow.tasks.pool.claim": "Claim", - // TODO New key - Add a translation - "submission.workflow.tasks.pool.claim": "Claim", + "submission.workflow.tasks.pool.claim": "Requerer", // "submission.workflow.tasks.pool.claim_help": "Assign this task to yourself.", - // TODO New key - Add a translation - "submission.workflow.tasks.pool.claim_help": "Assign this task to yourself.", + "submission.workflow.tasks.pool.claim_help": "Atribua esta tarefa a si mesmo.", // "submission.workflow.tasks.pool.hide-detail": "Hide detail", - // TODO New key - Add a translation - "submission.workflow.tasks.pool.hide-detail": "Hide detail", + "submission.workflow.tasks.pool.hide-detail": "Ocultar detalhes", // "submission.workflow.tasks.pool.show-detail": "Show detail", - // TODO New key - Add a translation - "submission.workflow.tasks.pool.show-detail": "Show detail", + "submission.workflow.tasks.pool.show-detail": "Mostrar detalhes", // "title": "DSpace", - // TODO New key - Add a translation "title": "DSpace", // "uploader.browse": "browse", - // TODO New key - Add a translation - "uploader.browse": "browse", + "uploader.browse": "Navegar", // "uploader.drag-message": "Drag & Drop your files here", - // TODO New key - Add a translation - "uploader.drag-message": "Drag & Drop your files here", + "uploader.drag-message": "Clique e arraste seus arquivos aqui", // "uploader.or": ", or", - // TODO New key - Add a translation - "uploader.or": ", or", + "uploader.or": ", ou", // "uploader.processing": "Processing", - // TODO New key - Add a translation - "uploader.processing": "Processing", + "uploader.processing": "Processando", // "uploader.queue-length": "Queue length", - // TODO New key - Add a translation - "uploader.queue-length": "Queue length", + "uploader.queue-length": "Tamanho da fila", 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-sidebar/admin-sidebar.component.ts b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts index 3ad1bd4272..185d083764 100644 --- a/src/app/+admin/admin-sidebar/admin-sidebar.component.ts +++ b/src/app/+admin/admin-sidebar/admin-sidebar.component.ts @@ -13,7 +13,6 @@ 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'; 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..12d5c200fd 100644 --- a/src/app/+collection-page/collection-page.component.html +++ b/src/app/+collection-page/collection-page.component.html @@ -3,6 +3,7 @@ *ngVar="(collectionRD$ | async) as collectionRD">
+
+
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/+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..1b915ae683 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..1145d1d013 --- /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/+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/item-relationships.component.spec.ts b/src/app/+item-page/edit-item-page/item-relationships/item-relationships.component.spec.ts index 48bc28a1b9..37745ec96a 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 @@ -156,7 +156,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/research-entities.module.ts b/src/app/entity-groups/research-entities/research-entities.module.ts index 8829318f34..86c2a375da 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,11 @@ 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'; const ENTRY_COMPONENTS = [ OrgUnitComponent, @@ -38,7 +43,12 @@ const ENTRY_COMPONENTS = [ ProjectSearchResultListElementComponent, PersonSearchResultGridElementComponent, OrgUnitSearchResultGridElementComponent, - ProjectSearchResultGridElementComponent + ProjectSearchResultGridElementComponent, + PersonSearchResultListSubmissionElementComponent, + PersonInputSuggestionsComponent, + NameVariantModalComponent, + OrgUnitSearchResultListSubmissionElementComponent, + OrgUnitInputSuggestionsComponent ]; @NgModule({ @@ -49,7 +59,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/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..75817d786a --- /dev/null +++ b/src/app/entity-groups/research-entities/submission/name-variant-modal/name-variant-modal.component.ts @@ -0,0 +1,18 @@ +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'] +}) +export class NameVariantModalComponent { + @Input() value: string; + + constructor(public modal: NgbActiveModal) { + } +} 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/shared/dso-selector/dso-selector/dso-selector.component.spec.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts index 234f13f4b1..2de59d614b 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.spec.ts @@ -2,14 +2,14 @@ import { async, ComponentFixture, TestBed } from '@angular/core/testing'; import { TranslateModule } from '@ngx-translate/core'; import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; import { DSOSelectorComponent } from './dso-selector.component'; -import { SearchService } from '../../../+search-page/search-service/search.service'; +import { SearchService } from '../../../core/shared/search/search.service'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; -import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model'; import { ItemSearchResult } from '../../object-collection/shared/item-search-result.model'; import { Item } from '../../../core/shared/item.model'; import { PaginatedList } from '../../../core/data/paginated-list'; import { MetadataValue } from '../../../core/shared/metadata.models'; import { createSuccessfulRemoteDataObject$ } from '../../testing/utils'; +import { PaginatedSearchOptions } from '../../search/paginated-search-options.model'; describe('DSOSelectorComponent', () => { let component: DSOSelectorComponent; diff --git a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts index af26f3f04f..3c9d399f8b 100644 --- a/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts +++ b/src/app/shared/dso-selector/dso-selector/dso-selector.component.ts @@ -12,12 +12,12 @@ import { FormControl } from '@angular/forms'; import { Observable } from 'rxjs'; import { debounceTime, startWith, switchMap } from 'rxjs/operators'; -import { SearchService } from '../../../+search-page/search-service/search.service'; -import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model'; +import { SearchService } from '../../../core/shared/search/search.service'; +import { PaginatedSearchOptions } from '../../search/paginated-search-options.model'; import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model'; import { RemoteData } from '../../../core/data/remote-data'; import { PaginatedList } from '../../../core/data/paginated-list'; -import { SearchResult } from '../../../+search-page/search-result.model'; +import { SearchResult } from '../../search/search-result.model'; import { DSpaceObject } from '../../../core/shared/dspace-object.model'; import { ViewMode } from '../../../core/shared/view-mode.model'; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html index 52a924604f..144848b478 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.html @@ -9,37 +9,60 @@ -
-
+
+
- + - + -
- {{ message | translate:model.validators }} -
+
+ {{ message | translate:model.validators }} +
+
+ +
+ +
+ +
+ +
-
- -
-
- + +
+
    +
  • + + + + +
  • +
+
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts index 13a9ba4e85..91c1dbc085 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.spec.ts @@ -1,5 +1,5 @@ import { async, ComponentFixture, inject, TestBed } from '@angular/core/testing'; -import { CUSTOM_ELEMENTS_SCHEMA, DebugElement, SimpleChange } from '@angular/core'; +import { CUSTOM_ELEMENTS_SCHEMA, DebugElement, NgZone, SimpleChange } from '@angular/core'; import { FormControl, FormGroup, FormsModule, ReactiveFormsModule } from '@angular/forms'; import { BrowserDynamicTestingModule } from '@angular/platform-browser-dynamic/testing'; import { By } from '@angular/platform-browser'; @@ -39,10 +39,7 @@ import { } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; -import { - DsDynamicFormControlContainerComponent, - dsDynamicFormControlMapFn -} from './ds-dynamic-form-control-container.component'; +import { DsDynamicFormControlContainerComponent, dsDynamicFormControlMapFn } from './ds-dynamic-form-control-container.component'; import { SharedModule } from '../../../shared.module'; import { DynamicDsDatePickerModel } from './models/date-picker/date-picker.model'; import { DynamicRelationGroupModel } from './models/relation-group/dynamic-relation-group.model'; @@ -65,6 +62,15 @@ import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-a import { DsDynamicFormGroupComponent } from './models/form-group/dynamic-form-group.component'; import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components'; import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component'; +import { RelationshipService } from '../../../../core/data/relationship.service'; +import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service'; +import { ItemDataService } from '../../../../core/data/item-data.service'; +import { Store } from '@ngrx/store'; +import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service'; +import { Item } from '../../../../core/shared/item.model'; +import { WorkspaceItem } from '../../../../core/submission/models/workspaceitem.model'; +import { of as observableOf } from 'rxjs'; +import { createSuccessfulRemoteDataObject } from '../../../testing/utils'; describe('DsDynamicFormControlContainerComponent test suite', () => { @@ -95,12 +101,15 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { new DynamicSwitchModel({ id: 'switch' }), new DynamicTextAreaModel({ id: 'textarea' }), new DynamicTimePickerModel({ id: 'timepicker' }), - new DynamicTypeaheadModel({ id: 'typeahead' }), + new DynamicTypeaheadModel({ id: 'typeahead', metadataFields: [], repeatable: false, submissionId: '1234' }), new DynamicScrollableDropdownModel({ id: 'scrollableDropdown', - authorityOptions: authorityOptions + authorityOptions: authorityOptions, + metadataFields: [], + repeatable: false, + submissionId: '1234' }), - new DynamicTagModel({ id: 'tag' }), + new DynamicTagModel({ id: 'tag', metadataFields: [], repeatable: false, submissionId: '1234' }), new DynamicListCheckboxGroupModel({ id: 'checkboxList', authorityOptions: authorityOptions, @@ -112,18 +121,21 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { repeatable: false }), new DynamicRelationGroupModel({ + submissionId: '1234', id: 'relationGroup', formConfiguration: [], mandatoryField: '', name: 'relationGroup', relationFields: [], scopeUUID: '', - submissionScope: '' + submissionScope: '', + repeatable: false, + metadataFields: [] }), new DynamicDsDatePickerModel({ id: 'datepicker' }), - new DynamicLookupModel({ id: 'lookup' }), - new DynamicLookupNameModel({ id: 'lookupName' }), - new DynamicQualdropModel({ id: 'combobox', readOnly: false }) + new DynamicLookupModel({ id: 'lookup', metadataFields: [], repeatable: false, submissionId: '1234' }), + new DynamicLookupNameModel({ id: 'lookupName', metadataFields: [], repeatable: false, submissionId: '1234' }), + new DynamicQualdropModel({ id: 'combobox', readOnly: false, required: false }) ]; const testModel = formModel[8]; let formGroup: FormGroup; @@ -131,7 +143,9 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { let component: DsDynamicFormControlContainerComponent; let debugElement: DebugElement; let testElement: DebugElement; - + const testItem: Item = new Item(); + const testWSI: WorkspaceItem = new WorkspaceItem(); + testWSI.item = observableOf(createSuccessfulRemoteDataObject(testItem)); beforeEach(async(() => { TestBed.overrideModule(BrowserDynamicTestingModule, { @@ -150,14 +164,34 @@ describe('DsDynamicFormControlContainerComponent test suite', () => { DynamicFormsCoreModule.forRoot(), SharedModule, TranslateModule.forRoot(), - TextMaskModule + TextMaskModule, + ], + providers: [ + DsDynamicFormControlContainerComponent, + DynamicFormService, + { provide: RelationshipService, useValue: {} }, + { provide: SelectableListService, useValue: {} }, + { provide: ItemDataService, useValue: {} }, + { provide: Store, useValue: {} }, + { provide: RelationshipService, useValue: {} }, + { provide: SelectableListService, useValue: {} }, + { + provide: SubmissionObjectDataService, + useValue: { + findById: () => observableOf(createSuccessfulRemoteDataObject(testWSI)) + } + }, + { provide: NgZone, useValue: new NgZone({}) } ], - providers: [DsDynamicFormControlContainerComponent, DynamicFormService], schemas: [CUSTOM_ELEMENTS_SCHEMA] }).compileComponents().then(() => { fixture = TestBed.createComponent(DsDynamicFormControlContainerComponent); + const ngZone = TestBed.get(NgZone); + + // tslint:disable-next-line:ban-types + spyOn(ngZone, 'runOutsideAngular').and.callFake((fn: Function) => fn()); component = fixture.componentInstance; debugElement = fixture.debugElement; }); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts index 455c1075ef..c85ef11e5a 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/ds-dynamic-form-control-container.component.ts @@ -5,7 +5,9 @@ import { ContentChildren, EventEmitter, Input, - OnChanges, + NgZone, + OnChanges, OnDestroy, + OnInit, Output, QueryList, SimpleChanges, @@ -47,6 +49,7 @@ import { DynamicNGBootstrapTimePickerComponent } from '@ng-dynamic-forms/ui-ng-bootstrap'; import { TranslateService } from '@ngx-translate/core'; +import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model'; import { DYNAMIC_FORM_CONTROL_TYPE_TYPEAHEAD } from './models/typeahead/dynamic-typeahead.model'; import { DYNAMIC_FORM_CONTROL_TYPE_SCROLLABLE_DROPDOWN } from './models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; @@ -55,7 +58,7 @@ import { DYNAMIC_FORM_CONTROL_TYPE_DSDATEPICKER } from './models/date-picker/dat import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP } from './models/lookup/dynamic-lookup.model'; import { DynamicListCheckboxGroupModel } from './models/list/dynamic-list-checkbox-group.model'; import { DynamicListRadioGroupModel } from './models/list/dynamic-list-radio-group.model'; -import { isNotEmpty, isNotUndefined } from '../../../empty.util'; +import { hasValue, isNotEmpty, isNotUndefined } from '../../../empty.util'; import { DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME } from './models/lookup/dynamic-lookup-name.model'; import { DsDynamicTagComponent } from './models/tag/dynamic-tag.component'; import { DsDatePickerComponent } from './models/date-picker/date-picker.component'; @@ -68,10 +71,37 @@ import { DsDynamicFormArrayComponent } from './models/array-group/dynamic-form-a import { DsDynamicRelationGroupComponent } from './models/relation-group/dynamic-relation-group.components'; import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from './models/relation-group/dynamic-relation-group.model'; import { DsDatePickerInlineComponent } from './models/date-picker-inline/dynamic-date-picker-inline.component'; +import { map, switchMap, take, tap } from 'rxjs/operators'; +import { combineLatest as observableCombineLatest, Observable, Subscription } from 'rxjs'; +import { SelectableListState } from '../../../object-list/selectable-list/selectable-list.reducer'; +import { SearchResult } from '../../../search/search-result.model'; +import { DSpaceObject } from '../../../../core/shared/dspace-object.model'; +import { NgbModal, NgbModalRef } from '@ng-bootstrap/ng-bootstrap'; +import { RelationshipService } from '../../../../core/data/relationship.service'; +import { SelectableListService } from '../../../object-list/selectable-list/selectable-list.service'; +import { DsDynamicDisabledComponent } from './models/disabled/dynamic-disabled.component'; +import { DYNAMIC_FORM_CONTROL_TYPE_DISABLED } from './models/disabled/dynamic-disabled.model'; +import { DsDynamicLookupRelationModalComponent } from './relation-lookup-modal/dynamic-lookup-relation-modal.component'; +import { + getAllSucceededRemoteData, + getRemoteDataPayload, + getSucceededRemoteData +} from '../../../../core/shared/operators'; +import { RemoteData } from '../../../../core/data/remote-data'; +import { Item } from '../../../../core/shared/item.model'; +import { ItemDataService } from '../../../../core/data/item-data.service'; +import { RemoveRelationshipAction } from './relation-lookup-modal/relationship.actions'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../../../app.reducer'; +import { SubmissionObjectDataService } from '../../../../core/submission/submission-object-data.service'; +import { SubmissionObject } from '../../../../core/submission/models/submission-object.model'; +import { PaginatedList } from '../../../../core/data/paginated-list'; +import { ItemSearchResult } from '../../../object-collection/shared/item-search-result.model'; +import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model'; +import { MetadataValue } from '../../../../core/shared/metadata.models'; export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type | null { switch (model.type) { - case DYNAMIC_FORM_CONTROL_TYPE_ARRAY: return DsDynamicFormArrayComponent; @@ -125,6 +155,9 @@ export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type< case DYNAMIC_FORM_CONTROL_TYPE_LOOKUP_NAME: return DsDynamicLookupComponent; + case DYNAMIC_FORM_CONTROL_TYPE_DISABLED: + return DsDynamicDisabledComponent; + default: return null; } @@ -136,8 +169,7 @@ export function dsDynamicFormControlMapFn(model: DynamicFormControlModel): Type< templateUrl: './ds-dynamic-form-control-container.component.html', changeDetection: ChangeDetectionStrategy.Default }) -export class DsDynamicFormControlContainerComponent extends DynamicFormControlContainerComponent implements OnChanges { - +export class DsDynamicFormControlContainerComponent extends DynamicFormControlContainerComponent implements OnInit, OnChanges, OnDestroy { @ContentChildren(DynamicTemplateDirective) contentTemplateList: QueryList; // tslint:disable-next-line:no-input-rename @Input('templates') inputTemplateList: QueryList; @@ -150,6 +182,20 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo @Input() hasErrorMessaging = false; @Input() layout = null as DynamicFormLayout; @Input() model: any; + relationships$: Observable>>; + hasRelationLookup: boolean; + modalRef: NgbModalRef; + item: Item; + listId: string; + searchConfig: string; + selectedValues$: Observable, + mdRep: MetadataRepresentation + }>>; + /** + * List of subscriptions to unsubscribe from + */ + private subs: Subscription[] = []; /* tslint:disable:no-output-rename */ @Output('dfBlur') blur: EventEmitter = new EventEmitter(); @@ -157,7 +203,7 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo @Output('dfFocus') focus: EventEmitter = new EventEmitter(); @Output('ngbEvent') customEvent: EventEmitter = new EventEmitter(); /* tslint:enable:no-output-rename */ - @ViewChild('componentViewContainer', {read: ViewContainerRef}) componentViewContainerRef: ViewContainerRef; + @ViewChild('componentViewContainer', { read: ViewContainerRef }) componentViewContainerRef: ViewContainerRef; private showErrorMessagesPreviousStage: boolean; @@ -165,17 +211,69 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo return this.layoutService.getCustomComponentType(this.model) || dsDynamicFormControlMapFn(this.model); } - protected test: boolean; constructor( protected componentFactoryResolver: ComponentFactoryResolver, protected layoutService: DynamicFormLayoutService, protected validationService: DynamicFormValidationService, - protected translateService: TranslateService + protected translateService: TranslateService, + private modalService: NgbModal, + private relationService: RelationshipService, + private selectableListService: SelectableListService, + private itemService: ItemDataService, + private relationshipService: RelationshipService, + private zone: NgZone, + private store: Store, + private submissionObjectService: SubmissionObjectDataService ) { - super(componentFactoryResolver, layoutService, validationService); } + /** + * Sets up the necessary variables for when this control can be used to add relationships to the submitted item + */ + ngOnInit(): void { + this.hasRelationLookup = hasValue(this.model.relationship); + if (this.hasRelationLookup) { + this.listId = 'list-' + this.model.relationship.relationshipType; + const item$ = this.submissionObjectService + .findById(this.model.submissionId).pipe( + getAllSucceededRemoteData(), + getRemoteDataPayload(), + switchMap((submissionObject: SubmissionObject) => (submissionObject.item as Observable>).pipe(getAllSucceededRemoteData(), getRemoteDataPayload()))); + + this.subs.push(item$.subscribe((item) => this.item = item)); + + this.relationService.getRelatedItemsByLabel(this.item, this.model.relationship.relationshipType).pipe( + map((items: RemoteData>) => items.payload.page.map((item) => Object.assign(new ItemSearchResult(), { indexableObject: item }))), + ).subscribe((relatedItems: Array>) => this.selectableListService.select(this.listId, relatedItems)); + + this.relationships$ = this.selectableListService.getSelectableList(this.listId).pipe( + map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : []), + ) as Observable>>; + this.selectedValues$ = + observableCombineLatest(item$, this.relationships$).pipe( + map(([item, relatedItems]: [Item, Array>]) => { + return relatedItems + .map((element: SearchResult) => { + const relationMD: MetadataValue = item.firstMetadata(this.model.relationship.metadataField, { value: element.indexableObject.uuid }); + if (hasValue(relationMD)) { + const metadataRepresentationMD: MetadataValue = item.firstMetadata(this.model.metadataFields, { authority: relationMD.authority }); + return { + selectedResult: element, + mdRep: Object.assign( + new ItemMetadataRepresentation(metadataRepresentationMD), + element.indexableObject + ) + }; + } + }).filter(hasValue) + } + ) + ); + + } + } + ngOnChanges(changes: SimpleChanges) { if (changes) { super.ngOnChanges(changes); @@ -212,4 +310,42 @@ export class DsDynamicFormControlContainerComponent extends DynamicFormControlCo this.onChange(event); } } + + public hasResultsSelected(): Observable { + return this.model.value.pipe(map((list: Array>) => isNotEmpty(list))); + } + + /** + * Open a modal where the user can select relationships to be added to item being submitted + */ + openLookup() { + this.modalRef = this.modalService.open(DsDynamicLookupRelationModalComponent, { + size: 'lg' + }); + const modalComp = this.modalRef.componentInstance; + modalComp.repeatable = this.model.repeatable; + modalComp.listId = this.listId; + modalComp.relationshipOptions = this.model.relationship; + modalComp.label = this.model.label; + modalComp.metadataFields = this.model.metadataFields; + modalComp.item = this.item; + } + + /** + * Method to remove a selected relationship from the item + * @param object The second item in the relationship, the submitted item being the first + */ + removeSelection(object: SearchResult) { + this.selectableListService.deselectSingle(this.listId, object); + this.store.dispatch(new RemoveRelationshipAction(this.item, object.indexableObject, this.model.relationship.relationshipType)) + } + + /** + * Unsubscribe from all subscriptions + */ + ngOnDestroy(): void { + this.subs + .filter((sub) => hasValue(sub)) + .forEach((sub) => sub.unsubscribe()); + } } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.html new file mode 100644 index 0000000000..18fddd1446 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.html @@ -0,0 +1,13 @@ +
+
+
+ +
+
+
diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.spec.ts new file mode 100644 index 0000000000..8e0c6fc20e --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.spec.ts @@ -0,0 +1,58 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { DebugElement, NO_ERRORS_SCHEMA } from '@angular/core'; +import { DsDynamicDisabledComponent } from './dynamic-disabled.component'; +import { FormsModule } from '@angular/forms'; +import { DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; +import { DynamicDisabledModel } from './dynamic-disabled.model'; +import { By } from '@angular/platform-browser'; +import { TranslateModule } from '@ngx-translate/core'; + +describe('DsDynamicDisabledComponent', () => { + let comp: DsDynamicDisabledComponent; + let fixture: ComponentFixture; + let de: DebugElement; + let el: HTMLElement; + let model; + + function init() { + model = new DynamicDisabledModel({ value: 'test', repeatable: false, metadataFields: [], submissionId: '1234', id: '1' }); + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + declarations: [DsDynamicDisabledComponent], + imports: [FormsModule, TranslateModule.forRoot()], + providers: [ + { + provide: DynamicFormLayoutService, + useValue: {} + }, + { + provide: DynamicFormValidationService, + useValue: {} + } + ], + schemas: [NO_ERRORS_SCHEMA] + }).compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DsDynamicDisabledComponent); + comp = fixture.componentInstance; // DsDynamicDisabledComponent test instance + de = fixture.debugElement; + el = de.nativeElement; + comp.model = model; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(comp).toBeTruthy(); + }); + + it('should have a disabled input', () => { + const input = de.query(By.css('input')); + console.log(input.nativeElement.getAttribute('disabled')); + expect(input.nativeElement.getAttribute('disabled')).toEqual(''); + }); +}); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts new file mode 100644 index 0000000000..490be050ef --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component.ts @@ -0,0 +1,30 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +import { DynamicFormControlComponent, DynamicFormLayoutService, DynamicFormValidationService } from '@ng-dynamic-forms/core'; +import { FormGroup } from '@angular/forms'; +import { DynamicDisabledModel } from './dynamic-disabled.model'; + +/** + * Component representing a simple disabled input field + */ +@Component({ + selector: 'ds-dynamic-disabled', + templateUrl: './dynamic-disabled.component.html' +}) +export class DsDynamicDisabledComponent extends DynamicFormControlComponent { + + @Input() formId: string; + @Input() group: FormGroup; + @Input() model: DynamicDisabledModel; + modelValuesString = ''; + + @Output() blur: EventEmitter = new EventEmitter(); + @Output() change: EventEmitter = new EventEmitter(); + @Output() focus: EventEmitter = new EventEmitter(); + + constructor(protected layoutService: DynamicFormLayoutService, + protected validationService: DynamicFormValidationService + ) { + super(layoutService, validationService); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.model.ts new file mode 100644 index 0000000000..0fa2b3e5ed --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.model.ts @@ -0,0 +1,24 @@ +import { DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; +import { DsDynamicInputModel, DsDynamicInputModelConfig } from '../ds-dynamic-input.model'; + +export const DYNAMIC_FORM_CONTROL_TYPE_DISABLED = 'EMPTY'; + +export interface DsDynamicDisabledModelConfig extends DsDynamicInputModelConfig { + value?: any; +} + +/** + * This model represents the data for a disabled input field + */ +export class DynamicDisabledModel extends DsDynamicInputModel { + + @serializable() readonly type: string = DYNAMIC_FORM_CONTROL_TYPE_DISABLED; + @serializable() value: any; + + constructor(config: DsDynamicDisabledModelConfig, layout?: DynamicFormControlLayout) { + super(config, layout); + this.readOnly = true; + this.disabled = true; + this.valueUpdates.next(config.value); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts index 66bdf97dad..af05d5bf35 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-concat.model.ts @@ -5,6 +5,7 @@ import { Subject } from 'rxjs'; import { isNotEmpty } from '../../../../empty.util'; import { DsDynamicInputModel } from './ds-dynamic-input.model'; import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model'; +import { RelationshipOptions } from '../../models/relationship-options.model'; export const CONCAT_GROUP_SUFFIX = '_CONCAT_GROUP'; export const CONCAT_FIRST_INPUT_SUFFIX = '_CONCAT_FIRST_INPUT'; @@ -12,12 +13,24 @@ export const CONCAT_SECOND_INPUT_SUFFIX = '_CONCAT_SECOND_INPUT'; export interface DynamicConcatModelConfig extends DynamicFormGroupModelConfig { separator: string; + value?: any; + relationship?: RelationshipOptions; + repeatable: boolean; + required: boolean; + metadataFields: string[]; + submissionId: string; } export class DynamicConcatModel extends DynamicFormGroupModel { @serializable() separator: string; @serializable() hasLanguages = false; + @serializable() relationship?: RelationshipOptions; + @serializable() repeatable?: boolean; + @serializable() required?: boolean; + @serializable() metadataFields: string[]; + @serializable() submissionId: string; + isCustomGroup = true; valueUpdates: Subject; @@ -26,6 +39,11 @@ export class DynamicConcatModel extends DynamicFormGroupModel { super(config, layout); this.separator = config.separator + ' '; + this.relationship = config.relationship; + this.repeatable = config.repeatable; + this.required = config.required; + this.metadataFields = config.metadataFields; + this.submissionId = config.submissionId; this.valueUpdates = new Subject(); this.valueUpdates.subscribe((value: string) => this.value = value); @@ -49,7 +67,7 @@ export class DynamicConcatModel extends DynamicFormGroupModel { let tempValue: string; if (typeof value === 'string') { - tempValue = value; + tempValue = value; } else { tempValue = value.value; } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts index 4e4a944319..3827df7be6 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-input.model.ts @@ -1,21 +1,21 @@ -import { - DynamicFormControlLayout, - DynamicInputModel, - DynamicInputModelConfig, - serializable -} from '@ng-dynamic-forms/core'; +import { DynamicFormControlLayout, DynamicInputModel, DynamicInputModelConfig, serializable } from '@ng-dynamic-forms/core'; import { Subject } from 'rxjs'; import { LanguageCode } from '../../models/form-field-language-value.model'; import { AuthorityOptions } from '../../../../../core/integration/models/authority-options.model'; import { hasValue } from '../../../../empty.util'; import { FormFieldMetadataValueObject } from '../../models/form-field-metadata-value.model'; +import { RelationshipOptions } from '../../models/relationship-options.model'; export interface DsDynamicInputModelConfig extends DynamicInputModelConfig { authorityOptions?: AuthorityOptions; languageCodes?: LanguageCode[]; language?: string; value?: any; + relationship?: RelationshipOptions; + repeatable: boolean; + metadataFields: string[]; + submissionId: string; } export class DsDynamicInputModel extends DynamicInputModel { @@ -24,13 +24,21 @@ export class DsDynamicInputModel extends DynamicInputModel { @serializable() private _languageCodes: LanguageCode[]; @serializable() private _language: string; @serializable() languageUpdates: Subject; + @serializable() relationship?: RelationshipOptions; + @serializable() repeatable?: boolean; + @serializable() metadataFields: string[]; + @serializable() submissionId: string; constructor(config: DsDynamicInputModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); - + this.repeatable = config.repeatable; + this.metadataFields = config.metadataFields; this.hint = config.hint; this.readOnly = config.readOnly; this.value = config.value; + this.relationship = config.relationship; + this.submissionId = config.submissionId; + this.language = config.language; if (!this.language) { // TypeAhead @@ -79,5 +87,4 @@ export class DsDynamicInputModel extends DynamicInputModel { this.language = this.languageCodes ? this.languageCodes[0].code : null; } } - } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts index 5d2cbc58b7..a2ed83f6c1 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-qualdrop.model.ts @@ -12,6 +12,7 @@ export interface DsDynamicQualdropModelConfig extends DynamicFormGroupModelConfi languageCodes?: LanguageCode[]; language?: string; readOnly: boolean; + required: boolean; hint?: string; } @@ -22,12 +23,14 @@ export class DynamicQualdropModel extends DynamicFormGroupModel { @serializable() hasLanguages = false; @serializable() readOnly: boolean; @serializable() hint: string; + @serializable() required: boolean; isCustomGroup = true; constructor(config: DsDynamicQualdropModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); this.readOnly = config.readOnly; + this.required = config.required; this.language = config.language; this.languageCodes = config.languageCodes; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts index b91af8f0c9..7de319bf56 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/ds-dynamic-row-array-model.ts @@ -1,21 +1,18 @@ -import { - DYNAMIC_FORM_CONTROL_TYPE_ARRAY, - DynamicFormArrayModel, DynamicFormArrayModelConfig, DynamicFormControlLayout, - serializable -} from '@ng-dynamic-forms/core'; -import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './tag/dynamic-tag.model'; +import { DynamicFormArrayModel, DynamicFormArrayModelConfig, DynamicFormControlLayout, serializable } from '@ng-dynamic-forms/core'; export interface DynamicRowArrayModelConfig extends DynamicFormArrayModelConfig { notRepeatable: boolean; + required: boolean; } export class DynamicRowArrayModel extends DynamicFormArrayModel { @serializable() notRepeatable = false; + @serializable() required = false; isRowArray = true; constructor(config: DynamicRowArrayModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); this.notRepeatable = config.notRepeatable; + this.required = config.required; } - } diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts index 39c2c61efe..b0ed3a1dc2 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/lookup/dynamic-lookup.component.spec.ts @@ -27,6 +27,8 @@ import { AuthorityConfidenceStateDirective } from '../../../../../authority-conf import { ObjNgFor } from '../../../../../utils/object-ngfor.pipe'; import { GLOBAL_CONFIG, GlobalConfig } from '../../../../../../../config'; import { MOCK_SUBMISSION_CONFIG } from '../../../../../testing/mock-submission-config'; +import { WorkspaceitemsEditPageModule } from '../../../../../../+workspaceitems-edit-page/workspaceitems-edit-page.module'; +import { WorkspaceItem } from '../../../../../../core/submission/models/workspaceitem.model'; let LOOKUP_TEST_MODEL_CONFIG = { authorityOptions: { @@ -47,7 +49,9 @@ let LOOKUP_TEST_MODEL_CONFIG = { repeatable: true, separator: ',', validators: { required: null }, - value: undefined + value: undefined, + metadataFields: [], + submissionId: '1234' }; let LOOKUP_NAME_TEST_MODEL_CONFIG = { @@ -69,7 +73,9 @@ let LOOKUP_NAME_TEST_MODEL_CONFIG = { repeatable: true, separator: ',', validators: { required: null }, - value: undefined + value: undefined, + metadataFields: [], + submissionId: '1234' }; let LOOKUP_TEST_GROUP = new FormGroup({ @@ -100,7 +106,9 @@ describe('Dynamic Lookup component', () => { repeatable: true, separator: ',', validators: { required: null }, - value: undefined + value: undefined, + metadataFields: [], + submissionId: '1234' }; LOOKUP_NAME_TEST_MODEL_CONFIG = { @@ -122,7 +130,9 @@ describe('Dynamic Lookup component', () => { repeatable: true, separator: ',', validators: { required: null }, - value: undefined + value: undefined, + metadataFields: [], + submissionId: '1234' }; LOOKUP_TEST_GROUP = new FormGroup({ diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts index 6d5839f867..75d30d9d79 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.component.spec.ts @@ -33,6 +33,8 @@ export let FORM_GROUP_TEST_GROUP; const config: GlobalConfig = MOCK_SUBMISSION_CONFIG; +const submissionId = '1234'; + function init() { FORM_GROUP_TEST_MODEL_CONFIG = { disabled: false, @@ -67,6 +69,7 @@ function init() { }] } as FormFieldModel] } as FormRowModel], + submissionId, id: 'dc_contributor_author', label: 'Authors', mandatoryField: 'dc.contributor.author', @@ -77,7 +80,9 @@ function init() { required: true, scopeUUID: '43fe1f8c-09a6-4fcf-9c78-5d4fed8f2c8f', submissionScope: undefined, - validators: { required: null } + validators: { required: null }, + repeatable: false, + metadataFields: [] } as DynamicRelationGroupModelConfig; FORM_GROUP_TEST_GROUP = new FormGroup({ @@ -183,7 +188,7 @@ describe('DsDynamicRelationGroupComponent test suite', () => { it('should init component properly', inject([FormBuilderService], (service: FormBuilderService) => { const formConfig = { rows: groupComp.model.formConfiguration } as SubmissionFormsModel; - const formModel = service.modelFromConfiguration(formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly); + const formModel = service.modelFromConfiguration(submissionId, formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly); const chips = new Chips([], 'value', 'dc.contributor.author'); groupComp.formCollapsed.subscribe((value) => { expect(value).toEqual(false); @@ -257,11 +262,11 @@ describe('DsDynamicRelationGroupComponent test suite', () => { it('should init component properly', inject([FormBuilderService], (service: FormBuilderService) => { const formConfig = { rows: groupComp.model.formConfiguration } as SubmissionFormsModel; - const formModel = service.modelFromConfiguration(formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly); + const formModel = service.modelFromConfiguration(submissionId, formConfig, groupComp.model.scopeUUID, {}, groupComp.model.submissionScope, groupComp.model.readOnly); const chips = new Chips(modelValue, 'value', 'dc.contributor.author'); groupComp.formCollapsed.subscribe((value) => { expect(value).toEqual(true); - }) + }); expect(groupComp.formModel.length).toEqual(formModel.length); expect(groupComp.chips.getChipsItems()).toEqual(chips.getChipsItems()); })); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts index 62b6b4effa..ea62eeb4ce 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.components.ts @@ -93,6 +93,7 @@ export class DsDynamicRelationGroupComponent extends DynamicFormControlComponent this.formId = this.formService.getUniqueId(this.model.id); this.formModel = this.formBuilderService.modelFromConfiguration( + this.model.submissionId, config, this.model.scopeUUID, {}, diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model.ts index e6d2b95afc..c1f76f0431 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model.ts @@ -10,6 +10,7 @@ export const PLACEHOLDER_PARENT_METADATA = '#PLACEHOLDER_PARENT_METADATA_VALUE#' * Dynamic Group Model configuration interface */ export interface DynamicRelationGroupModelConfig extends DsDynamicInputModelConfig { + submissionId: string, formConfiguration: FormRowModel[], mandatoryField: string, relationFields: string[], @@ -21,6 +22,7 @@ export interface DynamicRelationGroupModelConfig extends DsDynamicInputModelConf * Dynamic Group Model class */ export class DynamicRelationGroupModel extends DsDynamicInputModel { + @serializable() submissionId: string; @serializable() formConfiguration: FormRowModel[]; @serializable() mandatoryField: string; @serializable() relationFields: string[]; @@ -32,6 +34,7 @@ export class DynamicRelationGroupModel extends DsDynamicInputModel { constructor(config: DynamicRelationGroupModelConfig, layout?: DynamicFormControlLayout) { super(config, layout); + this.submissionId = config.submissionId; this.formConfiguration = config.formConfiguration; this.mandatoryField = config.mandatoryField; this.relationFields = config.relationFields; diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts index 2bcb42a73a..ab923a58fa 100644 --- a/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.component.spec.ts @@ -38,7 +38,9 @@ export const SD_TEST_MODEL_CONFIG = { readOnly: false, required: false, repeatable: false, - value: undefined + value: undefined, + metadataFields: [], + submissionId: '1234' }; describe('Dynamic Dynamic Scrollable Dropdown component', () => { diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html new file mode 100644 index 0000000000..52f983e723 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.html @@ -0,0 +1,45 @@ + + + \ No newline at end of file diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.scss new file mode 100644 index 0000000000..4fb77a7590 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.scss @@ -0,0 +1,3 @@ +.modal-footer { + justify-content: space-between; +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts new file mode 100644 index 0000000000..a4f77fd364 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.spec.ts @@ -0,0 +1,129 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NgZone, NO_ERRORS_SCHEMA } from '@angular/core'; +import { of as observableOf, Subscription } from 'rxjs'; +import { DsDynamicLookupRelationModalComponent } from './dynamic-lookup-relation-modal.component'; +import { NgbActiveModal, NgbModule } from '@ng-bootstrap/ng-bootstrap'; +import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; +import { RelationshipService } from '../../../../../core/data/relationship.service'; +import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service'; +import { Store } from '@ngrx/store'; +import { Item } from '../../../../../core/shared/item.model'; +import { ItemSearchResult } from '../../../../object-collection/shared/item-search-result.model'; +import { RelationshipOptions } from '../../models/relationship-options.model'; +import { AddRelationshipAction, RemoveRelationshipAction } from './relationship.actions'; + +describe('DsDynamicLookupRelationModalComponent', () => { + let component: DsDynamicLookupRelationModalComponent; + let fixture: ComponentFixture; + let item; + let item1; + let item2; + let searchResult1; + let searchResult2; + let listID; + let selection$; + let selectableListService; + let relationship; + let nameVariant; + let metadataField; + + function init() { + item = Object.assign(new Item(), { uuid: '7680ca97-e2bd-4398-bfa7-139a8673dc42', metadata: {} }); + item1 = Object.assign(new Item(), { uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e' }); + item2 = Object.assign(new Item(), { uuid: 'c8279647-1acc-41ae-b036-951d5f65649b' }); + searchResult1 = Object.assign(new ItemSearchResult(), { indexableObject: item1 }); + searchResult2 = Object.assign(new ItemSearchResult(), { indexableObject: item2 }); + listID = '6b0c8221-fcb4-47a8-b483-ca32363fffb3'; + selection$ = observableOf([searchResult1, searchResult2]); + selectableListService = { getSelectableList: () => selection$ }; + relationship = { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true } as RelationshipOptions; + nameVariant = 'Doe, J.'; + metadataField = 'dc.contributor.author'; + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + declarations: [DsDynamicLookupRelationModalComponent], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([]), NgbModule.forRoot()], + providers: [ + { + provide: SelectableListService, useValue: selectableListService + }, + { + provide: RelationshipService, useValue: { getNameVariant: () => observableOf(nameVariant) } + }, + { provide: RelationshipTypeService, useValue: {} }, + { + provide: Store, useValue: { + // tslint:disable-next-line:no-empty + dispatch: () => {} + } + }, + { provide: NgZone, useValue: new NgZone({}) }, + NgbActiveModal + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DsDynamicLookupRelationModalComponent); + component = fixture.componentInstance; + component.listId = listID; + component.relationshipOptions = relationship; + component.item = item; + component.metadataFields = metadataField; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('close', () => { + beforeEach(() => { + spyOn(component.modal, 'close'); + }); + + it('should call close on the modal', () => { + component.close(); + expect(component.modal.close).toHaveBeenCalled(); + }) + }); + + describe('select', () => { + beforeEach(() => { + spyOn((component as any).store, 'dispatch'); + }); + + it('should dispatch an AddRelationshipAction for each selected object', () => { + component.select(searchResult1, searchResult2); + const action = new AddRelationshipAction(component.item, searchResult1.indexableObject, relationship.relationshipType, nameVariant); + const action2 = new AddRelationshipAction(component.item, searchResult2.indexableObject, relationship.relationshipType, nameVariant); + + expect((component as any).store.dispatch).toHaveBeenCalledWith(action); + expect((component as any).store.dispatch).toHaveBeenCalledWith(action2); + }) + }); + + describe('deselect', () => { + beforeEach(() => { + component.subMap[searchResult1.indexableObject.uuid] = new Subscription(); + component.subMap[searchResult2.indexableObject.uuid] = new Subscription(); + spyOn((component as any).store, 'dispatch'); + }); + + it('should dispatch an RemoveRelationshipAction for each deselected object', () => { + component.deselect(searchResult1, searchResult2); + const action = new RemoveRelationshipAction(component.item, searchResult1.indexableObject, relationship.relationshipType); + const action2 = new RemoveRelationshipAction(component.item, searchResult2.indexableObject, relationship.relationshipType); + + expect((component as any).store.dispatch).toHaveBeenCalledWith(action); + expect((component as any).store.dispatch).toHaveBeenCalledWith(action2); + }) + }); +}); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts new file mode 100644 index 0000000000..f3ed3337a9 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component.ts @@ -0,0 +1,157 @@ +import { Component, NgZone, OnDestroy, OnInit } from '@angular/core'; +import { combineLatest, Observable, Subscription } from 'rxjs'; +import { NgbActiveModal } from '@ng-bootstrap/ng-bootstrap'; +import { hasValue } from '../../../../empty.util'; +import { map, skip, switchMap, take } from 'rxjs/operators'; +import { SEARCH_CONFIG_SERVICE } from '../../../../../+my-dspace-page/my-dspace-page.component'; +import { SearchConfigurationService } from '../../../../../core/shared/search/search-configuration.service'; +import { SelectableListService } from '../../../../object-list/selectable-list/selectable-list.service'; +import { SelectableListState } from '../../../../object-list/selectable-list/selectable-list.reducer'; +import { ListableObject } from '../../../../object-collection/shared/listable-object.model'; +import { RelationshipOptions } from '../../models/relationship-options.model'; +import { SearchResult } from '../../../../search/search-result.model'; +import { Item } from '../../../../../core/shared/item.model'; +import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../../core/shared/operators'; +import { AddRelationshipAction, RemoveRelationshipAction, UpdateRelationshipAction } from './relationship.actions'; +import { RelationshipService } from '../../../../../core/data/relationship.service'; +import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service'; +import { Store } from '@ngrx/store'; +import { AppState } from '../../../../../app.reducer'; +import { Context } from '../../../../../core/shared/context.model'; +import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; + +@Component({ + selector: 'ds-dynamic-lookup-relation-modal', + styleUrls: ['./dynamic-lookup-relation-modal.component.scss'], + templateUrl: './dynamic-lookup-relation-modal.component.html', + providers: [ + { + provide: SEARCH_CONFIG_SERVICE, + useClass: SearchConfigurationService + } + ] +}) + +/** + * Represents a modal where the submitter can select items to be added as a certain relationship type to the object being submitted + */ +export class DsDynamicLookupRelationModalComponent implements OnInit, OnDestroy { + label: string; + relationshipOptions: RelationshipOptions; + listId: string; + item; + repeatable: boolean; + selection$: Observable; + context: Context; + metadataFields: string; + subMap: { + [uuid: string]: Subscription + } = {}; + + constructor( + public modal: NgbActiveModal, + private selectableListService: SelectableListService, + private relationshipService: RelationshipService, + private relationshipTypeService: RelationshipTypeService, + private zone: NgZone, + private store: Store + ) { + } + + ngOnInit(): void { + this.selection$ = this.selectableListService + .getSelectableList(this.listId) + .pipe(map((listState: SelectableListState) => hasValue(listState) && hasValue(listState.selection) ? listState.selection : [])); + this.selection$.pipe(take(1)).subscribe((selection) => + selection.map((s: SearchResult) => this.addNameVariantSubscription(s)) + ); + if (this.relationshipOptions.nameVariants) { + this.context = Context.SubmissionModal; + } + + // this.setExistingNameVariants(); + } + + close() { + this.modal.close(); + } + + select(...selectableObjects: Array>) { + this.zone.runOutsideAngular( + () => { + const obs: Observable = combineLatest(...selectableObjects.map((sri: SearchResult) => { + this.addNameVariantSubscription(sri); + return this.relationshipService.getNameVariant(this.listId, sri.indexableObject.uuid) + .pipe( + take(1), + map((nameVariant: string) => { + return { + item: sri.indexableObject, + nameVariant + } + }) + ) + }) + ); + obs + .subscribe((arr: any[]) => { + return arr.forEach((object: any) => { + this.store.dispatch(new AddRelationshipAction(this.item, object.item, this.relationshipOptions.relationshipType, object.nameVariant)); + } + ); + }) + }); + } + + private addNameVariantSubscription(sri: SearchResult) { + const nameVariant$ = this.relationshipService.getNameVariant(this.listId, sri.indexableObject.uuid); + this.subMap[sri.indexableObject.uuid] = nameVariant$.pipe( + skip(1), + ).subscribe((nameVariant: string) => this.store.dispatch(new UpdateRelationshipAction(this.item, sri.indexableObject, this.relationshipOptions.relationshipType, nameVariant))) + } + + deselect(...selectableObjects: Array>) { + this.zone.runOutsideAngular( + () => selectableObjects.forEach((object) => { + this.subMap[object.indexableObject.uuid].unsubscribe(); + this.store.dispatch(new RemoveRelationshipAction(this.item, object.indexableObject, this.relationshipOptions.relationshipType)); + }) + ); + } + + private setExistingNameVariants() { + const virtualMDs: MetadataValue[] = this.item.allMetadata(this.metadataFields).filter((mdValue) => mdValue.isVirtual); + + const relatedItemPairs$: Observable> = + combineLatest(virtualMDs.map((md: MetadataValue) => this.relationshipService.findById(md.virtualValue).pipe(getSucceededRemoteData(), getRemoteDataPayload()))) + .pipe( + switchMap((relationships: Relationship[]) => combineLatest(relationships.map((relationship: Relationship) => + combineLatest( + relationship.leftItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()), + relationship.rightItem.pipe(getSucceededRemoteData(), getRemoteDataPayload()) + )) + ) + ) + ); + + const relatedItems$: Observable = relatedItemPairs$.pipe( + map(([relatedItemPairs,]: [Array<[Item, Item]>]) => relatedItemPairs.map(([left, right]: [Item, Item]) => left.uuid === this.item.uuid ? left : right)) + ); + + relatedItems$.pipe(take(1)).subscribe((relatedItems) => { + let index = 0; + virtualMDs.forEach( + (md: MetadataValue) => { + this.relationshipService.setNameVariant(this.listId, relatedItems[index].uuid, md.value); + index++; + } + ); + } + ) + } + + ngOnDestroy() { + Object.values(this.subMap).forEach((subscription) => subscription.unsubscribe()); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions.ts new file mode 100644 index 0000000000..f32836eef1 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.actions.ts @@ -0,0 +1,62 @@ +/** + * The list of NameVariantAction type definitions + */ +import { type } from '../../../../ngrx/type'; +import { Action } from '@ngrx/store'; +import { Item } from '../../../../../core/shared/item.model'; + +export const NameVariantActionTypes = { + SET_NAME_VARIANT: type('dspace/name-variant/SET_NAME_VARIANT'), + REMOVE_NAME_VARIANT: type('dspace/name-variant/REMOVE_NAME_VARIANT'), +}; + +/* tslint:disable:max-classes-per-file */ +/** + * Abstract class for actions that happen to name variants + */ +export abstract class NameVariantListAction implements Action { + type; + payload: { + listID: string; + itemID: string; + }; + + constructor(listID: string, itemID: string) { + this.payload = { listID, itemID }; + } +} + +/** + * Action for setting a new name on an item in a certain list + */ +export class SetNameVariantAction extends NameVariantListAction { + type = NameVariantActionTypes.SET_NAME_VARIANT; + payload: { + listID: string; + itemID: string; + nameVariant: string; + }; + + constructor(listID: string, itemID: string, nameVariant: string) { + super(listID, itemID); + this.payload.nameVariant = nameVariant; + } +} + +/** + * Action for removing a name on an item in a certain list + */ +export class RemoveNameVariantAction extends NameVariantListAction { + type = NameVariantActionTypes.REMOVE_NAME_VARIANT; + constructor(listID: string, itemID: string) { + super(listID, itemID); + } +} +/* tslint:enable:max-classes-per-file */ + +/** + * A type to encompass all RelationshipActions + */ +export type NameVariantAction + = SetNameVariantAction + | RemoveNameVariantAction diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer.spec.ts new file mode 100644 index 0000000000..1ac6cd55d1 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer.spec.ts @@ -0,0 +1,93 @@ +import * as deepFreeze from 'deep-freeze'; +import { NameVariantAction, RemoveNameVariantAction, SetNameVariantAction } from './name-variant.actions'; +import { Action } from '@ngrx/store'; +import { nameVariantReducer } from './name-variant.reducer'; + +class NullAction implements Action { + type = null; +} + +let listID1; +let listID2; +let itemID1; +let itemID2; +let variantList1Item1; +let variantList1Item1Update; +let variantList1Item2; + +function init() { + listID1 = 'dbfb81de-2930-4de6-ba2e-ea21c8534ee9'; + listID2 = 'd7f2c48d-e1e2-4996-ab8d-e271cabec78a'; + itemID1 = 'd1c81d4f-6b05-4844-986b-372d2e39c6aa'; + itemID2 = 'fe4ca421-d897-417f-9436-9724262d5c69'; + variantList1Item1 = 'Test Name Variant 1'; + variantList1Item1Update = 'Test Name Variant 1 Update'; + variantList1Item2 = 'Test Name Variant 2'; +} + +describe('nameVariantReducer', () => { + beforeEach(() => { + init(); + }); + + it('should return the current state when no valid actions have been made', () => { + const state = { [listID1]: { [itemID1]: variantList1Item1 } }; + const action = new NullAction() as any; + const newState = nameVariantReducer(state, action); + + expect(newState).toEqual(state); + }); + + it('should start with an empty object', () => { + const state = Object.create({}); + const action = new NullAction() as any; + const initialState = nameVariantReducer(undefined, action); + + // The search filter starts collapsed + expect(initialState).toEqual(state); + }); + + it('should set add a new name variant in response to the SET_NAME_VARIANT' + + ' action with a combination of list and item ID that does not exist yet', () => { + const state = {}; + state[listID1] = { [itemID1]: variantList1Item1 }; + const action = new SetNameVariantAction(listID1, itemID2, variantList1Item2); + const newState = nameVariantReducer(state, action); + + expect(newState[listID1][itemID1]).toEqual(variantList1Item1); + expect(newState[listID1][itemID2]).toEqual(variantList1Item2); + }); + + it('should set a name variant in response to the SET_NAME_VARIANT' + + ' action with a combination of list and item ID that already exists', () => { + const state = {}; + state[listID1] = { [itemID1]: variantList1Item1 }; + const action = new SetNameVariantAction(listID1, itemID1, variantList1Item1Update); + const newState = nameVariantReducer(state, action); + + expect(newState[listID1][itemID1]).toEqual(variantList1Item1Update); + }); + + it('should remove a name variant in response to the REMOVE_NAME_VARIANT' + + ' action with a combination of list and item ID that already exists', () => { + const state = {}; + state[listID1] = { [itemID1]: variantList1Item1 }; + expect(state[listID1][itemID1]).toEqual(variantList1Item1); + + const action = new RemoveNameVariantAction(listID1, itemID1); + const newState = nameVariantReducer(state, action); + + expect(newState[listID1][itemID1]).toBeUndefined(); + }); + + it('should do nothing in response to the REMOVE_NAME_VARIANT' + + ' action with a combination of list and item ID that does not exists', () => { + const state = {}; + state[listID1] = { [itemID1]: variantList1Item1 }; + + const action = new RemoveNameVariantAction(listID2, itemID1); + const newState = nameVariantReducer(state, action); + + expect(newState).toEqual(state); + }); +}); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer.ts new file mode 100644 index 0000000000..9f93cf2dd1 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/name-variant.reducer.ts @@ -0,0 +1,50 @@ +/** + * Represents the state of all lists containing name variants in the store + */ + +import { NameVariantAction, NameVariantActionTypes, SetNameVariantAction } from './name-variant.actions'; +import { hasValue } from '../../../../empty.util'; + +export interface NameVariantListsState { + [listID: string]: NameVariantListState; +} + +/** + * Represents the state of a single list containing nameVariants in the store + */ +export interface NameVariantListState { + [itemID: string]: string; +} + +/** + * Reducer that handles NameVariantAction to update the NameVariantListsState + * @param {NameVariantListsState} state The initial NameVariantListsState + * @param {NameVariantAction} action The Action to be performed on the state + * @returns {NameVariantListsState} The new, reduced NameVariantListsState + */ +export function nameVariantReducer(state: NameVariantListsState = {}, action: NameVariantAction): NameVariantListsState { + switch (action.type) { + case NameVariantActionTypes.SET_NAME_VARIANT: { + const listState: NameVariantListState = state[action.payload.listID] || {}; + const nameVariant = (action as SetNameVariantAction).payload.nameVariant; + const newListState = setNameVariant(listState, action.payload.itemID, nameVariant); + return Object.assign({}, state, { [action.payload.listID]: newListState }); + } + case NameVariantActionTypes.REMOVE_NAME_VARIANT: { + const listState: NameVariantListState = state[action.payload.listID]; + if (hasValue(listState) && hasValue(listState[action.payload.itemID])) { + const newListState = setNameVariant(listState, action.payload.itemID, undefined); + return Object.assign({}, state, { [action.payload.listID]: newListState }); + } else { + return state; + } + } + default: { + return state; + } + } +} + +function setNameVariant(state: NameVariantListState, itemID: string, nameVariant: string) { + return Object.assign({}, state, { [itemID]: nameVariant }); +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.actions.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.actions.ts new file mode 100644 index 0000000000..57375bd380 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.actions.ts @@ -0,0 +1,108 @@ +/** + * The list of RelationshipAction type definitions + */ +import { type } from '../../../../ngrx/type'; +import { Action } from '@ngrx/store'; +import { Item } from '../../../../../core/shared/item.model'; + +export const RelationshipActionTypes = { + ADD_RELATIONSHIP: type('dspace/relationship/ADD_RELATIONSHIP'), + REMOVE_RELATIONSHIP: type('dspace/relationship/REMOVE_RELATIONSHIP'), + UPDATE_RELATIONSHIP: type('dspace/relationship/UPDATE_RELATIONSHIP'), +}; + +/* tslint:disable:max-classes-per-file */ +/** + * An ngrx action to create a new relationship + */ +export class AddRelationshipAction implements Action { + type = RelationshipActionTypes.ADD_RELATIONSHIP; + + payload: { + item1: Item; + item2: Item; + relationshipType: string; + nameVariant: string; + }; + + /** + * Create a new AddRelationshipAction + * + * @param item1 The first item in the relationship + * @param item2 The second item in the relationship + * @param relationshipType The label of the relationshipType + * @param nameVariant The nameVariant of the relationshipType + */ + constructor( + item1: Item, + item2: Item, + relationshipType: string, + nameVariant?: string + ) { + this.payload = { item1, item2, relationshipType, nameVariant }; + } +} + +export class UpdateRelationshipAction implements Action { + type = RelationshipActionTypes.UPDATE_RELATIONSHIP; + + payload: { + item1: Item; + item2: Item; + relationshipType: string; + nameVariant: string; + }; + + /** + * Create a new UpdateRelationshipAction + * + * @param item1 The first item in the relationship + * @param item2 The second item in the relationship + * @param relationshipType The label of the relationshipType + * @param nameVariant The nameVariant of the relationshipType + */ + constructor( + item1: Item, + item2: Item, + relationshipType: string, + nameVariant?: string + ) { + this.payload = { item1, item2, relationshipType, nameVariant }; + } +} + +/** + * An ngrx action to remove an existing relationship + */ +export class RemoveRelationshipAction implements Action { + type = RelationshipActionTypes.REMOVE_RELATIONSHIP; + + payload: { + item1: Item; + item2: Item; + relationshipType: string; + }; + + /** + * Create a new RemoveRelationshipAction + * + * @param item1 The first item in the relationship + * @param item2 The second item in the relationship + * @param relationshipType The label of the relationshipType + */ + constructor( + item1: Item, + item2: Item, + relationshipType: string) { + this.payload = { item1, item2, relationshipType }; + } +} + +/* tslint:enable:max-classes-per-file */ + +/** + * A type to encompass all RelationshipActions + */ +export type RelationshipAction + = AddRelationshipAction + | RemoveRelationshipAction diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts new file mode 100644 index 0000000000..f9d7dabf9c --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.spec.ts @@ -0,0 +1,256 @@ +import { BehaviorSubject, Observable, of as observableOf } from 'rxjs'; +import { RelationshipEffects } from './relationship.effects'; +import { async, TestBed } from '@angular/core/testing'; +import { provideMockActions } from '@ngrx/effects/testing'; +import { AddRelationshipAction, RelationshipActionTypes, RemoveRelationshipAction } from './relationship.actions'; +import { Item } from '../../../../../core/shared/item.model'; +import { MetadataValue } from '../../../../../core/shared/metadata.models'; +import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service'; +import { RelationshipService } from '../../../../../core/data/relationship.service'; +import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; +import { createSuccessfulRemoteDataObject$, spyOnOperator } from '../../../../testing/utils'; +import { RelationshipType } from '../../../../../core/shared/item-relationships/relationship-type.model'; +import { cold, hot } from 'jasmine-marbles'; +import * as operators from 'rxjs/operators'; +import { last } from 'rxjs/operators'; +import { ItemType } from '../../../../../core/shared/item-relationships/item-type.model'; +import { RestResponse } from '../../../../../core/cache/response.models'; + +describe('RelationshipEffects', () => { + let relationEffects: RelationshipEffects; + let actions: Observable; + + let testUUID1; + let testUUID2; + let leftTypeString; + let rightTypeString; + let leftType; + let rightType; + let leftTypeMD; + let rightTypeMD; + let relationshipID; + let identifier; + + let leftItem; + + let rightItem; + + let relationshipType: RelationshipType; + + let relationship; + let mockRelationshipService; + let mockRelationshipTypeService; + + function init() { + testUUID1 = '20e24c2f-a00a-467c-bdee-c929e79bf08d'; + testUUID2 = '7f66a4d0-8557-4e77-8b1e-19930895f10a'; + leftTypeString = 'Publication'; + rightTypeString = 'Person'; + leftType = Object.assign(new ItemType(), {label: leftTypeString}); + rightType = Object.assign(new ItemType(), {label: rightTypeString}); + leftTypeMD = Object.assign(new MetadataValue(), { value: leftTypeString }); + rightTypeMD = Object.assign(new MetadataValue(), { value: rightTypeString }); + relationshipID = '1234'; + + leftItem = Object.assign(new Item(), { + uuid: testUUID1, + metadata: { 'relationship.type': [leftTypeMD] } + }); + + rightItem = Object.assign(new Item(), { + uuid: testUUID2, + metadata: { 'relationship.type': [rightTypeMD] } + }); + + relationshipType = Object.assign(new RelationshipType(), { + leftwardType: 'isAuthorOfPublication', + rightwardType: 'isPublicationOfAuthor', + leftType: createSuccessfulRemoteDataObject$(leftType), + rightType: createSuccessfulRemoteDataObject$(rightType) + }); + + relationship = Object.assign(new Relationship(), + { + uuid: relationshipID, + id: relationshipID, + leftItem: createSuccessfulRemoteDataObject$(leftItem), + rightItem: createSuccessfulRemoteDataObject$(rightItem), + relationshipType: createSuccessfulRemoteDataObject$(relationshipType) + }); + + mockRelationshipService = { + getRelationshipByItemsAndLabel: + () => observableOf(relationship), + deleteRelationship: () => observableOf(new RestResponse(true, 200, 'OK')), + addRelationship: () => observableOf(new RestResponse(true, 200, 'OK')) + + }; + mockRelationshipTypeService = { + getRelationshipTypeByLabelAndTypes: + () => observableOf(relationshipType) + }; + } + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + providers: [ + RelationshipEffects, + provideMockActions(() => actions), + { provide: RelationshipTypeService, useValue: mockRelationshipTypeService }, + { provide: RelationshipService, useValue: mockRelationshipService } + ], + }); + })); + + beforeEach(() => { + relationEffects = TestBed.get(RelationshipEffects); + identifier = (relationEffects as any).createIdentifier(leftItem, rightItem, relationshipType.leftwardType); + }); + + describe('mapLastActions$', () => { + describe('When an ADD_RELATIONSHIP action is triggered', () => { + describe('When it\'s the first time for this identifier', () => { + let action; + it('should set the current value debounceMap and the value of the initialActionMap to ADD_RELATIONSHIP', () => { + action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType); + actions = hot('--a-', { a: action }); + const expected = cold('--b-', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + + expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type); + expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); + }); + }); + + describe('When it\'s not the first time for this identifier', () => { + let action; + const testActionType = 'TEST_TYPE'; + beforeEach(() => { + (relationEffects as any).initialActionMap[identifier] = testActionType; + (relationEffects as any).debounceMap[identifier] = new BehaviorSubject(testActionType); + }); + + it('should set the current value debounceMap to ADD_RELATIONSHIP but not change the value of the initialActionMap', () => { + action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType); + actions = hot('--a-', { a: action }); + const expected = cold('--b-', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + + expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType); + expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); + }); + }); + + describe('When the initialActionMap contains an ADD_RELATIONSHIP action', () => { + let action; + describe('When the last value in the debounceMap is also an ADD_RELATIONSHIP action', () => { + beforeEach(() => { + (relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.ADD_RELATIONSHIP; + spyOnOperator(operators, 'debounceTime').and.returnValue((v) => v); + spyOn((relationEffects as any), 'addRelationship'); + }); + it('should call addRelationship on the effect', () => { + action = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType); + actions = hot('--a-', { a: action }); + const expected = cold('--b-', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + expect((relationEffects as any).addRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType, undefined) + }); + }); + + describe('When the last value in the debounceMap is instead a REMOVE_RELATIONSHIP action', () => { + beforeEach(() => { + /** + * Change debounceTime to last so there's no need to fire a certain amount of actions in the debounce time frame + */ + spyOnOperator(operators, 'debounceTime').and.returnValue((v) => v.pipe(last())); + spyOn((relationEffects as any), 'addRelationship'); + spyOn((relationEffects as any), 'removeRelationship'); + }); + it('should not call removeRelationship or addRelationship on the effect', () => { + const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType); + const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType); + actions = hot('--ab-', { a: actiona, b: actionb }); + const expected = cold('--bb-', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + expect((relationEffects as any).addRelationship).not.toHaveBeenCalled(); + expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled(); + }); + }); + }); + }); + + describe('When an REMOVE_RELATIONSHIP action is triggered', () => { + describe('When it\'s the first time for this identifier', () => { + let action; + it('should set the current value debounceMap and the value of the initialActionMap to REMOVE_RELATIONSHIP', () => { + action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType); + actions = hot('--a-', { a: action }); + const expected = cold('--b-', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + + expect((relationEffects as any).initialActionMap[identifier]).toBe(action.type); + expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); + }); + }); + + describe('When it\'s not the first time for this identifier', () => { + let action; + const testActionType = 'TEST_TYPE'; + beforeEach(() => { + (relationEffects as any).initialActionMap[identifier] = testActionType; + (relationEffects as any).debounceMap[identifier] = new BehaviorSubject(testActionType); + }); + + it('should set the current value debounceMap to REMOVE_RELATIONSHIP but not change the value of the initialActionMap', () => { + action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType); + actions = hot('--a-', { a: action }); + const expected = cold('--b-', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + + expect((relationEffects as any).initialActionMap[identifier]).toBe(testActionType); + expect((relationEffects as any).debounceMap[identifier].value).toBe(action.type); + }); + }); + + describe('When the initialActionMap contains an REMOVE_RELATIONSHIP action', () => { + let action; + describe('When the last value in the debounceMap is also an REMOVE_RELATIONSHIP action', () => { + beforeEach(() => { + (relationEffects as any).initialActionMap[identifier] = RelationshipActionTypes.REMOVE_RELATIONSHIP; + spyOnOperator(operators, 'debounceTime').and.returnValue((v) => v); + spyOn((relationEffects as any), 'removeRelationship'); + }); + + it('should call removeRelationship on the effect', () => { + action = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType); + actions = hot('--a-', { a: action }); + const expected = cold('--b-', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + expect((relationEffects as any).removeRelationship).toHaveBeenCalledWith(leftItem, rightItem, relationshipType.leftwardType) + }); + }); + + describe('When the last value in the debounceMap is instead a ADD_RELATIONSHIP action', () => { + beforeEach(() => { + /** + * Change debounceTime to last so there's no need to fire a certain amount of actions in the debounce time frame + */ + spyOnOperator(operators, 'debounceTime').and.returnValue((v) => v.pipe(last())); + spyOn((relationEffects as any), 'addRelationship'); + spyOn((relationEffects as any), 'removeRelationship'); + }); + it('should not call addRelationship or removeRelationship on the effect', () => { + const actionb = new RemoveRelationshipAction(leftItem, rightItem, relationshipType.leftwardType); + const actiona = new AddRelationshipAction(leftItem, rightItem, relationshipType.leftwardType); + actions = hot('--ab-', { a: actiona, b: actionb }); + const expected = cold('--bb-', { b: undefined }); + expect(relationEffects.mapLastActions$).toBeObservable(expected); + expect((relationEffects as any).addRelationship).not.toHaveBeenCalled(); + expect((relationEffects as any).removeRelationship).not.toHaveBeenCalled(); + }); + }); + }); + }); + }); +}); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts new file mode 100644 index 0000000000..9402ef6d19 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/relationship.effects.ts @@ -0,0 +1,134 @@ +import { Injectable } from '@angular/core'; +import { Actions, Effect, ofType } from '@ngrx/effects'; +import { debounceTime, map, mergeMap, take, tap } from 'rxjs/operators'; +import { BehaviorSubject } from 'rxjs'; +import { RelationshipService } from '../../../../../core/data/relationship.service'; +import { AddRelationshipAction, RelationshipAction, RelationshipActionTypes, RemoveRelationshipAction, UpdateRelationshipAction } from './relationship.actions'; +import { Item } from '../../../../../core/shared/item.model'; +import { hasNoValue, hasValue, hasValueOperator } from '../../../../empty.util'; +import { Relationship } from '../../../../../core/shared/item-relationships/relationship.model'; +import { RelationshipType } from '../../../../../core/shared/item-relationships/relationship-type.model'; +import { RelationshipTypeService } from '../../../../../core/data/relationship-type.service'; + +const DEBOUNCE_TIME = 5000; + +/** + * NGRX effects for RelationshipEffects + */ +@Injectable() +export class RelationshipEffects { + /** + * Map that keeps track of the latest RelationshipEffects for each relationship's composed identifier + */ + private debounceMap: { + [identifier: string]: BehaviorSubject + } = {}; + + private nameVariantUpdates: { + [identifier: string]: string + } = {}; + + private initialActionMap: { + [identifier: string]: string + } = {}; + + /** + * Effect that makes sure all last fired RelationshipActions' types are stored in the map of this service, with the object uuid as their key + */ + @Effect({ dispatch: false }) mapLastActions$ = this.actions$ + .pipe( + ofType(RelationshipActionTypes.ADD_RELATIONSHIP, RelationshipActionTypes.REMOVE_RELATIONSHIP), + map((action: RelationshipAction) => { + const { item1, item2, relationshipType } = action.payload; + const identifier: string = this.createIdentifier(item1, item2, relationshipType); + if (hasNoValue(this.debounceMap[identifier])) { + this.initialActionMap[identifier] = action.type; + this.debounceMap[identifier] = new BehaviorSubject(action.type); + this.debounceMap[identifier].pipe( + debounceTime(DEBOUNCE_TIME), + take(1) + ).subscribe( + (type) => { + if (this.initialActionMap[identifier] === type) { + if (type === RelationshipActionTypes.ADD_RELATIONSHIP) { + let nameVariant = (action as AddRelationshipAction).payload.nameVariant; + if (hasValue(this.nameVariantUpdates[identifier])) { + nameVariant = this.nameVariantUpdates[identifier]; + delete this.nameVariantUpdates[identifier]; + } + this.addRelationship(item1, item2, relationshipType, nameVariant) + } else { + this.removeRelationship(item1, item2, relationshipType); + } + } + delete this.debounceMap[identifier]; + delete this.initialActionMap[identifier]; + } + ) + } else { + this.debounceMap[identifier].next(action.type); + } + } + ) + ); + + /** + * Updates the namevariant in a relationship + * If the relationship is currently being added or removed, it will add the name variant to an update map so it will be sent with the next add request instead + * Otherwise the update is done immediately + */ + @Effect({ dispatch: false }) updateNameVariantsActions$ = this.actions$ + .pipe( + ofType(RelationshipActionTypes.UPDATE_RELATIONSHIP), + map((action: UpdateRelationshipAction) => { + const { item1, item2, relationshipType, nameVariant } = action.payload; + const identifier: string = this.createIdentifier(item1, item2, relationshipType); + const inProgress = hasValue(this.debounceMap[identifier]); + if (inProgress) { + this.nameVariantUpdates[identifier] = nameVariant; + } else { + this.relationshipService.updateNameVariant(item1, item2, relationshipType, nameVariant) + .pipe() + .subscribe(); + } + } + ) + ); + + constructor(private actions$: Actions, + private relationshipService: RelationshipService, + private relationshipTypeService: RelationshipTypeService, + ) { + } + + private createIdentifier(item1: Item, item2: Item, relationshipType: string): string { + return `${item1.uuid}-${item2.uuid}-${relationshipType}`; + } + + private addRelationship(item1: Item, item2: Item, relationshipType: string, nameVariant?: string) { + const type1: string = item1.firstMetadataValue('relationship.type'); + const type2: string = item2.firstMetadataValue('relationship.type'); + return this.relationshipTypeService.getRelationshipTypeByLabelAndTypes(relationshipType, type1, type2) + .pipe( + mergeMap((type: RelationshipType) => { + const isSwitched = type.rightwardType === relationshipType; + if (isSwitched) { + return this.relationshipService.addRelationship(type.id, item2, item1, nameVariant, undefined); + } else { + return this.relationshipService.addRelationship(type.id, item1, item2, undefined, nameVariant); + } + } + ) + ).pipe(take(1)) + .subscribe(); + } + + private removeRelationship(item1: Item, item2: Item, relationshipType: string) { + this.relationshipService.getRelationshipByItemsAndLabel(item1, item2, relationshipType).pipe( + take(1), + hasValueOperator(), + mergeMap((relationship: Relationship) => this.relationshipService.deleteRelationship(relationship.id)), + take(1) + ).subscribe(); + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html new file mode 100644 index 0000000000..4e2da1f12b --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.html @@ -0,0 +1,69 @@ +
+ +
+ + + + +
+
+
+
+ + + + +
+
+
+ + +
+ + + + +
+
+
+
+ + +
+
\ No newline at end of file diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss new file mode 100644 index 0000000000..4562a95080 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.scss @@ -0,0 +1,3 @@ +.position-absolute { + right: $spacer; +} \ No newline at end of file diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts new file mode 100644 index 0000000000..4434684cbb --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.spec.ts @@ -0,0 +1,144 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { DsDynamicLookupRelationSearchTabComponent } from './dynamic-lookup-relation-search-tab.component'; +import { SearchService } from '../../../../../../core/shared/search/search.service'; +import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service'; +import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; +import { RouteService } from '../../../../../../core/services/route.service'; +import { RouterTestingModule } from '@angular/router/testing'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { VarDirective } from '../../../../../utils/var.directive'; +import { RelationshipOptions } from '../../../models/relationship-options.model'; +import { of as observableOf } from 'rxjs'; +import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../testing/utils'; +import { PaginatedList } from '../../../../../../core/data/paginated-list'; +import { ItemSearchResult } from '../../../../../object-collection/shared/item-search-result.model'; +import { Item } from '../../../../../../core/shared/item.model'; + +describe('DsDynamicLookupRelationSearchTabComponent', () => { + let component: DsDynamicLookupRelationSearchTabComponent; + let fixture: ComponentFixture; + let relationship; + let pSearchOptions; + let item1; + let item2; + let item3; + let item4; + let searchResult1; + let searchResult2; + let searchResult3; + let searchResult4; + let listID; + let selection$; + + let results; + let selectableListService; + + function init() { + relationship = { filter: 'filter', relationshipType: 'isAuthorOfPublication', nameVariants: true } as RelationshipOptions; + pSearchOptions = new PaginatedSearchOptions({}); + item1 = Object.assign(new Item(), { uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e' }); + item2 = Object.assign(new Item(), { uuid: 'c8279647-1acc-41ae-b036-951d5f65649b' }); + item3 = Object.assign(new Item(), { uuid: 'c3bcbff5-ec0c-4831-8e4c-94b9c933ccac' }); + item4 = Object.assign(new Item(), { uuid: 'f96a385e-de10-45b2-be66-7f10bf52f765' }); + searchResult1 = Object.assign(new ItemSearchResult(), { indexableObject: item1 }); + searchResult2 = Object.assign(new ItemSearchResult(), { indexableObject: item2 }); + searchResult3 = Object.assign(new ItemSearchResult(), { indexableObject: item3 }); + searchResult4 = Object.assign(new ItemSearchResult(), { indexableObject: item4 }); + listID = '6b0c8221-fcb4-47a8-b483-ca32363fffb3'; + selection$ = observableOf([searchResult1, searchResult2]); + + results = new PaginatedList(undefined, [searchResult1, searchResult2, searchResult3]); + selectableListService = jasmine.createSpyObj('selectableListService', ['deselect', 'select', 'deselectAll']); + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + declarations: [DsDynamicLookupRelationSearchTabComponent, VarDirective], + imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], + providers: [ + { provide: SearchService, useValue: { search: () => createSuccessfulRemoteDataObject$(results) } }, + { + provide: SelectableListService, useValue: selectableListService + }, + { + provide: SearchConfigurationService, useValue: { + paginatedSearchOptions: observableOf(pSearchOptions) + } + }, + { + provide: RouteService, useValue: { + setParameter: () => { + // do nothing + } + } + }, + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DsDynamicLookupRelationSearchTabComponent); + component = fixture.componentInstance; + component.relationship = relationship; + component.selection$ = selection$; + component.listId = listID; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + describe('selectPage', () => { + beforeEach(() => { + spyOn(component.selectObject, 'emit'); + component.selectPage([searchResult1, searchResult2, searchResult4]); + }); + + it('should emit the page filtered from already selected objects and call select on the service for all objects', () => { + expect(component.selectObject.emit).toHaveBeenCalledWith(searchResult4); + expect(selectableListService.select).toHaveBeenCalledWith(listID, [searchResult1, searchResult2, searchResult4]); + }); + }); + + describe('deselectPage', () => { + beforeEach(() => { + spyOn(component.deselectObject, 'emit'); + component.deselectPage([searchResult1, searchResult2, searchResult3]); + }); + + it('should emit the page filtered from not yet selected objects and call select on the service for all objects', () => { + expect(component.deselectObject.emit).toHaveBeenCalledWith(searchResult1, searchResult2); + expect(selectableListService.deselect).toHaveBeenCalledWith(listID, [searchResult1, searchResult2, searchResult3]); + }); + }); + + describe('selectAll', () => { + beforeEach(() => { + spyOn(component.selectObject, 'emit'); + component.selectAll(); + }); + + it('should emit the page filtered from already selected objects and call select on the service for all objects', () => { + expect(component.selectObject.emit).toHaveBeenCalledWith(searchResult3); + expect(selectableListService.select).toHaveBeenCalledWith(listID, [searchResult1, searchResult2, searchResult3]); + }); + }); + + describe('deselectAll', () => { + beforeEach(() => { + spyOn(component.deselectObject, 'emit'); + component.deselectAll(); + }); + + it('should emit the page filtered from not yet selected objects and call select on the service for all objects', () => { + expect(component.deselectObject.emit).toHaveBeenCalledWith(searchResult1, searchResult2); + expect(selectableListService.deselectAll).toHaveBeenCalledWith(listID); + }); + }); +}); diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts new file mode 100644 index 0000000000..9c00d64953 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component.ts @@ -0,0 +1,181 @@ +import { Component, EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core'; +import { SEARCH_CONFIG_SERVICE } from '../../../../../../+my-dspace-page/my-dspace-page.component'; +import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; +import { Item } from '../../../../../../core/shared/item.model'; +import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; +import { SearchResult } from '../../../../../search/search-result.model'; +import { PaginatedList } from '../../../../../../core/data/paginated-list'; +import { RemoteData } from '../../../../../../core/data/remote-data'; +import { Observable, ReplaySubject } from 'rxjs'; +import { RelationshipOptions } from '../../../models/relationship-options.model'; +import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; +import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; +import { SearchService } from '../../../../../../core/shared/search/search.service'; +import { Router } from '@angular/router'; +import { SelectableListService } from '../../../../../object-list/selectable-list/selectable-list.service'; +import { hasValue, isNotEmpty } from '../../../../../empty.util'; +import { concat, map, multicast, switchMap, take, takeWhile, tap } from 'rxjs/operators'; +import { DSpaceObject } from '../../../../../../core/shared/dspace-object.model'; +import { getSucceededRemoteData } from '../../../../../../core/shared/operators'; +import { RouteService } from '../../../../../../core/services/route.service'; +import { CollectionElementLinkType } from '../../../../../object-collection/collection-element-link.type'; +import { Context } from '../../../../../../core/shared/context.model'; + +@Component({ + selector: 'ds-dynamic-lookup-relation-search-tab', + styleUrls: ['./dynamic-lookup-relation-search-tab.component.scss'], + templateUrl: './dynamic-lookup-relation-search-tab.component.html', + providers: [ + { + provide: SEARCH_CONFIG_SERVICE, + useClass: SearchConfigurationService + } + ] +}) + +/** + * Tab for inside the lookup model that represents the items that can be used as a relationship in this submission + */ +export class DsDynamicLookupRelationSearchTabComponent implements OnInit, OnDestroy { + @Input() relationship: RelationshipOptions; + @Input() listId: string; + @Input() repeatable: boolean; + @Input() selection$: Observable; + @Input() context: Context; + + @Output() deselectObject: EventEmitter = new EventEmitter(); + @Output() selectObject: EventEmitter = new EventEmitter(); + resultsRD$: Observable>>>; + searchConfig: PaginatedSearchOptions; + allSelected: boolean; + someSelected$: Observable; + selectAllLoading: boolean; + subscription; + initialPagination = Object.assign(new PaginationComponentOptions(), { + id: 'submission-relation-list', + pageSize: 5 + }); + linkTypes = CollectionElementLinkType; + + constructor( + private searchService: SearchService, + private router: Router, + private selectableListService: SelectableListService, + private searchConfigService: SearchConfigurationService, + private routeService: RouteService, + ) { + } + + /** + * Sets up the pagination and fixed query parameters + */ + ngOnInit(): void { + this.resetRoute(); + this.routeService.setParameter('fixedFilterQuery', this.relationship.filter); + this.routeService.setParameter('configuration', this.relationship.searchConfiguration); + + this.someSelected$ = this.selection$.pipe(map((selection) => isNotEmpty(selection))); + this.resultsRD$ = this.searchConfigService.paginatedSearchOptions.pipe( + map((options) => { + return Object.assign(new PaginatedSearchOptions({}), options, { fixedFilter: this.relationship.filter, configuration: this.relationship.searchConfiguration }) + }), + switchMap((options) => { + this.searchConfig = options; + return this.searchService.search(options).pipe( + /* Make sure to only listen to the first x results, until loading is finished */ + /* TODO: in Rxjs 6.4.0 and up, we can replace this with takeWhile(predicate, true) - see https://stackoverflow.com/a/44644237 */ + multicast( + () => new ReplaySubject(1), + (subject) => subject.pipe( + takeWhile((rd: RemoteData>>) => rd.isLoading), + concat(subject.pipe(take(1))) + ) + ) as any + ) + }) + ); + } + + /** + * Method to reset the route when the window is opened to make sure no strange pagination issues appears + */ + resetRoute() { + this.router.navigate([], { + queryParams: Object.assign({}, { page: 1, pageSize: this.initialPagination.pageSize }), + }); + } + + /** + * Selects a page in the store + * @param page The page to select + */ + selectPage(page: Array>) { + this.selection$ + .pipe(take(1)) + .subscribe((selection: Array>) => { + const filteredPage = page.filter((pageItem) => selection.findIndex((selected) => selected.equals(pageItem)) < 0); + this.selectObject.emit(...filteredPage); + }); + this.selectableListService.select(this.listId, page); + } + + /** + * Deselects a page in the store + * @param page the page to deselect + */ + deselectPage(page: Array>) { + this.allSelected = false; + this.selection$ + .pipe(take(1)) + .subscribe((selection: Array>) => { + const filteredPage = page.filter((pageItem) => selection.findIndex((selected) => selected.equals(pageItem)) >= 0); + this.deselectObject.emit(...filteredPage); + }); + this.selectableListService.deselect(this.listId, page); + } + + /** + * Select all items that were found using the current search query + */ + selectAll() { + this.allSelected = true; + this.selectAllLoading = true; + const fullPagination = Object.assign(new PaginationComponentOptions(), { + currentPage: 1, + pageSize: 9999 + }); + const fullSearchConfig = Object.assign(this.searchConfig, { pagination: fullPagination }); + const results$ = this.searchService.search(fullSearchConfig) as Observable>>>; + results$.pipe( + getSucceededRemoteData(), + map((resultsRD) => resultsRD.payload.page), + tap(() => this.selectAllLoading = false), + ).subscribe((results) => { + this.selection$ + .pipe(take(1)) + .subscribe((selection: Array>) => { + const filteredResults = results.filter((pageItem) => selection.findIndex((selected) => selected.equals(pageItem)) < 0); + this.selectObject.emit(...filteredResults); + }); + this.selectableListService.select(this.listId, results); + } + ); + } + + /** + * Deselect all items + */ + deselectAll() { + this.allSelected = false; + this.selection$ + .pipe(take(1)) + .subscribe((selection: Array>) => this.deselectObject.emit(...selection)); + this.selectableListService.deselectAll(this.listId); + } + + ngOnDestroy(): void { + if (hasValue(this.subscription)) { + this.subscription.unsubscribe(); + } + } +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.html b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.html new file mode 100644 index 0000000000..46ee1727fe --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.html @@ -0,0 +1,23 @@ +
+
+

{{ 'submission.sections.describe.relationship-lookup.selection-tab.settings' | translate}}

+ +
+
+
+ {{'submission.sections.describe.relationship-lookup.selection-tab.no-selection' | translate}} +
+
+

{{ 'submission.sections.describe.relationship-lookup.selection-tab.title.' + label | translate}}

+ +
+
+
\ No newline at end of file diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.scss b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts new file mode 100644 index 0000000000..203a4df0b0 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.spec.ts @@ -0,0 +1,97 @@ +import { async, ComponentFixture, TestBed } from '@angular/core/testing'; +import { TranslateModule } from '@ngx-translate/core'; +import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; +import { NO_ERRORS_SCHEMA } from '@angular/core'; +import { VarDirective } from '../../../../../utils/var.directive'; +import { Observable, of as observableOf } from 'rxjs'; +import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; +import { ItemSearchResult } from '../../../../../object-collection/shared/item-search-result.model'; +import { Item } from '../../../../../../core/shared/item.model'; +import { DsDynamicLookupRelationSelectionTabComponent } from './dynamic-lookup-relation-selection-tab.component'; +import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; +import { Router } from '@angular/router'; +import { By } from '@angular/platform-browser'; +import { RemoteData } from '../../../../../../core/data/remote-data'; +import { PaginatedList } from '../../../../../../core/data/paginated-list'; +import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; +import { createSuccessfulRemoteDataObject$ } from '../../../../../testing/utils'; + +describe('DsDynamicLookupRelationSelectionTabComponent', () => { + let component: DsDynamicLookupRelationSelectionTabComponent; + let fixture: ComponentFixture; + let pSearchOptions = new PaginatedSearchOptions({ pagination: new PaginationComponentOptions() }); + let item1 = Object.assign(new Item(), { uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e' }); + let item2 = Object.assign(new Item(), { uuid: 'c8279647-1acc-41ae-b036-951d5f65649b' }); + let searchResult1 = Object.assign(new ItemSearchResult(), { indexableObject: item1 }); + let searchResult2 = Object.assign(new ItemSearchResult(), { indexableObject: item2 }); + let listID = '6b0c8221-fcb4-47a8-b483-ca32363fffb3'; + let selection$; + let selectionRD$; + let router; + + function init() { + pSearchOptions = new PaginatedSearchOptions({ pagination: new PaginationComponentOptions() }); + item1 = Object.assign(new Item(), { uuid: 'e1c51c69-896d-42dc-8221-1d5f2ad5516e' }); + item2 = Object.assign(new Item(), { uuid: 'c8279647-1acc-41ae-b036-951d5f65649b' }); + searchResult1 = Object.assign(new ItemSearchResult(), { indexableObject: item1 }); + searchResult2 = Object.assign(new ItemSearchResult(), { indexableObject: item2 }); + listID = '6b0c8221-fcb4-47a8-b483-ca32363fffb3'; + selection$ = observableOf([searchResult1, searchResult2]); + selectionRD$ = createSelection([searchResult1, searchResult2]); + router = jasmine.createSpyObj('router', ['navigate']) + } + + beforeEach(async(() => { + init(); + TestBed.configureTestingModule({ + declarations: [DsDynamicLookupRelationSelectionTabComponent, VarDirective], + imports: [TranslateModule.forRoot()], + providers: [ + { + provide: SearchConfigurationService, useValue: { + paginatedSearchOptions: observableOf(pSearchOptions) + }, + }, + { + provide: Router, useValue: router + } + ], + schemas: [NO_ERRORS_SCHEMA] + }) + .compileComponents(); + })); + + beforeEach(() => { + fixture = TestBed.createComponent(DsDynamicLookupRelationSelectionTabComponent); + component = fixture.componentInstance; + component.selection$ = selection$; + component.listId = listID; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should call navigate on the router when is called resetRoute', () => { + component.resetRoute(); + expect(router.navigate).toHaveBeenCalled(); + }); + + it('should call navigate on the router when is called resetRoute', () => { + component.selectionRD$ = createSelection([]); + fixture.detectChanges(); + const colComponent = fixture.debugElement.query(By.css('ds-viewable-collection')); + expect(colComponent).toBe(null); + }); + + it('should call navigate on the router when is called resetRoute', () => { + component.selectionRD$ = selectionRD$; + const colComponent = fixture.debugElement.query(By.css('ds-viewable-collection')); + expect(colComponent).not.toBe(null); + }); +}); + +function createSelection(content: ListableObject[]): Observable>> { + return createSuccessfulRemoteDataObject$(new PaginatedList(undefined, content)); +} diff --git a/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts new file mode 100644 index 0000000000..8aa3dc3828 --- /dev/null +++ b/src/app/shared/form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component.ts @@ -0,0 +1,87 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; +import { SEARCH_CONFIG_SERVICE } from '../../../../../../+my-dspace-page/my-dspace-page.component'; +import { SearchConfigurationService } from '../../../../../../core/shared/search/search-configuration.service'; +import { Observable } from 'rxjs'; +import { ListableObject } from '../../../../../object-collection/shared/listable-object.model'; +import { RemoteData } from '../../../../../../core/data/remote-data'; +import { map, switchMap, take } from 'rxjs/operators'; +import { createSuccessfulRemoteDataObject } from '../../../../../testing/utils'; +import { PaginationComponentOptions } from '../../../../../pagination/pagination-component-options.model'; +import { PaginatedList } from '../../../../../../core/data/paginated-list'; +import { Router } from '@angular/router'; +import { PaginatedSearchOptions } from '../../../../../search/paginated-search-options.model'; +import { PageInfo } from '../../../../../../core/shared/page-info.model'; +import { Context } from '../../../../../../core/shared/context.model'; + +@Component({ + selector: 'ds-dynamic-lookup-relation-selection-tab', + styleUrls: ['./dynamic-lookup-relation-selection-tab.component.scss'], + templateUrl: './dynamic-lookup-relation-selection-tab.component.html', + providers: [ + { + provide: SEARCH_CONFIG_SERVICE, + useClass: SearchConfigurationService + } + ] +}) + +/** + * Tab for inside the lookup model that represents the currently selected relationships + */ +export class DsDynamicLookupRelationSelectionTabComponent { + @Input() label: string; + @Input() listId: string; + @Input() repeatable: boolean; + @Input() selection$: Observable; + @Input() selectionRD$: Observable>>; + @Input() context: Context; + @Output() deselectObject: EventEmitter = new EventEmitter(); + @Output() selectObject: EventEmitter = new EventEmitter(); + + initialPagination = Object.assign(new PaginationComponentOptions(), { + id: 'submission-relation-list', + pageSize: 5 + }); + + constructor(private router: Router, + private searchConfigService: SearchConfigurationService) { + } + + /** + * Set up the selection and pagination on load + */ + ngOnInit() { + this.resetRoute(); + this.selectionRD$ = this.searchConfigService.paginatedSearchOptions + .pipe( + map((options: PaginatedSearchOptions) => options.pagination), + switchMap((pagination: PaginationComponentOptions) => { + return this.selection$.pipe( + take(1), + map((selected) => { + const offset = (pagination.currentPage - 1) * pagination.pageSize; + const end = (offset + pagination.pageSize) > selected.length ? selected.length : offset + pagination.pageSize; + const selection = selected.slice(offset, end); + const pageInfo = new PageInfo( + { + elementsPerPage: pagination.pageSize, + totalElements: selected.length, + currentPage: pagination.currentPage, + totalPages: Math.ceil(selected.length / pagination.pageSize) + }); + return createSuccessfulRemoteDataObject(new PaginatedList(pageInfo, selection)); + }) + ); + }) + ) + } + + /** + * Method to reset the route when the window is opened to make sure no strange pagination issues appears + */ + resetRoute() { + this.router.navigate([], { + queryParams: Object.assign({}, { page: 1, pageSize: this.initialPagination.pageSize }), + }); + } +} diff --git a/src/app/shared/form/builder/form-builder.service.spec.ts b/src/app/shared/form/builder/form-builder.service.spec.ts index 58a1696a92..ea0957f689 100644 --- a/src/app/shared/form/builder/form-builder.service.spec.ts +++ b/src/app/shared/form/builder/form-builder.service.spec.ts @@ -56,6 +56,8 @@ describe('FormBuilderService test suite', () => { let testFormConfiguration: SubmissionFormsModel; let service: FormBuilderService; + const submissionId = '1234'; + function testValidator() { return {testValidator: {valid: true}}; } @@ -193,17 +195,18 @@ describe('FormBuilderService test suite', () => { new DynamicColorPickerModel({id: 'testColorPicker'}), - new DynamicTypeaheadModel({id: 'testTypeahead'}), + new DynamicTypeaheadModel({id: 'testTypeahead', repeatable: false, metadataFields: [], submissionId: '1234'}), - new DynamicScrollableDropdownModel({id: 'testScrollableDropdown', authorityOptions: authorityOptions}), + new DynamicScrollableDropdownModel({id: 'testScrollableDropdown', authorityOptions: authorityOptions, repeatable: false, metadataFields: [], submissionId: '1234'}), - new DynamicTagModel({id: 'testTag'}), + new DynamicTagModel({id: 'testTag', repeatable: false, metadataFields: [], submissionId: '1234'}), new DynamicListCheckboxGroupModel({id: 'testCheckboxList', authorityOptions: authorityOptions, repeatable: true}), new DynamicListRadioGroupModel({id: 'testRadioList', authorityOptions: authorityOptions, repeatable: false}), new DynamicRelationGroupModel({ + submissionId, id: 'testRelationGroup', formConfiguration: [{ fields: [{ @@ -239,16 +242,18 @@ describe('FormBuilderService test suite', () => { name: 'testRelationGroup', relationFields: [], scopeUUID: '', - submissionScope: '' + submissionScope: '', + repeatable: false, + metadataFields: [] }), new DynamicDsDatePickerModel({id: 'testDate'}), - new DynamicLookupModel({id: 'testLookup'}), + new DynamicLookupModel({id: 'testLookup', repeatable: false, metadataFields: [], submissionId: '1234'}), - new DynamicLookupNameModel({id: 'testLookupName'}), + new DynamicLookupNameModel({id: 'testLookupName', repeatable: false, metadataFields: [], submissionId: '1234'}), - new DynamicQualdropModel({id: 'testCombobox', readOnly: false}), + new DynamicQualdropModel({id: 'testCombobox', readOnly: false, required: false}), new DynamicRowArrayModel( { @@ -260,6 +265,7 @@ describe('FormBuilderService test suite', () => { new DynamicInputModel({id: 'testFormRowArrayGroupInput'}) ]; }, + required: false } ), ]; @@ -406,7 +412,7 @@ describe('FormBuilderService test suite', () => { }); it('should create an array of form models', () => { - const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID'); + const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID'); expect(formModel[0] instanceof DynamicRowGroupModel).toBe(true); expect((formModel[0] as DynamicRowGroupModel).group.length).toBe(3); @@ -427,7 +433,7 @@ describe('FormBuilderService test suite', () => { }); it('should return form\'s fields value from form model', () => { - const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID'); + const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID'); let value = {} as any; expect(service.getValueFromModel(formModel)).toEqual(value); @@ -448,7 +454,7 @@ describe('FormBuilderService test suite', () => { }); it('should clear all form\'s fields value', () => { - const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID'); + const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID'); const value = {} as any; ((formModel[0] as DynamicRowGroupModel).get(1) as DsDynamicInputModel).valueUpdates.next('test'); @@ -460,7 +466,7 @@ describe('FormBuilderService test suite', () => { }); it('should return true when model has a custom group model as parent', () => { - const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID'); + const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID'); let model = service.findById('dc_identifier_QUALDROP_VALUE', formModel); let modelParent = service.findById('dc_identifier_QUALDROP_GROUP', formModel); model.parent = modelParent; @@ -489,7 +495,7 @@ describe('FormBuilderService test suite', () => { }); it('should return true when model value is a map', () => { - const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID'); + const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID'); const model = service.findById('dc_identifier_QUALDROP_VALUE', formModel); const modelParent = service.findById('dc_identifier_QUALDROP_GROUP', formModel); model.parent = modelParent; @@ -498,7 +504,7 @@ describe('FormBuilderService test suite', () => { }); it('should return true when model is a Qualdrop Group', () => { - const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID'); + const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID'); let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel); expect(service.isQualdropGroup(model)).toBe(true); @@ -509,7 +515,7 @@ describe('FormBuilderService test suite', () => { }); it('should return true when model is a Custom or List Group', () => { - const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID'); + const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID'); let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel); expect(service.isCustomOrListGroup(model)).toBe(true); @@ -528,7 +534,7 @@ describe('FormBuilderService test suite', () => { }); it('should return true when model is a Custom Group', () => { - const formModel = service.modelFromConfiguration(testFormConfiguration, 'testScopeUUID'); + const formModel = service.modelFromConfiguration(submissionId, testFormConfiguration, 'testScopeUUID'); let model = service.findById('dc_identifier_QUALDROP_GROUP', formModel); expect(service.isCustomGroup(model)).toBe(true); diff --git a/src/app/shared/form/builder/form-builder.service.ts b/src/app/shared/form/builder/form-builder.service.ts index 21e702aabb..dcc9403d9b 100644 --- a/src/app/shared/form/builder/form-builder.service.ts +++ b/src/app/shared/form/builder/form-builder.service.ts @@ -11,6 +11,7 @@ import { DynamicFormControlModel, DynamicFormGroupModel, DynamicFormService, + DynamicFormValidationService, DynamicPathable, JSONUtils, } from '@ng-dynamic-forms/core'; @@ -21,10 +22,7 @@ import { DynamicQualdropModel } from './ds-dynamic-form-ui/models/ds-dynamic-qua import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; import { DYNAMIC_FORM_CONTROL_TYPE_TAG } from './ds-dynamic-form-ui/models/tag/dynamic-tag.model'; import { RowParser } from './parsers/row-parser'; -import { - DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP, - DynamicRelationGroupModel -} from './ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; +import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP, DynamicRelationGroupModel } from './ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; import { DynamicRowArrayModel } from './ds-dynamic-form-ui/models/ds-dynamic-row-array-model'; import { DsDynamicInputModel } from './ds-dynamic-form-ui/models/ds-dynamic-input.model'; import { FormFieldMetadataValueObject } from './models/form-field-metadata-value.model'; @@ -33,6 +31,13 @@ import { isNgbDateStruct } from '../../date.util'; @Injectable() export class FormBuilderService extends DynamicFormService { + constructor( + validationService: DynamicFormValidationService, + protected rowParser: RowParser + ) { + super(validationService); + } + findById(id: string, groupModel: DynamicFormControlModel[], arrayIndex = null): DynamicFormControlModel | null { let result = null; @@ -198,13 +203,13 @@ export class FormBuilderService extends DynamicFormService { return result; } - modelFromConfiguration(json: string | SubmissionFormsModel, scopeUUID: string, initFormValues: any = {}, submissionScope?: string, readOnly = false): DynamicFormControlModel[] | never { + modelFromConfiguration(submissionId: string, json: string | SubmissionFormsModel, scopeUUID: string, sectionData: any = {}, submissionScope?: string, readOnly = false): DynamicFormControlModel[] | never { let rows: DynamicFormControlModel[] = []; const rawData = typeof json === 'string' ? JSON.parse(json, JSONUtils.parseReviver) : json; if (rawData.rows && !isEmpty(rawData.rows)) { rawData.rows.forEach((currentRow) => { - const rowParsed = new RowParser(currentRow, scopeUUID, initFormValues, submissionScope, readOnly).parse(); + const rowParsed = this.rowParser.parse(submissionId, currentRow, scopeUUID, sectionData, submissionScope, readOnly); if (isNotNull(rowParsed)) { if (Array.isArray(rowParsed)) { rows = rows.concat(rowParsed); diff --git a/src/app/shared/form/builder/models/form-field.model.ts b/src/app/shared/form/builder/models/form-field.model.ts index 39f2d6b9f8..718b3f4f0d 100644 --- a/src/app/shared/form/builder/models/form-field.model.ts +++ b/src/app/shared/form/builder/models/form-field.model.ts @@ -1,6 +1,7 @@ import { autoserialize } from 'cerialize'; import { LanguageCode } from './form-field-language-value.model'; import { FormFieldMetadataValueObject } from './form-field-metadata-value.model'; +import { RelationshipOptions } from './relationship-options.model'; import { FormRowModel } from '../../../../core/config/models/config-submission-form.model'; export class FormFieldModel { @@ -32,6 +33,9 @@ export class FormFieldModel { @autoserialize selectableMetadata: FormFieldMetadataValueObject[]; + @autoserialize + selectableRelationship: RelationshipOptions; + @autoserialize rows: FormRowModel[]; diff --git a/src/app/shared/form/builder/models/relationship-options.model.ts b/src/app/shared/form/builder/models/relationship-options.model.ts new file mode 100644 index 0000000000..f1d3d0ae7a --- /dev/null +++ b/src/app/shared/form/builder/models/relationship-options.model.ts @@ -0,0 +1,15 @@ +const RELATION_METADATA_PREFIX = 'relation.' + +/** + * The submission options for fields that can represent relationships + */ +export class RelationshipOptions { + relationshipType: string; + filter: string; + searchConfiguration: string; + nameVariants: boolean; + + get metadataField() { + return RELATION_METADATA_PREFIX + this.relationshipType + } +} diff --git a/src/app/shared/form/builder/parsers/concat-field-parser.ts b/src/app/shared/form/builder/parsers/concat-field-parser.ts index 6323905555..33a92c726d 100644 --- a/src/app/shared/form/builder/parsers/concat-field-parser.ts +++ b/src/app/shared/form/builder/parsers/concat-field-parser.ts @@ -1,7 +1,11 @@ -import { FieldParser } from './field-parser'; +import { Inject } from '@angular/core'; import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; -import { DynamicFormControlLayout, DynamicInputModel, DynamicInputModelConfig } from '@ng-dynamic-forms/core'; +import { + DynamicFormControlLayout, + DynamicInputModel, + DynamicInputModelConfig +} from '@ng-dynamic-forms/core'; import { CONCAT_FIRST_INPUT_SUFFIX, CONCAT_GROUP_SUFFIX, @@ -11,16 +15,25 @@ import { } from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model'; import { isNotEmpty } from '../../../empty.util'; import { ParserOptions } from './parser-options'; +import { + CONFIG_DATA, + FieldParser, + INIT_FORM_VALUES, + PARSER_OPTIONS, + SUBMISSION_ID +} from './field-parser'; export class ConcatFieldParser extends FieldParser { - constructor(protected configData: FormFieldModel, - protected initFormValues, - protected parserOptions: ParserOptions, - protected separator: string, - protected firstPlaceholder: string = null, - protected secondPlaceholder: string = null) { - super(configData, initFormValues, parserOptions); + constructor( + @Inject(SUBMISSION_ID) submissionId: string, + @Inject(CONFIG_DATA) configData: FormFieldModel, + @Inject(INIT_FORM_VALUES) initFormValues, + @Inject(PARSER_OPTIONS) parserOptions: ParserOptions, + protected separator: string, + protected firstPlaceholder: string = null, + protected secondPlaceholder: string = null) { + super(submissionId, configData, initFormValues, parserOptions); this.separator = separator; this.firstPlaceholder = firstPlaceholder; @@ -40,16 +53,17 @@ export class ConcatFieldParser extends FieldParser { }; const groupId = id.replace(/\./g, '_') + CONCAT_GROUP_SUFFIX; - const concatGroup: DynamicConcatModelConfig = this.initModel(groupId, false, false); + const concatGroup: DynamicConcatModelConfig = this.initModel(groupId, label, false); concatGroup.group = []; concatGroup.separator = this.separator; - const input1ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_FIRST_INPUT_SUFFIX, label, false, false); - const input2ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_SECOND_INPUT_SUFFIX, label, true, false); + const input1ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_FIRST_INPUT_SUFFIX, false, false); + const input2ModelConfig: DynamicInputModelConfig = this.initModel(id + CONCAT_SECOND_INPUT_SUFFIX, false, false); input2ModelConfig.hint = ' '; if (this.configData.mandatory) { + concatGroup.required = true; input1ModelConfig.required = true; } diff --git a/src/app/shared/form/builder/parsers/date-field-parser.spec.ts b/src/app/shared/form/builder/parsers/date-field-parser.spec.ts index bbcfa60621..efa4f3cdb5 100644 --- a/src/app/shared/form/builder/parsers/date-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/date-field-parser.spec.ts @@ -1,6 +1,4 @@ import { FormFieldModel } from '../models/form-field.model'; -import { DynamicConcatModel } from '../ds-dynamic-form-ui/models/ds-dynamic-concat.model'; -import { SeriesFieldParser } from './series-field-parser'; import { DateFieldParser } from './date-field-parser'; import { DynamicDsDatePickerModel } from '../ds-dynamic-form-ui/models/date-picker/date-picker.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; @@ -10,6 +8,7 @@ describe('DateFieldParser test suite', () => { let field: FormFieldModel; let initFormValues: any = {}; + const submissionId = '1234'; const parserOptions: ParserOptions = { readOnly: false, submissionScope: null, @@ -37,13 +36,13 @@ describe('DateFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new DateFieldParser(field, initFormValues, parserOptions); + const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions); expect(parser instanceof DateFieldParser).toBe(true); }); it('should return a DynamicDsDatePickerModel object when repeatable option is false', () => { - const parser = new DateFieldParser(field, initFormValues, parserOptions); + const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -56,7 +55,7 @@ describe('DateFieldParser test suite', () => { }; const expectedValue = '1983-11-18'; - const parser = new DateFieldParser(field, initFormValues, parserOptions); + const parser = new DateFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts b/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts new file mode 100644 index 0000000000..7dce05f18d --- /dev/null +++ b/src/app/shared/form/builder/parsers/disabled-field-parser.spec.ts @@ -0,0 +1,66 @@ +import { FormFieldModel } from '../models/form-field.model'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; +import { ParserOptions } from './parser-options'; +import { DisabledFieldParser } from './disabled-field-parser'; +import { DynamicDisabledModel } from '../ds-dynamic-form-ui/models/disabled/dynamic-disabled.model'; + +describe('DisabledFieldParser test suite', () => { + let field: FormFieldModel; + let initFormValues: any = {}; + + const submissionId = '1234'; + const parserOptions: ParserOptions = { + readOnly: false, + submissionScope: null, + authorityUuid: null + }; + + beforeEach(() => { + field = { + input: { + type: '' + }, + label: 'Description', + mandatory: 'false', + repeatable: false, + hints: 'Enter a description.', + selectableMetadata: [ + { + metadata: 'description' + } + ], + languageCodes: [] + } as FormFieldModel; + + }); + + it('should init parser properly', () => { + const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions); + + expect(parser instanceof DisabledFieldParser).toBe(true); + }); + + it('should return a DynamicDisabledModel object when repeatable option is false', () => { + const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions); + + const fieldModel = parser.parse(); + + expect(fieldModel instanceof DynamicDisabledModel).toBe(true); + }); + + it('should set init value properly', () => { + initFormValues = { + description: [ + new FormFieldMetadataValueObject('test description'), + ], + }; + const expectedValue ='test description'; + + const parser = new DisabledFieldParser(submissionId, field, initFormValues, parserOptions); + + const fieldModel = parser.parse(); + console.log(fieldModel); + expect(fieldModel.value).toEqual(expectedValue); + }); + +}); diff --git a/src/app/shared/form/builder/parsers/disabled-field-parser.ts b/src/app/shared/form/builder/parsers/disabled-field-parser.ts new file mode 100644 index 0000000000..db3e4ac8b9 --- /dev/null +++ b/src/app/shared/form/builder/parsers/disabled-field-parser.ts @@ -0,0 +1,16 @@ +import { FieldParser } from './field-parser'; +import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; +import { DsDynamicDisabledModelConfig, DynamicDisabledModel } from '../ds-dynamic-form-ui/models/disabled/dynamic-disabled.model'; + +/** + * Field parser for disabled fields + */ +export class DisabledFieldParser extends FieldParser { + + public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { + console.log(fieldValue); + const emptyModelConfig: DsDynamicDisabledModelConfig = this.initModel(null, label); + this.setValues(emptyModelConfig, fieldValue); + return new DynamicDisabledModel(emptyModelConfig) + } +} diff --git a/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts b/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts index 5dfdcfa5ce..8dbd68e05a 100644 --- a/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/dropdown-field-parser.spec.ts @@ -6,6 +6,7 @@ import { ParserOptions } from './parser-options'; describe('DropdownFieldParser test suite', () => { let field: FormFieldModel; + const submissionId = '1234'; const initFormValues = {}; const parserOptions: ParserOptions = { readOnly: false, @@ -35,13 +36,13 @@ describe('DropdownFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new DropdownFieldParser(field, initFormValues, parserOptions); + const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions); expect(parser instanceof DropdownFieldParser).toBe(true); }); it('should return a DynamicScrollableDropdownModel object when repeatable option is false', () => { - const parser = new DropdownFieldParser(field, initFormValues, parserOptions); + const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -50,7 +51,7 @@ describe('DropdownFieldParser test suite', () => { it('should throw when authority is not passed', () => { field.selectableMetadata[0].authority = null; - const parser = new DropdownFieldParser(field, initFormValues, parserOptions); + const parser = new DropdownFieldParser(submissionId, field, initFormValues, parserOptions); expect(() => parser.parse()) .toThrow(); diff --git a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts index 1623829b15..4816a2a073 100644 --- a/src/app/shared/form/builder/parsers/dropdown-field-parser.ts +++ b/src/app/shared/form/builder/parsers/dropdown-field-parser.ts @@ -1,4 +1,12 @@ -import { FieldParser } from './field-parser'; +import { Inject } from '@angular/core'; +import { FormFieldModel } from '../models/form-field.model'; +import { + CONFIG_DATA, + FieldParser, + INIT_FORM_VALUES, + PARSER_OPTIONS, + SUBMISSION_ID +} from './field-parser'; import { DynamicFormControlLayout, } from '@ng-dynamic-forms/core'; import { DynamicScrollableDropdownModel, @@ -6,9 +14,19 @@ import { } from '../ds-dynamic-form-ui/models/scrollable-dropdown/dynamic-scrollable-dropdown.model'; import { isNotEmpty } from '../../../empty.util'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; +import { ParserOptions } from './parser-options'; export class DropdownFieldParser extends FieldParser { + constructor( + @Inject(SUBMISSION_ID) submissionId: string, + @Inject(CONFIG_DATA) configData: FormFieldModel, + @Inject(INIT_FORM_VALUES) initFormValues, + @Inject(PARSER_OPTIONS) parserOptions: ParserOptions + ) { + super(submissionId, configData, initFormValues, parserOptions) + } + public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean): any { const dropdownModelConfig: DynamicScrollableDropdownModelConfig = this.initModel(null, label); let layout: DynamicFormControlLayout; diff --git a/src/app/shared/form/builder/parsers/field-parser.ts b/src/app/shared/form/builder/parsers/field-parser.ts index dd37a45fba..f7bf12353c 100644 --- a/src/app/shared/form/builder/parsers/field-parser.ts +++ b/src/app/shared/form/builder/parsers/field-parser.ts @@ -1,4 +1,5 @@ -import { hasValue, isNotEmpty, isNotNull, isNotUndefined } from '../../../empty.util'; +import { Inject, InjectionToken } from '@angular/core'; +import { hasValue, isNotEmpty, isNotNull, isNotUndefined, isEmpty } from '../../../empty.util'; import { FormFieldModel } from '../models/form-field.model'; import { uniqueId } from 'lodash'; @@ -12,12 +13,23 @@ import { DynamicFormControlLayout } from '@ng-dynamic-forms/core'; import { setLayout } from './parser.utils'; import { AuthorityOptions } from '../../../../core/integration/models/authority-options.model'; import { ParserOptions } from './parser-options'; +import { RelationshipOptions } from '../models/relationship-options.model'; + +export const SUBMISSION_ID: InjectionToken = new InjectionToken('submissionId'); +export const CONFIG_DATA: InjectionToken = new InjectionToken('configData'); +export const INIT_FORM_VALUES: InjectionToken = new InjectionToken('initFormValues'); +export const PARSER_OPTIONS: InjectionToken = new InjectionToken('parserOptions'); export abstract class FieldParser { protected fieldId: string; - constructor(protected configData: FormFieldModel, protected initFormValues, protected parserOptions: ParserOptions) { + constructor( + @Inject(SUBMISSION_ID) protected submissionId: string, + @Inject(CONFIG_DATA) protected configData: FormFieldModel, + @Inject(INIT_FORM_VALUES) protected initFormValues: any, + @Inject(PARSER_OPTIONS) protected parserOptions: ParserOptions + ) { } public abstract modelFactory(fieldValue?: FormFieldMetadataValueObject, label?: boolean): any; @@ -27,6 +39,7 @@ export abstract class FieldParser { && (this.configData.input.type !== 'list') && (this.configData.input.type !== 'tag') && (this.configData.input.type !== 'group') + && isEmpty(this.configData.selectableRelationship) ) { let arrayCounter = 0; let fieldArrayCounter = 0; @@ -36,6 +49,7 @@ export abstract class FieldParser { label: this.configData.label, initialCount: this.getInitArrayIndex(), notRepeatable: !this.configData.repeatable, + required: isNotEmpty(this.configData.mandatory), groupFactory: () => { let model; if ((arrayCounter === 0)) { @@ -71,7 +85,7 @@ export abstract class FieldParser { } else { const model = this.modelFactory(this.getInitFieldValue()); - if (model.hasLanguages) { + if (model.hasLanguages || isNotEmpty(model.relationship)) { setLayout(model, 'grid', 'control', 'col'); } return model; @@ -164,11 +178,11 @@ export abstract class FieldParser { return ids; } } else { - return null; + return [this.configData.selectableRelationship.relationshipType]; } } - protected initModel(id?: string, label = true, labelEmpty = false, setErrors = true) { + protected initModel(id?: string, label = true, setErrors = true) { const controlModel = Object.create(null); @@ -184,9 +198,15 @@ export abstract class FieldParser { // Set read only option controlModel.readOnly = this.parserOptions.readOnly; controlModel.disabled = this.parserOptions.readOnly; + if (hasValue(this.configData.selectableRelationship)) { + controlModel.relationship = Object.assign(new RelationshipOptions(), this.configData.selectableRelationship); + } + controlModel.repeatable = this.configData.repeatable; + controlModel.metadataFields = isNotEmpty(this.configData.selectableMetadata) ? this.configData.selectableMetadata.map((metadataObject) => metadataObject.metadata) : []; + controlModel.submissionId = this.submissionId; // Set label - this.setLabel(controlModel, label, labelEmpty); + this.setLabel(controlModel, label); controlModel.placeholder = this.configData.label; @@ -204,14 +224,14 @@ export abstract class FieldParser { if (this.configData.languageCodes && this.configData.languageCodes.length > 0) { (controlModel as DsDynamicInputModel).languageCodes = this.configData.languageCodes; } -/* (controlModel as DsDynamicInputModel).languageCodes = [{ - display: 'English', - code: 'en_US' - }, - { - display: 'Italian', - code: 'it_IT' - }];*/ + /* (controlModel as DsDynamicInputModel).languageCodes = [{ + display: 'English', + code: 'en_US' + }, + { + display: 'Italian', + code: 'it_IT' + }];*/ return controlModel; } @@ -222,26 +242,26 @@ export abstract class FieldParser { protected addPatternValidator(controlModel) { const regex = new RegExp(this.configData.input.regex); - controlModel.validators = Object.assign({}, controlModel.validators, {pattern: regex}); + controlModel.validators = Object.assign({}, controlModel.validators, { pattern: regex }); controlModel.errorMessages = Object.assign( {}, controlModel.errorMessages, - {pattern: 'error.validation.pattern'}); + { pattern: 'error.validation.pattern' }); } protected markAsRequired(controlModel) { controlModel.required = true; - controlModel.validators = Object.assign({}, controlModel.validators, {required: null}); + controlModel.validators = Object.assign({}, controlModel.validators, { required: null }); controlModel.errorMessages = Object.assign( {}, controlModel.errorMessages, - {required: this.configData.mandatoryMessage}); + { required: this.configData.mandatoryMessage }); } protected setLabel(controlModel, label = true, labelEmpty = false) { if (label) { - controlModel.label = (labelEmpty) ? ' ' : this.configData.label; + controlModel.label = this.configData.label; } } @@ -253,13 +273,13 @@ export abstract class FieldParser { if (key === 0) { controlModel.value = option.metadata; } - controlModel.options.push({label: option.label, value: option.metadata}); + controlModel.options.push({ label: option.label, value: option.metadata }); }); } } public setAuthorityOptions(controlModel, authorityUuid) { - if (isNotEmpty(this.configData.selectableMetadata[0].authority)) { + if (isNotEmpty(this.configData.selectableMetadata) && isNotEmpty(this.configData.selectableMetadata[0].authority)) { controlModel.authorityOptions = new AuthorityOptions( this.configData.selectableMetadata[0].authority, this.configData.selectableMetadata[0].metadata, diff --git a/src/app/shared/form/builder/parsers/list-field-parser.spec.ts b/src/app/shared/form/builder/parsers/list-field-parser.spec.ts index b2fa0b2089..fab5ec3888 100644 --- a/src/app/shared/form/builder/parsers/list-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/list-field-parser.spec.ts @@ -9,6 +9,7 @@ describe('ListFieldParser test suite', () => { let field: FormFieldModel; let initFormValues = {}; + const submissionId = '1234'; const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', @@ -37,13 +38,13 @@ describe('ListFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new ListFieldParser(field, initFormValues, parserOptions); + const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions); expect(parser instanceof ListFieldParser).toBe(true); }); it('should return a DynamicListCheckboxGroupModel object when repeatable option is true', () => { - const parser = new ListFieldParser(field, initFormValues, parserOptions); + const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -52,7 +53,7 @@ describe('ListFieldParser test suite', () => { it('should return a DynamicListRadioGroupModel object when repeatable option is false', () => { field.repeatable = false; - const parser = new ListFieldParser(field, initFormValues, parserOptions); + const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -65,7 +66,7 @@ describe('ListFieldParser test suite', () => { }; const expectedValue = [new FormFieldMetadataValueObject('test type')]; - const parser = new ListFieldParser(field, initFormValues, parserOptions); + const parser = new ListFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts b/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts index c45d39d5bb..5e14e0c013 100644 --- a/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/lookup-field-parser.spec.ts @@ -8,6 +8,7 @@ describe('LookupFieldParser test suite', () => { let field: FormFieldModel; let initFormValues = {}; + const submissionId = '1234'; const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', @@ -36,13 +37,13 @@ describe('LookupFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new LookupFieldParser(field, initFormValues, parserOptions); + const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions); expect(parser instanceof LookupFieldParser).toBe(true); }); it('should return a DynamicLookupModel object when repeatable option is false', () => { - const parser = new LookupFieldParser(field, initFormValues, parserOptions); + const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -55,7 +56,7 @@ describe('LookupFieldParser test suite', () => { }; const expectedValue = new FormFieldMetadataValueObject('test journal'); - const parser = new LookupFieldParser(field, initFormValues, parserOptions); + const parser = new LookupFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts b/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts index b324ba7a7e..adc1e90166 100644 --- a/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/lookup-name-field-parser.spec.ts @@ -1,7 +1,5 @@ import { FormFieldModel } from '../models/form-field.model'; import { FormFieldMetadataValueObject } from '../models/form-field-metadata-value.model'; -import { LookupFieldParser } from './lookup-field-parser'; -import { DynamicLookupModel } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup.model'; import { LookupNameFieldParser } from './lookup-name-field-parser'; import { DynamicLookupNameModel } from '../ds-dynamic-form-ui/models/lookup/dynamic-lookup-name.model'; import { ParserOptions } from './parser-options'; @@ -10,6 +8,7 @@ describe('LookupNameFieldParser test suite', () => { let field: FormFieldModel; let initFormValues = {}; + const submissionId = '1234'; const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', @@ -38,13 +37,13 @@ describe('LookupNameFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new LookupNameFieldParser(field, initFormValues, parserOptions); + const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions); expect(parser instanceof LookupNameFieldParser).toBe(true); }); it('should return a DynamicLookupNameModel object when repeatable option is false', () => { - const parser = new LookupNameFieldParser(field, initFormValues, parserOptions); + const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -57,7 +56,7 @@ describe('LookupNameFieldParser test suite', () => { }; const expectedValue = new FormFieldMetadataValueObject('test author'); - const parser = new LookupNameFieldParser(field, initFormValues, parserOptions); + const parser = new LookupNameFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/name-field-parser.spec.ts b/src/app/shared/form/builder/parsers/name-field-parser.spec.ts index 889244e8f2..1b0c637030 100644 --- a/src/app/shared/form/builder/parsers/name-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/name-field-parser.spec.ts @@ -10,6 +10,7 @@ describe('NameFieldParser test suite', () => { let field3: FormFieldModel; let initFormValues: any = {}; + const submissionId = '1234'; const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', @@ -69,13 +70,13 @@ describe('NameFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new NameFieldParser(field1, initFormValues, parserOptions); + const parser = new NameFieldParser(submissionId, field1, initFormValues, parserOptions); expect(parser instanceof NameFieldParser).toBe(true); }); it('should return a DynamicConcatModel object when repeatable option is false', () => { - const parser = new NameFieldParser(field2, initFormValues, parserOptions); + const parser = new NameFieldParser(submissionId, field2, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -83,7 +84,7 @@ describe('NameFieldParser test suite', () => { }); it('should return a DynamicConcatModel object with the correct separator', () => { - const parser = new NameFieldParser(field2, initFormValues, parserOptions); + const parser = new NameFieldParser(submissionId, field2, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -96,7 +97,7 @@ describe('NameFieldParser test suite', () => { }; const expectedValue = new FormFieldMetadataValueObject('test, name'); - const parser = new NameFieldParser(field1, initFormValues, parserOptions); + const parser = new NameFieldParser(submissionId, field1, initFormValues, parserOptions); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/name-field-parser.ts b/src/app/shared/form/builder/parsers/name-field-parser.ts index 896b3cc478..e5ecb034ea 100644 --- a/src/app/shared/form/builder/parsers/name-field-parser.ts +++ b/src/app/shared/form/builder/parsers/name-field-parser.ts @@ -1,10 +1,17 @@ +import { Inject } from '@angular/core'; import { FormFieldModel } from '../models/form-field.model'; import { ConcatFieldParser } from './concat-field-parser'; +import { CONFIG_DATA, INIT_FORM_VALUES, PARSER_OPTIONS, SUBMISSION_ID } from './field-parser'; import { ParserOptions } from './parser-options'; export class NameFieldParser extends ConcatFieldParser { - constructor(protected configData: FormFieldModel, protected initFormValues, protected parserOptions: ParserOptions) { - super(configData, initFormValues, parserOptions, ',', 'form.last-name', 'form.first-name'); + constructor( + @Inject(SUBMISSION_ID) submissionId: string, + @Inject(CONFIG_DATA) configData: FormFieldModel, + @Inject(INIT_FORM_VALUES) initFormValues, + @Inject(PARSER_OPTIONS) parserOptions: ParserOptions + ) { + super(submissionId, configData, initFormValues, parserOptions, ',', 'form.last-name', 'form.first-name'); } } diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts index 89c576bf3a..4668b3017d 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.spec.ts @@ -10,6 +10,7 @@ describe('OneboxFieldParser test suite', () => { let field2: FormFieldModel; let field3: FormFieldModel; + const submissionId = '1234'; const initFormValues = {}; const parserOptions: ParserOptions = { readOnly: false, @@ -70,13 +71,13 @@ describe('OneboxFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new OneboxFieldParser(field1, initFormValues, parserOptions); + const parser = new OneboxFieldParser(submissionId, field1, initFormValues, parserOptions); expect(parser instanceof OneboxFieldParser).toBe(true); }); it('should return a DynamicQualdropModel object when selectableMetadata is multiple', () => { - const parser = new OneboxFieldParser(field2, initFormValues, parserOptions); + const parser = new OneboxFieldParser(submissionId, field2, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -84,7 +85,7 @@ describe('OneboxFieldParser test suite', () => { }); it('should return a DsDynamicInputModel object when selectableMetadata is not multiple', () => { - const parser = new OneboxFieldParser(field3, initFormValues, parserOptions); + const parser = new OneboxFieldParser(submissionId, field3, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -92,7 +93,7 @@ describe('OneboxFieldParser test suite', () => { }); it('should return a DynamicTypeaheadModel object when selectableMetadata has authority', () => { - const parser = new OneboxFieldParser(field1, initFormValues, parserOptions); + const parser = new OneboxFieldParser(submissionId, field1, initFormValues, parserOptions); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/onebox-field-parser.ts b/src/app/shared/form/builder/parsers/onebox-field-parser.ts index 284656cc95..d69c9d4677 100644 --- a/src/app/shared/form/builder/parsers/onebox-field-parser.ts +++ b/src/app/shared/form/builder/parsers/onebox-field-parser.ts @@ -56,8 +56,10 @@ export class OneboxFieldParser extends FieldParser { inputSelectGroup.group = []; inputSelectGroup.legend = this.configData.label; inputSelectGroup.hint = this.configData.hints; + this.setLabel(inputSelectGroup, label); + inputSelectGroup.required = isNotEmpty(this.configData.mandatory); - const selectModelConfig: DynamicSelectModelConfig = this.initModel(newId + QUALDROP_METADATA_SUFFIX, label); + const selectModelConfig: DynamicSelectModelConfig = this.initModel(newId + QUALDROP_METADATA_SUFFIX, label, false); selectModelConfig.hint = null; this.setOptions(selectModelConfig); if (isNotEmpty(fieldValue)) { @@ -65,11 +67,11 @@ export class OneboxFieldParser extends FieldParser { } inputSelectGroup.group.push(new DynamicSelectModel(selectModelConfig, clsSelect)); - const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + QUALDROP_VALUE_SUFFIX, label, true); + const inputModelConfig: DsDynamicInputModelConfig = this.initModel(newId + QUALDROP_VALUE_SUFFIX, label, false); inputModelConfig.hint = null; this.setValues(inputModelConfig, fieldValue); - inputSelectGroup.readOnly = selectModelConfig.disabled && inputModelConfig.readOnly; + inputSelectGroup.group.push(new DsDynamicInputModel(inputModelConfig, clsInput)); return new DynamicQualdropModel(inputSelectGroup, clsGroup); diff --git a/src/app/shared/form/builder/parsers/parser-factory.ts b/src/app/shared/form/builder/parsers/parser-factory.ts index 2cbee18783..d674007da4 100644 --- a/src/app/shared/form/builder/parsers/parser-factory.ts +++ b/src/app/shared/form/builder/parsers/parser-factory.ts @@ -1,6 +1,12 @@ +import { StaticProvider } from '@angular/core'; import { ParserType } from './parser-type'; -import { GenericConstructor } from '../../../../core/shared/generic-constructor'; -import { FieldParser } from './field-parser'; +import { + CONFIG_DATA, + FieldParser, + INIT_FORM_VALUES, + PARSER_OPTIONS, + SUBMISSION_ID +} from './field-parser'; import { DateFieldParser } from './date-field-parser'; import { DropdownFieldParser } from './dropdown-field-parser'; import { RelationGroupFieldParser } from './relation-group-field-parser'; @@ -12,44 +18,105 @@ import { NameFieldParser } from './name-field-parser'; import { SeriesFieldParser } from './series-field-parser'; import { TagFieldParser } from './tag-field-parser'; import { TextareaFieldParser } from './textarea-field-parser'; +import { DisabledFieldParser } from './disabled-field-parser'; +const fieldParserDeps = [ + SUBMISSION_ID, + CONFIG_DATA, + INIT_FORM_VALUES, + PARSER_OPTIONS, +]; + +/** + * Method to retrieve a field parder with its providers based on the input type + */ export class ParserFactory { - public static getConstructor(type: ParserType): GenericConstructor { + public static getProvider(type: ParserType): StaticProvider { switch (type) { case ParserType.Date: { - return DateFieldParser + return { + provide: FieldParser, + useClass: DateFieldParser, + deps: [...fieldParserDeps] + } } case ParserType.Dropdown: { - return DropdownFieldParser + return { + provide: FieldParser, + useClass: DropdownFieldParser, + deps: [...fieldParserDeps] + } } case ParserType.RelationGroup: { - return RelationGroupFieldParser + return { + provide: FieldParser, + useClass: RelationGroupFieldParser, + deps: [...fieldParserDeps] + } } case ParserType.List: { - return ListFieldParser + return { + provide: FieldParser, + useClass: ListFieldParser, + deps: [...fieldParserDeps] + } } case ParserType.Lookup: { - return LookupFieldParser + return { + provide: FieldParser, + useClass: LookupFieldParser, + deps: [...fieldParserDeps] + } } case ParserType.LookupName: { - return LookupNameFieldParser + return { + provide: FieldParser, + useClass: LookupNameFieldParser, + deps: [...fieldParserDeps] + } } case ParserType.Onebox: { - return OneboxFieldParser + return { + provide: FieldParser, + useClass: OneboxFieldParser, + deps: [...fieldParserDeps] + } } case ParserType.Name: { - return NameFieldParser + return { + provide: FieldParser, + useClass: NameFieldParser, + deps: [...fieldParserDeps] + } } case ParserType.Series: { - return SeriesFieldParser + return { + provide: FieldParser, + useClass: SeriesFieldParser, + deps: [...fieldParserDeps] + } } case ParserType.Tag: { - return TagFieldParser + return { + provide: FieldParser, + useClass: TagFieldParser, + deps: [...fieldParserDeps] + } } case ParserType.Textarea: { - return TextareaFieldParser + return { + provide: FieldParser, + useClass: TextareaFieldParser, + deps: [...fieldParserDeps] + } + } + case undefined: { + return { + provide: FieldParser, + useClass: DisabledFieldParser, + deps: [...fieldParserDeps] + } } - default: { return undefined; } diff --git a/src/app/shared/form/builder/parsers/parser-type.ts b/src/app/shared/form/builder/parsers/parser-type.ts index a9af87d73f..f43d4654a0 100644 --- a/src/app/shared/form/builder/parsers/parser-type.ts +++ b/src/app/shared/form/builder/parsers/parser-type.ts @@ -5,6 +5,7 @@ export enum ParserType { List = 'list', Lookup = 'lookup', LookupName = 'lookup-name', + LookupRelation = 'lookup-relation', Onebox = 'onebox', Name = 'name', Series = 'series', diff --git a/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts b/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts index e6bf0dc2c8..84f3df0365 100644 --- a/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/relation-group-field-parser.spec.ts @@ -8,6 +8,7 @@ describe('RelationGroupFieldParser test suite', () => { let field: FormFieldModel; let initFormValues = {}; + const submissionId = '1234'; const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', @@ -71,13 +72,13 @@ describe('RelationGroupFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new RelationGroupFieldParser(field, initFormValues, parserOptions); + const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions); expect(parser instanceof RelationGroupFieldParser).toBe(true); }); it('should return a DynamicRelationGroupModel object', () => { - const parser = new RelationGroupFieldParser(field, initFormValues, parserOptions); + const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -86,7 +87,7 @@ describe('RelationGroupFieldParser test suite', () => { it('should throw when rows configuration is empty', () => { field.rows = null; - const parser = new RelationGroupFieldParser(field, initFormValues, parserOptions); + const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions); expect(() => parser.parse()) .toThrow(); @@ -97,7 +98,7 @@ describe('RelationGroupFieldParser test suite', () => { author: [new FormFieldMetadataValueObject('test author')], affiliation: [new FormFieldMetadataValueObject('test affiliation')] }; - const parser = new RelationGroupFieldParser(field, initFormValues, parserOptions); + const parser = new RelationGroupFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); const expectedValue = [{ diff --git a/src/app/shared/form/builder/parsers/relation-group-field-parser.ts b/src/app/shared/form/builder/parsers/relation-group-field-parser.ts index b3f6e749f3..01699d9e78 100644 --- a/src/app/shared/form/builder/parsers/relation-group-field-parser.ts +++ b/src/app/shared/form/builder/parsers/relation-group-field-parser.ts @@ -15,6 +15,7 @@ export class RelationGroupFieldParser extends FieldParser { public modelFactory(fieldValue?: FormFieldMetadataValueObject | any, label?: boolean) { const modelConfiguration: DynamicRelationGroupModelConfig = this.initModel(null, label); + modelConfiguration.submissionId = this.submissionId; modelConfiguration.scopeUUID = this.parserOptions.authorityUuid; modelConfiguration.submissionScope = this.parserOptions.submissionScope; if (this.configData && this.configData.rows && this.configData.rows.length > 0) { diff --git a/src/app/shared/form/builder/parsers/row-parser.spec.ts b/src/app/shared/form/builder/parsers/row-parser.spec.ts index 58b1d1de99..435c6a6426 100644 --- a/src/app/shared/form/builder/parsers/row-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/row-parser.spec.ts @@ -17,6 +17,7 @@ describe('RowParser test suite', () => { let row9: FormRowModel; let row10: FormRowModel; + const submissionId = '1234'; const scopeUUID = 'testScopeUUID'; const initFormValues = {}; const submissionScope = 'WORKSPACE'; @@ -328,76 +329,98 @@ describe('RowParser test suite', () => { }); it('should init parser properly', () => { - let parser = new RowParser(row1, scopeUUID, initFormValues, submissionScope, readOnly); - - expect(parser instanceof RowParser).toBe(true); - - parser = new RowParser(row2, scopeUUID, initFormValues, submissionScope, readOnly); - - expect(parser instanceof RowParser).toBe(true); - - parser = new RowParser(row3, scopeUUID, initFormValues, submissionScope, readOnly); - - expect(parser instanceof RowParser).toBe(true); - - parser = new RowParser(row4, scopeUUID, initFormValues, submissionScope, readOnly); - - expect(parser instanceof RowParser).toBe(true); - - parser = new RowParser(row5, scopeUUID, initFormValues, submissionScope, readOnly); - - expect(parser instanceof RowParser).toBe(true); - - parser = new RowParser(row6, scopeUUID, initFormValues, submissionScope, readOnly); - - expect(parser instanceof RowParser).toBe(true); - - parser = new RowParser(row7, scopeUUID, initFormValues, submissionScope, readOnly); - - expect(parser instanceof RowParser).toBe(true); - - parser = new RowParser(row8, scopeUUID, initFormValues, submissionScope, readOnly); - - expect(parser instanceof RowParser).toBe(true); - - parser = new RowParser(row9, scopeUUID, initFormValues, submissionScope, readOnly); - - expect(parser instanceof RowParser).toBe(true); - - parser = new RowParser(row10, scopeUUID, initFormValues, submissionScope, readOnly); + const parser = new RowParser(undefined); expect(parser instanceof RowParser).toBe(true); }); - it('should return a DynamicRowGroupModel object', () => { - const parser = new RowParser(row1, scopeUUID, initFormValues, submissionScope, readOnly); + describe('parse', () => { + it('should return a DynamicRowGroupModel object', () => { + const parser = new RowParser(undefined); - const rowModel = parser.parse(); + const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly); - expect(rowModel instanceof DynamicRowGroupModel).toBe(true); - }); + expect(rowModel instanceof DynamicRowGroupModel).toBe(true); + }); - it('should return a row with three fields', () => { - const parser = new RowParser(row1, scopeUUID, initFormValues, submissionScope, readOnly); + it('should return a row with three fields', () => { + const parser = new RowParser(undefined); - const rowModel = parser.parse(); + const rowModel = parser.parse(submissionId, row1, scopeUUID, initFormValues, submissionScope, readOnly); - expect((rowModel as DynamicRowGroupModel).group.length).toBe(3); - }); + expect((rowModel as DynamicRowGroupModel).group.length).toBe(3); + }); - it('should return a DynamicRowArrayModel object', () => { - const parser = new RowParser(row2, scopeUUID, initFormValues, submissionScope, readOnly); + it('should return a DynamicRowArrayModel object', () => { + const parser = new RowParser(undefined); - const rowModel = parser.parse(); + const rowModel = parser.parse(submissionId, row2, scopeUUID, initFormValues, submissionScope, readOnly); - expect(rowModel instanceof DynamicRowArrayModel).toBe(true); - }); + expect(rowModel instanceof DynamicRowArrayModel).toBe(true); + }); - it('should return a row that contains only scoped fields', () => { - const parser = new RowParser(row3, scopeUUID, initFormValues, submissionScope, readOnly); + it('should return a row that contains only scoped fields', () => { + const parser = new RowParser(undefined); - const rowModel = parser.parse(); + const rowModel = parser.parse(submissionId, row3, scopeUUID, initFormValues, submissionScope, readOnly); - expect((rowModel as DynamicRowGroupModel).group.length).toBe(1); + expect((rowModel as DynamicRowGroupModel).group.length).toBe(1); + }); + + it('should be able to parse a dropdown combo field', () => { + const parser = new RowParser(undefined); + + const rowModel = parser.parse(submissionId, row4, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(rowModel).toBeDefined(); + }); + + it('should be able to parse a lookup-name field', () => { + const parser = new RowParser(undefined); + + const rowModel = parser.parse(submissionId, row5, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(rowModel).toBeDefined(); + }); + + it('should be able to parse a list field', () => { + const parser = new RowParser(undefined); + + const rowModel = parser.parse(submissionId, row6, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(rowModel).toBeDefined(); + }); + + it('should be able to parse a date field', () => { + const parser = new RowParser(undefined); + + const rowModel = parser.parse(submissionId, row7, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(rowModel).toBeDefined(); + }); + + it('should be able to parse a tag field', () => { + const parser = new RowParser(undefined); + + const rowModel = parser.parse(submissionId, row8, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(rowModel).toBeDefined(); + }); + + it('should be able to parse a textarea field', () => { + const parser = new RowParser(undefined); + + const rowModel = parser.parse(submissionId, row9, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(rowModel).toBeDefined(); + }); + + it('should be able to parse a group field', () => { + const parser = new RowParser(undefined); + + const rowModel = parser.parse(submissionId, row10, scopeUUID, initFormValues, submissionScope, readOnly); + + expect(rowModel).toBeDefined(); + }); }); }); diff --git a/src/app/shared/form/builder/parsers/row-parser.ts b/src/app/shared/form/builder/parsers/row-parser.ts index 0bb8a0e89a..4938b9859e 100644 --- a/src/app/shared/form/builder/parsers/row-parser.ts +++ b/src/app/shared/form/builder/parsers/row-parser.ts @@ -1,30 +1,46 @@ -import { DYNAMIC_FORM_CONTROL_TYPE_ARRAY, DynamicFormGroupModelConfig } from '@ng-dynamic-forms/core'; +import { Injectable, Injector } from '@angular/core'; +import { + DYNAMIC_FORM_CONTROL_TYPE_ARRAY, + DynamicFormGroupModelConfig +} from '@ng-dynamic-forms/core'; import { uniqueId } from 'lodash'; import { IntegrationSearchOptions } from '../../../../core/integration/models/integration-options.model'; -import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from '../ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; -import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; import { isEmpty } from '../../../empty.util'; -import { setLayout } from './parser.utils'; +import { DynamicRowGroupModel } from '../ds-dynamic-form-ui/models/ds-dynamic-row-group-model'; +import { DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP } from '../ds-dynamic-form-ui/models/relation-group/dynamic-relation-group.model'; import { FormFieldModel } from '../models/form-field.model'; -import { ParserType } from './parser-type'; -import { ParserOptions } from './parser-options'; +import { + CONFIG_DATA, + FieldParser, + INIT_FORM_VALUES, + PARSER_OPTIONS, + SUBMISSION_ID +} from './field-parser'; import { ParserFactory } from './parser-factory'; +import { ParserOptions } from './parser-options'; +import { ParserType } from './parser-type'; +import { setLayout } from './parser.utils'; export const ROW_ID_PREFIX = 'df-row-group-config-'; -export class RowParser { - protected authorityOptions: IntegrationSearchOptions; +@Injectable({ + providedIn: 'root' +}) - constructor(protected rowData, - protected scopeUUID, - protected initFormValues: any, - protected submissionScope, - protected readOnly: boolean) { - this.authorityOptions = new IntegrationSearchOptions(scopeUUID); +/** + * Parser the submission data for a single row + */ +export class RowParser { + constructor(private parentInjector: Injector) { } - public parse(): DynamicRowGroupModel { + public parse(submissionId: string, + rowData, + scopeUUID, + initFormValues: any, + submissionScope, + readOnly: boolean): DynamicRowGroupModel { let fieldModel: any = null; let parsedResult = null; const config: DynamicFormGroupModelConfig = { @@ -32,31 +48,44 @@ export class RowParser { group: [], }; - const scopedFields: FormFieldModel[] = this.filterScopedFields(this.rowData.fields); + const authorityOptions = new IntegrationSearchOptions(scopeUUID); + + const scopedFields: FormFieldModel[] = this.filterScopedFields(rowData.fields, submissionScope); const layoutDefaultGridClass = ' col-sm-' + Math.trunc(12 / scopedFields.length); const layoutClass = ' d-flex flex-column justify-content-start'; const parserOptions: ParserOptions = { - readOnly: this.readOnly, - submissionScope: this.submissionScope, - authorityUuid: this.authorityOptions.uuid + readOnly: readOnly, + submissionScope: submissionScope, + authorityUuid: authorityOptions.uuid }; // Iterate over row's fields scopedFields.forEach((fieldData: FormFieldModel) => { const layoutFieldClass = (fieldData.style || layoutDefaultGridClass) + layoutClass; - const parserCo = ParserFactory.getConstructor(fieldData.input.type as ParserType); - if (parserCo) { - fieldModel = new parserCo(fieldData, this.initFormValues, parserOptions).parse(); + const parserProvider = ParserFactory.getProvider(fieldData.input.type as ParserType); + if (parserProvider) { + const fieldInjector = Injector.create({ + providers: [ + parserProvider, + { provide: SUBMISSION_ID, useValue: submissionId }, + { provide: CONFIG_DATA, useValue: fieldData }, + { provide: INIT_FORM_VALUES, useValue: initFormValues }, + { provide: PARSER_OPTIONS, useValue: parserOptions } + ], + parent: this.parentInjector + }); + + fieldModel = fieldInjector.get(FieldParser).parse(); } else { - throw new Error(`unknown form control model type "${fieldData.input.type}" defined for Input field with label "${fieldData.label}".`, ); + throw new Error(`unknown form control model type "${fieldData.input.type}" defined for Input field with label "${fieldData.label}".`,); } if (fieldModel) { if (fieldModel.type === DYNAMIC_FORM_CONTROL_TYPE_ARRAY || fieldModel.type === DYNAMIC_FORM_CONTROL_TYPE_RELATION_GROUP) { - if (this.rowData.fields.length > 1) { + if (rowData.fields.length > 1) { setLayout(fieldModel, 'grid', 'host', layoutFieldClass); config.group.push(fieldModel); // if (isEmpty(parsedResult)) { @@ -98,15 +127,15 @@ export class RowParser { return parsedResult; } - checksFieldScope(fieldScope) { - return (isEmpty(fieldScope) || isEmpty(this.submissionScope) || fieldScope === this.submissionScope); + checksFieldScope(fieldScope, submissionScope) { + return (isEmpty(fieldScope) || isEmpty(submissionScope) || fieldScope === submissionScope); } - filterScopedFields(fields: FormFieldModel[]): FormFieldModel[] { + filterScopedFields(fields: FormFieldModel[], submissionScope): FormFieldModel[] { const filteredFields: FormFieldModel[] = []; fields.forEach((field: FormFieldModel) => { // Whether field scope doesn't match the submission scope, skip it - if (this.checksFieldScope(field.scope)) { + if (this.checksFieldScope(field.scope, submissionScope)) { filteredFields.push(field); } }); diff --git a/src/app/shared/form/builder/parsers/series-field-parser.spec.ts b/src/app/shared/form/builder/parsers/series-field-parser.spec.ts index 95351d027f..ceb4e96320 100644 --- a/src/app/shared/form/builder/parsers/series-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/series-field-parser.spec.ts @@ -8,6 +8,7 @@ describe('SeriesFieldParser test suite', () => { let field: FormFieldModel; let initFormValues: any = {}; + const submissionId = '1234'; const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', @@ -32,13 +33,13 @@ describe('SeriesFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new SeriesFieldParser(field, initFormValues, parserOptions); + const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions); expect(parser instanceof SeriesFieldParser).toBe(true); }); it('should return a DynamicConcatModel object when repeatable option is false', () => { - const parser = new SeriesFieldParser(field, initFormValues, parserOptions); + const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -46,7 +47,7 @@ describe('SeriesFieldParser test suite', () => { }); it('should return a DynamicConcatModel object with the correct separator', () => { - const parser = new SeriesFieldParser(field, initFormValues, parserOptions); + const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -59,7 +60,7 @@ describe('SeriesFieldParser test suite', () => { }; const expectedValue = new FormFieldMetadataValueObject('test; series'); - const parser = new SeriesFieldParser(field, initFormValues, parserOptions); + const parser = new SeriesFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/series-field-parser.ts b/src/app/shared/form/builder/parsers/series-field-parser.ts index 9857b4e993..36ee9c36c1 100644 --- a/src/app/shared/form/builder/parsers/series-field-parser.ts +++ b/src/app/shared/form/builder/parsers/series-field-parser.ts @@ -1,10 +1,17 @@ +import { Inject } from '@angular/core'; import { FormFieldModel } from '../models/form-field.model'; import { ConcatFieldParser } from './concat-field-parser'; +import { CONFIG_DATA, INIT_FORM_VALUES, PARSER_OPTIONS, SUBMISSION_ID } from './field-parser'; import { ParserOptions } from './parser-options'; export class SeriesFieldParser extends ConcatFieldParser { - constructor(protected configData: FormFieldModel, protected initFormValues, protected parserOptions: ParserOptions) { - super(configData, initFormValues, parserOptions, ';'); + constructor( + @Inject(SUBMISSION_ID) submissionId: string, + @Inject(CONFIG_DATA) configData: FormFieldModel, + @Inject(INIT_FORM_VALUES) initFormValues, + @Inject(PARSER_OPTIONS) parserOptions: ParserOptions + ) { + super(submissionId, configData, initFormValues, parserOptions, ';'); } } diff --git a/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts b/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts index 3051dc6395..90449e62e5 100644 --- a/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/tag-field-parser.spec.ts @@ -8,6 +8,7 @@ describe('TagFieldParser test suite', () => { let field: FormFieldModel; let initFormValues: any = {}; + const submissionId = '1234'; const parserOptions: ParserOptions = { readOnly: false, submissionScope: 'testScopeUUID', @@ -36,13 +37,13 @@ describe('TagFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new TagFieldParser(field, initFormValues, parserOptions); + const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions); expect(parser instanceof TagFieldParser).toBe(true); }); it('should return a DynamicTagModel object when repeatable option is false', () => { - const parser = new TagFieldParser(field, initFormValues, parserOptions); + const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -57,7 +58,7 @@ describe('TagFieldParser test suite', () => { ], }; - const parser = new TagFieldParser(field, initFormValues, parserOptions); + const parser = new TagFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts index c26d758e48..167f126cf2 100644 --- a/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts +++ b/src/app/shared/form/builder/parsers/textarea-field-parser.spec.ts @@ -8,6 +8,7 @@ describe('TextareaFieldParser test suite', () => { let field: FormFieldModel; let initFormValues: any = {}; + const submissionId = '1234'; const parserOptions: ParserOptions = { readOnly: false, submissionScope: null, @@ -34,13 +35,13 @@ describe('TextareaFieldParser test suite', () => { }); it('should init parser properly', () => { - const parser = new TextareaFieldParser(field, initFormValues, parserOptions); + const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions); expect(parser instanceof TextareaFieldParser).toBe(true); }); it('should return a DsDynamicTextAreaModel object when repeatable option is false', () => { - const parser = new TextareaFieldParser(field, initFormValues, parserOptions); + const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); @@ -55,7 +56,7 @@ describe('TextareaFieldParser test suite', () => { }; const expectedValue ='test description'; - const parser = new TextareaFieldParser(field, initFormValues, parserOptions); + const parser = new TextareaFieldParser(submissionId, field, initFormValues, parserOptions); const fieldModel = parser.parse(); diff --git a/src/app/shared/form/form.component.html b/src/app/shared/form/form.component.html index 21d4a81659..510bf7291b 100644 --- a/src/app/shared/form/form.component.html +++ b/src/app/shared/form/form.component.html @@ -13,10 +13,10 @@ - +
-
+
- +
-
+
- +
diff --git a/src/app/+search-page/search-sidebar/search-sidebar.component.scss b/src/app/shared/search/search-sidebar/search-sidebar.component.scss similarity index 100% rename from src/app/+search-page/search-sidebar/search-sidebar.component.scss rename to src/app/shared/search/search-sidebar/search-sidebar.component.scss diff --git a/src/app/+search-page/search-sidebar/search-sidebar.component.spec.ts b/src/app/shared/search/search-sidebar/search-sidebar.component.spec.ts similarity index 100% rename from src/app/+search-page/search-sidebar/search-sidebar.component.spec.ts rename to src/app/shared/search/search-sidebar/search-sidebar.component.spec.ts diff --git a/src/app/+search-page/search-sidebar/search-sidebar.component.ts b/src/app/shared/search/search-sidebar/search-sidebar.component.ts similarity index 93% rename from src/app/+search-page/search-sidebar/search-sidebar.component.ts rename to src/app/shared/search/search-sidebar/search-sidebar.component.ts index 9ee0a74942..42e8a444bc 100644 --- a/src/app/+search-page/search-sidebar/search-sidebar.component.ts +++ b/src/app/shared/search/search-sidebar/search-sidebar.component.ts @@ -34,6 +34,11 @@ export class SearchSidebarComponent { */ @Input() viewModeList; + /** + * Whether to show the view mode switch + */ + @Input() showViewModes = true; + /** * True when the search component should show results on the current page */ diff --git a/src/app/+search-page/search-switch-configuration/search-configuration-option.model.ts b/src/app/shared/search/search-switch-configuration/search-configuration-option.model.ts similarity index 100% rename from src/app/+search-page/search-switch-configuration/search-configuration-option.model.ts rename to src/app/shared/search/search-switch-configuration/search-configuration-option.model.ts diff --git a/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.html b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.html similarity index 100% rename from src/app/+search-page/search-switch-configuration/search-switch-configuration.component.html rename to src/app/shared/search/search-switch-configuration/search-switch-configuration.component.html diff --git a/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.scss b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.scss new file mode 100644 index 0000000000..e69de29bb2 diff --git a/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.spec.ts b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts similarity index 87% rename from src/app/+search-page/search-switch-configuration/search-switch-configuration.component.spec.ts rename to src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts index 602dee33e6..05108905f2 100644 --- a/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.spec.ts +++ b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.spec.ts @@ -6,13 +6,16 @@ import { of as observableOf } from 'rxjs'; import { TranslateLoader, TranslateModule } from '@ngx-translate/core'; import { SearchSwitchConfigurationComponent } from './search-switch-configuration.component'; -import { MYDSPACE_ROUTE, SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component'; -import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service-stub'; -import { MockTranslateLoader } from '../../shared/mocks/mock-translate-loader'; import { NavigationExtras, Router } from '@angular/router'; -import { RouterStub } from '../../shared/testing/router-stub'; -import { MyDSpaceConfigurationValueType } from '../../+my-dspace-page/my-dspace-configuration-value-type'; -import { SearchService } from '../search-service/search.service'; +import { SearchConfigurationServiceStub } from '../../testing/search-configuration-service-stub'; +import { RouterStub } from '../../testing/router-stub'; +import { SearchService } from '../../../core/shared/search/search.service'; +import { + MYDSPACE_ROUTE, + SEARCH_CONFIG_SERVICE +} from '../../../+my-dspace-page/my-dspace-page.component'; +import { MyDSpaceConfigurationValueType } from '../../../+my-dspace-page/my-dspace-configuration-value-type'; +import { MockTranslateLoader } from '../../mocks/mock-translate-loader'; describe('SearchSwitchConfigurationComponent', () => { diff --git a/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.ts b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts similarity index 83% rename from src/app/+search-page/search-switch-configuration/search-switch-configuration.component.ts rename to src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts index 1ce1bf84ec..73312e072e 100644 --- a/src/app/+search-page/search-switch-configuration/search-switch-configuration.component.ts +++ b/src/app/shared/search/search-switch-configuration/search-switch-configuration.component.ts @@ -3,12 +3,13 @@ import { NavigationExtras, Router } from '@angular/router'; import { Subscription } from 'rxjs'; -import { hasValue } from '../../shared/empty.util'; -import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component'; -import { SearchConfigurationService } from '../search-service/search-configuration.service'; -import { MyDSpaceConfigurationValueType } from '../../+my-dspace-page/my-dspace-configuration-value-type'; +import { hasValue } from '../../empty.util'; +import { SEARCH_CONFIG_SERVICE } from '../../../+my-dspace-page/my-dspace-page.component'; +import { SearchConfigurationService } from '../../../core/shared/search/search-configuration.service'; +import { MyDSpaceConfigurationValueType } from '../../../+my-dspace-page/my-dspace-configuration-value-type'; import { SearchConfigurationOption } from './search-configuration-option.model'; -import { SearchService } from '../search-service/search.service'; +import { SearchService } from '../../../core/shared/search/search.service'; +import { currentPath } from '../../utils/route.utils'; @Component({ selector: 'ds-search-switch-configuration', @@ -87,7 +88,7 @@ export class SearchSwitchConfigurationComponent implements OnDestroy, OnInit { */ public getSearchLink(): string { if (this.inPlaceSearch) { - return './'; + return currentPath(this.router); } return this.searchService.getSearchLink(); } diff --git a/src/app/shared/shared.module.ts b/src/app/shared/shared.module.ts index baab1d02e3..00826ccf43 100644 --- a/src/app/shared/shared.module.ts +++ b/src/app/shared/shared.module.ts @@ -132,6 +132,30 @@ import { ClaimedTaskActionsReturnToPoolComponent } from './mydspace-actions/clai import { ItemDetailPreviewFieldComponent } from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component'; import { AbstractTrackableComponent } from './trackable/abstract-trackable.component'; import { ComcolMetadataComponent } from './comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component'; +import { DsDynamicLookupRelationModalComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/dynamic-lookup-relation-modal.component'; +import { SearchResultsComponent } from './search/search-results/search-results.component'; +import { SearchSidebarComponent } from './search/search-sidebar/search-sidebar.component'; +import { SearchSettingsComponent } from './search/search-settings/search-settings.component'; +import { CollectionSearchResultGridElementComponent } from './object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component'; +import { CommunitySearchResultGridElementComponent } from './object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'; +import { SearchFiltersComponent } from './search/search-filters/search-filters.component'; +import { SearchFilterComponent } from './search/search-filters/search-filter/search-filter.component'; +import { SearchFacetFilterComponent } from './search/search-filters/search-filter/search-facet-filter/search-facet-filter.component'; +import { SearchLabelsComponent } from './search/search-labels/search-labels.component'; +import { SearchFacetFilterWrapperComponent } from './search/search-filters/search-filter/search-facet-filter-wrapper/search-facet-filter-wrapper.component'; +import { SearchRangeFilterComponent } from './search/search-filters/search-filter/search-range-filter/search-range-filter.component'; +import { SearchTextFilterComponent } from './search/search-filters/search-filter/search-text-filter/search-text-filter.component'; +import { SearchHierarchyFilterComponent } from './search/search-filters/search-filter/search-hierarchy-filter/search-hierarchy-filter.component'; +import { SearchBooleanFilterComponent } from './search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component'; +import { SearchFacetOptionComponent } from './search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component'; +import { SearchFacetSelectedOptionComponent } from './search/search-filters/search-filter/search-facet-filter-options/search-facet-selected-option/search-facet-selected-option.component'; +import { SearchFacetRangeOptionComponent } from './search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component'; +import { SearchSwitchConfigurationComponent } from './search/search-switch-configuration/search-switch-configuration.component'; +import { SearchAuthorityFilterComponent } from './search/search-filters/search-filter/search-authority-filter/search-authority-filter.component'; +import { DsDynamicDisabledComponent } from './form/builder/ds-dynamic-form-ui/models/disabled/dynamic-disabled.component'; +import { DsDynamicLookupRelationSearchTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/search-tab/dynamic-lookup-relation-search-tab.component'; +import { DsDynamicLookupRelationSelectionTabComponent } from './form/builder/ds-dynamic-form-ui/relation-lookup-modal/selection-tab/dynamic-lookup-relation-selection-tab.component'; +import { PageSizeSelectorComponent } from './page-size-selector/page-size-selector.component'; import { ItemSelectComponent } from './object-select/item-select/item-select.component'; import { CollectionSelectComponent } from './object-select/collection-select/collection-select.component'; import { FilterInputSuggestionsComponent } from './input-suggestions/filter-suggestions/filter-input-suggestions.component'; @@ -144,9 +168,14 @@ import { ListableObjectComponentLoaderComponent } from './object-collection/shar import { PublicationSearchResultListElementComponent } from './object-list/search-result-list-element/item-search-result/item-types/publication/publication-search-result-list-element.component'; import { PublicationSearchResultGridElementComponent } from './object-grid/search-result-grid-element/item-search-result/publication/publication-search-result-grid-element.component'; import { ListableObjectDirective } from './object-collection/shared/listable-object/listable-object.directive'; -import { CommunitySearchResultGridElementComponent } from './object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'; -import { CollectionSearchResultGridElementComponent } from './object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component'; +import { SearchLabelComponent } from './search/search-labels/search-label/search-label.component'; import { ItemMetadataRepresentationListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component'; +import { PageWithSidebarComponent } from './sidebar/page-with-sidebar.component'; +import { SidebarDropdownComponent } from './sidebar/sidebar-dropdown.component'; +import { SidebarFilterComponent } from './sidebar/filter/sidebar-filter.component'; +import { SidebarFilterSelectedOptionComponent } from './sidebar/filter/sidebar-filter-selected-option.component'; +import { MetadataRepresentationListComponent } from '../+item-page/simple/metadata-representation-list/metadata-representation-list.component'; +import { SelectableListItemControlComponent } from './object-collection/shared/selectable-list-item-control/selectable-list-item-control.component'; const MODULES = [ // Do NOT include UniversalModule, HttpModule, or JsonpModule here @@ -208,6 +237,8 @@ const COMPONENTS = [ DsDynamicFormControlContainerComponent, DsDynamicListComponent, DsDynamicLookupComponent, + DsDynamicDisabledComponent, + DsDynamicLookupRelationModalComponent, DsDynamicScrollableDropdownComponent, DsDynamicTagComponent, DsDynamicTypeaheadComponent, @@ -230,6 +261,10 @@ const COMPONENTS = [ ObjectCollectionComponent, PaginationComponent, SearchFormComponent, + PageWithSidebarComponent, + SidebarDropdownComponent, + SidebarFilterComponent, + SidebarFilterSelectedOptionComponent, ThumbnailComponent, GridThumbnailComponent, UploaderComponent, @@ -262,6 +297,29 @@ const COMPONENTS = [ EditItemSelectorComponent, CommunitySearchResultListElementComponent, CollectionSearchResultListElementComponent, + BrowseByComponent, + SearchResultsComponent, + SearchSidebarComponent, + SearchSettingsComponent, + CollectionSearchResultGridElementComponent, + CommunitySearchResultGridElementComponent, + SearchFiltersComponent, + SearchFilterComponent, + SearchFacetFilterComponent, + SearchLabelsComponent, + SearchLabelComponent, + SearchFacetFilterComponent, + SearchFacetFilterWrapperComponent, + SearchRangeFilterComponent, + SearchTextFilterComponent, + SearchHierarchyFilterComponent, + SearchBooleanFilterComponent, + SearchFacetOptionComponent, + SearchFacetSelectedOptionComponent, + SearchFacetRangeOptionComponent, + SearchSwitchConfigurationComponent, + SearchAuthorityFilterComponent, + PageSizeSelectorComponent, CommunitySearchResultGridElementComponent, CollectionSearchResultGridElementComponent, ListableObjectComponentLoaderComponent, @@ -275,7 +333,8 @@ const COMPONENTS = [ ItemTypeBadgeComponent, ItemSelectComponent, CollectionSelectComponent, - MetadataRepresentationLoaderComponent + MetadataRepresentationLoaderComponent, + SelectableListItemControlComponent ]; const ENTRY_COMPONENTS = [ @@ -299,6 +358,8 @@ const ENTRY_COMPONENTS = [ SearchResultGridElementComponent, DsDynamicListComponent, DsDynamicLookupComponent, + DsDynamicDisabledComponent, + DsDynamicLookupRelationModalComponent, DsDynamicScrollableDropdownComponent, DsDynamicTagComponent, DsDynamicTypeaheadComponent, @@ -320,7 +381,21 @@ const ENTRY_COMPONENTS = [ PlainTextMetadataListElementComponent, ItemMetadataListElementComponent, MetadataRepresentationListElementComponent, - ItemMetadataRepresentationListElementComponent + ItemMetadataRepresentationListElementComponent, + SearchResultsComponent, + CollectionSearchResultGridElementComponent, + CommunitySearchResultGridElementComponent, + SearchFacetFilterComponent, + SearchRangeFilterComponent, + SearchTextFilterComponent, + SearchHierarchyFilterComponent, + SearchBooleanFilterComponent, + SearchFacetOptionComponent, + SearchFacetSelectedOptionComponent, + SearchFacetRangeOptionComponent, + SearchAuthorityFilterComponent, + DsDynamicLookupRelationSearchTabComponent, + DsDynamicLookupRelationSelectionTabComponent ]; const SHARED_ITEM_PAGE_COMPONENTS = [ diff --git a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.html b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.html new file mode 100644 index 0000000000..bbe0b93566 --- /dev/null +++ b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.html @@ -0,0 +1,6 @@ + + + diff --git a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.scss b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.scss new file mode 100644 index 0000000000..b4e9cd340c --- /dev/null +++ b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.scss @@ -0,0 +1,11 @@ +a { + color: $body-color; + + &:hover, &focus { + text-decoration: none; + } + + span.badge { + vertical-align: text-top; + } +} diff --git a/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts new file mode 100644 index 0000000000..5c80a9cd87 --- /dev/null +++ b/src/app/shared/sidebar/filter/sidebar-filter-selected-option.component.ts @@ -0,0 +1,15 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'ds-sidebar-filter-selected-option', + styleUrls: ['./sidebar-filter-selected-option.component.scss'], + templateUrl: './sidebar-filter-selected-option.component.html', +}) + +/** + * Represents a single selected option in a sidebar filter + */ +export class SidebarFilterSelectedOptionComponent { + @Input() label:string; + @Output() click:EventEmitter = new EventEmitter(); +} diff --git a/src/app/shared/sidebar/filter/sidebar-filter.actions.ts b/src/app/shared/sidebar/filter/sidebar-filter.actions.ts new file mode 100644 index 0000000000..2391274489 --- /dev/null +++ b/src/app/shared/sidebar/filter/sidebar-filter.actions.ts @@ -0,0 +1,74 @@ +import { Action } from '@ngrx/store'; + +import { type } from '../../ngrx/type'; + +/** + * For each action type in an action group, make a simple + * enum object for all of this group's action types. + * + * The 'type' utility function coerces strings into string + * literal types and runs a simple check to guarantee all + * action types in the application are unique. + */ +export const SidebarFilterActionTypes = { + INITIALIZE: type('dspace/sidebar-filter/INITIALIZE'), + COLLAPSE: type('dspace/sidebar-filter/COLLAPSE'), + EXPAND: type('dspace/sidebar-filter/EXPAND'), + TOGGLE: type('dspace/sidebar-filter/TOGGLE'), +}; + +export class SidebarFilterAction implements Action { + /** + * Name of the filter the action is performed on, used to identify the filter + */ + filterName: string; + + /** + * Type of action that will be performed + */ + type; + + /** + * Initialize with the filter's name + * @param {string} name of the filter + */ + constructor(name: string) { + this.filterName = name; + } +} + +/* tslint:disable:max-classes-per-file */ +/** + * Used to initialize a filter + */ +export class FilterInitializeAction extends SidebarFilterAction { + type = SidebarFilterActionTypes.INITIALIZE; + initiallyExpanded; + + constructor(name:string, initiallyExpanded:boolean) { + super(name); + this.initiallyExpanded = initiallyExpanded; + } +} + +/** + * Used to collapse a filter + */ +export class FilterCollapseAction extends SidebarFilterAction { + type = SidebarFilterActionTypes.COLLAPSE; +} + +/** + * Used to expand a filter + */ +export class FilterExpandAction extends SidebarFilterAction { + type = SidebarFilterActionTypes.EXPAND; +} + +/** + * Used to collapse a filter when it's expanded and expand it when it's collapsed + */ +export class FilterToggleAction extends SidebarFilterAction { + type = SidebarFilterActionTypes.TOGGLE; +} +/* tslint:enable:max-classes-per-file */ diff --git a/src/app/shared/sidebar/filter/sidebar-filter.component.html b/src/app/shared/sidebar/filter/sidebar-filter.component.html new file mode 100644 index 0000000000..bd392aa715 --- /dev/null +++ b/src/app/shared/sidebar/filter/sidebar-filter.component.html @@ -0,0 +1,26 @@ +
+
+
+ {{ label | translate }} +
+ + +
+ +
diff --git a/src/app/shared/sidebar/filter/sidebar-filter.component.scss b/src/app/shared/sidebar/filter/sidebar-filter.component.scss new file mode 100644 index 0000000000..68949f3450 --- /dev/null +++ b/src/app/shared/sidebar/filter/sidebar-filter.component.scss @@ -0,0 +1,12 @@ +:host .facet-filter { + border: 1px solid map-get($theme-colors, light); + cursor: pointer; + + .sidebar-filter-wrapper.closed { + overflow: hidden; + } + + .filter-toggle { + line-height: $line-height-base; + } +} diff --git a/src/app/shared/sidebar/filter/sidebar-filter.component.ts b/src/app/shared/sidebar/filter/sidebar-filter.component.ts new file mode 100644 index 0000000000..2a98565639 --- /dev/null +++ b/src/app/shared/sidebar/filter/sidebar-filter.component.ts @@ -0,0 +1,89 @@ +import { Component, EventEmitter, Input, OnInit, Output } from '@angular/core'; +import { Observable } from 'rxjs'; +import { SidebarFilterService } from './sidebar-filter.service'; +import { slide } from '../../animations/slide'; + +@Component({ + selector: 'ds-sidebar-filter', + styleUrls: ['./sidebar-filter.component.scss'], + templateUrl: './sidebar-filter.component.html', + animations: [slide], +}) +/** + * This components renders a sidebar filter including the label and the selected values. + * The filter input itself should still be provided in the content. + */ +export class SidebarFilterComponent implements OnInit { + + @Input() name:string; + @Input() type:string; + @Input() label:string; + @Input() expanded = true; + @Input() singleValue = false; + @Input() selectedValues:Observable; + @Output() removeValue:EventEmitter = new EventEmitter(); + + /** + * True when the filter is 100% collapsed in the UI + */ + closed = true; + + /** + * Emits true when the filter is currently collapsed in the store + */ + collapsed$:Observable; + + constructor( + protected filterService:SidebarFilterService, + ) { + } + + /** + * Changes the state for this filter to collapsed when it's expanded and to expanded it when it's collapsed + */ + toggle() { + this.filterService.toggle(this.name); + } + + /** + * Method to change this.collapsed to false when the slide animation ends and is sliding open + * @param event The animation event + */ + finishSlide(event:any):void { + if (event.fromState === 'collapsed') { + this.closed = false; + } + } + + /** + * Method to change this.collapsed to true when the slide animation starts and is sliding closed + * @param event The animation event + */ + startSlide(event:any):void { + if (event.toState === 'collapsed') { + this.closed = true; + } + } + + ngOnInit():void { + this.closed = !this.expanded; + this.initializeFilter(); + this.collapsed$ = this.isCollapsed(); + } + + /** + * Sets the initial state of the filter + */ + initializeFilter() { + this.filterService.initializeFilter(this.name, this.expanded); + } + + /** + * Checks if the filter is currently collapsed + * @returns {Observable} Emits true when the current state of the filter is collapsed, false when it's expanded + */ + private isCollapsed():Observable { + return this.filterService.isCollapsed(this.name); + } + +} diff --git a/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts b/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts new file mode 100644 index 0000000000..d25737eaa9 --- /dev/null +++ b/src/app/shared/sidebar/filter/sidebar-filter.reducer.ts @@ -0,0 +1,70 @@ +import { + FilterInitializeAction, + SidebarFilterAction, + SidebarFilterActionTypes +} from './sidebar-filter.actions'; + +/** + * Interface that represents the state for a single filters + */ +export interface SidebarFilterState { + filterCollapsed:boolean, +} + +/** + * Interface that represents the state for all available filters + */ +export interface SidebarFiltersState { + [name:string]:SidebarFilterState +} + +const initialState:SidebarFiltersState = Object.create(null); + +/** + * Performs a filter action on the current state + * @param {SidebarFiltersState} state The state before the action is performed + * @param {SidebarFilterAction} action The action that should be performed + * @returns {SidebarFiltersState} The state after the action is performed + */ +export function sidebarFilterReducer(state = initialState, action:SidebarFilterAction):SidebarFiltersState { + + switch (action.type) { + + case SidebarFilterActionTypes.INITIALIZE: { + const initAction = (action as FilterInitializeAction); + return Object.assign({}, state, { + [action.filterName]: { + filterCollapsed: !initAction.initiallyExpanded, + } + }); + } + + case SidebarFilterActionTypes.COLLAPSE: { + return Object.assign({}, state, { + [action.filterName]: { + filterCollapsed: true, + } + }); + } + + case SidebarFilterActionTypes.EXPAND: { + return Object.assign({}, state, { + [action.filterName]: { + filterCollapsed: false, + } + }); + } + + case SidebarFilterActionTypes.TOGGLE: { + return Object.assign({}, state, { + [action.filterName]: { + filterCollapsed: !state[action.filterName].filterCollapsed, + } + }); + } + + default: { + return state; + } + } +} diff --git a/src/app/shared/sidebar/filter/sidebar-filter.service.ts b/src/app/shared/sidebar/filter/sidebar-filter.service.ts new file mode 100644 index 0000000000..2ff28fd2f5 --- /dev/null +++ b/src/app/shared/sidebar/filter/sidebar-filter.service.ts @@ -0,0 +1,90 @@ +import { Injectable } from '@angular/core'; +import { + FilterCollapseAction, + FilterExpandAction, FilterInitializeAction, + FilterToggleAction +} from './sidebar-filter.actions'; +import { createSelector, MemoizedSelector, select, Store } from '@ngrx/store'; +import { SidebarFiltersState, SidebarFilterState } from './sidebar-filter.reducer'; +import { Observable } from 'rxjs'; +import { distinctUntilChanged, map } from 'rxjs/operators'; +import { hasValue } from '../../empty.util'; + +/** + * Service that performs all actions that have to do with sidebar filters like collapsing or expanding them. + */ +@Injectable() +export class SidebarFilterService { + + constructor(private store:Store) { + } + + /** + * Dispatches an initialize action to the store for a given filter + * @param {string} filter The filter for which the action is dispatched + * @param {boolean} expanded If the filter should be open from the start + */ + public initializeFilter(filter:string, expanded:boolean):void { + this.store.dispatch(new FilterInitializeAction(filter, expanded)); + } + + /** + * Dispatches a collapse action to the store for a given filter + * @param {string} filterName The filter for which the action is dispatched + */ + public collapse(filterName:string):void { + this.store.dispatch(new FilterCollapseAction(filterName)); + } + + /** + * Dispatches an expand action to the store for a given filter + * @param {string} filterName The filter for which the action is dispatched + */ + public expand(filterName:string):void { + this.store.dispatch(new FilterExpandAction(filterName)); + } + + /** + * Dispatches a toggle action to the store for a given filter + * @param {string} filterName The filter for which the action is dispatched + */ + public toggle(filterName:string):void { + this.store.dispatch(new FilterToggleAction(filterName)); + } + + /** + * Checks if the state of a given filter is currently collapsed or not + * @param {string} filterName The filtername for which the collapsed state is checked + * @returns {Observable} Emits the current collapsed state of the given filter, if it's unavailable, return false + */ + isCollapsed(filterName:string):Observable { + return this.store.pipe( + select(filterByNameSelector(filterName)), + map((object:SidebarFilterState) => { + if (object) { + return object.filterCollapsed; + } else { + return false; + } + }), + distinctUntilChanged() + ); + } + +} + +const filterStateSelector = (state:SidebarFiltersState) => state.sidebarFilter; + +function filterByNameSelector(name:string):MemoizedSelector { + return keySelector(name); +} + +export function keySelector(key:string):MemoizedSelector { + return createSelector(filterStateSelector, (state:SidebarFilterState) => { + if (hasValue(state)) { + return state[key]; + } else { + return undefined; + } + }); +} diff --git a/src/app/shared/sidebar/page-with-sidebar.component.html b/src/app/shared/sidebar/page-with-sidebar.component.html new file mode 100644 index 0000000000..9feb6c792e --- /dev/null +++ b/src/app/shared/sidebar/page-with-sidebar.component.html @@ -0,0 +1,14 @@ +
+
+
+ +
+ +
+
+
+
diff --git a/src/app/shared/sidebar/page-with-sidebar.component.scss b/src/app/shared/sidebar/page-with-sidebar.component.scss new file mode 100644 index 0000000000..8be48cea2b --- /dev/null +++ b/src/app/shared/sidebar/page-with-sidebar.component.scss @@ -0,0 +1,52 @@ +@include media-breakpoint-down(md) { + .container { + width: 100%; + max-width: none; + } +} + +.row-with-sidebar { + + &.row-offcanvas { + width: 100%; + } + + @include media-breakpoint-up(md) { + display: flex; + } + + @include media-breakpoint-down(sm) { + position: relative; + + &.row-offcanvas { + position: relative; + } + + &.row-offcanvas-right .sidebar-content { + right: -100%; + } + + &.row-offcanvas-left .sidebar-content { + left: -100%; + } + + .sidebar-content { + position: absolute; + top: 0; + width: 100%; + } + } +} + +@include media-breakpoint-up(md) { + .sidebar-content { + position: sticky; + position: -webkit-sticky; + top: 0; + z-index: $zindex-sticky; + padding-top: $content-spacing; + margin-top: -$content-spacing; + align-self: flex-start; + display: block; + } +} diff --git a/src/app/shared/sidebar/page-with-sidebar.component.spec.ts b/src/app/shared/sidebar/page-with-sidebar.component.spec.ts new file mode 100644 index 0000000000..77f59090ab --- /dev/null +++ b/src/app/shared/sidebar/page-with-sidebar.component.spec.ts @@ -0,0 +1,75 @@ +import { By } from '@angular/platform-browser'; +import { of as observableOf } from 'rxjs'; +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { PageWithSidebarComponent } from './page-with-sidebar.component'; +import { SidebarService } from './sidebar.service'; +import { HostWindowService } from '../host-window.service'; +import { NoopAnimationsModule } from '@angular/platform-browser/animations'; + +describe('PageWithSidebarComponent', () => { + let comp:PageWithSidebarComponent; + let fixture:ComponentFixture; + + const sidebarService = { + isCollapsed: observableOf(true), + collapse: () => this.isCollapsed = observableOf(true), + expand: () => this.isCollapsed = observableOf(false) + }; + + beforeEach(() => { + TestBed.configureTestingModule({ + imports: [NoopAnimationsModule], + providers: [ + { + provide: SidebarService, + useValue: sidebarService + }, + { + provide: HostWindowService, useValue: jasmine.createSpyObj('hostWindowService', + { + isXs: observableOf(true), + isSm: observableOf(false), + isXsOrSm: observableOf(true) + }) + }, + ], + declarations: [PageWithSidebarComponent] + }).compileComponents(); + fixture = TestBed.createComponent(PageWithSidebarComponent); + comp = fixture.componentInstance; + comp.id = 'mock-id'; + fixture.detectChanges(); + }); + + describe('when sidebarCollapsed is true in mobile view', () => { + let menu:HTMLElement; + + beforeEach(() => { + menu = fixture.debugElement.query(By.css('#mock-id-sidebar-content')).nativeElement; + (comp as any).sidebarService.isCollapsed = observableOf(true); + comp.ngOnInit(); + fixture.detectChanges(); + }); + + it('should close the sidebar', () => { + expect(menu.classList).not.toContain('active'); + }); + + }); + + describe('when sidebarCollapsed is false in mobile view', () => { + let menu:HTMLElement; + + beforeEach(() => { + menu = fixture.debugElement.query(By.css('#mock-id-sidebar-content')).nativeElement; + (comp as any).sidebarService.isCollapsed = observableOf(false); + comp.ngOnInit(); + fixture.detectChanges(); + }); + + it('should open the menu', () => { + expect(menu.classList).toContain('active'); + }); + + }); +}); diff --git a/src/app/shared/sidebar/page-with-sidebar.component.ts b/src/app/shared/sidebar/page-with-sidebar.component.ts new file mode 100644 index 0000000000..8b7f987a37 --- /dev/null +++ b/src/app/shared/sidebar/page-with-sidebar.component.ts @@ -0,0 +1,77 @@ +import { Component, Input, OnInit, TemplateRef } from '@angular/core'; +import { SidebarService } from './sidebar.service'; +import { HostWindowService } from '../host-window.service'; +import { Observable } from 'rxjs'; +import { pushInOut } from '../animations/push'; +import { map } from 'rxjs/operators'; + +@Component({ + selector: 'ds-page-with-sidebar', + styleUrls: ['./page-with-sidebar.component.scss'], + templateUrl: './page-with-sidebar.component.html', + animations: [pushInOut], +}) +/** + * This component takes care of displaying the sidebar properly on all viewports. It does not + * provide default buttons to open or close the sidebar. Instead the parent component is expected + * to provide the content of the sidebar through an input. The main content of the page goes in + * the template outlet (inside the page-width-sidebar tags). + */ +export class PageWithSidebarComponent implements OnInit { + @Input() id:string; + @Input() sidebarContent:TemplateRef; + + /** + * Emits true if were on a small screen + */ + isXsOrSm$:Observable; + + /** + * The width of the sidebar (bootstrap columns) + */ + @Input() + sideBarWidth = 3; + + /** + * Observable for whether or not the sidebar is currently collapsed + */ + isSidebarCollapsed$:Observable; + + sidebarClasses:Observable; + + constructor(protected sidebarService:SidebarService, + protected windowService:HostWindowService, + ) { + } + + ngOnInit():void { + this.isXsOrSm$ = this.windowService.isXsOrSm(); + this.isSidebarCollapsed$ = this.isSidebarCollapsed(); + this.sidebarClasses = this.isSidebarCollapsed$.pipe( + map((isCollapsed) => isCollapsed ? '' : 'active') + ); + } + + /** + * Check if the sidebar is collapsed + * @returns {Observable} emits true if the sidebar is currently collapsed, false if it is expanded + */ + private isSidebarCollapsed():Observable { + return this.sidebarService.isCollapsed; + } + + /** + * Set the sidebar to a collapsed state + */ + public closeSidebar():void { + this.sidebarService.collapse() + } + + /** + * Set the sidebar to an expanded state + */ + public openSidebar():void { + this.sidebarService.expand(); + } + +} diff --git a/src/app/shared/sidebar/sidebar-dropdown.component.html b/src/app/shared/sidebar/sidebar-dropdown.component.html new file mode 100644 index 0000000000..0c2a1c05d2 --- /dev/null +++ b/src/app/shared/sidebar/sidebar-dropdown.component.html @@ -0,0 +1,6 @@ +
+
+ +
diff --git a/src/app/shared/sidebar/sidebar-dropdown.component.scss b/src/app/shared/sidebar/sidebar-dropdown.component.scss new file mode 100644 index 0000000000..1c025095dd --- /dev/null +++ b/src/app/shared/sidebar/sidebar-dropdown.component.scss @@ -0,0 +1,3 @@ +.setting-option { + border: 1px solid map-get($theme-colors, light); +} diff --git a/src/app/shared/sidebar/sidebar-dropdown.component.ts b/src/app/shared/sidebar/sidebar-dropdown.component.ts new file mode 100644 index 0000000000..313538eded --- /dev/null +++ b/src/app/shared/sidebar/sidebar-dropdown.component.ts @@ -0,0 +1,16 @@ +import { Component, EventEmitter, Input, Output } from '@angular/core'; + +@Component({ + selector: 'ds-sidebar-dropdown', + styleUrls: ['./sidebar-dropdown.component.scss'], + templateUrl: './sidebar-dropdown.component.html', +}) +/** + * This components renders a sidebar dropdown including the label. + * The options should still be provided in the content. + */ +export class SidebarDropdownComponent { + @Input() id:string; + @Input() label:string; + @Output() change:EventEmitter = new EventEmitter(); +} diff --git a/src/app/+search-page/search-sidebar/search-sidebar.effects.ts b/src/app/shared/sidebar/sidebar-effects.service.ts similarity index 85% rename from src/app/+search-page/search-sidebar/search-sidebar.effects.ts rename to src/app/shared/sidebar/sidebar-effects.service.ts index 1f5fb0ef60..fc6643be4b 100644 --- a/src/app/+search-page/search-sidebar/search-sidebar.effects.ts +++ b/src/app/shared/sidebar/sidebar-effects.service.ts @@ -3,14 +3,14 @@ import { Injectable } from '@angular/core'; import { Effect, Actions, ofType } from '@ngrx/effects' import * as fromRouter from '@ngrx/router-store'; -import { SearchSidebarCollapseAction } from './search-sidebar.actions'; +import { SidebarCollapseAction } from './sidebar.actions'; import { URLBaser } from '../../core/url-baser/url-baser'; /** * Makes sure that if the user navigates to another route, the sidebar is collapsed */ @Injectable() -export class SearchSidebarEffects { +export class SidebarEffects { private previousPath: string; @Effect() routeChange$ = this.actions$ .pipe( @@ -19,7 +19,7 @@ export class SearchSidebarEffects { tap((action) => { this.previousPath = this.getBaseUrl(action) }), - map(() => new SearchSidebarCollapseAction()) + map(() => new SidebarCollapseAction()) ); constructor(private actions$: Actions) { diff --git a/src/app/+search-page/search-sidebar/search-sidebar.actions.ts b/src/app/shared/sidebar/sidebar.actions.ts similarity index 51% rename from src/app/+search-page/search-sidebar/search-sidebar.actions.ts rename to src/app/shared/sidebar/sidebar.actions.ts index 84a34b2790..4a7c85696a 100644 --- a/src/app/+search-page/search-sidebar/search-sidebar.actions.ts +++ b/src/app/shared/sidebar/sidebar.actions.ts @@ -1,6 +1,6 @@ import { Action } from '@ngrx/store'; -import { type } from '../../shared/ngrx/type'; +import { type } from '../ngrx/type'; /** * For each action type in an action group, make a simple @@ -10,32 +10,32 @@ import { type } from '../../shared/ngrx/type'; * literal types and runs a simple check to guarantee all * action types in the application are unique. */ -export const SearchSidebarActionTypes = { - COLLAPSE: type('dspace/search-sidebar/COLLAPSE'), - EXPAND: type('dspace/search-sidebar/EXPAND'), - TOGGLE: type('dspace/search-sidebar/TOGGLE') +export const SidebarActionTypes = { + COLLAPSE: type('dspace/sidebar/COLLAPSE'), + EXPAND: type('dspace/sidebar/EXPAND'), + TOGGLE: type('dspace/sidebar/TOGGLE') }; /* tslint:disable:max-classes-per-file */ /** * Used to collapse the sidebar */ -export class SearchSidebarCollapseAction implements Action { - type = SearchSidebarActionTypes.COLLAPSE; +export class SidebarCollapseAction implements Action { + type = SidebarActionTypes.COLLAPSE; } /** * Used to expand the sidebar */ -export class SearchSidebarExpandAction implements Action { - type = SearchSidebarActionTypes.EXPAND; +export class SidebarExpandAction implements Action { + type = SidebarActionTypes.EXPAND; } /** * Used to collapse the sidebar when it's expanded and expand it when it's collapsed */ -export class SearchSidebarToggleAction implements Action { - type = SearchSidebarActionTypes.TOGGLE; +export class SidebarToggleAction implements Action { + type = SidebarActionTypes.TOGGLE; } /* tslint:enable:max-classes-per-file */ @@ -43,7 +43,7 @@ export class SearchSidebarToggleAction implements Action { * Export a type alias of all actions in this action group * so that reducers can easily compose action types */ -export type SearchSidebarAction - = SearchSidebarCollapseAction - | SearchSidebarExpandAction - | SearchSidebarToggleAction +export type SidebarAction + = SidebarCollapseAction + | SidebarExpandAction + | SidebarToggleAction diff --git a/src/app/+search-page/search-sidebar/search-sidebar.reducer.spec.ts b/src/app/shared/sidebar/sidebar.reducer.spec.ts similarity index 80% rename from src/app/+search-page/search-sidebar/search-sidebar.reducer.spec.ts rename to src/app/shared/sidebar/sidebar.reducer.spec.ts index dfdead4369..0fbfce84fc 100644 --- a/src/app/+search-page/search-sidebar/search-sidebar.reducer.spec.ts +++ b/src/app/shared/sidebar/sidebar.reducer.spec.ts @@ -1,12 +1,12 @@ import * as deepFreeze from 'deep-freeze'; -import { sidebarReducer } from './search-sidebar.reducer'; +import { sidebarReducer } from './sidebar.reducer'; import { - SearchSidebarCollapseAction, SearchSidebarExpandAction, - SearchSidebarToggleAction -} from './search-sidebar.actions'; + SidebarCollapseAction, SidebarExpandAction, + SidebarToggleAction +} from './sidebar.actions'; -class NullAction extends SearchSidebarCollapseAction { +class NullAction extends SidebarCollapseAction { type = null; constructor() { @@ -34,7 +34,7 @@ describe('sidebarReducer', () => { it('should set sidebarCollapsed to true in response to the COLLAPSE action', () => { const state = { sidebarCollapsed: false }; - const action = new SearchSidebarCollapseAction(); + const action = new SidebarCollapseAction(); const newState = sidebarReducer(state, action); expect(newState.sidebarCollapsed).toEqual(true); @@ -44,7 +44,7 @@ describe('sidebarReducer', () => { const state = { sidebarCollapsed: false }; deepFreeze([state]); - const action = new SearchSidebarCollapseAction(); + const action = new SidebarCollapseAction(); sidebarReducer(state, action); // no expect required, deepFreeze will ensure an exception is thrown if the state @@ -53,7 +53,7 @@ describe('sidebarReducer', () => { it('should set sidebarCollapsed to false in response to the EXPAND action', () => { const state = { sidebarCollapsed: true }; - const action = new SearchSidebarExpandAction(); + const action = new SidebarExpandAction(); const newState = sidebarReducer(state, action); expect(newState.sidebarCollapsed).toEqual(false); @@ -63,13 +63,13 @@ describe('sidebarReducer', () => { const state = { sidebarCollapsed: true }; deepFreeze([state]); - const action = new SearchSidebarExpandAction(); + const action = new SidebarExpandAction(); sidebarReducer(state, action); }); it('should flip the value of sidebarCollapsed in response to the TOGGLE action', () => { const state1 = { sidebarCollapsed: true }; - const action = new SearchSidebarToggleAction(); + const action = new SidebarToggleAction(); const state2 = sidebarReducer(state1, action); const state3 = sidebarReducer(state2, action); @@ -82,7 +82,7 @@ describe('sidebarReducer', () => { const state = { sidebarCollapsed: true }; deepFreeze([state]); - const action = new SearchSidebarToggleAction(); + const action = new SidebarToggleAction(); sidebarReducer(state, action); }); diff --git a/src/app/shared/sidebar/sidebar.reducer.ts b/src/app/shared/sidebar/sidebar.reducer.ts new file mode 100644 index 0000000000..05e7d38d48 --- /dev/null +++ b/src/app/shared/sidebar/sidebar.reducer.ts @@ -0,0 +1,47 @@ +import { SidebarAction, SidebarActionTypes } from './sidebar.actions'; + +/** + * Interface that represents the state of the sidebar + */ +export interface SidebarState { + sidebarCollapsed: boolean; +} + +const initialState: SidebarState = { + sidebarCollapsed: true +}; + +/** + * Performs a sidebar action on the current state + * @param {SidebarState} state The state before the action is performed + * @param {SidebarAction} action The action that should be performed + * @returns {SidebarState} The state after the action is performed + */ +export function sidebarReducer(state = initialState, action: SidebarAction): SidebarState { + switch (action.type) { + + case SidebarActionTypes.COLLAPSE: { + return Object.assign({}, state, { + sidebarCollapsed: true + }); + } + + case SidebarActionTypes.EXPAND: { + return Object.assign({}, state, { + sidebarCollapsed: false + }); + + } + + case SidebarActionTypes.TOGGLE: { + return Object.assign({}, state, { + sidebarCollapsed: !state.sidebarCollapsed + }); + + } + + default: { + return state; + } + } +} diff --git a/src/app/+search-page/search-sidebar/search-sidebar.service.spec.ts b/src/app/shared/sidebar/sidebar.service.spec.ts similarity index 61% rename from src/app/+search-page/search-sidebar/search-sidebar.service.spec.ts rename to src/app/shared/sidebar/sidebar.service.spec.ts index 0cccf9ea40..6d8e845836 100644 --- a/src/app/+search-page/search-sidebar/search-sidebar.service.spec.ts +++ b/src/app/shared/sidebar/sidebar.service.spec.ts @@ -1,13 +1,13 @@ import { Store } from '@ngrx/store'; -import { SearchSidebarService } from './search-sidebar.service'; +import { SidebarService } from './sidebar.service'; import { AppState } from '../../app.reducer'; import { async, TestBed } from '@angular/core/testing'; import { of as observableOf } from 'rxjs'; -import { SearchSidebarCollapseAction, SearchSidebarExpandAction } from './search-sidebar.actions'; -import { HostWindowService } from '../../shared/host-window.service'; +import { SidebarCollapseAction, SidebarExpandAction } from './sidebar.actions'; +import { HostWindowService } from '../host-window.service'; -describe('SearchSidebarService', () => { - let service: SearchSidebarService; +describe('SidebarService', () => { + let service: SidebarService; const store: Store = jasmine.createSpyObj('store', { /* tslint:disable:no-empty */ dispatch: {}, @@ -35,7 +35,7 @@ describe('SearchSidebarService', () => { })); beforeEach(() => { - service = new SearchSidebarService(store, windowService); + service = new SidebarService(store, windowService); }) ; describe('when the collapse method is triggered', () => { @@ -43,8 +43,8 @@ describe('SearchSidebarService', () => { service.collapse(); }); - it('SearchSidebarCollapseAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new SearchSidebarCollapseAction()); + it('SidebarCollapseAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new SidebarCollapseAction()); }); }); @@ -54,8 +54,8 @@ describe('SearchSidebarService', () => { service.expand(); }); - it('SearchSidebarExpandAction should be dispatched to the store', () => { - expect(store.dispatch).toHaveBeenCalledWith(new SearchSidebarExpandAction()); + it('SidebarExpandAction should be dispatched to the store', () => { + expect(store.dispatch).toHaveBeenCalledWith(new SidebarExpandAction()); }); }); diff --git a/src/app/+search-page/search-sidebar/search-sidebar.service.ts b/src/app/shared/sidebar/sidebar.service.ts similarity index 70% rename from src/app/+search-page/search-sidebar/search-sidebar.service.ts rename to src/app/shared/sidebar/sidebar.service.ts index 7185984538..8e3bbbf117 100644 --- a/src/app/+search-page/search-sidebar/search-sidebar.service.ts +++ b/src/app/shared/sidebar/sidebar.service.ts @@ -1,20 +1,20 @@ import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { Injectable } from '@angular/core'; -import { SearchSidebarState } from './search-sidebar.reducer'; +import { SidebarState } from './sidebar.reducer'; import { createSelector, select, Store } from '@ngrx/store'; -import { SearchSidebarCollapseAction, SearchSidebarExpandAction } from './search-sidebar.actions'; +import { SidebarCollapseAction, SidebarExpandAction } from './sidebar.actions'; import { AppState } from '../../app.reducer'; -import { HostWindowService } from '../../shared/host-window.service'; +import { HostWindowService } from '../host-window.service'; import { map } from 'rxjs/operators'; -const sidebarStateSelector = (state: AppState) => state.searchSidebar; -const sidebarCollapsedSelector = createSelector(sidebarStateSelector, (sidebar: SearchSidebarState) => sidebar.sidebarCollapsed); +const sidebarStateSelector = (state: AppState) => state.sidebar; +const sidebarCollapsedSelector = createSelector(sidebarStateSelector, (sidebar: SidebarState) => sidebar.sidebarCollapsed); /** - * Service that performs all actions that have to do with the search sidebar + * Service that performs all actions that have to do with the sidebar */ @Injectable() -export class SearchSidebarService { +export class SidebarService { /** * Emits true is the current screen size is mobile */ @@ -47,13 +47,13 @@ export class SearchSidebarService { * Dispatches a collapse action to the store */ public collapse(): void { - this.store.dispatch(new SearchSidebarCollapseAction()); + this.store.dispatch(new SidebarCollapseAction()); } /** * Dispatches an expand action to the store */ public expand(): void { - this.store.dispatch(new SearchSidebarExpandAction()); + this.store.dispatch(new SidebarExpandAction()); } } diff --git a/src/app/shared/utils/relation-query.utils.spec.ts b/src/app/shared/utils/relation-query.utils.spec.ts new file mode 100644 index 0000000000..f70e904422 --- /dev/null +++ b/src/app/shared/utils/relation-query.utils.spec.ts @@ -0,0 +1,18 @@ +import { getFilterByRelation, getQueryByRelations } from './relation-query.utils'; + +describe('Relation Query Utils', () => { + const relationtype = 'isAuthorOfPublication'; + const itemUUID = 'a7939af0-36ad-430d-af09-7be8b0a4dadd'; + describe('getQueryByRelations', () => { + it('Should return the correct query based on relationtype and uuid', () => { + const result = getQueryByRelations(relationtype, itemUUID); + expect(result).toEqual('query=relation.isAuthorOfPublication:a7939af0-36ad-430d-af09-7be8b0a4dadd'); + }); + }); + describe('getFilterByRelation', () => { + it('Should return the correct query based on relationtype and uuid', () => { + const result = getFilterByRelation(relationtype, itemUUID); + expect(result).toEqual('f.isAuthorOfPublication=a7939af0-36ad-430d-af09-7be8b0a4dadd'); + }); + }); +}); diff --git a/src/app/shared/utils/relation-query.utils.ts b/src/app/shared/utils/relation-query.utils.ts new file mode 100644 index 0000000000..74f9e64cc9 --- /dev/null +++ b/src/app/shared/utils/relation-query.utils.ts @@ -0,0 +1,18 @@ +/** + * Get the query for looking up items by relation type + * @param {string} relationType Relation type + * @param {string} itemUUID Item UUID + * @returns {string} Query + */ +export function getQueryByRelations(relationType: string, itemUUID: string): string { + return `query=relation.${relationType}:${itemUUID}`; +} + +/** + * Get the filter for a relation with the item's UUID + * @param relationType The type of relation e.g. 'isAuthorOfPublication' + * @param itemUUID The item's UUID + */ +export function getFilterByRelation(relationType: string, itemUUID: string): string { + return `f.${relationType}=${itemUUID}`; +} diff --git a/src/app/shared/utils/route.utils.spec.ts b/src/app/shared/utils/route.utils.spec.ts new file mode 100644 index 0000000000..610fd8756d --- /dev/null +++ b/src/app/shared/utils/route.utils.spec.ts @@ -0,0 +1,22 @@ +import { currentPath } from './route.utils'; + +describe('Route Utils', () => { + const urlTree = { + root: { + children: { + primary: { + segments: [ + { path: 'test' }, + { path: 'path' } + ] + } + + } + } + }; + const router = { parseUrl: () => urlTree } as any; + it('Should return the correct current path based on the router', () => { + const result = currentPath(router); + expect(result).toEqual('/test/path'); + }); + }); diff --git a/src/app/shared/utils/route.utils.ts b/src/app/shared/utils/route.utils.ts new file mode 100644 index 0000000000..6510fb8894 --- /dev/null +++ b/src/app/shared/utils/route.utils.ts @@ -0,0 +1,10 @@ +import { Router } from '@angular/router'; + +/** + * Util function to retrieve the current path (without query parameters) the user is on + * @param router The router service + */ +export function currentPath(router: Router) { + const urlTree = router.parseUrl(router.url); + return '/' + urlTree.root.children.primary.segments.map((it) => it.path).join('/') +} diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts index cc0175231e..146ba2e3c0 100644 --- a/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.spec.ts @@ -6,7 +6,7 @@ import { RouterTestingModule } from '@angular/router/testing'; import { ChangeDetectionStrategy, Component } from '@angular/core'; -import { SearchService } from '../../+search-page/search-service/search.service'; +import { SearchService } from '../../core/shared/search/search.service'; import { ViewModeSwitchComponent } from './view-mode-switch.component'; import { SearchServiceStub } from '../testing/search-service-stub'; import { ViewMode } from '../../core/shared/view-mode.model'; diff --git a/src/app/shared/view-mode-switch/view-mode-switch.component.ts b/src/app/shared/view-mode-switch/view-mode-switch.component.ts index d406573646..4feb8927c2 100644 --- a/src/app/shared/view-mode-switch/view-mode-switch.component.ts +++ b/src/app/shared/view-mode-switch/view-mode-switch.component.ts @@ -2,9 +2,11 @@ import { Component, Input, OnDestroy, OnInit } from '@angular/core'; import { Subscription } from 'rxjs'; -import { SearchService } from '../../+search-page/search-service/search.service'; +import { SearchService } from '../../core/shared/search/search.service'; import { ViewMode } from '../../core/shared/view-mode.model'; import { isEmpty } from '../empty.util'; +import { currentPath } from '../utils/route.utils'; +import { Router } from '@angular/router'; /** * Component to switch between list and grid views. @@ -33,7 +35,7 @@ export class ViewModeSwitchComponent implements OnInit, OnDestroy { viewModeEnum = ViewMode; private sub: Subscription; - constructor(private searchService: SearchService) { + constructor(private searchService: SearchService, private router: Router) { } /** @@ -76,7 +78,7 @@ export class ViewModeSwitchComponent implements OnInit, OnDestroy { */ public getSearchLink(): string { if (this.inPlaceSearch) { - return './'; + return currentPath(this.router); } return this.searchService.getSearchLink(); } diff --git a/src/app/statistics/angulartics/dspace-provider.spec.ts b/src/app/statistics/angulartics/dspace-provider.spec.ts new file mode 100644 index 0000000000..d89d2d9fc6 --- /dev/null +++ b/src/app/statistics/angulartics/dspace-provider.spec.ts @@ -0,0 +1,26 @@ +import { Angulartics2DSpace } from './dspace-provider'; +import { Angulartics2 } from 'angulartics2'; +import { StatisticsService } from '../statistics.service'; +import { filter } from 'rxjs/operators'; +import { of as observableOf } from 'rxjs'; + +describe('Angulartics2DSpace', () => { + let provider:Angulartics2DSpace; + let angulartics2:Angulartics2; + let statisticsService:jasmine.SpyObj; + + beforeEach(() => { + angulartics2 = { + eventTrack: observableOf({action: 'pageView', properties: {object: 'mock-object'}}), + filterDeveloperMode: () => filter(() => true) + } as any; + statisticsService = jasmine.createSpyObj('statisticsService', {trackViewEvent: null}); + provider = new Angulartics2DSpace(angulartics2, statisticsService); + }); + + it('should use the statisticsService', () => { + provider.startTracking(); + expect(statisticsService.trackViewEvent).toHaveBeenCalledWith('mock-object'); + }); + +}); diff --git a/src/app/statistics/angulartics/dspace-provider.ts b/src/app/statistics/angulartics/dspace-provider.ts new file mode 100644 index 0000000000..9ab01f6023 --- /dev/null +++ b/src/app/statistics/angulartics/dspace-provider.ts @@ -0,0 +1,38 @@ +import { Injectable } from '@angular/core'; +import { Angulartics2 } from 'angulartics2'; +import { StatisticsService } from '../statistics.service'; + +/** + * Angulartics2DSpace is a angulartics2 plugin that provides DSpace with the events. + */ +@Injectable({providedIn: 'root'}) +export class Angulartics2DSpace { + + constructor( + private angulartics2:Angulartics2, + private statisticsService:StatisticsService, + ) { + } + + /** + * Activates this plugin + */ + startTracking():void { + this.angulartics2.eventTrack + .pipe(this.angulartics2.filterDeveloperMode()) + .subscribe((event) => this.eventTrack(event)); + } + + private eventTrack(event) { + if (event.action === 'pageView') { + this.statisticsService.trackViewEvent(event.properties.object); + } else if (event.action === 'search') { + this.statisticsService.trackSearchEvent( + event.properties.searchOptions, + event.properties.page, + event.properties.sort, + event.properties.filters + ); + } + } +} diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.html b/src/app/statistics/angulartics/dspace/view-tracker.component.html new file mode 100644 index 0000000000..c0c0ffe181 --- /dev/null +++ b/src/app/statistics/angulartics/dspace/view-tracker.component.html @@ -0,0 +1 @@ +  diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.scss b/src/app/statistics/angulartics/dspace/view-tracker.component.scss new file mode 100644 index 0000000000..c76cafbe44 --- /dev/null +++ b/src/app/statistics/angulartics/dspace/view-tracker.component.scss @@ -0,0 +1,3 @@ +:host { + display: none +} diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.ts b/src/app/statistics/angulartics/dspace/view-tracker.component.ts new file mode 100644 index 0000000000..1151287ea8 --- /dev/null +++ b/src/app/statistics/angulartics/dspace/view-tracker.component.ts @@ -0,0 +1,27 @@ +import { Component, Input, OnInit } from '@angular/core'; +import { Angulartics2 } from 'angulartics2'; +import { DSpaceObject } from '../../../core/shared/dspace-object.model'; + +/** + * This component triggers a page view statistic + */ +@Component({ + selector: 'ds-view-tracker', + styleUrls: ['./view-tracker.component.scss'], + templateUrl: './view-tracker.component.html', +}) +export class ViewTrackerComponent implements OnInit { + @Input() object:DSpaceObject; + + constructor( + public angulartics2:Angulartics2 + ) { + } + + ngOnInit():void { + this.angulartics2.eventTrack.next({ + action: 'pageView', + properties: {object: this.object}, + }); + } +} diff --git a/src/app/statistics/statistics.module.ts b/src/app/statistics/statistics.module.ts new file mode 100644 index 0000000000..a67ff7613c --- /dev/null +++ b/src/app/statistics/statistics.module.ts @@ -0,0 +1,36 @@ +import { ModuleWithProviders, NgModule } from '@angular/core'; +import { CommonModule } from '@angular/common'; +import { CoreModule } from '../core/core.module'; +import { SharedModule } from '../shared/shared.module'; +import { ViewTrackerComponent } from './angulartics/dspace/view-tracker.component'; +import { StatisticsService } from './statistics.service'; + +@NgModule({ + imports: [ + CommonModule, + CoreModule.forRoot(), + SharedModule, + ], + declarations: [ + ViewTrackerComponent, + ], + exports: [ + ViewTrackerComponent, + ], + providers: [ + StatisticsService + ] +}) +/** + * This module handles the statistics + */ +export class StatisticsModule { + static forRoot():ModuleWithProviders { + return { + ngModule: StatisticsModule, + providers: [ + StatisticsService + ] + }; + } +} diff --git a/src/app/statistics/statistics.service.spec.ts b/src/app/statistics/statistics.service.spec.ts new file mode 100644 index 0000000000..c6cc4c10b5 --- /dev/null +++ b/src/app/statistics/statistics.service.spec.ts @@ -0,0 +1,144 @@ +import { StatisticsService } from './statistics.service'; +import { RequestService } from '../core/data/request.service'; +import { HALEndpointServiceStub } from '../shared/testing/hal-endpoint-service-stub'; +import { getMockRequestService } from '../shared/mocks/mock-request.service'; +import { TrackRequest } from './track-request.model'; +import { isEqual } from 'lodash'; +import { DSpaceObjectType } from '../core/shared/dspace-object-type.model'; +import { SearchOptions } from '../shared/search/search-options.model'; + +describe('StatisticsService', () => { + let service:StatisticsService; + let requestService:jasmine.SpyObj; + const restURL = 'https://rest.api'; + const halService:any = new HALEndpointServiceStub(restURL); + + function initTestService() { + return new StatisticsService( + requestService, + halService, + ); + } + + describe('trackViewEvent', () => { + requestService = getMockRequestService(); + service = initTestService(); + + it('should send a request to track an item view ', () => { + const mockItem:any = {uuid: 'mock-item-uuid', type: 'item'}; + service.trackViewEvent(mockItem); + const request:TrackRequest = requestService.configure.calls.mostRecent().args[0]; + expect(request.body).toBeDefined('request.body'); + const body = JSON.parse(request.body); + expect(body.targetId).toBe('mock-item-uuid'); + expect(body.targetType).toBe('item'); + }); + }); + + describe('trackSearchEvent', () => { + requestService = getMockRequestService(); + service = initTestService(); + + const mockSearch:any = new SearchOptions({ + query: 'mock-query', + }); + + const page = { + size: 10, + totalElements: 248, + totalPages: 25, + number: 4 + }; + const sort = {by: 'search-field', order: 'ASC'}; + service.trackSearchEvent(mockSearch, page, sort); + const request:TrackRequest = requestService.configure.calls.mostRecent().args[0]; + const body = JSON.parse(request.body); + + it('should specify the right query', () => { + expect(body.query).toBe('mock-query'); + }); + + it('should specify the pagination info', () => { + expect(body.page).toEqual({ + size: 10, + totalElements: 248, + totalPages: 25, + number: 4 + }); + }); + + it('should specify the sort options', () => { + expect(body.sort).toEqual({ + by: 'search-field', + order: 'asc' + }); + }); + }); + + describe('trackSearchEvent with optional parameters', () => { + requestService = getMockRequestService(); + service = initTestService(); + + const mockSearch:any = new SearchOptions({ + query: 'mock-query', + configuration: 'mock-configuration', + dsoType: DSpaceObjectType.ITEM, + scope: 'mock-scope' + }); + + const page = { + size: 10, + totalElements: 248, + totalPages: 25, + number: 4 + }; + const sort = {by: 'search-field', order: 'ASC'}; + const filters = [ + { + filter: 'title', + operator: 'notcontains', + value: 'dolor sit', + label: 'dolor sit' + }, + { + filter: 'author', + operator: 'authority', + value: '9zvxzdm4qru17or5a83wfgac', + label: 'Amet, Consectetur' + } + ]; + service.trackSearchEvent(mockSearch, page, sort, filters); + const request:TrackRequest = requestService.configure.calls.mostRecent().args[0]; + const body = JSON.parse(request.body); + + it('should specify the dsoType', () => { + expect(body.dsoType).toBe('item'); + }); + + it('should specify the scope', () => { + expect(body.scope).toBe('mock-scope'); + }); + + it('should specify the configuration', () => { + expect(body.configuration).toBe('mock-configuration'); + }); + + it('should specify the filters', () => { + expect(isEqual(body.appliedFilters, [ + { + filter: 'title', + operator: 'notcontains', + value: 'dolor sit', + label: 'dolor sit' + }, + { + filter: 'author', + operator: 'authority', + value: '9zvxzdm4qru17or5a83wfgac', + label: 'Amet, Consectetur' + } + ])).toBe(true); + }); + }); + +}); diff --git a/src/app/statistics/statistics.service.ts b/src/app/statistics/statistics.service.ts new file mode 100644 index 0000000000..004e013164 --- /dev/null +++ b/src/app/statistics/statistics.service.ts @@ -0,0 +1,93 @@ +import { RequestService } from '../core/data/request.service'; +import { Injectable } from '@angular/core'; +import { DSpaceObject } from '../core/shared/dspace-object.model'; +import { map, take } from 'rxjs/operators'; +import { TrackRequest } from './track-request.model'; +import { hasValue, isNotEmpty } from '../shared/empty.util'; +import { HALEndpointService } from '../core/shared/hal-endpoint.service'; +import { RestRequest } from '../core/data/request.models'; +import { SearchOptions } from '../shared/search/search-options.model'; + +/** + * The statistics service + */ +@Injectable() +export class StatisticsService { + + constructor( + protected requestService: RequestService, + protected halService: HALEndpointService, + ) { + } + + private sendEvent(linkPath: string, body: any) { + const requestId = this.requestService.generateRequestId(); + this.halService.getEndpoint(linkPath).pipe( + map((endpoint: string) => new TrackRequest(requestId, endpoint, JSON.stringify(body))), + take(1) // otherwise the previous events will fire again + ).subscribe((request: RestRequest) => this.requestService.configure(request)); + } + + /** + * To track a page view + * @param dso: The dso which was viewed + */ + trackViewEvent(dso: DSpaceObject) { + this.sendEvent('/statistics/viewevents', { + targetId: dso.uuid, + targetType: (dso as any).type + }); + } + + /** + * To track a search + * @param searchOptions: The query, scope, dsoType and configuration of the search. Filters from this object are ignored in favor of the filters parameter of this method. + * @param page: An object that describes the pagination status + * @param sort: An object that describes the sort status + * @param filters: An array of search filters used to filter the result set + */ + trackSearchEvent( + searchOptions: SearchOptions, + page: { size: number, totalElements: number, totalPages: number, number: number }, + sort: { by: string, order: string }, + filters?: Array<{ filter: string, operator: string, value: string, label: string }> + ) { + const body = { + query: searchOptions.query, + page: { + size: page.size, + totalElements: page.totalElements, + totalPages: page.totalPages, + number: page.number + }, + sort: { + by: sort.by, + order: sort.order.toLowerCase() + }, + }; + if (hasValue(searchOptions.configuration)) { + Object.assign(body, { configuration: searchOptions.configuration }) + } + if (hasValue(searchOptions.dsoType)) { + Object.assign(body, { dsoType: searchOptions.dsoType.toLowerCase() }) + } + if (hasValue(searchOptions.scope)) { + Object.assign(body, { scope: searchOptions.scope }) + } + if (isNotEmpty(filters)) { + const bodyFilters = []; + for (let i = 0, arrayLength = filters.length; i < arrayLength; i++) { + const filter = filters[i]; + bodyFilters.push({ + filter: filter.filter, + operator: filter.operator, + value: filter.value, + label: filter.label + }) + } + Object.assign(body, { appliedFilters: bodyFilters }) + } + this.sendEvent('/statistics/searchevents', body); + } + +} diff --git a/src/app/statistics/track-request.model.ts b/src/app/statistics/track-request.model.ts new file mode 100644 index 0000000000..770d3146c6 --- /dev/null +++ b/src/app/statistics/track-request.model.ts @@ -0,0 +1,11 @@ +import { ResponseParsingService } from '../core/data/parsing.service'; +import { PostRequest } from '../core/data/request.models'; +import { StatusCodeOnlyResponseParsingService } from '../core/data/status-code-only-response-parsing.service'; +import { GenericConstructor } from '../core/shared/generic-constructor'; + +export class TrackRequest extends PostRequest { + + getResponseParser(): GenericConstructor { + return StatusCodeOnlyResponseParsingService; + } +} diff --git a/src/app/submission/form/collection/submission-form-collection.component.spec.ts b/src/app/submission/form/collection/submission-form-collection.component.spec.ts index b95a140b46..454e3f6d75 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.spec.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.spec.ts @@ -19,8 +19,8 @@ import { SubmissionJsonPatchOperationsService } from '../../../core/submission/s import { SubmissionJsonPatchOperationsServiceStub } from '../../../shared/testing/submission-json-patch-operations-service-stub'; import { JsonPatchOperationsBuilder } from '../../../core/json-patch/builder/json-patch-operations-builder'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; -import { Community } from '../../../core/shared/community.model'; import { RemoteData } from '../../../core/data/remote-data'; +import { Community } from '../../../core/shared/community.model'; import { PaginatedList } from '../../../core/data/paginated-list'; import { PageInfo } from '../../../core/shared/page-info.model'; import { Collection } from '../../../core/shared/collection.model'; diff --git a/src/app/submission/form/collection/submission-form-collection.component.ts b/src/app/submission/form/collection/submission-form-collection.component.ts index 79d2f2a7bc..d318bfe687 100644 --- a/src/app/submission/form/collection/submission-form-collection.component.ts +++ b/src/app/submission/form/collection/submission-form-collection.component.ts @@ -1,28 +1,8 @@ -import { - ChangeDetectorRef, - Component, - EventEmitter, - HostListener, - Input, - OnChanges, - OnInit, - Output, - SimpleChanges -} from '@angular/core'; +import { ChangeDetectorRef, Component, EventEmitter, HostListener, Input, OnChanges, OnInit, Output, SimpleChanges } from '@angular/core'; import { FormControl } from '@angular/forms'; import { BehaviorSubject, combineLatest, Observable, of as observableOf, Subscription } from 'rxjs'; -import { - debounceTime, - distinctUntilChanged, - filter, - find, - flatMap, - map, - mergeMap, - reduce, - startWith -} from 'rxjs/operators'; +import { debounceTime, distinctUntilChanged, filter, map, mergeMap, reduce, startWith, flatMap, find } from 'rxjs/operators'; import { Collection } from '../../../core/shared/collection.model'; import { CommunityDataService } from '../../../core/data/community-data.service'; @@ -36,7 +16,7 @@ import { SubmissionService } from '../../submission.service'; import { SubmissionObject } from '../../../core/submission/models/submission-object.model'; import { SubmissionJsonPatchOperationsService } from '../../../core/submission/submission-json-patch-operations.service'; import { CollectionDataService } from '../../../core/data/collection-data.service'; -import { FindAllOptions } from '../../../core/data/request.models'; +import { FindListOptions } from '../../../core/data/request.models'; /** * An interface to represent a collection entry @@ -205,7 +185,7 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { map((collectionRD: RemoteData) => collectionRD.payload.name) ); - const findOptions: FindAllOptions = { + const findOptions: FindListOptions = { elementsPerPage: 1000 }; @@ -247,7 +227,8 @@ export class SubmissionFormCollectionComponent implements OnChanges, OnInit { } else { return listCollection.filter((v) => v.collection.name.toLowerCase().indexOf(searchTerm.toLowerCase()) > -1).slice(0, 5); } - })); + }) + ); } } } diff --git a/src/app/submission/form/submission-form.component.ts b/src/app/submission/form/submission-form.component.ts index b592972839..1732075bf8 100644 --- a/src/app/submission/form/submission-form.component.ts +++ b/src/app/submission/form/submission-form.component.ts @@ -1,7 +1,7 @@ import { ChangeDetectorRef, Component, Input, OnChanges, OnDestroy, SimpleChanges } from '@angular/core'; import { of as observableOf, Observable, Subscription } from 'rxjs'; -import { distinctUntilChanged, filter, flatMap, map } from 'rxjs/operators'; +import { distinctUntilChanged, filter, flatMap, map, switchMap } from 'rxjs/operators'; import { hasValue, isNotEmpty } from '../../shared/empty.util'; import { SubmissionObjectEntry } from '../objects/submission-objects.reducer'; @@ -125,7 +125,7 @@ export class SubmissionFormComponent implements OnChanges, OnDestroy { map((submission: SubmissionObjectEntry) => submission.isLoading), map((isLoading: boolean) => isLoading), distinctUntilChanged(), - flatMap((isLoading: boolean) => { + switchMap((isLoading: boolean) => { if (!isLoading) { return this.getSectionsList(); } else { diff --git a/src/app/submission/sections/form/section-form.component.spec.ts b/src/app/submission/sections/form/section-form.component.spec.ts index eb56a92113..be13c14941 100644 --- a/src/app/submission/sections/form/section-form.component.spec.ts +++ b/src/app/submission/sections/form/section-form.component.spec.ts @@ -43,6 +43,9 @@ import { SubmissionSectionError } from '../../objects/submission-objects.reducer import { DynamicFormControlEvent, DynamicFormControlEventType } from '@ng-dynamic-forms/core'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { FormRowModel } from '../../../core/config/models/config-submission-form.model'; +import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service'; +import { RemoteData } from '../../../core/data/remote-data'; +import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model'; function getMockSubmissionFormsConfigService(): SubmissionFormsConfigService { return jasmine.createSpyObj('FormOperationsService', { @@ -115,11 +118,11 @@ const testFormConfiguration = { const testFormModel = [ new DynamicRowGroupModel({ id: 'df-row-group-config-1', - group: [new DsDynamicInputModel({ id: 'dc.title' })], + group: [new DsDynamicInputModel({ id: 'dc.title', metadataFields: [], repeatable: false, submissionId: '1234' })], }), new DynamicRowGroupModel({ id: 'df-row-group-config-2', - group: [new DsDynamicInputModel({ id: 'dc.contributor' })], + group: [new DsDynamicInputModel({ id: 'dc.contributor', metadataFields: [], repeatable: false, submissionId: '1234' })], }) ]; @@ -179,6 +182,7 @@ describe('SubmissionSectionformComponent test suite', () => { { provide: 'collectionIdProvider', useValue: collectionId }, { provide: 'sectionDataProvider', useValue: sectionObject }, { provide: 'submissionIdProvider', useValue: submissionId }, + { provide: WorkspaceitemDataService, useValue: {findById: () => observableOf(new RemoteData(false, false, true, null, new WorkspaceItem()))}}, ChangeDetectorRef, SubmissionSectionformComponent ], diff --git a/src/app/submission/sections/form/section-form.component.ts b/src/app/submission/sections/form/section-form.component.ts index 2269ccd5f1..49dbaea807 100644 --- a/src/app/submission/sections/form/section-form.component.ts +++ b/src/app/submission/sections/form/section-form.component.ts @@ -15,7 +15,10 @@ import { hasValue, isNotEmpty, isUndefined } from '../../../shared/empty.util'; import { ConfigData } from '../../../core/config/config-data'; import { JsonPatchOperationPathCombiner } from '../../../core/json-patch/builder/json-patch-operation-path-combiner'; import { SubmissionFormsModel } from '../../../core/config/models/config-submission-forms.model'; -import { SubmissionSectionError, SubmissionSectionObject } from '../../objects/submission-objects.reducer'; +import { + SubmissionSectionError, + SubmissionSectionObject +} from '../../objects/submission-objects.reducer'; import { FormFieldPreviousValueObject } from '../../../shared/form/builder/models/form-field-previous-value-object'; import { GLOBAL_CONFIG } from '../../../../config'; import { GlobalConfig } from '../../../../config/global-config.interface'; @@ -28,6 +31,11 @@ import { NotificationsService } from '../../../shared/notifications/notification import { SectionsService } from '../sections.service'; import { difference } from '../../../shared/object.util'; import { WorkspaceitemSectionFormObject } from '../../../core/submission/models/workspaceitem-section-form.model'; +import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model'; +import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service'; +import { combineLatest as combineLatestObservable } from 'rxjs'; +import { getSucceededRemoteData } from '../../../core/shared/operators'; +import { RemoteData } from '../../../core/data/remote-data'; /** * This component represents a section that contains a Form. @@ -100,6 +108,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent { */ protected subs: Subscription[] = []; + protected workspaceItem: WorkspaceItem; /** * The FormComponent reference */ @@ -131,6 +140,7 @@ export class SubmissionSectionformComponent extends SectionModelComponent { protected sectionService: SectionsService, protected submissionService: SubmissionService, protected translate: TranslateService, + protected workspaceItemDataService: WorkspaceitemDataService, @Inject(GLOBAL_CONFIG) protected EnvConfig: GlobalConfig, @Inject('collectionIdProvider') public injectedCollectionId: string, @Inject('sectionDataProvider') public injectedSectionData: SectionDataObject, @@ -144,15 +154,19 @@ export class SubmissionSectionformComponent extends SectionModelComponent { onSectionInit() { this.pathCombiner = new JsonPatchOperationPathCombiner('sections', this.sectionData.id); this.formId = this.formService.getUniqueId(this.sectionData.id); - this.formConfigService.getConfigByHref(this.sectionData.config).pipe( map((configData: ConfigData) => configData.payload), tap((config: SubmissionFormsModel) => this.formConfig = config), - flatMap(() => this.sectionService.getSectionData(this.submissionId, this.sectionData.id)), + flatMap(() => + combineLatestObservable( + this.sectionService.getSectionData(this.submissionId, this.sectionData.id), + this.workspaceItemDataService.findById(this.submissionId).pipe(getSucceededRemoteData(), map((wsiRD: RemoteData) => wsiRD.payload)) + )), take(1)) - .subscribe((sectionData: WorkspaceitemSectionFormObject) => { + .subscribe(([sectionData, workspaceItem]: [WorkspaceitemSectionFormObject, WorkspaceItem]) => { if (isUndefined(this.formModel)) { this.sectionData.errors = []; + this.workspaceItem = workspaceItem; // Is the first loading so init form this.initForm(sectionData); this.sectionData.data = sectionData; @@ -215,16 +229,19 @@ export class SubmissionSectionformComponent extends SectionModelComponent { initForm(sectionData: WorkspaceitemSectionFormObject): void { try { this.formModel = this.formBuilderService.modelFromConfiguration( + this.submissionId, this.formConfig, this.collectionId, sectionData, - this.submissionService.getSubmissionScope()); + this.submissionService.getSubmissionScope() + ); } catch (e) { const msg: string = this.translate.instant('error.submission.sections.init-form-error') + e.toString(); const sectionError: SubmissionSectionError = { message: msg, path: '/sections/' + this.sectionData.id }; + console.error(e.stack); this.sectionService.setSectionError(this.submissionId, this.sectionData.id, sectionError); } } diff --git a/src/app/submission/sections/sections-type.ts b/src/app/submission/sections/sections-type.ts index 02e0ba478b..34ecafe42b 100644 --- a/src/app/submission/sections/sections-type.ts +++ b/src/app/submission/sections/sections-type.ts @@ -1,4 +1,5 @@ export enum SectionsType { + Relationships = 'relationships', SubmissionForm = 'submission-form', Upload = 'upload', License = 'license', diff --git a/src/app/submission/sections/sections.service.ts b/src/app/submission/sections/sections.service.ts index aed83143a5..f6ad5ef0cf 100644 --- a/src/app/submission/sections/sections.service.ts +++ b/src/app/submission/sections/sections.service.ts @@ -84,7 +84,7 @@ export class SectionsService { } else if (!isEqual(currentErrors, prevErrors)) { // compare previous error list with the current one const dispatchedErrors = []; - // Itereate over the current error list + // Iterate over the current error list currentErrors.forEach((error: SubmissionSectionError) => { const errorPaths: SectionErrorPath[] = parseSectionErrorPaths(error.path); diff --git a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts index b2575d1d58..8cf0d22d20 100644 --- a/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts +++ b/src/app/submission/sections/upload/file/edit/section-upload-file-edit.component.ts @@ -166,6 +166,7 @@ export class SubmissionSectionUploadFileEditComponent implements OnChanges { const formModel: DynamicFormControlModel[] = []; const metadataGroupModelConfig = Object.assign({}, BITSTREAM_METADATA_FORM_GROUP_CONFIG); metadataGroupModelConfig.group = this.formBuilderService.modelFromConfiguration( + this.submissionId, configForm, this.collectionId, this.fileData.metadata, diff --git a/src/app/submission/submission.service.spec.ts b/src/app/submission/submission.service.spec.ts index d67fb3679a..3a95b0747b 100644 --- a/src/app/submission/submission.service.spec.ts +++ b/src/app/submission/submission.service.spec.ts @@ -37,18 +37,16 @@ import { SaveSubmissionSectionFormAction, SetActiveSectionAction } from './objects/submission-objects.actions'; -import { RemoteData } from '../core/data/remote-data'; import { RemoteDataError } from '../core/data/remote-data-error'; import { throwError as observableThrowError } from 'rxjs/internal/observable/throwError'; import { createFailedRemoteDataObject, createSuccessfulRemoteDataObject, - createSuccessfulRemoteDataObject$ } from '../shared/testing/utils'; import { getMockSearchService } from '../shared/mocks/mock-search-service'; import { getMockRequestService } from '../shared/mocks/mock-request.service'; -import { SearchService } from '../+search-page/search-service/search.service'; import { RequestService } from '../core/data/request.service'; +import { SearchService } from '../core/shared/search/search.service'; describe('SubmissionService test suite', () => { const config = MOCK_SUBMISSION_CONFIG; diff --git a/src/app/submission/submission.service.ts b/src/app/submission/submission.service.ts index c9be658b31..fa8024af53 100644 --- a/src/app/submission/submission.service.ts +++ b/src/app/submission/submission.service.ts @@ -28,12 +28,7 @@ import { SaveSubmissionSectionFormAction, SetActiveSectionAction } from './objects/submission-objects.actions'; -import { - SubmissionObjectEntry, - SubmissionSectionEntry, - SubmissionSectionError, - SubmissionSectionObject -} from './objects/submission-objects.reducer'; +import { SubmissionObjectEntry, SubmissionSectionEntry, SubmissionSectionError, SubmissionSectionObject } from './objects/submission-objects.reducer'; import { submissionObjectFromIdSelector } from './selectors'; import { GlobalConfig } from '../../config/global-config.interface'; import { GLOBAL_CONFIG } from '../../config'; @@ -55,8 +50,8 @@ import { createSuccessfulRemoteDataObject, createSuccessfulRemoteDataObject$ } from '../shared/testing/utils'; -import { SearchService } from '../+search-page/search-service/search.service'; import { RequestService } from '../core/data/request.service'; +import { SearchService } from '../core/shared/search/search.service'; /** * A service that provides methods used in submission process. @@ -74,6 +69,8 @@ export class SubmissionService { */ protected timer$: Observable; + private workspaceLinkPath = 'workspaceitems'; + private workflowLinkPath = 'workflowitems'; /** * Initialize service variables * @param {GlobalConfig} EnvConfig @@ -116,7 +113,7 @@ export class SubmissionService { * observable of SubmissionObject */ createSubmission(): Observable { - return this.restService.postToEndpoint('workspaceitems', {}).pipe( + return this.restService.postToEndpoint(this.workspaceLinkPath, {}).pipe( map((workspaceitem: SubmissionObject) => workspaceitem[0]), catchError(() => observableOf({}))) } @@ -134,7 +131,7 @@ export class SubmissionService { let headers = new HttpHeaders(); headers = headers.append('Content-Type', 'text/uri-list'); options.headers = headers; - return this.restService.postToEndpoint('workflowitems', selfUrl, null, options) as Observable; + return this.restService.postToEndpoint(this.workflowLinkPath, selfUrl, null, options) as Observable; } /** @@ -328,9 +325,9 @@ export class SubmissionService { getSubmissionObjectLinkName(): string { const url = this.router.routerState.snapshot.url; if (url.startsWith('/workspaceitems') || url.startsWith('/submit')) { - return 'workspaceitems'; + return this.workspaceLinkPath; } else if (url.startsWith('/workflowitems')) { - return 'workflowitems'; + return this.workflowLinkPath; } else { return 'edititems'; } @@ -345,10 +342,10 @@ export class SubmissionService { getSubmissionScope(): SubmissionScopeType { let scope: SubmissionScopeType; switch (this.getSubmissionObjectLinkName()) { - case 'workspaceitems': + case this.workspaceLinkPath: scope = SubmissionScopeType.WorkspaceItem; break; - case 'workflowitems': + case this.workflowLinkPath: scope = SubmissionScopeType.WorkflowItem; break; } diff --git a/src/app/submission/submit/submission-submit.component.ts b/src/app/submission/submit/submission-submit.component.ts index dbfd2f5a40..448ccf97e2 100644 --- a/src/app/submission/submit/submission-submit.component.ts +++ b/src/app/submission/submit/submission-submit.component.ts @@ -56,7 +56,7 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { * * @param {ChangeDetectorRef} changeDetectorRef * @param {NotificationsService} notificationsService - * @param {SubmissionService} submissioService + * @param {SubmissionService} submissionService * @param {Router} router * @param {TranslateService} translate * @param {ViewContainerRef} viewContainerRef @@ -64,7 +64,7 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { constructor(private changeDetectorRef: ChangeDetectorRef, private notificationsService: NotificationsService, private router: Router, - private submissioService: SubmissionService, + private submissionService: SubmissionService, private translate: TranslateService, private viewContainerRef: ViewContainerRef) { } @@ -75,7 +75,7 @@ export class SubmissionSubmitComponent implements OnDestroy, OnInit { ngOnInit() { // NOTE execute the code on the browser side only, otherwise it is executed twice this.subs.push( - this.submissioService.createSubmission() + this.submissionService.createSubmission() .subscribe((submissionObject: SubmissionObject) => { // NOTE new submission is created on the browser side only if (isNotNull(submissionObject)) { diff --git a/src/backend/api.ts b/src/backend/api.ts index e1943b5d30..a4763f0be7 100644 --- a/src/backend/api.ts +++ b/src/backend/api.ts @@ -117,7 +117,7 @@ export function createMockApi() { const id = req.params.item_id; try { req.item_id = id; - req.item = ITEMS.items.find((item) => { + req.itemRD$ = ITEMS.items.find((item) => { return item.id === id; }); next(); @@ -127,7 +127,7 @@ export function createMockApi() { }); router.route('/items/:item_id').get((req, res) => { - res.json(toHALResponse(req, req.item)); + res.json(toHALResponse(req, req.itemRD$)); }); router.route('/bundles').get((req, res) => { diff --git a/src/modules/app/browser-app.module.ts b/src/modules/app/browser-app.module.ts index ede2b53e74..87b830ee7d 100644 --- a/src/modules/app/browser-app.module.ts +++ b/src/modules/app/browser-app.module.ts @@ -21,6 +21,8 @@ import { AuthService } from '../../app/core/auth/auth.service'; import { Angulartics2Module } from 'angulartics2'; import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import { SubmissionService } from '../../app/submission/submission.service'; +import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider'; +import { StatisticsModule } from '../../app/statistics/statistics.module'; export const REQ_KEY = makeStateKey('req'); @@ -47,7 +49,8 @@ export function getRequest(transferState: TransferState): any { preloadingStrategy: IdlePreload }), - Angulartics2Module.forRoot([Angulartics2GoogleAnalytics]), + StatisticsModule.forRoot(), + Angulartics2Module.forRoot([Angulartics2GoogleAnalytics, Angulartics2DSpace]), BrowserAnimationsModule, DSpaceBrowserTransferStateModule, TranslateModule.forRoot({ diff --git a/src/modules/app/server-app.module.ts b/src/modules/app/server-app.module.ts index 02abf6449b..44b21859bd 100644 --- a/src/modules/app/server-app.module.ts +++ b/src/modules/app/server-app.module.ts @@ -22,6 +22,8 @@ import { Angulartics2GoogleAnalytics } from 'angulartics2/ga'; import { AngularticsMock } from '../../app/shared/mocks/mock-angulartics.service'; import { SubmissionService } from '../../app/submission/submission.service'; import { ServerSubmissionService } from '../../app/submission/server-submission.service'; +import { Angulartics2DSpace } from '../../app/statistics/angulartics/dspace-provider'; +import { Angulartics2Module } from 'angulartics2'; export function createTranslateLoader() { return new TranslateJson5UniversalLoader('dist/assets/i18n/', '.json5'); @@ -45,6 +47,7 @@ export function createTranslateLoader() { deps: [] } }), + Angulartics2Module.forRoot([Angulartics2GoogleAnalytics, Angulartics2DSpace]), ServerModule, AppModule ], @@ -53,6 +56,10 @@ export function createTranslateLoader() { provide: Angulartics2GoogleAnalytics, useClass: AngularticsMock }, + { + provide: Angulartics2DSpace, + useClass: AngularticsMock + }, { provide: AuthService, useClass: ServerAuthService diff --git a/themes/mantis/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.scss b/themes/mantis/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.scss deleted file mode 100644 index 42b8e0205b..0000000000 --- a/themes/mantis/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.scss +++ /dev/null @@ -1,5 +0,0 @@ -@import 'src/app/+search-page/search-filters/search-filter/search-range-filter/search-range-filter.component.scss'; - -::ng-deep .noUi-connect { - background: $info; -} diff --git a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html index 99d92d2af8..907f70b941 100644 --- a/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html +++ b/themes/mantis/app/entity-groups/journal-entities/item-pages/journal-volume/journal-volume.component.html @@ -4,7 +4,7 @@ diff --git a/themes/mantis/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.html b/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html similarity index 79% rename from themes/mantis/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.html rename to themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html index 15529a1bd5..ee78d9c653 100644 --- a/themes/mantis/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.html +++ b/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.html @@ -53,18 +53,6 @@
- - - -
+
+
+ + +
+
diff --git a/themes/mantis/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.scss b/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss similarity index 86% rename from themes/mantis/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.scss rename to themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss index 54651aede0..4a1d2516da 100644 --- a/themes/mantis/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.scss +++ b/themes/mantis/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss @@ -1,4 +1,4 @@ -@import 'src/app/entity-groups/research-entities/item-pages/orgunit/orgunit.component.scss'; +@import 'src/app/entity-groups/research-entities/item-pages/org-unit/org-unit.component.scss'; :host { > * { diff --git a/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html b/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html index bb5cb1b787..1679f9354d 100644 --- a/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html +++ b/themes/mantis/app/entity-groups/research-entities/item-pages/person/person.component.html @@ -79,7 +79,10 @@

{{"item.page.person.search.title" | translate}}

- - + +
diff --git a/themes/mantis/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html b/themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html similarity index 100% rename from themes/mantis/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html rename to themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html diff --git a/themes/mantis/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html b/themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html similarity index 100% rename from themes/mantis/app/+search-page/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html rename to themes/mantis/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-range-option/search-facet-range-option.component.html diff --git a/themes/mantis/app/+search-page/search-filters/search-filter/search-filter.component.html b/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.html similarity index 100% rename from themes/mantis/app/+search-page/search-filters/search-filter/search-filter.component.html rename to themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.html diff --git a/themes/mantis/app/+search-page/search-filters/search-filter/search-filter.component.scss b/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.scss similarity index 61% rename from themes/mantis/app/+search-page/search-filters/search-filter/search-filter.component.scss rename to themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.scss index 8e9b1d32b1..4d2d29ae41 100644 --- a/themes/mantis/app/+search-page/search-filters/search-filter/search-filter.component.scss +++ b/themes/mantis/app/shared/search/search-filters/search-filter/search-filter.component.scss @@ -1,4 +1,4 @@ -@import 'src/app/+search-page/search-filters/search-filter/search-filter.component.scss'; +@import 'src/app/shared/search/search-filters/search-filter/search-filter.component.scss'; .facet-filter { background-color: map-get($theme-colors, light); diff --git a/themes/mantis/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss b/themes/mantis/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss new file mode 100644 index 0000000000..7edcb8f063 --- /dev/null +++ b/themes/mantis/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss @@ -0,0 +1,5 @@ +@import 'src/app/shared/search/search-filters/search-filter/search-range-filter/search-range-filter.component.scss'; + +::ng-deep .noUi-connect { + background: $info; +} diff --git a/themes/mantis/app/+search-page/search-filters/search-filters.component.html b/themes/mantis/app/shared/search/search-filters/search-filters.component.html similarity index 100% rename from themes/mantis/app/+search-page/search-filters/search-filters.component.html rename to themes/mantis/app/shared/search/search-filters/search-filters.component.html diff --git a/themes/mantis/app/+search-page/search-settings/search-settings.component.html b/themes/mantis/app/shared/search/search-settings/search-settings.component.html similarity index 100% rename from themes/mantis/app/+search-page/search-settings/search-settings.component.html rename to themes/mantis/app/shared/search/search-settings/search-settings.component.html diff --git a/themes/mantis/app/+search-page/search-settings/search-settings.component.scss b/themes/mantis/app/shared/search/search-settings/search-settings.component.scss similarity index 65% rename from themes/mantis/app/+search-page/search-settings/search-settings.component.scss rename to themes/mantis/app/shared/search/search-settings/search-settings.component.scss index 602c8ca4c3..073039dae8 100644 --- a/themes/mantis/app/+search-page/search-settings/search-settings.component.scss +++ b/themes/mantis/app/shared/search/search-settings/search-settings.component.scss @@ -1,4 +1,4 @@ -@import 'src/app/+search-page/search-settings/search-settings.component.scss'; +@import 'src/app/shared/search/search-settings/search-settings.component.scss'; .setting-option { background-color: map-get($theme-colors, light); diff --git a/webpack/webpack.common.js b/webpack/webpack.common.js index 028815d958..e63ae024ed 100644 --- a/webpack/webpack.common.js +++ b/webpack/webpack.common.js @@ -15,6 +15,9 @@ module.exports = (env) => { let copyWebpackOptions = [{ from: path.join(__dirname, '..', 'node_modules', '@fortawesome', 'fontawesome-free', 'webfonts'), to: path.join('assets', 'fonts') + }, { + from: path.join(__dirname, '..', 'resources', 'fonts'), + to: path.join('assets', 'fonts') }, { from: path.join(__dirname, '..', 'resources', 'images'), to: path.join('assets', 'images') @@ -24,6 +27,15 @@ module.exports = (env) => { } ]; + const themeFonts = path.join(themePath, 'resources', 'fonts'); + if(theme && fs.existsSync(themeFonts)) { + copyWebpackOptions.push({ + from: themeFonts, + to: path.join('assets', 'fonts') , + force: true, + }); + } + const themeImages = path.join(themePath, 'resources', 'images'); if(theme && fs.existsSync(themeImages)) { copyWebpackOptions.push({ @@ -107,12 +119,6 @@ module.exports = (env) => { sourceMap: true } }, - { - loader: 'resolve-url-loader', - options: { - sourceMap: true - } - }, { loader: 'sass-loader', options: { @@ -120,6 +126,12 @@ module.exports = (env) => { includePaths: [projectRoot('./'), path.join(themePath, 'styles')] } }, + { + loader: 'resolve-url-loader', + options: { + sourceMap: true + } + }, { loader: 'sass-resources-loader', options: { @@ -145,23 +157,23 @@ module.exports = (env) => { sourceMap: true } }, - { - loader: 'resolve-url-loader', - options: { - sourceMap: true - } - }, { loader: 'sass-loader', options: { sourceMap: true, includePaths: [projectRoot('./'), path.join(themePath, 'styles')] } - } + }, + { + loader: 'resolve-url-loader', + options: { + sourceMap: true + } + }, ] }, { - test: /\.html$/, + test: /\.(html|eot|ttf|otf|svg|woff|woff2)$/, loader: 'raw-loader' } ] diff --git a/webpack/webpack.test.js b/webpack/webpack.test.js index 83e6e44e79..de53de31c4 100644 --- a/webpack/webpack.test.js +++ b/webpack/webpack.test.js @@ -160,12 +160,6 @@ module.exports = function (env) { sourceMap: true } }, - { - loader: 'resolve-url-loader', - options: { - sourceMap: true - } - }, { loader: 'sass-loader', options: { @@ -173,6 +167,12 @@ module.exports = function (env) { includePaths: [projectRoot('./'), path.join(themePath, 'styles')] } }, + { + loader: 'resolve-url-loader', + options: { + sourceMap: true + } + }, { loader: 'sass-resources-loader', options: { @@ -198,19 +198,19 @@ module.exports = function (env) { sourceMap: true } }, - { - loader: 'resolve-url-loader', - options: { - sourceMap: true - } - }, { loader: 'sass-loader', options: { sourceMap: true, includePaths: [projectRoot('./'), path.join(themePath, 'styles')] } - } + }, + { + loader: 'resolve-url-loader', + options: { + sourceMap: true + } + }, ] }, diff --git a/yarn.lock b/yarn.lock index 69f4a072ae..b4ec416395 100644 --- a/yarn.lock +++ b/yarn.lock @@ -35,6 +35,13 @@ dependencies: tslib "^1.9.0" +"@angular/cdk@^6.4.7": + version "6.4.7" + resolved "https://registry.yarnpkg.com/@angular/cdk/-/cdk-6.4.7.tgz#1549b304dd412e82bd854cc55a7d5c6772ee0411" + integrity sha512-18x0U66fLD5kGQWZ9n3nb75xQouXlWs7kUDaTd8HTrHpT1s2QIAqlLd1KxfrYiVhsEC2jPQaoiae7VnBlcvkBg== + dependencies: + tslib "^1.7.1" + "@angular/cli@^6.1.5": version "6.1.5" resolved "https://registry.yarnpkg.com/@angular/cli/-/cli-6.1.5.tgz#312c062631285ff06fd07ecde8afe22cdef5a0e1" @@ -2093,15 +2100,14 @@ cliui@^5.0.0: strip-ansi "^5.2.0" wrap-ansi "^5.1.0" -clone-deep@^2.0.1: - version "2.0.2" - resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-2.0.2.tgz#00db3a1e173656730d1188c3d6aced6d7ea97713" - integrity sha512-SZegPTKjCgpQH63E+eN6mVEEPdQBOUzjyJm5Pora4lrwWRFS8I0QAxV/KD6vV/i0WuijHZWQC1fMsPEdxfdVCQ== +clone-deep@^4.0.1: + version "4.0.1" + resolved "https://registry.yarnpkg.com/clone-deep/-/clone-deep-4.0.1.tgz#c19fd9bdbbf85942b4fd979c84dcf7d5f07c2387" + integrity sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ== dependencies: - for-own "^1.0.0" is-plain-object "^2.0.4" - kind-of "^6.0.0" - shallow-clone "^1.0.0" + kind-of "^6.0.2" + shallow-clone "^3.0.0" clone-stats@^0.0.1: version "0.0.1" @@ -4121,11 +4127,6 @@ font-awesome@4.7.0: resolved "https://registry.yarnpkg.com/font-awesome/-/font-awesome-4.7.0.tgz#8fa8cf0411a1a31afd07b06d2902bb9fc815a133" integrity sha1-j6jPBBGhoxr9B7BtKQK7n8gVoTM= -for-in@^0.1.3: - version "0.1.8" - resolved "https://registry.yarnpkg.com/for-in/-/for-in-0.1.8.tgz#d8773908e31256109952b1fdb9b3fa867d2775e1" - integrity sha1-2Hc5COMSVhCZUrH9ubP6hn0ndeE= - for-in@^1.0.1, for-in@^1.0.2: version "1.0.2" resolved "https://registry.yarnpkg.com/for-in/-/for-in-1.0.2.tgz#81068d295a8142ec0ac726c6e2200c30fb6d5e80" @@ -4138,13 +4139,6 @@ for-own@^0.1.4: dependencies: for-in "^1.0.1" -for-own@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/for-own/-/for-own-1.0.0.tgz#c63332f415cedc4b04dbfe70cf836494c53cb44b" - integrity sha1-xjMy9BXO3EsE2/5wz4NklMU8tEs= - dependencies: - for-in "^1.0.1" - forever-agent@~0.6.1: version "0.6.1" resolved "https://registry.yarnpkg.com/forever-agent/-/forever-agent-0.6.1.tgz#fbc71f0c41adeb37f96c577ad1ed42d8fdacca91" @@ -6148,7 +6142,7 @@ loader-utils@^0.2.12, loader-utils@^0.2.15, loader-utils@^0.2.16: json5 "^0.5.0" object-assign "^4.0.1" -loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1.0: +loader-utils@^1.0.0, loader-utils@^1.0.2, loader-utils@^1.1.0: version "1.1.0" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.1.0.tgz#c98aef488bcceda2ffb5e2de646d6a754429f5cd" integrity sha1-yYrvSIvM7aL/teLeZG1qdUQp9c0= @@ -6157,7 +6151,7 @@ loader-utils@^1.0.0, loader-utils@^1.0.1, loader-utils@^1.0.2, loader-utils@^1.1 emojis-list "^2.0.0" json5 "^0.5.0" -loader-utils@^1.0.4: +loader-utils@^1.0.1, loader-utils@^1.0.4: version "1.2.3" resolved "https://registry.yarnpkg.com/loader-utils/-/loader-utils-1.2.3.tgz#1ff5dc6911c9f0a062531a4c04b609406108c2c7" dependencies: @@ -6371,11 +6365,6 @@ lodash.startswith@^4.2.1: resolved "https://registry.yarnpkg.com/lodash.startswith/-/lodash.startswith-4.2.1.tgz#c598c4adce188a27e53145731cdc6c0e7177600c" integrity sha1-xZjErc4YiiflMUVzHNxsDnF3YAw= -lodash.tail@^4.1.1: - version "4.1.1" - resolved "https://registry.yarnpkg.com/lodash.tail/-/lodash.tail-4.1.1.tgz#d2333a36d9e7717c8ad2f7cacafec7c32b444664" - integrity sha1-0jM6NtnncXyK0vfKyv7HwytERmQ= - lodash.template@^3.0.0: version "3.6.2" resolved "https://registry.yarnpkg.com/lodash.template/-/lodash.template-3.6.2.tgz#f8cdecc6169a255be9098ae8b0c53d378931d14f" @@ -6849,14 +6838,6 @@ mixin-deep@^1.2.0: for-in "^1.0.2" is-extendable "^1.0.1" -mixin-object@^2.0.1: - version "2.0.1" - resolved "https://registry.yarnpkg.com/mixin-object/-/mixin-object-2.0.1.tgz#4fb949441dab182540f1fe035ba60e1947a5e57e" - integrity sha1-T7lJRB2rGCVA8f4DW6YOGUel5X4= - dependencies: - for-in "^0.1.3" - is-extendable "^0.1.1" - mkdirp@0.5.1, mkdirp@0.5.x, "mkdirp@>=0.5 0", mkdirp@^0.5.0, mkdirp@^0.5.1, mkdirp@~0.5.0, mkdirp@~0.5.1: version "0.5.1" resolved "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz#30057438eac6cf7f8c4767f38648d6697d75c903" @@ -8967,6 +8948,11 @@ querystringify@^2.0.0: resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.0.0.tgz#fa3ed6e68eb15159457c89b37bc6472833195755" integrity sha512-eTPo5t/4bgaMNZxyjWx6N2a6AuE0mq51KWvpc7nU/MAqixcI6v6KrGUKES0HaomdnolQBBXU/++X6/QQ9KL4tw== +querystringify@^2.1.1: + version "2.1.1" + resolved "https://registry.yarnpkg.com/querystringify/-/querystringify-2.1.1.tgz#60e5a5fd64a7f8bfa4d2ab2ed6fdf4c85bad154e" + integrity sha512-w7fLxIRCRT7U8Qu53jQnJyPkYZIaR4n5151KMfcJlO/A9397Wxb1amJvROTK6TOnp7PfoAmg/qXiNHI+08jRfA== + random-bytes@~1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/random-bytes/-/random-bytes-1.0.0.tgz#4f68a1dc0ae58bd3fb95848c30324db75d64360b" @@ -9654,17 +9640,16 @@ sass-graph@^2.2.4: scss-tokenizer "^0.2.3" yargs "^7.0.0" -sass-loader@7.1.0: - version "7.1.0" - resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.1.0.tgz#16fd5138cb8b424bf8a759528a1972d72aad069d" - integrity sha512-+G+BKGglmZM2GUSfT9TLuEp6tzehHPjAMoRRItOojWIqIGPloVCMhNIQuG639eJ+y033PaGTSjLaTHts8Kw79w== +sass-loader@^7.1.0: + version "7.3.1" + resolved "https://registry.yarnpkg.com/sass-loader/-/sass-loader-7.3.1.tgz#a5bf68a04bcea1c13ff842d747150f7ab7d0d23f" + integrity sha512-tuU7+zm0pTCynKYHpdqaPpe+MMTQ76I9TPZ7i4/5dZsigE350shQWe5EZNl5dBidM49TPET75tNqRbcsUZWeNA== dependencies: - clone-deep "^2.0.1" + clone-deep "^4.0.1" loader-utils "^1.0.1" - lodash.tail "^4.1.1" neo-async "^2.5.0" - pify "^3.0.0" - semver "^5.5.0" + pify "^4.0.1" + semver "^6.3.0" sass-resources-loader@^2.0.0: version "2.0.0" @@ -9769,7 +9754,7 @@ semver-intersect@^1.1.2: dependencies: semver "^5.0.0" -"semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2 || 3 || 4 || 5", semver@^5.0.0, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0, semver@^5.5.0: +"semver@2 >=2.2.1 || 3.x || 4 || 5", "semver@2 || 3 || 4 || 5", semver@^5.0.0, semver@^5.0.3, semver@^5.1.0, semver@^5.3.0: version "5.5.1" resolved "https://registry.yarnpkg.com/semver/-/semver-5.5.1.tgz#7dfdd8814bdb7cabc7be0fb1d734cfb66c940477" integrity sha512-PqpAxfrEhlSUWge8dwIp4tZnQ25DIOthpiaHNIthsjEFQD6EvqUKUDM7L8O2rShkFccYo1VjJR0coWfNkCubRw== @@ -9779,7 +9764,12 @@ semver@^5.0.1: resolved "https://registry.yarnpkg.com/semver/-/semver-5.6.0.tgz#7e74256fbaa49c75aa7c7a205cc22799cac80004" integrity sha512-RS9R6R35NYgQn++fkDWaOmqGoj4Ek9gGs+DPxNUZKuwE183xjJroKvyo1IzVFeXvUrvmALy6FWD5xrdJT25gMg== -semver@^6.1.1: +semver@^5.5.0: + version "5.7.1" + resolved "https://registry.yarnpkg.com/semver/-/semver-5.7.1.tgz#a954f931aeba508d307bbf069eff0c01c96116f7" + integrity sha512-sauaDf/PZdVgrLTNYHRtpXa1iRiKcaebiKQ1BJdpQlWH2lCvexQdX55snPFyK7QzpudqbCI0qXFfOasHdyNDGQ== + +semver@^6.1.1, semver@^6.3.0: version "6.3.0" resolved "https://registry.yarnpkg.com/semver/-/semver-6.3.0.tgz#ee0a64c8af5e8ceea67687b133761e1becbd1d3d" integrity sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw== @@ -9910,14 +9900,12 @@ sha.js@^2.4.0, sha.js@^2.4.8: inherits "^2.0.1" safe-buffer "^5.0.1" -shallow-clone@^1.0.0: - version "1.0.0" - resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-1.0.0.tgz#4480cd06e882ef68b2ad88a3ea54832e2c48b571" - integrity sha512-oeXreoKR/SyNJtRJMAKPDSvd28OqEwG4eR/xc856cRGBII7gX9lvAqDxusPm0846z/w/hWYjI1NpKwJ00NHzRA== +shallow-clone@^3.0.0: + version "3.0.1" + resolved "https://registry.yarnpkg.com/shallow-clone/-/shallow-clone-3.0.1.tgz#8f2981ad92531f55035b01fb230769a40e02efa3" + integrity sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA== dependencies: - is-extendable "^0.1.1" - kind-of "^5.0.0" - mixin-object "^2.0.1" + kind-of "^6.0.2" shebang-command@^1.2.0: version "1.2.0" @@ -10833,6 +10821,11 @@ tsickle@^0.32.1: source-map "^0.6.0" source-map-support "^0.5.0" +tslib@^1.7.1: + version "1.10.0" + resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.10.0.tgz#c3c19f95973fb0a62973fb09d90d961ee43e5c8a" + integrity sha512-qOebF53frne81cf0S9B41ByenJ3/IuH8yJKngAX35CmiZySA0khhkovshKK+jGCaMnVomla7gVlIcc3EvKPbTQ== + tslib@^1.8.0, tslib@^1.8.1, tslib@^1.9.0: version "1.9.3" resolved "https://registry.yarnpkg.com/tslib/-/tslib-1.9.3.tgz#d7e4dd79245d85428c4d7e4822a79917954ca286" @@ -11162,6 +11155,14 @@ url-parse@^1.4.3: querystringify "^2.0.0" requires-port "^1.0.0" +url-parse@^1.4.7: + version "1.4.7" + resolved "https://registry.yarnpkg.com/url-parse/-/url-parse-1.4.7.tgz#a8a83535e8c00a316e403a5db4ac1b9b853ae278" + integrity sha512-d3uaVyzDB9tQoSXFvuSUNFibTd9zxd2bkVrDRvF5TmvWWQwqE4lgYJ5m+x1DbecWkw+LK4RNl2CU1hHuOKPVlg== + dependencies: + querystringify "^2.1.1" + requires-port "^1.0.0" + url@^0.11.0: version "0.11.0" resolved "https://registry.yarnpkg.com/url/-/url-0.11.0.tgz#3838e97cfc60521eb73c525a8e55bfdd9e2e28f1"