mirror of
https://github.com/DSpace/dspace-angular.git
synced 2025-10-07 01:54:15 +00:00
Merge remote-tracking branch 'upstream/main' into minor-themed-component-fixes_contribute-main
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -1,4 +1,5 @@
|
|||||||
/.angular/cache
|
/.angular/cache
|
||||||
|
/.nx
|
||||||
/__build__
|
/__build__
|
||||||
/__server_build__
|
/__server_build__
|
||||||
/node_modules
|
/node_modules
|
||||||
|
18
angular.json
18
angular.json
@@ -109,22 +109,22 @@
|
|||||||
"serve": {
|
"serve": {
|
||||||
"builder": "@angular-builders/custom-webpack:dev-server",
|
"builder": "@angular-builders/custom-webpack:dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "dspace-angular:build",
|
"buildTarget": "dspace-angular:build",
|
||||||
"port": 4000
|
"port": 4000
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"development": {
|
"development": {
|
||||||
"browserTarget": "dspace-angular:build:development"
|
"buildTarget": "dspace-angular:build:development"
|
||||||
},
|
},
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "dspace-angular:build:production"
|
"buildTarget": "dspace-angular:build:production"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"extract-i18n": {
|
"extract-i18n": {
|
||||||
"builder": "@angular-devkit/build-angular:extract-i18n",
|
"builder": "@angular-devkit/build-angular:extract-i18n",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "dspace-angular:build"
|
"buildTarget": "dspace-angular:build"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"test": {
|
"test": {
|
||||||
@@ -217,23 +217,23 @@
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
"serve-ssr": {
|
"serve-ssr": {
|
||||||
"builder": "@nguniversal/builders:ssr-dev-server",
|
"builder": "@angular-devkit/build-angular:ssr-dev-server",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "dspace-angular:build",
|
"buildTarget": "dspace-angular:build",
|
||||||
"serverTarget": "dspace-angular:server",
|
"serverTarget": "dspace-angular:server",
|
||||||
"port": 4000
|
"port": 4000
|
||||||
},
|
},
|
||||||
"configurations": {
|
"configurations": {
|
||||||
"production": {
|
"production": {
|
||||||
"browserTarget": "dspace-angular:build:production",
|
"buildTarget": "dspace-angular:build:production",
|
||||||
"serverTarget": "dspace-angular:server:production"
|
"serverTarget": "dspace-angular:server:production"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"prerender": {
|
"prerender": {
|
||||||
"builder": "@nguniversal/builders:prerender",
|
"builder": "@angular-devkit/build-angular:prerender",
|
||||||
"options": {
|
"options": {
|
||||||
"browserTarget": "dspace-angular:build:production",
|
"buildTarget": "dspace-angular:build:production",
|
||||||
"serverTarget": "dspace-angular:server:production",
|
"serverTarget": "dspace-angular:server:production",
|
||||||
"routes": [
|
"routes": [
|
||||||
"/"
|
"/"
|
||||||
|
15
cypress/e2e/item-template.cy.ts
Normal file
15
cypress/e2e/item-template.cy.ts
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
const ADD_TEMPLATE_ITEM_PAGE = '/collections/'.concat(Cypress.env('DSPACE_TEST_COLLECTION')).concat('/itemtemplate');
|
||||||
|
|
||||||
|
describe('Item Template', () => {
|
||||||
|
beforeEach(() => {
|
||||||
|
cy.visit(ADD_TEMPLATE_ITEM_PAGE);
|
||||||
|
cy.loginViaForm(Cypress.env('DSPACE_TEST_ADMIN_USER'), Cypress.env('DSPACE_TEST_ADMIN_PASSWORD'));
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should load properly', () => {
|
||||||
|
cy.contains('.ds-header-row .lbl-cell', 'Field', { timeout: 10000 }).should('exist').should('be.visible');
|
||||||
|
cy.contains('.ds-header-row b', 'Value', { timeout: 10000 }).should('exist').should('be.visible');
|
||||||
|
cy.contains('.ds-header-row b', 'Lang', { timeout: 10000 }).should('exist').should('be.visible');
|
||||||
|
cy.contains('.ds-header-row b', 'Edit', { timeout: 10000 }).should('exist').should('be.visible');
|
||||||
|
});
|
||||||
|
});
|
@@ -4,10 +4,11 @@
|
|||||||
"**/*.ts"
|
"**/*.ts"
|
||||||
],
|
],
|
||||||
"compilerOptions": {
|
"compilerOptions": {
|
||||||
|
"sourceMap": false,
|
||||||
"types": [
|
"types": [
|
||||||
"cypress",
|
"cypress",
|
||||||
"cypress-axe",
|
"cypress-axe",
|
||||||
"node"
|
"node"
|
||||||
]
|
]
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -20,7 +20,7 @@ the Docker compose scripts in this 'docker' folder.
|
|||||||
|
|
||||||
### Dockerfile
|
### Dockerfile
|
||||||
|
|
||||||
This Dockerfile is used to build a *development* DSpace 7 Angular UI image, published as 'dspace/dspace-angular'
|
This Dockerfile is used to build a *development* DSpace Angular UI image, published as 'dspace/dspace-angular'
|
||||||
|
|
||||||
```
|
```
|
||||||
docker build -t dspace/dspace-angular:latest .
|
docker build -t dspace/dspace-angular:latest .
|
||||||
@@ -46,11 +46,11 @@ A default/demo version of this image is built *automatically*.
|
|||||||
|
|
||||||
## 'docker' directory
|
## 'docker' directory
|
||||||
- docker-compose.yml
|
- 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.
|
- Starts DSpace Angular with Docker Compose from the current branch. This file assumes that a DSpace REST instance will also be started in Docker.
|
||||||
- docker-compose-rest.yml
|
- docker-compose-rest.yml
|
||||||
- Runs a published instance of the DSpace 7 REST API - persists data in Docker volumes
|
- Runs a published instance of the DSpace REST API - persists data in Docker volumes
|
||||||
- docker-compose-ci.yml
|
- docker-compose-ci.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.
|
- Runs a published instance of the DSpace REST API for CI testing. The database is re-populated from a SQL dump on each startup.
|
||||||
- cli.yml
|
- cli.yml
|
||||||
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
- Docker compose file that provides a DSpace CLI container to work with a running DSpace REST container.
|
||||||
- cli.assetstore.yml
|
- cli.assetstore.yml
|
||||||
@@ -71,7 +71,7 @@ docker-compose -f docker/docker-compose.yml build
|
|||||||
|
|
||||||
This command provides a quick way to start both the frontend & backend from this single codebase
|
This command provides a quick way to start both the frontend & backend from this single codebase
|
||||||
```
|
```
|
||||||
docker-compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d
|
docker-compose -p d8 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
Keep in mind, you may also start the backend by cloning the 'DSpace/DSpace' GitHub repository separately. See the next section.
|
Keep in mind, you may also start the backend by cloning the 'DSpace/DSpace' GitHub repository separately. See the next section.
|
||||||
@@ -86,14 +86,14 @@ _The system will be started in 2 steps. Each step shares the same docker network
|
|||||||
|
|
||||||
From 'DSpace/DSpace' clone (build first as needed):
|
From 'DSpace/DSpace' clone (build first as needed):
|
||||||
```
|
```
|
||||||
docker-compose -p d7 up -d
|
docker-compose -p d8 up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
NOTE: More detailed instructions on starting the backend via Docker can be found in the [Docker Compose instructions for the Backend](https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md).
|
NOTE: More detailed instructions on starting the backend via Docker can be found in the [Docker Compose instructions for the Backend](https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/README.md).
|
||||||
|
|
||||||
From 'DSpace/dspace-angular' clone (build first as needed)
|
From 'DSpace/dspace-angular' clone (build first as needed)
|
||||||
```
|
```
|
||||||
docker-compose -p d7 -f docker/docker-compose.yml up -d
|
docker-compose -p d8 -f docker/docker-compose.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
At this point, you should be able to access the UI from http://localhost:4000,
|
At this point, you should be able to access the UI from http://localhost:4000,
|
||||||
@@ -107,19 +107,19 @@ This allows you to run the Angular UI in *production* mode, pointing it at the d
|
|||||||
```
|
```
|
||||||
docker-compose -f docker/docker-compose-dist.yml pull
|
docker-compose -f docker/docker-compose-dist.yml pull
|
||||||
docker-compose -f docker/docker-compose-dist.yml build
|
docker-compose -f docker/docker-compose-dist.yml build
|
||||||
docker-compose -p d7 -f docker/docker-compose-dist.yml up -d
|
docker-compose -p d8 -f docker/docker-compose-dist.yml up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
## Ingest test data from AIPDIR
|
## Ingest test data from AIPDIR
|
||||||
|
|
||||||
Create an administrator
|
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
|
docker-compose -p d8 -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
|
Load content from AIP files
|
||||||
```
|
```
|
||||||
docker-compose -p d7 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
|
docker-compose -p d8 -f docker/cli.yml -f ./docker/cli.ingest.yml run --rm dspace-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
## Alternative Ingest - Use Entities dataset
|
## Alternative Ingest - Use Entities dataset
|
||||||
@@ -127,12 +127,12 @@ _Delete your docker volumes or use a unique project (-p) name_
|
|||||||
|
|
||||||
Start DSpace with Database Content from a database dump
|
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
|
docker-compose -p d8 -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
|
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
|
docker-compose -p d8 -f docker/cli.yml -f docker/cli.assetstore.yml run --rm dspace-cli
|
||||||
```
|
```
|
||||||
|
|
||||||
## End to end testing of the REST API (runs in GitHub Actions CI).
|
## End to end testing of the REST API (runs in GitHub Actions CI).
|
||||||
@@ -140,5 +140,5 @@ _In this instance, only the REST api runs in Docker using the Entities dataset.
|
|||||||
|
|
||||||
This command is only really useful for testing our Continuous Integration process.
|
This command is only really useful for testing our Continuous Integration process.
|
||||||
```
|
```
|
||||||
docker-compose -p d7ci -f docker/docker-compose-ci.yml up -d
|
docker-compose -p d8ci -f docker/docker-compose-ci.yml up -d
|
||||||
```
|
```
|
||||||
|
@@ -33,6 +33,7 @@ services:
|
|||||||
# Tell Statistics to commit all views immediately instead of waiting on Solr's autocommit.
|
# Tell Statistics to commit all views immediately instead of waiting on Solr's autocommit.
|
||||||
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
|
# This allows us to generate statistics in e2e tests so that statistics pages can be tested thoroughly.
|
||||||
solr__D__statistics__P__autoCommit: 'false'
|
solr__D__statistics__P__autoCommit: 'false'
|
||||||
|
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
|
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
@@ -60,15 +61,19 @@ services:
|
|||||||
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
# NOTE: This is customized to use our loadsql image, so that we are using a database with existing test data
|
||||||
dspacedb:
|
dspacedb:
|
||||||
container_name: dspacedb
|
container_name: dspacedb
|
||||||
|
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest-loadsql}"
|
||||||
environment:
|
environment:
|
||||||
# This LOADSQL should be kept in sync with the LOADSQL in
|
# This LOADSQL should be kept in sync with the LOADSQL in
|
||||||
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
|
# https://github.com/DSpace/DSpace/blob/main/dspace/src/main/docker-compose/db.entities.yml
|
||||||
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
# This SQL is available from https://github.com/DSpace-Labs/AIP-Files/releases/tag/demo-entities-data
|
||||||
LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
|
LOADSQL: https://github.com/DSpace-Labs/AIP-Files/releases/download/demo-entities-data/dspace7-entities-data.sql
|
||||||
PGDATA: /pgdata
|
PGDATA: /pgdata
|
||||||
image: dspace/dspace-postgres-pgcrypto:loadsql
|
POSTGRES_PASSWORD: dspace
|
||||||
networks:
|
networks:
|
||||||
- dspacenet
|
- dspacenet
|
||||||
|
ports:
|
||||||
|
- published: 5432
|
||||||
|
target: 5432
|
||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
volumes:
|
volumes:
|
||||||
@@ -105,6 +110,8 @@ services:
|
|||||||
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
|
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
|
||||||
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
|
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
|
||||||
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
|
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
|
||||||
|
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
|
||||||
|
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
|
||||||
exec solr -f
|
exec solr -f
|
||||||
volumes:
|
volumes:
|
||||||
assetstore:
|
assetstore:
|
||||||
|
@@ -29,8 +29,9 @@ services:
|
|||||||
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
|
# __D__ => "-" (e.g. google__D__metadata => google-metadata)
|
||||||
# dspace.dir, dspace.server.url, dspace.ui.url and dspace.name
|
# dspace.dir, dspace.server.url, dspace.ui.url and dspace.name
|
||||||
dspace__P__dir: /dspace
|
dspace__P__dir: /dspace
|
||||||
dspace__P__server__P__url: http://localhost:8080/server
|
# Uncomment to set a non-default value for dspace.server.url or dspace.ui.url
|
||||||
dspace__P__ui__P__url: http://localhost:4000
|
# dspace__P__server__P__url: http://localhost:8080/server
|
||||||
|
# dspace__P__ui__P__url: http://localhost:4000
|
||||||
dspace__P__name: 'DSpace Started with Docker Compose'
|
dspace__P__name: 'DSpace Started with Docker Compose'
|
||||||
# db.url: Ensure we are using the 'dspacedb' image for our database
|
# db.url: Ensure we are using the 'dspacedb' image for our database
|
||||||
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
|
db__P__url: 'jdbc:postgresql://dspacedb:5432/dspace'
|
||||||
@@ -39,6 +40,7 @@ services:
|
|||||||
# proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests
|
# proxies.trusted.ipranges: This setting is required for a REST API running in Docker to trust requests
|
||||||
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
# from the host machine. This IP range MUST correspond to the 'dspacenet' subnet defined above.
|
||||||
proxies__P__trusted__P__ipranges: '172.23.0'
|
proxies__P__trusted__P__ipranges: '172.23.0'
|
||||||
|
LOGGING_CONFIG: /dspace/config/log4j2-container.xml
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
|
image: "${DOCKER_OWNER:-dspace}/dspace:${DSPACE_VER:-latest-test}"
|
||||||
depends_on:
|
depends_on:
|
||||||
- dspacedb
|
- dspacedb
|
||||||
@@ -50,6 +52,7 @@ services:
|
|||||||
stdin_open: true
|
stdin_open: true
|
||||||
tty: true
|
tty: true
|
||||||
volumes:
|
volumes:
|
||||||
|
# Keep DSpace assetstore directory between reboots
|
||||||
- assetstore:/dspace/assetstore
|
- assetstore:/dspace/assetstore
|
||||||
# Ensure that the database is ready BEFORE starting tomcat
|
# Ensure that the database is ready BEFORE starting tomcat
|
||||||
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
# 1. While a TCP connection to dspacedb port 5432 is not available, continue to sleep
|
||||||
@@ -65,9 +68,11 @@ services:
|
|||||||
# DSpace database container
|
# DSpace database container
|
||||||
dspacedb:
|
dspacedb:
|
||||||
container_name: dspacedb
|
container_name: dspacedb
|
||||||
|
# Uses a custom Postgres image with pgcrypto installed
|
||||||
|
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}"
|
||||||
environment:
|
environment:
|
||||||
PGDATA: /pgdata
|
PGDATA: /pgdata
|
||||||
image: "${DOCKER_OWNER:-dspace}/dspace-postgres-pgcrypto:${DSPACE_VER:-latest}"
|
POSTGRES_PASSWORD: dspace
|
||||||
networks:
|
networks:
|
||||||
- dspacenet
|
- dspacenet
|
||||||
ports:
|
ports:
|
||||||
@@ -113,6 +118,8 @@ services:
|
|||||||
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
|
cp -r /opt/solr/server/solr/configsets/statistics/* statistics
|
||||||
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
|
precreate-core qaevent /opt/solr/server/solr/configsets/qaevent
|
||||||
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
|
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
|
||||||
|
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
|
||||||
|
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
|
||||||
exec solr -f
|
exec solr -f
|
||||||
volumes:
|
volumes:
|
||||||
assetstore:
|
assetstore:
|
||||||
|
60
package.json
60
package.json
@@ -55,17 +55,18 @@
|
|||||||
"ts-node": "10.2.1"
|
"ts-node": "10.2.1"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@angular/animations": "^16.2.12",
|
"@angular/animations": "^17.3.4",
|
||||||
"@angular/cdk": "^16.2.12",
|
"@angular/cdk": "^17.3.4",
|
||||||
"@angular/common": "^16.2.12",
|
"@angular/common": "^17.3.4",
|
||||||
"@angular/compiler": "^16.2.12",
|
"@angular/compiler": "^17.3.4",
|
||||||
"@angular/core": "^16.2.12",
|
"@angular/core": "^17.3.4",
|
||||||
"@angular/forms": "^16.2.12",
|
"@angular/forms": "^17.3.4",
|
||||||
"@angular/localize": "16.2.12",
|
"@angular/localize": "17.3.4",
|
||||||
"@angular/platform-browser": "^16.2.12",
|
"@angular/platform-browser": "^17.3.4",
|
||||||
"@angular/platform-browser-dynamic": "^16.2.12",
|
"@angular/platform-browser-dynamic": "^17.3.4",
|
||||||
"@angular/platform-server": "^16.2.12",
|
"@angular/platform-server": "^17.3.4",
|
||||||
"@angular/router": "^16.2.12",
|
"@angular/router": "^17.3.4",
|
||||||
|
"@angular/ssr": "^17.3.0",
|
||||||
"@babel/runtime": "7.21.0",
|
"@babel/runtime": "7.21.0",
|
||||||
"@kolkov/ngx-gallery": "^2.0.1",
|
"@kolkov/ngx-gallery": "^2.0.1",
|
||||||
"@material-ui/core": "^4.11.0",
|
"@material-ui/core": "^4.11.0",
|
||||||
@@ -73,10 +74,9 @@
|
|||||||
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
"@ng-bootstrap/ng-bootstrap": "^11.0.0",
|
||||||
"@ng-dynamic-forms/core": "^16.0.0",
|
"@ng-dynamic-forms/core": "^16.0.0",
|
||||||
"@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
|
"@ng-dynamic-forms/ui-ng-bootstrap": "^16.0.0",
|
||||||
"@ngrx/effects": "^16.3.0",
|
"@ngrx/effects": "^17.1.1",
|
||||||
"@ngrx/router-store": "^16.3.0",
|
"@ngrx/router-store": "^17.1.1",
|
||||||
"@ngrx/store": "^16.3.0",
|
"@ngrx/store": "^17.1.1",
|
||||||
"@nguniversal/express-engine": "^16.2.0",
|
|
||||||
"@ngx-translate/core": "^14.0.0",
|
"@ngx-translate/core": "^14.0.0",
|
||||||
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
|
"@nicky-lenaers/ngx-scroll-to": "^14.0.0",
|
||||||
"@types/grecaptcha": "^3.0.4",
|
"@types/grecaptcha": "^3.0.4",
|
||||||
@@ -130,24 +130,23 @@
|
|||||||
"sortablejs": "1.15.0",
|
"sortablejs": "1.15.0",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"webfontloader": "1.6.28",
|
"webfontloader": "1.6.28",
|
||||||
"zone.js": "~0.13.3"
|
"zone.js": "~0.14.4"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@angular-builders/custom-webpack": "~16.0.0",
|
"@angular-builders/custom-webpack": "~17.0.1",
|
||||||
"@angular-devkit/build-angular": "^16.2.12",
|
"@angular-devkit/build-angular": "^17.3.0",
|
||||||
"@angular-eslint/builder": "16.3.1",
|
"@angular-eslint/builder": "17.2.1",
|
||||||
"@angular-eslint/eslint-plugin": "16.3.1",
|
"@angular-eslint/eslint-plugin": "17.2.1",
|
||||||
"@angular-eslint/eslint-plugin-template": "16.3.1",
|
"@angular-eslint/eslint-plugin-template": "17.2.1",
|
||||||
"@angular-eslint/schematics": "16.3.1",
|
"@angular-eslint/schematics": "17.2.1",
|
||||||
"@angular-eslint/template-parser": "16.3.1",
|
"@angular-eslint/template-parser": "17.2.1",
|
||||||
"@angular/cli": "^16.2.12",
|
"@angular/cli": "^17.3.0",
|
||||||
"@angular/compiler-cli": "^16.2.12",
|
"@angular/compiler-cli": "^17.3.4",
|
||||||
"@angular/language-service": "^16.2.12",
|
"@angular/language-service": "^17.3.4",
|
||||||
"@cypress/schematic": "^1.5.0",
|
"@cypress/schematic": "^1.5.0",
|
||||||
"@fortawesome/fontawesome-free": "^6.4.0",
|
"@fortawesome/fontawesome-free": "^6.4.0",
|
||||||
"@ngrx/store-devtools": "^16.3.0",
|
"@ngrx/store-devtools": "^17.1.1",
|
||||||
"@ngtools/webpack": "^16.2.12",
|
"@ngtools/webpack": "^16.2.12",
|
||||||
"@nguniversal/builders": "^16.2.0",
|
|
||||||
"@types/deep-freeze": "0.1.2",
|
"@types/deep-freeze": "0.1.2",
|
||||||
"@types/ejs": "^3.1.2",
|
"@types/ejs": "^3.1.2",
|
||||||
"@types/express": "^4.17.17",
|
"@types/express": "^4.17.17",
|
||||||
@@ -159,6 +158,7 @@
|
|||||||
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
"@typescript-eslint/eslint-plugin": "^5.59.1",
|
||||||
"@typescript-eslint/parser": "^5.59.1",
|
"@typescript-eslint/parser": "^5.59.1",
|
||||||
"axe-core": "^4.7.2",
|
"axe-core": "^4.7.2",
|
||||||
|
"browser-sync": "^3.0.0",
|
||||||
"compression-webpack-plugin": "^9.2.0",
|
"compression-webpack-plugin": "^9.2.0",
|
||||||
"copy-webpack-plugin": "^6.4.1",
|
"copy-webpack-plugin": "^6.4.1",
|
||||||
"cross-env": "^7.0.3",
|
"cross-env": "^7.0.3",
|
||||||
@@ -200,10 +200,10 @@
|
|||||||
"sass-loader": "^12.6.0",
|
"sass-loader": "^12.6.0",
|
||||||
"sass-resources-loader": "^2.2.5",
|
"sass-resources-loader": "^2.2.5",
|
||||||
"ts-node": "^8.10.2",
|
"ts-node": "^8.10.2",
|
||||||
"typescript": "~4.9.3",
|
"typescript": "~5.3.3",
|
||||||
"webpack": "5.76.1",
|
"webpack": "5.76.1",
|
||||||
"webpack-bundle-analyzer": "^4.8.0",
|
"webpack-bundle-analyzer": "^4.8.0",
|
||||||
"webpack-cli": "^4.2.0",
|
"webpack-cli": "^4.2.0",
|
||||||
"webpack-dev-server": "^4.13.3"
|
"webpack-dev-server": "^4.13.3"
|
||||||
}
|
}
|
||||||
}
|
}
|
150
server.ts
150
server.ts
@@ -17,7 +17,6 @@
|
|||||||
|
|
||||||
import 'zone.js/node';
|
import 'zone.js/node';
|
||||||
import 'reflect-metadata';
|
import 'reflect-metadata';
|
||||||
import 'rxjs';
|
|
||||||
|
|
||||||
/* eslint-disable import/no-namespace */
|
/* eslint-disable import/no-namespace */
|
||||||
import * as morgan from 'morgan';
|
import * as morgan from 'morgan';
|
||||||
@@ -39,23 +38,26 @@ import { join } from 'path';
|
|||||||
|
|
||||||
import { enableProdMode } from '@angular/core';
|
import { enableProdMode } from '@angular/core';
|
||||||
|
|
||||||
import { ngExpressEngine } from '@nguniversal/express-engine';
|
|
||||||
import { REQUEST, RESPONSE } from '@nguniversal/express-engine/tokens';
|
|
||||||
|
|
||||||
import { environment } from './src/environments/environment';
|
import { environment } from './src/environments/environment';
|
||||||
import { createProxyMiddleware } from 'http-proxy-middleware';
|
import { createProxyMiddleware } from 'http-proxy-middleware';
|
||||||
import { hasNoValue, hasValue } from './src/app/shared/empty.util';
|
import { hasValue } from './src/app/shared/empty.util';
|
||||||
|
|
||||||
import { UIServerConfig } from './src/config/ui-server-config.interface';
|
import { UIServerConfig } from './src/config/ui-server-config.interface';
|
||||||
|
|
||||||
import bootstrap from './src/main.server';
|
import bootstrap from './src/main.server';
|
||||||
|
|
||||||
import { buildAppConfig } from './src/config/config.server';
|
import { buildAppConfig } from './src/config/config.server';
|
||||||
import { APP_CONFIG, AppConfig } from './src/config/app-config.interface';
|
import {
|
||||||
|
APP_CONFIG,
|
||||||
|
AppConfig,
|
||||||
|
} from './src/config/app-config.interface';
|
||||||
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
import { extendEnvironmentWithAppConfig } from './src/config/config.util';
|
||||||
import { logStartupMessage } from './startup-message';
|
import { logStartupMessage } from './startup-message';
|
||||||
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
|
import { TOKENITEM } from './src/app/core/auth/models/auth-token-info.model';
|
||||||
|
import { CommonEngine } from '@angular/ssr';
|
||||||
|
import { APP_BASE_HREF } from '@angular/common';
|
||||||
|
import {
|
||||||
|
REQUEST,
|
||||||
|
RESPONSE,
|
||||||
|
} from './src/express.tokens';
|
||||||
|
|
||||||
/*
|
/*
|
||||||
* Set path for the browser application's dist folder
|
* Set path for the browser application's dist folder
|
||||||
@@ -127,28 +129,6 @@ export function app() {
|
|||||||
*/
|
*/
|
||||||
server.use(json());
|
server.use(json());
|
||||||
|
|
||||||
// Our Universal express-engine (found @ https://github.com/angular/universal/tree/master/modules/express-engine)
|
|
||||||
server.engine('html', (_, options, callback) =>
|
|
||||||
ngExpressEngine({
|
|
||||||
bootstrap,
|
|
||||||
inlineCriticalCss: environment.universal.inlineCriticalCss,
|
|
||||||
providers: [
|
|
||||||
{
|
|
||||||
provide: REQUEST,
|
|
||||||
useValue: (options as any).req,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: RESPONSE,
|
|
||||||
useValue: (options as any).req.res,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
provide: APP_CONFIG,
|
|
||||||
useValue: environment,
|
|
||||||
},
|
|
||||||
],
|
|
||||||
})(_, (options as any), callback),
|
|
||||||
);
|
|
||||||
|
|
||||||
server.engine('ejs', ejs.renderFile);
|
server.engine('ejs', ejs.renderFile);
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -237,10 +217,10 @@ export function app() {
|
|||||||
/*
|
/*
|
||||||
* The callback function to serve server side angular
|
* The callback function to serve server side angular
|
||||||
*/
|
*/
|
||||||
function ngApp(req, res) {
|
function ngApp(req, res, next) {
|
||||||
if (environment.universal.preboot) {
|
if (environment.ssr.enabled) {
|
||||||
// Render the page to user via SSR (server side rendering)
|
// Render the page to user via SSR (server side rendering)
|
||||||
serverSideRender(req, res);
|
serverSideRender(req, res, next);
|
||||||
} else {
|
} else {
|
||||||
// If preboot is disabled, just serve the client
|
// If preboot is disabled, just serve the client
|
||||||
console.log('Universal off, serving for direct client-side rendering (CSR)');
|
console.log('Universal off, serving for direct client-side rendering (CSR)');
|
||||||
@@ -253,45 +233,66 @@ function ngApp(req, res) {
|
|||||||
* returned to the user.
|
* returned to the user.
|
||||||
* @param req current request
|
* @param req current request
|
||||||
* @param res current response
|
* @param res current response
|
||||||
|
* @param next the next function
|
||||||
* @param sendToUser if true (default), send the rendered content to the user.
|
* @param sendToUser if true (default), send the rendered content to the user.
|
||||||
* If false, then only save this rendered content to the in-memory cache (to refresh cache).
|
* If false, then only save this rendered content to the in-memory cache (to refresh cache).
|
||||||
*/
|
*/
|
||||||
function serverSideRender(req, res, sendToUser: boolean = true) {
|
function serverSideRender(req, res, next, sendToUser: boolean = true) {
|
||||||
|
const { protocol, originalUrl, baseUrl, headers } = req;
|
||||||
|
const commonEngine = new CommonEngine({ enablePerformanceProfiler: environment.ssr.enablePerformanceProfiler });
|
||||||
// Render the page via SSR (server side rendering)
|
// Render the page via SSR (server side rendering)
|
||||||
res.render(indexHtml, {
|
commonEngine
|
||||||
req,
|
.render({
|
||||||
res,
|
bootstrap,
|
||||||
preboot: environment.universal.preboot,
|
documentFilePath: indexHtml,
|
||||||
async: environment.universal.async,
|
inlineCriticalCss: environment.ssr.inlineCriticalCss,
|
||||||
time: environment.universal.time,
|
url: `${protocol}://${headers.host}${originalUrl}`,
|
||||||
baseUrl: environment.ui.nameSpace,
|
publicPath: DIST_FOLDER,
|
||||||
originUrl: environment.ui.baseUrl,
|
providers: [
|
||||||
requestUrl: req.originalUrl,
|
{ provide: APP_BASE_HREF, useValue: baseUrl },
|
||||||
}, (err, data) => {
|
{
|
||||||
if (hasNoValue(err) && hasValue(data)) {
|
provide: REQUEST,
|
||||||
// save server side rendered page to cache (if any are enabled)
|
useValue: req,
|
||||||
saveToCache(req, data);
|
},
|
||||||
if (sendToUser) {
|
{
|
||||||
res.locals.ssr = true; // mark response as SSR (enables text compression)
|
provide: RESPONSE,
|
||||||
// send rendered page to user
|
useValue: res,
|
||||||
res.send(data);
|
},
|
||||||
|
{
|
||||||
|
provide: APP_CONFIG,
|
||||||
|
useValue: environment,
|
||||||
|
},
|
||||||
|
],
|
||||||
|
})
|
||||||
|
.then((html) => {
|
||||||
|
if (hasValue(html)) {
|
||||||
|
// save server side rendered page to cache (if any are enabled)
|
||||||
|
saveToCache(req, html);
|
||||||
|
if (sendToUser) {
|
||||||
|
res.locals.ssr = true; // mark response as SSR (enables text compression)
|
||||||
|
// send rendered page to user
|
||||||
|
res.send(html);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
})
|
||||||
// When this error occurs we can't fall back to CSR because the response has already been
|
.catch((err) => {
|
||||||
// sent. These errors occur for various reasons in universal, not all of which are in our
|
if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') {
|
||||||
// control to solve.
|
// When this error occurs we can't fall back to CSR because the response has already been
|
||||||
console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client');
|
// sent. These errors occur for various reasons in universal, not all of which are in our
|
||||||
} else {
|
// control to solve.
|
||||||
console.warn('Error in server-side rendering (SSR)');
|
console.warn('Warning [ERR_HTTP_HEADERS_SENT]: Tried to set headers after they were sent to the client');
|
||||||
if (hasValue(err)) {
|
} else {
|
||||||
console.warn('Error details : ', err);
|
console.warn('Error in server-side rendering (SSR)');
|
||||||
|
if (hasValue(err)) {
|
||||||
|
console.warn('Error details : ', err);
|
||||||
|
}
|
||||||
|
if (sendToUser) {
|
||||||
|
console.warn('Falling back to serving direct client-side rendering (CSR).');
|
||||||
|
clientSideRender(req, res);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
if (sendToUser) {
|
next(err);
|
||||||
console.warn('Falling back to serving direct client-side rendering (CSR).');
|
});
|
||||||
clientSideRender(req, res);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -349,7 +350,7 @@ function initCache() {
|
|||||||
function botCacheEnabled(): boolean {
|
function botCacheEnabled(): boolean {
|
||||||
// Caching is only enabled if SSR is enabled AND
|
// Caching is only enabled if SSR is enabled AND
|
||||||
// "max" pages to cache is greater than zero
|
// "max" pages to cache is greater than zero
|
||||||
return environment.universal.preboot && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0);
|
return environment.ssr.enabled && environment.cache.serverSide.botCache.max && (environment.cache.serverSide.botCache.max > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -358,7 +359,7 @@ function botCacheEnabled(): boolean {
|
|||||||
function anonymousCacheEnabled(): boolean {
|
function anonymousCacheEnabled(): boolean {
|
||||||
// Caching is only enabled if SSR is enabled AND
|
// Caching is only enabled if SSR is enabled AND
|
||||||
// "max" pages to cache is greater than zero
|
// "max" pages to cache is greater than zero
|
||||||
return environment.universal.preboot && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0);
|
return environment.ssr.enabled && environment.cache.serverSide.anonymousCache.max && (environment.cache.serverSide.anonymousCache.max > 0);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -371,9 +372,9 @@ function cacheCheck(req, res, next) {
|
|||||||
|
|
||||||
// If the bot cache is enabled and this request looks like a bot, check the bot cache for a cached page.
|
// If the bot cache is enabled and this request looks like a bot, check the bot cache for a cached page.
|
||||||
if (botCacheEnabled() && isbot(req.get('user-agent'))) {
|
if (botCacheEnabled() && isbot(req.get('user-agent'))) {
|
||||||
cachedCopy = checkCacheForRequest('bot', botCache, req, res);
|
cachedCopy = checkCacheForRequest('bot', botCache, req, res, next);
|
||||||
} else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) {
|
} else if (anonymousCacheEnabled() && !isUserAuthenticated(req)) {
|
||||||
cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res);
|
cachedCopy = checkCacheForRequest('anonymous', anonymousCache, req, res, next);
|
||||||
}
|
}
|
||||||
|
|
||||||
// If cached copy exists, return it to the user.
|
// If cached copy exists, return it to the user.
|
||||||
@@ -409,9 +410,10 @@ function cacheCheck(req, res, next) {
|
|||||||
* @param cache LRU cache to check
|
* @param cache LRU cache to check
|
||||||
* @param req current request to look for in the cache
|
* @param req current request to look for in the cache
|
||||||
* @param res current response
|
* @param res current response
|
||||||
|
* @param next the next function
|
||||||
* @returns cached copy (if found) or undefined (if not found)
|
* @returns cached copy (if found) or undefined (if not found)
|
||||||
*/
|
*/
|
||||||
function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, res): any {
|
function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, res, next): any {
|
||||||
// Get the cache key for this request
|
// Get the cache key for this request
|
||||||
const key = getCacheKey(req);
|
const key = getCacheKey(req);
|
||||||
|
|
||||||
@@ -427,7 +429,7 @@ function checkCacheForRequest(cacheName: string, cache: LRU<string, any>, req, r
|
|||||||
// Update cached copy by rerendering server-side
|
// Update cached copy by rerendering server-side
|
||||||
// NOTE: In this scenario the currently cached copy will be returned to the current user.
|
// NOTE: In this scenario the currently cached copy will be returned to the current user.
|
||||||
// This re-render is peformed behind the scenes to update cached copy for next user.
|
// This re-render is peformed behind the scenes to update cached copy for next user.
|
||||||
serverSideRender(req, res, false);
|
serverSideRender(req, res, next, false);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); }
|
if (environment.cache.serverSide.debug) { console.log(`CACHE MISS FOR ${key} in ${cacheName} cache.`); }
|
||||||
@@ -531,7 +533,7 @@ function createHttpsServer(keys) {
|
|||||||
const listener = createServer({
|
const listener = createServer({
|
||||||
key: keys.serviceKey,
|
key: keys.serviceKey,
|
||||||
cert: keys.certificate,
|
cert: keys.certificate,
|
||||||
}, app).listen(environment.ui.port, environment.ui.host, () => {
|
}, app()).listen(environment.ui.port, environment.ui.host, () => {
|
||||||
serverStarted();
|
serverStarted();
|
||||||
});
|
});
|
||||||
|
|
||||||
|
@@ -37,7 +37,6 @@
|
|||||||
<ng-template ngbNavContent>
|
<ng-template ngbNavContent>
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
[paginationOptions]="(paginationOptions$ | async)"
|
[paginationOptions]="(paginationOptions$ | async)"
|
||||||
[pageInfoState]="(objectsSelected$|async)?.payload.pageInfo"
|
|
||||||
[collectionSize]="(objectsSelected$|async)?.payload?.totalElements"
|
[collectionSize]="(objectsSelected$|async)?.payload?.totalElements"
|
||||||
[objects]="(objectsSelected$|async)"
|
[objects]="(objectsSelected$|async)"
|
||||||
[showPaginator]="false"
|
[showPaginator]="false"
|
||||||
|
@@ -45,7 +45,6 @@
|
|||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && (searching$ | async) !== true"
|
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && (searching$ | async) !== true"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="pageInfoState$"
|
|
||||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true">
|
[hidePagerWhenSinglePage]="true">
|
||||||
|
@@ -52,7 +52,6 @@
|
|||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(groups$ | async)?.payload?.totalElements > 0"
|
*ngIf="(groups$ | async)?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="groupsPageInfoState$"
|
|
||||||
[collectionSize]="(groups$ | async)?.payload?.totalElements"
|
[collectionSize]="(groups$ | async)?.payload?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
|
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
<ds-pagination *ngIf="(ePeopleMembersOfGroup | async)?.totalElements > 0"
|
<ds-pagination *ngIf="(ePeopleMembersOfGroup | async)?.totalElements > 0"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="(ePeopleMembersOfGroup | async)"
|
|
||||||
[collectionSize]="(ePeopleMembersOfGroup | async)?.totalElements"
|
[collectionSize]="(ePeopleMembersOfGroup | async)?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true">
|
[hidePagerWhenSinglePage]="true">
|
||||||
@@ -86,7 +85,6 @@
|
|||||||
|
|
||||||
<ds-pagination *ngIf="(ePeopleSearch | async)?.totalElements > 0"
|
<ds-pagination *ngIf="(ePeopleSearch | async)?.totalElements > 0"
|
||||||
[paginationOptions]="configSearch"
|
[paginationOptions]="configSearch"
|
||||||
[pageInfoState]="(ePeopleSearch | async)"
|
|
||||||
[collectionSize]="(ePeopleSearch | async)?.totalElements"
|
[collectionSize]="(ePeopleSearch | async)?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true">
|
[hidePagerWhenSinglePage]="true">
|
||||||
|
@@ -5,7 +5,6 @@
|
|||||||
|
|
||||||
<ds-pagination *ngIf="(subGroups$ | async)?.payload?.totalElements > 0"
|
<ds-pagination *ngIf="(subGroups$ | async)?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="(subGroups$ | async)?.payload"
|
|
||||||
[collectionSize]="(subGroups$ | async)?.payload?.totalElements"
|
[collectionSize]="(subGroups$ | async)?.payload?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true">
|
[hidePagerWhenSinglePage]="true">
|
||||||
@@ -84,7 +83,6 @@
|
|||||||
|
|
||||||
<ds-pagination *ngIf="(searchResults$ | async)?.payload?.totalElements > 0"
|
<ds-pagination *ngIf="(searchResults$ | async)?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="configSearch"
|
[paginationOptions]="configSearch"
|
||||||
[pageInfoState]="(searchResults$ | async)?.payload"
|
|
||||||
[collectionSize]="(searchResults$ | async)?.payload?.totalElements"
|
[collectionSize]="(searchResults$ | async)?.payload?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true">
|
[hidePagerWhenSinglePage]="true">
|
||||||
|
@@ -37,7 +37,6 @@
|
|||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && (loading$ | async) !== true"
|
*ngIf="(pageInfoState$ | async)?.totalElements > 0 && (loading$ | async) !== true"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="pageInfoState$"
|
|
||||||
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
[collectionSize]="(pageInfoState$ | async)?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true">
|
[hidePagerWhenSinglePage]="true">
|
||||||
|
@@ -10,7 +10,6 @@
|
|||||||
[collectionSize]="(ldnServicesRD$ | async)?.payload?.totalElements"
|
[collectionSize]="(ldnServicesRD$ | async)?.payload?.totalElements"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
[pageInfoState]="(ldnServicesRD$ | async)?.payload"
|
|
||||||
[paginationOptions]="pageConfig">
|
[paginationOptions]="pageConfig">
|
||||||
<div class="table-responsive">
|
<div class="table-responsive">
|
||||||
<table class="table table-striped table-hover">
|
<table class="table table-striped table-hover">
|
||||||
|
@@ -11,7 +11,6 @@
|
|||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="(bitstreamFormats | async)?.payload?.totalElements > 0"
|
*ngIf="(bitstreamFormats | async)?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="pageConfig"
|
[paginationOptions]="pageConfig"
|
||||||
[pageInfoState]="(bitstreamFormats | async)?.payload"
|
|
||||||
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
|
[collectionSize]="(bitstreamFormats | async)?.payload?.totalElements"
|
||||||
[hideGear]="false"
|
[hideGear]="false"
|
||||||
[hidePagerWhenSinglePage]="true">
|
[hidePagerWhenSinglePage]="true">
|
||||||
@@ -35,7 +34,7 @@
|
|||||||
[checked]="isSelected(bitstreamFormat) | async"
|
[checked]="isSelected(bitstreamFormat) | async"
|
||||||
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
(change)="selectBitStreamFormat(bitstreamFormat, $event)"
|
||||||
>
|
>
|
||||||
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span>
|
<span class="sr-only">{{'admin.registries.bitstream-formats.select' | translate}}}</span>
|
||||||
</label>
|
</label>
|
||||||
</td>
|
</td>
|
||||||
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>
|
<td><a [routerLink]="['/admin/registries/bitstream-formats', bitstreamFormat.id, 'edit']">{{bitstreamFormat.id}}</a></td>
|
||||||
|
@@ -16,7 +16,6 @@
|
|||||||
<ds-pagination
|
<ds-pagination
|
||||||
*ngIf="fields?.totalElements > 0"
|
*ngIf="fields?.totalElements > 0"
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="fields"
|
|
||||||
[collectionSize]="fields?.totalElements"
|
[collectionSize]="fields?.totalElements"
|
||||||
[hideGear]="false"
|
[hideGear]="false"
|
||||||
[hidePagerWhenSinglePage]="true">
|
[hidePagerWhenSinglePage]="true">
|
||||||
|
@@ -23,7 +23,6 @@ import {
|
|||||||
SortDirection,
|
SortDirection,
|
||||||
SortOptions,
|
SortOptions,
|
||||||
} from '../../core/cache/models/sort-options.model';
|
} from '../../core/cache/models/sort-options.model';
|
||||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
|
||||||
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
@@ -39,6 +38,7 @@ import { SEARCH_CONFIG_SERVICE } from '../../my-dspace-page/my-dspace-configurat
|
|||||||
import { ErrorComponent } from '../../shared/error/error.component';
|
import { ErrorComponent } from '../../shared/error/error.component';
|
||||||
import { HostWindowService } from '../../shared/host-window.service';
|
import { HostWindowService } from '../../shared/host-window.service';
|
||||||
import { LoadingComponent } from '../../shared/loading/loading.component';
|
import { LoadingComponent } from '../../shared/loading/loading.component';
|
||||||
|
import { getMockThemeService } from '../../shared/mocks/theme-service.mock';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
import { ItemSelectComponent } from '../../shared/object-select/item-select/item-select.component';
|
import { ItemSelectComponent } from '../../shared/object-select/item-select/item-select.component';
|
||||||
import { ObjectSelectService } from '../../shared/object-select/object-select.service';
|
import { ObjectSelectService } from '../../shared/object-select/object-select.service';
|
||||||
@@ -58,6 +58,7 @@ import { RouterStub } from '../../shared/testing/router.stub';
|
|||||||
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
|
import { SearchConfigurationServiceStub } from '../../shared/testing/search-configuration-service.stub';
|
||||||
import { SearchServiceStub } from '../../shared/testing/search-service.stub';
|
import { SearchServiceStub } from '../../shared/testing/search-service.stub';
|
||||||
import { createPaginatedList } from '../../shared/testing/utils.test';
|
import { createPaginatedList } from '../../shared/testing/utils.test';
|
||||||
|
import { ThemeService } from '../../shared/theme-support/theme.service';
|
||||||
import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
|
import { EnumKeysPipe } from '../../shared/utils/enum-keys-pipe';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
import { CollectionItemMapperComponent } from './collection-item-mapper.component';
|
import { CollectionItemMapperComponent } from './collection-item-mapper.component';
|
||||||
@@ -190,7 +191,6 @@ describe('CollectionItemMapperComponent', () => {
|
|||||||
{ provide: SearchService, useValue: searchServiceStub },
|
{ provide: SearchService, useValue: searchServiceStub },
|
||||||
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
{ provide: ItemDataService, useValue: itemDataServiceStub },
|
{ provide: ItemDataService, useValue: itemDataServiceStub },
|
||||||
{ provide: CollectionDataService, useValue: collectionDataServiceStub },
|
|
||||||
{ provide: TranslateService, useValue: translateServiceStub },
|
{ provide: TranslateService, useValue: translateServiceStub },
|
||||||
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
{ provide: HostWindowService, useValue: new HostWindowServiceStub(0) },
|
||||||
{ provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() },
|
{ provide: ObjectSelectService, useValue: new ObjectSelectServiceStub() },
|
||||||
@@ -199,6 +199,7 @@ describe('CollectionItemMapperComponent', () => {
|
|||||||
{ provide: GroupDataService, useValue: groupDataService },
|
{ provide: GroupDataService, useValue: groupDataService },
|
||||||
{ provide: LinkHeadService, useValue: linkHeadService },
|
{ provide: LinkHeadService, useValue: linkHeadService },
|
||||||
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
{ provide: ConfigurationDataService, useValue: configurationDataService },
|
||||||
|
{ provide: ThemeService, useValue: getMockThemeService() },
|
||||||
],
|
],
|
||||||
}).overrideComponent(CollectionItemMapperComponent, {
|
}).overrideComponent(CollectionItemMapperComponent, {
|
||||||
set: {
|
set: {
|
||||||
|
@@ -35,7 +35,6 @@ import {
|
|||||||
SortDirection,
|
SortDirection,
|
||||||
SortOptions,
|
SortOptions,
|
||||||
} from '../../core/cache/models/sort-options.model';
|
} from '../../core/cache/models/sort-options.model';
|
||||||
import { CollectionDataService } from '../../core/data/collection-data.service';
|
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
import { ItemDataService } from '../../core/data/item-data.service';
|
||||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
@@ -150,7 +149,6 @@ export class CollectionItemMapperComponent implements OnInit {
|
|||||||
private searchService: SearchService,
|
private searchService: SearchService,
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private itemDataService: ItemDataService,
|
private itemDataService: ItemDataService,
|
||||||
private collectionDataService: CollectionDataService,
|
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private dsoNameService: DSONameService) {
|
private dsoNameService: DSONameService) {
|
||||||
}
|
}
|
||||||
@@ -187,6 +185,8 @@ export class CollectionItemMapperComponent implements OnInit {
|
|||||||
this.shouldUpdate$.next(false);
|
this.shouldUpdate$.next(false);
|
||||||
}
|
}
|
||||||
return this.itemDataService.findListByHref(collectionRD.payload._links.mappedItems.href, Object.assign(options, {
|
return this.itemDataService.findListByHref(collectionRD.payload._links.mappedItems.href, Object.assign(options, {
|
||||||
|
currentPage: options.pagination.currentPage,
|
||||||
|
elementsPerPage: options.pagination.pageSize,
|
||||||
sort: this.defaultSortOptions,
|
sort: this.defaultSortOptions,
|
||||||
}),!shouldUpdate, false, followLink('owningCollection')).pipe(
|
}),!shouldUpdate, false, followLink('owningCollection')).pipe(
|
||||||
getAllSucceededRemoteData(),
|
getAllSucceededRemoteData(),
|
||||||
|
@@ -7,6 +7,7 @@ import { browseByGuard } from '../browse-by/browse-by-guard';
|
|||||||
import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
|
import { browseByI18nBreadcrumbResolver } from '../browse-by/browse-by-i18n-breadcrumb.resolver';
|
||||||
import { authenticatedGuard } from '../core/auth/authenticated.guard';
|
import { authenticatedGuard } from '../core/auth/authenticated.guard';
|
||||||
import { collectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver';
|
import { collectionBreadcrumbResolver } from '../core/breadcrumbs/collection-breadcrumb.resolver';
|
||||||
|
import { communityBreadcrumbResolver } from '../core/breadcrumbs/community-breadcrumb.resolver';
|
||||||
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.resolver';
|
||||||
import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component';
|
import { ComcolBrowseByComponent } from '../shared/comcol/sections/comcol-browse-by/comcol-browse-by.component';
|
||||||
import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component';
|
import { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component';
|
||||||
@@ -27,12 +28,29 @@ import { itemTemplatePageResolver } from './edit-item-template-page/item-templat
|
|||||||
import { ThemedEditItemTemplatePageComponent } from './edit-item-template-page/themed-edit-item-template-page.component';
|
import { ThemedEditItemTemplatePageComponent } from './edit-item-template-page/themed-edit-item-template-page.component';
|
||||||
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
import { ThemedCollectionPageComponent } from './themed-collection-page.component';
|
||||||
|
|
||||||
|
|
||||||
export const ROUTES: Route[] = [
|
export const ROUTES: Route[] = [
|
||||||
{
|
{
|
||||||
path: COLLECTION_CREATE_PATH,
|
path: COLLECTION_CREATE_PATH,
|
||||||
component: CreateCollectionPageComponent,
|
|
||||||
canActivate: [authenticatedGuard, createCollectionPageGuard],
|
canActivate: [authenticatedGuard, createCollectionPageGuard],
|
||||||
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CreateCollectionPageComponent,
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: i18nBreadcrumbResolver,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
breadcrumbKey: 'collection.create',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
|
data: {
|
||||||
|
breadcrumbQueryParam: 'parent',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: communityBreadcrumbResolver,
|
||||||
|
},
|
||||||
|
runGuardsAndResolvers: 'always',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
|
@@ -28,8 +28,26 @@ import { ThemedCommunityPageComponent } from './themed-community-page.component'
|
|||||||
export const ROUTES: Route[] = [
|
export const ROUTES: Route[] = [
|
||||||
{
|
{
|
||||||
path: COMMUNITY_CREATE_PATH,
|
path: COMMUNITY_CREATE_PATH,
|
||||||
component: CreateCommunityPageComponent,
|
children: [
|
||||||
|
{
|
||||||
|
path: '',
|
||||||
|
component: CreateCommunityPageComponent,
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: i18nBreadcrumbResolver,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
breadcrumbKey: 'community.create',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
],
|
||||||
canActivate: [authenticatedGuard, createCommunityPageGuard],
|
canActivate: [authenticatedGuard, createCommunityPageGuard],
|
||||||
|
data: {
|
||||||
|
breadcrumbQueryParam: 'parent',
|
||||||
|
},
|
||||||
|
resolve: {
|
||||||
|
breadcrumb: communityBreadcrumbResolver,
|
||||||
|
},
|
||||||
|
runGuardsAndResolvers: 'always',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
|
@@ -12,7 +12,6 @@ import {
|
|||||||
Store,
|
Store,
|
||||||
StoreModule,
|
StoreModule,
|
||||||
} from '@ngrx/store';
|
} from '@ngrx/store';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { cold } from 'jasmine-marbles';
|
import { cold } from 'jasmine-marbles';
|
||||||
import {
|
import {
|
||||||
@@ -20,6 +19,7 @@ import {
|
|||||||
of as observableOf,
|
of as observableOf,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { REQUEST } from '../../../express.tokens';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
import { getMockTranslateService } from '../../shared/mocks/translate.service.mock';
|
||||||
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
import { NotificationsService } from '../../shared/notifications/notifications.service';
|
||||||
|
@@ -9,10 +9,6 @@ import {
|
|||||||
select,
|
select,
|
||||||
Store,
|
Store,
|
||||||
} from '@ngrx/store';
|
} from '@ngrx/store';
|
||||||
import {
|
|
||||||
REQUEST,
|
|
||||||
RESPONSE,
|
|
||||||
} from '@nguniversal/express-engine/tokens';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import { CookieAttributes } from 'js-cookie';
|
import { CookieAttributes } from 'js-cookie';
|
||||||
import {
|
import {
|
||||||
@@ -28,6 +24,10 @@ import {
|
|||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { environment } from '../../../environments/environment';
|
import { environment } from '../../../environments/environment';
|
||||||
|
import {
|
||||||
|
REQUEST,
|
||||||
|
RESPONSE,
|
||||||
|
} from '../../../express.tokens';
|
||||||
import { AppState } from '../../app.reducer';
|
import { AppState } from '../../app.reducer';
|
||||||
import {
|
import {
|
||||||
hasNoValue,
|
hasNoValue,
|
||||||
|
@@ -8,11 +8,15 @@ import { Observable } from 'rxjs';
|
|||||||
|
|
||||||
import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
||||||
import { COMMUNITY_PAGE_LINKS_TO_FOLLOW } from '../../community-page/community-page.resolver';
|
import { COMMUNITY_PAGE_LINKS_TO_FOLLOW } from '../../community-page/community-page.resolver';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
import { CommunityDataService } from '../data/community-data.service';
|
import { CommunityDataService } from '../data/community-data.service';
|
||||||
import { Community } from '../shared/community.model';
|
import { Community } from '../shared/community.model';
|
||||||
import { DSpaceObject } from '../shared/dspace-object.model';
|
import { DSpaceObject } from '../shared/dspace-object.model';
|
||||||
import { DSOBreadcrumbResolver } from './dso-breadcrumb.resolver';
|
import {
|
||||||
|
DSOBreadcrumbResolver,
|
||||||
|
DSOBreadcrumbResolverByUuid,
|
||||||
|
} from './dso-breadcrumb.resolver';
|
||||||
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
|
import { DSOBreadcrumbsService } from './dso-breadcrumbs.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -25,11 +29,22 @@ export const communityBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Community>>
|
|||||||
dataService: CommunityDataService = inject(CommunityDataService),
|
dataService: CommunityDataService = inject(CommunityDataService),
|
||||||
): Observable<BreadcrumbConfig<Community>> => {
|
): Observable<BreadcrumbConfig<Community>> => {
|
||||||
const linksToFollow: FollowLinkConfig<DSpaceObject>[] = COMMUNITY_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig<DSpaceObject>[];
|
const linksToFollow: FollowLinkConfig<DSpaceObject>[] = COMMUNITY_PAGE_LINKS_TO_FOLLOW as FollowLinkConfig<DSpaceObject>[];
|
||||||
return DSOBreadcrumbResolver(
|
if (hasValue(route.data.breadcrumbQueryParam) && hasValue(route.queryParams[route.data.breadcrumbQueryParam])) {
|
||||||
route,
|
return DSOBreadcrumbResolverByUuid(
|
||||||
state,
|
route,
|
||||||
breadcrumbService,
|
state,
|
||||||
dataService,
|
route.queryParams[route.data.breadcrumbQueryParam],
|
||||||
...linksToFollow,
|
breadcrumbService,
|
||||||
) as Observable<BreadcrumbConfig<Community>>;
|
dataService,
|
||||||
|
...linksToFollow,
|
||||||
|
) as Observable<BreadcrumbConfig<Community>>;
|
||||||
|
} else {
|
||||||
|
return DSOBreadcrumbResolver(
|
||||||
|
route,
|
||||||
|
state,
|
||||||
|
breadcrumbService,
|
||||||
|
dataService,
|
||||||
|
...linksToFollow,
|
||||||
|
) as Observable<BreadcrumbConfig<Community>>;
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
@@ -18,7 +18,10 @@ describe('DSOBreadcrumbResolver', () => {
|
|||||||
uuid = '1234-65487-12354-1235';
|
uuid = '1234-65487-12354-1235';
|
||||||
breadcrumbUrl = `/collections/${uuid}`;
|
breadcrumbUrl = `/collections/${uuid}`;
|
||||||
currentUrl = `${breadcrumbUrl}/edit`;
|
currentUrl = `${breadcrumbUrl}/edit`;
|
||||||
testCollection = Object.assign(new Collection(), { uuid });
|
testCollection = Object.assign(new Collection(), {
|
||||||
|
uuid: uuid,
|
||||||
|
type: 'collection',
|
||||||
|
});
|
||||||
dsoBreadcrumbService = {};
|
dsoBreadcrumbService = {};
|
||||||
collectionService = {
|
collectionService = {
|
||||||
findById: () => createSuccessfulRemoteDataObject$(testCollection),
|
findById: () => createSuccessfulRemoteDataObject$(testCollection),
|
||||||
|
@@ -5,6 +5,7 @@ import {
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { getDSORoute } from '../../app-routing-paths';
|
||||||
import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
import { BreadcrumbConfig } from '../../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
||||||
import { hasValue } from '../../shared/empty.util';
|
import { hasValue } from '../../shared/empty.util';
|
||||||
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
import { FollowLinkConfig } from '../../shared/utils/follow-link-config.model';
|
||||||
@@ -32,15 +33,34 @@ export const DSOBreadcrumbResolver: (route: ActivatedRouteSnapshot, state: Route
|
|||||||
dataService: IdentifiableDataService<DSpaceObject>,
|
dataService: IdentifiableDataService<DSpaceObject>,
|
||||||
...linksToFollow: FollowLinkConfig<DSpaceObject>[]
|
...linksToFollow: FollowLinkConfig<DSpaceObject>[]
|
||||||
): Observable<BreadcrumbConfig<DSpaceObject>> => {
|
): Observable<BreadcrumbConfig<DSpaceObject>> => {
|
||||||
const uuid = route.params.id;
|
return DSOBreadcrumbResolverByUuid(route, state, route.params.id, breadcrumbService, dataService, ...linksToFollow);
|
||||||
|
};
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving a breadcrumb config object with the given UUID
|
||||||
|
*
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @param {String} uuid The uuid of the DSO object
|
||||||
|
* @param {DSOBreadcrumbsService} breadcrumbService
|
||||||
|
* @param {IdentifiableDataService} dataService
|
||||||
|
* @param linksToFollow
|
||||||
|
* @returns BreadcrumbConfig object
|
||||||
|
*/
|
||||||
|
export const DSOBreadcrumbResolverByUuid: (route: ActivatedRouteSnapshot, state: RouterStateSnapshot, uuid: string, breadcrumbService: DSOBreadcrumbsService, dataService: IdentifiableDataService<DSpaceObject>, ...linksToFollow: FollowLinkConfig<DSpaceObject>[]) => Observable<BreadcrumbConfig<DSpaceObject>> = (
|
||||||
|
route: ActivatedRouteSnapshot,
|
||||||
|
state: RouterStateSnapshot,
|
||||||
|
uuid: string,
|
||||||
|
breadcrumbService: DSOBreadcrumbsService,
|
||||||
|
dataService: IdentifiableDataService<DSpaceObject>,
|
||||||
|
...linksToFollow: FollowLinkConfig<DSpaceObject>[]
|
||||||
|
): Observable<BreadcrumbConfig<DSpaceObject>> => {
|
||||||
return dataService.findById(uuid, true, false, ...linksToFollow).pipe(
|
return dataService.findById(uuid, true, false, ...linksToFollow).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
getRemoteDataPayload(),
|
getRemoteDataPayload(),
|
||||||
map((object: DSpaceObject) => {
|
map((object: DSpaceObject) => {
|
||||||
if (hasValue(object)) {
|
if (hasValue(object)) {
|
||||||
const fullPath = state.url;
|
return { provider: breadcrumbService, key: object, url: getDSORoute(object) };
|
||||||
const url = (fullPath.substring(0, fullPath.indexOf(uuid))).concat(uuid);
|
|
||||||
return { provider: breadcrumbService, key: object, url: url };
|
|
||||||
} else {
|
} else {
|
||||||
return undefined;
|
return undefined;
|
||||||
}
|
}
|
||||||
|
@@ -4,8 +4,8 @@ import {
|
|||||||
HttpTestingController,
|
HttpTestingController,
|
||||||
} from '@angular/common/http/testing';
|
} from '@angular/common/http/testing';
|
||||||
import { TestBed } from '@angular/core/testing';
|
import { TestBed } from '@angular/core/testing';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
|
|
||||||
|
import { REQUEST } from '../../../express.tokens';
|
||||||
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
|
import { DspaceRestService } from '../dspace-rest/dspace-rest.service';
|
||||||
import { ForwardClientIpInterceptor } from './forward-client-ip.interceptor';
|
import { ForwardClientIpInterceptor } from './forward-client-ip.interceptor';
|
||||||
|
|
||||||
|
@@ -8,9 +8,10 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { REQUEST } from '../../../express.tokens';
|
||||||
|
|
||||||
@Injectable({ providedIn: 'root' })
|
@Injectable({ providedIn: 'root' })
|
||||||
/**
|
/**
|
||||||
* Http Interceptor intercepting Http Requests, adding the client's IP to their X-Forwarded-For header
|
* Http Interceptor intercepting Http Requests, adding the client's IP to their X-Forwarded-For header
|
||||||
|
@@ -3,7 +3,6 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
combineLatest,
|
combineLatest,
|
||||||
@@ -16,6 +15,7 @@ import {
|
|||||||
take,
|
take,
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { REQUEST } from '../../../express.tokens';
|
||||||
import {
|
import {
|
||||||
hasValue,
|
hasValue,
|
||||||
isEmpty,
|
isEmpty,
|
||||||
|
@@ -2,8 +2,8 @@ import {
|
|||||||
TestBed,
|
TestBed,
|
||||||
waitForAsync,
|
waitForAsync,
|
||||||
} from '@angular/core/testing';
|
} from '@angular/core/testing';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
|
|
||||||
|
import { REQUEST } from '../../../express.tokens';
|
||||||
import {
|
import {
|
||||||
CookieService,
|
CookieService,
|
||||||
ICookieService,
|
ICookieService,
|
||||||
|
@@ -2,13 +2,14 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { CookieAttributes } from 'js-cookie';
|
import { CookieAttributes } from 'js-cookie';
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
Subject,
|
Subject,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { REQUEST } from '../../../express.tokens';
|
||||||
|
|
||||||
export interface ICookieService {
|
export interface ICookieService {
|
||||||
readonly cookies$: Observable<{ readonly [key: string]: any }>;
|
readonly cookies$: Observable<{ readonly [key: string]: any }>;
|
||||||
|
|
||||||
|
@@ -2,15 +2,15 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
|
||||||
REQUEST,
|
|
||||||
RESPONSE,
|
|
||||||
} from '@nguniversal/express-engine/tokens';
|
|
||||||
import {
|
import {
|
||||||
Request,
|
Request,
|
||||||
Response,
|
Response,
|
||||||
} from 'express';
|
} from 'express';
|
||||||
|
|
||||||
|
import {
|
||||||
|
REQUEST,
|
||||||
|
RESPONSE,
|
||||||
|
} from '../../../express.tokens';
|
||||||
import { HardRedirectService } from './hard-redirect.service';
|
import { HardRedirectService } from './hard-redirect.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -3,9 +3,10 @@ import {
|
|||||||
Injectable,
|
Injectable,
|
||||||
Optional,
|
Optional,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { RESPONSE } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { Response } from 'express';
|
import { Response } from 'express';
|
||||||
|
|
||||||
|
import { RESPONSE } from '../../../express.tokens';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Service responsible to provide method to manage the response object
|
* Service responsible to provide method to manage the response object
|
||||||
*/
|
*/
|
||||||
|
@@ -2,12 +2,12 @@ import {
|
|||||||
Inject,
|
Inject,
|
||||||
Injectable,
|
Injectable,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
of as observableOf,
|
of as observableOf,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { REQUEST } from '../../../express.tokens';
|
||||||
import { ReferrerService } from './referrer.service';
|
import { ReferrerService } from './referrer.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
|
@@ -48,7 +48,7 @@ export const DEBOUNCE_TIME_OPERATOR = new InjectionToken<<T>(dueTime: number) =>
|
|||||||
|
|
||||||
export const getRemoteDataPayload = <T>() =>
|
export const getRemoteDataPayload = <T>() =>
|
||||||
(source: Observable<RemoteData<T>>): Observable<T> =>
|
(source: Observable<RemoteData<T>>): Observable<T> =>
|
||||||
source.pipe(map((remoteData: RemoteData<T>) => remoteData.payload));
|
source.pipe(map((remoteData: RemoteData<T>) => remoteData?.payload));
|
||||||
|
|
||||||
export const getPaginatedListPayload = <T>() =>
|
export const getPaginatedListPayload = <T>() =>
|
||||||
(source: Observable<PaginatedList<T>>): Observable<T[]> =>
|
(source: Observable<PaginatedList<T>>): Observable<T[]> =>
|
||||||
|
@@ -0,0 +1,17 @@
|
|||||||
|
import {
|
||||||
|
followLink,
|
||||||
|
FollowLinkConfig,
|
||||||
|
} from '../../../shared/utils/follow-link-config.model';
|
||||||
|
import { WorkflowItem } from '../models/workflowitem.model';
|
||||||
|
import { WorkspaceItem } from '../models/workspaceitem.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The self links defined in this list are expected to be requested somewhere in the near future
|
||||||
|
* Requesting them as embeds will limit the number of requests
|
||||||
|
*
|
||||||
|
* Needs to be in a separate file to prevent circular dependencies in webpack.
|
||||||
|
*/
|
||||||
|
export const SUBMISSION_LINKS_TO_FOLLOW: FollowLinkConfig<WorkflowItem | WorkspaceItem>[] = [
|
||||||
|
followLink('item'),
|
||||||
|
followLink('collection'),
|
||||||
|
];
|
@@ -5,12 +5,12 @@ import {
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { switchMap } from 'rxjs/operators';
|
import { switchMap } from 'rxjs/operators';
|
||||||
|
|
||||||
import { followLink } from '../../../shared/utils/follow-link-config.model';
|
|
||||||
import { IdentifiableDataService } from '../../data/base/identifiable-data.service';
|
import { IdentifiableDataService } from '../../data/base/identifiable-data.service';
|
||||||
import { RemoteData } from '../../data/remote-data';
|
import { RemoteData } from '../../data/remote-data';
|
||||||
import { Item } from '../../shared/item.model';
|
import { Item } from '../../shared/item.model';
|
||||||
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
import { getFirstCompletedRemoteData } from '../../shared/operators';
|
||||||
import { SubmissionObject } from '../models/submission-object.model';
|
import { SubmissionObject } from '../models/submission-object.model';
|
||||||
|
import { SUBMISSION_LINKS_TO_FOLLOW } from './submission-links-to-follow';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method for resolving an item based on the parameters in the current route
|
* Method for resolving an item based on the parameters in the current route
|
||||||
@@ -28,7 +28,7 @@ export const SubmissionObjectResolver: (route: ActivatedRouteSnapshot, state: Ro
|
|||||||
return dataService.findById(route.params.id,
|
return dataService.findById(route.params.id,
|
||||||
true,
|
true,
|
||||||
false,
|
false,
|
||||||
followLink('item'),
|
...SUBMISSION_LINKS_TO_FOLLOW,
|
||||||
).pipe(
|
).pipe(
|
||||||
getFirstCompletedRemoteData(),
|
getFirstCompletedRemoteData(),
|
||||||
switchMap((wfiRD: RemoteData<any>) => wfiRD.payload.item as Observable<RemoteData<Item>>),
|
switchMap((wfiRD: RemoteData<any>) => wfiRD.payload.item as Observable<RemoteData<Item>>),
|
||||||
|
@@ -0,0 +1,51 @@
|
|||||||
|
import {
|
||||||
|
ActivatedRouteSnapshot,
|
||||||
|
Resolve,
|
||||||
|
RouterStateSnapshot,
|
||||||
|
} from '@angular/router';
|
||||||
|
import { Observable } from 'rxjs';
|
||||||
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { BreadcrumbConfig } from '../../../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
||||||
|
import { IdentifiableDataService } from '../../data/base/identifiable-data.service';
|
||||||
|
import {
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
getRemoteDataPayload,
|
||||||
|
} from '../../shared/operators';
|
||||||
|
import { SubmissionObject } from '../models/submission-object.model';
|
||||||
|
import { SubmissionParentBreadcrumbsService } from '../submission-parent-breadcrumb.service';
|
||||||
|
import { SUBMISSION_LINKS_TO_FOLLOW } from './submission-links-to-follow';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a resolver that requests a specific item before the route is activated
|
||||||
|
*/
|
||||||
|
export abstract class SubmissionParentBreadcrumbResolver implements Resolve<BreadcrumbConfig<SubmissionObject>> {
|
||||||
|
|
||||||
|
protected constructor(
|
||||||
|
protected dataService: IdentifiableDataService<any>,
|
||||||
|
protected breadcrumbService: SubmissionParentBreadcrumbsService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Method for resolving an item based on the parameters in the current route
|
||||||
|
* @param {ActivatedRouteSnapshot} route The current ActivatedRouteSnapshot
|
||||||
|
* @param {RouterStateSnapshot} state The current RouterStateSnapshot
|
||||||
|
* @returns Observable<<RemoteData<Item>> Emits the found item based on the parameters in the current route,
|
||||||
|
* or an error if something went wrong
|
||||||
|
*/
|
||||||
|
resolve(route: ActivatedRouteSnapshot, state: RouterStateSnapshot): Observable<BreadcrumbConfig<SubmissionObject>> {
|
||||||
|
return this.dataService.findById(route.params.id,
|
||||||
|
true,
|
||||||
|
false,
|
||||||
|
...SUBMISSION_LINKS_TO_FOLLOW,
|
||||||
|
).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
map((submissionObject: SubmissionObject) => ({
|
||||||
|
provider: this.breadcrumbService,
|
||||||
|
key: submissionObject,
|
||||||
|
} as BreadcrumbConfig<SubmissionObject>)),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
@@ -17,6 +17,9 @@ describe('SubmissionJsonPatchOperationsService', () => {
|
|||||||
const rdbService = {} as RemoteDataBuildService;
|
const rdbService = {} as RemoteDataBuildService;
|
||||||
const halEndpointService = {} as HALEndpointService;
|
const halEndpointService = {} as HALEndpointService;
|
||||||
|
|
||||||
|
const uuid = '91ecbeda-99fe-42ac-9430-b9b75af56f78';
|
||||||
|
const href = 'https://rest.api/some/self/link?with=maybe&a=few&other=parameters';
|
||||||
|
|
||||||
function initTestService() {
|
function initTestService() {
|
||||||
return new SubmissionJsonPatchOperationsService(
|
return new SubmissionJsonPatchOperationsService(
|
||||||
requestService,
|
requestService,
|
||||||
@@ -36,4 +39,16 @@ describe('SubmissionJsonPatchOperationsService', () => {
|
|||||||
expect((service as any).patchRequestConstructor).toEqual(SubmissionPatchRequest);
|
expect((service as any).patchRequestConstructor).toEqual(SubmissionPatchRequest);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
describe(`getRequestInstance`, () => {
|
||||||
|
it(`should add a parameter to embed the item to the request URL`, () => {
|
||||||
|
const result = (service as any).getRequestInstance(uuid, href);
|
||||||
|
const resultURL = new URL(result.href);
|
||||||
|
expect(resultURL.searchParams.get('embed')).toEqual('item');
|
||||||
|
|
||||||
|
// if we delete the embed item param, it should be identical to the original url
|
||||||
|
resultURL.searchParams.delete('embed', 'item');
|
||||||
|
expect(href).toEqual(resultURL.toString());
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -8,6 +8,7 @@ import { RequestService } from '../data/request.service';
|
|||||||
import { JsonPatchOperationsService } from '../json-patch/json-patch-operations.service';
|
import { JsonPatchOperationsService } from '../json-patch/json-patch-operations.service';
|
||||||
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
import { HALEndpointService } from '../shared/hal-endpoint.service';
|
||||||
import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-response-definition.model';
|
import { SubmitDataResponseDefinitionObject } from '../shared/submit-data-response-definition.model';
|
||||||
|
import { URLCombiner } from '../url-combiner/url-combiner';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that provides methods to make JSON Patch requests.
|
* A service that provides methods to make JSON Patch requests.
|
||||||
@@ -26,4 +27,20 @@ export class SubmissionJsonPatchOperationsService extends JsonPatchOperationsSer
|
|||||||
super();
|
super();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Return an instance for RestRequest class
|
||||||
|
*
|
||||||
|
* @param uuid
|
||||||
|
* The request uuid
|
||||||
|
* @param href
|
||||||
|
* The request href
|
||||||
|
* @param body
|
||||||
|
* The request body
|
||||||
|
* @return Object<PatchRequestDefinition>
|
||||||
|
* instance of PatchRequestDefinition
|
||||||
|
*/
|
||||||
|
protected getRequestInstance(uuid: string, href: string, body?: any): SubmissionPatchRequest {
|
||||||
|
return new this.patchRequestConstructor(uuid, new URLCombiner(href, '?embed=item').toString(), body);
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -0,0 +1,70 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import {
|
||||||
|
combineLatest,
|
||||||
|
Observable,
|
||||||
|
of as observableOf,
|
||||||
|
switchMap,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { getDSORoute } from '../../app-routing-paths';
|
||||||
|
import { Breadcrumb } from '../../breadcrumbs/breadcrumb/breadcrumb.model';
|
||||||
|
import { hasValue } from '../../shared/empty.util';
|
||||||
|
import { SubmissionService } from '../../submission/submission.service';
|
||||||
|
import { BreadcrumbsProviderService } from '../breadcrumbs/breadcrumbsProviderService';
|
||||||
|
import { DSOBreadcrumbsService } from '../breadcrumbs/dso-breadcrumbs.service';
|
||||||
|
import { DSONameService } from '../breadcrumbs/dso-name.service';
|
||||||
|
import { CollectionDataService } from '../data/collection-data.service';
|
||||||
|
import { RemoteData } from '../data/remote-data';
|
||||||
|
import { Collection } from '../shared/collection.model';
|
||||||
|
import {
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
getRemoteDataPayload,
|
||||||
|
} from '../shared/operators';
|
||||||
|
import { SubmissionObject } from './models/submission-object.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service to calculate the parent {@link DSpaceObject} breadcrumbs for a {@link SubmissionObject}
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class SubmissionParentBreadcrumbsService implements BreadcrumbsProviderService<SubmissionObject> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected dsoNameService: DSONameService,
|
||||||
|
protected dsoBreadcrumbsService: DSOBreadcrumbsService,
|
||||||
|
protected submissionService: SubmissionService,
|
||||||
|
protected collectionService: CollectionDataService,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates the parent breadcrumb structure for {@link SubmissionObject}s. It also automatically recreates the
|
||||||
|
* parent breadcrumb structure when you change a {@link SubmissionObject}'s by dispatching a
|
||||||
|
* {@link ChangeSubmissionCollectionAction}.
|
||||||
|
*
|
||||||
|
* @param submissionObject The {@link SubmissionObject} for which the parent breadcrumb structure needs to be created
|
||||||
|
*/
|
||||||
|
getBreadcrumbs(submissionObject: SubmissionObject): Observable<Breadcrumb[]> {
|
||||||
|
return combineLatest([
|
||||||
|
(submissionObject.collection as Observable<RemoteData<Collection>>).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
),
|
||||||
|
this.submissionService.getSubmissionCollectionId(submissionObject.id),
|
||||||
|
]).pipe(
|
||||||
|
switchMap(([collection, latestCollectionId]: [Collection, string]) => {
|
||||||
|
if (hasValue(latestCollectionId)) {
|
||||||
|
return this.collectionService.findById(latestCollectionId).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
getRemoteDataPayload(),
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
return observableOf(collection);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
switchMap((collection: Collection) => this.dsoBreadcrumbsService.getBreadcrumbs(collection, getDSORoute(collection))),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -24,8 +24,8 @@ import {
|
|||||||
import {
|
import {
|
||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest as observableCombineLatest,
|
combineLatest as observableCombineLatest,
|
||||||
EMPTY,
|
|
||||||
Observable,
|
Observable,
|
||||||
|
of,
|
||||||
Subscription,
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
@@ -188,7 +188,7 @@ export class DsoEditMetadataComponent implements OnInit, OnDestroy {
|
|||||||
const lazyProvider$: Observable<UpdateDataService<DSpaceObject>> = lazyDataService(this.dataServiceMap, this.dsoType, this.parentInjector);
|
const lazyProvider$: Observable<UpdateDataService<DSpaceObject>> = lazyDataService(this.dataServiceMap, this.dsoType, this.parentInjector);
|
||||||
return lazyProvider$;
|
return lazyProvider$;
|
||||||
} else {
|
} else {
|
||||||
return EMPTY;
|
return of(this.updateDataService);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@@ -13,7 +13,6 @@ import {
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
import { Store } from '@ngrx/store';
|
import { Store } from '@ngrx/store';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
@@ -21,6 +20,7 @@ import {
|
|||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
|
|
||||||
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
import { APP_CONFIG } from '../../../../../../config/app-config.interface';
|
||||||
|
import { REQUEST } from '../../../../../../express.tokens';
|
||||||
import { AuthService } from '../../../../../core/auth/auth.service';
|
import { AuthService } from '../../../../../core/auth/auth.service';
|
||||||
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
import { RemoteDataBuildService } from '../../../../../core/cache/builders/remote-data-build.service';
|
||||||
import { ObjectCacheService } from '../../../../../core/cache/object-cache.service';
|
import { ObjectCacheService } from '../../../../../core/cache/object-cache.service';
|
||||||
|
@@ -1,29 +1,25 @@
|
|||||||
<ds-home-coar></ds-home-coar>
|
<ds-home-coar></ds-home-coar>
|
||||||
<ds-themed-home-news></ds-themed-home-news>
|
<ds-themed-home-news></ds-themed-home-news>
|
||||||
<div [ngClass]="showDiscoverFilters ? 'container-fluid' : 'container'">
|
<ds-themed-configuration-search-page *ngIf="showDiscoverFilters"
|
||||||
<ds-page-with-sidebar [sidebarContent]="sidebar" [sideBarWidth]="showDiscoverFilters ? 3 : 0" [class]="showDiscoverFilters ? 'row mx-3' : ''">
|
[sideBarWidth]="3"
|
||||||
<div [class.col-sm-12]="showDiscoverFilters">
|
[showViewModes]="false"
|
||||||
<button *ngIf="showDiscoverFilters && (isXsOrSm$ | async) && sidebarService.isCollapsed" (click)="sidebarService.expand()"
|
[searchEnabled]="false"
|
||||||
class="btn btn-outline-primary d-block ml-auto mb-3">
|
[inPlaceSearch]="false"
|
||||||
<i class="fas fa-sliders"></i> {{ 'search.sidebar.open' | translate }}
|
[showScopeSelector]="false">
|
||||||
</button>
|
<ng-container searchContentTop *ngTemplateOutlet="homeContent"></ng-container>
|
||||||
<ng-container *ngIf="(site$ | async) as site">
|
</ds-themed-configuration-search-page>
|
||||||
<ds-view-tracker [object]="site"></ds-view-tracker>
|
<div *ngIf="!showDiscoverFilters" class="container">
|
||||||
</ng-container>
|
<ng-container *ngTemplateOutlet="homeContent"></ng-container>
|
||||||
<ds-themed-search-form [inPlaceSearch]="false"
|
|
||||||
[searchPlaceholder]="'home.search-form.placeholder' | translate">
|
|
||||||
</ds-themed-search-form>
|
|
||||||
<ds-themed-top-level-community-list></ds-themed-top-level-community-list>
|
|
||||||
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>
|
|
||||||
</div>
|
|
||||||
</ds-page-with-sidebar>
|
|
||||||
</div>
|
</div>
|
||||||
<ds-suggestions-popup></ds-suggestions-popup>
|
<ds-suggestions-popup></ds-suggestions-popup>
|
||||||
|
|
||||||
<ng-template #sidebar>
|
<ng-template #homeContent>
|
||||||
<div *ngIf="showDiscoverFilters">
|
<ng-container *ngIf="(site$ | async) as site">
|
||||||
<ds-themed-configuration-search-page [sideBarWidth]="12" [showViewModes]="false" [searchEnabled]="false"
|
<ds-view-tracker [object]="site"></ds-view-tracker>
|
||||||
[inPlaceSearch]="false" [showScopeSelector]="false">
|
</ng-container>
|
||||||
</ds-themed-configuration-search-page>
|
<ds-themed-search-form [inPlaceSearch]="false"
|
||||||
</div>
|
[searchPlaceholder]="'home.search-form.placeholder' | translate">
|
||||||
|
</ds-themed-search-form>
|
||||||
|
<ds-themed-top-level-community-list></ds-themed-top-level-community-list>
|
||||||
|
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>
|
||||||
</ng-template>
|
</ng-template>
|
||||||
|
@@ -1,5 +1,6 @@
|
|||||||
:host ::ng-deep {
|
@include media-breakpoint-down(md) {
|
||||||
.container-fluid .container {
|
ds-themed-configuration-search-page + .container {
|
||||||
padding: 0;
|
width: 100%;
|
||||||
|
max-width: none;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@@ -2,6 +2,7 @@ import {
|
|||||||
AsyncPipe,
|
AsyncPipe,
|
||||||
NgClass,
|
NgClass,
|
||||||
NgIf,
|
NgIf,
|
||||||
|
NgTemplateOutlet,
|
||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
@@ -21,10 +22,8 @@ import { Site } from '../core/shared/site.model';
|
|||||||
import { SuggestionsPopupComponent } from '../notifications/suggestions-popup/suggestions-popup.component';
|
import { SuggestionsPopupComponent } from '../notifications/suggestions-popup/suggestions-popup.component';
|
||||||
import { ConfigurationSearchPageComponent } from '../search-page/configuration-search-page.component';
|
import { ConfigurationSearchPageComponent } from '../search-page/configuration-search-page.component';
|
||||||
import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component';
|
import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component';
|
||||||
import { HostWindowService } from '../shared/host-window.service';
|
|
||||||
import { ThemedSearchFormComponent } from '../shared/search-form/themed-search-form.component';
|
import { ThemedSearchFormComponent } from '../shared/search-form/themed-search-form.component';
|
||||||
import { PageWithSidebarComponent } from '../shared/sidebar/page-with-sidebar.component';
|
import { PageWithSidebarComponent } from '../shared/sidebar/page-with-sidebar.component';
|
||||||
import { SidebarService } from '../shared/sidebar/sidebar.service';
|
|
||||||
import { ViewTrackerComponent } from '../statistics/angulartics/dspace/view-tracker.component';
|
import { ViewTrackerComponent } from '../statistics/angulartics/dspace/view-tracker.component';
|
||||||
import { HomeCoarComponent } from './home-coar/home-coar.component';
|
import { HomeCoarComponent } from './home-coar/home-coar.component';
|
||||||
import { ThemedHomeNewsComponent } from './home-news/themed-home-news.component';
|
import { ThemedHomeNewsComponent } from './home-news/themed-home-news.component';
|
||||||
@@ -36,27 +35,23 @@ import { ThemedTopLevelCommunityListComponent } from './top-level-community-list
|
|||||||
styleUrls: ['./home-page.component.scss'],
|
styleUrls: ['./home-page.component.scss'],
|
||||||
templateUrl: './home-page.component.html',
|
templateUrl: './home-page.component.html',
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [ThemedHomeNewsComponent, NgIf, ViewTrackerComponent, ThemedSearchFormComponent, ThemedTopLevelCommunityListComponent, RecentItemListComponent, AsyncPipe, TranslateModule, NgClass, ConfigurationSearchPageComponent, SuggestionsPopupComponent, ThemedConfigurationSearchPageComponent, PageWithSidebarComponent, HomeCoarComponent],
|
imports: [ThemedHomeNewsComponent, NgTemplateOutlet, NgIf, ViewTrackerComponent, ThemedSearchFormComponent, ThemedTopLevelCommunityListComponent, RecentItemListComponent, AsyncPipe, TranslateModule, NgClass, ConfigurationSearchPageComponent, SuggestionsPopupComponent, ThemedConfigurationSearchPageComponent, PageWithSidebarComponent, HomeCoarComponent],
|
||||||
})
|
})
|
||||||
export class HomePageComponent implements OnInit {
|
export class HomePageComponent implements OnInit {
|
||||||
|
|
||||||
site$: Observable<Site>;
|
site$: Observable<Site>;
|
||||||
isXsOrSm$: Observable<boolean>;
|
|
||||||
recentSubmissionspageSize: number;
|
recentSubmissionspageSize: number;
|
||||||
showDiscoverFilters: boolean;
|
showDiscoverFilters: boolean;
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
@Inject(APP_CONFIG) protected appConfig: AppConfig,
|
||||||
protected route: ActivatedRoute,
|
protected route: ActivatedRoute,
|
||||||
protected sidebarService: SidebarService,
|
|
||||||
protected windowService: HostWindowService,
|
|
||||||
) {
|
) {
|
||||||
this.recentSubmissionspageSize = this.appConfig.homePage.recentSubmissions.pageSize;
|
this.recentSubmissionspageSize = this.appConfig.homePage.recentSubmissions.pageSize;
|
||||||
this.showDiscoverFilters = this.appConfig.homePage.showDiscoverFilters;
|
this.showDiscoverFilters = this.appConfig.homePage.showDiscoverFilters;
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
this.isXsOrSm$ = this.windowService.isXsOrSm();
|
|
||||||
this.site$ = this.route.data.pipe(
|
this.site$ = this.route.data.pipe(
|
||||||
map((data) => data.site as Site),
|
map((data) => data.site as Site),
|
||||||
);
|
);
|
||||||
|
@@ -3,7 +3,6 @@
|
|||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
[hidePaginationDetail]="true"
|
[hidePaginationDetail]="true"
|
||||||
[paginationOptions]="options"
|
[paginationOptions]="options"
|
||||||
[pageInfoState]="(objectsRD$ | async)?.payload"
|
|
||||||
[collectionSize]="(objectsRD$ | async)?.payload?.totalElements">
|
[collectionSize]="(objectsRD$ | async)?.payload?.totalElements">
|
||||||
<ng-container *ngIf="(loading$ | async) !== true">
|
<ng-container *ngIf="(loading$ | async) !== true">
|
||||||
<div [id]="bundle.id" class="bundle-bitstreams-list"
|
<div [id]="bundle.id" class="bundle-bitstreams-list"
|
||||||
|
@@ -10,7 +10,6 @@
|
|||||||
<ng-container *ngVar="updates | dsObjectValues as updateValues">
|
<ng-container *ngVar="updates | dsObjectValues as updateValues">
|
||||||
<ds-pagination
|
<ds-pagination
|
||||||
[paginationOptions]="paginationConfig"
|
[paginationOptions]="paginationConfig"
|
||||||
[pageInfoState]="(relationshipsRd$ | async)?.payload?.pageInfo"
|
|
||||||
[collectionSize]="(relationshipsRd$ | async)?.payload?.totalElements + (this.nbAddedFields$ | async)"
|
[collectionSize]="(relationshipsRd$ | async)?.payload?.totalElements + (this.nbAddedFields$ | async)"
|
||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true">
|
[hidePagerWhenSinglePage]="true">
|
||||||
|
@@ -13,7 +13,6 @@ import {
|
|||||||
Router,
|
Router,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
import { AuthRequestService } from 'src/app/core/auth/auth-request.service';
|
import { AuthRequestService } from 'src/app/core/auth/auth-request.service';
|
||||||
@@ -23,6 +22,7 @@ import { ActivatedRouteStub } from 'src/app/shared/testing/active-router.stub';
|
|||||||
import { AuthRequestServiceStub } from 'src/app/shared/testing/auth-request-service.stub';
|
import { AuthRequestServiceStub } from 'src/app/shared/testing/auth-request-service.stub';
|
||||||
|
|
||||||
import { APP_CONFIG } from '../../../../../config/app-config.interface';
|
import { APP_CONFIG } from '../../../../../config/app-config.interface';
|
||||||
|
import { REQUEST } from '../../../../../express.tokens';
|
||||||
import { LinkService } from '../../../../core/cache/builders/link.service';
|
import { LinkService } from '../../../../core/cache/builders/link.service';
|
||||||
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../../../core/data/configuration-data.service';
|
||||||
import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model';
|
import { FieldChangeType } from '../../../../core/data/object-updates/field-change-type.model';
|
||||||
|
@@ -6,7 +6,6 @@
|
|||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
[paginationOptions]="originalOptions"
|
[paginationOptions]="originalOptions"
|
||||||
[pageInfoState]="originals"
|
|
||||||
[collectionSize]="originals?.totalElements"
|
[collectionSize]="originals?.totalElements"
|
||||||
[retainScrollPosition]="true">
|
[retainScrollPosition]="true">
|
||||||
|
|
||||||
@@ -49,7 +48,6 @@
|
|||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
[paginationOptions]="licenseOptions"
|
[paginationOptions]="licenseOptions"
|
||||||
[pageInfoState]="licenses"
|
|
||||||
[collectionSize]="licenses?.totalElements"
|
[collectionSize]="licenses?.totalElements"
|
||||||
[retainScrollPosition]="true">
|
[retainScrollPosition]="true">
|
||||||
|
|
||||||
|
@@ -0,0 +1,53 @@
|
|||||||
|
<div class="left-column">
|
||||||
|
<span *ngIf="(workspaceId$ | async) || (workflowId$ | async); then versionNumberWithoutLink else versionNumberWithLink"></span>
|
||||||
|
<ng-template #versionNumberWithLink>
|
||||||
|
<a [routerLink]="getVersionRoute(version.id)">{{version.version}}</a>
|
||||||
|
</ng-template>
|
||||||
|
<ng-template #versionNumberWithoutLink>
|
||||||
|
{{version.version}}
|
||||||
|
</ng-template>
|
||||||
|
<span *ngIf="version?.id === itemVersion?.id">*</span>
|
||||||
|
|
||||||
|
<span *ngIf="workspaceId$ | async" class="text-light badge badge-primary ml-3">
|
||||||
|
{{ "item.version.history.table.workspaceItem" | translate }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
<span *ngIf="workflowId$ | async" class="text-light badge badge-info ml-3">
|
||||||
|
{{ "item.version.history.table.workflowItem" | translate }}
|
||||||
|
</span>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div class="right-column">
|
||||||
|
|
||||||
|
<div class="btn-group edit-field space-children-mr" *ngIf="displayActions">
|
||||||
|
<!--EDIT WORKSPACE ITEM-->
|
||||||
|
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
|
||||||
|
*ngIf="workspaceId$ | async"
|
||||||
|
(click)="editWorkspaceItem(workspaceId$)"
|
||||||
|
title="{{'item.version.history.table.action.editWorkspaceItem' | translate }}">
|
||||||
|
<i class="fas fa-pencil-alt fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
<!--CREATE-->
|
||||||
|
<ng-container *ngIf="canCreateVersion$ | async">
|
||||||
|
<button class="btn btn-outline-primary btn-sm version-row-element-create"
|
||||||
|
[disabled]="isAnyBeingEdited() || hasDraftVersion"
|
||||||
|
(click)="createNewVersion(version)"
|
||||||
|
title="{{createVersionTitle | translate }}">
|
||||||
|
<i class="fas fa-code-branch fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
<!--DELETE-->
|
||||||
|
<ng-container *ngIf="canDeleteVersion$ | async">
|
||||||
|
<button class="btn btn-sm version-row-element-delete"
|
||||||
|
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
|
||||||
|
[disabled]="isAnyBeingEdited()"
|
||||||
|
(click)="deleteVersion(version, version.id === itemVersion.id)"
|
||||||
|
title="{{'item.version.history.table.action.deleteVersion' | translate}}">
|
||||||
|
<i class="fas fa-trash fa-fw"></i>
|
||||||
|
</button>
|
||||||
|
</ng-container>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
</div>
|
||||||
|
|
@@ -0,0 +1,9 @@
|
|||||||
|
.left-column {
|
||||||
|
float: left;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.right-column {
|
||||||
|
float: right;
|
||||||
|
text-align: right;
|
||||||
|
}
|
@@ -0,0 +1,188 @@
|
|||||||
|
import { CommonModule } from '@angular/common';
|
||||||
|
import { NO_ERRORS_SCHEMA } from '@angular/core';
|
||||||
|
import {
|
||||||
|
ComponentFixture,
|
||||||
|
TestBed,
|
||||||
|
waitForAsync,
|
||||||
|
} from '@angular/core/testing';
|
||||||
|
import {
|
||||||
|
FormsModule,
|
||||||
|
ReactiveFormsModule,
|
||||||
|
} from '@angular/forms';
|
||||||
|
import {
|
||||||
|
BrowserModule,
|
||||||
|
By,
|
||||||
|
} from '@angular/platform-browser';
|
||||||
|
import {
|
||||||
|
ActivatedRoute,
|
||||||
|
RouterModule,
|
||||||
|
} from '@angular/router';
|
||||||
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
EMPTY,
|
||||||
|
of as observableOf,
|
||||||
|
of,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { VersionDataService } from '../../../core/data/version-data.service';
|
||||||
|
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import { Version } from '../../../core/shared/version.model';
|
||||||
|
import { VersionHistory } from '../../../core/shared/version-history.model';
|
||||||
|
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
|
||||||
|
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils';
|
||||||
|
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub';
|
||||||
|
import { NotificationsServiceStub } from '../../../shared/testing/notifications-service.stub';
|
||||||
|
import { createPaginatedList } from '../../../shared/testing/utils.test';
|
||||||
|
import { ItemVersionsComponent } from '../item-versions.component';
|
||||||
|
import { ItemVersionsRowElementVersionComponent } from './item-versions-row-element-version.component';
|
||||||
|
|
||||||
|
describe('ItemVersionsRowElementVersionComponent', () => {
|
||||||
|
let component: ItemVersionsRowElementVersionComponent;
|
||||||
|
let fixture: ComponentFixture<ItemVersionsRowElementVersionComponent>;
|
||||||
|
|
||||||
|
const versionHistory = Object.assign(new VersionHistory(), {
|
||||||
|
id: '1',
|
||||||
|
draftVersion: true,
|
||||||
|
});
|
||||||
|
|
||||||
|
const version = Object.assign(new Version(), {
|
||||||
|
id: '1',
|
||||||
|
version: 1,
|
||||||
|
created: new Date(2020, 1, 1),
|
||||||
|
summary: 'first version',
|
||||||
|
versionhistory: createSuccessfulRemoteDataObject$(versionHistory),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: 'version2-url',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
versionHistory.versions = createSuccessfulRemoteDataObject$(createPaginatedList([version]));
|
||||||
|
|
||||||
|
|
||||||
|
const item = Object.assign(new Item(), { // is a workspace item
|
||||||
|
id: 'item-identifier-1',
|
||||||
|
uuid: 'item-identifier-1',
|
||||||
|
handle: '123456789/1',
|
||||||
|
version: createSuccessfulRemoteDataObject$(version),
|
||||||
|
_links: {
|
||||||
|
self: {
|
||||||
|
href: '/items/item-identifier-1',
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
version.item = createSuccessfulRemoteDataObject$(item);
|
||||||
|
|
||||||
|
const versionHistoryServiceSpy = jasmine.createSpyObj('versionHistoryService', {
|
||||||
|
getVersions: createSuccessfulRemoteDataObject$(createPaginatedList([version])),
|
||||||
|
getVersionHistoryFromVersion$: of(versionHistory),
|
||||||
|
getLatestVersionItemFromHistory$: of(item),
|
||||||
|
});
|
||||||
|
const authorizationServiceSpy = jasmine.createSpyObj('authorizationService', {
|
||||||
|
isAuthorized: observableOf(true),
|
||||||
|
});
|
||||||
|
const workspaceItemDataServiceSpy = jasmine.createSpyObj('workspaceItemDataService', {
|
||||||
|
findByItem: EMPTY,
|
||||||
|
});
|
||||||
|
const workflowItemDataServiceSpy = jasmine.createSpyObj('workflowItemDataService', {
|
||||||
|
findByItem: EMPTY,
|
||||||
|
});
|
||||||
|
const versionServiceSpy = jasmine.createSpyObj('versionService', {
|
||||||
|
findById: EMPTY,
|
||||||
|
});
|
||||||
|
const itemDataServiceSpy = jasmine.createSpyObj('itemDataService', {
|
||||||
|
delete: createSuccessfulRemoteDataObject$({}),
|
||||||
|
});
|
||||||
|
|
||||||
|
beforeEach(async () => {
|
||||||
|
await TestBed.configureTestingModule({
|
||||||
|
imports: [TranslateModule.forRoot(), RouterModule.forRoot([
|
||||||
|
{ path: 'items/:id/edit/versionhistory', component: {} as any },
|
||||||
|
]), CommonModule, FormsModule, ReactiveFormsModule, BrowserModule, ItemVersionsComponent],
|
||||||
|
providers: [
|
||||||
|
{ provide: NotificationsService, useValue: new NotificationsServiceStub() },
|
||||||
|
{ provide: AuthorizationDataService, useValue: authorizationServiceSpy },
|
||||||
|
{ provide: VersionHistoryDataService, useValue: versionHistoryServiceSpy },
|
||||||
|
{ provide: ItemDataService, useValue: itemDataServiceSpy },
|
||||||
|
{ provide: VersionDataService, useValue: versionServiceSpy },
|
||||||
|
{ provide: WorkspaceitemDataService, useValue: workspaceItemDataServiceSpy },
|
||||||
|
{ provide: WorkflowItemDataService, useValue: workflowItemDataServiceSpy },
|
||||||
|
{ provide: ActivatedRoute, useValue: new ActivatedRouteStub() },
|
||||||
|
],
|
||||||
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
|
})
|
||||||
|
.compileComponents();
|
||||||
|
|
||||||
|
fixture = TestBed.createComponent(ItemVersionsRowElementVersionComponent);
|
||||||
|
component = fixture.componentInstance;
|
||||||
|
|
||||||
|
component.version = version;
|
||||||
|
component.itemVersion = version;
|
||||||
|
component.item = item;
|
||||||
|
component.displayActions = true;
|
||||||
|
|
||||||
|
fixture.detectChanges();
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should create', () => {
|
||||||
|
expect(component).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should display version ${version.version} in the correct column for version ${version.id}`, () => {
|
||||||
|
const id = fixture.debugElement.query(By.css(`.left-column`));
|
||||||
|
expect(id.nativeElement.textContent).toContain(version.version.toString());
|
||||||
|
});
|
||||||
|
|
||||||
|
it(`should displau an asterisk in the correct column for current version`, () => {
|
||||||
|
const draft = fixture.debugElement.query(By.css(`.left-column`));
|
||||||
|
expect(draft.nativeElement.textContent).toContain('*');
|
||||||
|
});
|
||||||
|
|
||||||
|
it('should display action buttons in the correct column if displayActions is true', () => {
|
||||||
|
fixture.detectChanges();
|
||||||
|
const actions = fixture.debugElement.query(By.css(`.right-column`));
|
||||||
|
expect(actions).toBeTruthy();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('when deleting a version', () => {
|
||||||
|
let deleteButton;
|
||||||
|
|
||||||
|
beforeEach(() => {
|
||||||
|
deleteButton = fixture.debugElement.queryAll(By.css('.version-row-element-delete'))[0].nativeElement;
|
||||||
|
|
||||||
|
itemDataServiceSpy.delete.calls.reset();
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if confirmed via modal', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
deleteButton.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
(document as any).querySelector('.modal-footer .confirm').click();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should call ItemService.delete', () => {
|
||||||
|
expect(itemDataServiceSpy.delete).toHaveBeenCalledWith(item.id);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
describe('if canceled via modal', () => {
|
||||||
|
beforeEach(waitForAsync(() => {
|
||||||
|
deleteButton.click();
|
||||||
|
fixture.detectChanges();
|
||||||
|
(document as any).querySelector('.modal-footer .cancel').click();
|
||||||
|
}));
|
||||||
|
|
||||||
|
it('should not call ItemService.delete', () => {
|
||||||
|
expect(itemDataServiceSpy.delete).not.toHaveBeenCalled();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
});
|
@@ -0,0 +1,302 @@
|
|||||||
|
import {
|
||||||
|
AsyncPipe,
|
||||||
|
NgClass,
|
||||||
|
NgIf,
|
||||||
|
} from '@angular/common';
|
||||||
|
import {
|
||||||
|
Component,
|
||||||
|
EventEmitter,
|
||||||
|
Input,
|
||||||
|
OnInit,
|
||||||
|
Output,
|
||||||
|
} from '@angular/core';
|
||||||
|
import {
|
||||||
|
Router,
|
||||||
|
RouterLink,
|
||||||
|
} from '@angular/router';
|
||||||
|
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
||||||
|
import {
|
||||||
|
TranslateModule,
|
||||||
|
TranslateService,
|
||||||
|
} from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
combineLatest,
|
||||||
|
concatMap,
|
||||||
|
Observable,
|
||||||
|
of,
|
||||||
|
} from 'rxjs';
|
||||||
|
import {
|
||||||
|
map,
|
||||||
|
mergeMap,
|
||||||
|
switchMap,
|
||||||
|
take,
|
||||||
|
tap,
|
||||||
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
|
import { ItemDataService } from '../../../core/data/item-data.service';
|
||||||
|
import { RemoteData } from '../../../core/data/remote-data';
|
||||||
|
import { VersionDataService } from '../../../core/data/version-data.service';
|
||||||
|
import { VersionHistoryDataService } from '../../../core/data/version-history-data.service';
|
||||||
|
import { Item } from '../../../core/shared/item.model';
|
||||||
|
import {
|
||||||
|
getFirstCompletedRemoteData,
|
||||||
|
getFirstSucceededRemoteDataPayload,
|
||||||
|
} from '../../../core/shared/operators';
|
||||||
|
import { Version } from '../../../core/shared/version.model';
|
||||||
|
import { VersionHistory } from '../../../core/shared/version-history.model';
|
||||||
|
import { WorkspaceItem } from '../../../core/submission/models/workspaceitem.model';
|
||||||
|
import { WorkflowItemDataService } from '../../../core/submission/workflowitem-data.service';
|
||||||
|
import { WorkspaceitemDataService } from '../../../core/submission/workspaceitem-data.service';
|
||||||
|
import { NotificationsService } from '../../../shared/notifications/notifications.service';
|
||||||
|
import {
|
||||||
|
getItemEditVersionhistoryRoute,
|
||||||
|
getItemVersionRoute,
|
||||||
|
} from '../../item-page-routing-paths';
|
||||||
|
import { ItemVersionsDeleteModalComponent } from '../item-versions-delete-modal/item-versions-delete-modal.component';
|
||||||
|
import { ItemVersionsSharedService } from '../item-versions-shared.service';
|
||||||
|
import { ItemVersionsSummaryModalComponent } from '../item-versions-summary-modal/item-versions-summary-modal.component';
|
||||||
|
|
||||||
|
@Component({
|
||||||
|
selector: 'ds-item-versions-row-element-version',
|
||||||
|
standalone: true,
|
||||||
|
imports: [
|
||||||
|
AsyncPipe,
|
||||||
|
RouterLink,
|
||||||
|
TranslateModule,
|
||||||
|
NgClass,
|
||||||
|
NgIf,
|
||||||
|
],
|
||||||
|
templateUrl: './item-versions-row-element-version.component.html',
|
||||||
|
styleUrl: './item-versions-row-element-version.component.scss',
|
||||||
|
})
|
||||||
|
export class ItemVersionsRowElementVersionComponent implements OnInit {
|
||||||
|
@Input() hasDraftVersion: boolean | null;
|
||||||
|
@Input() version: Version;
|
||||||
|
@Input() itemVersion: Version;
|
||||||
|
@Input() item: Item;
|
||||||
|
@Input() displayActions: boolean;
|
||||||
|
@Input() versionBeingEditedNumber: number;
|
||||||
|
|
||||||
|
@Output() versionsHistoryChange = new EventEmitter<Observable<VersionHistory>>();
|
||||||
|
|
||||||
|
workspaceId$: Observable<string>;
|
||||||
|
workflowId$: Observable<string>;
|
||||||
|
canDeleteVersion$: Observable<boolean>;
|
||||||
|
canCreateVersion$: Observable<boolean>;
|
||||||
|
|
||||||
|
createVersionTitle: string;
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
private workspaceItemDataService: WorkspaceitemDataService,
|
||||||
|
private workflowItemDataService: WorkflowItemDataService,
|
||||||
|
private router: Router,
|
||||||
|
private itemService: ItemDataService,
|
||||||
|
private authorizationService: AuthorizationDataService,
|
||||||
|
private itemVersionShared: ItemVersionsSharedService,
|
||||||
|
private versionHistoryService: VersionHistoryDataService,
|
||||||
|
private versionService: VersionDataService,
|
||||||
|
private notificationsService: NotificationsService,
|
||||||
|
private translateService: TranslateService,
|
||||||
|
private modalService: NgbModal,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.workspaceId$ = this.getWorkspaceId(this.version.item);
|
||||||
|
this.workflowId$ = this.getWorkflowId(this.version.item);
|
||||||
|
this.canDeleteVersion$ = this.canDeleteVersion(this.version);
|
||||||
|
this.canCreateVersion$ = this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, this.item.self);
|
||||||
|
|
||||||
|
this.createVersionTitle = this.hasDraftVersion ? 'item.version.history.table.action.hasDraft' : 'item.version.history.table.action.newVersion';
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of the workspace item, if present, otherwise return undefined
|
||||||
|
* @param versionItem the item for which retrieve the workspace item id
|
||||||
|
*/
|
||||||
|
getWorkspaceId(versionItem: Observable<RemoteData<Item>>): Observable<string> {
|
||||||
|
if (!this.hasDraftVersion) {
|
||||||
|
return of(undefined);
|
||||||
|
}
|
||||||
|
return versionItem.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((item: Item) => item.uuid),
|
||||||
|
switchMap((itemUuid: string) => this.workspaceItemDataService.findByItem(itemUuid, true)),
|
||||||
|
getFirstCompletedRemoteData<WorkspaceItem>(),
|
||||||
|
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the ID of the workflow item, if present, otherwise return undefined
|
||||||
|
* @param versionItem the item for which retrieve the workspace item id
|
||||||
|
*/
|
||||||
|
getWorkflowId(versionItem: Observable<RemoteData<Item>>): Observable<string> {
|
||||||
|
return this.getWorkspaceId(versionItem).pipe(
|
||||||
|
concatMap((workspaceId: string) => {
|
||||||
|
if (workspaceId) {
|
||||||
|
return of(undefined);
|
||||||
|
}
|
||||||
|
return versionItem.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload(),
|
||||||
|
map((item: Item) => item.uuid),
|
||||||
|
switchMap((itemUuid: string) => this.workflowItemDataService.findByItem(itemUuid, true)),
|
||||||
|
getFirstCompletedRemoteData<WorkspaceItem>(),
|
||||||
|
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id),
|
||||||
|
);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* redirect to the edit page of the workspace item
|
||||||
|
* @param id$ the id of the workspace item
|
||||||
|
*/
|
||||||
|
editWorkspaceItem(id$: Observable<string>) {
|
||||||
|
id$.subscribe((id) => {
|
||||||
|
void this.router.navigateByUrl('workspaceitems/' + id + '/edit');
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check if the current user can delete the version
|
||||||
|
* @param version
|
||||||
|
*/
|
||||||
|
canDeleteVersion(version: Version): Observable<boolean> {
|
||||||
|
return this.authorizationService.isAuthorized(FeatureID.CanDeleteVersion, version.self);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the route to the specified version
|
||||||
|
* @param versionId the ID of the version for which the route will be retrieved
|
||||||
|
*/
|
||||||
|
getVersionRoute(versionId: string) {
|
||||||
|
return getItemVersionRoute(versionId);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Creates a new version starting from the specified one
|
||||||
|
* @param version the version from which a new one will be created
|
||||||
|
*/
|
||||||
|
createNewVersion(version: Version) {
|
||||||
|
const versionNumber = version.version;
|
||||||
|
|
||||||
|
// Open modal and set current version number
|
||||||
|
const activeModal = this.modalService.open(ItemVersionsSummaryModalComponent);
|
||||||
|
activeModal.componentInstance.versionNumber = versionNumber;
|
||||||
|
|
||||||
|
// On createVersionEvent emitted create new version and notify
|
||||||
|
activeModal.componentInstance.createVersionEvent.pipe(
|
||||||
|
mergeMap((summary: string) => combineLatest([
|
||||||
|
of(summary),
|
||||||
|
version.item.pipe(getFirstSucceededRemoteDataPayload()),
|
||||||
|
])),
|
||||||
|
mergeMap(([summary, item]: [string, Item]) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
// close model (should be displaying loading/waiting indicator) when version creation failed/succeeded
|
||||||
|
tap(() => activeModal.close()),
|
||||||
|
// show success/failure notification
|
||||||
|
tap((newVersionRD: RemoteData<Version>) => {
|
||||||
|
this.itemVersionShared.notifyCreateNewVersion(newVersionRD);
|
||||||
|
if (newVersionRD.hasSucceeded) {
|
||||||
|
const versionHistory$ = this.versionService.getHistoryFromVersion(version).pipe(
|
||||||
|
tap((versionHistory: VersionHistory) => {
|
||||||
|
this.itemService.invalidateItemCache(this.item.uuid);
|
||||||
|
this.versionHistoryService.invalidateVersionHistoryCache(versionHistory.id);
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
this.versionsHistoryChange.emit(versionHistory$);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
// get workspace item
|
||||||
|
getFirstSucceededRemoteDataPayload<Version>(),
|
||||||
|
switchMap((newVersion: Version) => this.itemService.findByHref(newVersion._links.item.href)),
|
||||||
|
getFirstSucceededRemoteDataPayload<Item>(),
|
||||||
|
switchMap((newVersionItem: Item) => this.workspaceItemDataService.findByItem(newVersionItem.uuid, true, false)),
|
||||||
|
getFirstSucceededRemoteDataPayload<WorkspaceItem>(),
|
||||||
|
).subscribe((wsItem) => {
|
||||||
|
const wsiId = wsItem.id;
|
||||||
|
const route = 'workspaceitems/' + wsiId + '/edit';
|
||||||
|
this.router.navigateByUrl(route);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the specified version, notify the success/failure and redirect to latest version
|
||||||
|
* @param version the version to be deleted
|
||||||
|
* @param redirectToLatest force the redirect to the latest version in the history
|
||||||
|
*/
|
||||||
|
deleteVersion(version: Version, redirectToLatest: boolean): void {
|
||||||
|
const successMessageKey = 'item.version.delete.notification.success';
|
||||||
|
const failureMessageKey = 'item.version.delete.notification.failure';
|
||||||
|
const versionNumber = version.version;
|
||||||
|
const versionItem$ = version.item;
|
||||||
|
|
||||||
|
// Open modal
|
||||||
|
const activeModal = this.modalService.open(ItemVersionsDeleteModalComponent);
|
||||||
|
activeModal.componentInstance.versionNumber = version.version;
|
||||||
|
activeModal.componentInstance.firstVersion = false;
|
||||||
|
|
||||||
|
// On modal submit/dismiss
|
||||||
|
activeModal.componentInstance.response.pipe(take(1)).subscribe((ok) => {
|
||||||
|
if (ok) {
|
||||||
|
versionItem$.pipe(
|
||||||
|
getFirstSucceededRemoteDataPayload<Item>(),
|
||||||
|
// Retrieve version history
|
||||||
|
mergeMap((item: Item) => combineLatest([
|
||||||
|
of(item),
|
||||||
|
this.versionHistoryService.getVersionHistoryFromVersion$(version),
|
||||||
|
])),
|
||||||
|
// Delete item
|
||||||
|
mergeMap(([item, versionHistory]: [Item, VersionHistory]) => combineLatest([
|
||||||
|
this.deleteItemAndGetResult$(item),
|
||||||
|
of(versionHistory),
|
||||||
|
])),
|
||||||
|
// Retrieve new latest version
|
||||||
|
mergeMap(([deleteItemResult, versionHistory]: [boolean, VersionHistory]) => combineLatest([
|
||||||
|
of(deleteItemResult),
|
||||||
|
this.versionHistoryService.getLatestVersionItemFromHistory$(versionHistory).pipe(
|
||||||
|
tap(() => {
|
||||||
|
this.versionsHistoryChange.emit(of(versionHistory));
|
||||||
|
}),
|
||||||
|
),
|
||||||
|
])),
|
||||||
|
).subscribe(([deleteHasSucceeded, newLatestVersionItem]: [boolean, Item]) => {
|
||||||
|
// Notify operation result and redirect to latest item
|
||||||
|
if (deleteHasSucceeded) {
|
||||||
|
this.notificationsService.success(null, this.translateService.get(successMessageKey, { 'version': versionNumber }));
|
||||||
|
} else {
|
||||||
|
this.notificationsService.error(null, this.translateService.get(failureMessageKey, { 'version': versionNumber }));
|
||||||
|
}
|
||||||
|
if (redirectToLatest) {
|
||||||
|
const path = getItemEditVersionhistoryRoute(newLatestVersionItem);
|
||||||
|
this.router.navigateByUrl(path);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Delete the item and get the result of the operation
|
||||||
|
* @param item
|
||||||
|
*/
|
||||||
|
deleteItemAndGetResult$(item: Item): Observable<boolean> {
|
||||||
|
return this.itemService.delete(item.id).pipe(
|
||||||
|
getFirstCompletedRemoteData(),
|
||||||
|
map((deleteItemRes) => deleteItemRes.hasSucceeded),
|
||||||
|
take(1),
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* True when a version is being edited
|
||||||
|
* (used to disable buttons for other versions)
|
||||||
|
*/
|
||||||
|
isAnyBeingEdited(): boolean {
|
||||||
|
return this.versionBeingEditedNumber != null;
|
||||||
|
}
|
||||||
|
}
|
@@ -10,14 +10,13 @@
|
|||||||
[hideGear]="true"
|
[hideGear]="true"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
[paginationOptions]="options"
|
[paginationOptions]="options"
|
||||||
[pageInfoState]="versions"
|
|
||||||
[collectionSize]="versions?.totalElements"
|
[collectionSize]="versions?.totalElements"
|
||||||
[retainScrollPosition]="true">
|
[retainScrollPosition]="true">
|
||||||
<table class="table table-striped table-bordered align-middle my-2">
|
<table class="table table-striped table-bordered align-middle my-2">
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
<th scope="col">{{"item.version.history.table.version" | translate}}</th>
|
<th scope="col">{{"item.version.history.table.version" | translate}}</th>
|
||||||
<th scope="col" *ngIf="(showSubmitter() | async)">{{"item.version.history.table.editor" | translate}}</th>
|
<th scope="col" *ngIf="(showSubmitter$ | async)">{{"item.version.history.table.editor" | translate}}</th>
|
||||||
<th scope="col">{{"item.version.history.table.date" | translate}}</th>
|
<th scope="col">{{"item.version.history.table.date" | translate}}</th>
|
||||||
<th scope="col">{{"item.version.history.table.summary" | translate}}</th>
|
<th scope="col">{{"item.version.history.table.summary" | translate}}</th>
|
||||||
</tr>
|
</tr>
|
||||||
@@ -25,69 +24,15 @@
|
|||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let version of versions?.page" [id]="'version-row-' + version.id">
|
<tr *ngFor="let version of versions?.page" [id]="'version-row-' + version.id">
|
||||||
<td class="version-row-element-version">
|
<td class="version-row-element-version">
|
||||||
<!-- Get the ID of the workspace/workflow item (`undefined` if they don't exist).
|
<ds-item-versions-row-element-version [hasDraftVersion]="hasDraftVersion$ | async"
|
||||||
Conditionals inside *ngVar are needed in order to avoid useless calls. -->
|
[version]="version"
|
||||||
<ng-container *ngVar="((hasDraftVersion$ | async) ? getWorkspaceId(version?.item) : undefined) as workspaceId$">
|
[item]="item" [displayActions]="displayActions"
|
||||||
<ng-container *ngVar=" ((workspaceId$ | async) ? undefined : getWorkflowId(version?.item)) as workflowId$">
|
[itemVersion]="itemVersion"
|
||||||
|
[versionBeingEditedNumber]="versionBeingEditedNumber"
|
||||||
<div class="left-column">
|
(versionsHistoryChange)="getAllVersions($event)"
|
||||||
|
></ds-item-versions-row-element-version>
|
||||||
<span *ngIf="(workspaceId$ | async) || (workflowId$ | async); then versionNumberWithoutLink else versionNumberWithLink"></span>
|
|
||||||
<ng-template #versionNumberWithLink>
|
|
||||||
<a [routerLink]="getVersionRoute(version.id)">{{version.version}}</a>
|
|
||||||
</ng-template>
|
|
||||||
<ng-template #versionNumberWithoutLink>
|
|
||||||
{{version.version}}
|
|
||||||
</ng-template>
|
|
||||||
<span *ngIf="version?.id === itemVersion?.id">*</span>
|
|
||||||
|
|
||||||
<span *ngIf="workspaceId$ | async" class="text-light badge badge-primary ml-3">
|
|
||||||
{{ "item.version.history.table.workspaceItem" | translate }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
<span *ngIf="workflowId$ | async" class="text-light badge badge-info ml-3">
|
|
||||||
{{ "item.version.history.table.workflowItem" | translate }}
|
|
||||||
</span>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="right-column">
|
|
||||||
|
|
||||||
<div class="btn-group edit-field space-children-mr" *ngIf="displayActions">
|
|
||||||
<!--EDIT WORKSPACE ITEM-->
|
|
||||||
<button class="btn btn-outline-primary btn-sm version-row-element-edit"
|
|
||||||
*ngIf="workspaceId$ | async"
|
|
||||||
(click)="editWorkspaceItem(workspaceId$)"
|
|
||||||
title="{{'item.version.history.table.action.editWorkspaceItem' | translate }}">
|
|
||||||
<i class="fas fa-pencil-alt fa-fw"></i>
|
|
||||||
</button>
|
|
||||||
<!--CREATE-->
|
|
||||||
<ng-container *ngIf="canCreateVersion$ | async">
|
|
||||||
<button class="btn btn-outline-primary btn-sm version-row-element-create"
|
|
||||||
[disabled]="isAnyBeingEdited() || (hasDraftVersion$ | async)"
|
|
||||||
(click)="createNewVersion(version)"
|
|
||||||
title="{{createVersionTitle$ | async | translate }}">
|
|
||||||
<i class="fas fa-code-branch fa-fw"></i>
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
<!--DELETE-->
|
|
||||||
<ng-container *ngIf="canDeleteVersion$(version) | async">
|
|
||||||
<button class="btn btn-sm version-row-element-delete"
|
|
||||||
[ngClass]="isAnyBeingEdited() ? 'btn-outline-primary' : 'btn-outline-danger'"
|
|
||||||
[disabled]="isAnyBeingEdited()"
|
|
||||||
(click)="deleteVersion(version, version.id===itemVersion.id)"
|
|
||||||
title="{{'item.version.history.table.action.deleteVersion' | translate}}">
|
|
||||||
<i class="fas fa-trash fa-fw"></i>
|
|
||||||
</button>
|
|
||||||
</ng-container>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
</ng-container>
|
|
||||||
</ng-container>
|
|
||||||
</td>
|
</td>
|
||||||
<td class="version-row-element-editor" *ngIf="(showSubmitter() | async)">
|
<td class="version-row-element-editor" *ngIf="(showSubmitter$ | async)">
|
||||||
{{version?.submitterName}}
|
{{version?.submitterName}}
|
||||||
</td>
|
</td>
|
||||||
<td class="version-row-element-date">
|
<td class="version-row-element-date">
|
||||||
|
@@ -1,9 +0,0 @@
|
|||||||
.left-column {
|
|
||||||
float: left;
|
|
||||||
text-align: left;
|
|
||||||
}
|
|
||||||
|
|
||||||
.right-column {
|
|
||||||
float: right;
|
|
||||||
text-align: right;
|
|
||||||
}
|
|
||||||
|
@@ -206,19 +206,6 @@ describe('ItemVersionsComponent', () => {
|
|||||||
versions.forEach((version: Version, index: number) => {
|
versions.forEach((version: Version, index: number) => {
|
||||||
const versionItem = items[index];
|
const versionItem = items[index];
|
||||||
|
|
||||||
it(`should display version ${version.version} in the correct column for version ${version.id}`, () => {
|
|
||||||
const id = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-version`));
|
|
||||||
expect(id.nativeElement.textContent).toContain(version.version.toString());
|
|
||||||
});
|
|
||||||
|
|
||||||
// Check if the current version contains an asterisk
|
|
||||||
if (item1.uuid === versionItem.uuid) {
|
|
||||||
it('should add an asterisk to the version of the selected item', () => {
|
|
||||||
const item = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-version`));
|
|
||||||
expect(item.nativeElement.textContent).toContain('*');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
it(`should display date ${version.created} in the correct column for version ${version.id}`, () => {
|
it(`should display date ${version.created} in the correct column for version ${version.id}`, () => {
|
||||||
const date = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-date`));
|
const date = fixture.debugElement.query(By.css(`#version-row-${version.id} .version-row-element-date`));
|
||||||
switch (versionItem.uuid) {
|
switch (versionItem.uuid) {
|
||||||
@@ -319,44 +306,4 @@ describe('ItemVersionsComponent', () => {
|
|||||||
expect(component.isThisBeingEdited(version2)).toBeFalse();
|
expect(component.isThisBeingEdited(version2)).toBeFalse();
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe('when deleting a version', () => {
|
|
||||||
let deleteButton;
|
|
||||||
|
|
||||||
beforeEach(() => {
|
|
||||||
const canDelete = (featureID: FeatureID, url: string ) => of(featureID === FeatureID.CanDeleteVersion);
|
|
||||||
authorizationServiceSpy.isAuthorized.and.callFake(canDelete);
|
|
||||||
|
|
||||||
fixture.detectChanges();
|
|
||||||
|
|
||||||
// delete the last version in the table (version2 → item2)
|
|
||||||
deleteButton = fixture.debugElement.queryAll(By.css('.version-row-element-delete'))[1].nativeElement;
|
|
||||||
|
|
||||||
itemDataServiceSpy.delete.calls.reset();
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('if confirmed via modal', () => {
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
deleteButton.click();
|
|
||||||
fixture.detectChanges();
|
|
||||||
(document as any).querySelector('.modal-footer .confirm').click();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should call ItemService.delete', () => {
|
|
||||||
expect(itemDataServiceSpy.delete).toHaveBeenCalledWith(item2.id);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
describe('if canceled via modal', () => {
|
|
||||||
beforeEach(waitForAsync(() => {
|
|
||||||
deleteButton.click();
|
|
||||||
fixture.detectChanges();
|
|
||||||
(document as any).querySelector('.modal-footer .cancel').click();
|
|
||||||
}));
|
|
||||||
|
|
||||||
it('should not call ItemService.delete', () => {
|
|
||||||
expect(itemDataServiceSpy.delete).not.toHaveBeenCalled();
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
@@ -11,15 +11,8 @@ import {
|
|||||||
OnDestroy,
|
OnDestroy,
|
||||||
OnInit,
|
OnInit,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import {
|
import { FormsModule } from '@angular/forms';
|
||||||
FormsModule,
|
import { RouterLink } from '@angular/router';
|
||||||
UntypedFormBuilder,
|
|
||||||
} from '@angular/forms';
|
|
||||||
import {
|
|
||||||
Router,
|
|
||||||
RouterLink,
|
|
||||||
} from '@angular/router';
|
|
||||||
import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
|
|
||||||
import {
|
import {
|
||||||
TranslateModule,
|
TranslateModule,
|
||||||
TranslateService,
|
TranslateService,
|
||||||
@@ -28,22 +21,18 @@ import {
|
|||||||
BehaviorSubject,
|
BehaviorSubject,
|
||||||
combineLatest,
|
combineLatest,
|
||||||
Observable,
|
Observable,
|
||||||
of,
|
|
||||||
Subscription,
|
Subscription,
|
||||||
} from 'rxjs';
|
} from 'rxjs';
|
||||||
import {
|
import {
|
||||||
map,
|
map,
|
||||||
mergeMap,
|
|
||||||
startWith,
|
startWith,
|
||||||
switchMap,
|
switchMap,
|
||||||
take,
|
take,
|
||||||
tap,
|
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
import { ConfigurationDataService } from '../../core/data/configuration-data.service';
|
||||||
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../core/data/feature-authorization/feature-id';
|
||||||
import { ItemDataService } from '../../core/data/item-data.service';
|
|
||||||
import { PaginatedList } from '../../core/data/paginated-list.model';
|
import { PaginatedList } from '../../core/data/paginated-list.model';
|
||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { VersionDataService } from '../../core/data/version-data.service';
|
import { VersionDataService } from '../../core/data/version-data.service';
|
||||||
@@ -60,9 +49,6 @@ import {
|
|||||||
} from '../../core/shared/operators';
|
} from '../../core/shared/operators';
|
||||||
import { Version } from '../../core/shared/version.model';
|
import { Version } from '../../core/shared/version.model';
|
||||||
import { VersionHistory } from '../../core/shared/version-history.model';
|
import { VersionHistory } from '../../core/shared/version-history.model';
|
||||||
import { WorkspaceItem } from '../../core/submission/models/workspaceitem.model';
|
|
||||||
import { WorkflowItemDataService } from '../../core/submission/workflowitem-data.service';
|
|
||||||
import { WorkspaceitemDataService } from '../../core/submission/workspaceitem-data.service';
|
|
||||||
import { AlertComponent } from '../../shared/alert/alert.component';
|
import { AlertComponent } from '../../shared/alert/alert.component';
|
||||||
import { AlertType } from '../../shared/alert/alert-type';
|
import { AlertType } from '../../shared/alert/alert-type';
|
||||||
import {
|
import {
|
||||||
@@ -75,21 +61,15 @@ import { PaginationComponentOptions } from '../../shared/pagination/pagination-c
|
|||||||
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
import { PaginatedSearchOptions } from '../../shared/search/models/paginated-search-options.model';
|
||||||
import { followLink } from '../../shared/utils/follow-link-config.model';
|
import { followLink } from '../../shared/utils/follow-link-config.model';
|
||||||
import { VarDirective } from '../../shared/utils/var.directive';
|
import { VarDirective } from '../../shared/utils/var.directive';
|
||||||
import {
|
import { getItemPageRoute } from '../item-page-routing-paths';
|
||||||
getItemEditVersionhistoryRoute,
|
import { ItemVersionsRowElementVersionComponent } from './item-versions-row-element-version/item-versions-row-element-version.component';
|
||||||
getItemPageRoute,
|
|
||||||
getItemVersionRoute,
|
|
||||||
} from '../item-page-routing-paths';
|
|
||||||
import { ItemVersionsDeleteModalComponent } from './item-versions-delete-modal/item-versions-delete-modal.component';
|
|
||||||
import { ItemVersionsSharedService } from './item-versions-shared.service';
|
|
||||||
import { ItemVersionsSummaryModalComponent } from './item-versions-summary-modal/item-versions-summary-modal.component';
|
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-item-versions',
|
selector: 'ds-item-versions',
|
||||||
templateUrl: './item-versions.component.html',
|
templateUrl: './item-versions.component.html',
|
||||||
styleUrls: ['./item-versions.component.scss'],
|
styleUrls: ['./item-versions.component.scss'],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [VarDirective, NgIf, AlertComponent, PaginationComponent, NgFor, RouterLink, NgClass, FormsModule, AsyncPipe, DatePipe, TranslateModule],
|
imports: [VarDirective, NgIf, AlertComponent, PaginationComponent, NgFor, RouterLink, NgClass, FormsModule, AsyncPipe, DatePipe, TranslateModule, ItemVersionsRowElementVersionComponent],
|
||||||
})
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -162,6 +142,11 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
|
|||||||
*/
|
*/
|
||||||
hasDraftVersion$: Observable<boolean>;
|
hasDraftVersion$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show submitter in version history table
|
||||||
|
*/
|
||||||
|
showSubmitter$: Observable<boolean> = this.showSubmitter();
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The amount of versions to display per page
|
* The amount of versions to display per page
|
||||||
*/
|
*/
|
||||||
@@ -206,17 +191,10 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
constructor(private versionHistoryService: VersionHistoryDataService,
|
constructor(private versionHistoryService: VersionHistoryDataService,
|
||||||
private versionService: VersionDataService,
|
private versionService: VersionDataService,
|
||||||
private itemService: ItemDataService,
|
|
||||||
private paginationService: PaginationService,
|
private paginationService: PaginationService,
|
||||||
private formBuilder: UntypedFormBuilder,
|
|
||||||
private modalService: NgbModal,
|
|
||||||
private notificationsService: NotificationsService,
|
private notificationsService: NotificationsService,
|
||||||
private translateService: TranslateService,
|
private translateService: TranslateService,
|
||||||
private router: Router,
|
|
||||||
private itemVersionShared: ItemVersionsSharedService,
|
|
||||||
private authorizationService: AuthorizationDataService,
|
private authorizationService: AuthorizationDataService,
|
||||||
private workspaceItemDataService: WorkspaceitemDataService,
|
|
||||||
private workflowItemDataService: WorkflowItemDataService,
|
|
||||||
private configurationService: ConfigurationDataService,
|
private configurationService: ConfigurationDataService,
|
||||||
) {
|
) {
|
||||||
}
|
}
|
||||||
@@ -255,14 +233,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
|
|||||||
this.versionBeingEditedId = undefined;
|
this.versionBeingEditedId = undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the route to the specified version
|
|
||||||
* @param versionId the ID of the version for which the route will be retrieved
|
|
||||||
*/
|
|
||||||
getVersionRoute(versionId: string) {
|
|
||||||
return getItemVersionRoute(versionId);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Applies changes to version currently being edited
|
* Applies changes to version currently being edited
|
||||||
*/
|
*/
|
||||||
@@ -291,121 +261,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Delete the item and get the result of the operation
|
|
||||||
* @param item
|
|
||||||
*/
|
|
||||||
deleteItemAndGetResult$(item: Item): Observable<boolean> {
|
|
||||||
return this.itemService.delete(item.id).pipe(
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
map((deleteItemRes) => deleteItemRes.hasSucceeded),
|
|
||||||
take(1),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Deletes the specified version, notify the success/failure and redirect to latest version
|
|
||||||
* @param version the version to be deleted
|
|
||||||
* @param redirectToLatest force the redirect to the latest version in the history
|
|
||||||
*/
|
|
||||||
deleteVersion(version: Version, redirectToLatest: boolean): void {
|
|
||||||
const successMessageKey = 'item.version.delete.notification.success';
|
|
||||||
const failureMessageKey = 'item.version.delete.notification.failure';
|
|
||||||
const versionNumber = version.version;
|
|
||||||
const versionItem$ = version.item;
|
|
||||||
|
|
||||||
// Open modal
|
|
||||||
const activeModal = this.modalService.open(ItemVersionsDeleteModalComponent);
|
|
||||||
activeModal.componentInstance.versionNumber = version.version;
|
|
||||||
activeModal.componentInstance.firstVersion = false;
|
|
||||||
|
|
||||||
// On modal submit/dismiss
|
|
||||||
activeModal.componentInstance.response.pipe(take(1)).subscribe((ok) => {
|
|
||||||
if (ok) {
|
|
||||||
versionItem$.pipe(
|
|
||||||
getFirstSucceededRemoteDataPayload<Item>(),
|
|
||||||
// Retrieve version history
|
|
||||||
mergeMap((item: Item) => combineLatest([
|
|
||||||
of(item),
|
|
||||||
this.versionHistoryService.getVersionHistoryFromVersion$(version),
|
|
||||||
])),
|
|
||||||
// Delete item
|
|
||||||
mergeMap(([item, versionHistory]: [Item, VersionHistory]) => combineLatest([
|
|
||||||
this.deleteItemAndGetResult$(item),
|
|
||||||
of(versionHistory),
|
|
||||||
])),
|
|
||||||
// Retrieve new latest version
|
|
||||||
mergeMap(([deleteItemResult, versionHistory]: [boolean, VersionHistory]) => combineLatest([
|
|
||||||
of(deleteItemResult),
|
|
||||||
this.versionHistoryService.getLatestVersionItemFromHistory$(versionHistory).pipe(
|
|
||||||
tap(() => {
|
|
||||||
this.getAllVersions(of(versionHistory));
|
|
||||||
}),
|
|
||||||
),
|
|
||||||
])),
|
|
||||||
).subscribe(([deleteHasSucceeded, newLatestVersionItem]: [boolean, Item]) => {
|
|
||||||
// Notify operation result and redirect to latest item
|
|
||||||
if (deleteHasSucceeded) {
|
|
||||||
this.notificationsService.success(null, this.translateService.get(successMessageKey, { 'version': versionNumber }));
|
|
||||||
} else {
|
|
||||||
this.notificationsService.error(null, this.translateService.get(failureMessageKey, { 'version': versionNumber }));
|
|
||||||
}
|
|
||||||
if (redirectToLatest) {
|
|
||||||
const path = getItemEditVersionhistoryRoute(newLatestVersionItem);
|
|
||||||
this.router.navigateByUrl(path);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Creates a new version starting from the specified one
|
|
||||||
* @param version the version from which a new one will be created
|
|
||||||
*/
|
|
||||||
createNewVersion(version: Version) {
|
|
||||||
const versionNumber = version.version;
|
|
||||||
|
|
||||||
// Open modal and set current version number
|
|
||||||
const activeModal = this.modalService.open(ItemVersionsSummaryModalComponent);
|
|
||||||
activeModal.componentInstance.versionNumber = versionNumber;
|
|
||||||
|
|
||||||
// On createVersionEvent emitted create new version and notify
|
|
||||||
activeModal.componentInstance.createVersionEvent.pipe(
|
|
||||||
mergeMap((summary: string) => combineLatest([
|
|
||||||
of(summary),
|
|
||||||
version.item.pipe(getFirstSucceededRemoteDataPayload()),
|
|
||||||
])),
|
|
||||||
mergeMap(([summary, item]: [string, Item]) => this.versionHistoryService.createVersion(item._links.self.href, summary)),
|
|
||||||
getFirstCompletedRemoteData(),
|
|
||||||
// close model (should be displaying loading/waiting indicator) when version creation failed/succeeded
|
|
||||||
tap(() => activeModal.close()),
|
|
||||||
// show success/failure notification
|
|
||||||
tap((newVersionRD: RemoteData<Version>) => {
|
|
||||||
this.itemVersionShared.notifyCreateNewVersion(newVersionRD);
|
|
||||||
if (newVersionRD.hasSucceeded) {
|
|
||||||
const versionHistory$ = this.versionService.getHistoryFromVersion(version).pipe(
|
|
||||||
tap((versionHistory: VersionHistory) => {
|
|
||||||
this.itemService.invalidateItemCache(this.item.uuid);
|
|
||||||
this.versionHistoryService.invalidateVersionHistoryCache(versionHistory.id);
|
|
||||||
}),
|
|
||||||
);
|
|
||||||
this.getAllVersions(versionHistory$);
|
|
||||||
}
|
|
||||||
}),
|
|
||||||
// get workspace item
|
|
||||||
getFirstSucceededRemoteDataPayload<Version>(),
|
|
||||||
switchMap((newVersion: Version) => this.itemService.findByHref(newVersion._links.item.href)),
|
|
||||||
getFirstSucceededRemoteDataPayload<Item>(),
|
|
||||||
switchMap((newVersionItem: Item) => this.workspaceItemDataService.findByItem(newVersionItem.uuid, true, false)),
|
|
||||||
getFirstSucceededRemoteDataPayload<WorkspaceItem>(),
|
|
||||||
).subscribe((wsItem) => {
|
|
||||||
const wsiId = wsItem.id;
|
|
||||||
const route = 'workspaceitems/' + wsiId + '/edit';
|
|
||||||
this.router.navigateByUrl(route);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check is the current user can edit the version summary
|
* Check is the current user can edit the version summary
|
||||||
* @param version
|
* @param version
|
||||||
@@ -444,14 +299,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
|
|||||||
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Check if the current user can delete the version
|
|
||||||
* @param version
|
|
||||||
*/
|
|
||||||
canDeleteVersion$(version: Version): Observable<boolean> {
|
|
||||||
return this.authorizationService.isAuthorized(FeatureID.CanDeleteVersion, version.self);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get all versions for the given version history and store them in versionRD$
|
* Get all versions for the given version history and store them in versionRD$
|
||||||
* @param versionHistory$
|
* @param versionHistory$
|
||||||
@@ -477,44 +324,6 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
|
|||||||
this.getAllVersions(this.versionHistory$);
|
this.getAllVersions(this.versionHistory$);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the ID of the workspace item, if present, otherwise return undefined
|
|
||||||
* @param versionItem the item for which retrieve the workspace item id
|
|
||||||
*/
|
|
||||||
getWorkspaceId(versionItem): Observable<string> {
|
|
||||||
return versionItem.pipe(
|
|
||||||
getFirstSucceededRemoteDataPayload(),
|
|
||||||
map((item: Item) => item.uuid),
|
|
||||||
switchMap((itemUuid: string) => this.workspaceItemDataService.findByItem(itemUuid, true)),
|
|
||||||
getFirstCompletedRemoteData<WorkspaceItem>(),
|
|
||||||
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id ),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the ID of the workflow item, if present, otherwise return undefined
|
|
||||||
* @param versionItem the item for which retrieve the workspace item id
|
|
||||||
*/
|
|
||||||
getWorkflowId(versionItem): Observable<string> {
|
|
||||||
return versionItem.pipe(
|
|
||||||
getFirstSucceededRemoteDataPayload(),
|
|
||||||
map((item: Item) => item.uuid),
|
|
||||||
switchMap((itemUuid: string) => this.workflowItemDataService.findByItem(itemUuid, true)),
|
|
||||||
getFirstCompletedRemoteData<WorkspaceItem>(),
|
|
||||||
map((res: RemoteData<WorkspaceItem>) => res?.payload?.id ),
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* redirect to the edit page of the workspace item
|
|
||||||
* @param id$ the id of the workspace item
|
|
||||||
*/
|
|
||||||
editWorkspaceItem(id$: Observable<string>) {
|
|
||||||
id$.subscribe((id) => {
|
|
||||||
this.router.navigateByUrl('workspaceitems/' + id + '/edit');
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Initialize all observables
|
* Initialize all observables
|
||||||
*/
|
*/
|
||||||
@@ -532,19 +341,12 @@ export class ItemVersionsComponent implements OnDestroy, OnInit {
|
|||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.canCreateVersion$ = this.authorizationService.isAuthorized(FeatureID.CanCreateVersion, this.item.self);
|
|
||||||
|
|
||||||
// If there is a draft item in the version history the 'Create version' button is disabled and a different tooltip message is shown
|
// If there is a draft item in the version history the 'Create version' button is disabled and a different tooltip message is shown
|
||||||
this.hasDraftVersion$ = this.versionHistoryRD$.pipe(
|
this.hasDraftVersion$ = this.versionHistoryRD$.pipe(
|
||||||
getFirstSucceededRemoteDataPayload(),
|
getFirstSucceededRemoteDataPayload(),
|
||||||
map((res) => Boolean(res?.draftVersion)),
|
map((res) => Boolean(res?.draftVersion)),
|
||||||
);
|
);
|
||||||
|
|
||||||
this.createVersionTitle$ = this.hasDraftVersion$.pipe(
|
|
||||||
take(1),
|
|
||||||
switchMap((res) => of(res ? 'item.version.history.table.action.hasDraft' : 'item.version.history.table.action.newVersion')),
|
|
||||||
);
|
|
||||||
|
|
||||||
this.getAllVersions(this.versionHistory$);
|
this.getAllVersions(this.versionHistory$);
|
||||||
this.hasEpersons$ = this.versionsRD$.pipe(
|
this.hasEpersons$ = this.versionsRD$.pipe(
|
||||||
getAllSucceededRemoteData(),
|
getAllSucceededRemoteData(),
|
||||||
|
@@ -7,6 +7,7 @@ import {
|
|||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of as observableOf } from 'rxjs';
|
import { of as observableOf } from 'rxjs';
|
||||||
|
import { ServerResponseService } from 'src/app/core/services/server-response.service';
|
||||||
|
|
||||||
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
|
||||||
import { ObjectNotFoundComponent } from './objectnotfound.component';
|
import { ObjectNotFoundComponent } from './objectnotfound.component';
|
||||||
@@ -21,6 +22,10 @@ describe('ObjectNotFoundComponent', () => {
|
|||||||
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
const activatedRouteStub = Object.assign(new ActivatedRouteStub(), {
|
||||||
params: observableOf({ id: testUUID, idType: uuidType }),
|
params: observableOf({ id: testUUID, idType: uuidType }),
|
||||||
});
|
});
|
||||||
|
const serverResponseServiceStub = jasmine.createSpyObj('ServerResponseService', {
|
||||||
|
setNotFound: jasmine.createSpy('setNotFound'),
|
||||||
|
});
|
||||||
|
|
||||||
const activatedRouteStubHandle = Object.assign(new ActivatedRouteStub(), {
|
const activatedRouteStubHandle = Object.assign(new ActivatedRouteStub(), {
|
||||||
params: observableOf({ id: handleId, idType: handlePrefix }),
|
params: observableOf({ id: handleId, idType: handlePrefix }),
|
||||||
});
|
});
|
||||||
@@ -31,6 +36,7 @@ describe('ObjectNotFoundComponent', () => {
|
|||||||
TranslateModule.forRoot(),
|
TranslateModule.forRoot(),
|
||||||
ObjectNotFoundComponent,
|
ObjectNotFoundComponent,
|
||||||
], providers: [
|
], providers: [
|
||||||
|
{ provide: ServerResponseService, useValue: serverResponseServiceStub } ,
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
{ provide: ActivatedRoute, useValue: activatedRouteStub },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
@@ -52,6 +58,10 @@ describe('ObjectNotFoundComponent', () => {
|
|||||||
expect(comp.idType).toEqual(uuidType);
|
expect(comp.idType).toEqual(uuidType);
|
||||||
expect(comp.missingItem).toEqual('uuid: ' + testUUID);
|
expect(comp.missingItem).toEqual('uuid: ' + testUUID);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should call serverResponseService.setNotFound', () => {
|
||||||
|
expect(serverResponseServiceStub.setNotFound).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
describe( 'legacy handle request', () => {
|
describe( 'legacy handle request', () => {
|
||||||
@@ -61,6 +71,7 @@ describe('ObjectNotFoundComponent', () => {
|
|||||||
TranslateModule.forRoot(),
|
TranslateModule.forRoot(),
|
||||||
ObjectNotFoundComponent,
|
ObjectNotFoundComponent,
|
||||||
], providers: [
|
], providers: [
|
||||||
|
{ provide: ServerResponseService, useValue: serverResponseServiceStub },
|
||||||
{ provide: ActivatedRoute, useValue: activatedRouteStubHandle },
|
{ provide: ActivatedRoute, useValue: activatedRouteStubHandle },
|
||||||
],
|
],
|
||||||
schemas: [NO_ERRORS_SCHEMA],
|
schemas: [NO_ERRORS_SCHEMA],
|
||||||
@@ -78,6 +89,10 @@ describe('ObjectNotFoundComponent', () => {
|
|||||||
expect(comp.idType).toEqual(handlePrefix);
|
expect(comp.idType).toEqual(handlePrefix);
|
||||||
expect(comp.missingItem).toEqual('handle: ' + handlePrefix + '/' + handleId);
|
expect(comp.missingItem).toEqual('handle: ' + handlePrefix + '/' + handleId);
|
||||||
});
|
});
|
||||||
|
|
||||||
|
it('should call serverResponseService.setNotFound', () => {
|
||||||
|
expect(serverResponseServiceStub.setNotFound).toHaveBeenCalled();
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
});
|
});
|
||||||
|
@@ -9,6 +9,7 @@ import {
|
|||||||
RouterLink,
|
RouterLink,
|
||||||
} from '@angular/router';
|
} from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import { ServerResponseService } from 'src/app/core/services/server-response.service';
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* This component representing the `PageNotFound` DSpace page.
|
* This component representing the `PageNotFound` DSpace page.
|
||||||
@@ -35,7 +36,7 @@ export class ObjectNotFoundComponent implements OnInit {
|
|||||||
* @param {AuthService} authservice
|
* @param {AuthService} authservice
|
||||||
* @param {ServerResponseService} responseService
|
* @param {ServerResponseService} responseService
|
||||||
*/
|
*/
|
||||||
constructor(private route: ActivatedRoute) {
|
constructor(private route: ActivatedRoute, private serverResponseService: ServerResponseService) {
|
||||||
route.params.subscribe((params) => {
|
route.params.subscribe((params) => {
|
||||||
this.idType = params.idType;
|
this.idType = params.idType;
|
||||||
this.id = params.id;
|
this.id = params.id;
|
||||||
@@ -48,6 +49,7 @@ export class ObjectNotFoundComponent implements OnInit {
|
|||||||
} else {
|
} else {
|
||||||
this.missingItem = 'handle: ' + this.idType + '/' + this.id;
|
this.missingItem = 'handle: ' + this.idType + '/' + this.id;
|
||||||
}
|
}
|
||||||
|
this.serverResponseService.setNotFound();
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -47,7 +47,7 @@
|
|||||||
(showNotification)="showNotification($event)"></ds-google-recaptcha>
|
(showNotification)="showNotification($event)"></ds-google-recaptcha>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<ng-container *ngIf="((googleRecaptchaService.captchaVersion() | async) !== 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible'); else v2Invisible">
|
<ng-container *ngIf="(!registrationVerification || ((googleRecaptchaService.captchaVersion() | async) !== 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible')); else v2Invisible">
|
||||||
<button class="btn btn-primary" [disabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()">
|
<button class="btn btn-primary" [disabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()">
|
||||||
{{ MESSAGE_PREFIX + '.submit' | translate }}
|
{{ MESSAGE_PREFIX + '.submit' | translate }}
|
||||||
</button>
|
</button>
|
||||||
|
@@ -7,12 +7,12 @@
|
|||||||
[formControl]="input" ngbAutofocus (keyup.enter)="selectSingleResult()">
|
[formControl]="input" ngbAutofocus (keyup.enter)="selectSingleResult()">
|
||||||
</div>
|
</div>
|
||||||
<div class="dropdown-divider"></div>
|
<div class="dropdown-divider"></div>
|
||||||
<div class="scrollable-menu list-group">
|
<div id="scrollable-menu-dso-selector-{{randomSeed}}" class="scrollable-menu list-group">
|
||||||
<div
|
<div
|
||||||
infiniteScroll
|
infiniteScroll
|
||||||
[infiniteScrollDistance]="1"
|
[infiniteScrollDistance]="1"
|
||||||
[infiniteScrollThrottle]="0"
|
[infiniteScrollThrottle]="0"
|
||||||
[infiniteScrollContainer]="'.scrollable-menu'"
|
[infiniteScrollContainer]="'#scrollable-menu-dso-selector-' + randomSeed"
|
||||||
[fromRoot]="true"
|
[fromRoot]="true"
|
||||||
(scrolled)="onScrollDown()">
|
(scrolled)="onScrollDown()">
|
||||||
<ng-container *ngIf="listEntries$ | async">
|
<ng-container *ngIf="listEntries$ | async">
|
||||||
|
@@ -172,6 +172,11 @@ export class DSOSelectorComponent implements OnInit, OnDestroy {
|
|||||||
*/
|
*/
|
||||||
public subs: Subscription[] = [];
|
public subs: Subscription[] = [];
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Random seed of 4 characters to avoid duplicate ids
|
||||||
|
*/
|
||||||
|
randomSeed: string = Math.random().toString(36).substring(2, 6);
|
||||||
|
|
||||||
constructor(
|
constructor(
|
||||||
protected searchService: SearchService,
|
protected searchService: SearchService,
|
||||||
protected notifcationsService: NotificationsService,
|
protected notifcationsService: NotificationsService,
|
||||||
|
@@ -12,7 +12,6 @@ import {
|
|||||||
import { By } from '@angular/platform-browser';
|
import { By } from '@angular/platform-browser';
|
||||||
import { ActivatedRoute } from '@angular/router';
|
import { ActivatedRoute } from '@angular/router';
|
||||||
import { provideMockStore } from '@ngrx/store/testing';
|
import { provideMockStore } from '@ngrx/store/testing';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
import { of } from 'rxjs';
|
import { of } from 'rxjs';
|
||||||
import { AuthRequestService } from 'src/app/core/auth/auth-request.service';
|
import { AuthRequestService } from 'src/app/core/auth/auth-request.service';
|
||||||
@@ -27,6 +26,7 @@ import {
|
|||||||
APP_CONFIG,
|
APP_CONFIG,
|
||||||
APP_DATA_SERVICES_MAP,
|
APP_DATA_SERVICES_MAP,
|
||||||
} from '../../../../../config/app-config.interface';
|
} from '../../../../../config/app-config.interface';
|
||||||
|
import { REQUEST } from '../../../../../express.tokens';
|
||||||
import { Context } from '../../../../core/shared/context.model';
|
import { Context } from '../../../../core/shared/context.model';
|
||||||
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
import { GenericConstructor } from '../../../../core/shared/generic-constructor';
|
||||||
import { ListableModule } from '../../../../core/shared/listable.module';
|
import { ListableModule } from '../../../../core/shared/listable.module';
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
<ds-pagination
|
<ds-pagination
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="objects?.payload"
|
|
||||||
[collectionSize]="objects?.payload?.totalElements"
|
[collectionSize]="objects?.payload?.totalElements"
|
||||||
[sortOptions]="sortConfig"
|
[sortOptions]="sortConfig"
|
||||||
[objects]="objects"
|
[objects]="objects"
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
<ds-pagination
|
<ds-pagination
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="objects?.payload"
|
|
||||||
[collectionSize]="objects?.payload?.totalElements"
|
[collectionSize]="objects?.payload?.totalElements"
|
||||||
[sortOptions]="sortConfig"
|
[sortOptions]="sortConfig"
|
||||||
[hideGear]="hideGear"
|
[hideGear]="hideGear"
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
<ds-pagination
|
<ds-pagination
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="$any(objects?.payload)"
|
|
||||||
[collectionSize]="objects?.payload?.totalElements"
|
[collectionSize]="objects?.payload?.totalElements"
|
||||||
[objects]="objects"
|
[objects]="objects"
|
||||||
[sortOptions]="sortConfig"
|
[sortOptions]="sortConfig"
|
||||||
|
@@ -3,7 +3,6 @@
|
|||||||
*ngIf="collectionsRD?.payload?.totalElements > 0 || collectionsRD?.payload?.page?.length > 0"
|
*ngIf="collectionsRD?.payload?.totalElements > 0 || collectionsRD?.payload?.page?.length > 0"
|
||||||
[paginationOptions]="paginationOptions"
|
[paginationOptions]="paginationOptions"
|
||||||
[sortOptions]="sortOptions"
|
[sortOptions]="sortOptions"
|
||||||
[pageInfoState]="collectionsRD?.payload"
|
|
||||||
[collectionSize]="collectionsRD?.payload?.totalElements"
|
[collectionSize]="collectionsRD?.payload?.totalElements"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
[hideGear]="true">
|
[hideGear]="true">
|
||||||
@@ -16,9 +15,9 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let collection of collectionsRD?.payload?.page">
|
<tr *ngFor="let selectCollection of selectCollections$ | async">
|
||||||
<td><input #selectCollectionBtn [attr.aria-label]="(selectCollectionBtn.checked ? 'collection.select.table.deselect' : 'collection.select.table.select') | translate" class="collection-checkbox" [ngModel]="getSelected(collection.id) | async" (change)="switch(collection.id)" type="checkbox" name="{{collection.id}}"></td>
|
<td><input #selectCollectionBtn [attr.aria-label]="(selectCollectionBtn.checked ? 'collection.select.table.deselect' : 'collection.select.table.select') | translate" [disabled]="(selectCollection.canSelect$ | async) === false" class="collection-checkbox" [ngModel]="selectCollection.selected$ | async" (change)="switch(selectCollection.dso.id)" type="checkbox" name="{{selectCollection.dso.id}}"></td>
|
||||||
<td><a [routerLink]="['/collections', collection.id]">{{ dsoNameService.getName(collection) }}</a></td>
|
<td><a [routerLink]="selectCollection.route">{{ dsoNameService.getName(selectCollection.dso) }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@@ -4,20 +4,31 @@ import {
|
|||||||
NgFor,
|
NgFor,
|
||||||
NgIf,
|
NgIf,
|
||||||
} from '@angular/common';
|
} from '@angular/common';
|
||||||
import { Component } from '@angular/core';
|
import {
|
||||||
|
Component,
|
||||||
|
OnInit,
|
||||||
|
} from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
import { TranslateModule } from '@ngx-translate/core';
|
import { TranslateModule } from '@ngx-translate/core';
|
||||||
|
import {
|
||||||
|
map,
|
||||||
|
Observable,
|
||||||
|
} from 'rxjs';
|
||||||
|
|
||||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
import { getCollectionPageRoute } from '../../../collection-page/collection-page-routing-paths';
|
||||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { Collection } from '../../../core/shared/collection.model';
|
import { Collection } from '../../../core/shared/collection.model';
|
||||||
import { isNotEmpty } from '../../empty.util';
|
import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||||
|
import {
|
||||||
|
hasValueOperator,
|
||||||
|
isNotEmpty,
|
||||||
|
} from '../../empty.util';
|
||||||
import { ErrorComponent } from '../../error/error.component';
|
import { ErrorComponent } from '../../error/error.component';
|
||||||
import { ThemedLoadingComponent } from '../../loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../loading/themed-loading.component';
|
||||||
import { PaginationComponent } from '../../pagination/pagination.component';
|
import { PaginationComponent } from '../../pagination/pagination.component';
|
||||||
import { VarDirective } from '../../utils/var.directive';
|
import { VarDirective } from '../../utils/var.directive';
|
||||||
import { ObjectSelectService } from '../object-select.service';
|
import { DSpaceObjectSelect } from '../object-select.model';
|
||||||
import { ObjectSelectComponent } from '../object-select/object-select.component';
|
import { ObjectSelectComponent } from '../object-select/object-select.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -31,21 +42,29 @@ import { ObjectSelectComponent } from '../object-select/object-select.component'
|
|||||||
/**
|
/**
|
||||||
* A component used to select collections from a specific list and returning the UUIDs of the selected collections
|
* A component used to select collections from a specific list and returning the UUIDs of the selected collections
|
||||||
*/
|
*/
|
||||||
export class CollectionSelectComponent extends ObjectSelectComponent<Collection> {
|
export class CollectionSelectComponent extends ObjectSelectComponent<Collection> implements OnInit {
|
||||||
|
|
||||||
constructor(
|
/**
|
||||||
protected objectSelectService: ObjectSelectService,
|
* Collection of all the data that is used to display the {@link Collection} in the HTML.
|
||||||
protected authorizationService: AuthorizationDataService,
|
* By collecting this data here it doesn't need to be recalculated on evey change detection.
|
||||||
public dsoNameService: DSONameService,
|
*/
|
||||||
) {
|
selectCollections$: Observable<DSpaceObjectSelect<Collection>[]>;
|
||||||
super(objectSelectService, authorizationService);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
if (!isNotEmpty(this.confirmButton)) {
|
if (!isNotEmpty(this.confirmButton)) {
|
||||||
this.confirmButton = 'collection.select.confirm';
|
this.confirmButton = 'collection.select.confirm';
|
||||||
}
|
}
|
||||||
|
this.selectCollections$ = this.dsoRD$.pipe(
|
||||||
|
hasValueOperator(),
|
||||||
|
getAllSucceededRemoteDataPayload(),
|
||||||
|
map((collections: PaginatedList<Collection>) => collections.page.map((collection: Collection) => Object.assign(new DSpaceObjectSelect<Collection>(), {
|
||||||
|
dso: collection,
|
||||||
|
canSelect$: this.canSelect(collection),
|
||||||
|
selected$: this.getSelected(collection.id),
|
||||||
|
route: getCollectionPageRoute(collection.id),
|
||||||
|
} as DSpaceObjectSelect<Collection>))),
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
@@ -3,7 +3,6 @@
|
|||||||
*ngIf="itemsRD?.payload?.totalElements > 0"
|
*ngIf="itemsRD?.payload?.totalElements > 0"
|
||||||
[paginationOptions]="paginationOptions"
|
[paginationOptions]="paginationOptions"
|
||||||
[sortOptions]="sortOptions"
|
[sortOptions]="sortOptions"
|
||||||
[pageInfoState]="itemsRD?.payload"
|
|
||||||
[collectionSize]="itemsRD?.payload?.totalElements"
|
[collectionSize]="itemsRD?.payload?.totalElements"
|
||||||
[hidePagerWhenSinglePage]="true"
|
[hidePagerWhenSinglePage]="true"
|
||||||
[hideGear]="true">
|
[hideGear]="true">
|
||||||
@@ -18,17 +17,17 @@
|
|||||||
</tr>
|
</tr>
|
||||||
</thead>
|
</thead>
|
||||||
<tbody>
|
<tbody>
|
||||||
<tr *ngFor="let item of itemsRD?.payload?.page">
|
<tr *ngFor="let selectItem of selectItems$ | async">
|
||||||
<td><input #selectItemBtn [attr.aria-label]="(selectItemBtn.checked ? 'item.select.table.deselect' : 'item.select.table.select') | translate" [disabled]="(canSelect(item) | async) !== true" class="item-checkbox" [ngModel]="getSelected(item.id) | async" (change)="switch(item.id)" type="checkbox" name="{{item.id}}"></td>
|
<td><input #selectItemBtn [attr.aria-label]="(selectItemBtn.checked ? 'item.select.table.deselect' : 'item.select.table.select') | translate" [disabled]="(selectItem.canSelect$ | async) === false" class="item-checkbox" [ngModel]="selectItem.selected$ | async" (change)="switch(selectItem.dso.id)" type="checkbox" name="{{selectItem.dso.id}}"></td>
|
||||||
<td *ngIf="!hideCollection">
|
<td *ngIf="!hideCollection">
|
||||||
<span *ngVar="(item.owningCollection | async)?.payload as collection">
|
<span *ngVar="(selectItem.dso.owningCollection | async)?.payload as collection">
|
||||||
<a *ngIf="collection" [routerLink]="['/collections', collection?.id]">
|
<a *ngIf="collection" [routerLink]="['/collections', collection?.id]">
|
||||||
{{ dsoNameService.getName(collection) }}
|
{{ dsoNameService.getName(collection) }}
|
||||||
</a>
|
</a>
|
||||||
</span>
|
</span>
|
||||||
</td>
|
</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><span *ngIf="selectItem.dso.hasMetadata(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])">{{selectItem.dso.firstMetadataValue(['dc.contributor.author', 'dc.creator', 'dc.contributor.*'])}}</span></td>
|
||||||
<td><a [routerLink]="[(itemPageRoutes$ | async)[item.id]]">{{ dsoNameService.getName(item) }}</a></td>
|
<td><a [routerLink]="selectItem.route">{{ dsoNameService.getName(selectItem.dso) }}</a></td>
|
||||||
</tr>
|
</tr>
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
@@ -187,15 +187,16 @@ describe('ItemSelectComponent', () => {
|
|||||||
beforeEach(() => {
|
beforeEach(() => {
|
||||||
comp.featureId = FeatureID.CanManageMappings;
|
comp.featureId = FeatureID.CanManageMappings;
|
||||||
spyOn(authorizationDataService, 'isAuthorized').and.returnValue(of(false));
|
spyOn(authorizationDataService, 'isAuthorized').and.returnValue(of(false));
|
||||||
|
comp.ngOnInit();
|
||||||
});
|
});
|
||||||
|
|
||||||
it('should disable the checkbox', waitForAsync(() => {
|
it('should disable the checkbox', waitForAsync(async () => {
|
||||||
fixture.detectChanges();
|
fixture.detectChanges();
|
||||||
fixture.whenStable().then(() => {
|
await fixture.whenStable();
|
||||||
const checkbox = fixture.debugElement.query(By.css('input.item-checkbox')).nativeElement;
|
|
||||||
expect(authorizationDataService.isAuthorized).toHaveBeenCalled();
|
const checkbox = fixture.debugElement.query(By.css('input.item-checkbox')).nativeElement;
|
||||||
expect(checkbox.disabled).toBeTrue();
|
expect(authorizationDataService.isAuthorized).toHaveBeenCalled();
|
||||||
});
|
expect(checkbox.disabled).toBeTrue();
|
||||||
}));
|
}));
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
@@ -7,6 +7,7 @@ import {
|
|||||||
import {
|
import {
|
||||||
Component,
|
Component,
|
||||||
Input,
|
Input,
|
||||||
|
OnInit,
|
||||||
} from '@angular/core';
|
} from '@angular/core';
|
||||||
import { FormsModule } from '@angular/forms';
|
import { FormsModule } from '@angular/forms';
|
||||||
import { RouterLink } from '@angular/router';
|
import { RouterLink } from '@angular/router';
|
||||||
@@ -14,8 +15,7 @@ import { TranslateModule } from '@ngx-translate/core';
|
|||||||
import { Observable } from 'rxjs';
|
import { Observable } from 'rxjs';
|
||||||
import { map } from 'rxjs/operators';
|
import { map } from 'rxjs/operators';
|
||||||
|
|
||||||
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
import { PaginatedList } from '../../../core/data/paginated-list.model';
|
||||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
|
||||||
import { Item } from '../../../core/shared/item.model';
|
import { Item } from '../../../core/shared/item.model';
|
||||||
import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
import { getAllSucceededRemoteDataPayload } from '../../../core/shared/operators';
|
||||||
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
|
import { getItemPageRoute } from '../../../item-page/item-page-routing-paths';
|
||||||
@@ -27,7 +27,7 @@ import { ErrorComponent } from '../../error/error.component';
|
|||||||
import { ThemedLoadingComponent } from '../../loading/themed-loading.component';
|
import { ThemedLoadingComponent } from '../../loading/themed-loading.component';
|
||||||
import { PaginationComponent } from '../../pagination/pagination.component';
|
import { PaginationComponent } from '../../pagination/pagination.component';
|
||||||
import { VarDirective } from '../../utils/var.directive';
|
import { VarDirective } from '../../utils/var.directive';
|
||||||
import { ObjectSelectService } from '../object-select.service';
|
import { DSpaceObjectSelect } from '../object-select.model';
|
||||||
import { ObjectSelectComponent } from '../object-select/object-select.component';
|
import { ObjectSelectComponent } from '../object-select/object-select.component';
|
||||||
|
|
||||||
@Component({
|
@Component({
|
||||||
@@ -40,7 +40,7 @@ import { ObjectSelectComponent } from '../object-select/object-select.component'
|
|||||||
/**
|
/**
|
||||||
* A component used to select items from a specific list and returning the UUIDs of the selected items
|
* A component used to select items from a specific list and returning the UUIDs of the selected items
|
||||||
*/
|
*/
|
||||||
export class ItemSelectComponent extends ObjectSelectComponent<Item> {
|
export class ItemSelectComponent extends ObjectSelectComponent<Item> implements OnInit {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Whether or not to hide the collection column
|
* Whether or not to hide the collection column
|
||||||
@@ -49,35 +49,25 @@ export class ItemSelectComponent extends ObjectSelectComponent<Item> {
|
|||||||
hideCollection = false;
|
hideCollection = false;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* The routes to the items their pages
|
* Collection of all the data that is used to display the {@link Item} in the HTML.
|
||||||
* Key: Item ID
|
* By collecting this data here it doesn't need to be recalculated on evey change detection.
|
||||||
* Value: Route to item page
|
|
||||||
*/
|
*/
|
||||||
itemPageRoutes$: Observable<{
|
selectItems$: Observable<DSpaceObjectSelect<Item>[]>;
|
||||||
[itemId: string]: string
|
|
||||||
}>;
|
|
||||||
|
|
||||||
constructor(
|
|
||||||
protected objectSelectService: ObjectSelectService,
|
|
||||||
protected authorizationService: AuthorizationDataService,
|
|
||||||
public dsoNameService: DSONameService,
|
|
||||||
) {
|
|
||||||
super(objectSelectService, authorizationService);
|
|
||||||
}
|
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
super.ngOnInit();
|
super.ngOnInit();
|
||||||
if (!isNotEmpty(this.confirmButton)) {
|
if (!isNotEmpty(this.confirmButton)) {
|
||||||
this.confirmButton = 'item.select.confirm';
|
this.confirmButton = 'item.select.confirm';
|
||||||
}
|
}
|
||||||
this.itemPageRoutes$ = this.dsoRD$.pipe(
|
this.selectItems$ = this.dsoRD$.pipe(
|
||||||
hasValueOperator(),
|
hasValueOperator(),
|
||||||
getAllSucceededRemoteDataPayload(),
|
getAllSucceededRemoteDataPayload(),
|
||||||
map((items) => {
|
map((items: PaginatedList<Item>) => items.page.map((item: Item) => Object.assign(new DSpaceObjectSelect<Item>(), {
|
||||||
const itemPageRoutes = {};
|
dso: item,
|
||||||
items.page.forEach((item) => itemPageRoutes[item.uuid] = getItemPageRoute(item));
|
canSelect$: this.canSelect(item),
|
||||||
return itemPageRoutes;
|
selected$: this.getSelected(item.id),
|
||||||
}),
|
route: getItemPageRoute(item),
|
||||||
|
} as DSpaceObjectSelect<Item>))),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
30
src/app/shared/object-select/object-select.model.ts
Normal file
30
src/app/shared/object-select/object-select.model.ts
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
import { Observable } from 'rxjs';
|
||||||
|
|
||||||
|
import { DSpaceObject } from '../../core/shared/dspace-object.model';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Class used to collect all the data that that is used by the {@link ObjectSelectComponent} in the HTML.
|
||||||
|
*/
|
||||||
|
export class DSpaceObjectSelect<T extends DSpaceObject> {
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link DSpaceObject} to display
|
||||||
|
*/
|
||||||
|
dso: T;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the {@link DSpaceObject} can be selected
|
||||||
|
*/
|
||||||
|
canSelect$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Whether the {@link DSpaceObject} is selected
|
||||||
|
*/
|
||||||
|
selected$: Observable<boolean>;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The {@link DSpaceObject}'s route
|
||||||
|
*/
|
||||||
|
route: string;
|
||||||
|
|
||||||
|
}
|
@@ -15,6 +15,7 @@ import {
|
|||||||
take,
|
take,
|
||||||
} from 'rxjs/operators';
|
} from 'rxjs/operators';
|
||||||
|
|
||||||
|
import { DSONameService } from '../../../core/breadcrumbs/dso-name.service';
|
||||||
import { SortOptions } from '../../../core/cache/models/sort-options.model';
|
import { SortOptions } from '../../../core/cache/models/sort-options.model';
|
||||||
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service';
|
||||||
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
import { FeatureID } from '../../../core/data/feature-authorization/feature-id';
|
||||||
@@ -31,7 +32,7 @@ import { ObjectSelectService } from '../object-select.service';
|
|||||||
selector: 'ds-object-select-abstract',
|
selector: 'ds-object-select-abstract',
|
||||||
template: '',
|
template: '',
|
||||||
})
|
})
|
||||||
export abstract class ObjectSelectComponent<TDomain> implements OnInit, OnDestroy {
|
export abstract class ObjectSelectComponent<TDomain extends DSpaceObject> implements OnInit, OnDestroy {
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A unique key used for the object select service
|
* A unique key used for the object select service
|
||||||
@@ -102,8 +103,11 @@ export abstract class ObjectSelectComponent<TDomain> implements OnInit, OnDestro
|
|||||||
*/
|
*/
|
||||||
selectedIds$: Observable<string[]>;
|
selectedIds$: Observable<string[]>;
|
||||||
|
|
||||||
constructor(protected objectSelectService: ObjectSelectService,
|
constructor(
|
||||||
protected authorizationService: AuthorizationDataService) {
|
protected objectSelectService: ObjectSelectService,
|
||||||
|
protected authorizationService: AuthorizationDataService,
|
||||||
|
public dsoNameService: DSONameService,
|
||||||
|
) {
|
||||||
}
|
}
|
||||||
|
|
||||||
ngOnInit(): void {
|
ngOnInit(): void {
|
||||||
|
@@ -1,6 +1,5 @@
|
|||||||
<ds-pagination
|
<ds-pagination
|
||||||
[paginationOptions]="config"
|
[paginationOptions]="config"
|
||||||
[pageInfoState]="objects?.payload"
|
|
||||||
[collectionSize]="objects?.payload?.totalElements"
|
[collectionSize]="objects?.payload?.totalElements"
|
||||||
[sortOptions]="sortConfig"
|
[sortOptions]="sortConfig"
|
||||||
[hideGear]="hideGear"
|
[hideGear]="hideGear"
|
||||||
|
@@ -39,13 +39,11 @@ import { PaginatedList } from '../../core/data/paginated-list.model';
|
|||||||
import { RemoteData } from '../../core/data/remote-data';
|
import { RemoteData } from '../../core/data/remote-data';
|
||||||
import { PaginationService } from '../../core/pagination/pagination.service';
|
import { PaginationService } from '../../core/pagination/pagination.service';
|
||||||
import { PaginationRouteParams } from '../../core/pagination/pagination-route-params.interface';
|
import { PaginationRouteParams } from '../../core/pagination/pagination-route-params.interface';
|
||||||
import { PageInfo } from '../../core/shared/page-info.model';
|
|
||||||
import { ViewMode } from '../../core/shared/view-mode.model';
|
import { ViewMode } from '../../core/shared/view-mode.model';
|
||||||
import { hasValue } from '../empty.util';
|
import { hasValue } from '../empty.util';
|
||||||
import { HostWindowService } from '../host-window.service';
|
import { HostWindowService } from '../host-window.service';
|
||||||
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
import { ListableObject } from '../object-collection/shared/listable-object.model';
|
||||||
import { RSSComponent } from '../rss-feed/rss.component';
|
import { RSSComponent } from '../rss-feed/rss.component';
|
||||||
import { HostWindowState } from '../search/host-window.reducer';
|
|
||||||
import { EnumKeysPipe } from '../utils/enum-keys-pipe';
|
import { EnumKeysPipe } from '../utils/enum-keys-pipe';
|
||||||
import { PaginationComponentOptions } from './pagination-component-options.model';
|
import { PaginationComponentOptions } from './pagination-component-options.model';
|
||||||
|
|
||||||
@@ -73,11 +71,6 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
|||||||
*/
|
*/
|
||||||
@Input() collectionSize: number;
|
@Input() collectionSize: number;
|
||||||
|
|
||||||
/**
|
|
||||||
* Page state of a Remote paginated objects.
|
|
||||||
*/
|
|
||||||
@Input() pageInfoState: Observable<PageInfo> = undefined;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Configuration for the NgbPagination component.
|
* Configuration for the NgbPagination component.
|
||||||
*/
|
*/
|
||||||
@@ -167,18 +160,13 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
|||||||
/**
|
/**
|
||||||
* Current page.
|
* Current page.
|
||||||
*/
|
*/
|
||||||
public currentPage$;
|
public currentPage$: Observable<number>;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Current page in the state of a Remote paginated objects.
|
* Current page in the state of a Remote paginated objects.
|
||||||
*/
|
*/
|
||||||
public currentPageState: number = undefined;
|
public currentPageState: number = undefined;
|
||||||
|
|
||||||
/**
|
|
||||||
* An observable of HostWindowState type
|
|
||||||
*/
|
|
||||||
public hostWindow: Observable<HostWindowState>;
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* ID for the pagination instance. This ID is used in the routing to retrieve the pagination options.
|
* ID for the pagination instance. This ID is used in the routing to retrieve the pagination options.
|
||||||
* This ID needs to be unique between different pagination components when more than one will be displayed on the same page.
|
* This ID needs to be unique between different pagination components when more than one will be displayed on the same page.
|
||||||
@@ -268,7 +256,7 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
|||||||
map((currentPagination) => currentPagination.pageSize),
|
map((currentPagination) => currentPagination.pageSize),
|
||||||
);
|
);
|
||||||
|
|
||||||
let sortOptions;
|
let sortOptions: SortOptions;
|
||||||
if (this.sortOptions) {
|
if (this.sortOptions) {
|
||||||
sortOptions = this.sortOptions;
|
sortOptions = this.sortOptions;
|
||||||
} else {
|
} else {
|
||||||
@@ -282,16 +270,6 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
|||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* @param cdRef
|
|
||||||
* ChangeDetectorRef is a singleton service provided by Angular.
|
|
||||||
* @param route
|
|
||||||
* Route is a singleton service provided by Angular.
|
|
||||||
* @param router
|
|
||||||
* Router is a singleton service provided by Angular.
|
|
||||||
* @param hostWindowService
|
|
||||||
* the HostWindowService singleton.
|
|
||||||
*/
|
|
||||||
constructor(private cdRef: ChangeDetectorRef,
|
constructor(private cdRef: ChangeDetectorRef,
|
||||||
private paginationService: PaginationService,
|
private paginationService: PaginationService,
|
||||||
public hostWindowService: HostWindowService) {
|
public hostWindowService: HostWindowService) {
|
||||||
@@ -330,17 +308,6 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
|||||||
this.emitPaginationChange();
|
this.emitPaginationChange();
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Method to change the route to the given sort field
|
|
||||||
*
|
|
||||||
* @param sortField
|
|
||||||
* The sort field being navigated to.
|
|
||||||
*/
|
|
||||||
public doSortFieldChange(field: string) {
|
|
||||||
this.updateParams({ page: 1, sortField: field });
|
|
||||||
this.emitPaginationChange();
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Method to emit a general pagination change event
|
* Method to emit a general pagination change event
|
||||||
*/
|
*/
|
||||||
@@ -364,10 +331,10 @@ export class PaginationComponent implements OnDestroy, OnInit {
|
|||||||
if (collectionSize) {
|
if (collectionSize) {
|
||||||
showingDetails = this.paginationService.getCurrentPagination(this.id, this.paginationOptions).pipe(
|
showingDetails = this.paginationService.getCurrentPagination(this.id, this.paginationOptions).pipe(
|
||||||
map((currentPaginationOptions) => {
|
map((currentPaginationOptions) => {
|
||||||
let lastItem;
|
let lastItem: number;
|
||||||
const pageMax = currentPaginationOptions.pageSize * currentPaginationOptions.currentPage;
|
const pageMax = currentPaginationOptions.pageSize * currentPaginationOptions.currentPage;
|
||||||
|
|
||||||
const firstItem = currentPaginationOptions.pageSize * (currentPaginationOptions.currentPage - 1) + 1;
|
const firstItem: number = currentPaginationOptions.pageSize * (currentPaginationOptions.currentPage - 1) + 1;
|
||||||
if (collectionSize > pageMax) {
|
if (collectionSize > pageMax) {
|
||||||
lastItem = pageMax;
|
lastItem = pageMax;
|
||||||
} else {
|
} else {
|
||||||
|
@@ -1,7 +1,9 @@
|
|||||||
<a class="badge badge-primary mr-1 mb-1"
|
<a class="badge badge-primary mb-1"
|
||||||
[attr.aria-label]="'search.filters.remove' | translate:{ type: ('search.filters.applied.' + key) | translate, value: normalizeFilterValue(value) }"
|
[attr.aria-label]="'search.filters.remove' | translate:{ type: ('search.filters.applied.' + key) | translate, value: normalizeFilterValue(value) }"
|
||||||
[routerLink]="searchLink"
|
[routerLink]="searchLink"
|
||||||
[queryParams]="(removeParameters | async)" queryParamsHandling="merge">
|
[queryParams]="(removeParameters | async)" queryParamsHandling="merge">
|
||||||
{{('search.filters.applied.' + key) | translate}}: {{'search.filters.' + filterName + '.' + value | translate: {default: normalizeFilterValue(value)} }}
|
<span class="d-flex">
|
||||||
<span aria-hidden="true"> ×</span>
|
<span class="flex-grow-1 text-left">{{('search.filters.applied.' + key) | translate}}: {{'search.filters.' + filterName + '.' + value | translate: {default: normalizeFilterValue(value)} }}</span>
|
||||||
|
<span class="pl-1" aria-hidden="true">×</span>
|
||||||
|
</span>
|
||||||
</a>
|
</a>
|
||||||
|
@@ -0,0 +1,3 @@
|
|||||||
|
.badge {
|
||||||
|
white-space: inherit;
|
||||||
|
}
|
@@ -26,6 +26,7 @@ import { stripOperatorFromFilterValue } from '../../search.utils';
|
|||||||
@Component({
|
@Component({
|
||||||
selector: 'ds-search-label',
|
selector: 'ds-search-label',
|
||||||
templateUrl: './search-label.component.html',
|
templateUrl: './search-label.component.html',
|
||||||
|
styleUrls: ['./search-label.component.scss'],
|
||||||
standalone: true,
|
standalone: true,
|
||||||
imports: [RouterLink, AsyncPipe, TranslateModule],
|
imports: [RouterLink, AsyncPipe, TranslateModule],
|
||||||
})
|
})
|
||||||
|
@@ -1,3 +1,10 @@
|
|||||||
:host {
|
:host {
|
||||||
line-height: 1;
|
line-height: 1;
|
||||||
}
|
.labels {
|
||||||
|
margin: 0 calc(-1 * var(--bs-spacer)/8);
|
||||||
|
ds-search-label {
|
||||||
|
display: inline-block;
|
||||||
|
padding: 0 calc(var(--bs-spacer)/8);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
@@ -33,6 +33,7 @@
|
|||||||
| translate}}
|
| translate}}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
<ng-content select="[searchContentTop]"></ng-content>
|
||||||
<ds-themed-search-results *ngIf="inPlaceSearch"
|
<ds-themed-search-results *ngIf="inPlaceSearch"
|
||||||
[searchResults]="resultsRD$ | async"
|
[searchResults]="resultsRD$ | async"
|
||||||
[searchConfig]="searchOptions$ | async"
|
[searchConfig]="searchOptions$ | async"
|
||||||
@@ -95,8 +96,8 @@
|
|||||||
[inPlaceSearch]="inPlaceSearch"
|
[inPlaceSearch]="inPlaceSearch"
|
||||||
[searchPlaceholder]="searchFormPlaceholder | translate">
|
[searchPlaceholder]="searchFormPlaceholder | translate">
|
||||||
</ds-themed-search-form>
|
</ds-themed-search-form>
|
||||||
<div class="row mb-3 mb-md-1">
|
<div class="mb-3 mb-md-1">
|
||||||
<div class="labels col-sm-9">
|
<div class="labels">
|
||||||
<ds-search-labels [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
<ds-search-labels [inPlaceSearch]="inPlaceSearch"></ds-search-labels>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
48
src/app/shared/utils/browser-only.directive.ts
Normal file
48
src/app/shared/utils/browser-only.directive.ts
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
import { isPlatformBrowser } from '@angular/common';
|
||||||
|
import {
|
||||||
|
ChangeDetectorRef,
|
||||||
|
Directive,
|
||||||
|
Inject,
|
||||||
|
OnInit,
|
||||||
|
PLATFORM_ID,
|
||||||
|
TemplateRef,
|
||||||
|
ViewContainerRef,
|
||||||
|
} from '@angular/core';
|
||||||
|
|
||||||
|
@Directive({
|
||||||
|
selector: '[dsRenderOnlyForBrowser]',
|
||||||
|
standalone: true,
|
||||||
|
})
|
||||||
|
/**
|
||||||
|
* Structural Directive for rendering a template reference on client side only
|
||||||
|
*/
|
||||||
|
export class BrowserOnlyDirective implements OnInit {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
@Inject(PLATFORM_ID) protected platformId: string,
|
||||||
|
private viewContainer: ViewContainerRef,
|
||||||
|
private changeDetector: ChangeDetectorRef,
|
||||||
|
private templateRef: TemplateRef<any>,
|
||||||
|
) {
|
||||||
|
}
|
||||||
|
|
||||||
|
ngOnInit(): void {
|
||||||
|
this.showTemplateBlockInView();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Show template in view container according to platform
|
||||||
|
*/
|
||||||
|
private showTemplateBlockInView(): void {
|
||||||
|
if (!this.templateRef) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
this.viewContainer.clear();
|
||||||
|
|
||||||
|
if (isPlatformBrowser(this.platformId)) {
|
||||||
|
this.viewContainer.createEmbeddedView(this.templateRef);
|
||||||
|
this.changeDetector.markForCheck();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -1,7 +1,12 @@
|
|||||||
import { HttpHeaders } from '@angular/common/http';
|
import { HttpHeaders } from '@angular/common/http';
|
||||||
import { Injectable } from '@angular/core';
|
import { Injectable } from '@angular/core';
|
||||||
import { Router } from '@angular/router';
|
import { Router } from '@angular/router';
|
||||||
import { Store } from '@ngrx/store';
|
import {
|
||||||
|
createSelector,
|
||||||
|
MemoizedSelector,
|
||||||
|
select,
|
||||||
|
Store,
|
||||||
|
} from '@ngrx/store';
|
||||||
import { TranslateService } from '@ngx-translate/core';
|
import { TranslateService } from '@ngx-translate/core';
|
||||||
import {
|
import {
|
||||||
Observable,
|
Observable,
|
||||||
@@ -71,6 +76,20 @@ import {
|
|||||||
SubmissionState,
|
SubmissionState,
|
||||||
} from './submission.reducers';
|
} from './submission.reducers';
|
||||||
|
|
||||||
|
function getSubmissionSelector(submissionId: string): MemoizedSelector<SubmissionState, SubmissionObjectEntry> {
|
||||||
|
return createSelector(
|
||||||
|
submissionSelector,
|
||||||
|
(state: SubmissionState) => state.objects[submissionId],
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
function getSubmissionCollectionIdSelector(submissionId: string): MemoizedSelector<SubmissionState, string> {
|
||||||
|
return createSelector(
|
||||||
|
getSubmissionSelector(submissionId),
|
||||||
|
(submission: SubmissionObjectEntry) => submission?.collection,
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* A service that provides methods used in submission process.
|
* A service that provides methods used in submission process.
|
||||||
*/
|
*/
|
||||||
@@ -120,10 +139,19 @@ export class SubmissionService {
|
|||||||
* @param collectionId
|
* @param collectionId
|
||||||
* The collection id
|
* The collection id
|
||||||
*/
|
*/
|
||||||
changeSubmissionCollection(submissionId, collectionId) {
|
changeSubmissionCollection(submissionId: string, collectionId: string): void {
|
||||||
this.store.dispatch(new ChangeSubmissionCollectionAction(submissionId, collectionId));
|
this.store.dispatch(new ChangeSubmissionCollectionAction(submissionId, collectionId));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Listen to collection changes for a certain {@link SubmissionObject}
|
||||||
|
*
|
||||||
|
* @param submissionId The submission id
|
||||||
|
*/
|
||||||
|
getSubmissionCollectionId(submissionId: string): Observable<string> {
|
||||||
|
return this.store.pipe(select(getSubmissionCollectionIdSelector(submissionId)));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Perform a REST call to create a new workspaceitem and return response
|
* Perform a REST call to create a new workspaceitem and return response
|
||||||
*
|
*
|
||||||
|
@@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Resolve } from '@angular/router';
|
||||||
|
|
||||||
|
import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
||||||
|
import { SubmissionObject } from '../core/submission/models/submission-object.model';
|
||||||
|
import { SubmissionParentBreadcrumbResolver } from '../core/submission/resolver/submission-parent-breadcrumb.resolver';
|
||||||
|
import { SubmissionParentBreadcrumbsService } from '../core/submission/submission-parent-breadcrumb.service';
|
||||||
|
import { WorkflowItemDataService } from '../core/submission/workflowitem-data.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a resolver that retrieves the breadcrumbs of the workflow item
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ItemFromWorkflowBreadcrumbResolver extends SubmissionParentBreadcrumbResolver implements Resolve<BreadcrumbConfig<SubmissionObject>> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected dataService: WorkflowItemDataService,
|
||||||
|
protected breadcrumbService: SubmissionParentBreadcrumbsService,
|
||||||
|
) {
|
||||||
|
super(dataService, breadcrumbService);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -6,6 +6,7 @@ import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item-
|
|||||||
import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component';
|
import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component';
|
||||||
import { AdvancedWorkflowActionPageComponent } from './advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component';
|
import { AdvancedWorkflowActionPageComponent } from './advanced-workflow-action/advanced-workflow-action-page/advanced-workflow-action-page.component';
|
||||||
import { itemFromWorkflowResolver } from './item-from-workflow.resolver';
|
import { itemFromWorkflowResolver } from './item-from-workflow.resolver';
|
||||||
|
import { ItemFromWorkflowBreadcrumbResolver } from './item-from-workflow-breadcrumb.resolver';
|
||||||
import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component';
|
import { ThemedWorkflowItemDeleteComponent } from './workflow-item-delete/themed-workflow-item-delete.component';
|
||||||
import { workflowItemPageResolver } from './workflow-item-page.resolver';
|
import { workflowItemPageResolver } from './workflow-item-page.resolver';
|
||||||
import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component';
|
import { ThemedWorkflowItemSendBackComponent } from './workflow-item-send-back/themed-workflow-item-send-back.component';
|
||||||
@@ -20,7 +21,10 @@ import {
|
|||||||
export const ROUTES: Routes = [
|
export const ROUTES: Routes = [
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
resolve: { wfi: workflowItemPageResolver },
|
resolve: {
|
||||||
|
breadcrumb: ItemFromWorkflowBreadcrumbResolver,
|
||||||
|
wfi: workflowItemPageResolver,
|
||||||
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
canActivate: [authenticatedGuard],
|
canActivate: [authenticatedGuard],
|
||||||
|
@@ -0,0 +1,25 @@
|
|||||||
|
import { Injectable } from '@angular/core';
|
||||||
|
import { Resolve } from '@angular/router';
|
||||||
|
|
||||||
|
import { BreadcrumbConfig } from '../breadcrumbs/breadcrumb/breadcrumb-config.model';
|
||||||
|
import { SubmissionObject } from '../core/submission/models/submission-object.model';
|
||||||
|
import { SubmissionParentBreadcrumbResolver } from '../core/submission/resolver/submission-parent-breadcrumb.resolver';
|
||||||
|
import { SubmissionParentBreadcrumbsService } from '../core/submission/submission-parent-breadcrumb.service';
|
||||||
|
import { WorkspaceitemDataService } from '../core/submission/workspaceitem-data.service';
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This class represents a resolver that retrieves the breadcrumbs of the workspace item
|
||||||
|
*/
|
||||||
|
@Injectable({
|
||||||
|
providedIn: 'root',
|
||||||
|
})
|
||||||
|
export class ItemFromWorkspaceBreadcrumbResolver extends SubmissionParentBreadcrumbResolver implements Resolve<BreadcrumbConfig<SubmissionObject>> {
|
||||||
|
|
||||||
|
constructor(
|
||||||
|
protected dataService: WorkspaceitemDataService,
|
||||||
|
protected breadcrumbService: SubmissionParentBreadcrumbsService,
|
||||||
|
) {
|
||||||
|
super(dataService, breadcrumbService);
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
@@ -5,6 +5,7 @@ import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
|
|||||||
import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item-page.component';
|
import { ThemedFullItemPageComponent } from '../item-page/full/themed-full-item-page.component';
|
||||||
import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component';
|
import { ThemedSubmissionEditComponent } from '../submission/edit/themed-submission-edit.component';
|
||||||
import { itemFromWorkspaceResolver } from './item-from-workspace.resolver';
|
import { itemFromWorkspaceResolver } from './item-from-workspace.resolver';
|
||||||
|
import { ItemFromWorkspaceBreadcrumbResolver } from './item-from-workspace-breadcrumb.resolver';
|
||||||
import { workspaceItemPageResolver } from './workspace-item-page.resolver';
|
import { workspaceItemPageResolver } from './workspace-item-page.resolver';
|
||||||
import { ThemedWorkspaceItemsDeletePageComponent } from './workspaceitems-delete-page/themed-workspaceitems-delete-page.component';
|
import { ThemedWorkspaceItemsDeletePageComponent } from './workspaceitems-delete-page/themed-workspaceitems-delete-page.component';
|
||||||
import { WorkspaceItemsDeletePageComponent } from './workspaceitems-delete-page/workspaceitems-delete-page.component';
|
import { WorkspaceItemsDeletePageComponent } from './workspaceitems-delete-page/workspaceitems-delete-page.component';
|
||||||
@@ -16,7 +17,10 @@ export const ROUTES: Route[] = [
|
|||||||
},
|
},
|
||||||
{
|
{
|
||||||
path: ':id',
|
path: ':id',
|
||||||
resolve: { wsi: workspaceItemPageResolver },
|
resolve: {
|
||||||
|
breadcrumb: ItemFromWorkspaceBreadcrumbResolver,
|
||||||
|
wsi: workspaceItemPageResolver,
|
||||||
|
},
|
||||||
children: [
|
children: [
|
||||||
{
|
{
|
||||||
canActivate: [authenticatedGuard],
|
canActivate: [authenticatedGuard],
|
||||||
|
@@ -1108,6 +1108,8 @@
|
|||||||
|
|
||||||
"claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow",
|
"claimed-declined-task-search-result-list-element.title": "Declined, sent back to Review Manager's workflow",
|
||||||
|
|
||||||
|
"collection.create.breadcrumbs": "Create collection",
|
||||||
|
|
||||||
"collection.browse.logo": "Browse for a collection logo",
|
"collection.browse.logo": "Browse for a collection logo",
|
||||||
|
|
||||||
"collection.create.head": "Create a Collection",
|
"collection.create.head": "Create a Collection",
|
||||||
@@ -1398,6 +1400,8 @@
|
|||||||
|
|
||||||
"community.subcoms-cols.breadcrumbs": "Subcommunities and Collections",
|
"community.subcoms-cols.breadcrumbs": "Subcommunities and Collections",
|
||||||
|
|
||||||
|
"community.create.breadcrumbs": "Create Community",
|
||||||
|
|
||||||
"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",
|
||||||
|
@@ -1,6 +1,6 @@
|
|||||||
import { AppConfig } from './app-config.interface';
|
import { AppConfig } from './app-config.interface';
|
||||||
import { UniversalConfig } from './universal-config.interface';
|
import { SSRConfig } from './ssr-config.interface';
|
||||||
|
|
||||||
export interface BuildConfig extends AppConfig {
|
export interface BuildConfig extends AppConfig {
|
||||||
universal: UniversalConfig;
|
ssr: SSRConfig;
|
||||||
}
|
}
|
||||||
|
@@ -34,7 +34,7 @@ export class DefaultAppConfig implements AppConfig {
|
|||||||
// NOTE: will log all redux actions and transfers in console
|
// NOTE: will log all redux actions and transfers in console
|
||||||
debug = false;
|
debug = false;
|
||||||
|
|
||||||
// Angular Universal server settings
|
// Angular express server settings
|
||||||
// NOTE: these must be 'synced' with the 'dspace.ui.url' setting in your backend's local.cfg.
|
// NOTE: these must be 'synced' with the 'dspace.ui.url' setting in your backend's local.cfg.
|
||||||
ui: UIServerConfig = {
|
ui: UIServerConfig = {
|
||||||
ssl: false,
|
ssl: false,
|
||||||
|
21
src/config/ssr-config.interface.ts
Normal file
21
src/config/ssr-config.interface.ts
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
import { Config } from './config.interface';
|
||||||
|
|
||||||
|
export interface SSRConfig extends Config {
|
||||||
|
/**
|
||||||
|
* A boolean flag indicating whether the SSR configuration is enabled
|
||||||
|
* Defaults to true.
|
||||||
|
*/
|
||||||
|
enabled: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Enable request performance profiling data collection and printing the results in the server console.
|
||||||
|
* Defaults to false.
|
||||||
|
*/
|
||||||
|
enablePerformanceProfiler: boolean;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Reduce render blocking requests by inlining critical CSS.
|
||||||
|
* Defaults to true.
|
||||||
|
*/
|
||||||
|
inlineCriticalCss: boolean;
|
||||||
|
}
|
@@ -6,5 +6,5 @@ export const StoreDevModules = [
|
|||||||
StoreDevtoolsModule.instrument({
|
StoreDevtoolsModule.instrument({
|
||||||
maxAge: 1000,
|
maxAge: 1000,
|
||||||
logOnly: false,
|
logOnly: false,
|
||||||
}),
|
connectInZone: true }),
|
||||||
];
|
];
|
||||||
|
@@ -1,16 +0,0 @@
|
|||||||
import { Config } from './config.interface';
|
|
||||||
|
|
||||||
export interface UniversalConfig extends Config {
|
|
||||||
preboot: boolean;
|
|
||||||
async: boolean;
|
|
||||||
time: boolean;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Whether to inline "critical" styles into the server-side rendered HTML.
|
|
||||||
*
|
|
||||||
* Determining which styles are critical is a relatively expensive operation;
|
|
||||||
* this option can be disabled to boost server performance at the expense of
|
|
||||||
* loading smoothness.
|
|
||||||
*/
|
|
||||||
inlineCriticalCss?: boolean;
|
|
||||||
}
|
|
@@ -3,11 +3,10 @@ import { BuildConfig } from '../config/build-config.interface';
|
|||||||
export const environment: Partial<BuildConfig> = {
|
export const environment: Partial<BuildConfig> = {
|
||||||
production: true,
|
production: true,
|
||||||
|
|
||||||
// Angular Universal settings
|
// Angular SSR settings
|
||||||
universal: {
|
ssr: {
|
||||||
preboot: true,
|
enabled: true,
|
||||||
async: true,
|
enablePerformanceProfiler: false,
|
||||||
time: false,
|
|
||||||
inlineCriticalCss: true,
|
inlineCriticalCss: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
@@ -7,14 +7,14 @@ import { NotificationAnimationsType } from '../app/shared/notifications/models/n
|
|||||||
export const environment: BuildConfig = {
|
export const environment: BuildConfig = {
|
||||||
production: false,
|
production: false,
|
||||||
|
|
||||||
// Angular Universal settings
|
// Angular SSR settings
|
||||||
universal: {
|
ssr: {
|
||||||
preboot: true,
|
enabled: true,
|
||||||
async: true,
|
enablePerformanceProfiler: false,
|
||||||
time: false,
|
inlineCriticalCss: true,
|
||||||
},
|
},
|
||||||
|
|
||||||
// Angular Universal server settings.
|
// Angular express server settings.
|
||||||
ui: {
|
ui: {
|
||||||
ssl: false,
|
ssl: false,
|
||||||
host: 'dspace.com',
|
host: 'dspace.com',
|
||||||
|
@@ -8,11 +8,10 @@ import { BuildConfig } from '../config/build-config.interface';
|
|||||||
export const environment: Partial<BuildConfig> = {
|
export const environment: Partial<BuildConfig> = {
|
||||||
production: false,
|
production: false,
|
||||||
|
|
||||||
// Angular Universal settings
|
// Angular SSR settings
|
||||||
universal: {
|
ssr: {
|
||||||
preboot: false,
|
enabled: false,
|
||||||
async: true,
|
enablePerformanceProfiler: false,
|
||||||
time: false,
|
|
||||||
inlineCriticalCss: true,
|
inlineCriticalCss: true,
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
8
src/express.tokens.ts
Normal file
8
src/express.tokens.ts
Normal file
@@ -0,0 +1,8 @@
|
|||||||
|
import { InjectionToken } from '@angular/core';
|
||||||
|
import {
|
||||||
|
Request,
|
||||||
|
Response,
|
||||||
|
} from 'express';
|
||||||
|
|
||||||
|
export const REQUEST: InjectionToken<Request> = new InjectionToken<Request>('REQUEST');
|
||||||
|
export const RESPONSE: InjectionToken<Response> = new InjectionToken<Response>('RESPONSE');
|
@@ -14,6 +14,4 @@ import { serverAppConfig } from './modules/app/server-app.config';
|
|||||||
|
|
||||||
const bootstrap = () => bootstrapApplication(AppComponent, serverAppConfig);
|
const bootstrap = () => bootstrapApplication(AppComponent, serverAppConfig);
|
||||||
|
|
||||||
export { renderModule } from '@angular/platform-server';
|
|
||||||
export { ngExpressEngine } from '@nguniversal/express-engine';
|
|
||||||
export default bootstrap;
|
export default bootstrap;
|
||||||
|
@@ -20,7 +20,6 @@ import {
|
|||||||
StoreConfig,
|
StoreConfig,
|
||||||
StoreModule,
|
StoreModule,
|
||||||
} from '@ngrx/store';
|
} from '@ngrx/store';
|
||||||
import { REQUEST } from '@nguniversal/express-engine/tokens';
|
|
||||||
import {
|
import {
|
||||||
MissingTranslationHandler,
|
MissingTranslationHandler,
|
||||||
TranslateLoader,
|
TranslateLoader,
|
||||||
@@ -59,6 +58,7 @@ import { KlaroService } from '../../app/shared/cookies/klaro.service';
|
|||||||
import { MissingTranslationHelper } from '../../app/shared/translate/missing-translation.helper';
|
import { MissingTranslationHelper } from '../../app/shared/translate/missing-translation.helper';
|
||||||
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
import { GoogleAnalyticsService } from '../../app/statistics/google-analytics.service';
|
||||||
import { SubmissionService } from '../../app/submission/submission.service';
|
import { SubmissionService } from '../../app/submission/submission.service';
|
||||||
|
import { REQUEST } from '../../express.tokens';
|
||||||
import { TranslateBrowserLoader } from '../../ngx-translate-loaders/translate-browser.loader';
|
import { TranslateBrowserLoader } from '../../ngx-translate-loaders/translate-browser.loader';
|
||||||
import { BrowserInitService } from './browser-init.service';
|
import { BrowserInitService } from './browser-init.service';
|
||||||
|
|
||||||
|
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user