mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 18:14:17 +00:00
Merge branch 'master' into w2p-65240_Community-and-collection-logos
Conflicts: resources/i18n/en.json5 src/app/shared/shared.module.ts
This commit is contained in:
@@ -17,13 +17,12 @@ before_install:
|
|||||||
- curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
|
- curl -L https://github.com/docker/compose/releases/download/${COMPOSE_VERSION}/docker-compose-`uname -s`-`uname -m` > docker-compose
|
||||||
- chmod +x docker-compose
|
- chmod +x docker-compose
|
||||||
- sudo mv docker-compose /usr/local/bin
|
- sudo mv docker-compose /usr/local/bin
|
||||||
- git clone https://github.com/DSpace-Labs/DSpace-Docker-Images.git
|
|
||||||
|
|
||||||
install:
|
install:
|
||||||
# Start up DSpace 7 using the entities database dump
|
# Start up DSpace 7 using the entities database dump
|
||||||
- docker-compose -f DSpace-Docker-Images/docker-compose-files/dspace-compose-v2/d7.travis.ci.yml up -d
|
- docker-compose -f ./docker/docker-compose-travis.yml up -d
|
||||||
# Use the dspace-cli image to populate the assetstore. Trigger a discovery and oai update
|
# Use the dspace-cli image to populate the assetstore. Trigger a discovery and oai update
|
||||||
- docker-compose -f DSpace-Docker-Images/docker-compose-files/dspace-compose-v2/d7.cli.yml -f DSpace-Docker-Images/docker-compose-files/dspace-compose-v2/d7.cli.assetstore.yml run --rm dspace-cli
|
- docker-compose -f ./docker/cli.yml -f ./docker/cli.assetstore.yml run --rm dspace-cli
|
||||||
- travis_retry yarn install
|
- travis_retry yarn install
|
||||||
|
|
||||||
before_script:
|
before_script:
|
||||||
@@ -32,7 +31,7 @@ before_script:
|
|||||||
#- curl http://localhost:8080/
|
#- curl http://localhost:8080/
|
||||||
|
|
||||||
after_script:
|
after_script:
|
||||||
- docker-compose -f DSpace-Docker-Images/docker-compose-files/dspace-compose-v2/d7.travis.ci.yml down
|
- docker-compose -f ./docker/docker-compose-travis.yml down
|
||||||
|
|
||||||
addons:
|
addons:
|
||||||
apt:
|
apt:
|
||||||
|
@@ -131,6 +131,11 @@ 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
|
||||||
-------
|
-------
|
||||||
|
|
||||||
|
79
docker/README.md
Normal file
79
docker/README.md
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
# Docker Compose files
|
||||||
|
|
||||||
|
## docker directory
|
||||||
|
- docker-compose.yml
|
||||||
|
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace 7 REST instance will also be started in Docker.
|
||||||
|
- docker-compose-rest.yml
|
||||||
|
- Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes
|
||||||
|
- docker-compose-travis.yml
|
||||||
|
- Runs a published instance of the DSpace 7 REST API for CI testing. The database is re-populated from a SQL dump on each startup.
|
||||||
|
- cli.yml
|
||||||
|
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
||||||
|
- cli.assetstore.yml
|
||||||
|
- Docker compose file that will download and install data into a DSpace REST assetstore. This script points to a default dataset that will be utilized for CI testing.
|
||||||
|
- environment.dev.js
|
||||||
|
- Environment file for running DSpace Angular in Docker
|
||||||
|
- local.cfg
|
||||||
|
- Environment file for running the DSpace 7 REST API in Docker.
|
||||||
|
|
||||||
|
|
||||||
|
## To refresh / pull DSpace images from Dockerhub
|
||||||
|
```
|
||||||
|
docker-compose -f docker/docker-compose.yml pull
|
||||||
|
```
|
||||||
|
|
||||||
|
## To build DSpace images using code in your branch
|
||||||
|
```
|
||||||
|
docker-compose -f docker/docker-compose.yml build
|
||||||
|
```
|
||||||
|
|
||||||
|
## To start DSpace (REST and Angular) from your branch
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Run DSpace REST and DSpace Angular from local branches.
|
||||||
|
_The system will be started in 2 steps. Each step shares the same docker network._
|
||||||
|
|
||||||
|
From DSpace/DSpace (build as needed)
|
||||||
|
```
|
||||||
|
docker-compose -p d7 up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
From DSpace/DSpace-angular
|
||||||
|
```
|
||||||
|
docker-compose -p d7 -f docker/docker-compose.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
## Ingest test data from AIPDIR
|
||||||
|
|
||||||
|
Create an administrator
|
||||||
|
```
|
||||||
|
docker-compose -p d7 -f docker/cli.yml run --rm dspace-cli create-administrator -e test@test.edu -f admin -l user -p admin -c en
|
||||||
|
```
|
||||||
|
|
||||||
|
Load content from AIP files
|
||||||
|
```
|
||||||
|
docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## Alternative Ingest - Use Entities dataset
|
||||||
|
_Delete your docker volumes or use a unique project (-p) name_
|
||||||
|
|
||||||
|
Start DSpace with Database Content from a database dump
|
||||||
|
```
|
||||||
|
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml -f docker/db.entities.yml up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
Load assetstore content and trigger a re-index of the repository
|
||||||
|
```
|
||||||
|
docker-compose -p d7 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
|
||||||
|
```
|
||||||
|
|
||||||
|
## End to end testing of the rest api (runs in travis).
|
||||||
|
_In this instance, only the REST api runs in Docker using the Entities dataset. Travis will perform CI testing of Angular using Node to drive the tests._
|
||||||
|
|
||||||
|
```
|
||||||
|
docker-compose -p d7ci -f docker/docker-compose-travis.yml up -d
|
||||||
|
```
|
23
docker/cli.assetstore.yml
Normal file
23
docker/cli.assetstore.yml
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
version: "3.7"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dspacenet:
|
||||||
|
|
||||||
|
services:
|
||||||
|
dspace-cli:
|
||||||
|
networks:
|
||||||
|
dspacenet: {}
|
||||||
|
environment:
|
||||||
|
- LOADASSETS=https://www.dropbox.com/s/zv7lj8j2lp3egjs/assetstore.tar.gz?dl=1
|
||||||
|
entrypoint:
|
||||||
|
- /bin/bash
|
||||||
|
- '-c'
|
||||||
|
- |
|
||||||
|
if [ ! -z $${LOADASSETS} ]
|
||||||
|
then
|
||||||
|
curl $${LOADASSETS} -L -s --output /tmp/assetstore.tar.gz
|
||||||
|
cd /dspace
|
||||||
|
tar xvfz /tmp/assetstore.tar.gz
|
||||||
|
fi
|
||||||
|
|
||||||
|
/dspace/bin/dspace index-discovery
|
32
docker/cli.ingest.yml
Normal file
32
docker/cli.ingest.yml
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
#
|
||||||
|
# The contents of this file are subject to the license and copyright
|
||||||
|
# detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
# tree and available online at
|
||||||
|
#
|
||||||
|
# http://www.dspace.org/license/
|
||||||
|
#
|
||||||
|
|
||||||
|
version: "3.7"
|
||||||
|
|
||||||
|
services:
|
||||||
|
dspace-cli:
|
||||||
|
environment:
|
||||||
|
- AIPZIP=https://github.com/DSpace-Labs/AIP-Files/raw/master/dogAndReport.zip
|
||||||
|
- ADMIN_EMAIL=test@test.edu
|
||||||
|
- AIPDIR=/tmp/aip-dir
|
||||||
|
entrypoint:
|
||||||
|
- /bin/bash
|
||||||
|
- '-c'
|
||||||
|
- |
|
||||||
|
rm -rf $${AIPDIR}
|
||||||
|
mkdir $${AIPDIR} /dspace/upload
|
||||||
|
cd $${AIPDIR}
|
||||||
|
pwd
|
||||||
|
curl $${AIPZIP} -L -s --output aip.zip
|
||||||
|
unzip aip.zip
|
||||||
|
cd $${AIPDIR}
|
||||||
|
|
||||||
|
/dspace/bin/dspace packager -r -a -t AIP -e $${ADMIN_EMAIL} -f -u SITE*.zip
|
||||||
|
/dspace/bin/dspace database update-sequences
|
||||||
|
|
||||||
|
/dspace/bin/dspace index-discovery
|
22
docker/cli.yml
Normal file
22
docker/cli.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
version: "3.7"
|
||||||
|
|
||||||
|
services:
|
||||||
|
dspace-cli:
|
||||||
|
image: "${DOCKER_OWNER:-dspace}/dspace-cli:${DSPACE_VER:-dspace-7_x}"
|
||||||
|
container_name: dspace-cli
|
||||||
|
#environment:
|
||||||
|
volumes:
|
||||||
|
- "assetstore:/dspace/assetstore"
|
||||||
|
- "./local.cfg:/dspace/config/local.cfg"
|
||||||
|
entrypoint: /dspace/bin/dspace
|
||||||
|
command: help
|
||||||
|
networks:
|
||||||
|
- dspacenet
|
||||||
|
tty: true
|
||||||
|
stdin_open: true
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
assetstore:
|
||||||
|
|
||||||
|
networks:
|
||||||
|
dspacenet:
|
16
docker/db.entities.yml
Normal file
16
docker/db.entities.yml
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
#
|
||||||
|
# The contents of this file are subject to the license and copyright
|
||||||
|
# detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
# tree and available online at
|
||||||
|
#
|
||||||
|
# http://www.dspace.org/license/
|
||||||
|
#
|
||||||
|
|
||||||
|
version: "3.7"
|
||||||
|
|
||||||
|
services:
|
||||||
|
dspacedb:
|
||||||
|
image: dspace/dspace-postgres-pgcrypto:loadsql
|
||||||
|
environment:
|
||||||
|
# Double underbars in env names will be replaced with periods for apache commons
|
||||||
|
- LOADSQL=https://www.dropbox.com/s/xh3ack0vg0922p2/configurable-entities-2019-05-08.sql?dl=1
|
59
docker/docker-compose-rest.yml
Normal file
59
docker/docker-compose-rest.yml
Normal file
@@ -0,0 +1,59 @@
|
|||||||
|
networks:
|
||||||
|
dspacenet:
|
||||||
|
services:
|
||||||
|
dspace:
|
||||||
|
container_name: dspace
|
||||||
|
depends_on:
|
||||||
|
- dspacedb
|
||||||
|
image: dspace/dspace:dspace-7_x-jdk8-test
|
||||||
|
networks:
|
||||||
|
dspacenet:
|
||||||
|
ports:
|
||||||
|
- published: 8080
|
||||||
|
target: 8080
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
volumes:
|
||||||
|
- assetstore:/dspace/assetstore
|
||||||
|
- "./local.cfg:/dspace/config/local.cfg"
|
||||||
|
# Ensure that the database is ready before starting tomcat
|
||||||
|
entrypoint:
|
||||||
|
- /bin/bash
|
||||||
|
- '-c'
|
||||||
|
- |
|
||||||
|
/dspace/bin/dspace database migrate
|
||||||
|
catalina.sh run
|
||||||
|
dspacedb:
|
||||||
|
container_name: dspacedb
|
||||||
|
image: dspace/dspace-postgres-pgcrypto
|
||||||
|
environment:
|
||||||
|
PGDATA: /pgdata
|
||||||
|
networks:
|
||||||
|
dspacenet:
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
volumes:
|
||||||
|
- pgdata:/pgdata
|
||||||
|
dspacesolr:
|
||||||
|
container_name: dspacesolr
|
||||||
|
image: dspace/dspace-solr
|
||||||
|
networks:
|
||||||
|
dspacenet:
|
||||||
|
ports:
|
||||||
|
- published: 8983
|
||||||
|
target: 8983
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
volumes:
|
||||||
|
- solr_authority:/opt/solr/server/solr/authority/data
|
||||||
|
- solr_oai:/opt/solr/server/solr/oai/data
|
||||||
|
- solr_search:/opt/solr/server/solr/search/data
|
||||||
|
- solr_statistics:/opt/solr/server/solr/statistics/data
|
||||||
|
version: '3.7'
|
||||||
|
volumes:
|
||||||
|
assetstore:
|
||||||
|
pgdata:
|
||||||
|
solr_authority:
|
||||||
|
solr_oai:
|
||||||
|
solr_search:
|
||||||
|
solr_statistics:
|
53
docker/docker-compose-travis.yml
Normal file
53
docker/docker-compose-travis.yml
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
networks:
|
||||||
|
dspacenet:
|
||||||
|
services:
|
||||||
|
dspace:
|
||||||
|
container_name: dspace
|
||||||
|
depends_on:
|
||||||
|
- dspacedb
|
||||||
|
image: dspace/dspace:dspace-7_x-jdk8-test
|
||||||
|
networks:
|
||||||
|
dspacenet:
|
||||||
|
ports:
|
||||||
|
- published: 8080
|
||||||
|
target: 8080
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
volumes:
|
||||||
|
- assetstore:/dspace/assetstore
|
||||||
|
- "./local.cfg:/dspace/config/local.cfg"
|
||||||
|
dspacedb:
|
||||||
|
container_name: dspacedb
|
||||||
|
environment:
|
||||||
|
LOADSQL: https://www.dropbox.com/s/xh3ack0vg0922p2/configurable-entities-2019-05-08.sql?dl=1
|
||||||
|
PGDATA: /pgdata
|
||||||
|
image: dspace/dspace-postgres-pgcrypto:loadsql
|
||||||
|
networks:
|
||||||
|
dspacenet:
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
volumes:
|
||||||
|
- pgdata:/pgdata
|
||||||
|
dspacesolr:
|
||||||
|
container_name: dspacesolr
|
||||||
|
image: dspace/dspace-solr
|
||||||
|
networks:
|
||||||
|
dspacenet:
|
||||||
|
ports:
|
||||||
|
- published: 8983
|
||||||
|
target: 8983
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
volumes:
|
||||||
|
- solr_authority:/opt/solr/server/solr/authority/data
|
||||||
|
- solr_oai:/opt/solr/server/solr/oai/data
|
||||||
|
- solr_search:/opt/solr/server/solr/search/data
|
||||||
|
- solr_statistics:/opt/solr/server/solr/statistics/data
|
||||||
|
version: '3.7'
|
||||||
|
volumes:
|
||||||
|
assetstore:
|
||||||
|
pgdata:
|
||||||
|
solr_authority:
|
||||||
|
solr_oai:
|
||||||
|
solr_search:
|
||||||
|
solr_statistics:
|
26
docker/docker-compose.yml
Normal file
26
docker/docker-compose.yml
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
version: '3.7'
|
||||||
|
networks:
|
||||||
|
dspacenet:
|
||||||
|
services:
|
||||||
|
dspace-angular:
|
||||||
|
container_name: dspace-angular
|
||||||
|
environment:
|
||||||
|
DSPACE_HOST: dspace-angular
|
||||||
|
DSPACE_NAMESPACE: /
|
||||||
|
DSPACE_PORT: '3000'
|
||||||
|
DSPACE_SSL: "false"
|
||||||
|
image: dspace/dspace-angular:latest
|
||||||
|
build:
|
||||||
|
context: ..
|
||||||
|
dockerfile: Dockerfile
|
||||||
|
networks:
|
||||||
|
dspacenet:
|
||||||
|
ports:
|
||||||
|
- published: 3000
|
||||||
|
target: 3000
|
||||||
|
- published: 9876
|
||||||
|
target: 9876
|
||||||
|
stdin_open: true
|
||||||
|
tty: true
|
||||||
|
volumes:
|
||||||
|
- ./environment.dev.js:/app/config/environment.dev.js
|
16
docker/environment.dev.js
Normal file
16
docker/environment.dev.js
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
/*
|
||||||
|
* The contents of this file are subject to the license and copyright
|
||||||
|
* detailed in the LICENSE and NOTICE files at the root of the source
|
||||||
|
* tree and available online at
|
||||||
|
*
|
||||||
|
* http://www.dspace.org/license/
|
||||||
|
*/
|
||||||
|
module.exports = {
|
||||||
|
rest: {
|
||||||
|
ssl: false,
|
||||||
|
host: 'localhost',
|
||||||
|
port: 8080,
|
||||||
|
// NOTE: Space is capitalized because 'namespace' is a reserved string in TypeScript
|
||||||
|
nameSpace: '/server/api'
|
||||||
|
}
|
||||||
|
};
|
6
docker/local.cfg
Normal file
6
docker/local.cfg
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
dspace.dir=/dspace
|
||||||
|
db.url=jdbc:postgresql://dspacedb:5432/dspace
|
||||||
|
dspace.hostname=dspace
|
||||||
|
dspace.baseUrl=http://localhost:8080/server
|
||||||
|
dspace.name=DSpace Started with Docker Compose
|
||||||
|
solr.server=http://dspacesolr:8983/solr
|
@@ -236,7 +236,7 @@
|
|||||||
"tslint": "5.11.0",
|
"tslint": "5.11.0",
|
||||||
"typedoc": "^0.9.0",
|
"typedoc": "^0.9.0",
|
||||||
"typescript": "^2.9.1",
|
"typescript": "^2.9.1",
|
||||||
"webdriver-manager": "^12.1.6",
|
"webdriver-manager": "^12.1.7",
|
||||||
"webpack": "^4.17.1",
|
"webpack": "^4.17.1",
|
||||||
"webpack-bundle-analyzer": "^3.3.2",
|
"webpack-bundle-analyzer": "^3.3.2",
|
||||||
"webpack-dev-middleware": "3.2.0",
|
"webpack-dev-middleware": "3.2.0",
|
||||||
|
@@ -129,8 +129,28 @@
|
|||||||
"collection.delete.notification.fail": "Collection could not be deleted",
|
"collection.delete.notification.fail": "Collection could not be deleted",
|
||||||
"collection.delete.notification.success": "Successfully deleted collection",
|
"collection.delete.notification.success": "Successfully deleted collection",
|
||||||
"collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"",
|
"collection.delete.text": "Are you sure you want to delete collection \"{{ dso }}\"",
|
||||||
|
|
||||||
"collection.edit.delete": "Delete this collection",
|
"collection.edit.delete": "Delete this collection",
|
||||||
"collection.edit.head": "Edit Collection",
|
"collection.edit.head": "Edit Collection",
|
||||||
|
|
||||||
|
"collection.edit.item-mapper.cancel": "Cancel",
|
||||||
|
"collection.edit.item-mapper.collection": "Collection: \"<b>{{name}}</b>\"",
|
||||||
|
"collection.edit.item-mapper.confirm": "Map selected items",
|
||||||
|
"collection.edit.item-mapper.description": "This is the item mapper tool that allows collection administrators to map items from other collections into this collection. You can search for items from other collections and map them, or browse the list of currently mapped items.",
|
||||||
|
"collection.edit.item-mapper.head": "Item Mapper - Map Items from Other Collections",
|
||||||
|
"collection.edit.item-mapper.no-search": "Please enter a query to search",
|
||||||
|
"collection.edit.item-mapper.notifications.map.error.content": "Errors occurred for mapping of {{amount}} items.",
|
||||||
|
"collection.edit.item-mapper.notifications.map.error.head": "Mapping errors",
|
||||||
|
"collection.edit.item-mapper.notifications.map.success.content": "Successfully mapped {{amount}} items.",
|
||||||
|
"collection.edit.item-mapper.notifications.map.success.head": "Mapping completed",
|
||||||
|
"collection.edit.item-mapper.notifications.unmap.error.content": "Errors occurred for removing the mappings of {{amount}} items.",
|
||||||
|
"collection.edit.item-mapper.notifications.unmap.error.head": "Remove mapping errors",
|
||||||
|
"collection.edit.item-mapper.notifications.unmap.success.content": "Successfully removed the mappings of {{amount}} items.",
|
||||||
|
"collection.edit.item-mapper.notifications.unmap.success.head": "Remove mapping completed",
|
||||||
|
"collection.edit.item-mapper.remove": "Remove selected item mappings",
|
||||||
|
"collection.edit.item-mapper.tabs.browse": "Browse mapped items",
|
||||||
|
"collection.edit.item-mapper.tabs.map": "Map new items",
|
||||||
|
|
||||||
"collection.edit.logo.label": "Collection logo",
|
"collection.edit.logo.label": "Collection logo",
|
||||||
"collection.edit.logo.notifications.add.error": "Uploading Collection logo failed. Please verify the content before retrying.",
|
"collection.edit.logo.notifications.add.error": "Uploading Collection logo failed. Please verify the content before retrying.",
|
||||||
"collection.edit.logo.notifications.add.success": "Upload Collection logo successful.",
|
"collection.edit.logo.notifications.add.success": "Upload Collection logo successful.",
|
||||||
@@ -138,8 +158,10 @@
|
|||||||
"collection.edit.logo.notifications.delete.success.content": "Successfully deleted the collection's logo",
|
"collection.edit.logo.notifications.delete.success.content": "Successfully deleted the collection's logo",
|
||||||
"collection.edit.logo.notifications.delete.error.title": "Error deleting logo",
|
"collection.edit.logo.notifications.delete.error.title": "Error deleting logo",
|
||||||
"collection.edit.logo.upload": "Drop a Collection Logo to upload",
|
"collection.edit.logo.upload": "Drop a Collection Logo to upload",
|
||||||
|
|
||||||
"collection.edit.notifications.success": "Successfully edited the Collection",
|
"collection.edit.notifications.success": "Successfully edited the Collection",
|
||||||
"collection.edit.return": "Return",
|
"collection.edit.return": "Return",
|
||||||
|
|
||||||
"collection.edit.tabs.curate.head": "Curate",
|
"collection.edit.tabs.curate.head": "Curate",
|
||||||
"collection.edit.tabs.curate.title": "Collection Edit - Curate",
|
"collection.edit.tabs.curate.title": "Collection Edit - Curate",
|
||||||
"collection.edit.tabs.metadata.head": "Edit Metadata",
|
"collection.edit.tabs.metadata.head": "Edit Metadata",
|
||||||
@@ -148,6 +170,7 @@
|
|||||||
"collection.edit.tabs.roles.title": "Collection Edit - Roles",
|
"collection.edit.tabs.roles.title": "Collection Edit - Roles",
|
||||||
"collection.edit.tabs.source.head": "Content Source",
|
"collection.edit.tabs.source.head": "Content Source",
|
||||||
"collection.edit.tabs.source.title": "Collection Edit - Content Source",
|
"collection.edit.tabs.source.title": "Collection Edit - Content Source",
|
||||||
|
|
||||||
"collection.form.abstract": "Short Description",
|
"collection.form.abstract": "Short Description",
|
||||||
"collection.form.description": "Introductory text (HTML)",
|
"collection.form.description": "Introductory text (HTML)",
|
||||||
"collection.form.errors.title.required": "Please enter a collection name",
|
"collection.form.errors.title.required": "Please enter a collection name",
|
||||||
@@ -156,11 +179,17 @@
|
|||||||
"collection.form.rights": "Copyright text (HTML)",
|
"collection.form.rights": "Copyright text (HTML)",
|
||||||
"collection.form.tableofcontents": "News (HTML)",
|
"collection.form.tableofcontents": "News (HTML)",
|
||||||
"collection.form.title": "Name",
|
"collection.form.title": "Name",
|
||||||
|
|
||||||
"collection.page.browse.recent.head": "Recent Submissions",
|
"collection.page.browse.recent.head": "Recent Submissions",
|
||||||
"collection.page.browse.recent.empty": "No items to show",
|
"collection.page.browse.recent.empty": "No items to show",
|
||||||
|
"collection.page.handle": "Permanent URI for this collection",
|
||||||
"collection.page.license": "License",
|
"collection.page.license": "License",
|
||||||
"collection.page.news": "News",
|
"collection.page.news": "News",
|
||||||
|
|
||||||
|
"collection.select.confirm": "Confirm selected",
|
||||||
|
"collection.select.empty": "No collections to show",
|
||||||
|
"collection.select.table.title": "Title",
|
||||||
|
|
||||||
"community.create.head": "Create a Community",
|
"community.create.head": "Create a Community",
|
||||||
"community.create.notifications.success": "Successfully created the Community",
|
"community.create.notifications.success": "Successfully created the Community",
|
||||||
"community.create.sub-head": "Create a Sub-Community for Community {{ parent }}",
|
"community.create.sub-head": "Create a Sub-Community for Community {{ parent }}",
|
||||||
@@ -193,8 +222,10 @@
|
|||||||
"community.form.rights": "Copyright text (HTML)",
|
"community.form.rights": "Copyright text (HTML)",
|
||||||
"community.form.tableofcontents": "News (HTML)",
|
"community.form.tableofcontents": "News (HTML)",
|
||||||
"community.form.title": "Name",
|
"community.form.title": "Name",
|
||||||
|
"community.page.handle": "Permanent URI for this community",
|
||||||
"community.page.license": "License",
|
"community.page.license": "License",
|
||||||
"community.page.news": "News",
|
"community.page.news": "News",
|
||||||
|
"community.all-lists.head": "Subcommunities and Collections",
|
||||||
"community.sub-collection-list.head": "Collections of this Community",
|
"community.sub-collection-list.head": "Collections of this Community",
|
||||||
"community.sub-community-list.head": "Communities of this Community",
|
"community.sub-community-list.head": "Communities of this Community",
|
||||||
|
|
||||||
@@ -211,9 +242,11 @@
|
|||||||
|
|
||||||
"error.browse-by": "Error fetching items",
|
"error.browse-by": "Error fetching items",
|
||||||
"error.collection": "Error fetching collection",
|
"error.collection": "Error fetching collection",
|
||||||
|
"error.collections": "Error fetching collections",
|
||||||
"error.community": "Error fetching community",
|
"error.community": "Error fetching community",
|
||||||
"error.default": "Error",
|
"error.default": "Error",
|
||||||
"error.item": "Error fetching item",
|
"error.item": "Error fetching item",
|
||||||
|
"error.items": "Error fetching items",
|
||||||
"error.objects": "Error fetching objects",
|
"error.objects": "Error fetching objects",
|
||||||
"error.recent-submissions": "Error fetching recent submissions",
|
"error.recent-submissions": "Error fetching recent submissions",
|
||||||
"error.search-results": "Error fetching search results",
|
"error.search-results": "Error fetching search results",
|
||||||
@@ -263,6 +296,24 @@
|
|||||||
"item.edit.delete.success": "The item has been deleted",
|
"item.edit.delete.success": "The item has been deleted",
|
||||||
"item.edit.head": "Edit Item",
|
"item.edit.head": "Edit Item",
|
||||||
|
|
||||||
|
"item.edit.item-mapper.buttons.add": "Map item to selected collections",
|
||||||
|
"item.edit.item-mapper.buttons.remove": "Remove item's mapping for selected collections",
|
||||||
|
"item.edit.item-mapper.cancel": "Cancel",
|
||||||
|
"item.edit.item-mapper.description": "This is the item mapper tool that allows administrators to map this item to other collections. You can search for collections and map them, or browse the list of collections the item is currently mapped to.",
|
||||||
|
"item.edit.item-mapper.head": "Item Mapper - Map Item to Collections",
|
||||||
|
"item.edit.item-mapper.item": "Item: \"<b>{{name}}</b>\"",
|
||||||
|
"item.edit.item-mapper.no-search": "Please enter a query to search",
|
||||||
|
"item.edit.item-mapper.notifications.add.error.content": "Errors occurred for mapping of item to {{amount}} collections.",
|
||||||
|
"item.edit.item-mapper.notifications.add.error.head": "Mapping errors",
|
||||||
|
"item.edit.item-mapper.notifications.add.success.content": "Successfully mapped item to {{amount}} collections.",
|
||||||
|
"item.edit.item-mapper.notifications.add.success.head": "Mapping completed",
|
||||||
|
"item.edit.item-mapper.notifications.remove.error.content": "Errors occurred for the removal of the mapping to {{amount}} collections.",
|
||||||
|
"item.edit.item-mapper.notifications.remove.error.head": "Removal of mapping errors",
|
||||||
|
"item.edit.item-mapper.notifications.remove.success.content": "Successfully removed mapping of item to {{amount}} collections.",
|
||||||
|
"item.edit.item-mapper.notifications.remove.success.head": "Removal of mapping completed",
|
||||||
|
"item.edit.item-mapper.tabs.browse": "Browse mapped collections",
|
||||||
|
"item.edit.item-mapper.tabs.map": "Map new collections",
|
||||||
|
|
||||||
"item.edit.metadata.add-button": "Add",
|
"item.edit.metadata.add-button": "Add",
|
||||||
"item.edit.metadata.discard-button": "Discard",
|
"item.edit.metadata.discard-button": "Discard",
|
||||||
"item.edit.metadata.edit.buttons.edit": "Edit",
|
"item.edit.metadata.edit.buttons.edit": "Edit",
|
||||||
@@ -395,6 +446,7 @@
|
|||||||
"item.page.uri": "URI",
|
"item.page.uri": "URI",
|
||||||
|
|
||||||
"item.select.confirm": "Confirm selected",
|
"item.select.confirm": "Confirm selected",
|
||||||
|
"item.select.empty": "No items to show",
|
||||||
"item.select.table.author": "Author",
|
"item.select.table.author": "Author",
|
||||||
"item.select.table.collection": "Collection",
|
"item.select.table.collection": "Collection",
|
||||||
"item.select.table.title": "Title",
|
"item.select.table.title": "Title",
|
||||||
@@ -426,9 +478,11 @@
|
|||||||
"loading.browse-by": "Loading items...",
|
"loading.browse-by": "Loading items...",
|
||||||
"loading.browse-by-page": "Loading page...",
|
"loading.browse-by-page": "Loading page...",
|
||||||
"loading.collection": "Loading collection...",
|
"loading.collection": "Loading collection...",
|
||||||
|
"loading.collections": "Loading collections...",
|
||||||
"loading.community": "Loading community...",
|
"loading.community": "Loading community...",
|
||||||
"loading.default": "Loading...",
|
"loading.default": "Loading...",
|
||||||
"loading.item": "Loading item...",
|
"loading.item": "Loading item...",
|
||||||
|
"loading.items": "Loading items...",
|
||||||
"loading.mydspace-results": "Loading items...",
|
"loading.mydspace-results": "Loading items...",
|
||||||
"loading.objects": "Loading...",
|
"loading.objects": "Loading...",
|
||||||
"loading.recent-submissions": "Loading recent submissions...",
|
"loading.recent-submissions": "Loading recent submissions...",
|
||||||
|
@@ -19,6 +19,7 @@ import { ENV_CONFIG, GLOBAL_CONFIG } from '../../../config';
|
|||||||
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
import { BrowseEntrySearchOptions } from '../../core/browse/browse-entry-search-options.model';
|
||||||
import { toRemoteData } from '../+browse-by-metadata-page/browse-by-metadata-page.component.spec';
|
import { toRemoteData } from '../+browse-by-metadata-page/browse-by-metadata-page.component.spec';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
|
||||||
describe('BrowseByDatePageComponent', () => {
|
describe('BrowseByDatePageComponent', () => {
|
||||||
let comp: BrowseByDatePageComponent;
|
let comp: BrowseByDatePageComponent;
|
||||||
@@ -69,7 +70,7 @@ describe('BrowseByDatePageComponent', () => {
|
|||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
declarations: [BrowseByDatePageComponent, EnumKeysPipe],
|
declarations: [BrowseByDatePageComponent, EnumKeysPipe, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
|
{ provide: GLOBAL_CONFIG, useValue: ENV_CONFIG },
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
@@ -1,7 +1,31 @@
|
|||||||
<div class="container">
|
<div class="container">
|
||||||
|
<ng-container *ngVar="(parent$ | async) as parent">
|
||||||
|
<ng-container *ngIf="parent?.payload as parentContext">
|
||||||
|
<header class="comcol-header border-bottom mb-4 pb-4">
|
||||||
|
<!-- Parent Name -->
|
||||||
|
<ds-comcol-page-header [name]="parentContext.name">
|
||||||
|
</ds-comcol-page-header>
|
||||||
|
<!-- Handle -->
|
||||||
|
<ds-comcol-page-handle
|
||||||
|
[content]="parentContext.handle"
|
||||||
|
[title]="parentContext.type+'.page.handle'" >
|
||||||
|
</ds-comcol-page-handle>
|
||||||
|
<!-- Introductory text -->
|
||||||
|
<ds-comcol-page-content [content]="parentContext.introductoryText" [hasInnerHtml]="true">
|
||||||
|
</ds-comcol-page-content>
|
||||||
|
<!-- News -->
|
||||||
|
<ds-comcol-page-content [content]="parentContext.sidebarText" [hasInnerHtml]="true" [title]="'community.page.news'">
|
||||||
|
</ds-comcol-page-content>
|
||||||
|
</header>
|
||||||
|
<!-- Browse-By Links -->
|
||||||
|
<ds-comcol-page-browse-by [id]="parentContext.id" [contentType]="parentContext.type"></ds-comcol-page-browse-by>
|
||||||
|
</ng-container></ng-container>
|
||||||
|
|
||||||
|
<section class="comcol-page-browse-section">
|
||||||
<div class="browse-by-metadata w-100">
|
<div class="browse-by-metadata w-100">
|
||||||
<ds-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
|
<ds-browse-by *ngIf="startsWithOptions" class="col-xs-12 w-100"
|
||||||
title="{{'browse.title' | translate:{collection: (parent$ | async)?.payload?.name || '', field: 'browse.metadata.' + browseId | translate, value: (value)? '"' + value + '"': ''} }}"
|
title="{{'browse.title' | translate:{collection: (parent$ | async)?.payload?.name || '', field: 'browse.metadata.' + browseId | translate, value: (value)? '"' + value + '"': ''} }}"
|
||||||
|
parentname="{{(parent$ | async)?.payload?.name || ''}}"
|
||||||
[objects$]="(items$ !== undefined)? items$ : browseEntries$"
|
[objects$]="(items$ !== undefined)? items$ : browseEntries$"
|
||||||
[paginationConfig]="paginationConfig"
|
[paginationConfig]="paginationConfig"
|
||||||
[sortConfig]="sortConfig"
|
[sortConfig]="sortConfig"
|
||||||
@@ -15,4 +39,17 @@
|
|||||||
</ds-browse-by>
|
</ds-browse-by>
|
||||||
<ds-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-loading>
|
<ds-loading *ngIf="!startsWithOptions" message="{{'loading.browse-by-page' | translate}}"></ds-loading>
|
||||||
</div>
|
</div>
|
||||||
|
</section>
|
||||||
|
<ng-container *ngVar="(parent$ | async) as parent">
|
||||||
|
<ng-container *ngIf="parent?.payload as parentContext">
|
||||||
|
<footer *ngIf="parentContext.copyrightText" class="border-top my-5 pt-4">
|
||||||
|
<div >
|
||||||
|
|
||||||
|
<!-- Copyright -->
|
||||||
|
<ds-comcol-page-content [content]="parentContext.copyrightText" [hasInnerHtml]="true">
|
||||||
|
</ds-comcol-page-content>
|
||||||
|
</div>
|
||||||
|
</footer>
|
||||||
|
</ng-container>
|
||||||
|
</ng-container>
|
||||||
</div>
|
</div>
|
||||||
|
@@ -23,6 +23,7 @@ import { MockRouter } from '../../shared/mocks/mock-router';
|
|||||||
import { ResourceType } from '../../core/shared/resource-type';
|
import { ResourceType } from '../../core/shared/resource-type';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
import { BrowseEntry } from '../../core/shared/browse-entry.model';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
|
||||||
describe('BrowseByMetadataPageComponent', () => {
|
describe('BrowseByMetadataPageComponent', () => {
|
||||||
let comp: BrowseByMetadataPageComponent;
|
let comp: BrowseByMetadataPageComponent;
|
||||||
@@ -86,7 +87,7 @@ describe('BrowseByMetadataPageComponent', () => {
|
|||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
declarations: [BrowseByMetadataPageComponent, EnumKeysPipe],
|
declarations: [BrowseByMetadataPageComponent, EnumKeysPipe, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
{ provide: BrowseService, useValue: mockBrowseService },
|
{ provide: BrowseService, useValue: mockBrowseService },
|
||||||
|
@@ -18,6 +18,7 @@ import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.serv
|
|||||||
import { BrowseService } from '../../core/browse/browse.service';
|
import { BrowseService } from '../../core/browse/browse.service';
|
||||||
import { MockRouter } from '../../shared/mocks/mock-router';
|
import { MockRouter } from '../../shared/mocks/mock-router';
|
||||||
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
import { createSuccessfulRemoteDataObject$ } from '../../shared/testing/utils';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
|
||||||
describe('BrowseByTitlePageComponent', () => {
|
describe('BrowseByTitlePageComponent', () => {
|
||||||
let comp: BrowseByTitlePageComponent;
|
let comp: BrowseByTitlePageComponent;
|
||||||
@@ -64,7 +65,7 @@ describe('BrowseByTitlePageComponent', () => {
|
|||||||
beforeEach(async(() => {
|
beforeEach(async(() => {
|
||||||
TestBed.configureTestingModule({
|
TestBed.configureTestingModule({
|
||||||
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
imports: [CommonModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
declarations: [BrowseByTitlePageComponent, EnumKeysPipe],
|
declarations: [BrowseByTitlePageComponent, EnumKeysPipe, VarDirective],
|
||||||
providers: [
|
providers: [
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
{ provide: BrowseService, useValue: mockBrowseService },
|
{ provide: BrowseService, useValue: mockBrowseService },
|
||||||
|
@@ -0,0 +1,57 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2>{{'collection.edit.item-mapper.head' | translate}}</h2>
|
||||||
|
<p [innerHTML]="'collection.edit.item-mapper.collection' | translate:{ name: (collectionRD$ | async)?.payload?.name }" id="collection-name"></p>
|
||||||
|
<p>{{'collection.edit.item-mapper.description' | translate}}</p>
|
||||||
|
|
||||||
|
<ngb-tabset (tabChange)="tabChange($event)" [destroyOnHide]="true" #tabs="ngbTabset">
|
||||||
|
<ngb-tab title="{{'collection.edit.item-mapper.tabs.browse' | translate}}" id="browseTab">
|
||||||
|
<ng-template ngbTabContent>
|
||||||
|
<div class="mt-2">
|
||||||
|
<ds-item-select class="mt-2"
|
||||||
|
[key]="'browse'"
|
||||||
|
[dsoRD$]="collectionItemsRD$"
|
||||||
|
[paginationOptions]="(searchOptions$ | async)?.pagination"
|
||||||
|
[confirmButton]="'collection.edit.item-mapper.remove'"
|
||||||
|
[cancelButton]="'collection.edit.item-mapper.cancel'"
|
||||||
|
[dangerConfirm]="true"
|
||||||
|
[hideCollection]="true"
|
||||||
|
(confirm)="mapItems($event, true)"
|
||||||
|
(cancel)="onCancel()"></ds-item-select>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ngb-tab>
|
||||||
|
<ngb-tab title="{{'collection.edit.item-mapper.tabs.map' | translate}}" id="mapTab">
|
||||||
|
<ng-template ngbTabContent>
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<ds-search-form id="search-form"
|
||||||
|
[query]="(searchOptions$ | async)?.query"
|
||||||
|
[scope]="(searchOptions$ | async)?.scope"
|
||||||
|
[currentUrl]="'./'"
|
||||||
|
[inPlaceSearch]="true"
|
||||||
|
(submitSearch)="performedSearch = true">
|
||||||
|
</ds-search-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="performedSearch">
|
||||||
|
<ds-item-select class="mt-2"
|
||||||
|
[key]="'map'"
|
||||||
|
[dsoRD$]="mappedItemsRD$"
|
||||||
|
[paginationOptions]="(searchOptions$ | async)?.pagination"
|
||||||
|
[confirmButton]="'collection.edit.item-mapper.confirm'"
|
||||||
|
[cancelButton]="'collection.edit.item-mapper.cancel'"
|
||||||
|
(confirm)="mapItems($event)"
|
||||||
|
(cancel)="onCancel()"></ds-item-select>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!performedSearch" class="alert alert-info w-100" role="alert">
|
||||||
|
{{'collection.edit.item-mapper.no-search' | translate}}
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ngb-tab>
|
||||||
|
</ngb-tabset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1 @@
|
|||||||
|
@import '../../../styles/variables.scss';
|
@@ -0,0 +1,214 @@
|
|||||||
|
import { CollectionItemMapperComponent } from './collection-item-mapper.component';
|
||||||
|
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
|
import { SearchFormComponent } from '../../shared/search-form/search-form.component';
|
||||||
|
import { SearchPageModule } from '../../+search-page/search-page.module';
|
||||||
|
import { ObjectCollectionComponent } from '../../shared/object-collection/object-collection.component';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { ActivatedRouteStub } from '../../shared/testing/active-router-stub';
|
||||||
|
import { RouterStub } from '../../shared/testing/router-stub';
|
||||||
|
import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service';
|
||||||
|
import { SearchService } from '../../+search-page/search-service/search.service';
|
||||||
|
import { SearchServiceStub } from '../../shared/testing/search-service-stub';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { NotificationsServiceStub } from '../../shared/testing/notifications-service-stub';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { SharedModule } from '../../shared/shared.module';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model';
|
||||||
|
import { PaginationComponentOptions } from '../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
import { EventEmitter, NgModule } from '@angular/core';
|
||||||
|
import { HostWindowService } from '../../shared/host-window.service';
|
||||||
|
import { HostWindowServiceStub } from '../../shared/testing/host-window-service-stub';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../core/shared/page-info.model';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
import { PaginationComponent } from '../../shared/pagination/pagination.component';
|
||||||
|
import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
|
||||||
|
import { ItemSelectComponent } from '../../shared/object-select/item-select/item-select.component';
|
||||||
|
import { ObjectSelectService } from '../../shared/object-select/object-select.service';
|
||||||
|
import { ObjectSelectServiceStub } from '../../shared/testing/object-select-service-stub';
|
||||||
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { of as observableOf, of } from 'rxjs/internal/observable/of';
|
||||||
|
import { RestResponse } from '../../core/cache/response.models';
|
||||||
|
import { SearchFixedFilterService } from '../../+search-page/search-filters/search-filter/search-fixed-filter.service';
|
||||||
|
import { RouteService } from '../../core/services/route.service';
|
||||||
|
import { ErrorComponent } from '../../shared/error/error.component';
|
||||||
|
import { LoadingComponent } from '../../shared/loading/loading.component';
|
||||||
|
|
||||||
|
describe('CollectionItemMapperComponent', () => {
|
||||||
|
let comp: CollectionItemMapperComponent;
|
||||||
|
let fixture: ComponentFixture<CollectionItemMapperComponent>;
|
||||||
|
|
||||||
|
let route: ActivatedRoute;
|
||||||
|
let router: Router;
|
||||||
|
let searchConfigService: SearchConfigurationService;
|
||||||
|
let searchService: SearchService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
let itemDataService: ItemDataService;
|
||||||
|
|
||||||
|
const mockCollection: Collection = Object.assign(new Collection(), {
|
||||||
|
id: 'ce41d451-97ed-4a9c-94a1-7de34f16a9f4',
|
||||||
|
name: 'test-collection'
|
||||||
|
});
|
||||||
|
const mockCollectionRD: RemoteData<Collection> = new RemoteData<Collection>(false, false, true, null, mockCollection);
|
||||||
|
const mockSearchOptions = of(new PaginatedSearchOptions({
|
||||||
|
pagination: Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'search-page-configuration',
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1
|
||||||
|
}),
|
||||||
|
sort: new SortOptions('dc.title', SortDirection.ASC),
|
||||||
|
scope: mockCollection.id
|
||||||
|
}));
|
||||||
|
const url = 'http://test.url';
|
||||||
|
const urlWithParam = url + '?param=value';
|
||||||
|
const routerStub = Object.assign(new RouterStub(), {
|
||||||
|
url: urlWithParam,
|
||||||
|
navigateByUrl: {},
|
||||||
|
navigate: {}
|
||||||
|
});
|
||||||
|
const searchConfigServiceStub = {
|
||||||
|
paginatedSearchOptions: mockSearchOptions
|
||||||
|
};
|
||||||
|
const itemDataServiceStub = {
|
||||||
|
mapToCollection: () => of(new RestResponse(true, 200, 'OK'))
|
||||||
|
};
|
||||||
|
const activatedRouteStub = new ActivatedRouteStub({}, { collection: mockCollectionRD });
|
||||||
|
const translateServiceStub = {
|
||||||
|
get: () => of('test-message of collection ' + mockCollection.name),
|
||||||
|
onLangChange: new EventEmitter(),
|
||||||
|
onTranslationChange: new EventEmitter(),
|
||||||
|
onDefaultLangChange: new EventEmitter()
|
||||||
|
};
|
||||||
|
const emptyList = new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []));
|
||||||
|
const searchServiceStub = Object.assign(new SearchServiceStub(), {
|
||||||
|
search: () => of(emptyList),
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
clearDiscoveryRequests: () => {}
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
});
|
||||||
|
const collectionDataServiceStub = {
|
||||||
|
getMappedItems: () => of(emptyList),
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
clearMappedItemsRequests: () => {}
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
};
|
||||||
|
const routeServiceStub = {
|
||||||
|
getRouteParameterValue: () => {
|
||||||
|
return observableOf('');
|
||||||
|
},
|
||||||
|
getQueryParameterValue: () => {
|
||||||
|
return observableOf('')
|
||||||
|
},
|
||||||
|
getQueryParamsWithPrefix: () => {
|
||||||
|
return observableOf('')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
const fixedFilterServiceStub = {
|
||||||
|
getQueryByFilterName: () => {
|
||||||
|
return observableOf('')
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [CollectionItemMapperComponent, ItemSelectComponent, SearchFormComponent, PaginationComponent, EnumKeysPipe, VarDirective, ErrorComponent, LoadingComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
{ provide: Router, useValue: routerStub },
|
||||||
|
{ provide: SearchConfigurationService, useValue: searchConfigServiceStub },
|
||||||
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
|
{ provide: ItemDataService, useValue: itemDataServiceStub },
|
||||||
|
{ provide: CollectionDataService, useValue: collectionDataServiceStub },
|
||||||
|
{ provide: TranslateService, useValue: translateServiceStub },
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
|
{ provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() },
|
||||||
|
{ provide: RouteService, useValue: routeServiceStub },
|
||||||
|
{ provide: SearchFixedFilterService, useValue: fixedFilterServiceStub }
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CollectionItemMapperComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
route = (comp as any).route;
|
||||||
|
router = (comp as any).router;
|
||||||
|
searchConfigService = (comp as any).searchConfigService;
|
||||||
|
searchService = (comp as any).searchService;
|
||||||
|
notificationsService = (comp as any).notificationsService;
|
||||||
|
itemDataService = (comp as any).itemDataService;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the correct collection name', () => {
|
||||||
|
const name: HTMLElement = fixture.debugElement.query(By.css('#collection-name')).nativeElement;
|
||||||
|
expect(name.innerHTML).toContain(mockCollection.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mapItems', () => {
|
||||||
|
const ids = ['id1', 'id2', 'id3', 'id4'];
|
||||||
|
|
||||||
|
it('should display a success message if at least one mapping was successful', () => {
|
||||||
|
comp.mapItems(ids);
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
expect(notificationsService.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display an error message if at least one mapping was unsuccessful', () => {
|
||||||
|
spyOn(itemDataService, 'mapToCollection').and.returnValue(of(new RestResponse(false, 404, 'Not Found')));
|
||||||
|
comp.mapItems(ids);
|
||||||
|
expect(notificationsService.success).not.toHaveBeenCalled();
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tabChange', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routerStub, 'navigateByUrl');
|
||||||
|
comp.tabChange({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to the same page to remove parameters', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith(url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildQuery', () => {
|
||||||
|
const query = 'query';
|
||||||
|
const expected = `-location.coll:\"${mockCollection.id}\" AND ${query}`;
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
result = comp.buildQuery(mockCollection.id, query);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build a solr query to exclude the provided collection', () => {
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onCancel', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routerStub, 'navigate');
|
||||||
|
comp.onCancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to the collection page', () => {
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/collections/', mockCollection.id]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,256 @@
|
|||||||
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { ChangeDetectionStrategy, Component, Inject, OnInit, ViewChild } from '@angular/core';
|
||||||
|
import { fadeIn, fadeInOut } from '../../shared/animations/fade';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
|
import { Collection } from '../../core/shared/collection.model';
|
||||||
|
import { SearchConfigurationService } from '../../+search-page/search-service/search-configuration.service';
|
||||||
|
import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model';
|
||||||
|
import { PaginatedList } from '../../core/data/paginated-list';
|
||||||
|
import { map, startWith, switchMap, take, tap } from 'rxjs/operators';
|
||||||
|
import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } from '../../core/shared/operators';
|
||||||
|
import { SearchService } from '../../+search-page/search-service/search.service';
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
import { DSpaceObjectType } from '../../core/shared/dspace-object-type.model';
|
||||||
|
import { SortDirection, SortOptions } from '../../core/cache/models/sort-options.model';
|
||||||
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { CollectionDataService } from '../../core/data/collection-data.service';
|
||||||
|
import { isNotEmpty } from '../../shared/empty.util';
|
||||||
|
import { RestResponse } from '../../core/cache/response.models';
|
||||||
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
|
import { SEARCH_CONFIG_SERVICE } from '../../+my-dspace-page/my-dspace-page.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-collection-item-mapper',
|
||||||
|
styleUrls: ['./collection-item-mapper.component.scss'],
|
||||||
|
templateUrl: './collection-item-mapper.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
animations: [
|
||||||
|
fadeIn,
|
||||||
|
fadeInOut
|
||||||
|
],
|
||||||
|
providers: [
|
||||||
|
{
|
||||||
|
provide: SEARCH_CONFIG_SERVICE,
|
||||||
|
useClass: SearchConfigurationService
|
||||||
|
}
|
||||||
|
]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component used to map items to a collection
|
||||||
|
*/
|
||||||
|
export class CollectionItemMapperComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view on the tabset element
|
||||||
|
* Used to switch tabs programmatically
|
||||||
|
*/
|
||||||
|
@ViewChild('tabs') tabs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The collection to map items to
|
||||||
|
*/
|
||||||
|
collectionRD$: Observable<RemoteData<Collection>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search options
|
||||||
|
*/
|
||||||
|
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of items to show under the "Browse" tab
|
||||||
|
* Items inside the collection
|
||||||
|
*/
|
||||||
|
collectionItemsRD$: Observable<RemoteData<PaginatedList<DSpaceObject>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of items to show under the "Map" tab
|
||||||
|
* Items outside the collection
|
||||||
|
*/
|
||||||
|
mappedItemsRD$: Observable<RemoteData<PaginatedList<DSpaceObject>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Sort on title ASC by default
|
||||||
|
* @type {SortOptions}
|
||||||
|
*/
|
||||||
|
defaultSortOptions: SortOptions = new SortOptions('dc.title', SortDirection.ASC);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Firing this observable (shouldUpdate$.next(true)) forces the two lists to reload themselves
|
||||||
|
* Usually fired after the lists their cache is cleared (to force a new request to the REST API)
|
||||||
|
*/
|
||||||
|
shouldUpdate$: BehaviorSubject<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track whether at least one search has been performed or not
|
||||||
|
* As soon as at least one search has been performed, we display the search results
|
||||||
|
*/
|
||||||
|
performedSearch = false;
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
@Inject(SEARCH_CONFIG_SERVICE) private searchConfigService: SearchConfigurationService,
|
||||||
|
private searchService: SearchService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private itemDataService: ItemDataService,
|
||||||
|
private collectionDataService: CollectionDataService,
|
||||||
|
private translateService: TranslateService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.collectionRD$ = this.route.data.pipe(map((data) => data.collection)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Collection>>;
|
||||||
|
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
||||||
|
this.loadItemLists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load collectionItemsRD$ with a fixed scope to only obtain the items this collection owns
|
||||||
|
* Load mappedItemsRD$ to only obtain items this collection doesn't own
|
||||||
|
*/
|
||||||
|
loadItemLists() {
|
||||||
|
this.shouldUpdate$ = new BehaviorSubject<boolean>(true);
|
||||||
|
const collectionAndOptions$ = observableCombineLatest(
|
||||||
|
this.collectionRD$,
|
||||||
|
this.searchOptions$,
|
||||||
|
this.shouldUpdate$
|
||||||
|
);
|
||||||
|
this.collectionItemsRD$ = collectionAndOptions$.pipe(
|
||||||
|
switchMap(([collectionRD, options, shouldUpdate]) => {
|
||||||
|
if (shouldUpdate) {
|
||||||
|
return this.collectionDataService.getMappedItems(collectionRD.payload.id, Object.assign(options, {
|
||||||
|
sort: this.defaultSortOptions
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
this.mappedItemsRD$ = collectionAndOptions$.pipe(
|
||||||
|
switchMap(([collectionRD, options, shouldUpdate]) => {
|
||||||
|
if (shouldUpdate) {
|
||||||
|
return this.searchService.search(Object.assign(new PaginatedSearchOptions(options), {
|
||||||
|
query: this.buildQuery(collectionRD.payload.id, options.query),
|
||||||
|
scope: undefined,
|
||||||
|
dsoType: DSpaceObjectType.ITEM,
|
||||||
|
sort: this.defaultSortOptions
|
||||||
|
}), 10000).pipe(
|
||||||
|
toDSpaceObjectListRD(),
|
||||||
|
startWith(undefined)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map/Unmap the selected items to the collection and display notifications
|
||||||
|
* @param ids The list of item UUID's to map/unmap to the collection
|
||||||
|
* @param remove Whether or not it's supposed to remove mappings
|
||||||
|
*/
|
||||||
|
mapItems(ids: string[], remove?: boolean) {
|
||||||
|
const responses$ = this.collectionRD$.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
map((collectionRD: RemoteData<Collection>) => collectionRD.payload),
|
||||||
|
switchMap((collection: Collection) =>
|
||||||
|
observableCombineLatest(ids.map((id: string) =>
|
||||||
|
remove ? this.itemDataService.removeMappingFromCollection(id, collection.id) : this.itemDataService.mapToCollection(id, collection.self)
|
||||||
|
))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
this.showNotifications(responses$, remove);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display notifications
|
||||||
|
* @param {Observable<RestResponse[]>} responses$ The responses after adding/removing a mapping
|
||||||
|
* @param {boolean} remove Whether or not the goal was to remove mappings
|
||||||
|
*/
|
||||||
|
private showNotifications(responses$: Observable<RestResponse[]>, remove?: boolean) {
|
||||||
|
const messageInsertion = remove ? 'unmap' : 'map';
|
||||||
|
|
||||||
|
responses$.subscribe((responses: RestResponse[]) => {
|
||||||
|
const successful = responses.filter((response: RestResponse) => response.isSuccessful);
|
||||||
|
const unsuccessful = responses.filter((response: RestResponse) => !response.isSuccessful);
|
||||||
|
if (successful.length > 0) {
|
||||||
|
const successMessages = observableCombineLatest(
|
||||||
|
this.translateService.get(`collection.edit.item-mapper.notifications.${messageInsertion}.success.head`),
|
||||||
|
this.translateService.get(`collection.edit.item-mapper.notifications.${messageInsertion}.success.content`, { amount: successful.length })
|
||||||
|
);
|
||||||
|
|
||||||
|
successMessages.subscribe(([head, content]) => {
|
||||||
|
this.notificationsService.success(head, content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (unsuccessful.length > 0) {
|
||||||
|
const unsuccessMessages = observableCombineLatest(
|
||||||
|
this.translateService.get(`collection.edit.item-mapper.notifications.${messageInsertion}.error.head`),
|
||||||
|
this.translateService.get(`collection.edit.item-mapper.notifications.${messageInsertion}.error.content`, { amount: unsuccessful.length })
|
||||||
|
);
|
||||||
|
|
||||||
|
unsuccessMessages.subscribe(([head, content]) => {
|
||||||
|
this.notificationsService.error(head, content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Force an update on all lists and switch back to the first tab
|
||||||
|
this.shouldUpdate$.next(true);
|
||||||
|
this.switchToFirstTab();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear url parameters on tab change (temporary fix until pagination is improved)
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
tabChange(event) {
|
||||||
|
this.performedSearch = false;
|
||||||
|
this.router.navigateByUrl(this.getCurrentUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current url without parameters
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getCurrentUrl(): string {
|
||||||
|
if (this.router.url.indexOf('?') > -1) {
|
||||||
|
return this.router.url.substring(0, this.router.url.indexOf('?'));
|
||||||
|
}
|
||||||
|
return this.router.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a query where items that are already mapped to a collection are excluded from
|
||||||
|
* @param collectionId The collection's UUID
|
||||||
|
* @param query The query to add to it
|
||||||
|
*/
|
||||||
|
buildQuery(collectionId: string, query: string): string {
|
||||||
|
const excludeColQuery = `-location.coll:\"${collectionId}\"`;
|
||||||
|
if (isNotEmpty(query)) {
|
||||||
|
return `${excludeColQuery} AND ${query}`;
|
||||||
|
} else {
|
||||||
|
return excludeColQuery;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch the view to focus on the first tab
|
||||||
|
*/
|
||||||
|
switchToFirstTab() {
|
||||||
|
this.tabs.select('browseTab');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a cancel event is fired, return to the collection page
|
||||||
|
*/
|
||||||
|
onCancel() {
|
||||||
|
this.collectionRD$.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
take(1)
|
||||||
|
).subscribe((collection: Collection) => {
|
||||||
|
this.router.navigate(['/collections/', collection.id])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -9,6 +9,7 @@ import { CreateCollectionPageGuard } from './create-collection-page/create-colle
|
|||||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||||
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
import { URLCombiner } from '../core/url-combiner/url-combiner';
|
||||||
import { getCollectionModulePath } from '../app-routing.module';
|
import { getCollectionModulePath } from '../app-routing.module';
|
||||||
|
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
||||||
|
|
||||||
export const COLLECTION_PARENT_PARAMETER = 'parent';
|
export const COLLECTION_PARENT_PARAMETER = 'parent';
|
||||||
|
|
||||||
@@ -56,6 +57,15 @@ const COLLECTION_EDIT_PATH = ':id/edit';
|
|||||||
resolve: {
|
resolve: {
|
||||||
collection: CollectionPageResolver
|
collection: CollectionPageResolver
|
||||||
}
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
path: ':id/edit/mapper',
|
||||||
|
component: CollectionItemMapperComponent,
|
||||||
|
pathMatch: 'full',
|
||||||
|
resolve: {
|
||||||
|
collection: CollectionPageResolver
|
||||||
|
},
|
||||||
|
canActivate: [AuthenticatedGuard]
|
||||||
}
|
}
|
||||||
])
|
])
|
||||||
],
|
],
|
||||||
|
@@ -3,18 +3,22 @@
|
|||||||
*ngVar="(collectionRD$ | async) as collectionRD">
|
*ngVar="(collectionRD$ | async) as collectionRD">
|
||||||
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
|
<div *ngIf="collectionRD?.hasSucceeded" @fadeInOut>
|
||||||
<div *ngIf="collectionRD?.payload as collection">
|
<div *ngIf="collectionRD?.payload as collection">
|
||||||
|
<header class="comcol-header border-bottom mb-4 pb-4">
|
||||||
|
<!-- Collection logo -->
|
||||||
|
<ds-comcol-page-logo *ngIf="logoRD$"
|
||||||
|
[logo]="(logoRD$ | async)?.payload" [alternateText]="'Collection Logo'">
|
||||||
|
[alternateText]="'Collection Logo'">
|
||||||
|
</ds-comcol-page-logo>
|
||||||
<!-- Collection Name -->
|
<!-- Collection Name -->
|
||||||
<ds-comcol-page-header
|
<ds-comcol-page-header
|
||||||
[name]="collection.name">
|
[name]="collection.name">
|
||||||
</ds-comcol-page-header>
|
</ds-comcol-page-header>
|
||||||
<!-- Browse-By Links -->
|
<!-- Handle -->
|
||||||
<ds-comcol-page-browse-by [id]="collection.id"></ds-comcol-page-browse-by>
|
<ds-comcol-page-handle
|
||||||
<!-- Collection logo -->
|
[content]="collection.handle"
|
||||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
[title]="'collection.page.handle'" >
|
||||||
[logo]="(logoRD$ | async)?.payload"
|
</ds-comcol-page-handle>
|
||||||
[alternateText]="'Collection Logo'">
|
<!-- Introductory text -->
|
||||||
</ds-comcol-page-logo>
|
|
||||||
<!-- Introductionary text -->
|
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="collection.introductoryText"
|
[content]="collection.introductoryText"
|
||||||
[hasInnerHtml]="true">
|
[hasInnerHtml]="true">
|
||||||
@@ -23,23 +27,20 @@
|
|||||||
<ds-comcol-page-content
|
<ds-comcol-page-content
|
||||||
[content]="collection.sidebarText"
|
[content]="collection.sidebarText"
|
||||||
[hasInnerHtml]="true"
|
[hasInnerHtml]="true"
|
||||||
[title]="'community.page.news'">
|
[title]="'collection.page.news'">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
<!-- Copyright -->
|
|
||||||
<ds-comcol-page-content
|
</header>
|
||||||
[content]="collection.copyrightText"
|
<section class="comcol-page-browse-section">
|
||||||
[hasInnerHtml]="true">
|
<!-- Browse-By Links -->
|
||||||
</ds-comcol-page-content>
|
<ds-comcol-page-browse-by
|
||||||
<!-- Licence -->
|
[id]="collection.id"
|
||||||
<ds-comcol-page-content
|
[contentType]="collection.type">
|
||||||
[content]="collection.dcLicense"
|
</ds-comcol-page-browse-by>
|
||||||
[title]="'collection.page.license'">
|
|
||||||
</ds-comcol-page-content>
|
|
||||||
</div>
|
|
||||||
<br>
|
|
||||||
<ng-container *ngVar="(itemRD$ | async) as itemRD">
|
<ng-container *ngVar="(itemRD$ | async) as itemRD">
|
||||||
<div *ngIf="itemRD?.hasSucceeded" @fadeIn>
|
<div class="mt-4" *ngIf="itemRD?.hasSucceeded" @fadeIn>
|
||||||
<h2>{{'collection.page.browse.recent.head' | translate}}</h2>
|
<h3 class="sr-only">{{'collection.page.browse.recent.head' | translate}}</h3>
|
||||||
<ds-viewable-collection
|
<ds-viewable-collection
|
||||||
[config]="paginationConfig"
|
[config]="paginationConfig"
|
||||||
[sortConfig]="sortConfig"
|
[sortConfig]="sortConfig"
|
||||||
@@ -56,6 +57,15 @@
|
|||||||
{{'collection.page.browse.recent.empty' | translate}}
|
{{'collection.page.browse.recent.empty' | translate}}
|
||||||
</div>
|
</div>
|
||||||
</ng-container>
|
</ng-container>
|
||||||
|
</section>
|
||||||
|
<footer *ngIf="collection.copyrightText" class="border-top my-5 pt-4">
|
||||||
|
<!-- Copyright -->
|
||||||
|
<ds-comcol-page-content
|
||||||
|
[content]="collection.copyrightText"
|
||||||
|
[hasInnerHtml]="true">
|
||||||
|
</ds-comcol-page-content>
|
||||||
|
</footer>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<ds-error *ngIf="collectionRD?.hasFailed"
|
<ds-error *ngIf="collectionRD?.hasFailed"
|
||||||
message="{{'error.collection' | translate}}"></ds-error>
|
message="{{'error.collection' | translate}}"></ds-error>
|
||||||
|
@@ -9,6 +9,8 @@ import { CreateCollectionPageComponent } from './create-collection-page/create-c
|
|||||||
import { CollectionFormComponent } from './collection-form/collection-form.component';
|
import { CollectionFormComponent } from './collection-form/collection-form.component';
|
||||||
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
import { DeleteCollectionPageComponent } from './delete-collection-page/delete-collection-page.component';
|
||||||
import { SearchService } from '../+search-page/search-service/search.service';
|
import { SearchService } from '../+search-page/search-service/search.service';
|
||||||
|
import { CollectionItemMapperComponent } from './collection-item-mapper/collection-item-mapper.component';
|
||||||
|
import { SearchFixedFilterService } from '../+search-page/search-filters/search-filter/search-fixed-filter.service';
|
||||||
|
|
||||||
@NgModule({
|
@NgModule({
|
||||||
imports: [
|
imports: [
|
||||||
@@ -20,13 +22,15 @@ import { SearchService } from '../+search-page/search-service/search.service';
|
|||||||
CollectionPageComponent,
|
CollectionPageComponent,
|
||||||
CreateCollectionPageComponent,
|
CreateCollectionPageComponent,
|
||||||
DeleteCollectionPageComponent,
|
DeleteCollectionPageComponent,
|
||||||
CollectionFormComponent
|
CollectionFormComponent,
|
||||||
|
CollectionItemMapperComponent
|
||||||
],
|
],
|
||||||
exports: [
|
exports: [
|
||||||
CollectionFormComponent
|
CollectionFormComponent
|
||||||
],
|
],
|
||||||
providers: [
|
providers: [
|
||||||
SearchService
|
SearchService,
|
||||||
|
SearchFixedFilterService
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
export class CollectionPageModule {
|
export class CollectionPageModule {
|
||||||
|
@@ -1,33 +1,38 @@
|
|||||||
<div class="container" *ngVar="(communityRD$ | async) as communityRD">
|
<div class="container" *ngVar="(communityRD$ | async) as communityRD">
|
||||||
<div class="community-page" *ngIf="communityRD?.hasSucceeded" @fadeInOut>
|
<div class="community-page" *ngIf="communityRD?.hasSucceeded" @fadeInOut>
|
||||||
<div *ngIf="communityRD?.payload; let communityPayload">
|
<div *ngIf="communityRD?.payload; let communityPayload">
|
||||||
|
<header class="comcol-header border-bottom mb-4 pb-4">
|
||||||
|
<!-- Community logo -->
|
||||||
|
<ds-comcol-page-logo *ngIf="logoRD$" [logo]="(logoRD$ | async)?.payload" [alternateText]="'Community Logo'">
|
||||||
|
</ds-comcol-page-logo>
|
||||||
|
|
||||||
<!-- Community name -->
|
<!-- Community name -->
|
||||||
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
|
<ds-comcol-page-header [name]="communityPayload.name"></ds-comcol-page-header>
|
||||||
<!-- Browse-By Links -->
|
<!-- Handle -->
|
||||||
<ds-comcol-page-browse-by [id]="communityPayload.id"></ds-comcol-page-browse-by>
|
<ds-comcol-page-handle [content]="communityPayload.handle" [title]="'community.page.handle'">
|
||||||
<!-- Community logo -->
|
</ds-comcol-page-handle>
|
||||||
<ds-comcol-page-logo *ngIf="logoRD$"
|
|
||||||
[logo]="(logoRD$ | async)?.payload"
|
|
||||||
[alternateText]="'Community Logo'">
|
|
||||||
</ds-comcol-page-logo>
|
|
||||||
<!-- Introductory text -->
|
<!-- Introductory text -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content [content]="communityPayload.introductoryText" [hasInnerHtml]="true">
|
||||||
[content]="communityPayload.introductoryText"
|
|
||||||
[hasInnerHtml]="true">
|
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
<!-- News -->
|
<!-- News -->
|
||||||
<ds-comcol-page-content
|
<ds-comcol-page-content [content]="communityPayload.sidebarText" [hasInnerHtml]="true"
|
||||||
[content]="communityPayload.sidebarText"
|
|
||||||
[hasInnerHtml]="true"
|
|
||||||
[title]="'community.page.news'">
|
[title]="'community.page.news'">
|
||||||
</ds-comcol-page-content>
|
</ds-comcol-page-content>
|
||||||
<!-- Copyright -->
|
|
||||||
<ds-comcol-page-content
|
</header>
|
||||||
[content]="communityPayload.copyrightText"
|
<section class="comcol-page-browse-section">
|
||||||
[hasInnerHtml]="true">
|
<!-- Browse-By Links -->
|
||||||
</ds-comcol-page-content>
|
<ds-comcol-page-browse-by [id]="communityPayload.id" [contentType]="communityPayload.type">
|
||||||
|
</ds-comcol-page-browse-by>
|
||||||
|
|
||||||
<ds-community-page-sub-community-list [community]="communityPayload"></ds-community-page-sub-community-list>
|
<ds-community-page-sub-community-list [community]="communityPayload"></ds-community-page-sub-community-list>
|
||||||
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
|
<ds-community-page-sub-collection-list [community]="communityPayload"></ds-community-page-sub-collection-list>
|
||||||
|
</section>
|
||||||
|
<footer *ngIf="communityPayload.copyrightText" class="border-top my-5 pt-4">
|
||||||
|
<!-- Copyright -->
|
||||||
|
<ds-comcol-page-content [content]="communityPayload.copyrightText" [hasInnerHtml]="true">
|
||||||
|
</ds-comcol-page-content>
|
||||||
|
</footer>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
@@ -15,6 +15,8 @@ import { ItemDeleteComponent } from './item-delete/item-delete.component';
|
|||||||
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
||||||
import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component';
|
import { EditInPlaceFieldComponent } from './item-metadata/edit-in-place-field/edit-in-place-field.component';
|
||||||
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
|
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
|
||||||
|
import { SearchPageModule } from '../../+search-page/search-page.module';
|
||||||
|
import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component';
|
||||||
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
||||||
import { EditRelationshipComponent } from './item-relationships/edit-relationship/edit-relationship.component';
|
import { EditRelationshipComponent } from './item-relationships/edit-relationship/edit-relationship.component';
|
||||||
import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
|
import { EditRelationshipListComponent } from './item-relationships/edit-relationship-list/edit-relationship-list.component';
|
||||||
@@ -27,7 +29,8 @@ import { ItemMoveComponent } from './item-move/item-move.component';
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
EditItemPageRoutingModule
|
EditItemPageRoutingModule,
|
||||||
|
SearchPageModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
EditItemPageComponent,
|
EditItemPageComponent,
|
||||||
@@ -46,6 +49,7 @@ import { ItemMoveComponent } from './item-move/item-move.component';
|
|||||||
EditInPlaceFieldComponent,
|
EditInPlaceFieldComponent,
|
||||||
EditRelationshipComponent,
|
EditRelationshipComponent,
|
||||||
EditRelationshipListComponent,
|
EditRelationshipListComponent,
|
||||||
|
ItemCollectionMapperComponent,
|
||||||
ItemMoveComponent,
|
ItemMoveComponent,
|
||||||
]
|
]
|
||||||
})
|
})
|
||||||
|
@@ -10,6 +10,7 @@ import { ItemDeleteComponent } from './item-delete/item-delete.component';
|
|||||||
import { ItemStatusComponent } from './item-status/item-status.component';
|
import { ItemStatusComponent } from './item-status/item-status.component';
|
||||||
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
import { ItemMetadataComponent } from './item-metadata/item-metadata.component';
|
||||||
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
|
import { ItemBitstreamsComponent } from './item-bitstreams/item-bitstreams.component';
|
||||||
|
import { ItemCollectionMapperComponent } from './item-collection-mapper/item-collection-mapper.component';
|
||||||
import { ItemMoveComponent } from './item-move/item-move.component';
|
import { ItemMoveComponent } from './item-move/item-move.component';
|
||||||
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
import { ItemRelationshipsComponent } from './item-relationships/item-relationships.component';
|
||||||
|
|
||||||
@@ -72,6 +73,13 @@ const ITEM_EDIT_MOVE_PATH = 'move';
|
|||||||
},
|
},
|
||||||
]
|
]
|
||||||
},
|
},
|
||||||
|
{
|
||||||
|
path: 'mapper',
|
||||||
|
component: ItemCollectionMapperComponent,
|
||||||
|
resolve: {
|
||||||
|
item: ItemPageResolver
|
||||||
|
}
|
||||||
|
},
|
||||||
{
|
{
|
||||||
path: ITEM_EDIT_WITHDRAW_PATH,
|
path: ITEM_EDIT_WITHDRAW_PATH,
|
||||||
component: ItemWithdrawComponent,
|
component: ItemWithdrawComponent,
|
||||||
|
@@ -0,0 +1,56 @@
|
|||||||
|
<div class="container">
|
||||||
|
<div class="row">
|
||||||
|
<div class="col-12">
|
||||||
|
<h2>{{'item.edit.item-mapper.head' | translate}}</h2>
|
||||||
|
<p [innerHTML]="'item.edit.item-mapper.item' | translate:{ name: (itemRD$ | async)?.payload?.name }" id="item-name"></p>
|
||||||
|
<p>{{'item.edit.item-mapper.description' | translate}}</p>
|
||||||
|
|
||||||
|
<ngb-tabset (tabChange)="tabChange($event)" [destroyOnHide]="true" #tabs="ngbTabset">
|
||||||
|
<ngb-tab title="{{'item.edit.item-mapper.tabs.browse' | translate}}" id="browseTab">
|
||||||
|
<ng-template ngbTabContent>
|
||||||
|
<div class="mt-2">
|
||||||
|
<ds-collection-select class="mt-2"
|
||||||
|
[key]="'browse'"
|
||||||
|
[dsoRD$]="itemCollectionsRD$"
|
||||||
|
[paginationOptions]="(searchOptions$ | async)?.pagination"
|
||||||
|
[confirmButton]="'item.edit.item-mapper.buttons.remove'"
|
||||||
|
[cancelButton]="'item.edit.item-mapper.cancel'"
|
||||||
|
[dangerConfirm]="true"
|
||||||
|
(confirm)="removeMappings($event)"
|
||||||
|
(cancel)="onCancel()"></ds-collection-select>
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ngb-tab>
|
||||||
|
<ngb-tab title="{{'item.edit.item-mapper.tabs.map' | translate}}" id="mapTab">
|
||||||
|
<ng-template ngbTabContent>
|
||||||
|
<div class="row mt-2">
|
||||||
|
<div class="col-12 col-lg-6">
|
||||||
|
<ds-search-form id="search-form"
|
||||||
|
[query]="(searchOptions$ | async)?.query"
|
||||||
|
[currentUrl]="'./'"
|
||||||
|
[inPlaceSearch]="true"
|
||||||
|
(submitSearch)="performedSearch = true">
|
||||||
|
</ds-search-form>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div *ngIf="performedSearch">
|
||||||
|
<ds-collection-select class="mt-2"
|
||||||
|
[key]="'map'"
|
||||||
|
[dsoRD$]="mappedCollectionsRD$"
|
||||||
|
[paginationOptions]="(searchOptions$ | async)?.pagination"
|
||||||
|
[sortOptions]="(searchOptions$ | async)?.sort"
|
||||||
|
[confirmButton]="'item.edit.item-mapper.buttons.add'"
|
||||||
|
[cancelButton]="'item.edit.item-mapper.cancel'"
|
||||||
|
(confirm)="mapCollections($event)"
|
||||||
|
(cancel)="onCancel()"></ds-collection-select>
|
||||||
|
</div>
|
||||||
|
<div *ngIf="!performedSearch" class="alert alert-info w-100" role="alert">
|
||||||
|
{{'item.edit.item-mapper.no-search' | translate}}
|
||||||
|
</div>
|
||||||
|
</ng-template>
|
||||||
|
</ngb-tab>
|
||||||
|
</ngb-tabset>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
@@ -0,0 +1,207 @@
|
|||||||
|
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { NgbModule } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { ItemCollectionMapperComponent } from './item-collection-mapper.component';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service';
|
||||||
|
import { SearchService } from '../../../+search-page/search-service/search.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model';
|
||||||
|
import { PaginationComponentOptions } from '../../../shared/pagination/pagination-component-options.model';
|
||||||
|
import { SortDirection, SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
|
import { RouterStub } from '../../../shared/testing/router-stub';
|
||||||
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router-stub';
|
||||||
|
import { EventEmitter } from '@angular/core';
|
||||||
|
import { SearchServiceStub } from '../../../shared/testing/search-service-stub';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
|
import { FormsModule } from '@angular/forms';
|
||||||
|
import { SharedModule } from '../../../shared/shared.module';
|
||||||
|
import { TranslateModule, TranslateService } from '@ngx-translate/core';
|
||||||
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service-stub';
|
||||||
|
import { HostWindowService } from '../../../shared/host-window.service';
|
||||||
|
import { HostWindowServiceStub } from '../../../shared/testing/host-window-service-stub';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { ObjectSelectService } from '../../../shared/object-select/object-select.service';
|
||||||
|
import { ObjectSelectServiceStub } from '../../../shared/testing/object-select-service-stub';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { of } from 'rxjs/internal/observable/of';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
import { CollectionSelectComponent } from '../../../shared/object-select/collection-select/collection-select.component';
|
||||||
|
import { PaginationComponent } from '../../../shared/pagination/pagination.component';
|
||||||
|
import { EnumKeysPipe } from '../../../shared/utils/enum-keys-pipe';
|
||||||
|
import { VarDirective } from '../../../shared/utils/var.directive';
|
||||||
|
import { SearchFormComponent } from '../../../shared/search-form/search-form.component';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
import { ErrorComponent } from '../../../shared/error/error.component';
|
||||||
|
import { LoadingComponent } from '../../../shared/loading/loading.component';
|
||||||
|
|
||||||
|
describe('ItemCollectionMapperComponent', () => {
|
||||||
|
let comp: ItemCollectionMapperComponent;
|
||||||
|
let fixture: ComponentFixture<ItemCollectionMapperComponent>;
|
||||||
|
|
||||||
|
let route: ActivatedRoute;
|
||||||
|
let router: Router;
|
||||||
|
let searchConfigService: SearchConfigurationService;
|
||||||
|
let searchService: SearchService;
|
||||||
|
let notificationsService: NotificationsService;
|
||||||
|
let itemDataService: ItemDataService;
|
||||||
|
|
||||||
|
const mockCollection = Object.assign(new Collection(), { id: 'collection1' });
|
||||||
|
const mockItem: Item = Object.assign(new Item(), {
|
||||||
|
id: '932c7d50-d85a-44cb-b9dc-b427b12877bd',
|
||||||
|
name: 'test-item'
|
||||||
|
});
|
||||||
|
const mockItemRD: RemoteData<Item> = new RemoteData<Item>(false, false, true, null, mockItem);
|
||||||
|
const mockSearchOptions = of(new PaginatedSearchOptions({
|
||||||
|
pagination: Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'search-page-configuration',
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1
|
||||||
|
}),
|
||||||
|
sort: new SortOptions('dc.title', SortDirection.ASC)
|
||||||
|
}));
|
||||||
|
const url = 'http://test.url';
|
||||||
|
const urlWithParam = url + '?param=value';
|
||||||
|
const routerStub = Object.assign(new RouterStub(), {
|
||||||
|
url: urlWithParam,
|
||||||
|
navigateByUrl: {},
|
||||||
|
navigate: {}
|
||||||
|
});
|
||||||
|
const searchConfigServiceStub = {
|
||||||
|
paginatedSearchOptions: mockSearchOptions
|
||||||
|
};
|
||||||
|
const mockCollectionsRD = new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), []));
|
||||||
|
const itemDataServiceStub = {
|
||||||
|
mapToCollection: () => of(new RestResponse(true, 200, 'OK')),
|
||||||
|
removeMappingFromCollection: () => of(new RestResponse(true, 200, 'OK')),
|
||||||
|
getMappedCollections: () => of(mockCollectionsRD),
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
clearMappedCollectionsRequests: () => {}
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
};
|
||||||
|
const searchServiceStub = Object.assign(new SearchServiceStub(), {
|
||||||
|
search: () => of(mockCollectionsRD),
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
clearDiscoveryRequests: () => {}
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
});
|
||||||
|
const activatedRouteStub = new ActivatedRouteStub({}, { item: mockItemRD });
|
||||||
|
const translateServiceStub = {
|
||||||
|
get: () => of('test-message of item ' + mockItem.name),
|
||||||
|
onLangChange: new EventEmitter(),
|
||||||
|
onTranslationChange: new EventEmitter(),
|
||||||
|
onDefaultLangChange: new EventEmitter()
|
||||||
|
};
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [CommonModule, FormsModule, RouterTestingModule.withRoutes([]), TranslateModule.forRoot(), NgbModule.forRoot()],
|
||||||
|
declarations: [ItemCollectionMapperComponent, CollectionSelectComponent, SearchFormComponent, PaginationComponent, EnumKeysPipe, VarDirective, ErrorComponent, LoadingComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
|
{ provide: Router, useValue: routerStub },
|
||||||
|
{ provide: SearchConfigurationService, useValue: searchConfigServiceStub },
|
||||||
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
|
{ provide: ItemDataService, useValue: itemDataServiceStub },
|
||||||
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
|
{ provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() },
|
||||||
|
{ provide: TranslateService, useValue: translateServiceStub },
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
|
||||||
|
]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemCollectionMapperComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
fixture.detectChanges();
|
||||||
|
route = (comp as any).route;
|
||||||
|
router = (comp as any).router;
|
||||||
|
searchConfigService = (comp as any).searchConfigService;
|
||||||
|
notificationsService = (comp as any).notificationsService;
|
||||||
|
itemDataService = (comp as any).itemDataService;
|
||||||
|
searchService = (comp as any).searchService;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display the correct collection name', () => {
|
||||||
|
const name: HTMLElement = fixture.debugElement.query(By.css('#item-name')).nativeElement;
|
||||||
|
expect(name.innerHTML).toContain(mockItem.name);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mapCollections', () => {
|
||||||
|
const ids = ['id1', 'id2', 'id3', 'id4'];
|
||||||
|
|
||||||
|
it('should display a success message if at least one mapping was successful', () => {
|
||||||
|
comp.mapCollections(ids);
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
expect(notificationsService.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display an error message if at least one mapping was unsuccessful', () => {
|
||||||
|
spyOn(itemDataService, 'mapToCollection').and.returnValue(of(new RestResponse(false, 404, 'Not Found')));
|
||||||
|
comp.mapCollections(ids);
|
||||||
|
expect(notificationsService.success).not.toHaveBeenCalled();
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('removeMappings', () => {
|
||||||
|
const ids = ['id1', 'id2', 'id3', 'id4'];
|
||||||
|
|
||||||
|
it('should display a success message if the removal of at least one mapping was successful', () => {
|
||||||
|
comp.removeMappings(ids);
|
||||||
|
expect(notificationsService.success).toHaveBeenCalled();
|
||||||
|
expect(notificationsService.error).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display an error message if the removal of at least one mapping was unsuccessful', () => {
|
||||||
|
spyOn(itemDataService, 'removeMappingFromCollection').and.returnValue(of(new RestResponse(false, 404, 'Not Found')));
|
||||||
|
comp.removeMappings(ids);
|
||||||
|
expect(notificationsService.success).not.toHaveBeenCalled();
|
||||||
|
expect(notificationsService.error).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('tabChange', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routerStub, 'navigateByUrl');
|
||||||
|
comp.tabChange({});
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to the same page to remove parameters', () => {
|
||||||
|
expect(router.navigateByUrl).toHaveBeenCalledWith(url);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('buildQuery', () => {
|
||||||
|
const query = 'query';
|
||||||
|
const expected = `${query} AND -search.resourceid:${mockCollection.id}`;
|
||||||
|
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
result = comp.buildQuery([mockCollection], query);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should build a solr query to exclude the provided collection', () => {
|
||||||
|
expect(result).toEqual(expected);
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('onCancel', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
spyOn(routerStub, 'navigate');
|
||||||
|
comp.onCancel();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should navigate to the item page', () => {
|
||||||
|
expect(router.navigate).toHaveBeenCalledWith(['/items/', mockItem.id]);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,283 @@
|
|||||||
|
import { combineLatest as observableCombineLatest, Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { ChangeDetectionStrategy, Component, OnInit, ViewChild } from '@angular/core';
|
||||||
|
import { fadeIn, fadeInOut } from '../../../shared/animations/fade';
|
||||||
|
import { PaginatedSearchOptions } from '../../../+search-page/paginated-search-options.model';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { getRemoteDataPayload, getSucceededRemoteData, toDSpaceObjectListRD } from '../../../core/shared/operators';
|
||||||
|
import { ActivatedRoute, Router } from '@angular/router';
|
||||||
|
import { SearchService } from '../../../+search-page/search-service/search.service';
|
||||||
|
import { SearchConfigurationService } from '../../../+search-page/search-service/search-configuration.service';
|
||||||
|
import { map, startWith, switchMap, take } from 'rxjs/operators';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { DSpaceObjectType } from '../../../core/shared/dspace-object-type.model';
|
||||||
|
import { isNotEmpty } from '../../../shared/empty.util';
|
||||||
|
import { RestResponse } from '../../../core/cache/response.models';
|
||||||
|
import { BehaviorSubject } from 'rxjs/internal/BehaviorSubject';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-collection-mapper',
|
||||||
|
styleUrls: ['./item-collection-mapper.component.scss'],
|
||||||
|
templateUrl: './item-collection-mapper.component.html',
|
||||||
|
changeDetection: ChangeDetectionStrategy.OnPush,
|
||||||
|
animations: [
|
||||||
|
fadeIn,
|
||||||
|
fadeInOut
|
||||||
|
]
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Component for mapping collections to an item
|
||||||
|
*/
|
||||||
|
export class ItemCollectionMapperComponent implements OnInit {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A view on the tabset element
|
||||||
|
* Used to switch tabs programmatically
|
||||||
|
*/
|
||||||
|
@ViewChild('tabs') tabs;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The item to map to collections
|
||||||
|
*/
|
||||||
|
itemRD$: Observable<RemoteData<Item>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search options
|
||||||
|
*/
|
||||||
|
searchOptions$: Observable<PaginatedSearchOptions>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of collections to show under the "Browse" tab
|
||||||
|
* Collections that are mapped to the item
|
||||||
|
*/
|
||||||
|
itemCollectionsRD$: Observable<RemoteData<PaginatedList<Collection>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* List of collections to show under the "Map" tab
|
||||||
|
* Collections that are not mapped to the item
|
||||||
|
*/
|
||||||
|
mappedCollectionsRD$: Observable<RemoteData<PaginatedList<Collection>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Firing this observable (shouldUpdate$.next(true)) forces the two lists to reload themselves
|
||||||
|
* Usually fired after the lists their cache is cleared (to force a new request to the REST API)
|
||||||
|
*/
|
||||||
|
shouldUpdate$: BehaviorSubject<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Track whether at least one search has been performed or not
|
||||||
|
* As soon as at least one search has been performed, we display the search results
|
||||||
|
*/
|
||||||
|
performedSearch = false;
|
||||||
|
|
||||||
|
constructor(private route: ActivatedRoute,
|
||||||
|
private router: Router,
|
||||||
|
private searchConfigService: SearchConfigurationService,
|
||||||
|
private searchService: SearchService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private itemDataService: ItemDataService,
|
||||||
|
private translateService: TranslateService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.itemRD$ = this.route.data.pipe(map((data) => data.item)).pipe(getSucceededRemoteData()) as Observable<RemoteData<Item>>;
|
||||||
|
this.searchOptions$ = this.searchConfigService.paginatedSearchOptions;
|
||||||
|
this.loadCollectionLists();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Load itemCollectionsRD$ with a fixed scope to only obtain the collections that own this item
|
||||||
|
* Load mappedCollectionsRD$ to only obtain collections that don't own this item
|
||||||
|
*/
|
||||||
|
loadCollectionLists() {
|
||||||
|
this.shouldUpdate$ = new BehaviorSubject<boolean>(true);
|
||||||
|
this.itemCollectionsRD$ = observableCombineLatest(this.itemRD$, this.shouldUpdate$).pipe(
|
||||||
|
map(([itemRD, shouldUpdate]) => {
|
||||||
|
if (shouldUpdate) {
|
||||||
|
return itemRD.payload
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
switchMap((item: Item) => this.itemDataService.getMappedCollections(item.id))
|
||||||
|
);
|
||||||
|
|
||||||
|
const owningCollectionRD$ = this.itemRD$.pipe(
|
||||||
|
switchMap((itemRD: RemoteData<Item>) => itemRD.payload.owningCollection)
|
||||||
|
);
|
||||||
|
const itemCollectionsAndOptions$ = observableCombineLatest(
|
||||||
|
this.itemCollectionsRD$,
|
||||||
|
owningCollectionRD$,
|
||||||
|
this.searchOptions$
|
||||||
|
);
|
||||||
|
this.mappedCollectionsRD$ = itemCollectionsAndOptions$.pipe(
|
||||||
|
switchMap(([itemCollectionsRD, owningCollectionRD, searchOptions]) => {
|
||||||
|
return this.searchService.search(Object.assign(new PaginatedSearchOptions(searchOptions), {
|
||||||
|
query: this.buildQuery([...itemCollectionsRD.payload.page, owningCollectionRD.payload], searchOptions.query),
|
||||||
|
dsoType: DSpaceObjectType.COLLECTION
|
||||||
|
}), 10000).pipe(
|
||||||
|
toDSpaceObjectListRD(),
|
||||||
|
startWith(undefined)
|
||||||
|
);
|
||||||
|
})
|
||||||
|
) as Observable<RemoteData<PaginatedList<Collection>>>;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Map the item to the selected collections and display notifications
|
||||||
|
* @param {string[]} ids The list of collection UUID's to map the item to
|
||||||
|
*/
|
||||||
|
mapCollections(ids: string[]) {
|
||||||
|
const itemIdAndExcludingIds$ = observableCombineLatest(
|
||||||
|
this.itemRD$.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
take(1),
|
||||||
|
map((rd: RemoteData<Item>) => rd.payload),
|
||||||
|
map((item: Item) => item.id)
|
||||||
|
),
|
||||||
|
this.itemCollectionsRD$.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
take(1),
|
||||||
|
map((rd: RemoteData<PaginatedList<Collection>>) => rd.payload.page),
|
||||||
|
map((collections: Collection[]) => collections.map((collection: Collection) => collection.id))
|
||||||
|
)
|
||||||
|
);
|
||||||
|
|
||||||
|
// Map the item to the collections found in ids, excluding the collections the item is already mapped to
|
||||||
|
const responses$ = itemIdAndExcludingIds$.pipe(
|
||||||
|
switchMap(([itemId, excludingIds]) => observableCombineLatest(this.filterIds(ids, excludingIds).map((id: string) => this.itemDataService.mapToCollection(itemId, id))))
|
||||||
|
);
|
||||||
|
|
||||||
|
this.showNotifications(responses$, 'item.edit.item-mapper.notifications.add');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Remove the mapping of the item to the selected collections and display notifications
|
||||||
|
* @param {string[]} ids The list of collection UUID's to remove the mapping of the item for
|
||||||
|
*/
|
||||||
|
removeMappings(ids: string[]) {
|
||||||
|
const responses$ = this.itemRD$.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
map((itemRD: RemoteData<Item>) => itemRD.payload.id),
|
||||||
|
switchMap((itemId: string) => observableCombineLatest(ids.map((id: string) => this.itemDataService.removeMappingFromCollection(itemId, id))))
|
||||||
|
);
|
||||||
|
|
||||||
|
this.showNotifications(responses$, 'item.edit.item-mapper.notifications.remove');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Filters ids from a given list of ids, which exist in a second given list of ids
|
||||||
|
* @param {string[]} ids The list of ids to filter out of
|
||||||
|
* @param {string[]} excluding The ids that should be excluded from the first list
|
||||||
|
* @returns {string[]}
|
||||||
|
*/
|
||||||
|
private filterIds(ids: string[], excluding: string[]): string[] {
|
||||||
|
return ids.filter((id: string) => excluding.indexOf(id) < 0);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Display notifications
|
||||||
|
* @param {Observable<RestResponse[]>} responses$ The responses after adding/removing a mapping
|
||||||
|
* @param {string} messagePrefix The prefix to build the notification messages with
|
||||||
|
*/
|
||||||
|
private showNotifications(responses$: Observable<RestResponse[]>, messagePrefix: string) {
|
||||||
|
responses$.subscribe((responses: RestResponse[]) => {
|
||||||
|
const successful = responses.filter((response: RestResponse) => response.isSuccessful);
|
||||||
|
const unsuccessful = responses.filter((response: RestResponse) => !response.isSuccessful);
|
||||||
|
if (successful.length > 0) {
|
||||||
|
const successMessages = observableCombineLatest(
|
||||||
|
this.translateService.get(`${messagePrefix}.success.head`),
|
||||||
|
this.translateService.get(`${messagePrefix}.success.content`, { amount: successful.length })
|
||||||
|
);
|
||||||
|
|
||||||
|
successMessages.subscribe(([head, content]) => {
|
||||||
|
this.notificationsService.success(head, content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (unsuccessful.length > 0) {
|
||||||
|
const unsuccessMessages = observableCombineLatest(
|
||||||
|
this.translateService.get(`${messagePrefix}.error.head`),
|
||||||
|
this.translateService.get(`${messagePrefix}.error.content`, { amount: unsuccessful.length })
|
||||||
|
);
|
||||||
|
|
||||||
|
unsuccessMessages.subscribe(([head, content]) => {
|
||||||
|
this.notificationsService.error(head, content);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
// Force an update on all lists and switch back to the first tab
|
||||||
|
this.shouldUpdate$.next(true);
|
||||||
|
this.switchToFirstTab();
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Clear url parameters on tab change (temporary fix until pagination is improved)
|
||||||
|
* @param event
|
||||||
|
*/
|
||||||
|
tabChange(event) {
|
||||||
|
this.performedSearch = false;
|
||||||
|
this.router.navigateByUrl(this.getCurrentUrl());
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get current url without parameters
|
||||||
|
* @returns {string}
|
||||||
|
*/
|
||||||
|
getCurrentUrl(): string {
|
||||||
|
if (this.router.url.indexOf('?') > -1) {
|
||||||
|
return this.router.url.substring(0, this.router.url.indexOf('?'));
|
||||||
|
}
|
||||||
|
return this.router.url;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Build a query to exclude collections from
|
||||||
|
* @param collections The collections their UUIDs
|
||||||
|
* @param query The query to add to it
|
||||||
|
*/
|
||||||
|
buildQuery(collections: Collection[], query: string): string {
|
||||||
|
let result = query;
|
||||||
|
for (const collection of collections) {
|
||||||
|
result = this.addExcludeCollection(collection.id, result);
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Add an exclusion of a collection to a query
|
||||||
|
* @param collectionId The collection's UUID
|
||||||
|
* @param query The query to add the exclusion to
|
||||||
|
*/
|
||||||
|
addExcludeCollection(collectionId: string, query: string): string {
|
||||||
|
const excludeQuery = `-search.resourceid:${collectionId}`;
|
||||||
|
if (isNotEmpty(query)) {
|
||||||
|
return `${query} AND ${excludeQuery}`;
|
||||||
|
} else {
|
||||||
|
return excludeQuery;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch the view to focus on the first tab
|
||||||
|
*/
|
||||||
|
switchToFirstTab() {
|
||||||
|
this.tabs.select('browseTab');
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* When a cancel event is fired, return to the item page
|
||||||
|
*/
|
||||||
|
onCancel() {
|
||||||
|
this.itemRD$.pipe(
|
||||||
|
getSucceededRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
take(1)
|
||||||
|
).subscribe((item: Item) => {
|
||||||
|
this.router.navigate(['/items/', item.id])
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -68,6 +68,7 @@ export class ItemStatusComponent implements OnInit {
|
|||||||
The value is supposed to be a href for the button
|
The value is supposed to be a href for the button
|
||||||
*/
|
*/
|
||||||
this.operations = [];
|
this.operations = [];
|
||||||
|
this.operations.push(new ItemOperation('mappedCollections', this.getCurrentUrl(item) + '/mapper'));
|
||||||
if (item.isWithdrawn) {
|
if (item.isWithdrawn) {
|
||||||
this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate'));
|
this.operations.push(new ItemOperation('reinstate', this.getCurrentUrl(item) + '/reinstate'));
|
||||||
} else {
|
} else {
|
||||||
|
@@ -31,8 +31,8 @@ import { MetadataFieldWrapperComponent } from './field-components/metadata-field
|
|||||||
imports: [
|
imports: [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
SharedModule,
|
SharedModule,
|
||||||
EditItemPageModule,
|
|
||||||
ItemPageRoutingModule,
|
ItemPageRoutingModule,
|
||||||
|
EditItemPageModule,
|
||||||
SearchPageModule
|
SearchPageModule
|
||||||
],
|
],
|
||||||
declarations: [
|
declarations: [
|
||||||
|
@@ -117,9 +117,8 @@ export const getRelatedItemsByTypeLabel = (thisId: string, label: string) =>
|
|||||||
* @param parentId The id of the parent item
|
* @param parentId The id of the parent item
|
||||||
* @param itemType The type of relation this list resembles (for creating representations)
|
* @param itemType The type of relation this list resembles (for creating representations)
|
||||||
* @param metadata The list of original Metadatum objects
|
* @param metadata The list of original Metadatum objects
|
||||||
* @param ids The ItemDataService to use for fetching Items from the Rest API
|
|
||||||
*/
|
*/
|
||||||
export const relationsToRepresentations = (parentId: string, itemType: string, metadata: MetadataValue[], ids: ItemDataService) =>
|
export const relationsToRepresentations = (parentId: string, itemType: string, metadata: MetadataValue[]) =>
|
||||||
(source: Observable<Relationship[]>): Observable<MetadataRepresentation[]> =>
|
(source: Observable<Relationship[]>): Observable<MetadataRepresentation[]> =>
|
||||||
source.pipe(
|
source.pipe(
|
||||||
flatMap((rels: Relationship[]) =>
|
flatMap((rels: Relationship[]) =>
|
||||||
@@ -139,7 +138,7 @@ export const relationsToRepresentations = (parentId: string, itemType: string, m
|
|||||||
return leftItem.payload;
|
return leftItem.payload;
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
map((item: Item) => Object.assign(new ItemMetadataRepresentation(), item))
|
map((item: Item) => Object.assign(new ItemMetadataRepresentation(metadatum), item))
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@@ -413,7 +413,7 @@ describe('ItemComponent', () => {
|
|||||||
representations.subscribe((reps: MetadataRepresentation[]) => {
|
representations.subscribe((reps: MetadataRepresentation[]) => {
|
||||||
expect(reps[0].getValue()).toEqual('First value');
|
expect(reps[0].getValue()).toEqual('First value');
|
||||||
expect(reps[1].getValue()).toEqual('Second value');
|
expect(reps[1].getValue()).toEqual('Second value');
|
||||||
expect(reps[2].getValue()).toEqual('related item');
|
expect(reps[2].getValue()).toEqual('Third value');
|
||||||
expect(reps[3].getValue()).toEqual('Fourth value');
|
expect(reps[3].getValue()).toEqual('Fourth value');
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -1,7 +1,6 @@
|
|||||||
import { Component, Inject, OnInit } from '@angular/core';
|
import { Component, Inject, OnInit } from '@angular/core';
|
||||||
import { Observable , zip as observableZip, combineLatest as observableCombineLatest } from 'rxjs';
|
import { Observable , zip as observableZip, combineLatest as observableCombineLatest } from 'rxjs';
|
||||||
import { distinctUntilChanged, filter, flatMap, map } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, flatMap, map } from 'rxjs/operators';
|
||||||
import { ItemDataService } from '../../../../core/data/item-data.service';
|
|
||||||
import { PaginatedList } from '../../../../core/data/paginated-list';
|
import { PaginatedList } from '../../../../core/data/paginated-list';
|
||||||
import { RemoteData } from '../../../../core/data/remote-data';
|
import { RemoteData } from '../../../../core/data/remote-data';
|
||||||
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
import { RelationshipType } from '../../../../core/shared/item-relationships/relationship-type.model';
|
||||||
@@ -10,49 +9,7 @@ import { Item } from '../../../../core/shared/item.model';
|
|||||||
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
import { getRemoteDataPayload, getSucceededRemoteData } from '../../../../core/shared/operators';
|
||||||
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
|
import { ITEM } from '../../../../shared/items/switcher/item-type-switcher.component';
|
||||||
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
|
import { MetadataRepresentation } from '../../../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
import { compareArraysUsingIds, relationsToRepresentations } from './item-relationships-utils';
|
||||||
import { MetadatumRepresentation } from '../../../../core/shared/metadata-representation/metadatum/metadatum-representation.model';
|
|
||||||
import { of } from 'rxjs/internal/observable/of';
|
|
||||||
import { MetadataValue } from '../../../../core/shared/metadata.models';
|
|
||||||
import { compareArraysUsingIds } from './item-relationships-utils';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Operator for turning a list of relationships into a list of metadatarepresentations given the original metadata
|
|
||||||
* @param thisId The id of the parent item
|
|
||||||
* @param itemType The type of relation this list resembles (for creating representations)
|
|
||||||
* @param metadata The list of original Metadatum objects
|
|
||||||
*/
|
|
||||||
export const relationsToRepresentations = (thisId: string, itemType: string, metadata: MetadataValue[]) =>
|
|
||||||
(source: Observable<Relationship[]>): Observable<MetadataRepresentation[]> =>
|
|
||||||
source.pipe(
|
|
||||||
flatMap((rels: Relationship[]) =>
|
|
||||||
observableZip(
|
|
||||||
...metadata
|
|
||||||
.map((metadatum: any) => Object.assign(new MetadataValue(), metadatum))
|
|
||||||
.map((metadatum: MetadataValue) => {
|
|
||||||
if (metadatum.isVirtual) {
|
|
||||||
const matchingRels = rels.filter((rel: Relationship) => ('' + rel.id) === metadatum.virtualValue);
|
|
||||||
if (matchingRels.length > 0) {
|
|
||||||
const matchingRel = matchingRels[0];
|
|
||||||
return observableCombineLatest(matchingRel.leftItem, matchingRel.rightItem).pipe(
|
|
||||||
filter(([leftItem, rightItem]) => leftItem.hasSucceeded && rightItem.hasSucceeded),
|
|
||||||
map(([leftItem, rightItem]) => {
|
|
||||||
if (leftItem.payload.id === thisId) {
|
|
||||||
return rightItem.payload;
|
|
||||||
} else if (rightItem.payload.id === thisId) {
|
|
||||||
return leftItem.payload;
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
map((item: Item) => Object.assign(new ItemMetadataRepresentation(), item))
|
|
||||||
);
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
return of(Object.assign(new MetadatumRepresentation(itemType), metadatum));
|
|
||||||
}
|
|
||||||
})
|
|
||||||
)
|
|
||||||
)
|
|
||||||
);
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item',
|
selector: 'ds-item',
|
||||||
|
@@ -7,7 +7,7 @@ import { ItemMetadataRepresentation } from '../../../core/shared/metadata-repres
|
|||||||
|
|
||||||
const itemType = 'type';
|
const itemType = 'type';
|
||||||
const metadataRepresentation1 = new MetadatumRepresentation(itemType);
|
const metadataRepresentation1 = new MetadatumRepresentation(itemType);
|
||||||
const metadataRepresentation2 = new ItemMetadataRepresentation();
|
const metadataRepresentation2 = new ItemMetadataRepresentation(Object.assign({}));
|
||||||
const representations = [metadataRepresentation1, metadataRepresentation2];
|
const representations = [metadataRepresentation1, metadataRepresentation2];
|
||||||
|
|
||||||
describe('MetadataRepresentationListComponent', () => {
|
describe('MetadataRepresentationListComponent', () => {
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { combineLatest as observableCombineLatest, Observable, of as observableOf, zip as observableZip } from 'rxjs';
|
import { combineLatest as observableCombineLatest, Observable, of as observableOf, zip as observableZip } from 'rxjs';
|
||||||
import { Injectable, OnDestroy } from '@angular/core';
|
import { Injectable, OnDestroy } from '@angular/core';
|
||||||
import { NavigationExtras, PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router';
|
import { NavigationExtras, PRIMARY_OUTLET, Router, UrlSegmentGroup } from '@angular/router';
|
||||||
import { first, map, switchMap, tap } from 'rxjs/operators';
|
import { first, map, switchMap, take, tap } from 'rxjs/operators';
|
||||||
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../core/cache/builders/remote-data-build.service';
|
||||||
import {
|
import {
|
||||||
FacetConfigSuccessResponse,
|
FacetConfigSuccessResponse,
|
||||||
@@ -100,9 +100,10 @@ export class SearchService implements OnDestroy {
|
|||||||
/**
|
/**
|
||||||
* Method to retrieve a paginated list of search results from the server
|
* Method to retrieve a paginated list of search results from the server
|
||||||
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
|
* @param {PaginatedSearchOptions} searchOptions The configuration necessary to perform this search
|
||||||
|
* @param responseMsToLive The amount of milliseconds for the response to live in cache
|
||||||
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
* @returns {Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>>} Emits a paginated list with all search results found
|
||||||
*/
|
*/
|
||||||
search(searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
search(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number): Observable<RemoteData<PaginatedList<SearchResult<DSpaceObject>>>> {
|
||||||
const hrefObs = this.halService.getEndpoint(this.searchLinkPath).pipe(
|
const hrefObs = this.halService.getEndpoint(this.searchLinkPath).pipe(
|
||||||
map((url: string) => {
|
map((url: string) => {
|
||||||
if (hasValue(searchOptions)) {
|
if (hasValue(searchOptions)) {
|
||||||
@@ -122,6 +123,7 @@ export class SearchService implements OnDestroy {
|
|||||||
};
|
};
|
||||||
|
|
||||||
return Object.assign(request, {
|
return Object.assign(request, {
|
||||||
|
responseMsToLive: hasValue(responseMsToLive) ? responseMsToLive : request.responseMsToLive,
|
||||||
getResponseParser: getResponseParserFn
|
getResponseParser: getResponseParserFn
|
||||||
});
|
});
|
||||||
}),
|
}),
|
||||||
|
@@ -27,6 +27,7 @@ import {
|
|||||||
bitstreamFormatReducer,
|
bitstreamFormatReducer,
|
||||||
BitstreamFormatRegistryState
|
BitstreamFormatRegistryState
|
||||||
} from './+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
|
} from './+admin/admin-registries/bitstream-formats/bitstream-format.reducers';
|
||||||
|
import { ObjectSelectionListState, objectSelectionReducer } from './shared/object-select/object-select.reducer';
|
||||||
|
|
||||||
export interface AppState {
|
export interface AppState {
|
||||||
router: fromRouter.RouterReducerState;
|
router: fromRouter.RouterReducerState;
|
||||||
@@ -41,6 +42,7 @@ export interface AppState {
|
|||||||
truncatable: TruncatablesState;
|
truncatable: TruncatablesState;
|
||||||
cssVariables: CSSVariablesState;
|
cssVariables: CSSVariablesState;
|
||||||
menus: MenusState;
|
menus: MenusState;
|
||||||
|
objectSelection: ObjectSelectionListState;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const appReducers: ActionReducerMap<AppState> = {
|
export const appReducers: ActionReducerMap<AppState> = {
|
||||||
@@ -56,6 +58,7 @@ export const appReducers: ActionReducerMap<AppState> = {
|
|||||||
truncatable: truncatableReducer,
|
truncatable: truncatableReducer,
|
||||||
cssVariables: cssVariablesReducer,
|
cssVariables: cssVariablesReducer,
|
||||||
menus: menusReducer,
|
menus: menusReducer,
|
||||||
|
objectSelection: objectSelectionReducer
|
||||||
};
|
};
|
||||||
|
|
||||||
export const routerStateSelector = (state: AppState) => state.router;
|
export const routerStateSelector = (state: AppState) => state.router;
|
||||||
|
@@ -82,8 +82,8 @@ export class RemoteDataBuildService {
|
|||||||
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, payload$: Observable<T>) {
|
toRemoteDataObservable<T>(requestEntry$: Observable<RequestEntry>, payload$: Observable<T>) {
|
||||||
return observableCombineLatest(requestEntry$, payload$).pipe(
|
return observableCombineLatest(requestEntry$, payload$).pipe(
|
||||||
map(([reqEntry, payload]) => {
|
map(([reqEntry, payload]) => {
|
||||||
const requestPending = hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
|
const requestPending = hasValue(reqEntry) && hasValue(reqEntry.requestPending) ? reqEntry.requestPending : true;
|
||||||
const responsePending = hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
|
const responsePending = hasValue(reqEntry) && hasValue(reqEntry.responsePending) ? reqEntry.responsePending : false;
|
||||||
let isSuccessful: boolean;
|
let isSuccessful: boolean;
|
||||||
let error: RemoteDataError;
|
let error: RemoteDataError;
|
||||||
if (hasValue(reqEntry) && hasValue(reqEntry.response)) {
|
if (hasValue(reqEntry) && hasValue(reqEntry.response)) {
|
||||||
|
@@ -119,6 +119,8 @@ import { MetadatafieldParsingService } from './data/metadatafield-parsing.servic
|
|||||||
import { NormalizedSubmissionUploadsModel } from './config/models/normalized-config-submission-uploads.model';
|
import { NormalizedSubmissionUploadsModel } from './config/models/normalized-config-submission-uploads.model';
|
||||||
import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model';
|
import { NormalizedBrowseEntry } from './shared/normalized-browse-entry.model';
|
||||||
import { BrowseDefinition } from './shared/browse-definition.model';
|
import { BrowseDefinition } from './shared/browse-definition.model';
|
||||||
|
import { MappedCollectionsReponseParsingService } from './data/mapped-collections-reponse-parsing.service';
|
||||||
|
import { ObjectSelectService } from '../shared/object-select/object-select.service';
|
||||||
|
|
||||||
const IMPORTS = [
|
const IMPORTS = [
|
||||||
CommonModule,
|
CommonModule,
|
||||||
@@ -166,6 +168,7 @@ const PROVIDERS = [
|
|||||||
RegistryMetadataschemasResponseParsingService,
|
RegistryMetadataschemasResponseParsingService,
|
||||||
RegistryMetadatafieldsResponseParsingService,
|
RegistryMetadatafieldsResponseParsingService,
|
||||||
RegistryBitstreamformatsResponseParsingService,
|
RegistryBitstreamformatsResponseParsingService,
|
||||||
|
MappedCollectionsReponseParsingService,
|
||||||
DebugResponseParsingService,
|
DebugResponseParsingService,
|
||||||
SearchResponseParsingService,
|
SearchResponseParsingService,
|
||||||
MyDSpaceResponseParsingService,
|
MyDSpaceResponseParsingService,
|
||||||
@@ -197,6 +200,7 @@ const PROVIDERS = [
|
|||||||
DSpaceObjectDataService,
|
DSpaceObjectDataService,
|
||||||
DSOChangeAnalyzer,
|
DSOChangeAnalyzer,
|
||||||
DefaultChangeAnalyzer,
|
DefaultChangeAnalyzer,
|
||||||
|
ObjectSelectService,
|
||||||
CSSVariableService,
|
CSSVariableService,
|
||||||
MenuService,
|
MenuService,
|
||||||
ObjectUpdatesService,
|
ObjectUpdatesService,
|
||||||
|
44
src/app/core/data/collection-data.service.spec.ts
Normal file
44
src/app/core/data/collection-data.service.spec.ts
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
import { CollectionDataService } from './collection-data.service';
|
||||||
|
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service-stub';
|
||||||
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
|
import { RequestService } from './request.service';
|
||||||
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
|
import { GetRequest } from './request.models';
|
||||||
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
|
|
||||||
|
describe('CollectionDataService', () => {
|
||||||
|
let service: CollectionDataService;
|
||||||
|
let objectCache: ObjectCacheService;
|
||||||
|
let requestService: RequestService;
|
||||||
|
let halService: HALEndpointService;
|
||||||
|
let rdbService: RemoteDataBuildService;
|
||||||
|
|
||||||
|
const url = 'fake-collections-url';
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
objectCache = jasmine.createSpyObj('objectCache', {
|
||||||
|
remove: jasmine.createSpy('remove')
|
||||||
|
});
|
||||||
|
requestService = getMockRequestService();
|
||||||
|
halService = Object.assign(new HALEndpointServiceStub(url));
|
||||||
|
rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
buildList: jasmine.createSpy('buildList')
|
||||||
|
});
|
||||||
|
|
||||||
|
service = new CollectionDataService(requestService, rdbService, null, null, null, objectCache, halService, null, null, null);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('getMappedItems', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
result = service.getMappedItems('collection-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure a GET request', () => {
|
||||||
|
expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(GetRequest), undefined);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -1,6 +1,6 @@
|
|||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
|
|
||||||
import { filter, map, take } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, map, take } from 'rxjs/operators';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
|
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
@@ -16,9 +16,16 @@ import { HttpClient } from '@angular/common/http';
|
|||||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
import { Observable } from 'rxjs/internal/Observable';
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
import { FindAllOptions } from './request.models';
|
import { FindAllOptions, GetRequest } from './request.models';
|
||||||
import { RemoteData } from './remote-data';
|
import { RemoteData } from './remote-data';
|
||||||
import { PaginatedList } from './paginated-list';
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { configureRequest } from '../shared/operators';
|
||||||
|
import { DSOResponseParsingService } from './dso-response-parsing.service';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { GenericConstructor } from '../shared/generic-constructor';
|
||||||
|
import { hasValue, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
|
import { PaginatedSearchOptions } from '../../+search-page/paginated-search-options.model';
|
||||||
import { SearchParam } from '../cache/models/search-param.model';
|
import { SearchParam } from '../cache/models/search-param.model';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
@@ -88,4 +95,46 @@ export class CollectionDataService extends ComColDataService<Collection> {
|
|||||||
map((collections: RemoteData<PaginatedList<Collection>>) => collections.payload.totalElements > 0)
|
map((collections: RemoteData<PaginatedList<Collection>>) => collections.payload.totalElements > 0)
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the endpoint used for mapping items to a collection
|
||||||
|
* @param collectionId The id of the collection to map items to
|
||||||
|
*/
|
||||||
|
getMappedItemsEndpoint(collectionId): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
map((endpoint: string) => this.getIDHref(endpoint, collectionId)),
|
||||||
|
map((endpoint: string) => `${endpoint}/mappedItems`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches a list of items that are mapped to a collection
|
||||||
|
* @param collectionId The id of the collection
|
||||||
|
* @param searchOptions Search options to sort or filter out items
|
||||||
|
*/
|
||||||
|
getMappedItems(collectionId: string, searchOptions?: PaginatedSearchOptions): Observable<RemoteData<PaginatedList<DSpaceObject>>> {
|
||||||
|
const requestUuid = this.requestService.generateRequestId();
|
||||||
|
|
||||||
|
const href$ = this.getMappedItemsEndpoint(collectionId).pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpoint: string) => hasValue(searchOptions) ? searchOptions.toRestUrl(endpoint) : endpoint)
|
||||||
|
);
|
||||||
|
|
||||||
|
href$.pipe(
|
||||||
|
map((endpoint: string) => {
|
||||||
|
const request = new GetRequest(requestUuid, endpoint);
|
||||||
|
return Object.assign(request, {
|
||||||
|
responseMsToLive: 0,
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return DSOResponseParsingService;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}),
|
||||||
|
configureRequest(this.requestService)
|
||||||
|
).subscribe();
|
||||||
|
|
||||||
|
return this.rdbService.buildList(href$);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -7,7 +7,14 @@ import { CoreState } from '../core.reducers';
|
|||||||
import { ItemDataService } from './item-data.service';
|
import { ItemDataService } from './item-data.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { FindAllOptions, RestRequest } from './request.models';
|
import {
|
||||||
|
DeleteRequest,
|
||||||
|
FindAllOptions,
|
||||||
|
GetRequest,
|
||||||
|
MappedCollectionsRequest,
|
||||||
|
PostRequest,
|
||||||
|
RestRequest
|
||||||
|
} from './request.models';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { RestResponse } from '../cache/response.models';
|
import { RestResponse } from '../cache/response.models';
|
||||||
@@ -16,12 +23,13 @@ import { NormalizedObjectBuildService } from '../cache/builders/normalized-objec
|
|||||||
import { HttpClient } from '@angular/common/http';
|
import { HttpClient } from '@angular/common/http';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { getMockRequestService } from '../../shared/mocks/mock-request.service';
|
||||||
|
|
||||||
describe('ItemDataService', () => {
|
describe('ItemDataService', () => {
|
||||||
let scheduler: TestScheduler;
|
let scheduler: TestScheduler;
|
||||||
let service: ItemDataService;
|
let service: ItemDataService;
|
||||||
let bs: BrowseService;
|
let bs: BrowseService;
|
||||||
const requestService = {
|
const requestService = Object.assign(getMockRequestService(), {
|
||||||
generateRequestId(): string {
|
generateRequestId(): string {
|
||||||
return scopeID;
|
return scopeID;
|
||||||
},
|
},
|
||||||
@@ -32,9 +40,14 @@ describe('ItemDataService', () => {
|
|||||||
const responseCacheEntry = new RequestEntry();
|
const responseCacheEntry = new RequestEntry();
|
||||||
responseCacheEntry.response = new RestResponse(true, 200, 'OK');
|
responseCacheEntry.response = new RestResponse(true, 200, 'OK');
|
||||||
return observableOf(responseCacheEntry);
|
return observableOf(responseCacheEntry);
|
||||||
|
},
|
||||||
|
removeByHrefSubstring(href: string) {
|
||||||
|
// Do nothing
|
||||||
}
|
}
|
||||||
} as RequestService;
|
}) as RequestService;
|
||||||
const rdbService = {} as RemoteDataBuildService;
|
const rdbService = jasmine.createSpyObj('rdbService', {
|
||||||
|
toRemoteDataObservable: observableOf({})
|
||||||
|
});
|
||||||
|
|
||||||
const store = {} as Store<CoreState>;
|
const store = {} as Store<CoreState>;
|
||||||
const objectCache = {} as ObjectCacheService;
|
const objectCache = {} as ObjectCacheService;
|
||||||
@@ -162,4 +175,32 @@ describe('ItemDataService', () => {
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe('removeMappingFromCollection', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = initTestService();
|
||||||
|
spyOn(requestService, 'configure');
|
||||||
|
result = service.removeMappingFromCollection('item-id', 'collection-id');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure a DELETE request', () => {
|
||||||
|
result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(DeleteRequest), undefined));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('mapToCollection', () => {
|
||||||
|
let result;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = initTestService();
|
||||||
|
spyOn(requestService, 'configure');
|
||||||
|
result = service.mapToCollection('item-id', 'collection-href');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should configure a POST request', () => {
|
||||||
|
result.subscribe(() => expect(requestService.configure).toHaveBeenCalledWith(jasmine.any(PostRequest), undefined));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -1,8 +1,8 @@
|
|||||||
import { distinctUntilChanged, filter, find, map } from 'rxjs/operators';
|
import { distinctUntilChanged, filter, find, map, switchMap, take } from 'rxjs/operators';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { hasValue, isNotEmpty } from '../../shared/empty.util';
|
import { hasValue, isNotEmpty, isNotEmptyOperator } from '../../shared/empty.util';
|
||||||
import { BrowseService } from '../browse/browse.service';
|
import { BrowseService } from '../browse/browse.service';
|
||||||
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../cache/builders/remote-data-build.service';
|
||||||
import { CoreState } from '../core.reducers';
|
import { CoreState } from '../core.reducers';
|
||||||
@@ -12,17 +12,31 @@ import { URLCombiner } from '../url-combiner/url-combiner';
|
|||||||
import { DataService } from './data.service';
|
import { DataService } from './data.service';
|
||||||
import { RequestService } from './request.service';
|
import { RequestService } from './request.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { FindAllOptions, PatchRequest, PutRequest, RestRequest } from './request.models';
|
import {
|
||||||
|
DeleteRequest,
|
||||||
|
FindAllOptions,
|
||||||
|
MappedCollectionsRequest,
|
||||||
|
PatchRequest,
|
||||||
|
PostRequest, PutRequest,
|
||||||
|
RestRequest
|
||||||
|
} from './request.models';
|
||||||
import { ObjectCacheService } from '../cache/object-cache.service';
|
import { ObjectCacheService } from '../cache/object-cache.service';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
import { DSOChangeAnalyzer } from './dso-change-analyzer.service';
|
||||||
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
import { HttpClient, HttpHeaders } from '@angular/common/http';
|
||||||
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
import { NormalizedObjectBuildService } from '../cache/builders/normalized-object-build.service';
|
||||||
import { configureRequest, getRequestFromRequestHref } from '../shared/operators';
|
import {
|
||||||
|
configureRequest,
|
||||||
|
filterSuccessfulResponses,
|
||||||
|
getRequestFromRequestHref,
|
||||||
|
getResponseFromEntry
|
||||||
|
} from '../shared/operators';
|
||||||
import { RequestEntry } from './request.reducer';
|
import { RequestEntry } from './request.reducer';
|
||||||
import { RestResponse } from '../cache/response.models';
|
import { GenericSuccessResponse, RestResponse } from '../cache/response.models';
|
||||||
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
import { HttpOptions } from '../dspace-rest-v2/dspace-rest-v2.service';
|
||||||
import { Collection } from '../shared/collection.model';
|
import { Collection } from '../shared/collection.model';
|
||||||
|
import { RemoteData } from './remote-data';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
|
||||||
@Injectable()
|
@Injectable()
|
||||||
export class ItemDataService extends DataService<Item> {
|
export class ItemDataService extends DataService<Item> {
|
||||||
@@ -60,6 +74,80 @@ export class ItemDataService extends DataService<Item> {
|
|||||||
distinctUntilChanged(),);
|
distinctUntilChanged(),);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches the endpoint used for mapping an item to a collection,
|
||||||
|
* or for fetching all collections the item is mapped to if no collection is provided
|
||||||
|
* @param itemId The item's id
|
||||||
|
* @param collectionId The collection's id (optional)
|
||||||
|
*/
|
||||||
|
public getMappedCollectionsEndpoint(itemId: string, collectionId?: string): Observable<string> {
|
||||||
|
return this.halService.getEndpoint(this.linkPath).pipe(
|
||||||
|
map((endpoint: string) => this.getIDHref(endpoint, itemId)),
|
||||||
|
map((endpoint: string) => `${endpoint}/mappedCollections${collectionId ? `/${collectionId}` : ''}`)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes the mapping of an item from a collection
|
||||||
|
* @param itemId The item's id
|
||||||
|
* @param collectionId The collection's id
|
||||||
|
*/
|
||||||
|
public removeMappingFromCollection(itemId: string, collectionId: string): Observable<RestResponse> {
|
||||||
|
return this.getMappedCollectionsEndpoint(itemId, collectionId).pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) => new DeleteRequest(this.requestService.generateRequestId(), endpointURL)),
|
||||||
|
configureRequest(this.requestService),
|
||||||
|
switchMap((request: RestRequest) => this.requestService.getByUUID(request.uuid)),
|
||||||
|
getResponseFromEntry()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Maps an item to a collection
|
||||||
|
* @param itemId The item's id
|
||||||
|
* @param collectionHref The collection's self link
|
||||||
|
*/
|
||||||
|
public mapToCollection(itemId: string, collectionHref: string): Observable<RestResponse> {
|
||||||
|
return this.getMappedCollectionsEndpoint(itemId).pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) => {
|
||||||
|
const options: HttpOptions = Object.create({});
|
||||||
|
let headers = new HttpHeaders();
|
||||||
|
headers = headers.append('Content-Type', 'text/uri-list');
|
||||||
|
options.headers = headers;
|
||||||
|
return new PostRequest(this.requestService.generateRequestId(), endpointURL, collectionHref, options);
|
||||||
|
}),
|
||||||
|
configureRequest(this.requestService),
|
||||||
|
switchMap((request: RestRequest) => this.requestService.getByUUID(request.uuid)),
|
||||||
|
getResponseFromEntry()
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fetches all collections the item is mapped to
|
||||||
|
* @param itemId The item's id
|
||||||
|
*/
|
||||||
|
public getMappedCollections(itemId: string): Observable<RemoteData<PaginatedList<Collection>>> {
|
||||||
|
const request$ = this.getMappedCollectionsEndpoint(itemId).pipe(
|
||||||
|
isNotEmptyOperator(),
|
||||||
|
distinctUntilChanged(),
|
||||||
|
map((endpointURL: string) => new MappedCollectionsRequest(this.requestService.generateRequestId(), endpointURL)),
|
||||||
|
configureRequest(this.requestService)
|
||||||
|
);
|
||||||
|
|
||||||
|
const requestEntry$ = request$.pipe(
|
||||||
|
switchMap((request: RestRequest) => this.requestService.getByHref(request.href))
|
||||||
|
);
|
||||||
|
const payload$ = requestEntry$.pipe(
|
||||||
|
filterSuccessfulResponses(),
|
||||||
|
map((response: GenericSuccessResponse<PaginatedList<Collection>>) => response.payload)
|
||||||
|
);
|
||||||
|
|
||||||
|
return this.rdbService.toRemoteDataObservable(requestEntry$, payload$);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the endpoint for item withdrawal and reinstatement
|
* Get the endpoint for item withdrawal and reinstatement
|
||||||
* @param itemId
|
* @param itemId
|
||||||
|
@@ -0,0 +1,37 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { ResponseParsingService } from './parsing.service';
|
||||||
|
import { RestRequest } from './request.models';
|
||||||
|
import { DSpaceRESTV2Response } from '../dspace-rest-v2/dspace-rest-v2-response.model';
|
||||||
|
import { PaginatedList } from './paginated-list';
|
||||||
|
import { PageInfo } from '../shared/page-info.model';
|
||||||
|
import { ErrorResponse, GenericSuccessResponse, RestResponse } from '../cache/response.models';
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
/**
|
||||||
|
* A ResponseParsingService used to parse DSpaceRESTV2Response coming from the REST API to a GenericSuccessResponse
|
||||||
|
* containing a PaginatedList of mapped collections
|
||||||
|
*/
|
||||||
|
export class MappedCollectionsReponseParsingService implements ResponseParsingService {
|
||||||
|
parse(request: RestRequest, data: DSpaceRESTV2Response): RestResponse {
|
||||||
|
const payload = data.payload;
|
||||||
|
|
||||||
|
if (payload._embedded && payload._embedded.mappedCollections) {
|
||||||
|
const mappedCollections = payload._embedded.mappedCollections;
|
||||||
|
// TODO: When the API supports it, change this to fetch a paginated list, instead of creating static one
|
||||||
|
// Reason: Pagination is currently not supported on the mappedCollections endpoint
|
||||||
|
const paginatedMappedCollections = new PaginatedList(Object.assign(new PageInfo(), {
|
||||||
|
elementsPerPage: mappedCollections.length,
|
||||||
|
totalElements: mappedCollections.length,
|
||||||
|
totalPages: 1,
|
||||||
|
currentPage: 1
|
||||||
|
}), mappedCollections);
|
||||||
|
return new GenericSuccessResponse(paginatedMappedCollections, data.statusCode, data.statusText);
|
||||||
|
} else {
|
||||||
|
return new ErrorResponse(
|
||||||
|
Object.assign(
|
||||||
|
new Error('Unexpected response from mappedCollections endpoint'), data
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
@@ -18,6 +18,7 @@ import { MetadataschemaParsingService } from './metadataschema-parsing.service';
|
|||||||
import { MetadatafieldParsingService } from './metadatafield-parsing.service';
|
import { MetadatafieldParsingService } from './metadatafield-parsing.service';
|
||||||
import { URLCombiner } from '../url-combiner/url-combiner';
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
import { TaskResponseParsingService } from '../tasks/task-response-parsing.service';
|
||||||
|
import { MappedCollectionsReponseParsingService } from './mapped-collections-reponse-parsing.service';
|
||||||
|
|
||||||
/* tslint:disable:max-classes-per-file */
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
|
||||||
@@ -185,6 +186,17 @@ export class BrowseItemsRequest extends GetRequest {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request to fetch the mapped collections of an item
|
||||||
|
*/
|
||||||
|
export class MappedCollectionsRequest extends GetRequest {
|
||||||
|
public responseMsToLive = 10000;
|
||||||
|
|
||||||
|
getResponseParser(): GenericConstructor<ResponseParsingService> {
|
||||||
|
return MappedCollectionsReponseParsingService;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
export class ConfigRequest extends GetRequest {
|
export class ConfigRequest extends GetRequest {
|
||||||
constructor(uuid: string, href: string, public options?: HttpOptions) {
|
constructor(uuid: string, href: string, public options?: HttpOptions) {
|
||||||
super(uuid, href, null, options);
|
super(uuid, href, null, options);
|
||||||
|
@@ -2,7 +2,6 @@ import {
|
|||||||
catchError,
|
catchError,
|
||||||
distinctUntilKeyChanged,
|
distinctUntilKeyChanged,
|
||||||
filter,
|
filter,
|
||||||
find,
|
|
||||||
first,
|
first,
|
||||||
map,
|
map,
|
||||||
take
|
take
|
||||||
|
@@ -1,21 +1,27 @@
|
|||||||
import { MetadataRepresentationType } from '../metadata-representation.model';
|
import { MetadataRepresentationType } from '../metadata-representation.model';
|
||||||
import { ItemMetadataRepresentation, ItemTypeToValue } from './item-metadata-representation.model';
|
import { ItemMetadataRepresentation } from './item-metadata-representation.model';
|
||||||
import { Item } from '../../item.model';
|
import { Item } from '../../item.model';
|
||||||
import { MetadataMap, MetadataValue } from '../../metadata.models';
|
import { MetadataValue } from '../../metadata.models';
|
||||||
|
|
||||||
describe('ItemMetadataRepresentation', () => {
|
describe('ItemMetadataRepresentation', () => {
|
||||||
const valuePrefix = 'Test value for ';
|
const valuePrefix = 'Test value for ';
|
||||||
const item = new Item();
|
const item = new Item();
|
||||||
|
const itemType = 'Item Type';
|
||||||
let itemMetadataRepresentation: ItemMetadataRepresentation;
|
let itemMetadataRepresentation: ItemMetadataRepresentation;
|
||||||
const metadataMap = new MetadataMap();
|
item.metadata = {
|
||||||
for (const key of Object.keys(ItemTypeToValue)) {
|
'dc.title': [
|
||||||
metadataMap[ItemTypeToValue[key]] = [Object.assign(new MetadataValue(), {
|
{
|
||||||
value: `${valuePrefix}${ItemTypeToValue[key]}`
|
value: `${valuePrefix}dc.title`
|
||||||
})];
|
|
||||||
}
|
}
|
||||||
item.metadata = metadataMap;
|
] as MetadataValue[],
|
||||||
|
'dc.contributor.author': [
|
||||||
|
{
|
||||||
|
value: `${valuePrefix}dc.contributor.author`
|
||||||
|
}
|
||||||
|
] as MetadataValue[]
|
||||||
|
};
|
||||||
|
|
||||||
for (const itemType of Object.keys(ItemTypeToValue)) {
|
for (const metadataField of Object.keys(item.metadata)) {
|
||||||
describe(`when creating an ItemMetadataRepresentation`, () => {
|
describe(`when creating an ItemMetadataRepresentation`, () => {
|
||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
item.metadata['relationship.type'] = [
|
item.metadata['relationship.type'] = [
|
||||||
@@ -23,8 +29,7 @@ describe('ItemMetadataRepresentation', () => {
|
|||||||
value: itemType
|
value: itemType
|
||||||
})
|
})
|
||||||
];
|
];
|
||||||
|
itemMetadataRepresentation = Object.assign(new ItemMetadataRepresentation(item.metadata[metadataField][0]), item);
|
||||||
itemMetadataRepresentation = Object.assign(new ItemMetadataRepresentation(), item);
|
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should have a representation type of item', () => {
|
it('should have a representation type of item', () => {
|
||||||
@@ -32,7 +37,7 @@ describe('ItemMetadataRepresentation', () => {
|
|||||||
});
|
});
|
||||||
|
|
||||||
it('should return the correct value when calling getValue', () => {
|
it('should return the correct value when calling getValue', () => {
|
||||||
expect(itemMetadataRepresentation.getValue()).toEqual(`${valuePrefix}${ItemTypeToValue[itemType]}`);
|
expect(itemMetadataRepresentation.getValue()).toEqual(`${valuePrefix}${metadataField}`);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should return the correct item type', () => {
|
it('should return the correct item type', () => {
|
||||||
|
@@ -1,21 +1,22 @@
|
|||||||
import { Item } from '../../item.model';
|
import { Item } from '../../item.model';
|
||||||
import { MetadataRepresentation, MetadataRepresentationType } from '../metadata-representation.model';
|
import { MetadataRepresentation, MetadataRepresentationType } from '../metadata-representation.model';
|
||||||
import { hasValue } from '../../../../shared/empty.util';
|
import { MetadataValue } from '../../metadata.models';
|
||||||
|
|
||||||
/**
|
|
||||||
* An object to convert item types into the metadata field it should render for the item's value
|
|
||||||
*/
|
|
||||||
export const ItemTypeToValue = {
|
|
||||||
Default: 'dc.title',
|
|
||||||
Person: 'dc.contributor.author',
|
|
||||||
OrgUnit: 'dc.title'
|
|
||||||
};
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This class determines which fields to use when rendering an Item as a metadata value.
|
* This class determines which fields to use when rendering an Item as a metadata value.
|
||||||
*/
|
*/
|
||||||
export class ItemMetadataRepresentation extends Item implements MetadataRepresentation {
|
export class ItemMetadataRepresentation extends Item implements MetadataRepresentation {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The virtual metadata value representing this item
|
||||||
|
*/
|
||||||
|
virtualMetadata: MetadataValue;
|
||||||
|
|
||||||
|
constructor(virtualMetadata: MetadataValue) {
|
||||||
|
super();
|
||||||
|
this.virtualMetadata = virtualMetadata;
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The type of item this item can be represented as
|
* The type of item this item can be represented as
|
||||||
*/
|
*/
|
||||||
@@ -34,13 +35,7 @@ export class ItemMetadataRepresentation extends Item implements MetadataRepresen
|
|||||||
* Get the value to display, depending on the itemType
|
* Get the value to display, depending on the itemType
|
||||||
*/
|
*/
|
||||||
getValue(): string {
|
getValue(): string {
|
||||||
let metadata;
|
return this.virtualMetadata.value;
|
||||||
if (hasValue(ItemTypeToValue[this.itemType])) {
|
|
||||||
metadata = ItemTypeToValue[this.itemType];
|
|
||||||
} else {
|
|
||||||
metadata = ItemTypeToValue.Default;
|
|
||||||
}
|
|
||||||
return this.firstMetadataValue(metadata);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -103,7 +103,7 @@ describe('Core Module - RxJS Operators', () => {
|
|||||||
scheduler.schedule(() => source.pipe(getRequestFromRequestUUID(requestService)).subscribe());
|
scheduler.schedule(() => source.pipe(getRequestFromRequestUUID(requestService)).subscribe());
|
||||||
scheduler.flush();
|
scheduler.flush();
|
||||||
|
|
||||||
expect(requestService.getByUUID).toHaveBeenCalledWith(testRequestUUID)
|
expect(requestService.getByUUID).toHaveBeenCalledWith(testRequestUUID);
|
||||||
});
|
});
|
||||||
|
|
||||||
it('shouldn\'t return anything if there is no request matching the request uuid', () => {
|
it('shouldn\'t return anything if there is no request matching the request uuid', () => {
|
||||||
|
@@ -1,13 +1,13 @@
|
|||||||
<ng-template #descTemplate>
|
<ng-template #descTemplate>
|
||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
<span *ngIf="item.allMetadata(['dc.description']).length > 0"
|
<span *ngIf="metadataRepresentation.allMetadata(['dc.description']).length > 0"
|
||||||
class="item-list-job-title">
|
class="item-list-job-title">
|
||||||
<span [innerHTML]="firstMetadataValue(['dc.description'])"></span>
|
<span [innerHTML]="metadataRepresentation.firstMetadataValue(['dc.description'])"></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ds-truncatable [id]="item.id">
|
<ds-truncatable [id]="metadataRepresentation.id">
|
||||||
<a [routerLink]="['/items/' + item.id]"
|
<a [routerLink]="['/items/' + metadataRepresentation.id]"
|
||||||
[innerHTML]="firstMetadataValue('organization.legalName')"
|
[innerHTML]="metadataRepresentation.getValue()"
|
||||||
[tooltip]="descTemplate"></a>
|
[tooltip]="metadataRepresentation.allMetadata(['dc.description']).length > 0 ? descTemplate : null"></a>
|
||||||
</ds-truncatable>
|
</ds-truncatable>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model';
|
import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
|
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
|
||||||
import { TypedItemSearchResultListElementComponent } from '../../../../shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component';
|
import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component';
|
||||||
|
|
||||||
@rendersItemType('OrgUnit', ItemViewMode.Element, MetadataRepresentationType.Item)
|
@rendersItemType('OrgUnit', ItemViewMode.Element, MetadataRepresentationType.Item)
|
||||||
@Component({
|
@Component({
|
||||||
@@ -11,5 +11,5 @@ import { TypedItemSearchResultListElementComponent } from '../../../../shared/ob
|
|||||||
/**
|
/**
|
||||||
* The component for displaying a list element for an item of the type OrgUnit
|
* The component for displaying a list element for an item of the type OrgUnit
|
||||||
*/
|
*/
|
||||||
export class OrgUnitMetadataListElementComponent extends TypedItemSearchResultListElementComponent {
|
export class OrgUnitMetadataListElementComponent extends ItemMetadataRepresentationListElementComponent {
|
||||||
}
|
}
|
||||||
|
@@ -1,15 +1,15 @@
|
|||||||
<ng-template #descTemplate>
|
<ng-template #descTemplate>
|
||||||
<span class="text-muted">
|
<span class="text-muted">
|
||||||
<span *ngIf="item.allMetadata(['person.jobTitle']).length > 0"
|
<span *ngIf="metadataRepresentation.allMetadata(['person.jobTitle']).length > 0"
|
||||||
class="item-list-job-title">
|
class="item-list-job-title">
|
||||||
<span *ngFor="let value of allMetadataValues(['person.jobTitle']); let last=last;">
|
<span *ngFor="let value of metadataRepresentation.allMetadataValues(['person.jobTitle']); let last=last;">
|
||||||
<span [innerHTML]="value"><span [innerHTML]="value"></span></span>
|
<span [innerHTML]="value"><span [innerHTML]="value"></span></span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</span>
|
</span>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
<ds-truncatable [id]="item.id">
|
<ds-truncatable [id]="metadataRepresentation.id">
|
||||||
<a [routerLink]="['/items/' + item.id]"
|
<a [routerLink]="['/items/' + metadataRepresentation.id]"
|
||||||
[innerHTML]="firstMetadataValue('person.familyName') + ', ' + firstMetadataValue('person.givenName')"
|
[innerHTML]="metadataRepresentation.getValue()"
|
||||||
[tooltip]="descTemplate"></a>
|
[tooltip]="metadataRepresentation.allMetadata(['person.jobTitle']).length > 0 ? descTemplate : null"></a>
|
||||||
</ds-truncatable>
|
</ds-truncatable>
|
||||||
|
@@ -1,7 +1,7 @@
|
|||||||
import { Component } from '@angular/core';
|
import { Component } from '@angular/core';
|
||||||
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
|
import { ItemViewMode, rendersItemType } from '../../../../shared/items/item-type-decorator';
|
||||||
import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model';
|
import { MetadataRepresentationType } from '../../../../core/shared/metadata-representation/metadata-representation.model';
|
||||||
import { TypedItemSearchResultListElementComponent } from '../../../../shared/object-list/item-list-element/item-types/typed-item-search-result-list-element.component';
|
import { ItemMetadataRepresentationListElementComponent } from '../../../../shared/object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component';
|
||||||
|
|
||||||
@rendersItemType('Person', ItemViewMode.Element, MetadataRepresentationType.Item)
|
@rendersItemType('Person', ItemViewMode.Element, MetadataRepresentationType.Item)
|
||||||
@Component({
|
@Component({
|
||||||
@@ -11,5 +11,5 @@ import { TypedItemSearchResultListElementComponent } from '../../../../shared/ob
|
|||||||
/**
|
/**
|
||||||
* The component for displaying a list element for an item of the type Person
|
* The component for displaying a list element for an item of the type Person
|
||||||
*/
|
*/
|
||||||
export class PersonMetadataListElementComponent extends TypedItemSearchResultListElementComponent {
|
export class PersonMetadataListElementComponent extends ItemMetadataRepresentationListElementComponent {
|
||||||
}
|
}
|
||||||
|
@@ -1,5 +1,5 @@
|
|||||||
<ng-container *ngVar="(objects$ | async) as objects">
|
<ng-container *ngVar="(objects$ | async) as objects">
|
||||||
<h2 class="w-100">{{title | translate}}</h2>
|
<h3 [ngClass]="{'sr-only': parentname }" >{{title | translate}}</h3>
|
||||||
<ng-container *ngComponentOutlet="getStartsWithComponent(); injector: objectInjector;"></ng-container>
|
<ng-container *ngComponentOutlet="getStartsWithComponent(); injector: objectInjector;"></ng-container>
|
||||||
<div *ngIf="objects?.hasSucceeded && !objects?.isLoading && objects?.payload?.page.length > 0" @fadeIn>
|
<div *ngIf="objects?.hasSucceeded && !objects?.isLoading && objects?.payload?.page.length > 0" @fadeIn>
|
||||||
<div *ngIf="!enableArrows">
|
<div *ngIf="!enableArrows">
|
||||||
|
@@ -26,6 +26,10 @@ export class BrowseByComponent implements OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() title: string;
|
@Input() title: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The parent name
|
||||||
|
*/
|
||||||
|
@Input() parentname: string;
|
||||||
/**
|
/**
|
||||||
* The list of objects to display
|
* The list of objects to display
|
||||||
*/
|
*/
|
||||||
|
@@ -1,6 +1,24 @@
|
|||||||
<h3>{{'browse.comcol.head' | translate}}</h3>
|
<h2 class="comcol-browse-label h5">{{'browse.comcol.head' | translate}}</h2>
|
||||||
<ul>
|
<nav class="comcol-browse mb-4" aria-label="Browse Community or Collection">
|
||||||
<li *ngFor="let config of types">
|
<div class="d-none d-sm-block">
|
||||||
<a [routerLink]="['/browse/' + config.id]" [queryParams]="{scope: id}">{{'browse.comcol.by.' + config.id | translate}}</a>
|
|
||||||
</li>
|
<div class="list-group list-group-horizontal">
|
||||||
</ul>
|
<a *ngFor="let option of allOptions"
|
||||||
|
class="list-group-item"
|
||||||
|
[routerLink]="option.routerLink"
|
||||||
|
[queryParams]="option.params"
|
||||||
|
routerLinkActive="active">{{ option.label | translate }}</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="d-block d-sm-none">
|
||||||
|
<select name="browse-type"
|
||||||
|
class="form-control"
|
||||||
|
aria-label="Browse Community or Collection"
|
||||||
|
(ngModelChange)="onSelectChange($event)" [ngModel]="currentOptionId$ | async">
|
||||||
|
<option *ngFor="let option of allOptions"
|
||||||
|
[ngValue]="option.id"
|
||||||
|
[attr.selected]="(currentOptionId$ | async) === option.id ? 'selected' : null">{{ option.label | translate }}</option>
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
</nav>
|
||||||
|
@@ -1,6 +1,27 @@
|
|||||||
import { Component, Inject, Input, OnInit } from '@angular/core';
|
import {
|
||||||
|
ChangeDetectionStrategy,
|
||||||
|
Component,
|
||||||
|
Inject,
|
||||||
|
Input, NgZone,
|
||||||
|
OnDestroy,
|
||||||
|
OnInit
|
||||||
|
} from '@angular/core';
|
||||||
|
import { Observable } from 'rxjs/internal/Observable';
|
||||||
|
import { Subscription } from 'rxjs/internal/Subscription';
|
||||||
|
import { filter, map, startWith, tap } from 'rxjs/operators';
|
||||||
|
import { getCollectionPageRoute } from '../../+collection-page/collection-page-routing.module';
|
||||||
|
import { getCommunityPageRoute } from '../../+community-page/community-page-routing.module';
|
||||||
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
import { GLOBAL_CONFIG, GlobalConfig } from '../../../config';
|
||||||
|
import { Router, ActivatedRoute, RouterModule, UrlSegment } from '@angular/router';
|
||||||
import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface';
|
import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interface';
|
||||||
|
import { hasValue } from '../empty.util';
|
||||||
|
|
||||||
|
export interface ComColPageNavOption {
|
||||||
|
id: string;
|
||||||
|
label: string,
|
||||||
|
routerLink: string
|
||||||
|
params?: any;
|
||||||
|
};
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A component to display the "Browse By" section of a Community or Collection page
|
* A component to display the "Browse By" section of a Community or Collection page
|
||||||
@@ -8,24 +29,63 @@ import { BrowseByTypeConfig } from '../../../config/browse-by-type-config.interf
|
|||||||
*/
|
*/
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-comcol-page-browse-by',
|
selector: 'ds-comcol-page-browse-by',
|
||||||
templateUrl: './comcol-page-browse-by.component.html',
|
styleUrls: ['./comcol-page-browse-by.component.scss'],
|
||||||
|
templateUrl: './comcol-page-browse-by.component.html'
|
||||||
})
|
})
|
||||||
export class ComcolPageBrowseByComponent implements OnInit {
|
export class ComcolPageBrowseByComponent implements OnInit {
|
||||||
/**
|
/**
|
||||||
* The ID of the Community or Collection
|
* The ID of the Community or Collection
|
||||||
*/
|
*/
|
||||||
@Input() id: string;
|
@Input() id: string;
|
||||||
|
@Input() contentType: string;
|
||||||
/**
|
/**
|
||||||
* List of currently active browse configurations
|
* List of currently active browse configurations
|
||||||
*/
|
*/
|
||||||
types: BrowseByTypeConfig[];
|
types: BrowseByTypeConfig[];
|
||||||
|
|
||||||
constructor(@Inject(GLOBAL_CONFIG) public config: GlobalConfig) {
|
allOptions: ComColPageNavOption[];
|
||||||
|
|
||||||
|
currentOptionId$: Observable<string>;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(GLOBAL_CONFIG) public config: GlobalConfig,
|
||||||
|
private route: ActivatedRoute,
|
||||||
|
private router: Router) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.types = this.config.browseBy.types;
|
this.allOptions = this.config.browseBy.types
|
||||||
|
.map((config: BrowseByTypeConfig) => ({
|
||||||
|
id: config.id,
|
||||||
|
label: `browse.comcol.by.${config.id}`,
|
||||||
|
routerLink: `/browse/${config.id}`,
|
||||||
|
params: { scope: this.id }
|
||||||
|
}));
|
||||||
|
|
||||||
|
if (this.contentType === 'collection') {
|
||||||
|
this.allOptions = [ {
|
||||||
|
id: this.id,
|
||||||
|
label: 'collection.page.browse.recent.head',
|
||||||
|
routerLink: getCollectionPageRoute(this.id)
|
||||||
|
}, ...this.allOptions ];
|
||||||
|
} else if (this.contentType === 'community') {
|
||||||
|
this.allOptions = [{
|
||||||
|
id: this.id,
|
||||||
|
label: 'community.all-lists.head',
|
||||||
|
routerLink: getCommunityPageRoute(this.id)
|
||||||
|
}, ...this.allOptions ];
|
||||||
}
|
}
|
||||||
|
|
||||||
|
this.currentOptionId$ = this.route.url.pipe(
|
||||||
|
filter((urlSegments: UrlSegment[]) => hasValue(urlSegments)),
|
||||||
|
map((urlSegments: UrlSegment[]) => urlSegments[urlSegments.length - 1].path)
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
onSelectChange(newId: string) {
|
||||||
|
const selectedOption = this.allOptions
|
||||||
|
.find((option: ComColPageNavOption) => option.id === newId);
|
||||||
|
|
||||||
|
this.router.navigate([selectedOption.routerLink], { queryParams: selectedOption.params });
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,4 @@
|
|||||||
|
<div *ngIf="content" class="content-with-optional-title mb-2">
|
||||||
|
<h2 class="d-inline-block h6" *ngIf="title">{{ title | translate }}</h2>
|
||||||
|
<div class="d-inline-block "><a href="{{getHandle()}}">{{getHandle()}}</a></div>
|
||||||
|
</div>
|
@@ -0,0 +1,5 @@
|
|||||||
|
div {
|
||||||
|
word-break: break-word;
|
||||||
|
word-wrap: break-word;
|
||||||
|
overflow-wrap: break-word;
|
||||||
|
}
|
@@ -0,0 +1,29 @@
|
|||||||
|
import { Component, Input, Inject, Injectable } from '@angular/core';
|
||||||
|
import { GlobalConfig } from '../../../config/global-config.interface';
|
||||||
|
import { GLOBAL_CONFIG } from '../../../config';
|
||||||
|
import { UIURLCombiner } from '../../core/url-combiner/ui-url-combiner';
|
||||||
|
/**
|
||||||
|
* This component builds a URL from the value of "handle"
|
||||||
|
*/
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-comcol-page-handle',
|
||||||
|
styleUrls: ['./comcol-page-handle.component.scss'],
|
||||||
|
templateUrl: './comcol-page-handle.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
@Injectable()
|
||||||
|
export class ComcolPageHandleComponent {
|
||||||
|
|
||||||
|
// Optional title
|
||||||
|
@Input() title: string;
|
||||||
|
|
||||||
|
// The value of "handle"
|
||||||
|
@Input() content: string;
|
||||||
|
|
||||||
|
constructor(@Inject(GLOBAL_CONFIG) private EnvConfig: GlobalConfig) {
|
||||||
|
}
|
||||||
|
public getHandle(): string {
|
||||||
|
return new UIURLCombiner(this.EnvConfig, '/handle/', this.content).toString();
|
||||||
|
}
|
||||||
|
}
|
@@ -28,7 +28,7 @@ const mockItem: Item = Object.assign(new Item(), {
|
|||||||
]
|
]
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const mockItemMetadataRepresentation = Object.assign(new ItemMetadataRepresentation(), mockItem);
|
const mockItemMetadataRepresentation = Object.assign(new ItemMetadataRepresentation(Object.assign({})), mockItem);
|
||||||
let viewMode = ItemViewMode.Full;
|
let viewMode = ItemViewMode.Full;
|
||||||
|
|
||||||
describe('ItemTypeSwitcherComponent', () => {
|
describe('ItemTypeSwitcherComponent', () => {
|
||||||
|
@@ -5,7 +5,7 @@ import { ItemMetadataListElementComponent } from './item-metadata-list-element.c
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||||
|
|
||||||
const mockItemMetadataRepresentation = new ItemMetadataRepresentation();
|
const mockItemMetadataRepresentation = new ItemMetadataRepresentation(Object.assign({}));
|
||||||
|
|
||||||
describe('ItemMetadataListElementComponent', () => {
|
describe('ItemMetadataListElementComponent', () => {
|
||||||
let comp: ItemMetadataListElementComponent;
|
let comp: ItemMetadataListElementComponent;
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
import { MetadataRepresentationListElementComponent } from '../metadata-representation-list-element.component';
|
||||||
|
import { Component, Inject } from '@angular/core';
|
||||||
|
import { ITEM } from '../../../items/switcher/item-type-switcher.component';
|
||||||
|
import { ItemMetadataRepresentation } from '../../../../core/shared/metadata-representation/item/item-metadata-representation.model';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-metadata-representation-list-element',
|
||||||
|
template: ''
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* An abstract class for displaying a single ItemMetadataRepresentation
|
||||||
|
*/
|
||||||
|
export class ItemMetadataRepresentationListElementComponent extends MetadataRepresentationListElementComponent {
|
||||||
|
constructor(@Inject(ITEM) public metadataRepresentation: ItemMetadataRepresentation) {
|
||||||
|
super(metadataRepresentation);
|
||||||
|
}
|
||||||
|
}
|
@@ -0,0 +1,41 @@
|
|||||||
|
<ng-container *ngVar="(dsoRD$ | async) as collectionsRD">
|
||||||
|
<ds-pagination
|
||||||
|
*ngIf="collectionsRD?.payload?.totalElements > 0 || collectionsRD?.payload?.page?.length > 0"
|
||||||
|
[paginationOptions]="paginationOptions"
|
||||||
|
[sortOptions]="sortOptions"
|
||||||
|
[pageInfoState]="collectionsRD?.payload"
|
||||||
|
[collectionSize]="collectionsRD?.payload?.totalElements"
|
||||||
|
[hidePagerWhenSinglePage]="true"
|
||||||
|
[hideGear]="true">
|
||||||
|
<div class="table-responsive mt-2">
|
||||||
|
<table id="collection-select" class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th scope="col">{{'collection.select.table.title' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let collection of collectionsRD?.payload?.page">
|
||||||
|
<td><input class="collection-checkbox" [ngModel]="getSelected(collection.id) | async" (change)="switch(collection.id)" type="checkbox" name="{{collection.id}}"></td>
|
||||||
|
<td><a [routerLink]="['/collections', collection.id]">{{collection.name}}</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</ds-pagination>
|
||||||
|
<div *ngIf="collectionsRD?.payload?.totalElements === 0 || collectionsRD?.payload?.page?.length === 0" class="alert alert-info w-100" role="alert">
|
||||||
|
{{'collection.select.empty' | translate}}
|
||||||
|
</div>
|
||||||
|
<ds-error *ngIf="collectionsRD?.hasFailed" message="{{'error.collections' | translate}}"></ds-error>
|
||||||
|
<ds-loading *ngIf="!collectionsRD || collectionsRD?.isLoading" message="{{'loading.collections' | translate}}"></ds-loading>
|
||||||
|
<div *ngVar="(selectedIds$ | async) as selectedIds">
|
||||||
|
<button class="btn btn-outline-secondary collection-cancel float-left" (click)="onCancel()">{{cancelButton | translate}}</button>
|
||||||
|
<button class="btn collection-confirm float-right"
|
||||||
|
[ngClass]="{'btn-danger': dangerConfirm, 'btn-primary': !dangerConfirm}"
|
||||||
|
[disabled]="selectedIds?.length === 0"
|
||||||
|
(click)="confirmSelected()">
|
||||||
|
{{confirmButton | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
@@ -0,0 +1,118 @@
|
|||||||
|
import { async, ComponentFixture, TestBed } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
|
import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { SharedModule } from '../../shared.module';
|
||||||
|
import { ObjectSelectServiceStub } from '../../testing/object-select-service-stub';
|
||||||
|
import { ObjectSelectService } from '../object-select.service';
|
||||||
|
import { HostWindowService } from '../../host-window.service';
|
||||||
|
import { HostWindowServiceStub } from '../../testing/host-window-service-stub';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { CollectionSelectComponent } from './collection-select.component';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
import { of } from 'rxjs/internal/observable/of';
|
||||||
|
|
||||||
|
describe('CollectionSelectComponent', () => {
|
||||||
|
let comp: CollectionSelectComponent;
|
||||||
|
let fixture: ComponentFixture<CollectionSelectComponent>;
|
||||||
|
let objectSelectService: ObjectSelectService;
|
||||||
|
|
||||||
|
const mockCollectionList = [
|
||||||
|
Object.assign(new Collection(), {
|
||||||
|
id: 'id1',
|
||||||
|
name: 'name1'
|
||||||
|
}),
|
||||||
|
Object.assign(new Collection(), {
|
||||||
|
id: 'id2',
|
||||||
|
name: 'name2'
|
||||||
|
})
|
||||||
|
];
|
||||||
|
const mockCollections = of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), mockCollectionList)));
|
||||||
|
const mockPaginationOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'search-page-configuration',
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, RouterTestingModule.withRoutes([])],
|
||||||
|
declarations: [],
|
||||||
|
providers: [
|
||||||
|
{ provide: ObjectSelectService, useValue: new ObjectSelectServiceStub([mockCollectionList[1].id]) },
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(CollectionSelectComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.dsoRD$ = mockCollections;
|
||||||
|
comp.paginationOptions = mockPaginationOptions;
|
||||||
|
fixture.detectChanges();
|
||||||
|
objectSelectService = (comp as any).objectSelectService;
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should show a list of ${mockCollectionList.length} collections`, () => {
|
||||||
|
const tbody: HTMLElement = fixture.debugElement.query(By.css('table#collection-select tbody')).nativeElement;
|
||||||
|
expect(tbody.children.length).toBe(mockCollectionList.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkboxes', () => {
|
||||||
|
let checkbox: HTMLInputElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
checkbox = fixture.debugElement.query(By.css('input.collection-checkbox')).nativeElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initially be unchecked',() => {
|
||||||
|
expect(checkbox.checked).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be checked when clicked', () => {
|
||||||
|
checkbox.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(checkbox.checked).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch the value through object-select-service', () => {
|
||||||
|
spyOn((comp as any).objectSelectService, 'switch').and.callThrough();
|
||||||
|
checkbox.click();
|
||||||
|
expect((comp as any).objectSelectService.switch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when confirm is clicked', () => {
|
||||||
|
let confirmButton: HTMLButtonElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
confirmButton = fixture.debugElement.query(By.css('button.collection-confirm')).nativeElement;
|
||||||
|
spyOn(comp.confirm, 'emit').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit the selected collections',() => {
|
||||||
|
confirmButton.click();
|
||||||
|
expect(comp.confirm.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when cancel is clicked', () => {
|
||||||
|
let cancelButton: HTMLButtonElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cancelButton = fixture.debugElement.query(By.css('button.collection-cancel')).nativeElement;
|
||||||
|
spyOn(comp.cancel, 'emit').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit a cancel event',() => {
|
||||||
|
cancelButton.click();
|
||||||
|
expect(comp.cancel.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,28 @@
|
|||||||
|
import { Component } from '@angular/core';
|
||||||
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
|
import { ObjectSelectComponent } from '../object-select/object-select.component';
|
||||||
|
import { isNotEmpty } from '../../empty.util';
|
||||||
|
import { ObjectSelectService } from '../object-select.service';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-collection-select',
|
||||||
|
templateUrl: './collection-select.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component used to select collections from a specific list and returning the UUIDs of the selected collections
|
||||||
|
*/
|
||||||
|
export class CollectionSelectComponent extends ObjectSelectComponent<Collection> {
|
||||||
|
|
||||||
|
constructor(protected objectSelectService: ObjectSelectService) {
|
||||||
|
super(objectSelectService);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
if (!isNotEmpty(this.confirmButton)) {
|
||||||
|
this.confirmButton = 'collection.select.confirm';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -0,0 +1,49 @@
|
|||||||
|
<ng-container *ngVar="(dsoRD$ | async) as itemsRD">
|
||||||
|
<ds-pagination
|
||||||
|
*ngIf="itemsRD?.payload?.totalElements > 0"
|
||||||
|
[paginationOptions]="paginationOptions"
|
||||||
|
[sortOptions]="sortOptions"
|
||||||
|
[pageInfoState]="itemsRD?.payload"
|
||||||
|
[collectionSize]="itemsRD?.payload?.totalElements"
|
||||||
|
[hidePagerWhenSinglePage]="true"
|
||||||
|
[hideGear]="true">
|
||||||
|
<div class="table-responsive mt-2">
|
||||||
|
<table id="item-select" class="table table-striped table-hover">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th></th>
|
||||||
|
<th *ngIf="!hideCollection" scope="col">{{'item.select.table.collection' | translate}}</th>
|
||||||
|
<th scope="col">{{'item.select.table.author' | translate}}</th>
|
||||||
|
<th scope="col">{{'item.select.table.title' | translate}}</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
<tr *ngFor="let item of itemsRD?.payload?.page">
|
||||||
|
<td><input class="item-checkbox" [ngModel]="getSelected(item.id) | async" (change)="switch(item.id)" type="checkbox" name="{{item.id}}"></td>
|
||||||
|
<td *ngIf="!hideCollection">
|
||||||
|
<span *ngVar="(item.owningCollection | async)?.payload as collection">
|
||||||
|
<a *ngIf="collection" [routerLink]="['/collections', collection?.id]">{{collection?.name}}</a>
|
||||||
|
</span>
|
||||||
|
</td>
|
||||||
|
<td><span *ngIf="item.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])">{{item.firstMetadataValue(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])}}</span></td>
|
||||||
|
<td><a [routerLink]="['/items', item.id]">{{item.firstMetadataValue("dc.title")}}</a></td>
|
||||||
|
</tr>
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</ds-pagination>
|
||||||
|
<div *ngIf="itemsRD?.payload?.totalElements === 0 || itemsRD?.payload?.page?.length === 0" class="alert alert-info w-100" role="alert">
|
||||||
|
{{'item.select.empty' | translate}}
|
||||||
|
</div>
|
||||||
|
<ds-error *ngIf="itemsRD?.hasFailed" message="{{'error.items' | translate}}"></ds-error>
|
||||||
|
<ds-loading *ngIf="!itemsRD || itemsRD?.isLoading" message="{{'loading.items' | translate}}"></ds-loading>
|
||||||
|
<div *ngVar="(selectedIds$ | async) as selectedIds">
|
||||||
|
<button class="btn btn-outline-secondary item-cancel float-left" (click)="onCancel()">{{cancelButton | translate}}</button>
|
||||||
|
<button class="btn item-confirm float-right"
|
||||||
|
[ngClass]="{'btn-danger': dangerConfirm, 'btn-primary': !dangerConfirm}"
|
||||||
|
[disabled]="selectedIds?.length === 0"
|
||||||
|
(click)="confirmSelected()">
|
||||||
|
{{confirmButton | translate}}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</ng-container>
|
@@ -0,0 +1,140 @@
|
|||||||
|
import { ItemSelectComponent } from './item-select.component';
|
||||||
|
import { async, ComponentFixture, fakeAsync, TestBed, tick } from '@angular/core/testing';
|
||||||
|
import { RouterTestingModule } from '@angular/router/testing';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import { PageInfo } from '../../../core/shared/page-info.model';
|
||||||
|
import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { SharedModule } from '../../shared.module';
|
||||||
|
import { ObjectSelectServiceStub } from '../../testing/object-select-service-stub';
|
||||||
|
import { ObjectSelectService } from '../object-select.service';
|
||||||
|
import { HostWindowService } from '../../host-window.service';
|
||||||
|
import { HostWindowServiceStub } from '../../testing/host-window-service-stub';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import { By } from '@angular/platform-browser';
|
||||||
|
import { of } from 'rxjs/internal/observable/of';
|
||||||
|
|
||||||
|
describe('ItemSelectComponent', () => {
|
||||||
|
let comp: ItemSelectComponent;
|
||||||
|
let fixture: ComponentFixture<ItemSelectComponent>;
|
||||||
|
let objectSelectService: ObjectSelectService;
|
||||||
|
|
||||||
|
const mockItemList = [
|
||||||
|
Object.assign(new Item(), {
|
||||||
|
id: 'id1',
|
||||||
|
bitstreams: of({}),
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just a title'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dc.type',
|
||||||
|
language: null,
|
||||||
|
value: 'Article'
|
||||||
|
}]
|
||||||
|
}),
|
||||||
|
Object.assign(new Item(), {
|
||||||
|
id: 'id2',
|
||||||
|
bitstreams: of({}),
|
||||||
|
metadata: [
|
||||||
|
{
|
||||||
|
key: 'dc.title',
|
||||||
|
language: 'en_US',
|
||||||
|
value: 'This is just another title'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
key: 'dc.type',
|
||||||
|
language: null,
|
||||||
|
value: 'Article'
|
||||||
|
}]
|
||||||
|
})
|
||||||
|
];
|
||||||
|
const mockItems = of(new RemoteData(false, false, true, null, new PaginatedList(new PageInfo(), mockItemList)));
|
||||||
|
const mockPaginationOptions = Object.assign(new PaginationComponentOptions(), {
|
||||||
|
id: 'search-page-configuration',
|
||||||
|
pageSize: 10,
|
||||||
|
currentPage: 1
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async(() => {
|
||||||
|
TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), SharedModule, RouterTestingModule.withRoutes([])],
|
||||||
|
declarations: [],
|
||||||
|
providers: [
|
||||||
|
{ provide: ObjectSelectService, useValue: new ObjectSelectServiceStub([mockItemList[1].id]) },
|
||||||
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) }
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA]
|
||||||
|
}).compileComponents();
|
||||||
|
}));
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
fixture = TestBed.createComponent(ItemSelectComponent);
|
||||||
|
comp = fixture.componentInstance;
|
||||||
|
comp.dsoRD$ = mockItems;
|
||||||
|
comp.paginationOptions = mockPaginationOptions;
|
||||||
|
fixture.detectChanges();
|
||||||
|
objectSelectService = (comp as any).objectSelectService;
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should show a list of ${mockItemList.length} items`, () => {
|
||||||
|
const tbody: HTMLElement = fixture.debugElement.query(By.css('table#item-select tbody')).nativeElement;
|
||||||
|
expect(tbody.children.length).toBe(mockItemList.length);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('checkboxes', () => {
|
||||||
|
let checkbox: HTMLInputElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
checkbox = fixture.debugElement.query(By.css('input.item-checkbox')).nativeElement;
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should initially be unchecked',() => {
|
||||||
|
expect(checkbox.checked).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should be checked when clicked', () => {
|
||||||
|
checkbox.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
expect(checkbox.checked).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should switch the value through object-select-service', () => {
|
||||||
|
spyOn((comp as any).objectSelectService, 'switch').and.callThrough();
|
||||||
|
checkbox.click();
|
||||||
|
expect((comp as any).objectSelectService.switch).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when confirm is clicked', () => {
|
||||||
|
let confirmButton: HTMLButtonElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
confirmButton = fixture.debugElement.query(By.css('button.item-confirm')).nativeElement;
|
||||||
|
spyOn(comp.confirm, 'emit').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit the selected items',() => {
|
||||||
|
confirmButton.click();
|
||||||
|
expect(comp.confirm.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when cancel is clicked', () => {
|
||||||
|
let cancelButton: HTMLButtonElement;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
cancelButton = fixture.debugElement.query(By.css('button.item-cancel')).nativeElement;
|
||||||
|
spyOn(comp.cancel, 'emit').and.callThrough();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should emit a cancel event',() => {
|
||||||
|
cancelButton.click();
|
||||||
|
expect(comp.cancel.emit).toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
@@ -0,0 +1,34 @@
|
|||||||
|
import { Component, Input } from '@angular/core';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { ObjectSelectService } from '../object-select.service';
|
||||||
|
import { ObjectSelectComponent } from '../object-select/object-select.component';
|
||||||
|
import { isNotEmpty } from '../../empty.util';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-select',
|
||||||
|
templateUrl: './item-select.component.html'
|
||||||
|
})
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A component used to select items from a specific list and returning the UUIDs of the selected items
|
||||||
|
*/
|
||||||
|
export class ItemSelectComponent extends ObjectSelectComponent<Item> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to hide the collection column
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
hideCollection = false;
|
||||||
|
|
||||||
|
constructor(protected objectSelectService: ObjectSelectService) {
|
||||||
|
super(objectSelectService);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
super.ngOnInit();
|
||||||
|
if (!isNotEmpty(this.confirmButton)) {
|
||||||
|
this.confirmButton = 'item.select.confirm';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
82
src/app/shared/object-select/object-select.actions.ts
Normal file
82
src/app/shared/object-select/object-select.actions.ts
Normal file
@@ -0,0 +1,82 @@
|
|||||||
|
import { type } from '../ngrx/type';
|
||||||
|
import { Action } from '@ngrx/store';
|
||||||
|
|
||||||
|
export const ObjectSelectionActionTypes = {
|
||||||
|
INITIAL_DESELECT: type('dspace/object-select/INITIAL_DESELECT'),
|
||||||
|
INITIAL_SELECT: type('dspace/object-select/INITIAL_SELECT'),
|
||||||
|
SELECT: type('dspace/object-select/SELECT'),
|
||||||
|
DESELECT: type('dspace/object-select/DESELECT'),
|
||||||
|
SWITCH: type('dspace/object-select/SWITCH'),
|
||||||
|
RESET: type('dspace/object-select/RESET')
|
||||||
|
};
|
||||||
|
|
||||||
|
export class ObjectSelectionAction implements Action {
|
||||||
|
/**
|
||||||
|
* Key of the list (of selections) for which the action should be performed
|
||||||
|
*/
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* UUID of the object a select action can be performed on
|
||||||
|
*/
|
||||||
|
id: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Type of action that will be performed
|
||||||
|
*/
|
||||||
|
type;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialize with the object's UUID
|
||||||
|
* @param {string} key of the list
|
||||||
|
* @param {string} id of the object
|
||||||
|
*/
|
||||||
|
constructor(key: string, id: string) {
|
||||||
|
this.key = key;
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* tslint:disable:max-classes-per-file */
|
||||||
|
/**
|
||||||
|
* Used to set the initial state to deselected
|
||||||
|
*/
|
||||||
|
export class ObjectSelectionInitialDeselectAction extends ObjectSelectionAction {
|
||||||
|
type = ObjectSelectionActionTypes.INITIAL_DESELECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to set the initial state to selected
|
||||||
|
*/
|
||||||
|
export class ObjectSelectionInitialSelectAction extends ObjectSelectionAction {
|
||||||
|
type = ObjectSelectionActionTypes.INITIAL_SELECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to select an object
|
||||||
|
*/
|
||||||
|
export class ObjectSelectionSelectAction extends ObjectSelectionAction {
|
||||||
|
type = ObjectSelectionActionTypes.SELECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to deselect an object
|
||||||
|
*/
|
||||||
|
export class ObjectSelectionDeselectAction extends ObjectSelectionAction {
|
||||||
|
type = ObjectSelectionActionTypes.DESELECT;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to switch an object between selected and deselected
|
||||||
|
*/
|
||||||
|
export class ObjectSelectionSwitchAction extends ObjectSelectionAction {
|
||||||
|
type = ObjectSelectionActionTypes.SWITCH;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Used to reset all objects selected to be deselected
|
||||||
|
*/
|
||||||
|
export class ObjectSelectionResetAction extends ObjectSelectionAction {
|
||||||
|
type = ObjectSelectionActionTypes.RESET;
|
||||||
|
}
|
||||||
|
/* tslint:enable:max-classes-per-file */
|
105
src/app/shared/object-select/object-select.reducer.spec.ts
Normal file
105
src/app/shared/object-select/object-select.reducer.spec.ts
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
import {
|
||||||
|
ObjectSelectionDeselectAction, ObjectSelectionInitialDeselectAction,
|
||||||
|
ObjectSelectionInitialSelectAction, ObjectSelectionResetAction,
|
||||||
|
ObjectSelectionSelectAction, ObjectSelectionSwitchAction
|
||||||
|
} from './object-select.actions';
|
||||||
|
import { objectSelectionReducer } from './object-select.reducer';
|
||||||
|
|
||||||
|
const key = 'key';
|
||||||
|
const objectId1 = 'id1';
|
||||||
|
const objectId2 = 'id2';
|
||||||
|
|
||||||
|
class NullAction extends ObjectSelectionSelectAction {
|
||||||
|
type = null;
|
||||||
|
|
||||||
|
constructor() {
|
||||||
|
super(undefined, undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
describe('objectSelectionReducer', () => {
|
||||||
|
|
||||||
|
it('should return the current state when no valid actions have been made', () => {
|
||||||
|
const state = {};
|
||||||
|
state[key] = {};
|
||||||
|
state[key][objectId1] = { checked: true };
|
||||||
|
const action = new NullAction();
|
||||||
|
const newState = objectSelectionReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should start with an empty object', () => {
|
||||||
|
const state = {};
|
||||||
|
const action = new NullAction();
|
||||||
|
const newState = objectSelectionReducer(undefined, action);
|
||||||
|
|
||||||
|
expect(newState).toEqual(state);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set checked to true in response to the INITIAL_SELECT action', () => {
|
||||||
|
const action = new ObjectSelectionInitialSelectAction(key, objectId1);
|
||||||
|
const newState = objectSelectionReducer(undefined, action);
|
||||||
|
|
||||||
|
expect(newState[key][objectId1].checked).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set checked to true in response to the INITIAL_DESELECT action', () => {
|
||||||
|
const action = new ObjectSelectionInitialDeselectAction(key, objectId1);
|
||||||
|
const newState = objectSelectionReducer(undefined, action);
|
||||||
|
|
||||||
|
expect(newState[key][objectId1].checked).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set checked to true in response to the SELECT action', () => {
|
||||||
|
const state = {};
|
||||||
|
state[key] = {};
|
||||||
|
state[key][objectId1] = { checked: false };
|
||||||
|
const action = new ObjectSelectionSelectAction(key, objectId1);
|
||||||
|
const newState = objectSelectionReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState[key][objectId1].checked).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set checked to false in response to the DESELECT action', () => {
|
||||||
|
const state = {};
|
||||||
|
state[key] = {};
|
||||||
|
state[key][objectId1] = { checked: true };
|
||||||
|
const action = new ObjectSelectionDeselectAction(key, objectId1);
|
||||||
|
const newState = objectSelectionReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState[key][objectId1].checked).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set checked from false to true in response to the SWITCH action', () => {
|
||||||
|
const state = {};
|
||||||
|
state[key] = {};
|
||||||
|
state[key][objectId1] = { checked: false };
|
||||||
|
const action = new ObjectSelectionSwitchAction(key, objectId1);
|
||||||
|
const newState = objectSelectionReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState[key][objectId1].checked).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should set checked from true to false in response to the SWITCH action', () => {
|
||||||
|
const state = {};
|
||||||
|
state[key] = {};
|
||||||
|
state[key][objectId1] = { checked: true };
|
||||||
|
const action = new ObjectSelectionSwitchAction(key, objectId1);
|
||||||
|
const newState = objectSelectionReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState[key][objectId1].checked).toBeFalsy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should reset the state in response to the RESET action', () => {
|
||||||
|
const state = {};
|
||||||
|
state[key] = {};
|
||||||
|
state[key][objectId1] = { checked: true };
|
||||||
|
state[key][objectId2] = { checked: false };
|
||||||
|
const action = new ObjectSelectionResetAction(key, undefined);
|
||||||
|
const newState = objectSelectionReducer(state, action);
|
||||||
|
|
||||||
|
expect(newState[key]).toEqual({});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
107
src/app/shared/object-select/object-select.reducer.ts
Normal file
107
src/app/shared/object-select/object-select.reducer.ts
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
import { isEmpty } from '../empty.util';
|
||||||
|
import { ObjectSelectionAction, ObjectSelectionActionTypes } from './object-select.actions';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that represents the state for a single selection of an object
|
||||||
|
*/
|
||||||
|
export interface ObjectSelectionState {
|
||||||
|
checked: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that represents the state for all selected items within a certain category defined by a key
|
||||||
|
*/
|
||||||
|
export interface ObjectSelectionsState {
|
||||||
|
[id: string]: ObjectSelectionState
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Interface that represents the state for all selected items
|
||||||
|
*/
|
||||||
|
export interface ObjectSelectionListState {
|
||||||
|
[key: string]: ObjectSelectionsState
|
||||||
|
}
|
||||||
|
|
||||||
|
const initialState: ObjectSelectionListState = Object.create(null);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Performs a selection action on the current state
|
||||||
|
* @param {ObjectSelectionListState} state The state before the action is performed
|
||||||
|
* @param {ObjectSelectionAction} action The action that should be performed
|
||||||
|
* @returns {ObjectSelectionListState} The state after the action is performed
|
||||||
|
*/
|
||||||
|
export function objectSelectionReducer(state = initialState, action: ObjectSelectionAction): ObjectSelectionListState {
|
||||||
|
|
||||||
|
switch (action.type) {
|
||||||
|
|
||||||
|
case ObjectSelectionActionTypes.INITIAL_SELECT: {
|
||||||
|
if (isEmpty(state) || isEmpty(state[action.key]) || isEmpty(state[action.key][action.id])) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.key]: Object.assign({}, state[action.key], {
|
||||||
|
[action.id]: {
|
||||||
|
checked: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ObjectSelectionActionTypes.INITIAL_DESELECT: {
|
||||||
|
if (isEmpty(state) || isEmpty(state[action.key]) || isEmpty(state[action.key][action.id])) {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.key]: Object.assign({}, state[action.key], {
|
||||||
|
[action.id]: {
|
||||||
|
checked: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
|
||||||
|
case ObjectSelectionActionTypes.SELECT: {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.key]: Object.assign({}, state[action.key], {
|
||||||
|
[action.id]: {
|
||||||
|
checked: true
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case ObjectSelectionActionTypes.DESELECT: {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.key]: Object.assign({}, state[action.key], {
|
||||||
|
[action.id]: {
|
||||||
|
checked: false
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case ObjectSelectionActionTypes.SWITCH: {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.key]: Object.assign({}, state[action.key], {
|
||||||
|
[action.id]: {
|
||||||
|
checked: (isEmpty(state) || isEmpty(state[action.key]) || isEmpty(state[action.key][action.id])) ? true : !state[action.key][action.id].checked
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
case ObjectSelectionActionTypes.RESET: {
|
||||||
|
if (isEmpty(action.key)) {
|
||||||
|
return {};
|
||||||
|
} else {
|
||||||
|
return Object.assign({}, state, {
|
||||||
|
[action.key]: {}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
default: {
|
||||||
|
return state;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
104
src/app/shared/object-select/object-select.service.spec.ts
Normal file
104
src/app/shared/object-select/object-select.service.spec.ts
Normal file
@@ -0,0 +1,104 @@
|
|||||||
|
import { ObjectSelectService } from './object-select.service';
|
||||||
|
import { Store } from '@ngrx/store';
|
||||||
|
import { ObjectSelectionListState, ObjectSelectionsState } from './object-select.reducer';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
import {
|
||||||
|
ObjectSelectionDeselectAction,
|
||||||
|
ObjectSelectionInitialDeselectAction,
|
||||||
|
ObjectSelectionInitialSelectAction, ObjectSelectionResetAction,
|
||||||
|
ObjectSelectionSelectAction, ObjectSelectionSwitchAction
|
||||||
|
} from './object-select.actions';
|
||||||
|
import { of } from 'rxjs/internal/observable/of';
|
||||||
|
|
||||||
|
describe('ObjectSelectService', () => {
|
||||||
|
let service: ObjectSelectService;
|
||||||
|
|
||||||
|
const mockKey = 'key';
|
||||||
|
const mockObjectId = 'id1';
|
||||||
|
|
||||||
|
const selectionStore: Store<ObjectSelectionListState> = jasmine.createSpyObj('selectionStore', {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
dispatch: {},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
select: of(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
const store: Store<ObjectSelectionsState> = jasmine.createSpyObj('store', {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
dispatch: {},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
select: of(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
const appStore: Store<AppState> = jasmine.createSpyObj('appStore', {
|
||||||
|
/* tslint:disable:no-empty */
|
||||||
|
dispatch: {},
|
||||||
|
/* tslint:enable:no-empty */
|
||||||
|
select: of(true)
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
service = new ObjectSelectService(selectionStore, appStore);
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the initialSelect method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.initialSelect(mockKey, mockObjectId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ObjectSelectionInitialSelectAction should be dispatched to the store', () => {
|
||||||
|
expect(selectionStore.dispatch).toHaveBeenCalledWith(new ObjectSelectionInitialSelectAction(mockKey, mockObjectId));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the initialDeselect method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.initialDeselect(mockKey, mockObjectId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ObjectSelectionInitialDeselectAction should be dispatched to the store', () => {
|
||||||
|
expect(selectionStore.dispatch).toHaveBeenCalledWith(new ObjectSelectionInitialDeselectAction(mockKey, mockObjectId));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the select method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.select(mockKey, mockObjectId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ObjectSelectionSelectAction should be dispatched to the store', () => {
|
||||||
|
expect(selectionStore.dispatch).toHaveBeenCalledWith(new ObjectSelectionSelectAction(mockKey, mockObjectId));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the deselect method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.deselect(mockKey, mockObjectId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ObjectSelectionDeselectAction should be dispatched to the store', () => {
|
||||||
|
expect(selectionStore.dispatch).toHaveBeenCalledWith(new ObjectSelectionDeselectAction(mockKey, mockObjectId));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the switch method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.switch(mockKey, mockObjectId);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ObjectSelectionSwitchAction should be dispatched to the store', () => {
|
||||||
|
expect(selectionStore.dispatch).toHaveBeenCalledWith(new ObjectSelectionSwitchAction(mockKey, mockObjectId));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when the reset method is triggered', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
service.reset(mockKey);
|
||||||
|
});
|
||||||
|
|
||||||
|
it('ObjectSelectionInitialSelectAction should be dispatched to the store', () => {
|
||||||
|
expect(selectionStore.dispatch).toHaveBeenCalledWith(new ObjectSelectionResetAction(mockKey, null));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
131
src/app/shared/object-select/object-select.service.ts
Normal file
131
src/app/shared/object-select/object-select.service.ts
Normal file
@@ -0,0 +1,131 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { createSelector, MemoizedSelector, Store } from '@ngrx/store';
|
||||||
|
import { ObjectSelectionListState, ObjectSelectionsState, ObjectSelectionState } from './object-select.reducer';
|
||||||
|
import {
|
||||||
|
ObjectSelectionDeselectAction,
|
||||||
|
ObjectSelectionInitialDeselectAction,
|
||||||
|
ObjectSelectionInitialSelectAction, ObjectSelectionResetAction,
|
||||||
|
ObjectSelectionSelectAction, ObjectSelectionSwitchAction
|
||||||
|
} from './object-select.actions';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { hasValue } from '../empty.util';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
import { AppState } from '../../app.reducer';
|
||||||
|
|
||||||
|
const objectSelectionsStateSelector = (state: ObjectSelectionListState) => state.objectSelection;
|
||||||
|
const objectSelectionListStateSelector = (state: AppState) => state.objectSelection;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service that takes care of selecting and deselecting objects
|
||||||
|
*/
|
||||||
|
@Injectable()
|
||||||
|
export class ObjectSelectService {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private store: Store<ObjectSelectionListState>,
|
||||||
|
private appStore: Store<AppState>
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the current selection of a given object in a given list
|
||||||
|
* @param {string} key The key of the list where the selection resides in
|
||||||
|
* @param {string} id The UUID of the object
|
||||||
|
* @returns {Observable<boolean>} Emits the current selection state of the given object, if it's unavailable, return false
|
||||||
|
*/
|
||||||
|
getSelected(key: string, id: string): Observable<boolean> {
|
||||||
|
return this.store.select(selectionByKeyAndIdSelector(key, id)).pipe(
|
||||||
|
map((object: ObjectSelectionState) => {
|
||||||
|
if (object) {
|
||||||
|
return object.checked;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Request the current selection of all objects within a specific list
|
||||||
|
* @returns {Observable<boolean>} Emits the current selection state of all objects
|
||||||
|
*/
|
||||||
|
getAllSelected(key: string): Observable<string[]> {
|
||||||
|
return this.appStore.select(objectSelectionListStateSelector).pipe(
|
||||||
|
map((state: ObjectSelectionListState) => {
|
||||||
|
if (hasValue(state[key])) {
|
||||||
|
return Object.keys(state[key]).filter((id) => state[key][id].checked);
|
||||||
|
} else {
|
||||||
|
return [];
|
||||||
|
}
|
||||||
|
})
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches an initial select action to the store for a given object in a given list
|
||||||
|
* @param {string} key The key of the list to select the object in
|
||||||
|
* @param {string} id The UUID of the object to select
|
||||||
|
*/
|
||||||
|
public initialSelect(key: string, id: string): void {
|
||||||
|
this.store.dispatch(new ObjectSelectionInitialSelectAction(key, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches an initial deselect action to the store for a given object in a given list
|
||||||
|
* @param {string} key The key of the list to deselect the object in
|
||||||
|
* @param {string} id The UUID of the object to deselect
|
||||||
|
*/
|
||||||
|
public initialDeselect(key: string, id: string): void {
|
||||||
|
this.store.dispatch(new ObjectSelectionInitialDeselectAction(key, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches a select action to the store for a given object in a given list
|
||||||
|
* @param {string} key The key of the list to select the object in
|
||||||
|
* @param {string} id The UUID of the object to select
|
||||||
|
*/
|
||||||
|
public select(key: string, id: string): void {
|
||||||
|
this.store.dispatch(new ObjectSelectionSelectAction(key, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches a deselect action to the store for a given object in a given list
|
||||||
|
* @param {string} key The key of the list to deselect the object in
|
||||||
|
* @param {string} id The UUID of the object to deselect
|
||||||
|
*/
|
||||||
|
public deselect(key: string, id: string): void {
|
||||||
|
this.store.dispatch(new ObjectSelectionDeselectAction(key, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches a switch action to the store for a given object in a given list
|
||||||
|
* @param {string} key The key of the list to select the object in
|
||||||
|
* @param {string} id The UUID of the object to select
|
||||||
|
*/
|
||||||
|
public switch(key: string, id: string): void {
|
||||||
|
this.store.dispatch(new ObjectSelectionSwitchAction(key, id));
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Dispatches a reset action to the store for all objects (in a list)
|
||||||
|
* @param {string} key The key of the list to clear all selections for
|
||||||
|
*/
|
||||||
|
public reset(key?: string): void {
|
||||||
|
this.store.dispatch(new ObjectSelectionResetAction(key, null));
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
function selectionByKeyAndIdSelector(key: string, id: string): MemoizedSelector<ObjectSelectionListState, ObjectSelectionState> {
|
||||||
|
return keyAndIdSelector<ObjectSelectionState>(key, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function keyAndIdSelector<T>(key: string, id: string): MemoizedSelector<ObjectSelectionListState, T> {
|
||||||
|
return createSelector(objectSelectionsStateSelector, (state: ObjectSelectionsState) => {
|
||||||
|
if (hasValue(state) && hasValue(state[key])) {
|
||||||
|
return state[key][id];
|
||||||
|
} else {
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
@@ -0,0 +1,126 @@
|
|||||||
|
import { EventEmitter, Input, OnDestroy, OnInit, Output } from '@angular/core';
|
||||||
|
import { take } from 'rxjs/operators';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { PaginatedList } from '../../../core/data/paginated-list';
|
||||||
|
import { PaginationComponentOptions } from '../../pagination/pagination-component-options.model';
|
||||||
|
import { ObjectSelectService } from '../object-select.service';
|
||||||
|
import { SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An abstract component used to select DSpaceObjects from a specific list and returning the UUIDs of the selected DSpaceObjects
|
||||||
|
*/
|
||||||
|
export abstract class ObjectSelectComponent<TDomain> implements OnInit, OnDestroy {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* A unique key used for the object select service
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
key: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of DSpaceObjects to display
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
dsoRD$: Observable<RemoteData<PaginatedList<TDomain>>>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The pagination options used to display the DSpaceObjects
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
paginationOptions: PaginationComponentOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The sorting options used to display the DSpaceObjects
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
sortOptions: SortOptions;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The message key used for the confirm button
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
confirmButton: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The message key used for the cancel button
|
||||||
|
* @type {string}
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
cancelButton: string;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An event fired when the cancel button is clicked
|
||||||
|
*/
|
||||||
|
@Output()
|
||||||
|
cancel = new EventEmitter<any>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* EventEmitter to return the selected UUIDs when the confirm button is pressed
|
||||||
|
* @type {EventEmitter<string[]>}
|
||||||
|
*/
|
||||||
|
@Output()
|
||||||
|
confirm: EventEmitter<string[]> = new EventEmitter<string[]>();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether or not to render the confirm button as danger (for example if confirm deletes objects)
|
||||||
|
* Defaults to false
|
||||||
|
*/
|
||||||
|
@Input()
|
||||||
|
dangerConfirm = false;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The list of selected UUIDs
|
||||||
|
*/
|
||||||
|
selectedIds$: Observable<string[]>;
|
||||||
|
|
||||||
|
constructor(protected objectSelectService: ObjectSelectService) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.selectedIds$ = this.objectSelectService.getAllSelected(this.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnDestroy(): void {
|
||||||
|
this.objectSelectService.reset(this.key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Switch the state of a checkbox
|
||||||
|
* @param {string} id
|
||||||
|
*/
|
||||||
|
switch(id: string) {
|
||||||
|
this.objectSelectService.switch(this.key, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current state of a checkbox
|
||||||
|
* @param {string} id The dso's UUID
|
||||||
|
* @returns {Observable<boolean>}
|
||||||
|
*/
|
||||||
|
getSelected(id: string): Observable<boolean> {
|
||||||
|
return this.objectSelectService.getSelected(this.key, id);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Called when the confirm button is pressed
|
||||||
|
* Sends the selected UUIDs to the parent component
|
||||||
|
*/
|
||||||
|
confirmSelected() {
|
||||||
|
this.selectedIds$.pipe(
|
||||||
|
take(1)
|
||||||
|
).subscribe((ids: string[]) => {
|
||||||
|
this.confirm.emit(ids);
|
||||||
|
this.objectSelectService.reset(this.key);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Fire a cancel event
|
||||||
|
*/
|
||||||
|
onCancel() {
|
||||||
|
this.cancel.emit();
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,4 +1,4 @@
|
|||||||
import { Component, Input } from '@angular/core';
|
import { Component, EventEmitter, Input, Output } from '@angular/core';
|
||||||
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { hasValue, isNotEmpty } from '../empty.util';
|
import { hasValue, isNotEmpty } from '../empty.util';
|
||||||
@@ -56,6 +56,11 @@ export class SearchFormComponent {
|
|||||||
*/
|
*/
|
||||||
@Input() brandColor = 'primary';
|
@Input() brandColor = 'primary';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Output the search data on submit
|
||||||
|
*/
|
||||||
|
@Output() submitSearch = new EventEmitter<any>();
|
||||||
|
|
||||||
constructor(private router: Router, private searchService: SearchService) {
|
constructor(private router: Router, private searchService: SearchService) {
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -65,6 +70,7 @@ export class SearchFormComponent {
|
|||||||
*/
|
*/
|
||||||
onSubmit(data: any) {
|
onSubmit(data: any) {
|
||||||
this.updateSearch(data);
|
this.updateSearch(data);
|
||||||
|
this.submitSearch.emit(data);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -36,6 +36,7 @@ import { WrapperGridElementComponent } from './object-grid/wrapper-grid-element/
|
|||||||
import { ObjectGridComponent } from './object-grid/object-grid.component';
|
import { ObjectGridComponent } from './object-grid/object-grid.component';
|
||||||
import { ObjectCollectionComponent } from './object-collection/object-collection.component';
|
import { ObjectCollectionComponent } from './object-collection/object-collection.component';
|
||||||
import { ComcolPageContentComponent } from './comcol-page-content/comcol-page-content.component';
|
import { ComcolPageContentComponent } from './comcol-page-content/comcol-page-content.component';
|
||||||
|
import { ComcolPageHandleComponent } from './comcol-page-handle/comcol-page-handle.component';
|
||||||
import { ComcolPageHeaderComponent } from './comcol-page-header/comcol-page-header.component';
|
import { ComcolPageHeaderComponent } from './comcol-page-header/comcol-page-header.component';
|
||||||
import { ComcolPageLogoComponent } from './comcol-page-logo/comcol-page-logo.component';
|
import { ComcolPageLogoComponent } from './comcol-page-logo/comcol-page-logo.component';
|
||||||
import { ErrorComponent } from './error/error.component';
|
import { ErrorComponent } from './error/error.component';
|
||||||
@@ -140,11 +141,14 @@ import { ClaimedTaskActionsReturnToPoolComponent } from './mydspace-actions/clai
|
|||||||
import { ItemDetailPreviewFieldComponent } from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component';
|
import { ItemDetailPreviewFieldComponent } from './object-detail/my-dspace-result-detail-element/item-detail-preview/item-detail-preview-field/item-detail-preview-field.component';
|
||||||
import { AbstractTrackableComponent } from './trackable/abstract-trackable.component';
|
import { AbstractTrackableComponent } from './trackable/abstract-trackable.component';
|
||||||
import { ComcolMetadataComponent } from './comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component';
|
import { ComcolMetadataComponent } from './comcol-forms/edit-comcol-page/comcol-metadata/comcol-metadata.component';
|
||||||
|
import { ItemSelectComponent } from './object-select/item-select/item-select.component';
|
||||||
|
import { CollectionSelectComponent } from './object-select/collection-select/collection-select.component';
|
||||||
import { FilterInputSuggestionsComponent } from './input-suggestions/filter-suggestions/filter-input-suggestions.component';
|
import { FilterInputSuggestionsComponent } from './input-suggestions/filter-suggestions/filter-input-suggestions.component';
|
||||||
import { DsoInputSuggestionsComponent } from './input-suggestions/dso-input-suggestions/dso-input-suggestions.component';
|
import { DsoInputSuggestionsComponent } from './input-suggestions/dso-input-suggestions/dso-input-suggestions.component';
|
||||||
import { TypedItemSearchResultGridElementComponent } from './object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component';
|
import { TypedItemSearchResultGridElementComponent } from './object-grid/item-grid-element/item-types/typed-item-search-result-grid-element.component';
|
||||||
import { PublicationGridElementComponent } from './object-grid/item-grid-element/item-types/publication/publication-grid-element.component';
|
import { PublicationGridElementComponent } from './object-grid/item-grid-element/item-types/publication/publication-grid-element.component';
|
||||||
import { ItemTypeBadgeComponent } from './object-list/item-type-badge/item-type-badge.component';
|
import { ItemTypeBadgeComponent } from './object-list/item-type-badge/item-type-badge.component';
|
||||||
|
import { ItemMetadataRepresentationListElementComponent } from './object-list/metadata-representation-list-element/item/item-metadata-representation-list-element.component';
|
||||||
|
|
||||||
const MODULES = [
|
const MODULES = [
|
||||||
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
// Do NOT include UniversalModule, HttpModule, or JsonpModule here
|
||||||
@@ -194,6 +198,7 @@ const COMPONENTS = [
|
|||||||
UserMenuComponent,
|
UserMenuComponent,
|
||||||
ChipsComponent,
|
ChipsComponent,
|
||||||
ComcolPageContentComponent,
|
ComcolPageContentComponent,
|
||||||
|
ComcolPageHandleComponent,
|
||||||
ComcolPageHeaderComponent,
|
ComcolPageHeaderComponent,
|
||||||
ComcolPageLogoComponent,
|
ComcolPageLogoComponent,
|
||||||
ComColFormComponent,
|
ComColFormComponent,
|
||||||
@@ -270,7 +275,9 @@ const COMPONENTS = [
|
|||||||
BrowseByComponent,
|
BrowseByComponent,
|
||||||
AbstractTrackableComponent,
|
AbstractTrackableComponent,
|
||||||
ComcolMetadataComponent,
|
ComcolMetadataComponent,
|
||||||
ItemTypeBadgeComponent
|
ItemTypeBadgeComponent,
|
||||||
|
ItemSelectComponent,
|
||||||
|
CollectionSelectComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const ENTRY_COMPONENTS = [
|
const ENTRY_COMPONENTS = [
|
||||||
@@ -314,7 +321,8 @@ const ENTRY_COMPONENTS = [
|
|||||||
StartsWithTextComponent,
|
StartsWithTextComponent,
|
||||||
PlainTextMetadataListElementComponent,
|
PlainTextMetadataListElementComponent,
|
||||||
ItemMetadataListElementComponent,
|
ItemMetadataListElementComponent,
|
||||||
MetadataRepresentationListElementComponent
|
MetadataRepresentationListElementComponent,
|
||||||
|
ItemMetadataRepresentationListElementComponent
|
||||||
];
|
];
|
||||||
|
|
||||||
const SHARED_ITEM_PAGE_COMPONENTS = [
|
const SHARED_ITEM_PAGE_COMPONENTS = [
|
||||||
|
@@ -7,20 +7,28 @@ import { BehaviorSubject } from 'rxjs';
|
|||||||
export class ActivatedRouteStub {
|
export class ActivatedRouteStub {
|
||||||
|
|
||||||
private _testParams?: any;
|
private _testParams?: any;
|
||||||
|
private _testData?: any;
|
||||||
// ActivatedRoute.params is Observable
|
// ActivatedRoute.params is Observable
|
||||||
private subject?: BehaviorSubject<any> = new BehaviorSubject(this.testParams);
|
private subject?: BehaviorSubject<any> = new BehaviorSubject(this.testParams);
|
||||||
|
private dataSubject?: BehaviorSubject<any> = new BehaviorSubject(this.testData);
|
||||||
|
|
||||||
params = this.subject.asObservable();
|
params = this.subject.asObservable();
|
||||||
queryParams = this.subject.asObservable();
|
queryParams = this.subject.asObservable();
|
||||||
paramMap = this.subject.asObservable().pipe(map((params: Params) => convertToParamMap(params)));;
|
paramMap = this.subject.asObservable().pipe(map((params: Params) => convertToParamMap(params)));;
|
||||||
queryParamMap = this.subject.asObservable().pipe(map((params: Params) => convertToParamMap(params)));
|
queryParamMap = this.subject.asObservable().pipe(map((params: Params) => convertToParamMap(params)));
|
||||||
|
data = this.dataSubject.asObservable();
|
||||||
|
|
||||||
constructor(params?: Params) {
|
constructor(params?: Params, data?: any) {
|
||||||
if (params) {
|
if (params) {
|
||||||
this.testParams = params;
|
this.testParams = params;
|
||||||
} else {
|
} else {
|
||||||
this.testParams = {};
|
this.testParams = {};
|
||||||
}
|
}
|
||||||
|
if (data) {
|
||||||
|
this.testData = data;
|
||||||
|
} else {
|
||||||
|
this.testData = {};
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Test parameters
|
// Test parameters
|
||||||
@@ -33,6 +41,16 @@ export class ActivatedRouteStub {
|
|||||||
this.subject.next(params);
|
this.subject.next(params);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Test data
|
||||||
|
get testData() {
|
||||||
|
return this._testParams;
|
||||||
|
}
|
||||||
|
|
||||||
|
set testData(data: {}) {
|
||||||
|
this._testData = data;
|
||||||
|
this.dataSubject.next(data);
|
||||||
|
}
|
||||||
|
|
||||||
// ActivatedRoute.snapshot.params
|
// ActivatedRoute.snapshot.params
|
||||||
get snapshot() {
|
get snapshot() {
|
||||||
return {
|
return {
|
||||||
|
38
src/app/shared/testing/object-select-service-stub.ts
Normal file
38
src/app/shared/testing/object-select-service-stub.ts
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { of } from 'rxjs/internal/observable/of';
|
||||||
|
|
||||||
|
export class ObjectSelectServiceStub {
|
||||||
|
|
||||||
|
ids: string[] = [];
|
||||||
|
|
||||||
|
constructor(ids?: string[]) {
|
||||||
|
if (ids) {
|
||||||
|
this.ids = ids;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getSelected(id: string): Observable<boolean> {
|
||||||
|
if (this.ids.indexOf(id) > -1) {
|
||||||
|
return of(true);
|
||||||
|
} else {
|
||||||
|
return of(false);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
getAllSelected(): Observable<string[]> {
|
||||||
|
return of(this.ids);
|
||||||
|
}
|
||||||
|
|
||||||
|
switch(id: string) {
|
||||||
|
const index = this.ids.indexOf(id);
|
||||||
|
if (index > -1) {
|
||||||
|
this.ids.splice(index, 1);
|
||||||
|
} else {
|
||||||
|
this.ids.push(id);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
reset() {
|
||||||
|
this.ids = [];
|
||||||
|
}
|
||||||
|
}
|
@@ -11355,10 +11355,10 @@ webdriver-manager@^12.0.6:
|
|||||||
semver "^5.3.0"
|
semver "^5.3.0"
|
||||||
xml2js "^0.4.17"
|
xml2js "^0.4.17"
|
||||||
|
|
||||||
webdriver-manager@^12.1.6:
|
webdriver-manager@^12.1.7:
|
||||||
version "12.1.6"
|
version "12.1.7"
|
||||||
resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-12.1.6.tgz#9e5410c506d1a7e0a7aa6af91ba3d5bb37f362b6"
|
resolved "https://registry.yarnpkg.com/webdriver-manager/-/webdriver-manager-12.1.7.tgz#ed4eaee8f906b33c146e869b55e850553a1b1162"
|
||||||
integrity sha512-B1mOycNCrbk7xODw7Jgq/mdD3qzPxMaTsnKIQDy2nXlQoyjTrJTTD0vRpEZI9b8RibPEyQvh9zIZ0M1mpOxS3w==
|
integrity sha512-XINj6b8CYuUYC93SG3xPkxlyUc3IJbD6Vvo75CVGuG9uzsefDzWQrhz0Lq8vbPxtb4d63CZdYophF8k8Or/YiA==
|
||||||
dependencies:
|
dependencies:
|
||||||
adm-zip "^0.4.9"
|
adm-zip "^0.4.9"
|
||||||
chalk "^1.1.1"
|
chalk "^1.1.1"
|
||||||
|
Reference in New Issue
Block a user