Merge branch 'master' into w2p-66074_Keep-virtual-metadata-on-relationship-delete

This commit is contained in:
Samuel
2019-12-03 16:54:48 +01:00
47 changed files with 1755 additions and 1905 deletions

View File

@@ -3,13 +3,12 @@
dspace-angular 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 Quick start
----------- -----------
@@ -32,8 +31,6 @@ yarn start
Then go to [http://localhost:3000](http://localhost:3000) in your browser 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. 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 Table of Contents
@@ -42,24 +39,27 @@ Table of Contents
- [Introduction to the technology](#introduction-to-the-technology) - [Introduction to the technology](#introduction-to-the-technology)
- [Requirements](#requirements) - [Requirements](#requirements)
- [Installing](#installing) - [Installing](#installing)
- [Configuring](#configuring) - [Configuring](#configuring)
- [Running the app](#running-the-app) - [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) - [Cleaning](#cleaning)
- [Testing](#testing) - [Testing](#testing)
- [Test a Pull Request](#test-a-pull-request)
- [Documentation](#documentation) - [Documentation](#documentation)
- [Other commands](#other-commands) - [Other commands](#other-commands)
- [Recommended Editors/IDEs](#recommended-editorsides) - [Recommended Editors/IDEs](#recommended-editorsides)
- [Collaborating](#collaborating) - [Collaborating](#collaborating)
- [File Structure](#file-structure) - [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) - [Frequently asked questions](#frequently-asked-questions)
- [License](#license) - [License](#license)
Introduction to the technology 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 Requirements
------------ ------------
@@ -75,8 +75,7 @@ Installing
- `yarn run global` to install the required global dependencies - `yarn run global` to install the required global dependencies
- `yarn install` to install the local dependencies - `yarn install` to install the local dependencies
Configuring ### Configuring
-----------
Default configuration file is located in `config/` folder. 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`. 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. 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 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 Cleaning
-------- --------
@@ -131,10 +142,6 @@ yarn run clean:prod
yarn run clean:dist yarn run clean:dist
``` ```
Running the application with Docker
-----------------------------------
See [Docker Runtime Options](docker/README.md)
Testing Testing
------- -------
@@ -189,21 +196,14 @@ To run all the tests (e.g.: to run tests with Continuous Integration software) y
Documentation 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. 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. 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 Other commands
-------------- --------------
@@ -229,7 +229,7 @@ To get the most out of TypeScript, you'll need a TypeScript-aware editor. We've
Collaborating 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 File Structure
-------------- --------------
@@ -335,10 +335,20 @@ dspace-angular
└── yarn.lock * Yarn lockfile (https://yarnpkg.com/en/docs/yarn-lock) └── 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: 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'; 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 Frequently asked questions
-------------------------- --------------------------
@@ -411,5 +403,4 @@ Frequently asked questions
License License
------- -------
This project's source code is made available under the DSpace BSD License: http://www.dspace.org/license
http://www.dspace.org/license

View File

@@ -141,6 +141,10 @@ module.exports = {
code: 'nl', code: 'nl',
label: 'Nederlands', label: 'Nederlands',
active: false, active: false,
}, {
code: 'pt',
label: 'Português',
active: true,
}], }],
// Browse-By Pages // Browse-By Pages
browseBy: { browseBy: {

View File

@@ -1125,9 +1125,9 @@
// TODO New key - Add a translation // 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": "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 // 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}}", // "item.edit.move.head": "Move item: {{id}}",
// TODO New key - Add a translation // TODO New key - Add a translation
@@ -1153,9 +1153,9 @@
// TODO New key - Add a translation // TODO New key - Add a translation
"item.edit.move.search.placeholder": "Enter a search query to look for collections", "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 // 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", // "item.edit.move.title": "Move item",
// TODO New key - Add a translation // TODO New key - Add a translation
@@ -2911,9 +2911,9 @@
// TODO New key - Add a translation // 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": "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 <strong>upload additional files just dragging & dropping them everywhere in the page</strong>", // "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 <strong>upload additional files just dragging & dropping them everywhere in the page</strong>",
// TODO New key - Add a translation // 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 <strong>upload additional files just dragging & dropping them everywhere in the page</strong>", "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 <strong>upload additional files just dragging & dropping them everywhere in the page</strong>",
// "submission.sections.upload.no-entry": "No", // "submission.sections.upload.no-entry": "No",
// TODO New key - Add a translation // TODO New key - Add a translation

View File

@@ -587,7 +587,7 @@
"item.edit.move.cancel": "Abbrechen", "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.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.error": "Ein Fehler ist beim Verschieben der Ressource aufgetreten",
// "item.edit.move.head": "Move item: {{id}}", // "item.edit.move.head": "Move item: {{id}}",
"item.edit.move.head": "Ressource verschieben: {{id}}", "item.edit.move.head": "Ressource verschieben: {{id}}",
@@ -601,7 +601,7 @@
"item.edit.move.processing": "Verschieben...", "item.edit.move.processing": "Verschieben...",
// "item.edit.move.search.placeholder": "Enter a search query to look for collections", // "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.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.success": "Die Ressource wurde erfolgreich verschoben",
// "item.edit.move.title": "Move item", // "item.edit.move.title": "Move item",
"item.edit.move.title": "Ressource verschieben", "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.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": "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.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 <strong>upload additional files just dragging & dropping them everywhere in the page</strong>", // "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 <strong>upload additional files just dragging & dropping them everywhere in the page</strong>",
"submission.sections.upload.info": "Hier finden Sie alle Dateien, die aktuell zur Ressource gehören. Sie können die Metadaten und Zugriffsrechte bearbeiten oder <strong>weitere Dateien hinzufügen, indem Sie sie einfach irgenwo auf diese Seite ziehen.</strong>", "submission.sections.upload.info": "Hier finden Sie alle Dateien, die aktuell zur Ressource gehören. Sie können die Metadaten und Zugriffsrechte bearbeiten oder <strong>weitere Dateien hinzufügen, indem Sie sie einfach irgenwo auf diese Seite ziehen.</strong>",
// "submission.sections.upload.no-entry": "No", // "submission.sections.upload.no-entry": "No",
"submission.sections.upload.no-entry": "Kein Eintrag", "submission.sections.upload.no-entry": "Kein Eintrag",

View File

@@ -614,7 +614,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.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}}", "item.edit.move.head": "Move item: {{id}}",
@@ -628,7 +628,7 @@
"item.edit.move.search.placeholder": "Enter a search query to look for collections", "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", "item.edit.move.title": "Move item",
@@ -1592,7 +1592,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.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 <strong>upload additional files just dragging & dropping them everywhere in the page</strong>", "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 <strong>upload additional files just dragging & dropping them everywhere in the page</strong>",
"submission.sections.upload.no-entry": "No", "submission.sections.upload.no-entry": "No",

View File

@@ -591,7 +591,7 @@
"item.edit.move.cancel": "Cancelar", "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": "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.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.error": "Ha ocurrido un error cuando ha intentado mover el ítem",
// "item.edit.move.head": "Move item: {{id}}", // "item.edit.move.head": "Move item: {{id}}",
"item.edit.move.head": "Mover el ítem: {{id}}", "item.edit.move.head": "Mover el ítem: {{id}}",
@@ -605,7 +605,7 @@
"item.edit.move.processing": "Moviendo...", "item.edit.move.processing": "Moviendo...",
// "item.edit.move.search.placeholder": "Enter a search query to look for collections", // "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.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.success": "El ítem ha sido movido exitosamente",
// "item.edit.move.title": "Move item", // "item.edit.move.title": "Move item",
"item.edit.move.title": "Mover ítem", "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.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": "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.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 <strong>upload additional files just dragging & dropping them everywhere in the page</strong>", // "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 <strong>upload additional files just dragging & dropping them everywhere in the page</strong>",
"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 <strong> cargar los archivos adicionales simplemente arrastrándolos y soltándolos en cualquier parte de la página</strong>", "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 <strong> cargar los archivos adicionales simplemente arrastrándolos y soltándolos en cualquier parte de la página</strong>",
// "submission.sections.upload.no-entry": "No", // "submission.sections.upload.no-entry": "No",
"submission.sections.upload.no-entry": "No", "submission.sections.upload.no-entry": "No",

View File

@@ -1122,9 +1122,9 @@
// TODO New key - Add a translation // 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": "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 // 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}}", // "item.edit.move.head": "Move item: {{id}}",
// TODO New key - Add a translation // TODO New key - Add a translation
@@ -1150,9 +1150,9 @@
// TODO New key - Add a translation // TODO New key - Add a translation
"item.edit.move.search.placeholder": "Enter a search query to look for collections", "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 // 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", // "item.edit.move.title": "Move item",
// TODO New key - Add a translation // TODO New key - Add a translation
@@ -2908,9 +2908,9 @@
// TODO New key - Add a translation // 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": "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 <strong>upload additional files just dragging & dropping them everywhere in the page</strong>", // "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 <strong>upload additional files just dragging & dropping them everywhere in the page</strong>",
// TODO New key - Add a translation // 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 <strong>upload additional files just dragging & dropping them everywhere in the page</strong>", "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 <strong>upload additional files just dragging & dropping them everywhere in the page</strong>",
// "submission.sections.upload.no-entry": "No", // "submission.sections.upload.no-entry": "No",
// TODO New key - Add a translation // TODO New key - Add a translation

File diff suppressed because it is too large Load Diff

View File

@@ -22,7 +22,7 @@ import { SearchConfigurationServiceStub } from '../shared/testing/search-configu
import { SearchService } from '../+search-page/search-service/search.service'; import { SearchService } from '../+search-page/search-service/search.service';
import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service'; import { SearchConfigurationService } from '../+search-page/search-service/search-configuration.service';
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model'; import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
import { SearchSidebarService } from '../+search-page/search-sidebar/search-sidebar.service'; import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchFilterService } from '../+search-page/search-filters/search-filter/search-filter.service'; import { SearchFilterService } from '../+search-page/search-filters/search-filter/search-filter.service';
import { RoleDirective } from '../shared/roles/role.directive'; import { RoleDirective } from '../shared/roles/role.directive';
import { RoleService } from '../core/roles/role.service'; import { RoleService } from '../core/roles/role.service';
@@ -109,7 +109,7 @@ describe('MyDSpacePageComponent', () => {
}) })
}, },
{ {
provide: SearchSidebarService, provide: SidebarService,
useValue: sidebarService useValue: sidebarService
}, },
{ {

View File

@@ -17,7 +17,7 @@ import { pushInOut } from '../shared/animations/push';
import { HostWindowService } from '../shared/host-window.service'; import { HostWindowService } from '../shared/host-window.service';
import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model'; import { PaginatedSearchOptions } from '../+search-page/paginated-search-options.model';
import { SearchService } from '../+search-page/search-service/search.service'; import { SearchService } from '../+search-page/search-service/search.service';
import { SearchSidebarService } from '../+search-page/search-sidebar/search-sidebar.service'; import { SidebarService } from '../shared/sidebar/sidebar.service';
import { hasValue } from '../shared/empty.util'; import { hasValue } from '../shared/empty.util';
import { getSucceededRemoteData } from '../core/shared/operators'; import { getSucceededRemoteData } from '../core/shared/operators';
import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service'; import { MyDSpaceResponseParsingService } from '../core/data/mydspace-response-parsing.service';
@@ -102,7 +102,7 @@ export class MyDSpacePageComponent implements OnInit {
context$: Observable<Context>; context$: Observable<Context>;
constructor(private service: SearchService, constructor(private service: SearchService,
private sidebarService: SearchSidebarService, private sidebarService: SidebarService,
private windowService: HostWindowService, private windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) { @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: MyDSpaceConfigurationService) {
this.isXsOrSm$ = this.windowService.isXsOrSm(); this.isXsOrSm$ = this.windowService.isXsOrSm();

View File

@@ -1,6 +1,6 @@
import { HostWindowService } from '../shared/host-window.service'; import { HostWindowService } from '../shared/host-window.service';
import { SearchService } from './search-service/search.service'; import { SearchService } from './search-service/search.service';
import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchPageComponent } from './search-page.component'; import { SearchPageComponent } from './search-page.component';
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
import { pushInOut } from '../shared/animations/push'; import { pushInOut } from '../shared/animations/push';
@@ -36,7 +36,7 @@ export class ConfigurationSearchPageComponent extends SearchPageComponent implem
@Input() configuration: string; @Input() configuration: string;
constructor(protected service: SearchService, constructor(protected service: SearchService,
protected sidebarService: SearchSidebarService, protected sidebarService: SidebarService,
protected windowService: HostWindowService, protected windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
protected routeService: RouteService) { protected routeService: RouteService) {

View File

@@ -1,6 +1,6 @@
import { HostWindowService } from '../shared/host-window.service'; import { HostWindowService } from '../shared/host-window.service';
import { SearchService } from './search-service/search.service'; import { SearchService } from './search-service/search.service';
import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchPageComponent } from './search-page.component'; import { SearchPageComponent } from './search-page.component';
import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core'; import { ChangeDetectionStrategy, Component, Inject, Input, OnInit } from '@angular/core';
import { pushInOut } from '../shared/animations/push'; import { pushInOut } from '../shared/animations/push';
@@ -38,7 +38,7 @@ export class FilteredSearchPageComponent extends SearchPageComponent implements
@Input() fixedFilterQuery: string; @Input() fixedFilterQuery: string;
constructor(protected service: SearchService, constructor(protected service: SearchService,
protected sidebarService: SearchSidebarService, protected sidebarService: SidebarService,
protected windowService: HostWindowService, protected windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
protected routeService: RouteService) { protected routeService: RouteService) {

View File

@@ -1,7 +1,18 @@
<div class="facet-filter d-block mb-3 p-3" *ngIf="active$ | async"> <div class="facet-filter d-block mb-3 p-3" *ngIf="active$ | async">
<div (click)="toggle()" class="filter-name"><h5 class="d-inline-block mb-0">{{'search.filters.filter.' + filter.name + '.head'| translate}}</h5> <span class="filter-toggle fas float-right" <div (click)="toggle()" class="filter-name">
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'"></span></div> <h5 class="d-inline-block mb-0">
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'" (@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)" class="search-filter-wrapper" [ngClass]="{'closed' : closed}"> {{'search.filters.filter.' + filter.name + '.head'| translate}}
<ds-search-facet-filter-wrapper [filterConfig]="filter" [inPlaceSearch]="inPlaceSearch"></ds-search-facet-filter-wrapper> </h5>
</div> <span class="filter-toggle fas float-right"
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'">
</span>
</div>
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'"
(@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)"
class="search-filter-wrapper" [ngClass]="{'closed' : closed}">
<ds-search-facet-filter-wrapper
[filterConfig]="filter"
[inPlaceSearch]="inPlaceSearch">
</ds-search-facet-filter-wrapper>
</div>
</div> </div>

View File

@@ -138,7 +138,7 @@ describe('SearchFilterService', () => {
service.expand(mockFilterConfig.name); service.expand(mockFilterConfig.name);
}); });
it('SearchSidebarExpandAction should be dispatched to the store', () => { it('SidebarExpandAction should be dispatched to the store', () => {
expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterExpandAction(mockFilterConfig.name)); expect(store.dispatch).toHaveBeenCalledWith(new SearchFilterExpandAction(mockFilterConfig.name));
}); });
}); });

View File

@@ -1,43 +1,50 @@
<div class="container"> <div class="container" *ngIf="(isXsOrSm$ | async)">
<div class="search-page row"> <div class="row">
<ds-search-sidebar *ngIf="!(isXsOrSm$ | async)" class="col-{{sideBarWidth}} sidebar-md-sticky" <div class="col-12">
id="search-sidebar" <ng-template *ngTemplateOutlet="searchForm"></ng-template>
[resultCount]="(resultsRD$ | async)?.payload?.totalElements" [inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
<div class="col-12 col-md-{{12 - sideBarWidth}}">
<ds-search-form *ngIf="searchEnabled" id="search-form"
[query]="(searchOptions$ | async)?.query"
[scope]="(searchOptions$ | async)?.scope"
[currentUrl]="searchLink"
[scopes]="(scopeListRD$ | async)"
[inPlaceSearch]="inPlaceSearch">
</ds-search-form>
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
<div class="row">
<div id="search-body"
class="row-offcanvas row-offcanvas-left"
[@pushInOut]="(isSidebarCollapsed$ | async) ? 'collapsed' : 'expanded'">
<ds-search-sidebar *ngIf="(isXsOrSm$ | async)" class="col-12"
id="search-sidebar-sm"
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
(toggleSidebar)="closeSidebar()"
[ngClass]="{'active': !(isSidebarCollapsed$ | async)}">
</ds-search-sidebar>
<div id="search-content" class="col-12">
<div class="d-block d-md-none search-controls clearfix">
<ds-view-mode-switch [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
<button (click)="openSidebar()" aria-controls="#search-body"
class="btn btn-outline-primary float-right open-sidebar"><i
class="fas fa-sliders"></i> {{"search.sidebar.open"
| translate}}
</button>
</div>
<ds-search-results [searchResults]="resultsRD$ | async"
[searchConfig]="searchOptions$ | async"
[configuration]="configuration$ | async"
[disableHeader]="!searchEnabled"></ds-search-results>
</div>
</div>
</div>
</div>
</div> </div>
</div>
</div> </div>
<ds-page-with-sidebar [id]="'search-page'" [sidebarContent]="sidebarContent">
<div class="row">
<div class="col-12" *ngIf="!(isXsOrSm$ | async)">
<ng-template *ngTemplateOutlet="searchForm"></ng-template>
</div>
<div id="search-content" class="col-12">
<div class="d-block d-md-none search-controls clearfix">
<ds-view-mode-switch [inPlaceSearch]="inPlaceSearch"></ds-view-mode-switch>
<button (click)="openSidebar()" aria-controls="#search-body"
class="btn btn-outline-primary float-right open-sidebar"><i
class="fas fa-sliders"></i> {{"search.sidebar.open"
| translate}}
</button>
</div>
<ds-search-results [searchResults]="resultsRD$ | async"
[searchConfig]="searchOptions$ | async"
[configuration]="configuration$ | async"
[disableHeader]="!searchEnabled"></ds-search-results>
</div>
</div>
</ds-page-with-sidebar>
<ng-template #sidebarContent>
<ds-search-sidebar id="search-sidebar" *ngIf="!(isXsOrSm$ | async)"
[resultCount]="(resultsRD$ | async)?.payload?.totalElements"
[inPlaceSearch]="inPlaceSearch"></ds-search-sidebar>
<ds-search-sidebar id="search-sidebar-sm" *ngIf="(isXsOrSm$ | async)"
[resultCount]="(resultsRD$ | async)?.payload.totalElements"
(toggleSidebar)="closeSidebar()"
>
</ds-search-sidebar>
</ng-template>
<ng-template #searchForm>
<ds-search-form *ngIf="searchEnabled" id="search-form"
[query]="(searchOptions$ | async)?.query"
[scope]="(searchOptions$ | async)?.scope"
[currentUrl]="searchLink"
[scopes]="(scopeListRD$ | async)"
[inPlaceSearch]="inPlaceSearch">
</ds-search-form>
<ds-search-labels *ngIf="searchEnabled" [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
</ng-template>

View File

@@ -1,52 +1,10 @@
@include media-breakpoint-down(md) { @include media-breakpoint-down(md) {
.container { .container {
width: 100%; width: 100%;
max-width: none; max-width: none;
} }
} }
/deep/ .search-controls { /deep/ .search-controls {
margin-bottom: $spacer; margin-bottom: $spacer;
} }
#search-body {
&.row-offcanvas {
width: 100%;
}
@include media-breakpoint-down(sm) {
position: relative;
&.row-offcanvas {
position: relative;
}
&.row-offcanvas-right #search-sidebar-sm {
right: -100%;
}
&.row-offcanvas-left #search-sidebar-sm {
left: -100%;
}
#search-sidebar-sm {
position: absolute;
top: 0;
width: 100%;
}
}
}
@include media-breakpoint-up(md) {
.sidebar-md-sticky {
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;
}
}

View File

@@ -16,7 +16,7 @@ import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap'; import { NgbCollapseModule } from '@ng-bootstrap/ng-bootstrap';
import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchFilterService } from './search-filters/search-filter/search-filter.service'; import { SearchFilterService } from './search-filters/search-filter/search-filter.service';
import { SearchConfigurationService } from './search-service/search-configuration.service'; import { SearchConfigurationService } from './search-service/search-configuration.service';
import { RemoteData } from '../core/data/remote-data'; import { RemoteData } from '../core/data/remote-data';
@@ -115,7 +115,7 @@ export function configureSearchComponentTestingModule(compType) {
}) })
}, },
{ {
provide: SearchSidebarService, provide: SidebarService,
useValue: sidebarService useValue: sidebarService
}, },
{ {
@@ -191,34 +191,4 @@ describe('SearchPageComponent', () => {
}); });
}); });
describe('when sidebarCollapsed is true in mobile view', () => {
let menu: HTMLElement;
beforeEach(() => {
menu = fixture.debugElement.query(By.css('#search-sidebar-sm')).nativeElement;
(comp as any).isSidebarCollapsed$ = observableOf(true);
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('#search-sidebar-sm')).nativeElement;
(comp as any).isSidebarCollapsed$ = observableOf(false);
fixture.detectChanges();
});
it('should open the menu', () => {
expect(menu.classList).toContain('active');
});
});
}); });

View File

@@ -9,7 +9,7 @@ import { HostWindowService } from '../shared/host-window.service';
import { PaginatedSearchOptions } from './paginated-search-options.model'; import { PaginatedSearchOptions } from './paginated-search-options.model';
import { SearchResult } from './search-result.model'; import { SearchResult } from './search-result.model';
import { SearchService } from './search-service/search.service'; import { SearchService } from './search-service/search.service';
import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; import { SidebarService } from '../shared/sidebar/sidebar.service';
import { hasValue, isNotEmpty } from '../shared/empty.util'; import { hasValue, isNotEmpty } from '../shared/empty.util';
import { SearchConfigurationService } from './search-service/search-configuration.service'; import { SearchConfigurationService } from './search-service/search-configuration.service';
import { getSucceededRemoteData } from '../core/shared/operators'; import { getSucceededRemoteData } from '../core/shared/operators';
@@ -102,7 +102,7 @@ export class SearchPageComponent implements OnInit {
isSidebarCollapsed$: Observable<boolean>; isSidebarCollapsed$: Observable<boolean>;
constructor(protected service: SearchService, constructor(protected service: SearchService,
protected sidebarService: SearchSidebarService, protected sidebarService: SidebarService,
protected windowService: HostWindowService, protected windowService: HostWindowService,
@Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService, @Inject(SEARCH_CONFIG_SERVICE) public searchConfigService: SearchConfigurationService,
protected routeService: RouteService) { protected routeService: RouteService) {

View File

@@ -8,8 +8,8 @@ import { SearchResultsComponent } from './search-results/search-results.componen
import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component' import { CommunitySearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'
import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component'; import { CollectionSearchResultGridElementComponent } from '../shared/object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component';
import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component'; import { SearchSidebarComponent } from './search-sidebar/search-sidebar.component';
import { SearchSidebarService } from './search-sidebar/search-sidebar.service'; import { SidebarService } from '../shared/sidebar/sidebar.service';
import { SearchSidebarEffects } from './search-sidebar/search-sidebar.effects'; import { SidebarEffects } from '../shared/sidebar/sidebar-effects.service';
import { SearchSettingsComponent } from './search-settings/search-settings.component'; import { SearchSettingsComponent } from './search-settings/search-settings.component';
import { EffectsModule } from '@ngrx/effects'; import { EffectsModule } from '@ngrx/effects';
import { SearchFiltersComponent } from './search-filters/search-filters.component'; import { SearchFiltersComponent } from './search-filters/search-filters.component';
@@ -33,9 +33,10 @@ import { SearchLabelComponent } from './search-labels/search-label/search-label.
import { ConfigurationSearchPageComponent } from './configuration-search-page.component'; import { ConfigurationSearchPageComponent } from './configuration-search-page.component';
import { ConfigurationSearchPageGuard } from './configuration-search-page.guard'; import { ConfigurationSearchPageGuard } from './configuration-search-page.guard';
import { FilteredSearchPageComponent } from './filtered-search-page.component'; import { FilteredSearchPageComponent } from './filtered-search-page.component';
import { SidebarFilterService } from '../shared/sidebar/filter/sidebar-filter.service';
const effects = [ const effects = [
SearchSidebarEffects SidebarEffects
]; ];
const components = [ const components = [
@@ -69,11 +70,12 @@ const components = [
CommonModule, CommonModule,
SharedModule, SharedModule,
EffectsModule.forFeature(effects), EffectsModule.forFeature(effects),
CoreModule.forRoot() CoreModule.forRoot(),
], ],
declarations: components, declarations: components,
providers: [ providers: [
SearchSidebarService, SidebarService,
SidebarFilterService,
SearchFilterService, SearchFilterService,
SearchFixedFilterService, SearchFixedFilterService,
ConfigurationSearchPageGuard, ConfigurationSearchPageGuard,

View File

@@ -1,24 +1,32 @@
<ng-container *ngVar="(searchOptions$ | async) as config"> <ng-container *ngVar="(searchOptions$ | async) as config">
<h3>{{ 'search.sidebar.settings.title' | translate}}</h3> <h3>{{ 'search.sidebar.settings.title' | translate}}</h3>
<div *ngIf="config?.sort" class="setting-option result-order-settings mb-3 p-3">
<h5>{{ 'search.sidebar.settings.sort-by' | translate}}</h5>
<select class="form-control" (change)="reloadOrder($event)">
<option *ngFor="let sortOption of searchOptionPossibilities"
[value]="sortOption.field + ',' + sortOption.direction.toString()"
[selected]="sortOption.field === config?.sort.field && sortOption.direction === (config?.sort.direction)? 'selected': null">
{{'sorting.' + sortOption.field + '.' + sortOption.direction | translate}}
</option>
</select>
</div>
<div class="setting-option page-size-settings mb-3 p-3"> <div class="result-order-settings">
<h5>{{ 'search.sidebar.settings.rpp' | translate}}</h5> <ds-sidebar-dropdown
<select class="form-control" (change)="reloadRPP($event)"> *ngIf="config?.sort"
<option *ngFor="let pageSizeOption of config?.pagination.pageSizeOptions" [id]="'search-sidebar-sort'"
[value]="pageSizeOption" [label]="'search.sidebar.settings.sort-by'"
[selected]="pageSizeOption === +config?.pagination.pageSize ? 'selected': null"> (change)="reloadOrder($event)"
{{pageSizeOption}} >
</option> <option *ngFor="let sortOption of searchOptionPossibilities"
</select> [value]="sortOption.field + ',' + sortOption.direction.toString()"
</div> [selected]="sortOption.field === config?.sort.field && sortOption.direction === (config?.sort.direction)? 'selected': null">
{{'sorting.' + sortOption.field + '.' + sortOption.direction | translate}}
</option>
</ds-sidebar-dropdown>
</div>
<div class="page-size-settings">
<ds-sidebar-dropdown
[id]="'search-sidebar-rpp'"
[label]="'search.sidebar.settings.rpp'"
(change)="reloadRPP($event)"
>
<option *ngFor="let pageSizeOption of config?.pagination.pageSizeOptions"
[value]="pageSizeOption"
[selected]="pageSizeOption === +config?.pagination.pageSize ? 'selected': null">
{{pageSizeOption}}
</option>
</ds-sidebar-dropdown>
</div>
</ng-container> </ng-container>

View File

@@ -7,84 +7,92 @@ import { SortDirection, SortOptions } from '../../core/cache/models/sort-options
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { RouterTestingModule } from '@angular/router/testing'; import { RouterTestingModule } from '@angular/router/testing';
import { ActivatedRoute } from '@angular/router'; import { ActivatedRoute } from '@angular/router';
import { SearchSidebarService } from '../search-sidebar/search-sidebar.service'; import { SidebarService } from '../../shared/sidebar/sidebar.service';
import { NO_ERRORS_SCHEMA } from '@angular/core'; import { NO_ERRORS_SCHEMA } from '@angular/core';
import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe'; import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
import { By } from '@angular/platform-browser'; import { By } from '@angular/platform-browser';
import { SearchFilterService } from '../search-filters/search-filter/search-filter.service'; import { SearchFilterService } from '../search-filters/search-filter/search-filter.service';
import { hot } from 'jasmine-marbles';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { first } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component'; import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
describe('SearchSettingsComponent', () => { describe('SearchSettingsComponent', () => {
let comp: SearchSettingsComponent; let comp:SearchSettingsComponent;
let fixture: ComponentFixture<SearchSettingsComponent>; let fixture:ComponentFixture<SearchSettingsComponent>;
let searchServiceObject: SearchService; let searchServiceObject:SearchService;
const pagination: PaginationComponentOptions = new PaginationComponentOptions(); let pagination:PaginationComponentOptions;
pagination.id = 'search-results-pagination'; let sort:SortOptions;
pagination.currentPage = 1; let mockResults;
pagination.pageSize = 10; let searchServiceStub;
const sort: SortOptions = new SortOptions('score', SortDirection.DESC);
const mockResults = ['test', 'data'];
const searchServiceStub = {
searchOptions: { pagination: pagination, sort: sort },
search: () => mockResults
};
const queryParam = 'test query'; let queryParam;
const scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f'; let scopeParam;
const paginatedSearchOptions = { let paginatedSearchOptions;
query: queryParam,
scope: scopeParam,
pagination,
sort
};
const activatedRouteStub = { let activatedRouteStub;
queryParams: observableOf({
query: queryParam,
scope: scopeParam
})
};
const sidebarService = { let sidebarService;
isCollapsed: observableOf(true),
collapse: () => this.isCollapsed = observableOf(true),
expand: () => this.isCollapsed = observableOf(false)
};
beforeEach(async(() => { beforeEach(async(() => {
pagination = new PaginationComponentOptions();
pagination.id = 'search-results-pagination';
pagination.currentPage = 1;
pagination.pageSize = 10;
sort = new SortOptions('score', SortDirection.DESC);
mockResults = ['test', 'data'];
searchServiceStub = {
searchOptions: {pagination: pagination, sort: sort},
search: () => mockResults,
};
queryParam = 'test query';
scopeParam = '7669c72a-3f2a-451f-a3b9-9210e7a4c02f';
paginatedSearchOptions = {
query: queryParam,
scope: scopeParam,
pagination,
sort,
};
activatedRouteStub = {
queryParams: observableOf({
query: queryParam,
scope: scopeParam,
}),
};
sidebarService = {
isCollapsed: observableOf(true),
collapse: () => this.isCollapsed = observableOf(true),
expand: () => this.isCollapsed = observableOf(false),
};
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])], imports: [TranslateModule.forRoot(), RouterTestingModule.withRoutes([])],
declarations: [SearchSettingsComponent, EnumKeysPipe, VarDirective], declarations: [SearchSettingsComponent, EnumKeysPipe, VarDirective],
providers: [ providers: [
{ provide: SearchService, useValue: searchServiceStub }, {provide: SearchService, useValue: searchServiceStub},
{ provide: ActivatedRoute, useValue: activatedRouteStub }, {provide: ActivatedRoute, useValue: activatedRouteStub},
{ {
provide: SearchSidebarService, provide: SidebarService,
useValue: sidebarService useValue: sidebarService,
}, },
{ {
provide: SearchFilterService, provide: SearchFilterService,
useValue: {} useValue: {},
}, },
{ {
provide: SEARCH_CONFIG_SERVICE, provide: SEARCH_CONFIG_SERVICE,
useValue: { useValue: {
paginatedSearchOptions: hot('a', { paginatedSearchOptions: observableOf(paginatedSearchOptions),
a: paginatedSearchOptions getCurrentScope: observableOf('test-id'),
}), },
getCurrentScope: hot('a', {
a: 'test-id'
}),
}
}, },
], ],
schemas: [NO_ERRORS_SCHEMA] schemas: [NO_ERRORS_SCHEMA],
}).compileComponents(); }).compileComponents();
})); }));
@@ -101,42 +109,46 @@ describe('SearchSettingsComponent', () => {
}); });
it('it should show the order settings with the respective selectable options', () => { it('it should show the order settings with the respective selectable options', (done) => {
(comp as any).searchOptions$.pipe(first()).subscribe((options) => { (comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
fixture.detectChanges(); fixture.detectChanges();
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings')); const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
expect(orderSetting).toBeDefined(); expect(orderSetting).toBeDefined();
const childElements = orderSetting.query(By.css('.form-control')).children; const childElements = orderSetting.queryAll(By.css('option'));
expect(childElements.length).toEqual(comp.searchOptionPossibilities.length); expect(childElements.length).toEqual(comp.searchOptionPossibilities.length);
done();
}); });
}); });
it('it should show the size settings with the respective selectable options', () => { it('it should show the size settings with the respective selectable options', (done) => {
(comp as any).searchOptions$.pipe(first()).subscribe((options) => { (comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
fixture.detectChanges(); fixture.detectChanges();
const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings')); const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
expect(pageSizeSetting).toBeDefined(); expect(pageSizeSetting).toBeDefined();
const childElements = pageSizeSetting.query(By.css('.form-control')).children; const childElements = pageSizeSetting.queryAll(By.css('option'));
expect(childElements.length).toEqual(options.pagination.pageSizeOptions.length); expect(childElements.length).toEqual(options.pagination.pageSizeOptions.length);
} done();
) },
);
}); });
it('should have the proper order value selected by default', () => { it('should have the proper order value selected by default', (done) => {
(comp as any).searchOptions$.pipe(first()).subscribe((options) => { (comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
fixture.detectChanges(); fixture.detectChanges();
const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings')); const orderSetting = fixture.debugElement.query(By.css('div.result-order-settings'));
const childElementToBeSelected = orderSetting.query(By.css('.form-control option[value="0"][selected="selected"]')); const childElementToBeSelected = orderSetting.query(By.css('option[value="0"][selected="selected"]'));
expect(childElementToBeSelected).toBeDefined(); expect(childElementToBeSelected).toBeDefined();
done();
}); });
}); });
it('should have the proper rpp value selected by default', () => { it('should have the proper rpp value selected by default', (done) => {
(comp as any).searchOptions$.pipe(first()).subscribe((options) => { (comp as any).searchOptions$.pipe(take(1)).subscribe((options) => {
fixture.detectChanges(); fixture.detectChanges();
const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings')); const pageSizeSetting = fixture.debugElement.query(By.css('div.page-size-settings'));
const childElementToBeSelected = pageSizeSetting.query(By.css('.form-control option[value="10"][selected="selected"]')); const childElementToBeSelected = pageSizeSetting.query(By.css('option[value="10"][selected="selected"]'));
expect(childElementToBeSelected).toBeDefined(); expect(childElementToBeSelected).toBeDefined();
done();
}); });
}); });

View File

@@ -1,47 +0,0 @@
import { SearchSidebarAction, SearchSidebarActionTypes } from './search-sidebar.actions';
/**
* Interface that represents the state of the sidebar
*/
export interface SearchSidebarState {
sidebarCollapsed: boolean;
}
const initialState: SearchSidebarState = {
sidebarCollapsed: true
};
/**
* Performs a search sidebar action on the current state
* @param {SearchSidebarState} state The state before the action is performed
* @param {SearchSidebarAction} action The action that should be performed
* @returns {SearchSidebarState} The state after the action is performed
*/
export function sidebarReducer(state = initialState, action: SearchSidebarAction): SearchSidebarState {
switch (action.type) {
case SearchSidebarActionTypes.COLLAPSE: {
return Object.assign({}, state, {
sidebarCollapsed: true
});
}
case SearchSidebarActionTypes.EXPAND: {
return Object.assign({}, state, {
sidebarCollapsed: false
});
}
case SearchSidebarActionTypes.TOGGLE: {
return Object.assign({}, state, {
sidebarCollapsed: !state.sidebarCollapsed
});
}
default: {
return state;
}
}
}

View File

@@ -1,11 +1,15 @@
import { ActionReducerMap, createSelector, MemoizedSelector } from '@ngrx/store'; import { ActionReducerMap, createSelector, MemoizedSelector, State } from '@ngrx/store';
import * as fromRouter from '@ngrx/router-store'; import * as fromRouter from '@ngrx/router-store';
import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer'; import { hostWindowReducer, HostWindowState } from './shared/host-window.reducer';
import { formReducer, FormState } from './shared/form/form.reducer'; import { formReducer, FormState } from './shared/form/form.reducer';
import { import {
SearchSidebarState, SidebarState,
sidebarReducer sidebarReducer
} from './+search-page/search-sidebar/search-sidebar.reducer'; } from './shared/sidebar/sidebar.reducer';
import {
SidebarFilterState,
sidebarFilterReducer, SidebarFiltersState
} from './shared/sidebar/filter/sidebar-filter.reducer';
import { import {
filterReducer, filterReducer,
SearchFiltersState SearchFiltersState
@@ -37,7 +41,8 @@ export interface AppState {
metadataRegistry: MetadataRegistryState; metadataRegistry: MetadataRegistryState;
bitstreamFormats: BitstreamFormatRegistryState; bitstreamFormats: BitstreamFormatRegistryState;
notifications: NotificationsState; notifications: NotificationsState;
searchSidebar: SearchSidebarState; sidebar: SidebarState;
sidebarFilter: SidebarFiltersState;
searchFilter: SearchFiltersState; searchFilter: SearchFiltersState;
truncatable: TruncatablesState; truncatable: TruncatablesState;
cssVariables: CSSVariablesState; cssVariables: CSSVariablesState;
@@ -53,7 +58,8 @@ export const appReducers: ActionReducerMap<AppState> = {
metadataRegistry: metadataRegistryReducer, metadataRegistry: metadataRegistryReducer,
bitstreamFormats: bitstreamFormatReducer, bitstreamFormats: bitstreamFormatReducer,
notifications: notificationsReducer, notifications: notificationsReducer,
searchSidebar: sidebarReducer, sidebar: sidebarReducer,
sidebarFilter: sidebarFilterReducer,
searchFilter: filterReducer, searchFilter: filterReducer,
truncatable: truncatableReducer, truncatable: truncatableReducer,
cssVariables: cssVariablesReducer, cssVariables: cssVariablesReducer,

View File

@@ -145,6 +145,10 @@ import { ListableObjectDirective } from './object-collection/shared/listable-obj
import { CommunitySearchResultGridElementComponent } from './object-grid/search-result-grid-element/community-search-result/community-search-result-grid-element.component'; 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 { CollectionSearchResultGridElementComponent } from './object-grid/search-result-grid-element/collection-search-result/collection-search-result-grid-element.component';
import { ItemMetadataRepresentationListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.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';
const MODULES = [ const MODULES = [
// Do NOT include UniversalModule, HttpModule, or JsonpModule here // Do NOT include UniversalModule, HttpModule, or JsonpModule here
@@ -228,6 +232,10 @@ const COMPONENTS = [
ObjectCollectionComponent, ObjectCollectionComponent,
PaginationComponent, PaginationComponent,
SearchFormComponent, SearchFormComponent,
PageWithSidebarComponent,
SidebarDropdownComponent,
SidebarFilterComponent,
SidebarFilterSelectedOptionComponent,
ThumbnailComponent, ThumbnailComponent,
GridThumbnailComponent, GridThumbnailComponent,
UploaderComponent, UploaderComponent,

View File

@@ -0,0 +1,6 @@
<a class="d-flex flex-row" (click)="click.emit($event)">
<label>
<input type="checkbox" [checked]="true" class="my-1 align-self-stretch"/>
<span class="filter-value pl-1 text-capitalize">{{label}}</span>
</label>
</a>

View File

@@ -0,0 +1,11 @@
a {
color: $body-color;
&:hover, &focus {
text-decoration: none;
}
span.badge {
vertical-align: text-top;
}
}

View File

@@ -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<any> = new EventEmitter<any>();
}

View File

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

View File

@@ -0,0 +1,26 @@
<div class="facet-filter d-block mb-3 p-3">
<div (click)="toggle()" class="filter-name">
<h5 class="d-inline-block mb-0">
{{ label | translate }}
</h5>
<span class="filter-toggle fas float-right"
[ngClass]="(collapsed$ | async) ? 'fa-plus' : 'fa-minus'">
</span>
</div>
<div [@slide]="(collapsed$ | async) ? 'collapsed' : 'expanded'"
(@slide.start)="startSlide($event)" (@slide.done)="finishSlide($event)"
class="sidebar-filter-wrapper" [ngClass]="{'closed' : closed}">
<div>
<div class="filters py-2">
<ng-template *ngIf="!singleValue">
<ds-sidebar-filter-selected-option
*ngFor="let value of (selectedValues | async)"
[label]="value"
(click)="removeValue.emit(value)">
</ds-sidebar-filter-selected-option>
</ng-template>
</div>
<ng-content></ng-content>
</div>
</div>
</div>

View File

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

View File

@@ -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<string[]>;
@Output() removeValue:EventEmitter<any> = new EventEmitter<any>();
/**
* 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<boolean>;
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<boolean>} Emits true when the current state of the filter is collapsed, false when it's expanded
*/
private isCollapsed():Observable<boolean> {
return this.filterService.isCollapsed(this.name);
}
}

View File

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

View File

@@ -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<SidebarFilterState>) {
}
/**
* 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<boolean>} Emits the current collapsed state of the given filter, if it's unavailable, return false
*/
isCollapsed(filterName:string):Observable<boolean> {
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<SidebarFiltersState, SidebarFilterState> {
return keySelector<SidebarFilterState>(name);
}
export function keySelector<T>(key:string):MemoizedSelector<SidebarFiltersState, T> {
return createSelector(filterStateSelector, (state:SidebarFilterState) => {
if (hasValue(state)) {
return state[key];
} else {
return undefined;
}
});
}

View File

@@ -0,0 +1,14 @@
<div class="container">
<div class="row">
<div class="row-with-sidebar row-offcanvas row-offcanvas-left"
[@pushInOut]="(isSidebarCollapsed$ | async) ? 'collapsed' : 'expanded'">
<div id="{{id}}-sidebar-content"
class="col-12 col-md-{{sideBarWidth}} sidebar-content {{sidebarClasses | async}}">
<ng-container *ngTemplateOutlet="sidebarContent"></ng-container>
</div>
<div class="col-12 col-md-{{12 - sideBarWidth}}">
<ng-content></ng-content>
</div>
</div>
</div>
</div>

View File

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

View File

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

View File

@@ -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<any>;
/**
* Emits true if were on a small screen
*/
isXsOrSm$:Observable<boolean>;
/**
* The width of the sidebar (bootstrap columns)
*/
@Input()
sideBarWidth = 3;
/**
* Observable for whether or not the sidebar is currently collapsed
*/
isSidebarCollapsed$:Observable<boolean>;
sidebarClasses:Observable<string>;
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<boolean>} emits true if the sidebar is currently collapsed, false if it is expanded
*/
private isSidebarCollapsed():Observable<boolean> {
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();
}
}

View File

@@ -3,24 +3,24 @@ import { Observable } from 'rxjs';
import { provideMockActions } from '@ngrx/effects/testing'; import { provideMockActions } from '@ngrx/effects/testing';
import { cold, hot } from 'jasmine-marbles'; import { cold, hot } from 'jasmine-marbles';
import * as fromRouter from '@ngrx/router-store'; import * as fromRouter from '@ngrx/router-store';
import { SearchSidebarCollapseAction } from './search-sidebar.actions'; import { SidebarCollapseAction } from './sidebar.actions';
import { SearchSidebarEffects } from './search-sidebar.effects'; import { SidebarEffects } from './sidebar-effects.service';
describe('SearchSidebarEffects', () => { describe('SidebarEffects', () => {
let sidebarEffects: SearchSidebarEffects; let sidebarEffects: SidebarEffects;
let actions: Observable<any>; let actions: Observable<any>;
const dummyURL = 'http://f4fb15e2-1bd3-4e63-8d0d-486ad8bc714a'; const dummyURL = 'http://f4fb15e2-1bd3-4e63-8d0d-486ad8bc714a';
beforeEach(() => { beforeEach(() => {
TestBed.configureTestingModule({ TestBed.configureTestingModule({
providers: [ providers: [
SearchSidebarEffects, SidebarEffects,
provideMockActions(() => actions), provideMockActions(() => actions),
// other providers // other providers
], ],
}); });
sidebarEffects = TestBed.get(SearchSidebarEffects); sidebarEffects = TestBed.get(SidebarEffects);
}); });
describe('routeChange$', () => { describe('routeChange$', () => {
@@ -28,7 +28,7 @@ describe('SearchSidebarEffects', () => {
it('should return a COLLAPSE action in response to an UPDATE_LOCATION action to a new route', () => { it('should return a COLLAPSE action in response to an UPDATE_LOCATION action to a new route', () => {
actions = hot('--a-', { a: { type: fromRouter.ROUTER_NAVIGATION, payload: {routerState: {url: dummyURL}} } }); actions = hot('--a-', { a: { type: fromRouter.ROUTER_NAVIGATION, payload: {routerState: {url: dummyURL}} } });
const expected = cold('--b-', { b: new SearchSidebarCollapseAction() }); const expected = cold('--b-', { b: new SidebarCollapseAction() });
expect(sidebarEffects.routeChange$).toBeObservable(expected); expect(sidebarEffects.routeChange$).toBeObservable(expected);
}); });

View File

@@ -0,0 +1,6 @@
<div class="setting-option mb-3 p-3">
<h5><label for="{{id}}">{{label | translate}}</label></h5>
<select id="{{id}}" class="form-control" (change)="change.emit($event)">
<ng-content></ng-content>
</select>
</div>

View File

@@ -0,0 +1,3 @@
.setting-option {
border: 1px solid map-get($theme-colors, light);
}

View File

@@ -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<any> = new EventEmitter<number>();
}

View File

@@ -3,14 +3,14 @@ import { Injectable } from '@angular/core';
import { Effect, Actions, ofType } from '@ngrx/effects' import { Effect, Actions, ofType } from '@ngrx/effects'
import * as fromRouter from '@ngrx/router-store'; 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'; import { URLBaser } from '../../core/url-baser/url-baser';
/** /**
* Makes sure that if the user navigates to another route, the sidebar is collapsed * Makes sure that if the user navigates to another route, the sidebar is collapsed
*/ */
@Injectable() @Injectable()
export class SearchSidebarEffects { export class SidebarEffects {
private previousPath: string; private previousPath: string;
@Effect() routeChange$ = this.actions$ @Effect() routeChange$ = this.actions$
.pipe( .pipe(
@@ -19,7 +19,7 @@ export class SearchSidebarEffects {
tap((action) => { tap((action) => {
this.previousPath = this.getBaseUrl(action) this.previousPath = this.getBaseUrl(action)
}), }),
map(() => new SearchSidebarCollapseAction()) map(() => new SidebarCollapseAction())
); );
constructor(private actions$: Actions) { constructor(private actions$: Actions) {

View File

@@ -1,6 +1,6 @@
import { Action } from '@ngrx/store'; 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 * 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 * literal types and runs a simple check to guarantee all
* action types in the application are unique. * action types in the application are unique.
*/ */
export const SearchSidebarActionTypes = { export const SidebarActionTypes = {
COLLAPSE: type('dspace/search-sidebar/COLLAPSE'), COLLAPSE: type('dspace/sidebar/COLLAPSE'),
EXPAND: type('dspace/search-sidebar/EXPAND'), EXPAND: type('dspace/sidebar/EXPAND'),
TOGGLE: type('dspace/search-sidebar/TOGGLE') TOGGLE: type('dspace/sidebar/TOGGLE')
}; };
/* tslint:disable:max-classes-per-file */ /* tslint:disable:max-classes-per-file */
/** /**
* Used to collapse the sidebar * Used to collapse the sidebar
*/ */
export class SearchSidebarCollapseAction implements Action { export class SidebarCollapseAction implements Action {
type = SearchSidebarActionTypes.COLLAPSE; type = SidebarActionTypes.COLLAPSE;
} }
/** /**
* Used to expand the sidebar * Used to expand the sidebar
*/ */
export class SearchSidebarExpandAction implements Action { export class SidebarExpandAction implements Action {
type = SearchSidebarActionTypes.EXPAND; type = SidebarActionTypes.EXPAND;
} }
/** /**
* Used to collapse the sidebar when it's expanded and expand it when it's collapsed * Used to collapse the sidebar when it's expanded and expand it when it's collapsed
*/ */
export class SearchSidebarToggleAction implements Action { export class SidebarToggleAction implements Action {
type = SearchSidebarActionTypes.TOGGLE; type = SidebarActionTypes.TOGGLE;
} }
/* tslint:enable:max-classes-per-file */ /* 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 * Export a type alias of all actions in this action group
* so that reducers can easily compose action types * so that reducers can easily compose action types
*/ */
export type SearchSidebarAction export type SidebarAction
= SearchSidebarCollapseAction = SidebarCollapseAction
| SearchSidebarExpandAction | SidebarExpandAction
| SearchSidebarToggleAction | SidebarToggleAction

View File

@@ -1,12 +1,12 @@
import * as deepFreeze from 'deep-freeze'; import * as deepFreeze from 'deep-freeze';
import { sidebarReducer } from './search-sidebar.reducer'; import { sidebarReducer } from './sidebar.reducer';
import { import {
SearchSidebarCollapseAction, SearchSidebarExpandAction, SidebarCollapseAction, SidebarExpandAction,
SearchSidebarToggleAction SidebarToggleAction
} from './search-sidebar.actions'; } from './sidebar.actions';
class NullAction extends SearchSidebarCollapseAction { class NullAction extends SidebarCollapseAction {
type = null; type = null;
constructor() { constructor() {
@@ -34,7 +34,7 @@ describe('sidebarReducer', () => {
it('should set sidebarCollapsed to true in response to the COLLAPSE action', () => { it('should set sidebarCollapsed to true in response to the COLLAPSE action', () => {
const state = { sidebarCollapsed: false }; const state = { sidebarCollapsed: false };
const action = new SearchSidebarCollapseAction(); const action = new SidebarCollapseAction();
const newState = sidebarReducer(state, action); const newState = sidebarReducer(state, action);
expect(newState.sidebarCollapsed).toEqual(true); expect(newState.sidebarCollapsed).toEqual(true);
@@ -44,7 +44,7 @@ describe('sidebarReducer', () => {
const state = { sidebarCollapsed: false }; const state = { sidebarCollapsed: false };
deepFreeze([state]); deepFreeze([state]);
const action = new SearchSidebarCollapseAction(); const action = new SidebarCollapseAction();
sidebarReducer(state, action); sidebarReducer(state, action);
// no expect required, deepFreeze will ensure an exception is thrown if the state // 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', () => { it('should set sidebarCollapsed to false in response to the EXPAND action', () => {
const state = { sidebarCollapsed: true }; const state = { sidebarCollapsed: true };
const action = new SearchSidebarExpandAction(); const action = new SidebarExpandAction();
const newState = sidebarReducer(state, action); const newState = sidebarReducer(state, action);
expect(newState.sidebarCollapsed).toEqual(false); expect(newState.sidebarCollapsed).toEqual(false);
@@ -63,13 +63,13 @@ describe('sidebarReducer', () => {
const state = { sidebarCollapsed: true }; const state = { sidebarCollapsed: true };
deepFreeze([state]); deepFreeze([state]);
const action = new SearchSidebarExpandAction(); const action = new SidebarExpandAction();
sidebarReducer(state, action); sidebarReducer(state, action);
}); });
it('should flip the value of sidebarCollapsed in response to the TOGGLE action', () => { it('should flip the value of sidebarCollapsed in response to the TOGGLE action', () => {
const state1 = { sidebarCollapsed: true }; const state1 = { sidebarCollapsed: true };
const action = new SearchSidebarToggleAction(); const action = new SidebarToggleAction();
const state2 = sidebarReducer(state1, action); const state2 = sidebarReducer(state1, action);
const state3 = sidebarReducer(state2, action); const state3 = sidebarReducer(state2, action);
@@ -82,7 +82,7 @@ describe('sidebarReducer', () => {
const state = { sidebarCollapsed: true }; const state = { sidebarCollapsed: true };
deepFreeze([state]); deepFreeze([state]);
const action = new SearchSidebarToggleAction(); const action = new SidebarToggleAction();
sidebarReducer(state, action); sidebarReducer(state, action);
}); });

View File

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

View File

@@ -1,13 +1,13 @@
import { Store } from '@ngrx/store'; import { Store } from '@ngrx/store';
import { SearchSidebarService } from './search-sidebar.service'; import { SidebarService } from './sidebar.service';
import { AppState } from '../../app.reducer'; import { AppState } from '../../app.reducer';
import { async, TestBed } from '@angular/core/testing'; import { async, TestBed } from '@angular/core/testing';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
import { SearchSidebarCollapseAction, SearchSidebarExpandAction } from './search-sidebar.actions'; import { SidebarCollapseAction, SidebarExpandAction } from './sidebar.actions';
import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowService } from '../host-window.service';
describe('SearchSidebarService', () => { describe('SidebarService', () => {
let service: SearchSidebarService; let service: SidebarService;
const store: Store<AppState> = jasmine.createSpyObj('store', { const store: Store<AppState> = jasmine.createSpyObj('store', {
/* tslint:disable:no-empty */ /* tslint:disable:no-empty */
dispatch: {}, dispatch: {},
@@ -35,7 +35,7 @@ describe('SearchSidebarService', () => {
})); }));
beforeEach(() => { beforeEach(() => {
service = new SearchSidebarService(store, windowService); service = new SidebarService(store, windowService);
}) ; }) ;
describe('when the collapse method is triggered', () => { describe('when the collapse method is triggered', () => {
@@ -43,8 +43,8 @@ describe('SearchSidebarService', () => {
service.collapse(); service.collapse();
}); });
it('SearchSidebarCollapseAction should be dispatched to the store', () => { it('SidebarCollapseAction should be dispatched to the store', () => {
expect(store.dispatch).toHaveBeenCalledWith(new SearchSidebarCollapseAction()); expect(store.dispatch).toHaveBeenCalledWith(new SidebarCollapseAction());
}); });
}); });
@@ -54,8 +54,8 @@ describe('SearchSidebarService', () => {
service.expand(); service.expand();
}); });
it('SearchSidebarExpandAction should be dispatched to the store', () => { it('SidebarExpandAction should be dispatched to the store', () => {
expect(store.dispatch).toHaveBeenCalledWith(new SearchSidebarExpandAction()); expect(store.dispatch).toHaveBeenCalledWith(new SidebarExpandAction());
}); });
}); });

View File

@@ -1,20 +1,20 @@
import { combineLatest as observableCombineLatest, Observable } from 'rxjs'; import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
import { Injectable } from '@angular/core'; import { Injectable } from '@angular/core';
import { SearchSidebarState } from './search-sidebar.reducer'; import { SidebarState } from './sidebar.reducer';
import { createSelector, select, Store } from '@ngrx/store'; 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 { AppState } from '../../app.reducer';
import { HostWindowService } from '../../shared/host-window.service'; import { HostWindowService } from '../host-window.service';
import { map } from 'rxjs/operators'; import { map } from 'rxjs/operators';
const sidebarStateSelector = (state: AppState) => state.searchSidebar; const sidebarStateSelector = (state: AppState) => state.sidebar;
const sidebarCollapsedSelector = createSelector(sidebarStateSelector, (sidebar: SearchSidebarState) => sidebar.sidebarCollapsed); 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() @Injectable()
export class SearchSidebarService { export class SidebarService {
/** /**
* Emits true is the current screen size is mobile * Emits true is the current screen size is mobile
*/ */
@@ -47,13 +47,13 @@ export class SearchSidebarService {
* Dispatches a collapse action to the store * Dispatches a collapse action to the store
*/ */
public collapse(): void { public collapse(): void {
this.store.dispatch(new SearchSidebarCollapseAction()); this.store.dispatch(new SidebarCollapseAction());
} }
/** /**
* Dispatches an expand action to the store * Dispatches an expand action to the store
*/ */
public expand(): void { public expand(): void {
this.store.dispatch(new SearchSidebarExpandAction()); this.store.dispatch(new SidebarExpandAction());
} }
} }