Merge remote-tracking branch 'upstream/main' into minor-themed-component-fixes_contribute-main

This commit is contained in:
Alexandre Vryghem
2024-04-29 23:53:27 +02:00
125 changed files with 3033 additions and 2784 deletions

1
.gitignore vendored
View File

@@ -1,4 +1,5 @@
/.angular/cache /.angular/cache
/.nx
/__build__ /__build__
/__server_build__ /__server_build__
/node_modules /node_modules

View File

@@ -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": [
"/" "/"

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

View File

@@ -4,6 +4,7 @@
"**/*.ts" "**/*.ts"
], ],
"compilerOptions": { "compilerOptions": {
"sourceMap": false,
"types": [ "types": [
"cypress", "cypress",
"cypress-axe", "cypress-axe",

View File

@@ -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
``` ```

View File

@@ -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:

View File

@@ -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:

View File

@@ -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,7 +200,7 @@
"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",

114
server.ts
View File

@@ -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,30 +233,50 @@ 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,
useValue: req,
},
{
provide: RESPONSE,
useValue: res,
},
{
provide: APP_CONFIG,
useValue: environment,
},
],
})
.then((html) => {
if (hasValue(html)) {
// save server side rendered page to cache (if any are enabled) // save server side rendered page to cache (if any are enabled)
saveToCache(req, data); saveToCache(req, html);
if (sendToUser) { if (sendToUser) {
res.locals.ssr = true; // mark response as SSR (enables text compression) res.locals.ssr = true; // mark response as SSR (enables text compression)
// send rendered page to user // send rendered page to user
res.send(data); res.send(html);
} }
} else if (hasValue(err) && err.code === 'ERR_HTTP_HEADERS_SENT') { }
})
.catch((err) => {
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 // When this error occurs we can't fall back to CSR because the response has already been
// sent. These errors occur for various reasons in universal, not all of which are in our // sent. These errors occur for various reasons in universal, not all of which are in our
// control to solve. // control to solve.
@@ -291,6 +291,7 @@ function serverSideRender(req, res, sendToUser: boolean = true) {
clientSideRender(req, res); clientSideRender(req, res);
} }
} }
next(err);
}); });
} }
@@ -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();
}); });

View File

@@ -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"

View File

@@ -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">

View File

@@ -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"

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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">

View File

@@ -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}}&#125;</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>

View File

@@ -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">

View File

@@ -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: {

View File

@@ -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(),

View File

@@ -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',

View File

@@ -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,
children: [
{
path: '',
component: CreateCommunityPageComponent, 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',

View File

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

View File

@@ -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,

View File

@@ -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,6 +29,16 @@ 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>[];
if (hasValue(route.data.breadcrumbQueryParam) && hasValue(route.queryParams[route.data.breadcrumbQueryParam])) {
return DSOBreadcrumbResolverByUuid(
route,
state,
route.queryParams[route.data.breadcrumbQueryParam],
breadcrumbService,
dataService,
...linksToFollow,
) as Observable<BreadcrumbConfig<Community>>;
} else {
return DSOBreadcrumbResolver( return DSOBreadcrumbResolver(
route, route,
state, state,
@@ -32,4 +46,5 @@ export const communityBreadcrumbResolver: ResolveFn<BreadcrumbConfig<Community>>
dataService, dataService,
...linksToFollow, ...linksToFollow,
) as Observable<BreadcrumbConfig<Community>>; ) as Observable<BreadcrumbConfig<Community>>;
}
}; };

View File

@@ -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),

View File

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

View File

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

View File

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

View File

@@ -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,

View File

@@ -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,

View File

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

View File

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

View File

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

View File

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

View File

@@ -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[]> =>

View File

@@ -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'),
];

View File

@@ -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>>),

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -1,12 +1,19 @@
<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>
</ds-themed-configuration-search-page>
<div *ngIf="!showDiscoverFilters" class="container">
<ng-container *ngTemplateOutlet="homeContent"></ng-container>
</div>
<ds-suggestions-popup></ds-suggestions-popup>
<ng-template #homeContent>
<ng-container *ngIf="(site$ | async) as site"> <ng-container *ngIf="(site$ | async) as site">
<ds-view-tracker [object]="site"></ds-view-tracker> <ds-view-tracker [object]="site"></ds-view-tracker>
</ng-container> </ng-container>
@@ -15,15 +22,4 @@
</ds-themed-search-form> </ds-themed-search-form>
<ds-themed-top-level-community-list></ds-themed-top-level-community-list> <ds-themed-top-level-community-list></ds-themed-top-level-community-list>
<ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list> <ds-recent-item-list *ngIf="recentSubmissionspageSize>0"></ds-recent-item-list>
</div>
</ds-page-with-sidebar>
</div>
<ds-suggestions-popup></ds-suggestions-popup>
<ng-template #sidebar>
<div *ngIf="showDiscoverFilters">
<ds-themed-configuration-search-page [sideBarWidth]="12" [showViewModes]="false" [searchEnabled]="false"
[inPlaceSearch]="false" [showScopeSelector]="false">
</ds-themed-configuration-search-page>
</div>
</ng-template> </ng-template>

View File

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

View File

@@ -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),
); );

View File

@@ -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"

View File

@@ -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">

View File

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

View File

@@ -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">

View File

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

View File

@@ -0,0 +1,9 @@
.left-column {
float: left;
text-align: left;
}
.right-column {
float: right;
text-align: right;
}

View File

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

View File

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

View File

@@ -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">

View File

@@ -1,9 +0,0 @@
.left-column {
float: left;
text-align: left;
}
.right-column {
float: right;
text-align: right;
}

View File

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

View File

@@ -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(),

View File

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

View File

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

View File

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

View File

@@ -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">

View File

@@ -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,

View File

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

View File

@@ -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"

View File

@@ -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"

View File

@@ -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"

View File

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

View File

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

View File

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

View File

@@ -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; const checkbox = fixture.debugElement.query(By.css('input.item-checkbox')).nativeElement;
expect(authorizationDataService.isAuthorized).toHaveBeenCalled(); expect(authorizationDataService.isAuthorized).toHaveBeenCalled();
expect(checkbox.disabled).toBeTrue(); expect(checkbox.disabled).toBeTrue();
});
})); }));
}); });
}); });

View File

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

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

View File

@@ -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 {

View File

@@ -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"

View File

@@ -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 {

View File

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

View File

@@ -0,0 +1,3 @@
.badge {
white-space: inherit;
}

View File

@@ -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],
}) })

View File

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

View File

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

View 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();
}
}
}

View File

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

View File

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

View File

@@ -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],

View File

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

View File

@@ -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],

View File

@@ -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",

View File

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

View File

@@ -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,

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

View File

@@ -6,5 +6,5 @@ export const StoreDevModules = [
StoreDevtoolsModule.instrument({ StoreDevtoolsModule.instrument({
maxAge: 1000, maxAge: 1000,
logOnly: false, logOnly: false,
}), connectInZone: true }),
]; ];

View File

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

View File

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

View File

@@ -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',

View File

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

View File

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

View File

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