Merge remote-tracking branch 'dspace/main' into accessibility-settings-main

This commit is contained in:
Andreas Awouters
2025-05-06 09:33:03 +02:00
231 changed files with 3525 additions and 3823 deletions

View File

@@ -12,7 +12,6 @@
"eslint-plugin-rxjs", "eslint-plugin-rxjs",
"eslint-plugin-simple-import-sort", "eslint-plugin-simple-import-sort",
"eslint-plugin-import-newlines", "eslint-plugin-import-newlines",
"eslint-plugin-jsonc",
"dspace-angular-ts", "dspace-angular-ts",
"dspace-angular-html" "dspace-angular-html"
], ],
@@ -303,10 +302,13 @@
"*.json5" "*.json5"
], ],
"extends": [ "extends": [
"plugin:jsonc/recommended-with-jsonc" "plugin:jsonc/recommended-with-json5"
], ],
"rules": { "rules": {
"no-irregular-whitespace": "error", // The ESLint core no-irregular-whitespace rule doesn't work well in JSON
// See: https://ota-meshi.github.io/eslint-plugin-jsonc/rules/no-irregular-whitespace.html
"no-irregular-whitespace": "off",
"jsonc/no-irregular-whitespace": "error",
"no-trailing-spaces": "error", "no-trailing-spaces": "error",
"jsonc/comma-dangle": [ "jsonc/comma-dangle": [
"error", "error",

View File

@@ -93,7 +93,10 @@ services:
volumes: volumes:
# Keep Solr data directory between reboots # Keep Solr data directory between reboots
- solr_data:/var/solr/data - solr_data:/var/solr/data
# Initialize all DSpace Solr cores using the mounted configsets (see above), then start Solr # NOTE: We are not running Solr as "root", but we need root permissions to copy our cores to the mounted
# /var/solr/data directory. Then we start Solr as the "solr" user.
user: root
# Initialize all DSpace Solr cores, then start Solr
entrypoint: entrypoint:
- /bin/bash - /bin/bash
- '-c' - '-c'
@@ -111,7 +114,8 @@ services:
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 precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
exec solr -f chown -R solr:solr /var/solr
runuser -u solr -- solr-foreground
volumes: volumes:
assetstore: assetstore:
pgdata: pgdata:

View File

@@ -97,11 +97,16 @@ services:
volumes: volumes:
# Keep Solr data directory between reboots # Keep Solr data directory between reboots
- solr_data:/var/solr/data - solr_data:/var/solr/data
# NOTE: We are not running Solr as "root", but we need root permissions to copy our cores to the mounted
# /var/solr/data directory. Then we start Solr as the "solr" user.
user: root
# Initialize all DSpace Solr cores using the mounted local configsets (see above), then start Solr # Initialize all DSpace Solr cores using the mounted local configsets (see above), then start Solr
# * First, run precreate-core to create the core (if it doesn't yet exist). If exists already, this is a no-op # * First, run precreate-core to create the core (if it doesn't yet exist). If exists already, this is a no-op
# * Second, copy configsets to this core: # * Second, copy configsets to this core:
# Updates to Solr configs require the container to be rebuilt/restarted: # Updates to Solr configs require the container to be rebuilt/restarted:
# `docker compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --build dspacesolr` # `docker compose -p d7 -f docker/docker-compose.yml -f docker/docker-compose-rest.yml up -d --build dspacesolr`
# * Third, ensure all new folders are owned by "solr" user
# * Finally, start Solr as the "solr" user via the provided solr-foreground script
entrypoint: entrypoint:
- /bin/bash - /bin/bash
- '-c' - '-c'
@@ -119,7 +124,8 @@ services:
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 precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
exec solr -f chown -R solr:solr /var/solr
runuser -u solr -- solr-foreground
volumes: volumes:
assetstore: assetstore:
pgdata: pgdata:

195
package-lock.json generated
View File

@@ -20,8 +20,8 @@
"@angular/platform-browser-dynamic": "^18.2.12", "@angular/platform-browser-dynamic": "^18.2.12",
"@angular/platform-server": "^18.2.12", "@angular/platform-server": "^18.2.12",
"@angular/router": "^18.2.12", "@angular/router": "^18.2.12",
"@angular/ssr": "^18.2.18", "@angular/ssr": "^18.2.19",
"@babel/runtime": "7.27.0", "@babel/runtime": "7.27.1",
"@kolkov/ngx-gallery": "^2.0.1", "@kolkov/ngx-gallery": "^2.0.1",
"@ng-bootstrap/ng-bootstrap": "^12.0.0", "@ng-bootstrap/ng-bootstrap": "^12.0.0",
"@ng-dynamic-forms/core": "^16.0.0", "@ng-dynamic-forms/core": "^16.0.0",
@@ -35,7 +35,7 @@
"@terraformer/wkt": "^2.2.1", "@terraformer/wkt": "^2.2.1",
"altcha": "^0.9.0", "altcha": "^0.9.0",
"angulartics2": "^12.2.0", "angulartics2": "^12.2.0",
"axios": "^1.8.4", "axios": "^1.9.0",
"bootstrap": "^5.3", "bootstrap": "^5.3",
"cerialize": "0.1.18", "cerialize": "0.1.18",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
@@ -53,7 +53,7 @@
"filesize": "^10.1.6", "filesize": "^10.1.6",
"http-proxy-middleware": "^2.0.9", "http-proxy-middleware": "^2.0.9",
"http-terminator": "^3.2.0", "http-terminator": "^3.2.0",
"isbot": "^5.1.26", "isbot": "^5.1.27",
"js-cookie": "2.2.1", "js-cookie": "2.2.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"json5": "^2.2.3", "json5": "^2.2.3",
@@ -86,7 +86,7 @@
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "~18.0.0", "@angular-builders/custom-webpack": "~18.0.0",
"@angular-devkit/build-angular": "^18.2.18", "@angular-devkit/build-angular": "^18.2.19",
"@angular-eslint/builder": "^18.4.1", "@angular-eslint/builder": "^18.4.1",
"@angular-eslint/bundled-angular-compiler": "^18.4.1", "@angular-eslint/bundled-angular-compiler": "^18.4.1",
"@angular-eslint/eslint-plugin": "^18.4.1", "@angular-eslint/eslint-plugin": "^18.4.1",
@@ -94,13 +94,13 @@
"@angular-eslint/schematics": "^18.4.1", "@angular-eslint/schematics": "^18.4.1",
"@angular-eslint/template-parser": "^18.4.1", "@angular-eslint/template-parser": "^18.4.1",
"@angular-eslint/utils": "^18.4.1", "@angular-eslint/utils": "^18.4.1",
"@angular/cli": "^18.2.18", "@angular/cli": "^18.2.19",
"@angular/compiler-cli": "^18.2.12", "@angular/compiler-cli": "^18.2.12",
"@angular/language-service": "^18.2.12", "@angular/language-service": "^18.2.12",
"@cypress/schematic": "^1.5.0", "@cypress/schematic": "^1.5.0",
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@ngrx/store-devtools": "^18.1.1", "@ngrx/store-devtools": "^18.1.1",
"@ngtools/webpack": "^18.2.18", "@ngtools/webpack": "^18.2.19",
"@types/deep-freeze": "0.1.5", "@types/deep-freeze": "0.1.5",
"@types/ejs": "^3.1.2", "@types/ejs": "^3.1.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
@@ -150,12 +150,12 @@
"postcss-loader": "^4.0.3", "postcss-loader": "^4.0.3",
"postcss-preset-env": "^7.4.2", "postcss-preset-env": "^7.4.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sass": "~1.86.3", "sass": "~1.87.0",
"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": "~5.4.5", "typescript": "~5.4.5",
"webpack": "5.99.5", "webpack": "5.99.7",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1" "webpack-dev-server": "^4.15.1"
} }
@@ -266,13 +266,13 @@
} }
}, },
"node_modules/@angular-devkit/architect": { "node_modules/@angular-devkit/architect": {
"version": "0.1802.18", "version": "0.1802.19",
"resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.18.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.19.tgz",
"integrity": "sha512-3OitvTddHp7bSqEGOJlH7Zqv07DdmZHktU2jsekjcbUxmoC1WIpWSYy+Bqyu7HjidJc0xVP7wyE/NPYkrwT5SA==", "integrity": "sha512-M4B1tzxGX1nWCZr9GMM8OO0yBJO2HFSdK8M8P74vEFQfKIeq3y16IQ5zlEveJrkCOFVtmlIy2C9foMCdNyBRMA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@angular-devkit/core": "18.2.18", "@angular-devkit/core": "18.2.19",
"rxjs": "7.8.1" "rxjs": "7.8.1"
}, },
"engines": { "engines": {
@@ -291,17 +291,17 @@
} }
}, },
"node_modules/@angular-devkit/build-angular": { "node_modules/@angular-devkit/build-angular": {
"version": "18.2.18", "version": "18.2.19",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.18.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.19.tgz",
"integrity": "sha512-yNw5b46BB27YW2lgP9pAt15xtfTS8F1JdWR79bLci0MYL7VPmRBrRtZk+sozRCziit1+oNAVpOUT8QyvDmvAZA==", "integrity": "sha512-xwY7v+nGE7TXOc4pgY6u57bLzIPSHuecosYr3TiWHAl9iEcKHzkCCFKsLZyunohHmq/i1uA6g3cC6iwp2xNYyg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ampproject/remapping": "2.3.0", "@ampproject/remapping": "2.3.0",
"@angular-devkit/architect": "0.1802.18", "@angular-devkit/architect": "0.1802.19",
"@angular-devkit/build-webpack": "0.1802.18", "@angular-devkit/build-webpack": "0.1802.19",
"@angular-devkit/core": "18.2.18", "@angular-devkit/core": "18.2.19",
"@angular/build": "18.2.18", "@angular/build": "18.2.19",
"@babel/core": "7.26.10", "@babel/core": "7.26.10",
"@babel/generator": "7.26.10", "@babel/generator": "7.26.10",
"@babel/helper-annotate-as-pure": "7.25.9", "@babel/helper-annotate-as-pure": "7.25.9",
@@ -312,7 +312,7 @@
"@babel/preset-env": "7.26.9", "@babel/preset-env": "7.26.9",
"@babel/runtime": "7.26.10", "@babel/runtime": "7.26.10",
"@discoveryjs/json-ext": "0.6.1", "@discoveryjs/json-ext": "0.6.1",
"@ngtools/webpack": "18.2.18", "@ngtools/webpack": "18.2.19",
"ansi-colors": "4.1.3", "ansi-colors": "4.1.3",
"autoprefixer": "10.4.20", "autoprefixer": "10.4.20",
"babel-loader": "9.1.3", "babel-loader": "9.1.3",
@@ -322,7 +322,7 @@
"css-loader": "7.1.2", "css-loader": "7.1.2",
"esbuild-wasm": "0.23.0", "esbuild-wasm": "0.23.0",
"fast-glob": "3.3.2", "fast-glob": "3.3.2",
"http-proxy-middleware": "3.0.3", "http-proxy-middleware": "3.0.5",
"https-proxy-agent": "7.0.5", "https-proxy-agent": "7.0.5",
"istanbul-lib-instrument": "6.0.3", "istanbul-lib-instrument": "6.0.3",
"jsonc-parser": "3.3.1", "jsonc-parser": "3.3.1",
@@ -418,13 +418,13 @@
} }
}, },
"node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/build-webpack": { "node_modules/@angular-devkit/build-angular/node_modules/@angular-devkit/build-webpack": {
"version": "0.1802.18", "version": "0.1802.19",
"resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.18.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.19.tgz",
"integrity": "sha512-xSiUC2EeELKgs70aceet/iK57y2nk6VobgeeQzGzTtE5HXWX0n5/g9FIOVM1rznv/tj+9VFZpQKCdLqiP7JmCQ==", "integrity": "sha512-axz1Sasn+c+GJpJexBL+B3Rh1w3wJrQq8k8gkniodjJ594p4ti2qGk7i9Tj8A4cXx5fGY+EpuZvKfI/9Tr7QwA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@angular-devkit/architect": "0.1802.18", "@angular-devkit/architect": "0.1802.19",
"rxjs": "7.8.1" "rxjs": "7.8.1"
}, },
"engines": { "engines": {
@@ -694,10 +694,11 @@
} }
}, },
"node_modules/@angular-devkit/build-angular/node_modules/http-proxy-middleware": { "node_modules/@angular-devkit/build-angular/node_modules/http-proxy-middleware": {
"version": "3.0.3", "version": "3.0.5",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.3.tgz", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-3.0.5.tgz",
"integrity": "sha512-usY0HG5nyDUwtqpiZdETNbmKtw3QQ1jwYFZ9wi5iHzX2BcILwQKtYDJPo7XHTsu5Z0B2Hj3W9NNnbd+AjFWjqg==", "integrity": "sha512-GLZZm1X38BPY4lkXA01jhwxvDoOkkXqjgVyUzVxiEK4iuRu03PZoYHhHRwxnfhQMDuaxi3vVri0YgSro/1oWqg==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@types/http-proxy": "^1.17.15", "@types/http-proxy": "^1.17.15",
"debug": "^4.3.6", "debug": "^4.3.6",
@@ -740,6 +741,7 @@
"resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz", "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-5.0.0.tgz",
"integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==", "integrity": "sha512-VRSzKkbMm5jMDoKLbltAkFQ5Qr7VDiTFGXxYFXXowVj387GeGNOCsOH6Msy00SGZ3Fp84b1Naa1psqgcCIEP5Q==",
"dev": true, "dev": true,
"license": "MIT",
"engines": { "engines": {
"node": ">=0.10.0" "node": ">=0.10.0"
} }
@@ -1083,9 +1085,9 @@
} }
}, },
"node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/http-proxy-middleware": { "node_modules/@angular-devkit/build-angular/node_modules/webpack-dev-server/node_modules/http-proxy-middleware": {
"version": "2.0.8", "version": "2.0.9",
"resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.8.tgz", "resolved": "https://registry.npmjs.org/http-proxy-middleware/-/http-proxy-middleware-2.0.9.tgz",
"integrity": "sha512-/iazaeFPmL8KLA6QB7DFAU4O5j+9y/TA0D019MbLtPuFI56VK4BXFzM6j6QS9oGpScy8IIDH4S2LHv3zg/63Bw==", "integrity": "sha512-c1IyJYLYppU574+YI7R4QyX2ystMtVXZwIdzazUIPIJsHuWNd+mho2j+bKoHftndicGj9yh+xjd+l0yj7VeT1Q==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1149,9 +1151,9 @@
} }
}, },
"node_modules/@angular-devkit/core": { "node_modules/@angular-devkit/core": {
"version": "18.2.18", "version": "18.2.19",
"resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.18.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.19.tgz",
"integrity": "sha512-gncn8QN73mi4in7oAfoWnJglLx5iI8d87796h1LTuAxULSkfzhW3E03NZU764FBiIAWFxuty4PWmrHxMlmbtbw==", "integrity": "sha512-Ptf92Zomc6FCr7GWmHKdgOUbA1GpctZwH/hRcpYpU3tM56MG2t5FOFpufnE595GgolOCktabkFEoODMG8PBVDQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1186,13 +1188,13 @@
} }
}, },
"node_modules/@angular-devkit/schematics": { "node_modules/@angular-devkit/schematics": {
"version": "18.2.18", "version": "18.2.19",
"resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.18.tgz", "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.19.tgz",
"integrity": "sha512-i7dy3x32Z8+lmVMKlKHdrSuCya5hUP24BOUn5lXKFAFGcJC0JT30OJrDPqQMA2RzNQiiyacPhxaCdLloEFVh3Q==", "integrity": "sha512-P/0KjkzOf2ZShuShx3cBbjLI7XlcS6B/yCRBo1MQfCC4cZfmzPQoUEOSQeYZgy5pnC24f+dKh/+TWc5uYL/Lvg==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@angular-devkit/core": "18.2.18", "@angular-devkit/core": "18.2.19",
"jsonc-parser": "3.3.1", "jsonc-parser": "3.3.1",
"magic-string": "0.30.11", "magic-string": "0.30.11",
"ora": "5.4.1", "ora": "5.4.1",
@@ -1324,14 +1326,14 @@
} }
}, },
"node_modules/@angular/build": { "node_modules/@angular/build": {
"version": "18.2.18", "version": "18.2.19",
"resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.18.tgz", "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.19.tgz",
"integrity": "sha512-8PEhrkS1t9xpvBLaLVgi0OWt/0B72ENKKVc6BAKEZ5gg+SD7uf47sJcT1d23r7d/V6FaOJnWim6BrqgFs4rW9A==", "integrity": "sha512-dTqR+mhcZWtCRyOafvzHNVpYxMQnt8HHHqNM0kyEMzcztXL2L9zDlKr0H9d+AgGGq/v4qwCh+1gFDxsHByZwMQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@ampproject/remapping": "2.3.0", "@ampproject/remapping": "2.3.0",
"@angular-devkit/architect": "0.1802.18", "@angular-devkit/architect": "0.1802.19",
"@babel/core": "7.25.2", "@babel/core": "7.25.2",
"@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-annotate-as-pure": "7.24.7",
"@babel/helper-split-export-declaration": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7",
@@ -1797,9 +1799,9 @@
} }
}, },
"node_modules/@angular/build/node_modules/@types/node": { "node_modules/@angular/build/node_modules/@types/node": {
"version": "22.14.0", "version": "22.15.3",
"resolved": "https://registry.npmjs.org/@types/node/-/node-22.14.0.tgz", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz",
"integrity": "sha512-Kmpl+z84ILoG+3T/zQFyAJsU6EPTmOCj8/2+83fSN6djd6I4o7uOuGIH6vq3PrjY5BGitSbFuMN18j3iknubbA==", "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"optional": true, "optional": true,
@@ -1856,9 +1858,9 @@
"peer": true "peer": true
}, },
"node_modules/@angular/build/node_modules/vite": { "node_modules/@angular/build/node_modules/vite": {
"version": "5.4.17", "version": "5.4.18",
"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.17.tgz", "resolved": "https://registry.npmjs.org/vite/-/vite-5.4.18.tgz",
"integrity": "sha512-5+VqZryDj4wgCs55o9Lp+p8GE78TLVg0lasCH5xFZ4jacZjtqZa6JUw9/p0WeAojaOfncSM6v77InkFPGnvPvg==", "integrity": "sha512-1oDcnEp3lVyHCuQ2YFelM4Alm2o91xNoMncRm1U7S+JdYfYOvbiGZ3/CxGttrOu2M/KcGz7cRC2DoNUA6urmMA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -1971,18 +1973,18 @@
} }
}, },
"node_modules/@angular/cli": { "node_modules/@angular/cli": {
"version": "18.2.18", "version": "18.2.19",
"resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.18.tgz", "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.19.tgz",
"integrity": "sha512-UwwI03FVvTHbb9kgR9D0HdLajxsVm1jYkcWMfbSMnQGYM1qy1EWj9HvGnfIoQxAEzA8aeQbmsn9+h3w6MQmyCg==", "integrity": "sha512-LGVMTc36JQuw8QX8Sclxyei306EQW3KslopXbf7cfqt6D5/fHS+FqqA0O7V8ob/vOGMca+l6hQD27nW5Y3W6pA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@angular-devkit/architect": "0.1802.18", "@angular-devkit/architect": "0.1802.19",
"@angular-devkit/core": "18.2.18", "@angular-devkit/core": "18.2.19",
"@angular-devkit/schematics": "18.2.18", "@angular-devkit/schematics": "18.2.19",
"@inquirer/prompts": "5.3.8", "@inquirer/prompts": "5.3.8",
"@listr2/prompt-adapter-inquirer": "2.0.15", "@listr2/prompt-adapter-inquirer": "2.0.15",
"@schematics/angular": "18.2.18", "@schematics/angular": "18.2.19",
"@yarnpkg/lockfile": "1.1.0", "@yarnpkg/lockfile": "1.1.0",
"ini": "4.1.3", "ini": "4.1.3",
"jsonc-parser": "3.3.1", "jsonc-parser": "3.3.1",
@@ -2230,9 +2232,9 @@
} }
}, },
"node_modules/@angular/ssr": { "node_modules/@angular/ssr": {
"version": "18.2.18", "version": "18.2.19",
"resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-18.2.18.tgz", "resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-18.2.19.tgz",
"integrity": "sha512-WJ56mpiRGp18vcSH4jFHWR6dylBtUk3QOz+RQhuqYFPAfKk2YXEH5BiBXcjNicW5tIxaU8NlHfZVwWHQyEjpiA==", "integrity": "sha512-kMNPWZiLGhtrXFwQpDn1laKXxwMpaiXVajpDT7m/yQkyKMH5EbyZASFcyDHK6EsRV2LQsPaXeKzeQof/C1zNcw==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"critters": "0.0.24", "critters": "0.0.24",
@@ -3783,13 +3785,10 @@
} }
}, },
"node_modules/@babel/runtime": { "node_modules/@babel/runtime": {
"version": "7.27.0", "version": "7.27.1",
"resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.0.tgz", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.27.1.tgz",
"integrity": "sha512-VtPOkrdPHZsKc/clNqyi9WUA8TINkZ4cGk63UUE3u4pmB2k+ZMQRDuIOagv8UVd6j7k0T3+RRIb7beKTebNbcw==", "integrity": "sha512-1x3D2xEk2fRo3PAhwQwu5UubzgiVWSXTBfWpVd2Mx2AzRqJuDJCsgaDVZ7HB5iGzDW1Hl1sWN2mFyKjmR9uAog==",
"license": "MIT", "license": "MIT",
"dependencies": {
"regenerator-runtime": "^0.14.0"
},
"engines": { "engines": {
"node": ">=6.9.0" "node": ">=6.9.0"
} }
@@ -5960,9 +5959,9 @@
} }
}, },
"node_modules/@ngtools/webpack": { "node_modules/@ngtools/webpack": {
"version": "18.2.18", "version": "18.2.19",
"resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.18.tgz", "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.19.tgz",
"integrity": "sha512-rFTf3zrAMp7KJF8F/sOn0SNits+HhRaNKw4g20Pxk4QG5XZsXChsQIKrrzAnmlCfMb3nQmBnElAhr1rvBmzZWQ==", "integrity": "sha512-bExj5JrByKPibsqBbn5Pjn8lo91AUOTsyP2hgKpnOnmSr62rhWSiRwXltgz2MCiZRmuUznpt93WiOLixgYfYvQ==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"engines": { "engines": {
@@ -6958,14 +6957,14 @@
"dev": true "dev": true
}, },
"node_modules/@schematics/angular": { "node_modules/@schematics/angular": {
"version": "18.2.18", "version": "18.2.19",
"resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.18.tgz", "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.19.tgz",
"integrity": "sha512-ko5KmtCZz8SqZLKrNeqMauS2LPHBKf7mT01waoOD1uN2gQkSIiLzDEYuXOaIarG6VnxAy5pL6NjkD+EmPsH6eg==", "integrity": "sha512-s9aynH/fwB/LT94miVfsaL2C4Qd5BLgjMzWFx7iJ8Hyv7FjOBGYO6eGVovjCt2c6/abG+GQAk4EBOCfg3AUtCA==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@angular-devkit/core": "18.2.18", "@angular-devkit/core": "18.2.19",
"@angular-devkit/schematics": "18.2.18", "@angular-devkit/schematics": "18.2.19",
"jsonc-parser": "3.3.1" "jsonc-parser": "3.3.1"
}, },
"engines": { "engines": {
@@ -8830,9 +8829,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.8.4", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.4.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-eBSYY4Y68NNlHbHBMdeDmKNtDgXWhQsJcGqzO3iLUM0GraQFSS9cVgPX5I9b3lbdFKyYoAEGAZF1DwhTaljNAw==", "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",
@@ -9109,9 +9108,9 @@
"integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==" "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww=="
}, },
"node_modules/bootstrap": { "node_modules/bootstrap": {
"version": "5.3.3", "version": "5.3.5",
"resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.3.tgz", "resolved": "https://registry.npmjs.org/bootstrap/-/bootstrap-5.3.5.tgz",
"integrity": "sha512-8HLCdWgyoMguSO9o+aH+iuZ+aht+mzW0u3HIMzVu7Srrpv7EBBxTnrFlSCskwdY1+EOFQSm7uMJhNQHkdPcmjg==", "integrity": "sha512-ct1CHKtiobRimyGzmsSldEtM03E8fcEX4Tb3dGXz1V8faRwM50+vfHwTzOxB3IlKO7m+9vTH3s/3C6T2EAPeTA==",
"funding": [ "funding": [
{ {
"type": "github", "type": "github",
@@ -9122,6 +9121,7 @@
"url": "https://opencollective.com/bootstrap" "url": "https://opencollective.com/bootstrap"
} }
], ],
"license": "MIT",
"peerDependencies": { "peerDependencies": {
"@popperjs/core": "^2.11.8" "@popperjs/core": "^2.11.8"
} }
@@ -11059,9 +11059,9 @@
} }
}, },
"node_modules/detect-libc": { "node_modules/detect-libc": {
"version": "2.0.3", "version": "2.0.4",
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz",
"integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==",
"dev": true, "dev": true,
"license": "Apache-2.0", "license": "Apache-2.0",
"engines": { "engines": {
@@ -14730,9 +14730,9 @@
} }
}, },
"node_modules/isbot": { "node_modules/isbot": {
"version": "5.1.26", "version": "5.1.27",
"resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.26.tgz", "resolved": "https://registry.npmjs.org/isbot/-/isbot-5.1.27.tgz",
"integrity": "sha512-3wqJEYSIm59dYQjEF7zJ7T42aqaqxbCyJQda5rKCudJykuAnISptCHR/GSGpOnw8UrvU+mGueNLRJS5HXnbsXQ==", "integrity": "sha512-V3W56Hnztt4Wdh3VUlAMbdNicX/tOM38eChW3a2ixP6KEBJAeehxzYzTD59JrU5NCTgBZwRt9lRWr8D7eMZVYQ==",
"license": "Unlicense", "license": "Unlicense",
"engines": { "engines": {
"node": ">=18" "node": ">=18"
@@ -20194,7 +20194,8 @@
"node_modules/regenerator-runtime": { "node_modules/regenerator-runtime": {
"version": "0.14.1", "version": "0.14.1",
"resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz",
"integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==" "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==",
"dev": true
}, },
"node_modules/regenerator-transform": { "node_modules/regenerator-transform": {
"version": "0.15.2", "version": "0.15.2",
@@ -20676,9 +20677,9 @@
"integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg=="
}, },
"node_modules/sass": { "node_modules/sass": {
"version": "1.86.3", "version": "1.87.0",
"resolved": "https://registry.npmjs.org/sass/-/sass-1.86.3.tgz", "resolved": "https://registry.npmjs.org/sass/-/sass-1.87.0.tgz",
"integrity": "sha512-iGtg8kus4GrsGLRDLRBRHY9dNVA78ZaS7xr01cWnS7PEMQyFtTqBiyCrfpTYTZXRWM94akzckYjh8oADfFNTzw==", "integrity": "sha512-d0NoFH4v6SjEK7BoX810Jsrhj7IQSYHAHLi/iSpgqKc7LaIDshFRlSg5LOymf9FqQhxEHs2W5ZQXlvy0KD45Uw==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
@@ -20806,10 +20807,11 @@
} }
}, },
"node_modules/schema-utils": { "node_modules/schema-utils": {
"version": "4.3.0", "version": "4.3.2",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.0.tgz", "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.2.tgz",
"integrity": "sha512-Gf9qqc58SpCA/xdziiHz35F4GNIWYWZrEshUc/G/r5BnLph6xpKuLeoJoQuj5WfBIx/eQLf+hmVPYHaxJu7V2g==", "integrity": "sha512-Gn/JaSk/Mt9gYubxTtSn/QCV4em9mpAPiR1rqy/Ocu19u/G9J5WWdNoUT4SiV6mFC3y6cxyFcFwdzPM3FgxGAQ==",
"dev": true, "dev": true,
"license": "MIT",
"dependencies": { "dependencies": {
"@types/json-schema": "^7.0.9", "@types/json-schema": "^7.0.9",
"ajv": "^8.9.0", "ajv": "^8.9.0",
@@ -22770,14 +22772,15 @@
"integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==" "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ=="
}, },
"node_modules/webpack": { "node_modules/webpack": {
"version": "5.99.5", "version": "5.99.7",
"resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.5.tgz", "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.99.7.tgz",
"integrity": "sha512-q+vHBa6H9qwBLUlHL4Y7L0L1/LlyBKZtS9FHNCQmtayxjI5RKC9yD8gpvLeqGv5lCQp1Re04yi0MF40pf30Pvg==", "integrity": "sha512-CNqKBRMQjwcmKR0idID5va1qlhrqVUKpovi+Ec79ksW8ux7iS1+A6VqzfZXgVYCFRKl7XL5ap3ZoMpwBJxcg0w==",
"dev": true, "dev": true,
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"@types/eslint-scope": "^3.7.7", "@types/eslint-scope": "^3.7.7",
"@types/estree": "^1.0.6", "@types/estree": "^1.0.6",
"@types/json-schema": "^7.0.15",
"@webassemblyjs/ast": "^1.14.1", "@webassemblyjs/ast": "^1.14.1",
"@webassemblyjs/wasm-edit": "^1.14.1", "@webassemblyjs/wasm-edit": "^1.14.1",
"@webassemblyjs/wasm-parser": "^1.14.1", "@webassemblyjs/wasm-parser": "^1.14.1",
@@ -22794,7 +22797,7 @@
"loader-runner": "^4.2.0", "loader-runner": "^4.2.0",
"mime-types": "^2.1.27", "mime-types": "^2.1.27",
"neo-async": "^2.6.2", "neo-async": "^2.6.2",
"schema-utils": "^4.3.0", "schema-utils": "^4.3.2",
"tapable": "^2.1.1", "tapable": "^2.1.1",
"terser-webpack-plugin": "^5.3.11", "terser-webpack-plugin": "^5.3.11",
"watchpack": "^2.4.1", "watchpack": "^2.4.1",

View File

@@ -102,8 +102,8 @@
"@angular/platform-browser-dynamic": "^18.2.12", "@angular/platform-browser-dynamic": "^18.2.12",
"@angular/platform-server": "^18.2.12", "@angular/platform-server": "^18.2.12",
"@angular/router": "^18.2.12", "@angular/router": "^18.2.12",
"@angular/ssr": "^18.2.18", "@angular/ssr": "^18.2.19",
"@babel/runtime": "7.27.0", "@babel/runtime": "7.27.1",
"@kolkov/ngx-gallery": "^2.0.1", "@kolkov/ngx-gallery": "^2.0.1",
"@ng-bootstrap/ng-bootstrap": "^12.0.0", "@ng-bootstrap/ng-bootstrap": "^12.0.0",
"@ng-dynamic-forms/core": "^16.0.0", "@ng-dynamic-forms/core": "^16.0.0",
@@ -117,7 +117,7 @@
"@terraformer/wkt": "^2.2.1", "@terraformer/wkt": "^2.2.1",
"altcha": "^0.9.0", "altcha": "^0.9.0",
"angulartics2": "^12.2.0", "angulartics2": "^12.2.0",
"axios": "^1.8.4", "axios": "^1.9.0",
"bootstrap": "^5.3", "bootstrap": "^5.3",
"cerialize": "0.1.18", "cerialize": "0.1.18",
"cli-progress": "^3.12.0", "cli-progress": "^3.12.0",
@@ -135,7 +135,7 @@
"filesize": "^10.1.6", "filesize": "^10.1.6",
"http-proxy-middleware": "^2.0.9", "http-proxy-middleware": "^2.0.9",
"http-terminator": "^3.2.0", "http-terminator": "^3.2.0",
"isbot": "^5.1.26", "isbot": "^5.1.27",
"js-cookie": "2.2.1", "js-cookie": "2.2.1",
"js-yaml": "^4.1.0", "js-yaml": "^4.1.0",
"json5": "^2.2.3", "json5": "^2.2.3",
@@ -168,7 +168,7 @@
}, },
"devDependencies": { "devDependencies": {
"@angular-builders/custom-webpack": "~18.0.0", "@angular-builders/custom-webpack": "~18.0.0",
"@angular-devkit/build-angular": "^18.2.18", "@angular-devkit/build-angular": "^18.2.19",
"@angular-eslint/builder": "^18.4.1", "@angular-eslint/builder": "^18.4.1",
"@angular-eslint/bundled-angular-compiler": "^18.4.1", "@angular-eslint/bundled-angular-compiler": "^18.4.1",
"@angular-eslint/eslint-plugin": "^18.4.1", "@angular-eslint/eslint-plugin": "^18.4.1",
@@ -176,13 +176,13 @@
"@angular-eslint/schematics": "^18.4.1", "@angular-eslint/schematics": "^18.4.1",
"@angular-eslint/template-parser": "^18.4.1", "@angular-eslint/template-parser": "^18.4.1",
"@angular-eslint/utils": "^18.4.1", "@angular-eslint/utils": "^18.4.1",
"@angular/cli": "^18.2.18", "@angular/cli": "^18.2.19",
"@angular/compiler-cli": "^18.2.12", "@angular/compiler-cli": "^18.2.12",
"@angular/language-service": "^18.2.12", "@angular/language-service": "^18.2.12",
"@cypress/schematic": "^1.5.0", "@cypress/schematic": "^1.5.0",
"@fortawesome/fontawesome-free": "^6.7.2", "@fortawesome/fontawesome-free": "^6.7.2",
"@ngrx/store-devtools": "^18.1.1", "@ngrx/store-devtools": "^18.1.1",
"@ngtools/webpack": "^18.2.18", "@ngtools/webpack": "^18.2.19",
"@types/deep-freeze": "0.1.5", "@types/deep-freeze": "0.1.5",
"@types/ejs": "^3.1.2", "@types/ejs": "^3.1.2",
"@types/express": "^4.17.17", "@types/express": "^4.17.17",
@@ -232,12 +232,12 @@
"postcss-loader": "^4.0.3", "postcss-loader": "^4.0.3",
"postcss-preset-env": "^7.4.2", "postcss-preset-env": "^7.4.2",
"rimraf": "^3.0.2", "rimraf": "^3.0.2",
"sass": "~1.86.3", "sass": "~1.87.0",
"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": "~5.4.5", "typescript": "~5.4.5",
"webpack": "5.99.5", "webpack": "5.99.7",
"webpack-cli": "^5.1.4", "webpack-cli": "^5.1.4",
"webpack-dev-server": "^4.15.1" "webpack-dev-server": "^4.15.1"
} }

View File

@@ -30,7 +30,7 @@
</div> </div>
} }
@if (canImpersonate$ | async) { @if (canImpersonate$ | async) {
<div between class="btn-group ms-1"> <div between class="btn-group">
@if (!isImpersonated) { @if (!isImpersonated) {
<button class="btn btn-primary" type="button" (click)="impersonate()"> <button class="btn btn-primary" type="button" (click)="impersonate()">
<i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}} <i class="fa fa-user-secret"></i> {{'admin.access-control.epeople.actions.impersonate' | translate}}

View File

@@ -1,7 +1,4 @@
import { import { AsyncPipe } from '@angular/common';
AsyncPipe,
NgClass,
} from '@angular/common';
import { import {
ChangeDetectorRef, ChangeDetectorRef,
Component, Component,
@@ -84,7 +81,6 @@ import { ValidateEmailNotTaken } from './validators/email-taken.validator';
FormComponent, FormComponent,
AsyncPipe, AsyncPipe,
TranslateModule, TranslateModule,
NgClass,
ThemedLoadingComponent, ThemedLoadingComponent,
PaginationComponent, PaginationComponent,
RouterLink, RouterLink,

View File

@@ -1,4 +1,4 @@
@if (registryService.getActiveMetadataField() | async) { @if (activeMetadataField$ | async) {
<h2>{{messagePrefix + '.edit' | translate}}</h2> <h2>{{messagePrefix + '.edit' | translate}}</h2>
} @else { } @else {
<h2>{{messagePrefix + '.create' | translate}}</h2> <h2>{{messagePrefix + '.create' | translate}}</h2>

View File

@@ -19,7 +19,7 @@ import {
TranslateModule, TranslateModule,
TranslateService, TranslateService,
} from '@ngx-translate/core'; } from '@ngx-translate/core';
import { combineLatest } from 'rxjs'; import { Observable } from 'rxjs';
import { take } from 'rxjs/operators'; import { take } from 'rxjs/operators';
import { MetadataField } from '../../../../core/metadata/metadata-field.model'; import { MetadataField } from '../../../../core/metadata/metadata-field.model';
@@ -109,6 +109,8 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
*/ */
@Output() submitForm: EventEmitter<any> = new EventEmitter(); @Output() submitForm: EventEmitter<any> = new EventEmitter();
activeMetadataField$: Observable<MetadataField>;
constructor(public registryService: RegistryService, constructor(public registryService: RegistryService,
private formBuilderService: FormBuilderService, private formBuilderService: FormBuilderService,
private translateService: TranslateService) { private translateService: TranslateService) {
@@ -117,15 +119,11 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
/** /**
* Initialize the component, setting up the necessary Models for the dynamic form * Initialize the component, setting up the necessary Models for the dynamic form
*/ */
ngOnInit() { ngOnInit(): void {
combineLatest([ this.activeMetadataField$ = this.registryService.getActiveMetadataField();
this.translateService.get(`${this.messagePrefix}.element`),
this.translateService.get(`${this.messagePrefix}.qualifier`),
this.translateService.get(`${this.messagePrefix}.scopenote`),
]).subscribe(([element, qualifier, scopenote]) => {
this.element = new DynamicInputModel({ this.element = new DynamicInputModel({
id: 'element', id: 'element',
label: element, label: this.translateService.instant(`${this.messagePrefix}.element`),
name: 'element', name: 'element',
validators: { validators: {
required: null, required: null,
@@ -140,7 +138,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
}); });
this.qualifier = new DynamicInputModel({ this.qualifier = new DynamicInputModel({
id: 'qualifier', id: 'qualifier',
label: qualifier, label: this.translateService.instant(`${this.messagePrefix}.qualifier`),
name: 'qualifier', name: 'qualifier',
validators: { validators: {
pattern: '^[^. ,]*$', pattern: '^[^. ,]*$',
@@ -154,14 +152,13 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
}); });
this.scopeNote = new DynamicTextAreaModel({ this.scopeNote = new DynamicTextAreaModel({
id: 'scopeNote', id: 'scopeNote',
label: scopenote, label: this.translateService.instant(`${this.messagePrefix}.scopenote`),
name: 'scopeNote', name: 'scopeNote',
required: false, required: false,
rows: 5, rows: 5,
}); });
this.formModel = [ this.formModel = [
new DynamicFormGroupModel( new DynamicFormGroupModel({
{
id: 'metadatadatafieldgroup', id: 'metadatadatafieldgroup',
group:[this.element, this.qualifier, this.scopeNote], group:[this.element, this.qualifier, this.scopeNote],
}), }),
@@ -182,7 +179,6 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
this.qualifier.disabled = true; this.qualifier.disabled = true;
} }
}); });
});
} }
/** /**

View File

@@ -33,12 +33,14 @@ import { reloadGuard } from './core/reload/reload.guard';
import { forgotPasswordCheckGuard } from './core/rest-property/forgot-password-check-guard.guard'; import { forgotPasswordCheckGuard } from './core/rest-property/forgot-password-check-guard.guard';
import { ServerCheckGuard } from './core/server-check/server-check.guard'; import { ServerCheckGuard } from './core/server-check/server-check.guard';
import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component'; import { ThemedForbiddenComponent } from './forbidden/themed-forbidden.component';
import { homePageResolver } from './home-page/home-page.resolver';
import { ITEM_MODULE_PATH } from './item-page/item-page-routing-paths'; import { ITEM_MODULE_PATH } from './item-page/item-page-routing-paths';
import { provideSuggestionNotificationsState } from './notifications/provide-suggestion-notifications-state'; import { provideSuggestionNotificationsState } from './notifications/provide-suggestion-notifications-state';
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component'; import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component'; import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component'; import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
import { PROCESS_MODULE_PATH } from './process-page/process-page-routing.paths'; import { PROCESS_MODULE_PATH } from './process-page/process-page-routing.paths';
import { viewTrackerResolver } from './statistics/angulartics/dspace/view-tracker.resolver';
import { provideSubmissionState } from './submission/provide-submission-state'; import { provideSubmissionState } from './submission/provide-submission-state';
import { SUGGESTION_MODULE_PATH } from './suggestions-page/suggestions-page-routing-paths'; import { SUGGESTION_MODULE_PATH } from './suggestions-page/suggestions-page-routing-paths';
@@ -61,9 +63,17 @@ export const APP_ROUTES: Route[] = [
path: 'home', path: 'home',
loadChildren: () => import('./home-page/home-page-routes') loadChildren: () => import('./home-page/home-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
data: { showBreadcrumbs: false, enableRSS: true }, data: {
showBreadcrumbs: false,
enableRSS: true,
dsoPath: 'site',
},
providers: [provideSuggestionNotificationsState()], providers: [provideSuggestionNotificationsState()],
canActivate: [endUserAgreementCurrentUserGuard], canActivate: [endUserAgreementCurrentUserGuard],
resolve: {
site: homePageResolver,
tracking: viewTrackerResolver,
},
}, },
{ {
path: 'community-list', path: 'community-list',
@@ -99,14 +109,12 @@ export const APP_ROUTES: Route[] = [
path: COMMUNITY_MODULE_PATH, path: COMMUNITY_MODULE_PATH,
loadChildren: () => import('./community-page/community-page-routes') loadChildren: () => import('./community-page/community-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
data: { enableRSS: true },
canActivate: [endUserAgreementCurrentUserGuard], canActivate: [endUserAgreementCurrentUserGuard],
}, },
{ {
path: COLLECTION_MODULE_PATH, path: COLLECTION_MODULE_PATH,
loadChildren: () => import('./collection-page/collection-page-routes') loadChildren: () => import('./collection-page/collection-page-routes')
.then((m) => m.ROUTES), .then((m) => m.ROUTES),
data: { enableRSS: true },
canActivate: [endUserAgreementCurrentUserGuard], canActivate: [endUserAgreementCurrentUserGuard],
}, },
{ {

View File

@@ -12,7 +12,7 @@
} }
<ng-template #breadcrumb let-text="text" let-url="url"> <ng-template #breadcrumb let-text="text" let-url="url">
<li class="breadcrumb-item"><div class="breadcrumb-item-limiter"><a [routerLink]="url" class="text-truncate" [ngbTooltip]="text | translate" placement="bottom" >{{text | translate}}</a></div></li> <li class="breadcrumb-item"><div class="breadcrumb-item-limiter"><a [routerLink]="url" class="text-truncate" [ngbTooltip]="text | translate" placement="bottom" role="link" tabindex="0">{{text | translate}}</a></div></li>
</ng-template> </ng-template>
<ng-template #activeBreadcrumb let-text="text"> <ng-template #activeBreadcrumb let-text="text">

View File

@@ -20,6 +20,8 @@
<a class="btn btn-primary" <a class="btn btn-primary"
[routerLink]="['/search']" [routerLink]="['/search']"
[queryParams]="queryParams" [queryParams]="queryParams"
[queryParamsHandling]="'merge'"> [queryParamsHandling]="'merge'"
role="link"
tabindex="0">
{{ 'browse.taxonomy.button' | translate }}</a> {{ 'browse.taxonomy.button' | translate }}</a>
</section> </section>

View File

@@ -9,6 +9,7 @@ import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
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';
import { MenuRoute } from '../shared/menu/menu-route.model'; import { MenuRoute } from '../shared/menu/menu-route.model';
import { viewTrackerResolver } from '../statistics/angulartics/dspace/view-tracker.resolver';
import { collectionPageResolver } from './collection-page.resolver'; import { collectionPageResolver } from './collection-page.resolver';
import { collectionPageAdministratorGuard } from './collection-page-administrator.guard'; import { collectionPageAdministratorGuard } from './collection-page-administrator.guard';
import { import {
@@ -99,6 +100,7 @@ export const ROUTES: Route[] = [
data: { data: {
breadcrumbKey: 'collection.search', breadcrumbKey: 'collection.search',
menuRoute: MenuRoute.COLLECTION_PAGE, menuRoute: MenuRoute.COLLECTION_PAGE,
enableRSS: true,
}, },
}, },
{ {
@@ -115,6 +117,9 @@ export const ROUTES: Route[] = [
}, },
}, },
], ],
resolve: {
tracking: viewTrackerResolver,
},
}, },
], ],
}, },

View File

@@ -5,7 +5,6 @@
<div @fadeInOut> <div @fadeInOut>
@if (collectionRD?.payload; as collection) { @if (collectionRD?.payload; as collection) {
<div> <div>
<ds-view-tracker [object]="collection"></ds-view-tracker>
<div class="d-flex flex-row border-bottom mb-4 pb-4"> <div class="d-flex flex-row border-bottom mb-4 pb-4">
<header class="comcol-header me-auto"> <header class="comcol-header me-auto">
<!-- Collection Name --> <!-- Collection Name -->

View File

@@ -47,7 +47,6 @@ import { ThemedLoadingComponent } from '../shared/loading/themed-loading.compone
import { ObjectCollectionComponent } from '../shared/object-collection/object-collection.component'; import { ObjectCollectionComponent } from '../shared/object-collection/object-collection.component';
import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { VarDirective } from '../shared/utils/var.directive'; import { VarDirective } from '../shared/utils/var.directive';
import { ViewTrackerComponent } from '../statistics/angulartics/dspace/view-tracker.component';
import { getCollectionPageRoute } from './collection-page-routing-paths'; import { getCollectionPageRoute } from './collection-page-routing-paths';
@Component({ @Component({
@@ -64,7 +63,6 @@ import { getCollectionPageRoute } from './collection-page-routing-paths';
ErrorComponent, ErrorComponent,
ThemedLoadingComponent, ThemedLoadingComponent,
TranslateModule, TranslateModule,
ViewTrackerComponent,
VarDirective, VarDirective,
AsyncPipe, AsyncPipe,
ComcolPageHeaderComponent, ComcolPageHeaderComponent,

View File

@@ -12,7 +12,7 @@
<div class="align-middle my-auto"> <div class="align-middle my-auto">
@if ((dataSource.loading$ | async) !== true) { @if ((dataSource.loading$ | async) !== true) {
<button (click)="getNextPage(node)" <button (click)="getNextPage(node)"
class="btn btn-outline-primary btn-sm" role="button"> class="btn btn-outline-primary btn-sm" role="button" tabindex="0">
<i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }} <i class="fas fa-angle-down"></i> {{ 'communityList.showMore' | translate }}
</button> </button>
} }
@@ -34,7 +34,11 @@
<button type="button" class="btn btn-default btn-transparent" cdkTreeNodeToggle <button type="button" class="btn btn-default btn-transparent" cdkTreeNodeToggle
[attr.aria-label]="(node.isExpanded ? 'communityList.collapse' : 'communityList.expand') | translate:{ name: dsoNameService.getName(node.payload) }" [attr.aria-label]="(node.isExpanded ? 'communityList.collapse' : 'communityList.expand') | translate:{ name: dsoNameService.getName(node.payload) }"
(click)="toggleExpanded(node)" (click)="toggleExpanded(node)"
data-test="expand-button"> (keyup.enter)="toggleExpanded(node)"
(keyup.space)="toggleExpanded(node)"
data-test="expand-button"
role="button"
tabindex="0">
<span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}" <span class="{{node.isExpanded ? 'fa fa-chevron-down' : 'fa fa-chevron-right'}}"
aria-hidden="true"></span> aria-hidden="true"></span>
<span class="sr-only">{{ (node.isExpanded ? 'communityList.collapse' : 'communityList.expand') | translate:{ name: dsoNameService.getName(node.payload) } }}</span> <span class="sr-only">{{ (node.isExpanded ? 'communityList.collapse' : 'communityList.expand') | translate:{ name: dsoNameService.getName(node.payload) } }}</span>
@@ -48,7 +52,7 @@
} }
<div class="d-flex flex-row"> <div class="d-flex flex-row">
<span class="d-flex align-middle my-auto"> <span class="d-flex align-middle my-auto">
<a [routerLink]="node.route" class="lead">{{ dsoNameService.getName(node.payload) }}</a> <a [routerLink]="node.route" class="lead" role="link" tabindex="0">{{ dsoNameService.getName(node.payload) }}</a>
<span class="pe-2">&nbsp;</span> <span class="pe-2">&nbsp;</span>
@if (node.payload.archivedItemsCount >= 0) { @if (node.payload.archivedItemsCount >= 0) {
<span class="badge rounded-pill bg-secondary align-top archived-items-lead my-auto ps-2 pe-2">{{node.payload.archivedItemsCount}}</span> <span class="badge rounded-pill bg-secondary align-top archived-items-lead my-auto ps-2 pe-2">{{node.payload.archivedItemsCount}}</span>
@@ -88,7 +92,7 @@
<span class="fa fa-chevron-right"></span> <span class="fa fa-chevron-right"></span>
</span> </span>
<h6 class="align-middle my-auto"> <h6 class="align-middle my-auto">
<a [routerLink]="node.route" class="lead">{{ dsoNameService.getName(node.payload) }}</a> <a [routerLink]="node.route" class="lead" role="link" tabindex="0">{{ dsoNameService.getName(node.payload) }}</a>
</h6> </h6>
</div> </div>
<ds-truncatable [id]="node.id"> <ds-truncatable [id]="node.id">

View File

@@ -8,6 +8,7 @@ import { i18nBreadcrumbResolver } from '../core/breadcrumbs/i18n-breadcrumb.reso
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';
import { MenuRoute } from '../shared/menu/menu-route.model'; import { MenuRoute } from '../shared/menu/menu-route.model';
import { viewTrackerResolver } from '../statistics/angulartics/dspace/view-tracker.resolver';
import { communityPageResolver } from './community-page.resolver'; import { communityPageResolver } from './community-page.resolver';
import { communityPageAdministratorGuard } from './community-page-administrator.guard'; import { communityPageAdministratorGuard } from './community-page-administrator.guard';
import { import {
@@ -70,6 +71,9 @@ export const ROUTES: Route[] = [
data: { data: {
menuRoute: MenuRoute.COMMUNITY_PAGE, menuRoute: MenuRoute.COMMUNITY_PAGE,
}, },
resolve: {
tracking: viewTrackerResolver,
},
children: [ children: [
{ {
path: '', path: '',
@@ -86,6 +90,7 @@ export const ROUTES: Route[] = [
data: { data: {
breadcrumbKey: 'community.search', breadcrumbKey: 'community.search',
menuRoute: MenuRoute.COMMUNITY_PAGE, menuRoute: MenuRoute.COMMUNITY_PAGE,
enableRSS: true,
}, },
}, },
{ {

View File

@@ -3,7 +3,6 @@
<div class="community-page" @fadeInOut> <div class="community-page" @fadeInOut>
@if (communityRD?.payload; as communityPayload) { @if (communityRD?.payload; as communityPayload) {
<div> <div>
<ds-view-tracker [object]="communityPayload"></ds-view-tracker>
<div class="d-flex flex-row border-bottom mb-4 pb-4"> <div class="d-flex flex-row border-bottom mb-4 pb-4">
<header class="comcol-header me-auto"> <header class="comcol-header me-auto">
<!-- Community name --> <!-- Community name -->

View File

@@ -38,7 +38,6 @@ import { hasValue } from '../shared/empty.util';
import { ErrorComponent } from '../shared/error/error.component'; import { ErrorComponent } from '../shared/error/error.component';
import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component';
import { VarDirective } from '../shared/utils/var.directive'; import { VarDirective } from '../shared/utils/var.directive';
import { ViewTrackerComponent } from '../statistics/angulartics/dspace/view-tracker.component';
import { getCommunityPageRoute } from './community-page-routing-paths'; import { getCommunityPageRoute } from './community-page-routing-paths';
import { ThemedCollectionPageSubCollectionListComponent } from './sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component'; import { ThemedCollectionPageSubCollectionListComponent } from './sections/sub-com-col-section/sub-collection-list/themed-community-page-sub-collection-list.component';
import { ThemedCommunityPageSubCommunityListComponent } from './sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component'; import { ThemedCommunityPageSubCommunityListComponent } from './sections/sub-com-col-section/sub-community-list/themed-community-page-sub-community-list.component';
@@ -62,7 +61,6 @@ import { ThemedCommunityPageSubCommunityListComponent } from './sections/sub-com
ComcolPageLogoComponent, ComcolPageLogoComponent,
ComcolPageHeaderComponent, ComcolPageHeaderComponent,
AsyncPipe, AsyncPipe,
ViewTrackerComponent,
VarDirective, VarDirective,
RouterOutlet, RouterOutlet,
RouterModule, RouterModule,

View File

@@ -37,6 +37,8 @@ import {
} from './request.models'; } from './request.models';
import { RequestService } from './request.service'; import { RequestService } from './request.service';
import objectContaining = jasmine.objectContaining; import objectContaining = jasmine.objectContaining;
import { RestResponse } from '../cache/response.models';
import { RequestEntry } from './request-entry.model';
describe('BitstreamDataService', () => { describe('BitstreamDataService', () => {
let service: BitstreamDataService; let service: BitstreamDataService;
@@ -47,6 +49,7 @@ describe('BitstreamDataService', () => {
let rdbService: RemoteDataBuildService; let rdbService: RemoteDataBuildService;
let bundleDataService: BundleDataService; let bundleDataService: BundleDataService;
const bitstreamFormatHref = 'rest-api/bitstreamformats'; const bitstreamFormatHref = 'rest-api/bitstreamformats';
let responseCacheEntry: RequestEntry;
const bitstream1 = Object.assign(new Bitstream(), { const bitstream1 = Object.assign(new Bitstream(), {
id: 'fake-bitstream1', id: 'fake-bitstream1',
@@ -71,8 +74,13 @@ describe('BitstreamDataService', () => {
const url = 'fake-bitstream-url'; const url = 'fake-bitstream-url';
beforeEach(() => { beforeEach(() => {
responseCacheEntry = new RequestEntry();
responseCacheEntry.request = { href: 'https://rest.api/' } as any;
responseCacheEntry.response = new RestResponse(true, 200, 'Success');
objectCache = jasmine.createSpyObj('objectCache', { objectCache = jasmine.createSpyObj('objectCache', {
remove: jasmine.createSpy('remove'), remove: jasmine.createSpy('remove'),
getByHref: observableOf(responseCacheEntry),
}); });
requestService = getMockRequestService(); requestService = getMockRequestService();
halService = Object.assign(new HALEndpointServiceStub(url)); halService = Object.assign(new HALEndpointServiceStub(url));

View File

@@ -163,12 +163,25 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
sendRequest(this.requestService), sendRequest(this.requestService),
take(1), take(1),
).subscribe(() => { ).subscribe(() => {
this.requestService.removeByHrefSubstring(bitstream.self + '/format'); this.deleteFormatCache(bitstream);
}); });
return this.rdbService.buildFromRequestUUID(requestId); return this.rdbService.buildFromRequestUUID(requestId);
} }
private deleteFormatCache(bitstream: Bitstream) {
const bitsreamFormatUrl = bitstream.self + '/format';
this.requestService.setStaleByHrefSubstring(bitsreamFormatUrl);
// Delete also cache by uuid as the format could be cached also there
this.objectCache.getByHref(bitsreamFormatUrl).pipe(take(1)).subscribe((cachedRequest) => {
if (cachedRequest.requestUUIDs && cachedRequest.requestUUIDs.length > 0){
const requestUuid = cachedRequest.requestUUIDs[0];
if (this.requestService.hasByUUID(requestUuid)) {
this.requestService.setStaleByUUID(requestUuid);
}
}
});
}
/** /**
* Returns an observable of {@link RemoteData} of a {@link Bitstream}, based on a handle and an * Returns an observable of {@link RemoteData} of a {@link Bitstream}, based on a handle and an
* optional sequenceId or filename, with a list of {@link FollowLinkConfig}, to automatically * optional sequenceId or filename, with a list of {@link FollowLinkConfig}, to automatically

View File

@@ -31,6 +31,7 @@ import {
import { import {
EPersonMock, EPersonMock,
EPersonMock2, EPersonMock2,
EPersonMockWithNoName,
} from '../../shared/testing/eperson.mock'; } from '../../shared/testing/eperson.mock';
import { GroupMock } from '../../shared/testing/group-mock'; import { GroupMock } from '../../shared/testing/group-mock';
import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub'; import { HALEndpointServiceStub } from '../../shared/testing/hal-endpoint-service.stub';
@@ -281,6 +282,37 @@ describe('EPersonDataService', () => {
}); });
}); });
describe('updateEPerson with non existing metadata', () => {
beforeEach(() => {
spyOn(service, 'findByHref').and.returnValue(createSuccessfulRemoteDataObject$(EPersonMockWithNoName));
});
describe('add name that was not previously set', () => {
beforeEach(() => {
const changedEPerson = Object.assign(new EPerson(), {
id: EPersonMock.id,
metadata: Object.assign(EPersonMock.metadata, {
'eperson.firstname': [
{
language: null,
value: 'User',
},
],
}),
email: EPersonMock.email,
canLogIn: EPersonMock.canLogIn,
requireCertificate: EPersonMock.requireCertificate,
_links: EPersonMock._links,
});
service.updateEPerson(changedEPerson).subscribe();
});
it('should send PatchRequest with add email operation', () => {
const operations = [{ op: 'add', path: '/eperson.firstname', value: [{ language: null, value: 'User' }] }];
const expected = new PatchRequest(requestService.generateRequestId(), epersonsEndpoint + '/' + EPersonMock.uuid, operations);
expect(requestService.send).toHaveBeenCalledWith(expected);
});
});
});
describe('clearEPersonRequests', () => { describe('clearEPersonRequests', () => {
beforeEach(() => { beforeEach(() => {
spyOn(halService, 'getEndpoint').and.callFake((linkPath: string) => { spyOn(halService, 'getEndpoint').and.callFake((linkPath: string) => {

View File

@@ -269,7 +269,8 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
* @param newEPerson * @param newEPerson
*/ */
private generateOperations(oldEPerson: EPerson, newEPerson: EPerson): Operation[] { private generateOperations(oldEPerson: EPerson, newEPerson: EPerson): Operation[] {
let operations = this.comparator.diff(oldEPerson, newEPerson).filter((operation: Operation) => operation.op === 'replace'); let operations = this.comparator.diff(oldEPerson, newEPerson)
.filter((operation: Operation) => ['replace', 'add'].includes(operation.op));
if (hasValue(oldEPerson.email) && oldEPerson.email !== newEPerson.email) { if (hasValue(oldEPerson.email) && oldEPerson.email !== newEPerson.email) {
operations = [...operations, { operations = [...operations, {
op: 'replace', path: '/email', value: newEPerson.email, op: 'replace', path: '/email', value: newEPerson.email,

View File

@@ -1,34 +1,26 @@
import { CommonModule } from '@angular/common'; import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing'; import { TestBed } from '@angular/core/testing';
import { import { RouterModule } from '@angular/router';
Router,
UrlTree,
} from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { Angulartics2 } from 'angulartics2'; import { Angulartics2 } from 'angulartics2';
import { import { of as observableOf } from 'rxjs';
combineLatest as observableCombineLatest, import { TestScheduler } from 'rxjs/testing';
Observable,
of as observableOf,
} from 'rxjs';
import { map } from 'rxjs/operators';
import { environment } from '../../../../environments/environment.test';
import { getMockRemoteDataBuildService } from '../../../shared/mocks/remote-data-build.service.mock';
import { getMockRequestService } from '../../../shared/mocks/request.service.mock'; import { getMockRequestService } from '../../../shared/mocks/request.service.mock';
import { createSuccessfulRemoteDataObject$ } from '../../../shared/remote-data.utils'; import { FacetValues } from '../../../shared/search/models/facet-values.model';
import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model'; import { PaginatedSearchOptions } from '../../../shared/search/models/paginated-search-options.model';
import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model'; import { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.model';
import { SearchObjects } from '../../../shared/search/models/search-objects.model'; import { SearchObjects } from '../../../shared/search/models/search-objects.model';
import { ActivatedRouteStub } from '../../../shared/testing/active-router.stub'; import { HALEndpointServiceStub } from '../../../shared/testing/hal-endpoint-service.stub';
import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub'; import { PaginationServiceStub } from '../../../shared/testing/pagination-service.stub';
import { routeServiceStub } from '../../../shared/testing/route-service.stub'; import { routeServiceStub } from '../../../shared/testing/route-service.stub';
import { RouterStub } from '../../../shared/testing/router.stub'; import { SearchConfigurationServiceStub } from '../../../shared/testing/search-configuration-service.stub';
import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service'; import { RemoteDataBuildService } from '../../cache/builders/remote-data-build.service';
import { CommunityDataService } from '../../data/community-data.service';
import { DSpaceObjectDataService } from '../../data/dspace-object-data.service'; import { DSpaceObjectDataService } from '../../data/dspace-object-data.service';
import { RemoteData } from '../../data/remote-data'; import { RemoteData } from '../../data/remote-data';
import { RequestService } from '../../data/request.service'; import { RequestService } from '../../data/request.service';
import { RequestEntry } from '../../data/request-entry.model'; import { RequestEntryState } from '../../data/request-entry-state.model';
import { PaginationService } from '../../pagination/pagination.service'; import { PaginationService } from '../../pagination/pagination.service';
import { RouteService } from '../../services/route.service'; import { RouteService } from '../../services/route.service';
import { HALEndpointService } from '../hal-endpoint.service'; import { HALEndpointService } from '../hal-endpoint.service';
@@ -36,7 +28,8 @@ import { ViewMode } from '../view-mode.model';
import { SearchService } from './search.service'; import { SearchService } from './search.service';
import { SearchConfigurationService } from './search-configuration.service'; import { SearchConfigurationService } from './search-configuration.service';
import anything = jasmine.anything; import anything = jasmine.anything;
import SpyObj = jasmine.SpyObj;
import { Component } from '@angular/core';
@Component({ @Component({
template: '', template: '',
@@ -47,94 +40,38 @@ class DummyComponent {
} }
describe('SearchService', () => { describe('SearchService', () => {
describe('By default', () => { let service: SearchService;
let searchService: SearchService;
const router = new RouterStub(); let halService: HALEndpointServiceStub;
const route = new ActivatedRouteStub(); let paginationService: PaginationServiceStub;
const searchConfigService = { paginationID: 'page-id' }; let remoteDataBuildService: RemoteDataBuildService;
let requestService: SpyObj<RequestService>;
let routeService: RouteService;
let searchConfigService: SearchConfigurationServiceStub;
let testScheduler: TestScheduler;
let msToLive: number;
let remoteDataTimestamp: number;
beforeEach(() => { beforeEach(() => {
halService = new HALEndpointServiceStub(environment.rest.baseUrl);
paginationService = new PaginationServiceStub();
remoteDataBuildService = getMockRemoteDataBuildService();
requestService = getMockRequestService();
searchConfigService = new SearchConfigurationServiceStub();
initTestData();
TestBed.configureTestingModule({ TestBed.configureTestingModule({
imports: [ imports: [
CommonModule, CommonModule,
RouterTestingModule.withRoutes([ RouterModule.forRoot([]),
{ path: 'search', component: DummyComponent, pathMatch: 'full' },
]),
DummyComponent,
], ],
providers: [ providers: [
{ provide: Router, useValue: router },
{ provide: RouteService, useValue: routeServiceStub },
{ provide: RequestService, useValue: getMockRequestService() },
{ provide: RemoteDataBuildService, useValue: {} },
{ provide: HALEndpointService, useValue: {} },
{ provide: CommunityDataService, useValue: {} },
{ provide: DSpaceObjectDataService, useValue: {} },
{ provide: PaginationService, useValue: {} },
{ provide: SearchConfigurationService, useValue: searchConfigService },
{ provide: Angulartics2, useValue: {} },
SearchService,
],
});
searchService = TestBed.inject(SearchService);
});
it('should return list view mode', () => {
searchService.getViewMode().subscribe((viewMode) => {
expect(viewMode).toBe(ViewMode.ListElement);
});
});
});
describe('', () => {
let searchService: SearchService;
const router = new RouterStub();
let routeService;
const halService = {
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
getEndpoint: () => {
},
/* eslint-enable no-empty,@typescript-eslint/no-empty-function */
};
const remoteDataBuildService = {
toRemoteDataObservable: (requestEntryObs: Observable<RequestEntry>, payloadObs: Observable<any>) => {
return observableCombineLatest([requestEntryObs, payloadObs]).pipe(
map(([req, pay]) => {
return { req, pay };
}),
);
},
aggregate: (input: Observable<RemoteData<any>>[]): Observable<RemoteData<any[]>> => {
return createSuccessfulRemoteDataObject$([]);
},
buildFromHref: (href: string): Observable<RemoteData<any>> => {
return createSuccessfulRemoteDataObject$(Object.assign(new SearchObjects(), {
page: [],
}));
},
};
const paginationService = new PaginationServiceStub();
const searchConfigService = { paginationID: 'page-id' };
const requestService = getMockRequestService();
beforeEach(() => {
TestBed.configureTestingModule({
imports: [
CommonModule,
RouterTestingModule.withRoutes([
{ path: 'search', component: DummyComponent, pathMatch: 'full' },
]),
DummyComponent,
],
providers: [
{ provide: Router, useValue: router },
{ provide: RouteService, useValue: routeServiceStub }, { provide: RouteService, useValue: routeServiceStub },
{ provide: RequestService, useValue: requestService }, { provide: RequestService, useValue: requestService },
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService }, { provide: RemoteDataBuildService, useValue: remoteDataBuildService },
{ provide: HALEndpointService, useValue: halService }, { provide: HALEndpointService, useValue: halService },
{ provide: CommunityDataService, useValue: {} },
{ provide: DSpaceObjectDataService, useValue: {} }, { provide: DSpaceObjectDataService, useValue: {} },
{ provide: PaginationService, useValue: paginationService }, { provide: PaginationService, useValue: paginationService },
{ provide: SearchConfigurationService, useValue: searchConfigService }, { provide: SearchConfigurationService, useValue: searchConfigService },
@@ -142,84 +79,263 @@ describe('SearchService', () => {
SearchService, SearchService,
], ],
}); });
searchService = TestBed.inject(SearchService); service = TestBed.inject(SearchService);
routeService = TestBed.inject(RouteService); routeService = TestBed.inject(RouteService);
const urlTree = Object.assign(new UrlTree(), { root: { children: { primary: 'search' } } });
router.parseUrl.and.returnValue(urlTree);
}); });
function initTestData(): void {
testScheduler = new TestScheduler((actual, expected) => {
expect(actual).toEqual(expected);
});
msToLive = 15 * 60 * 1000;
// The response's lastUpdated equals the time of 60 seconds after the test started, ensuring they are not perceived
// as cached values.
remoteDataTimestamp = new Date().getTime() + 60 * 1000;
}
describe('setViewMode', () => {
it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => { it('should call the navigate method on the Router with view mode list parameter as a parameter when setViewMode is called', () => {
searchService.setViewMode(ViewMode.ListElement); service.setViewMode(ViewMode.ListElement);
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], { page: 1 }, { view: ViewMode.ListElement },
); expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('test-id', ['/search'], { page: 1 }, { view: ViewMode.ListElement });
}); });
it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => { it('should call the navigate method on the Router with view mode grid parameter as a parameter when setViewMode is called', () => {
searchService.setViewMode(ViewMode.GridElement); service.setViewMode(ViewMode.GridElement);
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], { page: 1 }, { view: ViewMode.GridElement },
); expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('test-id', ['/search'], { page: 1 }, { view: ViewMode.GridElement });
});
});
describe('getViewMode', () => {
it('should return list view mode', () => {
testScheduler.run(({ expectObservable }) => {
expectObservable(service.getViewMode()).toBe('(a|)', {
a: ViewMode.ListElement,
});
});
}); });
it('should return ViewMode.List when the viewMode is set to ViewMode.List in the ActivatedRoute', () => { it('should return ViewMode.List when the viewMode is set to ViewMode.List in the ActivatedRoute', () => {
let viewMode = ViewMode.GridElement; testScheduler.run(({ expectObservable }) => {
spyOn(routeService, 'getQueryParamMap').and.returnValue(observableOf(new Map([ spyOn(routeService, 'getQueryParamMap').and.returnValue(observableOf(new Map([
['view', ViewMode.ListElement], ['view', ViewMode.ListElement],
]))); ])));
searchService.getViewMode().subscribe((mode) => viewMode = mode); expectObservable(service.getViewMode()).toBe('(a|)', {
expect(viewMode).toEqual(ViewMode.ListElement); a: ViewMode.ListElement,
});
});
}); });
it('should return ViewMode.Grid when the viewMode is set to ViewMode.Grid in the ActivatedRoute', () => { it('should return ViewMode.Grid when the viewMode is set to ViewMode.Grid in the ActivatedRoute', () => {
let viewMode = ViewMode.ListElement; testScheduler.run(({ expectObservable }) => {
spyOn(routeService, 'getQueryParamMap').and.returnValue(observableOf(new Map([ spyOn(routeService, 'getQueryParamMap').and.returnValue(observableOf(new Map([
['view', ViewMode.GridElement], ['view', ViewMode.GridElement],
]))); ])));
searchService.getViewMode().subscribe((mode) => viewMode = mode);
expect(viewMode).toEqual(ViewMode.GridElement); expectObservable(service.getViewMode()).toBe('(a|)', {
a: ViewMode.GridElement,
});
});
});
}); });
describe('when search is called', () => { describe('search', () => {
const endPoint = 'http://endpoint.com/test/test'; let remoteDataMocks: Record<string, RemoteData<SearchObjects<any>>>;
const searchOptions = new PaginatedSearchOptions({});
beforeEach(() => { beforeEach(() => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint)); remoteDataMocks = {
spyOn((searchService as any).rdb, 'buildFromHref').and.callThrough(); RequestPending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */ ResponsePending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
searchService.search(searchOptions).subscribe((t) => { Success: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Success, undefined, new SearchObjects(), 200),
}); // subscribe to make sure all methods are called SuccessStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.SuccessStale, undefined, new SearchObjects(), 200),
/* eslint-enable no-empty,@typescript-eslint/no-empty-function */ };
});
describe('when useCachedVersionIfAvailable is true', () => {
it(`should emit a cached completed RemoteData immediately, and keep emitting if it gets re-requested`, () => {
testScheduler.run(({ cold, expectObservable }) => {
spyOn(remoteDataBuildService, 'buildFromHref').and.returnValue(cold('a-b-c-d-e', {
a: remoteDataMocks.Success,
b: remoteDataMocks.RequestPending,
c: remoteDataMocks.ResponsePending,
d: remoteDataMocks.Success,
e: remoteDataMocks.SuccessStale,
}));
const expected = 'a-b-c-d-e';
const values = {
a: remoteDataMocks.Success,
b: remoteDataMocks.RequestPending,
c: remoteDataMocks.ResponsePending,
d: remoteDataMocks.Success,
e: remoteDataMocks.SuccessStale,
};
expectObservable(service.search(undefined, msToLive, true)).toBe(expected, values);
});
});
});
describe('when useCachedVersionIfAvailable is false', () => {
it('should not emit a cached completed RemoteData', () => {
// Old cached value from 1 minute before the test started
const oldCachedSucceededData: RemoteData<SearchObjects<any>> = Object.assign(new SearchObjects(), remoteDataMocks.Success, {
timeCompleted: remoteDataTimestamp - 2 * 60 * 1000,
lastUpdated: remoteDataTimestamp - 2 * 60 * 1000,
});
testScheduler.run(({ cold, expectObservable }) => {
spyOn(remoteDataBuildService, 'buildFromHref').and.returnValue(cold('a-b-c-d-e', {
a: oldCachedSucceededData,
b: remoteDataMocks.RequestPending,
c: remoteDataMocks.ResponsePending,
d: remoteDataMocks.Success,
e: remoteDataMocks.SuccessStale,
}));
const expected = '--b-c-d-e';
const values = {
b: remoteDataMocks.RequestPending,
c: remoteDataMocks.ResponsePending,
d: remoteDataMocks.Success,
e: remoteDataMocks.SuccessStale,
};
expectObservable(service.search(undefined, msToLive, false)).toBe(expected, values);
});
});
it('should emit the first completed RemoteData since the request was made', () => {
testScheduler.run(({ cold, expectObservable }) => {
spyOn(remoteDataBuildService, 'buildFromHref').and.returnValue(cold('a-b', {
a: remoteDataMocks.Success,
b: remoteDataMocks.SuccessStale,
}));
const expected = 'a-b';
const values = {
a: remoteDataMocks.Success,
b: remoteDataMocks.SuccessStale,
};
expectObservable(service.search(undefined, msToLive, false)).toBe(expected, values);
});
});
}); });
it('should call getEndpoint on the halService', () => { it('should call getEndpoint on the halService', () => {
expect((searchService as any).halService.getEndpoint).toHaveBeenCalled(); spyOn(halService, 'getEndpoint').and.callThrough();
service.search(new PaginatedSearchOptions({})).subscribe();
expect(halService.getEndpoint).toHaveBeenCalled();
}); });
it('should send out the request on the request service', () => { it('should send out the request on the request service', () => {
expect((searchService as any).requestService.send).toHaveBeenCalled(); service.search(new PaginatedSearchOptions({})).subscribe();
expect(requestService.send).toHaveBeenCalled();
}); });
it('should call getByHref on the request service with the correct request url', () => { it('should call getByHref on the request service with the correct request url', () => {
expect((searchService as any).rdb.buildFromHref).toHaveBeenCalledWith(endPoint); spyOn(remoteDataBuildService, 'buildFromHref').and.callThrough();
service.search(new PaginatedSearchOptions({})).subscribe();
expect(remoteDataBuildService.buildFromHref).toHaveBeenCalledWith(environment.rest.baseUrl + '/discover/search/objects');
}); });
}); });
describe('when getFacetValuesFor is called with a filterQuery', () => { describe('getFacetValuesFor', () => {
it('should add the encoded filterQuery to the args list', () => { let remoteDataMocks: Record<string, RemoteData<FacetValues>>;
jasmine.getEnv().allowRespy(true); let filterConfig: SearchFilterConfig;
const spyRequest = spyOn((searchService as any), 'request').and.stub();
spyOn(requestService, 'send').and.returnValue(true); beforeEach(() => {
const searchFilterConfig = new SearchFilterConfig(); remoteDataMocks = {
searchFilterConfig._links = { RequestPending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.RequestPending, undefined, undefined, undefined),
ResponsePending: new RemoteData(undefined, msToLive, remoteDataTimestamp, RequestEntryState.ResponsePending, undefined, undefined, undefined),
Success: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.Success, undefined, new FacetValues(), 200),
SuccessStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.SuccessStale, undefined, new FacetValues(), 200),
};
filterConfig = new SearchFilterConfig();
filterConfig._links = {
self: { self: {
href: 'https://demo.dspace.org/', href: environment.rest.baseUrl,
}, },
}; };
searchService.getFacetValuesFor(searchFilterConfig, 1, undefined, 'filter&Query');
expect(spyRequest).toHaveBeenCalledWith(anything(), 'https://demo.dspace.org?page=0&size=5&prefix=filter%26Query');
}); });
describe('when useCachedVersionIfAvailable is true', () => {
it(`should emit a cached completed RemoteData immediately, and keep emitting if it gets re-requested`, () => {
testScheduler.run(({ cold, expectObservable }) => {
spyOn(remoteDataBuildService, 'buildFromHref').and.returnValue(cold('a-b-c-d-e', {
a: remoteDataMocks.Success,
b: remoteDataMocks.RequestPending,
c: remoteDataMocks.ResponsePending,
d: remoteDataMocks.Success,
e: remoteDataMocks.SuccessStale,
}));
const expected = 'a-b-c-d-e';
const values = {
a: remoteDataMocks.Success,
b: remoteDataMocks.RequestPending,
c: remoteDataMocks.ResponsePending,
d: remoteDataMocks.Success,
e: remoteDataMocks.SuccessStale,
};
expectObservable(service.getFacetValuesFor(filterConfig, 1, undefined, undefined, true)).toBe(expected, values);
});
});
});
describe('when useCachedVersionIfAvailable is false', () => {
it('should not emit a cached completed RemoteData', () => {
// Old cached value from 1 minute before the test started
const oldCachedSucceededData: RemoteData<FacetValues> = Object.assign(new FacetValues(), remoteDataMocks.Success, {
timeCompleted: remoteDataTimestamp - 2 * 60 * 1000,
lastUpdated: remoteDataTimestamp - 2 * 60 * 1000,
});
testScheduler.run(({ cold, expectObservable }) => {
spyOn(remoteDataBuildService, 'buildFromHref').and.returnValue(cold('a-b-c-d-e', {
a: oldCachedSucceededData,
b: remoteDataMocks.RequestPending,
c: remoteDataMocks.ResponsePending,
d: remoteDataMocks.Success,
e: remoteDataMocks.SuccessStale,
}));
const expected = '--b-c-d-e';
const values = {
b: remoteDataMocks.RequestPending,
c: remoteDataMocks.ResponsePending,
d: remoteDataMocks.Success,
e: remoteDataMocks.SuccessStale,
};
expectObservable(service.getFacetValuesFor(filterConfig, 1, undefined, undefined, false)).toBe(expected, values);
});
});
it('should emit the first completed RemoteData since the request was made', () => {
testScheduler.run(({ cold, expectObservable }) => {
spyOn(remoteDataBuildService, 'buildFromHref').and.returnValue(cold('a-b', {
a: remoteDataMocks.Success,
b: remoteDataMocks.SuccessStale,
}));
const expected = 'a-b';
const values = {
a: remoteDataMocks.Success,
b: remoteDataMocks.SuccessStale,
};
expectObservable(service.getFacetValuesFor(filterConfig, 1, undefined, undefined, false)).toBe(expected, values);
});
});
});
it('should encode the filterQuery', () => {
spyOn((service as any), 'request').and.callThrough();
service.getFacetValuesFor(filterConfig, 1, undefined, 'filter&Query');
expect((service as any).request).toHaveBeenCalledWith(anything(), environment.rest.baseUrl + '?page=0&size=5&prefix=filter%26Query');
}); });
}); });
}); });

View File

@@ -9,6 +9,7 @@ import {
import { import {
distinctUntilChanged, distinctUntilChanged,
map, map,
skipWhile,
switchMap, switchMap,
take, take,
tap, tap,
@@ -168,6 +169,7 @@ export class SearchService {
search<T extends DSpaceObject>(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<SearchObjects<T>>> { search<T extends DSpaceObject>(searchOptions?: PaginatedSearchOptions, responseMsToLive?: number, useCachedVersionIfAvailable = true, reRequestOnStale = true, ...linksToFollow: FollowLinkConfig<T>[]): Observable<RemoteData<SearchObjects<T>>> {
const href$ = this.getEndpoint(searchOptions); const href$ = this.getEndpoint(searchOptions);
let startTime: number;
href$.pipe( href$.pipe(
take(1), take(1),
map((href: string) => { map((href: string) => {
@@ -191,6 +193,7 @@ export class SearchService {
searchOptions: searchOptions, searchOptions: searchOptions,
}); });
startTime = new Date().getTime();
this.requestService.send(request, useCachedVersionIfAvailable); this.requestService.send(request, useCachedVersionIfAvailable);
}); });
@@ -198,7 +201,13 @@ export class SearchService {
switchMap((href: string) => this.rdb.buildFromHref<SearchObjects<T>>(href)), switchMap((href: string) => this.rdb.buildFromHref<SearchObjects<T>>(href)),
); );
return this.directlyAttachIndexableObjects(sqr$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow); return this.directlyAttachIndexableObjects(sqr$, useCachedVersionIfAvailable, reRequestOnStale, ...linksToFollow).pipe(
// This skip ensures that if a stale object is present in the cache when you do a
// call it isn't immediately returned, but we wait until the remote data for the new request
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
// cached completed object
skipWhile((rd: RemoteData<SearchObjects<T>>) => rd.isStale || (!useCachedVersionIfAvailable && rd.lastUpdated < startTime)),
);
} }
/** /**
@@ -304,9 +313,15 @@ export class SearchService {
return FacetValueResponseParsingService; return FacetValueResponseParsingService;
}, },
}); });
const startTime = new Date().getTime();
this.requestService.send(request, useCachedVersionIfAvailable); this.requestService.send(request, useCachedVersionIfAvailable);
return this.rdb.buildFromHref(href).pipe( return this.rdb.buildFromHref(href).pipe(
// This skip ensures that if a stale object is present in the cache when you do a
// call it isn't immediately returned, but we wait until the remote data for the new request
// is created. If useCachedVersionIfAvailable is false it also ensures you don't get a
// cached completed object
skipWhile((rd: RemoteData<FacetValues>) => rd.isStale || (!useCachedVersionIfAvailable && rd.lastUpdated < startTime)),
tap((facetValuesRD: RemoteData<FacetValues>) => { tap((facetValuesRD: RemoteData<FacetValues>) => {
if (facetValuesRD.hasSucceeded) { if (facetValuesRD.hasSucceeded) {
const appliedFilters: AppliedFilter[] = (facetValuesRD.payload.appliedFilters ?? []) const appliedFilters: AppliedFilter[] = (facetValuesRD.payload.appliedFilters ?? [])

View File

@@ -7,7 +7,7 @@
<a <a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>
@@ -48,7 +48,7 @@
<div class="text-center"> <div class="text-center">
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a> class="lead btn btn-primary viewButton" role="link" tabindex="0">{{ 'search.results.view-result' | translate}}</a>
</div> </div>
} }
</div> </div>

View File

@@ -7,7 +7,7 @@
<a <a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>
@@ -48,7 +48,7 @@
<div class="text-center"> <div class="text-center">
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a> class="lead btn btn-primary viewButton" role="link" tabindex="0">{{ 'search.results.view-result' | translate}}</a>
</div> </div>
} }
</div> </div>

View File

@@ -7,7 +7,7 @@
<a <a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>
@@ -54,7 +54,7 @@
<div class="text-center"> <div class="text-center">
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a> class="lead btn btn-primary viewButton" role="link" tabindex="0">{{ 'search.results.view-result' | translate}}</a>
</div> </div>
} }
</div> </div>

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"> [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" role="link" tabindex="0">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail> </ds-thumbnail>
</a> </a>
@@ -26,7 +26,7 @@
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a> [innerHTML]="dsoTitle" role="link" tabindex="0"></a>
} }
@if (linkType === linkTypes.None) { @if (linkType === linkTypes.None) {
<span <span

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"> [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" role="link" tabindex="0">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail> </ds-thumbnail>
</a> </a>
@@ -26,7 +26,7 @@
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a> [innerHTML]="dsoTitle" role="link" tabindex="0"></a>
} }
@if (linkType === linkTypes.None) { @if (linkType === linkTypes.None) {
<span <span

View File

@@ -3,7 +3,7 @@
<div class="col-3 col-md-2"> <div class="col-3 col-md-2">
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"> [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" role="link" tabindex="0">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="true">
</ds-thumbnail> </ds-thumbnail>
</a> </a>
@@ -24,7 +24,7 @@
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a> [innerHTML]="dsoTitle" role="link" tabindex="0"></a>
} }
@if (linkType === linkTypes.None) { @if (linkType === linkTypes.None) {
<span <span

View File

@@ -53,7 +53,7 @@
[label]="'journalissue.page.keyword'"> [label]="'journalissue.page.keyword'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
{{"item.page.link.full" | translate}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -36,7 +36,7 @@
[label]="'journalvolume.page.description'"> [label]="'journalvolume.page.description'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
{{"item.page.link.full" | translate}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -35,7 +35,7 @@
[label]="'journal.page.description'"> [label]="'journal.page.description'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
{{"item.page.link.full" | translate}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -7,7 +7,7 @@
<a <a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>
@@ -56,7 +56,7 @@
<div class="text-center"> <div class="text-center">
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a> class="lead btn btn-primary viewButton" role="link" tabindex="0">{{ 'search.results.view-result' | translate}}</a>
</div> </div>
} }
</div> </div>

View File

@@ -7,7 +7,7 @@
<a <a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>
@@ -47,7 +47,7 @@
<div class="text-center"> <div class="text-center">
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a> class="lead btn btn-primary viewButton" role="link" tabindex="0">{{ 'search.results.view-result' | translate}}</a>
</div> </div>
} }
</div> </div>

View File

@@ -7,7 +7,7 @@
<a <a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate"> class="card-img-top full-width" [attr.title]="'search.results.view-result' | translate" role="link" tabindex="0">
<div> <div>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false"> <ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail> </ds-thumbnail>
@@ -40,7 +40,7 @@
<div class="text-center"> <div class="text-center">
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [routerLink]="[itemPageRoute]"
class="lead btn btn-primary viewButton">{{ 'search.results.view-result' | translate}}</a> class="lead btn btn-primary viewButton" role="link" tabindex="0">{{ 'search.results.view-result' | translate}}</a>
</div> </div>
} }
</div> </div>

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="dont-break-out"> [routerLink]="[itemPageRoute]" class="dont-break-out" role="link" tabindex="0">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" <ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/orgunit-placeholder.svg'" [defaultImage]="'assets/images/orgunit-placeholder.svg'"
[alt]="'thumbnail.orgunit.alt'" [alt]="'thumbnail.orgunit.alt'"
@@ -32,7 +32,7 @@
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead" [routerLink]="[itemPageRoute]" class="lead"
[innerHTML]="dsoTitle || ('orgunit.listelement.no-title' | translate)"></a> [innerHTML]="dsoTitle || ('orgunit.listelement.no-title' | translate)" role="link" tabindex="0"></a>
} }
@if (linkType === linkTypes.None) { @if (linkType === linkTypes.None) {
<span <span

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="dont-break-out"> [routerLink]="[itemPageRoute]" class="dont-break-out" role="link" tabindex="0">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" <ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/person-placeholder.svg'" [defaultImage]="'assets/images/person-placeholder.svg'"
[alt]="'thumbnail.person.alt'" [alt]="'thumbnail.person.alt'"
@@ -32,7 +32,7 @@
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead" [routerLink]="[itemPageRoute]" class="lead"
[innerHTML]="dsoTitle || ('person.listelement.no-title' | translate)"></a> [innerHTML]="dsoTitle || ('person.listelement.no-title' | translate)" role="link" tabindex="0"></a>
} }
@if (linkType === linkTypes.None) { @if (linkType === linkTypes.None) {
<span <span

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) { @if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="dont-break-out"> [routerLink]="[itemPageRoute]" class="dont-break-out" role="link" tabindex="0">
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" <ds-thumbnail [thumbnail]="dso?.thumbnail | async"
[defaultImage]="'assets/images/project-placeholder.svg'" [defaultImage]="'assets/images/project-placeholder.svg'"
[alt]="'thumbnail.project.alt'" [alt]="'thumbnail.project.alt'"
@@ -32,7 +32,7 @@
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'" <a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null" [attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out" [routerLink]="[itemPageRoute]" class="lead item-list-title dont-break-out"
[innerHTML]="dsoTitle"></a> [innerHTML]="dsoTitle" role="link" tabindex="0"></a>
} }
@if (linkType === linkTypes.None) { @if (linkType === linkTypes.None) {
<span <span

View File

@@ -56,7 +56,7 @@
[label]="'orgunit.page.description'"> [label]="'orgunit.page.description'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
{{"item.page.link.full" | translate}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -52,7 +52,7 @@
[label]="'person.page.name'"> [label]="'person.page.name'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
{{"item.page.link.full" | translate}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -64,7 +64,7 @@
[label]="'project.page.keyword'"> [label]="'project.page.keyword'">
</ds-generic-item-page-field> </ds-generic-item-page-field>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
{{"item.page.link.full" | translate}} {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -26,7 +26,7 @@
<h5 class="text-uppercase">Footer Content</h5> <h5 class="text-uppercase">Footer Content</h5>
<ul class="list-unstyled mb-0"> <ul class="list-unstyled mb-0">
<li> <li>
<a routerLink="./" class="">Suspendisse potenti</a> <a routerLink="./" class="" role="link" tabindex="0">Suspendisse potenti</a>
</li> </li>
</ul> </ul>
</div> </div>
@@ -53,14 +53,14 @@
<div class="content-container"> <div class="content-container">
<p class="m-0"> <p class="m-0">
<a class="text-white" <a class="text-white"
href="http://www.dspace.org/">{{ 'footer.link.dspace' | translate}}</a> href="http://www.dspace.org/" role="link" tabindex="0">{{ 'footer.link.dspace' | translate}}</a>
{{ 'footer.copyright' | translate:{year: dateObj | date:'y'} }} {{ 'footer.copyright' | translate:{year: dateObj | date:'y'} }}
<a class="text-white" <a class="text-white"
href="https://www.lyrasis.org/">{{ 'footer.link.lyrasis' | translate}}</a> href="https://www.lyrasis.org/" role="link" tabindex="0">{{ 'footer.link.lyrasis' | translate}}</a>
</p> </p>
<ul class="footer-info list-unstyled d-flex justify-content-center mb-0"> <ul class="footer-info list-unstyled d-flex justify-content-center mb-0">
<li> <li>
<button class="btn btn-link text-white" type="button" (click)="showCookieSettings()"> <button class="btn btn-link text-white" type="button" (click)="showCookieSettings()" role="button" tabindex="0">
{{ 'footer.link.cookies' | translate}} {{ 'footer.link.cookies' | translate}}
</button> </button>
</li> </li>
@@ -71,26 +71,26 @@
@if (showPrivacyPolicy) { @if (showPrivacyPolicy) {
<li> <li>
<a class="btn text-white" <a class="btn text-white"
routerLink="info/privacy">{{ 'footer.link.privacy-policy' | translate}}</a> routerLink="info/privacy" role="link" tabindex="0">{{ 'footer.link.privacy-policy' | translate}}</a>
</li> </li>
} }
@if (showEndUserAgreement) { @if (showEndUserAgreement) {
<li> <li>
<a class="btn text-white" <a class="btn text-white"
routerLink="info/end-user-agreement">{{ 'footer.link.end-user-agreement' | translate}}</a> routerLink="info/end-user-agreement" role="link" tabindex="0">{{ 'footer.link.end-user-agreement' | translate}}</a>
</li> </li>
} }
@if (showSendFeedback$ | async) { @if (showSendFeedback$ | async) {
<li> <li>
<a class="btn text-white" <a class="btn text-white"
routerLink="info/feedback">{{ 'footer.link.feedback' | translate}}</a> routerLink="info/feedback" role="link" tabindex="0">{{ 'footer.link.feedback' | translate}}</a>
</li> </li>
} }
</ul> </ul>
</div> </div>
@if (coarLdnEnabled$ | async) { @if (coarLdnEnabled$ | async) {
<div class="notify-enabled text-white"> <div class="notify-enabled text-white">
<a class="coar-notify-support-route" routerLink="info/coar-notify-support"> <a class="coar-notify-support-route" routerLink="info/coar-notify-support" role="link" tabindex="0">
<img class="n-coar" src="assets/images/n-coar.svg" [attr.alt]="'menu.header.image.logo' | translate" /> <img class="n-coar" src="assets/images/n-coar.svg" [attr.alt]="'menu.header.image.logo' | translate" />
{{ 'footer.link.coar-notify-support' | translate }} {{ 'footer.link.coar-notify-support' | translate }}
</a> </a>

View File

@@ -1,7 +1,7 @@
<header> <header>
<div class="container"> <div class="container">
<div class="d-flex flex-row justify-content-between"> <div class="d-flex flex-row justify-content-between">
<a class="navbar-brand my-2" routerLink="/home"> <a class="navbar-brand my-2" routerLink="/home" role="button" tabindex="0">
<img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/> <img src="assets/images/dspace-logo.svg" [attr.alt]="'menu.header.image.logo' | translate"/>
</a> </a>

View File

@@ -1,4 +1,4 @@
<div class="jumbotron py-4 px-2 py-sm-5 px-sm-0 mt-0 mb-4"> <div class="jumbotron py-4 px-2 py-sm-5 px-sm-0 mt-ncs mb-4">
<div class="container"> <div class="container">
<div class="d-flex flex-wrap"> <div class="d-flex flex-wrap">
<div> <div>
@@ -14,7 +14,7 @@
<li>issue permanent urls and trustworthy identifiers, including optional integrations with handle.net and DataCite DOI</li> <li>issue permanent urls and trustworthy identifiers, including optional integrations with handle.net and DataCite DOI</li>
</ul> </ul>
<p>Join an international community of <a href="https://wiki.lyrasis.org/display/DSPACE/DSpace+Positioning" <p>Join an international community of <a href="https://wiki.lyrasis.org/display/DSPACE/DSpace+Positioning"
target="_blank">leading institutions using DSpace</a>. target="_blank" role="link" tabindex="0">leading institutions using DSpace</a>.
</p> </p>
</div> </div>
</div> </div>

View File

@@ -2,7 +2,6 @@ import { Route } from '@angular/router';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model'; import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { MenuItemType } from '../shared/menu/menu-item-type.model'; import { MenuItemType } from '../shared/menu/menu-item-type.model';
import { homePageResolver } from './home-page.resolver';
import { ThemedHomePageComponent } from './themed-home-page.component'; import { ThemedHomePageComponent } from './themed-home-page.component';
export const ROUTES: Route[] = [ export const ROUTES: Route[] = [
@@ -26,8 +25,5 @@ export const ROUTES: Route[] = [
}], }],
}, },
}, },
resolve: {
site: homePageResolver,
},
}, },
]; ];

View File

@@ -18,9 +18,6 @@
<ds-suggestions-popup></ds-suggestions-popup> <ds-suggestions-popup></ds-suggestions-popup>
<ng-template #homeContent> <ng-template #homeContent>
@if ((site$ | async); as site) {
<ds-view-tracker [object]="site"></ds-view-tracker>
}
<ds-search-form [inPlaceSearch]="false" <ds-search-form [inPlaceSearch]="false"
[searchPlaceholder]="'home.search-form.placeholder' | translate"> [searchPlaceholder]="'home.search-form.placeholder' | translate">
</ds-search-form> </ds-search-form>

View File

@@ -22,7 +22,6 @@ import { SuggestionsPopupComponent } from '../notifications/suggestions/popup/su
import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component'; import { ThemedConfigurationSearchPageComponent } from '../search-page/themed-configuration-search-page.component';
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 { 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';
import { RecentItemListComponent } from './recent-item-list/recent-item-list.component'; import { RecentItemListComponent } from './recent-item-list/recent-item-list.component';
@@ -33,7 +32,7 @@ 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, NgTemplateOutlet, ViewTrackerComponent, ThemedSearchFormComponent, ThemedTopLevelCommunityListComponent, RecentItemListComponent, AsyncPipe, TranslateModule, NgClass, SuggestionsPopupComponent, ThemedConfigurationSearchPageComponent, PageWithSidebarComponent, HomeCoarComponent], imports: [ThemedHomeNewsComponent, NgTemplateOutlet, ThemedSearchFormComponent, ThemedTopLevelCommunityListComponent, RecentItemListComponent, AsyncPipe, TranslateModule, NgClass, SuggestionsPopupComponent, ThemedConfigurationSearchPageComponent, PageWithSidebarComponent, HomeCoarComponent],
}) })
export class HomePageComponent implements OnInit { export class HomePageComponent implements OnInit {

View File

@@ -9,7 +9,7 @@
</ds-listable-object-component-loader> </ds-listable-object-component-loader>
</div> </div>
} }
<button (click)="onLoadMore()" class="btn btn-primary search-button mt-4 float-start ng-tns-c290-40"> {{'vocabulary-treeview.load-more' | translate }} ...</button> <button (click)="onLoadMore()" class="btn btn-primary search-button mt-4 float-start ng-tns-c290-40" role="button" tabindex="0"> {{'vocabulary-treeview.load-more' | translate }} ...</button>
</div> </div>
} }
@if (itemRD?.hasFailed) { @if (itemRD?.hasFailed) {

View File

@@ -8,6 +8,7 @@
<ds-viewable-collection <ds-viewable-collection
[config]="config" [config]="config"
[sortConfig]="sortConfig" [sortConfig]="sortConfig"
[showRSS]="rssSortConfig"
[objects]="communitiesRD$ | async" [objects]="communitiesRD$ | async"
[hideGear]="true"> [hideGear]="true">
</ds-viewable-collection> </ds-viewable-collection>

View File

@@ -65,9 +65,10 @@ export class TopLevelCommunityListComponent implements OnInit, OnDestroy {
pageId = 'tl'; pageId = 'tl';
/** /**
* The sorting configuration * The sorting configuration for the community list itself, and the optional RSS feed button
*/ */
sortConfig: SortOptions; sortConfig: SortOptions;
rssSortConfig: SortOptions;
/** /**
* The subscription to the observable for the current page. * The subscription to the observable for the current page.
@@ -84,6 +85,7 @@ export class TopLevelCommunityListComponent implements OnInit, OnDestroy {
this.config.pageSize = appConfig.homePage.topLevelCommunityList.pageSize; this.config.pageSize = appConfig.homePage.topLevelCommunityList.pageSize;
this.config.currentPage = 1; this.config.currentPage = 1;
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC); this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
this.rssSortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
} }
ngOnInit() { ngOnInit() {

View File

@@ -10,7 +10,7 @@
<div class="d-flex justify-content-between flex-wrap"> <div class="d-flex justify-content-between flex-wrap">
<span class="align-self-center">{{'item.alerts.withdrawn' | translate}}</span> <span class="align-self-center">{{'item.alerts.withdrawn' | translate}}</span>
<div class="gap-2 d-flex"> <div class="gap-2 d-flex">
<a routerLink="/home" class="btn btn-primary btn-sm">{{"404.link.home-page" | translate}}</a> <a routerLink="/home" class="btn btn-primary btn-sm" role="button" tabindex="0">{{"404.link.home-page" | translate}}</a>
@if (showReinstateButton$ | async) { @if (showReinstateButton$ | async) {
<a class="btn btn-primary btn-sm" (click)="openReinstateModal()">{{ 'item.alerts.reinstate-request' | translate}}</a> <a class="btn btn-primary btn-sm" (click)="openReinstateModal()">{{ 'item.alerts.reinstate-request' | translate}}</a>
} }

View File

@@ -1,7 +1,7 @@
<div class="container"> <div class="container">
<ds-alert [type]="'alert-info'" [content]="'item.edit.authorizations.heading'"></ds-alert> <ds-alert [type]="AlertType.Info" [content]="'item.edit.authorizations.heading'"></ds-alert>
<ds-resource-policies [resourceType]="'item'" [resourceName]="(getItemName() | async)" <ds-resource-policies [resourceType]="'item'" [resourceName]="itemName$ | async"
[resourceUUID]="(getItemUUID() | async)"> [resourceUUID]="(item$ | async)?.id">
</ds-resource-policies> </ds-resource-policies>
@for (bundle of (bundles$ | async); track bundle) { @for (bundle of (bundles$ | async); track bundle) {
<ds-resource-policies [resourceType]="'bundle'" [resourceUUID]="bundle.id" [resourceName]="bundle.name"> <ds-resource-policies [resourceType]="'bundle'" [resourceUUID]="bundle.id" [resourceName]="bundle.name">

View File

@@ -169,17 +169,9 @@ describe('ItemAuthorizationsComponent test suite', () => {
})); }));
}); });
it('should get the item UUID', () => {
expect(comp.getItemUUID()).toBeObservable(cold('(a|)', {
a: item.id,
}));
});
it('should get the item\'s bundle', () => { it('should get the item\'s bundle', () => {
expect(comp.getItemBundles()).toBeObservable(cold('a', { expect(comp.bundles$).toBeObservable(cold('a', {
a: bundles, a: bundles,
})); }));

View File

@@ -17,7 +17,6 @@ import {
import { import {
catchError, catchError,
filter, filter,
first,
map, map,
mergeMap, mergeMap,
take, take,
@@ -37,11 +36,11 @@ import {
getFirstSucceededRemoteDataWithNotEmptyPayload, getFirstSucceededRemoteDataWithNotEmptyPayload,
} from '../../../core/shared/operators'; } from '../../../core/shared/operators';
import { AlertComponent } from '../../../shared/alert/alert.component'; import { AlertComponent } from '../../../shared/alert/alert.component';
import { AlertType } from '../../../shared/alert/alert-type';
import { import {
hasValue, hasValue,
isNotEmpty, isNotEmpty,
} from '../../../shared/empty.util'; } from '../../../shared/empty.util';
import { NgForTrackByIdDirective } from '../../../shared/ng-for-track-by-id.directive';
import { ResourcePoliciesComponent } from '../../../shared/resource-policies/resource-policies.component'; import { ResourcePoliciesComponent } from '../../../shared/resource-policies/resource-policies.component';
import { followLink } from '../../../shared/utils/follow-link-config.model'; import { followLink } from '../../../shared/utils/follow-link-config.model';
@@ -61,7 +60,6 @@ interface BundleBitstreamsMapEntry {
ResourcePoliciesComponent, ResourcePoliciesComponent,
NgbCollapseModule, NgbCollapseModule,
TranslateModule, TranslateModule,
NgForTrackByIdDirective,
AsyncPipe, AsyncPipe,
AlertComponent, AlertComponent,
], ],
@@ -88,7 +86,7 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
* The target editing item * The target editing item
* @type {Observable<Item>} * @type {Observable<Item>}
*/ */
private item$: Observable<Item>; item$: Observable<Item>;
/** /**
* Array to track all subscriptions and unsubscribe them onDestroy * Array to track all subscriptions and unsubscribe them onDestroy
@@ -127,16 +125,13 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
*/ */
private bitstreamPageSize = 4; private bitstreamPageSize = 4;
/** itemName$: Observable<string>;
* Initialize instance variables
* readonly AlertType = AlertType;
* @param {LinkService} linkService
* @param {ActivatedRoute} route
* @param nameService
*/
constructor( constructor(
private linkService: LinkService, protected linkService: LinkService,
private route: ActivatedRoute, protected route: ActivatedRoute,
public nameService: DSONameService, public nameService: DSONameService,
) { ) {
} }
@@ -146,36 +141,18 @@ export class ItemAuthorizationsComponent implements OnInit, OnDestroy {
*/ */
ngOnInit(): void { ngOnInit(): void {
this.getBundlesPerItem(); this.getBundlesPerItem();
} this.itemName$ = this.getItemName();
/**
* Return the item's UUID
*/
getItemUUID(): Observable<string> {
return this.item$.pipe(
map((item: Item) => item.id),
first((UUID: string) => isNotEmpty(UUID)),
);
} }
/** /**
* Return the item's name * Return the item's name
*/ */
getItemName(): Observable<string> { private getItemName(): Observable<string> {
return this.item$.pipe( return this.item$.pipe(
map((item: Item) => this.nameService.getName(item)), map((item: Item) => this.nameService.getName(item)),
); );
} }
/**
* Return all item's bundles
*
* @return an observable that emits all item's bundles
*/
getItemBundles(): Observable<Bundle[]> {
return this.bundles$.asObservable();
}
/** /**
* Get all bundles per item * Get all bundles per item
* and all the bitstreams per bundle * and all the bitstreams per bundle

View File

@@ -30,7 +30,6 @@ import {
map, map,
switchMap, switchMap,
take, take,
tap,
} from 'rxjs/operators'; } from 'rxjs/operators';
import { AlertComponent } from 'src/app/shared/alert/alert.component'; import { AlertComponent } from 'src/app/shared/alert/alert.component';
import { AlertType } from 'src/app/shared/alert/alert-type'; import { AlertType } from 'src/app/shared/alert/alert-type';
@@ -239,15 +238,28 @@ export class ItemBitstreamsComponent extends AbstractItemUpdateComponent impleme
this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({ pagination: this.bundlesOptions })).pipe( this.itemService.getBundles(this.item.id, new PaginatedSearchOptions({ pagination: this.bundlesOptions })).pipe(
getFirstSucceededRemoteData(), getFirstSucceededRemoteData(),
getRemoteDataPayload(), getRemoteDataPayload(),
tap((bundlesPL: PaginatedList<Bundle>) => ).subscribe((bundles: PaginatedList<Bundle>) => {
this.showLoadMoreLink$.next(bundlesPL.pageInfo.currentPage < bundlesPL.pageInfo.totalPages), this.updateBundles(bundles);
),
map((bundlePage: PaginatedList<Bundle>) => bundlePage.page),
).subscribe((bundles: Bundle[]) => {
this.bundlesSubject.next([...this.bundlesSubject.getValue(), ...bundles]);
}); });
} }
/**
* Update the subject containing the bundles with the provided bundles.
* Also updates the showLoadMoreLink observable so it does not show up when it is no longer necessary.
*/
updateBundles(newBundlesPL: PaginatedList<Bundle>) {
const currentBundles = this.bundlesSubject.getValue();
// Only add bundles to the bundle subject if they are not present yet
const bundlesToAdd = newBundlesPL.page
.filter(bundleToAdd => !currentBundles.some(currentBundle => currentBundle.id === bundleToAdd.id));
const updatedBundles = [...currentBundles, ...bundlesToAdd];
this.showLoadMoreLink$.next(updatedBundles.length < newBundlesPL.totalElements);
this.bundlesSubject.next(updatedBundles);
}
/** /**
* Submit the current changes * Submit the current changes

View File

@@ -1,7 +1,7 @@
<ds-metadata-field-wrapper [label]="label | translate"> <ds-metadata-field-wrapper [label]="label | translate">
<div class="collections"> <div class="collections">
@for (collection of (this.collections$ | async); track collection; let last = $last) { @for (collection of (this.collections$ | async); track collection; let last = $last) {
<a [routerLink]="['/collections', collection.id]"> <a [routerLink]="['/collections', collection.id]" role="button" tabindex="0">
<span>{{ dsoNameService.getName(collection) }}</span>@if (!last) { <span>{{ dsoNameService.getName(collection) }}</span>@if (!last) {
<span [innerHTML]="separator"></span> <span [innerHTML]="separator"></span>
} }
@@ -21,6 +21,8 @@
class="load-more-btn btn btn-sm btn-outline-secondary" class="load-more-btn btn btn-sm btn-outline-secondary"
role="button" role="button"
href="javascript:void(0);" href="javascript:void(0);"
role="button"
tabindex="0"
> >
{{'item.page.collections.load-more' | translate}} {{'item.page.collections.load-more' | translate}}
</a> </a>

View File

@@ -1,6 +1,6 @@
<ds-metadata-field-wrapper [label]="label | translate"> <ds-metadata-field-wrapper [label]="label | translate">
@for (mdValue of mdValues; track mdValue; let last = $last) { @for (mdValue of mdValues; track mdValue; let last = $last) {
<a class="dont-break-out" [href]="mdValue.value" [target]="hasInternalLink(mdValue.value) ? '_self' : '_blank'"> <a class="dont-break-out" [href]="mdValue.value" [target]="hasInternalLink(mdValue.value) ? '_self' : '_blank'" role="link" tabindex="0">
{{ linktext || mdValue.value }}@if (!last) { {{ linktext || mdValue.value }}@if (!last) {
<span [innerHTML]="separator"></span> <span [innerHTML]="separator"></span>
} }

View File

@@ -23,14 +23,14 @@
<a class="dont-break-out ds-simple-metadata-link" <a class="dont-break-out ds-simple-metadata-link"
[href]="value" [href]="value"
[attr.target]="getLinkAttributes(value).target" [attr.target]="getLinkAttributes(value).target"
[attr.rel]="getLinkAttributes(value).rel"> [attr.rel]="getLinkAttributes(value).rel" role="link" tabindex="0">
{{value}} {{value}}
</a> </a>
</ng-template> </ng-template>
<!-- Render value as a link with icon --> <!-- Render value as a link with icon -->
<ng-template #linkImg let-img="img" let-value="value"> <ng-template #linkImg let-img="img" let-value="value">
<a [href]="value" class="link-anchor dont-break-out ds-simple-metadata-link" target="_blank"> <a [href]="value" class="link-anchor dont-break-out ds-simple-metadata-link" target="_blank" role="link" tabindex="0">
<img class="link-logo" <img class="link-logo"
[alt]="img.alt | translate" [alt]="img.alt | translate"
[style.height]="'var(' + img.heightVar + ', --ds-item-page-img-field-default-inline-height)'" [style.height]="'var(' + img.heightVar + ', --ds-item-page-img-field-default-inline-height)'"
@@ -48,5 +48,5 @@
<ng-template #browselink let-value="value"> <ng-template #browselink let-value="value">
<a class="dont-break-out preserve-line-breaks ds-browse-link" <a class="dont-break-out preserve-line-breaks ds-browse-link"
[routerLink]="['/browse', browseDefinition.id]" [routerLink]="['/browse', browseDefinition.id]"
[queryParams]="getQueryParams(value)">{{value}}</a> [queryParams]="getQueryParams(value)" role="link" tabindex="0">{{value}}</a>
</ng-template> </ng-template>

View File

@@ -5,7 +5,6 @@
<div> <div>
<ds-item-alerts [item]="item"></ds-item-alerts> <ds-item-alerts [item]="item"></ds-item-alerts>
<ds-item-versions-notice [item]="item"></ds-item-versions-notice> <ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker>
@if (!item.isWithdrawn || (isAdmin$|async)) { @if (!item.isWithdrawn || (isAdmin$|async)) {
<div class="full-item-info"> <div class="full-item-info">
<div class="d-flex flex-row"> <div class="d-flex flex-row">

View File

@@ -45,7 +45,6 @@ import { createPaginatedList } from '../../shared/testing/utils.test';
import { ThemeService } from '../../shared/theme-support/theme.service'; import { ThemeService } from '../../shared/theme-support/theme.service';
import { TruncatePipe } from '../../shared/utils/truncate.pipe'; import { TruncatePipe } from '../../shared/utils/truncate.pipe';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { ViewTrackerComponent } from '../../statistics/angulartics/dspace/view-tracker.component';
import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component'; import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component';
import { CollectionsComponent } from '../field-components/collections/collections.component'; import { CollectionsComponent } from '../field-components/collections/collections.component';
import { ThemedItemPageTitleFieldComponent } from '../simple/field-components/specific-field/title/themed-item-page-field.component'; import { ThemedItemPageTitleFieldComponent } from '../simple/field-components/specific-field/title/themed-item-page-field.component';
@@ -162,7 +161,6 @@ describe('FullItemPageComponent', () => {
ThemedLoadingComponent, ThemedLoadingComponent,
ThemedItemPageTitleFieldComponent, ThemedItemPageTitleFieldComponent,
DsoEditMenuComponent, DsoEditMenuComponent,
ViewTrackerComponent,
ThemedItemAlertsComponent, ThemedItemAlertsComponent,
CollectionsComponent, CollectionsComponent,
ThemedFullFileSectionComponent, ThemedFullFileSectionComponent,

View File

@@ -42,7 +42,6 @@ import { hasValue } from '../../shared/empty.util';
import { ErrorComponent } from '../../shared/error/error.component'; import { ErrorComponent } from '../../shared/error/error.component';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { ViewTrackerComponent } from '../../statistics/angulartics/dspace/view-tracker.component';
import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component'; import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component';
import { CollectionsComponent } from '../field-components/collections/collections.component'; import { CollectionsComponent } from '../field-components/collections/collections.component';
import { ThemedItemPageTitleFieldComponent } from '../simple/field-components/specific-field/title/themed-item-page-field.component'; import { ThemedItemPageTitleFieldComponent } from '../simple/field-components/specific-field/title/themed-item-page-field.component';
@@ -75,7 +74,6 @@ import { ThemedFullFileSectionComponent } from './field-components/file-section/
ThemedItemPageTitleFieldComponent, ThemedItemPageTitleFieldComponent,
DsoEditMenuComponent, DsoEditMenuComponent,
ItemVersionsNoticeComponent, ItemVersionsNoticeComponent,
ViewTrackerComponent,
ThemedItemAlertsComponent, ThemedItemAlertsComponent,
VarDirective, VarDirective,
], ],

View File

@@ -5,6 +5,7 @@ import { accessTokenResolver } from '../core/auth/access-token.resolver';
import { authenticatedGuard } from '../core/auth/authenticated.guard'; import { authenticatedGuard } from '../core/auth/authenticated.guard';
import { itemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver'; import { itemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
import { MenuRoute } from '../shared/menu/menu-route.model'; import { MenuRoute } from '../shared/menu/menu-route.model';
import { viewTrackerResolver } from '../statistics/angulartics/dspace/view-tracker.resolver';
import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/bitstream-request-a-copy-page.component'; import { BitstreamRequestACopyPageComponent } from './bitstreams/request-a-copy/bitstream-request-a-copy-page.component';
import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component'; import { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component'; import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
@@ -38,7 +39,9 @@ export const ROUTES: Route[] = [
data: { data: {
menuRoute: MenuRoute.ITEM_PAGE, menuRoute: MenuRoute.ITEM_PAGE,
}, },
resolve: {
tracking: viewTrackerResolver,
},
}, },
{ {
path: 'full', path: 'full',
@@ -46,7 +49,9 @@ export const ROUTES: Route[] = [
data: { data: {
menuRoute: MenuRoute.ITEM_PAGE, menuRoute: MenuRoute.ITEM_PAGE,
}, },
resolve: {
tracking: viewTrackerResolver,
},
}, },
{ {
path: ITEM_EDIT_PATH, path: ITEM_EDIT_PATH,

View File

@@ -1,16 +1,16 @@
<div class="container mb-5"> <div class="container mb-5">
<h1>{{'person.orcid.registry.auth' | translate}}</h1> <h1>{{'person.orcid.registry.auth' | translate}}</h1>
@if ((isLinkedToOrcid() | async)) { @if ((isOrcidLinked$ | async)) {
<div data-test="orcidLinked"> <div data-test="orcidLinked">
<div class="row"> <div class="row">
@if ((hasOrcidAuthorizations() | async)) { @if ((hasOrcidAuthorizations$ | async)) {
<div class="col-sm-6 mb-3" data-test="hasOrcidAuthorizations"> <div class="col-sm-6 mb-3" data-test="hasOrcidAuthorizations">
<div class="card h-100"> <div class="card h-100">
<div class="card-header">{{ 'person.page.orcid.granted-authorizations'| translate }}</div> <div class="card-header">{{ 'person.page.orcid.granted-authorizations'| translate }}</div>
<div class="card-body"> <div class="card-body">
<div class="container p-0"> <div class="container p-0">
<ul> <ul>
@for (auth of (getOrcidAuthorizations() | async); track auth) { @for (auth of (profileAuthorizationScopes$ | async); track auth) {
<li data-test="orcidAuthorization"> <li data-test="orcidAuthorization">
{{getAuthorizationDescription(auth) | translate}} {{getAuthorizationDescription(auth) | translate}}
</li> </li>
@@ -26,16 +26,16 @@
<div class="card-header">{{ 'person.page.orcid.missing-authorizations'| translate }}</div> <div class="card-header">{{ 'person.page.orcid.missing-authorizations'| translate }}</div>
<div class="card-body"> <div class="card-body">
<div class="container"> <div class="container">
@if ((hasMissingOrcidAuthorizations() | async) !== true) { @if ((hasMissingOrcidAuthorizations$ | async) !== true) {
<ds-alert [type]="'alert-success'" data-test="noMissingOrcidAuthorizations"> <ds-alert [type]="AlertType.Success" data-test="noMissingOrcidAuthorizations">
{{'person.page.orcid.no-missing-authorizations-message' | translate}} {{'person.page.orcid.no-missing-authorizations-message' | translate}}
</ds-alert> </ds-alert>
} }
@if ((hasMissingOrcidAuthorizations() | async)) { @if ((hasMissingOrcidAuthorizations$ | async)) {
<ds-alert [type]="'alert-warning'" data-test="missingOrcidAuthorizations"> <ds-alert [type]="AlertType.Warning" data-test="missingOrcidAuthorizations">
{{'person.page.orcid.missing-authorizations-message' | translate}} {{'person.page.orcid.missing-authorizations-message' | translate}}
<ul> <ul>
@for (auth of (getMissingOrcidAuthorizations() | async); track auth) { @for (auth of (profileAuthorizationScopes$ | async); track auth) {
<li data-test="missingOrcidAuthorization"> <li data-test="missingOrcidAuthorization">
{{getAuthorizationDescription(auth) | translate }} {{getAuthorizationDescription(auth) | translate }}
</li> </li>
@@ -48,13 +48,13 @@
</div> </div>
</div> </div>
</div> </div>
@if ((onlyAdminCanDisconnectProfileFromOrcid() | async) && (ownerCanDisconnectProfileFromOrcid() | async) !== true) { @if ((onlyAdminCanDisconnectProfileFromOrcid$ | async) && (ownerCanDisconnectProfileFromOrcid$ | async) !== true) {
<ds-alert <ds-alert
[type]="'alert-warning'" data-test="unlinkOnlyAdmin"> [type]="AlertType.Warning" data-test="unlinkOnlyAdmin">
{{ 'person.page.orcid.remove-orcid-message' | translate}} {{ 'person.page.orcid.remove-orcid-message' | translate}}
</ds-alert> </ds-alert>
} }
@if ((ownerCanDisconnectProfileFromOrcid() | async)) { @if ((ownerCanDisconnectProfileFromOrcid$ | async)) {
<div class="row" data-test="unlinkOwner"> <div class="row" data-test="unlinkOwner">
<div class="col"> <div class="col">
<button type="submit" class="btn btn-danger float-end" (click)="unlinkOrcid()" <button type="submit" class="btn btn-danger float-end" (click)="unlinkOrcid()"
@@ -68,7 +68,7 @@
class='fas fa-circle-notch fa-spin'></i> {{'person.page.orcid.unlink.processing' | translate}}</span> class='fas fa-circle-notch fa-spin'></i> {{'person.page.orcid.unlink.processing' | translate}}</span>
} }
</button> </button>
@if ((hasMissingOrcidAuthorizations() | async)) { @if ((hasMissingOrcidAuthorizations$ | async)) {
<button type="submit" <button type="submit"
class="btn btn-primary float-end" (click)="linkOrcid()"> class="btn btn-primary float-end" (click)="linkOrcid()">
<span><i class="fas fa-check"></i> {{ 'person.page.orcid.grant-authorizations' | translate }}</span> <span><i class="fas fa-check"></i> {{ 'person.page.orcid.grant-authorizations' | translate }}</span>
@@ -83,7 +83,7 @@
<div class="row"> <div class="row">
<div class="col-2"><img alt="orcid-logo" src="../../../../assets/images/orcid.logo.icon.svg"/></div> <div class="col-2"><img alt="orcid-logo" src="../../../../assets/images/orcid.logo.icon.svg"/></div>
<div class="col"> <div class="col">
<ds-alert [type]="'alert-info'">{{ getOrcidNotLinkedMessage() | async }}</ds-alert> <ds-alert [type]="AlertType.Info">{{ getOrcidNotLinkedMessage() }}</ds-alert>
</div> </div>
</div> </div>
<div class="row"> <div class="row">
@@ -97,103 +97,3 @@
</div> </div>
} }
</div> </div>
<ng-template #orcidLinked>
<div data-test="orcidLinked">
<div class="row">
@if ((hasOrcidAuthorizations() | async)) {
<div class="col-sm-6 mb-3" data-test="hasOrcidAuthorizations">
<div class="card h-100">
<div class="card-header">{{ 'person.page.orcid.granted-authorizations'| translate }}</div>
<div class="card-body">
<div class="container p-0">
<ul>
@for (auth of (getOrcidAuthorizations() | async); track auth) {
<li data-test="orcidAuthorization">
{{getAuthorizationDescription(auth) | translate}}
</li>
}
</ul>
</div>
</div>
</div>
</div>
}
<div class="col-sm-6 mb-3">
<div class="card h-100">
<div class="card-header">{{ 'person.page.orcid.missing-authorizations'| translate }}</div>
<div class="card-body">
<div class="container">
@if ((hasMissingOrcidAuthorizations() | async) !== true) {
<ds-alert [type]="'alert-success'" data-test="noMissingOrcidAuthorizations">
{{'person.page.orcid.no-missing-authorizations-message' | translate}}
</ds-alert>
}
@if ((hasMissingOrcidAuthorizations() | async)) {
<ds-alert [type]="'alert-warning'" data-test="missingOrcidAuthorizations">
{{'person.page.orcid.missing-authorizations-message' | translate}}
<ul>
@for (auth of (getMissingOrcidAuthorizations() | async); track auth) {
<li data-test="missingOrcidAuthorization">
{{getAuthorizationDescription(auth) | translate }}
</li>
}
</ul>
</ds-alert>
}
</div>
</div>
</div>
</div>
</div>
@if ((onlyAdminCanDisconnectProfileFromOrcid() | async) && (ownerCanDisconnectProfileFromOrcid() | async) !== true) {
<ds-alert
[type]="'alert-warning'" data-test="unlinkOnlyAdmin">
{{ 'person.page.orcid.remove-orcid-message' | translate}}
</ds-alert>
}
@if ((ownerCanDisconnectProfileFromOrcid() | async)) {
<div class="row" data-test="unlinkOwner">
<div class="col">
<button type="submit" class="btn btn-danger float-end" (click)="unlinkOrcid()"
[dsBtnDisabled]="(unlinkProcessing | async)">
@if ((unlinkProcessing | async) !== true) {
<span><i
class="fas fa-unlink"></i> {{ 'person.page.orcid.unlink' | translate }}</span>
}
@if ((unlinkProcessing | async)) {
<span><i
class='fas fa-circle-notch fa-spin'></i> {{'person.page.orcid.unlink.processing' | translate}}</span>
}
</button>
@if ((hasMissingOrcidAuthorizations() | async)) {
<button type="submit"
class="btn btn-primary float-end" (click)="linkOrcid()">
<span><i class="fas fa-check"></i> {{ 'person.page.orcid.grant-authorizations' | translate }}</span>
</button>
}
</div>
</div>
}
</div>
</ng-template>
<ng-template #orcidNotLinked>
<div data-test="orcidNotLinked">
<div class="row">
<div class="col-2"><img alt="orcid-logo" src="../../../../assets/images/orcid.logo.icon.svg"/></div>
<div class="col">
<ds-alert [type]="'alert-info'">{{ getOrcidNotLinkedMessage() | async }}</ds-alert>
</div>
</div>
<div class="row">
<div class="col">
<button class="btn btn-primary float-end" (click)="linkOrcid()">
<i class="fas fa-link"></i>
{{'person.page.orcid.link' | translate}}
</button>
</div>
</div>
</div>
</ng-template>

View File

@@ -30,6 +30,7 @@ import {
import { Item } from '../../../core/shared/item.model'; import { Item } from '../../../core/shared/item.model';
import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { AlertComponent } from '../../../shared/alert/alert.component'; import { AlertComponent } from '../../../shared/alert/alert.component';
import { AlertType } from '../../../shared/alert/alert-type';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive'; import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { NotificationsService } from '../../../shared/notifications/notifications.service'; import { NotificationsService } from '../../../shared/notifications/notifications.service';
import { createFailedRemoteDataObjectFromError$ } from '../../../shared/remote-data.utils'; import { createFailedRemoteDataObjectFromError$ } from '../../../shared/remote-data.utils';
@@ -56,43 +57,49 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
/** /**
* The list of exposed orcid authorization scopes for the orcid profile * The list of exposed orcid authorization scopes for the orcid profile
*/ */
profileAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]); profileAuthorizationScopes$: BehaviorSubject<string[]> = new BehaviorSubject([]);
hasOrcidAuthorizations$: Observable<boolean>;
/** /**
* The list of all orcid authorization scopes missing in the orcid profile * The list of all orcid authorization scopes missing in the orcid profile
*/ */
missingAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]); missingAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject([]);
hasMissingOrcidAuthorizations$: Observable<boolean>;
/** /**
* The list of all orcid authorization scopes available * The list of all orcid authorization scopes available
*/ */
orcidAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]); orcidAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject([]);
/** /**
* A boolean representing if unlink operation is processing * A boolean representing if unlink operation is processing
*/ */
unlinkProcessing: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); unlinkProcessing: BehaviorSubject<boolean> = new BehaviorSubject(false);
/** /**
* A boolean representing if orcid profile is linked * A boolean representing if orcid profile is linked
*/ */
private isOrcidLinked$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); isOrcidLinked$: BehaviorSubject<boolean> = new BehaviorSubject(false);
/** /**
* A boolean representing if only admin can disconnect orcid profile * A boolean representing if only admin can disconnect orcid profile
*/ */
private onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject(false);
/** /**
* A boolean representing if owner can disconnect orcid profile * A boolean representing if owner can disconnect orcid profile
*/ */
private ownerCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false); ownerCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject(false);
/** /**
* An event emitted when orcid profile is unliked successfully * An event emitted when orcid profile is unliked successfully
*/ */
@Output() unlink: EventEmitter<void> = new EventEmitter<void>(); @Output() unlink: EventEmitter<void> = new EventEmitter<void>();
readonly AlertType = AlertType;
constructor( constructor(
private orcidAuthService: OrcidAuthService, private orcidAuthService: OrcidAuthService,
private translateService: TranslateService, private translateService: TranslateService,
@@ -106,6 +113,8 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
this.orcidAuthorizationScopes.next(scopes); this.orcidAuthorizationScopes.next(scopes);
this.initOrcidAuthSettings(); this.initOrcidAuthSettings();
}); });
this.hasOrcidAuthorizations$ = this.hasOrcidAuthorizations();
this.hasMissingOrcidAuthorizations$ = this.hasMissingOrcidAuthorizations();
} }
ngOnChanges(changes: SimpleChanges): void { ngOnChanges(changes: SimpleChanges): void {
@@ -118,18 +127,11 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
* Check if the list of exposed orcid authorization scopes for the orcid profile has values * Check if the list of exposed orcid authorization scopes for the orcid profile has values
*/ */
hasOrcidAuthorizations(): Observable<boolean> { hasOrcidAuthorizations(): Observable<boolean> {
return this.profileAuthorizationScopes.asObservable().pipe( return this.profileAuthorizationScopes$.pipe(
map((scopes: string[]) => scopes.length > 0), map((scopes: string[]) => scopes.length > 0),
); );
} }
/**
* Return the list of exposed orcid authorization scopes for the orcid profile
*/
getOrcidAuthorizations(): Observable<string[]> {
return this.profileAuthorizationScopes.asObservable();
}
/** /**
* Check if the list of exposed orcid authorization scopes for the orcid profile has values * Check if the list of exposed orcid authorization scopes for the orcid profile has values
*/ */
@@ -139,26 +141,12 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
); );
} }
/** getOrcidNotLinkedMessage(): string {
* Return the list of exposed orcid authorization scopes for the orcid profile
*/
getMissingOrcidAuthorizations(): Observable<string[]> {
return this.profileAuthorizationScopes.asObservable();
}
/**
* Return a boolean representing if orcid profile is linked
*/
isLinkedToOrcid(): Observable<boolean> {
return this.isOrcidLinked$.asObservable();
}
getOrcidNotLinkedMessage(): Observable<string> {
const orcid = this.item.firstMetadataValue('person.identifier.orcid'); const orcid = this.item.firstMetadataValue('person.identifier.orcid');
if (orcid) { if (orcid) {
return this.translateService.get('person.page.orcid.orcid-not-linked-message', { 'orcid': orcid }); return this.translateService.instant('person.page.orcid.orcid-not-linked-message', { 'orcid': orcid });
} else { } else {
return this.translateService.get('person.page.orcid.no-orcid-message'); return this.translateService.instant('person.page.orcid.no-orcid-message');
} }
} }
@@ -171,13 +159,6 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
return 'person.page.orcid.scope.' + scope.substring(1).replace('/', '-'); return 'person.page.orcid.scope.' + scope.substring(1).replace('/', '-');
} }
/**
* Return a boolean representing if only admin can disconnect orcid profile
*/
onlyAdminCanDisconnectProfileFromOrcid(): Observable<boolean> {
return this.onlyAdminCanDisconnectProfileFromOrcid$.asObservable();
}
/** /**
* Return a boolean representing if owner can disconnect orcid profile * Return a boolean representing if owner can disconnect orcid profile
*/ */
@@ -243,7 +224,7 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
} }
private setOrcidAuthorizationsFromItem(): void { private setOrcidAuthorizationsFromItem(): void {
this.profileAuthorizationScopes.next(this.orcidAuthService.getOrcidAuthorizationScopesByItem(this.item)); this.profileAuthorizationScopes$.next(this.orcidAuthService.getOrcidAuthorizationScopesByItem(this.item));
} }
} }

View File

@@ -5,16 +5,16 @@
<div class="container"> <div class="container">
<h2>{{ 'person.orcid.registry.queue' | translate }}</h2> <h2>{{ 'person.orcid.registry.queue' | translate }}</h2>
@if ((processing$ | async) !== true && (getList() | async)?.payload?.totalElements === 0) { @if ((processing$ | async) !== true && (list$ | async)?.payload?.totalElements === 0) {
<ds-alert <ds-alert
[type]="AlertTypeEnum.Info"> [type]="AlertTypeEnum.Info">
{{ 'person.page.orcid.sync-queue.empty-message' | translate}} {{ 'person.page.orcid.sync-queue.empty-message' | translate}}
</ds-alert> </ds-alert>
} }
@if ((processing$ | async) !== true && (getList() | async)?.payload?.totalElements > 0) { @if ((processing$ | async) !== true && (list$ | async)?.payload?.totalElements > 0) {
<ds-pagination <ds-pagination
[paginationOptions]="paginationOptions" [paginationOptions]="paginationOptions"
[collectionSize]="(getList() | async)?.payload?.totalElements" [collectionSize]="(list$ | async)?.payload?.totalElements"
[retainScrollPosition]="false" [hideGear]="true" (paginationChange)="updateList()"> [retainScrollPosition]="false" [hideGear]="true" (paginationChange)="updateList()">
<div class="table-responsive"> <div class="table-responsive">
<table id="groups" class="table table-sm table-striped table-hover table-bordered"> <table id="groups" class="table table-sm table-striped table-hover table-bordered">
@@ -26,7 +26,7 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@for (entry of (getList() | async)?.payload?.page; track entry) { @for (entry of (list$ | async)?.payload?.page; track entry) {
<tr data-test="orcidQueueElementRow"> <tr data-test="orcidQueueElementRow">
<td style="width: 15%" class="text-center align-middle"> <td style="width: 15%" class="text-center align-middle">
<i [ngClass]="getIconClass(entry)" [ngbTooltip]="getIconTooltip(entry) | translate" <i [ngClass]="getIconClass(entry)" [ngbTooltip]="getIconTooltip(entry) | translate"

View File

@@ -80,13 +80,12 @@ export class OrcidQueueComponent implements OnInit, OnDestroy, OnChanges {
/** /**
* A list of orcid queue records * A list of orcid queue records
*/ */
private list$: BehaviorSubject<RemoteData<PaginatedList<OrcidQueue>>> = new BehaviorSubject<RemoteData<PaginatedList<OrcidQueue>>>({} as any); list$: BehaviorSubject<RemoteData<PaginatedList<OrcidQueue>>> = new BehaviorSubject<RemoteData<PaginatedList<OrcidQueue>>>({} as any);
/** /**
* The AlertType enumeration * The AlertType enumeration
* @type {AlertType}
*/ */
AlertTypeEnum = AlertType; readonly AlertTypeEnum = AlertType;
/** /**
* Array to track all subscriptions and unsubscribe them onDestroy * Array to track all subscriptions and unsubscribe them onDestroy
@@ -132,13 +131,6 @@ export class OrcidQueueComponent implements OnInit, OnDestroy, OnChanges {
); );
} }
/**
* Return the list of orcid queue records
*/
getList(): Observable<RemoteData<PaginatedList<OrcidQueue>>> {
return this.list$.asObservable();
}
/** /**
* Return the icon class for the queue object type * Return the icon class for the queue object type
* *

View File

@@ -9,7 +9,6 @@
<ds-qa-event-notification [item]="item"></ds-qa-event-notification> <ds-qa-event-notification [item]="item"></ds-qa-event-notification>
<ds-notify-requests-status [itemUuid]="item.uuid"></ds-notify-requests-status> <ds-notify-requests-status [itemUuid]="item.uuid"></ds-notify-requests-status>
<ds-item-versions-notice [item]="item"></ds-item-versions-notice> <ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker>
@if (!item.isWithdrawn || (isAdmin$|async)) { @if (!item.isWithdrawn || (isAdmin$|async)) {
<ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader> <ds-listable-object-component-loader [object]="item" [viewMode]="viewMode"></ds-listable-object-component-loader>
} }

View File

@@ -44,7 +44,6 @@ import {
import { ActivatedRouteStub } from '../../shared/testing/active-router.stub'; import { ActivatedRouteStub } from '../../shared/testing/active-router.stub';
import { createPaginatedList } from '../../shared/testing/utils.test'; import { createPaginatedList } from '../../shared/testing/utils.test';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { ViewTrackerComponent } from '../../statistics/angulartics/dspace/view-tracker.component';
import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component'; import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component';
import { ItemVersionsComponent } from '../versions/item-versions.component'; import { ItemVersionsComponent } from '../versions/item-versions.component';
import { ItemVersionsNoticeComponent } from '../versions/notice/item-versions-notice.component'; import { ItemVersionsNoticeComponent } from '../versions/notice/item-versions-notice.component';
@@ -142,7 +141,6 @@ describe('ItemPageComponent', () => {
remove: { imports: [ remove: { imports: [
ThemedItemAlertsComponent, ThemedItemAlertsComponent,
ItemVersionsNoticeComponent, ItemVersionsNoticeComponent,
ViewTrackerComponent,
ListableObjectComponentLoaderComponent, ListableObjectComponentLoaderComponent,
ItemVersionsComponent, ItemVersionsComponent,
ErrorComponent, ErrorComponent,

View File

@@ -51,7 +51,6 @@ import { ErrorComponent } from '../../shared/error/error.component';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component'; import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { ListableObjectComponentLoaderComponent } from '../../shared/object-collection/shared/listable-object/listable-object-component-loader.component'; import { ListableObjectComponentLoaderComponent } from '../../shared/object-collection/shared/listable-object/listable-object-component-loader.component';
import { VarDirective } from '../../shared/utils/var.directive'; import { VarDirective } from '../../shared/utils/var.directive';
import { ViewTrackerComponent } from '../../statistics/angulartics/dspace/view-tracker.component';
import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component'; import { ThemedItemAlertsComponent } from '../alerts/themed-item-alerts.component';
import { getItemPageRoute } from '../item-page-routing-paths'; import { getItemPageRoute } from '../item-page-routing-paths';
import { ItemVersionsComponent } from '../versions/item-versions.component'; import { ItemVersionsComponent } from '../versions/item-versions.component';
@@ -76,7 +75,6 @@ import { QaEventNotificationComponent } from './qa-event-notification/qa-event-n
VarDirective, VarDirective,
ThemedItemAlertsComponent, ThemedItemAlertsComponent,
ItemVersionsNoticeComponent, ItemVersionsNoticeComponent,
ViewTrackerComponent,
ListableObjectComponentLoaderComponent, ListableObjectComponentLoaderComponent,
ItemVersionsComponent, ItemVersionsComponent,
ErrorComponent, ErrorComponent,

View File

@@ -116,7 +116,7 @@
</ds-geospatial-item-page-field> </ds-geospatial-item-page-field>
} }
<div> <div>
<a class="btn btn-outline-primary" role="button" [routerLink]="[itemPageRoute + '/full']"> <a class="btn btn-outline-primary" role="button" [routerLink]="[itemPageRoute + '/full']" role="button" tabindex="0">
<i class="fas fa-info-circle"></i> {{"item.page.link.full" | translate}} <i class="fas fa-info-circle"></i> {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -104,7 +104,7 @@
<ds-item-page-cc-license-field [item]="object" [variant]="'full'"> <ds-item-page-cc-license-field [item]="object" [variant]="'full'">
</ds-item-page-cc-license-field> </ds-item-page-cc-license-field>
<div> <div>
<a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button"> <a class="btn btn-outline-primary" [routerLink]="[itemPageRoute + '/full']" role="button" role="button" tabindex="0">
<i class="fas fa-info-circle"></i> {{"item.page.link.full" | translate}} <i class="fas fa-info-circle"></i> {{"item.page.link.full" | translate}}
</a> </a>
</div> </div>

View File

@@ -3,6 +3,6 @@
<h2><small><em>{{missingItem}}</em></small></h2> <h2><small><em>{{missingItem}}</em></small></h2>
<br /> <br />
<p class="text-center"> <p class="text-center">
<a routerLink="/home" class="btn btn-primary">{{"404.link.home-page" | translate}}</a> <a routerLink="/home" class="btn btn-primary" role="button" tabindex="0">{{"404.link.home-page" | translate}}</a>
</p> </p>
</div> </div>

View File

@@ -1,5 +1,5 @@
<nav [ngClass]="{'open': (menuCollapsed | async) !== true}" <nav [ngClass]="{'open': (menuCollapsed | async) !== true}"
[@slideMobileNav]="(windowService.isXsOrSm() | async) !== true ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')" [@slideMobileNav]="(isMobile$ | async) !== true ? 'default' : ((menuCollapsed | async) ? 'collapsed' : 'expanded')"
class="navbar navbar-light navbar-expand-md px-md-0 navbar-container" role="navigation" class="navbar navbar-light navbar-expand-md px-md-0 navbar-container" role="navigation"
[attr.aria-label]="'nav.main.description' | translate" id="main-navbar"> [attr.aria-label]="'nav.main.description' | translate" id="main-navbar">
<!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed --> <!-- TODO remove navbar-container class when https://github.com/twbs/bootstrap/issues/24726 is fixed -->

View File

@@ -5,6 +5,6 @@
<p>{{"error-page." + code | translate}}</p> <p>{{"error-page." + code | translate}}</p>
<br/> <br/>
<p class="text-center"> <p class="text-center">
<a href="/home" class="btn btn-primary">{{ status + ".link.home-page" | translate}}</a> <a href="/home" class="btn btn-primary" role="link" tabindex="0">{{ status + ".link.home-page" | translate}}</a>
</p> </p>
</div> </div>

View File

@@ -5,6 +5,6 @@
<p>{{"404.help" | translate}}</p> <p>{{"404.help" | translate}}</p>
<br/> <br/>
<p class="text-center"> <p class="text-center">
<a routerLink="/home" class="btn btn-primary">{{"404.link.home-page" | translate}}</a> <a routerLink="/home" class="btn btn-primary" role="button" tabindex="0">{{"404.link.home-page" | translate}}</a>
</p> </p>
</div> </div>

View File

@@ -0,0 +1,11 @@
<input [attr.aria-label]="'process.new.parameter.label' | translate" required #integer="ngModel" type="number" step="1" name="integer-value-{{index}}" class="form-control" id="integer-value-{{index}}" [ngModel]="value" (ngModelChange)="setValue($event)"/>
@if (integer.invalid && (integer.dirty || integer.touched)) {
<div
class="alert alert-danger validation-error mb-0">
@if (integer.errors.required) {
<div>
{{'process.new.parameter.integer.required' | translate}}
</div>
}
</div>
}

View File

@@ -0,0 +1,5 @@
:host {
display: flex;
flex-direction: column;
gap: calc(var(--bs-spacer) / 2);
}

View File

@@ -0,0 +1,81 @@
import {
ComponentFixture,
fakeAsync,
TestBed,
tick,
waitForAsync,
} from '@angular/core/testing';
import { FormsModule } from '@angular/forms';
import { By } from '@angular/platform-browser';
import {
TranslateLoader,
TranslateModule,
} from '@ngx-translate/core';
import { TranslateLoaderMock } from '../../../../../shared/mocks/translate-loader.mock';
import { IntegerValueInputComponent } from './integer-value-input.component';
describe('IntegerValueInputComponent', () => {
let component: IntegerValueInputComponent;
let fixture: ComponentFixture<IntegerValueInputComponent>;
beforeEach(waitForAsync(() => {
TestBed.configureTestingModule({
imports: [
FormsModule,
TranslateModule.forRoot({
loader: {
provide: TranslateLoader,
useClass: TranslateLoaderMock,
},
}),
IntegerValueInputComponent,
],
providers: [],
})
.compileComponents();
}));
beforeEach(() => {
fixture = TestBed.createComponent(IntegerValueInputComponent);
component = fixture.componentInstance;
fixture.detectChanges();
});
it('should create', () => {
expect(component).toBeTruthy();
});
it('should not show a validation error if the input field was left untouched but left empty', () => {
const validationError = fixture.debugElement.query(By.css('.validation-error'));
expect(validationError).toBeFalsy();
});
it('should show a validation error if the input field was touched but left empty', fakeAsync(() => {
component.value = undefined;
fixture.detectChanges();
tick();
const input = fixture.debugElement.query(By.css('input'));
input.triggerEventHandler('blur', null);
fixture.detectChanges();
const validationError = fixture.debugElement.query(By.css('.validation-error'));
expect(validationError).toBeTruthy();
}));
it('should not show a validation error if the input field was touched but not left empty', fakeAsync(() => {
component.value = 1;
fixture.detectChanges();
tick();
const input = fixture.debugElement.query(By.css('input'));
input.triggerEventHandler('blur', null);
fixture.detectChanges();
const validationError = fixture.debugElement.query(By.css('.validation-error'));
expect(validationError).toBeFalsy();
}));
});

View File

@@ -0,0 +1,49 @@
import {
Component,
Input,
OnInit,
Optional,
} from '@angular/core';
import {
ControlContainer,
FormsModule,
NgForm,
} from '@angular/forms';
import { TranslateModule } from '@ngx-translate/core';
import { controlContainerFactory } from '../../../process-form-factory';
import { ValueInputComponent } from '../value-input.component';
/**
* Represents the user-inputted value of an integer parameter
*/
@Component({
selector: 'ds-integer-value-input',
templateUrl: './integer-value-input.component.html',
styleUrls: ['./integer-value-input.component.scss'],
viewProviders: [{ provide: ControlContainer,
useFactory: controlContainerFactory,
deps: [[new Optional(), NgForm]] }],
standalone: true,
imports: [FormsModule, TranslateModule],
})
export class IntegerValueInputComponent extends ValueInputComponent<number> implements OnInit {
/**
* The current value of the integer
*/
value: number;
/**
* Initial value of the field
*/
@Input() initialValue;
ngOnInit(): void {
this.value = this.initialValue;
}
setValue(value) {
this.value = value;
this.updateValue.emit(value);
}
}

View File

@@ -3,6 +3,9 @@
@case (parameterTypes.STRING) { @case (parameterTypes.STRING) {
<ds-string-value-input [initialValue]="initialValue" (updateValue)="updateValue.emit($event)" [index]="index"></ds-string-value-input> <ds-string-value-input [initialValue]="initialValue" (updateValue)="updateValue.emit($event)" [index]="index"></ds-string-value-input>
} }
@case (parameterTypes.INTEGER) {
<ds-integer-value-input [initialValue]="initialValue" (updateValue)="updateValue.emit($event)" [index]="index"></ds-integer-value-input>
}
@case (parameterTypes.OUTPUT) { @case (parameterTypes.OUTPUT) {
<ds-string-value-input [initialValue]="initialValue" (updateValue)="updateValue.emit($event)" [index]="index"></ds-string-value-input> <ds-string-value-input [initialValue]="initialValue" (updateValue)="updateValue.emit($event)" [index]="index"></ds-string-value-input>
} }

View File

@@ -17,6 +17,7 @@ import { controlContainerFactory } from '../../process-form-factory';
import { BooleanValueInputComponent } from './boolean-value-input/boolean-value-input.component'; import { BooleanValueInputComponent } from './boolean-value-input/boolean-value-input.component';
import { DateValueInputComponent } from './date-value-input/date-value-input.component'; import { DateValueInputComponent } from './date-value-input/date-value-input.component';
import { FileValueInputComponent } from './file-value-input/file-value-input.component'; import { FileValueInputComponent } from './file-value-input/file-value-input.component';
import { IntegerValueInputComponent } from './number-value-input/integer-value-input.component';
import { StringValueInputComponent } from './string-value-input/string-value-input.component'; import { StringValueInputComponent } from './string-value-input/string-value-input.component';
/** /**
@@ -30,7 +31,7 @@ import { StringValueInputComponent } from './string-value-input/string-value-inp
useFactory: controlContainerFactory, useFactory: controlContainerFactory,
deps: [[new Optional(), NgForm]] }], deps: [[new Optional(), NgForm]] }],
standalone: true, standalone: true,
imports: [StringValueInputComponent, DateValueInputComponent, FileValueInputComponent, BooleanValueInputComponent], imports: [StringValueInputComponent, DateValueInputComponent, FileValueInputComponent, BooleanValueInputComponent, IntegerValueInputComponent],
}) })
export class ParameterValueInputComponent { export class ParameterValueInputComponent {
@Input() index: number; @Input() index: number;

View File

@@ -62,20 +62,20 @@ class="fas fa-plus pe-2"></i>{{'process.overview.new' | translate}}</button>
</div> </div>
<div class="modal-body"> <div class="modal-body">
@if ((processBulkDeleteService.isProcessing$() | async) !== true) { @let isProcessing = (isProcessing$ | async);
<div>{{'process.overview.delete.body' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</div> @if (isProcessing) {
}
@if (processBulkDeleteService.isProcessing$() |async) {
<div class="alert alert-info"> <div class="alert alert-info">
<span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span> <span class="spinner-border spinner-border-sm spinner-button" role="status" aria-hidden="true"></span>
<span> {{ 'process.overview.delete.processing' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</span> <span> {{ 'process.overview.delete.processing' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</span>
</div> </div>
} @else {
<div>{{'process.overview.delete.body' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</div>
} }
<div class="mt-4"> <div class="mt-4">
<button class="btn btn-primary me-2" [dsBtnDisabled]="processBulkDeleteService.isProcessing$() |async" <button class="btn btn-primary me-2" [dsBtnDisabled]="isProcessing"
(click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button> (click)="closeModal()">{{'process.detail.delete.cancel' | translate}}</button>
<button id="delete-confirm" class="btn btn-danger" <button id="delete-confirm" class="btn btn-danger"
[dsBtnDisabled]="processBulkDeleteService.isProcessing$() |async" [dsBtnDisabled]="isProcessing"
(click)="deleteSelected()">{{ 'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }} (click)="deleteSelected()">{{ 'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
</button> </button>
</div> </div>

View File

@@ -12,7 +12,10 @@ import {
import { RouterLink } from '@angular/router'; import { RouterLink } from '@angular/router';
import { NgbModal } from '@ng-bootstrap/ng-bootstrap'; import { NgbModal } from '@ng-bootstrap/ng-bootstrap';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { Subscription } from 'rxjs'; import {
Observable,
Subscription,
} from 'rxjs';
import { BtnDisabledDirective } from '../../shared/btn-disabled.directive'; import { BtnDisabledDirective } from '../../shared/btn-disabled.directive';
import { hasValue } from '../../shared/empty.util'; import { hasValue } from '../../shared/empty.util';
@@ -45,6 +48,8 @@ export class ProcessOverviewComponent implements OnInit, OnDestroy {
isProcessingSub: Subscription; isProcessingSub: Subscription;
isProcessing$: Observable<boolean>;
constructor(protected processOverviewService: ProcessOverviewService, constructor(protected processOverviewService: ProcessOverviewService,
protected modalService: NgbModal, protected modalService: NgbModal,
public processBulkDeleteService: ProcessBulkDeleteService, public processBulkDeleteService: ProcessBulkDeleteService,
@@ -53,6 +58,7 @@ export class ProcessOverviewComponent implements OnInit, OnDestroy {
ngOnInit(): void { ngOnInit(): void {
this.processBulkDeleteService.clearAllProcesses(); this.processBulkDeleteService.clearAllProcesses();
this.isProcessing$ = this.processBulkDeleteService.isProcessing$();
} }
ngOnDestroy(): void { ngOnDestroy(): void {

View File

@@ -3,6 +3,7 @@
*/ */
export enum ScriptParameterType { export enum ScriptParameterType {
STRING = 'String', STRING = 'String',
INTEGER = 'Integer',
DATE = 'date', DATE = 'date',
BOOLEAN = 'boolean', BOOLEAN = 'boolean',
FILE = 'InputStream', FILE = 'InputStream',

View File

@@ -17,15 +17,15 @@
</div> </div>
} }
@if (!researcherProfile) { @if (!researcherProfile) {
@let processingCreate = (processingCreate$ | async);
<button class="btn btn-primary me-2" <button class="btn btn-primary me-2"
[dsBtnDisabled]="(isProcessingCreate() | async)" [dsBtnDisabled]="processingCreate"
(click)="createProfile()"> (click)="createProfile()">
@if ((isProcessingCreate() | async)) { @if (processingCreate) {
<span> <span>
<i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}} <i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}}
</span> </span>
} } @else {
@if ((isProcessingCreate() | async) !== true) {
<span> <span>
<i class="fas fa-plus"></i> &nbsp;{{'researcher.profile.create.new' | translate}} <i class="fas fa-plus"></i> &nbsp;{{'researcher.profile.create.new' | translate}}
</span> </span>
@@ -37,12 +37,11 @@
<i class="fas fa-info-circle"></i> {{'researcher.profile.view' | translate}} <i class="fas fa-info-circle"></i> {{'researcher.profile.view' | translate}}
</button> </button>
<button class="btn btn-danger" [dsBtnDisabled]="!researcherProfile" (click)="deleteProfile(researcherProfile)"> <button class="btn btn-danger" [dsBtnDisabled]="!researcherProfile" (click)="deleteProfile(researcherProfile)">
@if ((isProcessingDelete() | async)) { @if ((processingDelete$ | async)) {
<span> <span>
<i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}} <i class='fas fa-circle-notch fa-spin'></i> {{'researcher.profile.action.processing' | translate}}
</span> </span>
} } @else {
@if ((isProcessingDelete() | async) !== true) {
<span> <span>
<i class="fas fa-trash-alt"></i> &nbsp;{{'researcher.profile.delete' | translate}} <i class="fas fa-trash-alt"></i> &nbsp;{{'researcher.profile.delete' | translate}}
</span> </span>

View File

@@ -11,10 +11,7 @@ import {
TranslateService, TranslateService,
} from '@ngx-translate/core'; } from '@ngx-translate/core';
import { UiSwitchModule } from 'ngx-ui-switch'; import { UiSwitchModule } from 'ngx-ui-switch';
import { import { BehaviorSubject } from 'rxjs';
BehaviorSubject,
Observable,
} from 'rxjs';
import { import {
map, map,
mergeMap, mergeMap,
@@ -187,24 +184,6 @@ export class ProfilePageResearcherFormComponent implements OnInit {
}); });
} }
/**
* Return a boolean representing if a delete operation is pending.
*
* @return {Observable<boolean>}
*/
isProcessingDelete(): Observable<boolean> {
return this.processingDelete$.asObservable();
}
/**
* Return a boolean representing if a create operation is pending.
*
* @return {Observable<boolean>}
*/
isProcessingCreate(): Observable<boolean> {
return this.processingCreate$.asObservable();
}
/** /**
* Create a new profile related to the current user from scratch. * Create a new profile related to the current user from scratch.
*/ */

View File

@@ -54,15 +54,15 @@
</ds-alert> </ds-alert>
} }
@if (isRecaptchaCookieAccepted() && (googleRecaptchaService.captchaVersion() | async) === 'v2') { @if (isRecaptchaCookieAccepted() && (captchaVersion$ | async) === 'v2') {
<div class="my-3"> <div class="my-3">
<ds-google-recaptcha [captchaMode]="(googleRecaptchaService.captchaMode() | async)" <ds-google-recaptcha [captchaMode]="(captchaMode$ | async)"
(executeRecaptcha)="register($event)" (checkboxChecked)="onCheckboxChecked($event)" (executeRecaptcha)="register($event)" (checkboxChecked)="onCheckboxChecked($event)"
(showNotification)="showNotification($event)"></ds-google-recaptcha> (showNotification)="showNotification($event)"></ds-google-recaptcha>
</div> </div>
} }
@if ((!registrationVerification || ((googleRecaptchaService.captchaVersion() | async) !== 'v2' && (googleRecaptchaService.captchaMode() | async) === 'invisible'))) { @if ((!registrationVerification || ((captchaVersion$ | async) !== 'v2' && (captchaMode$ | async) === 'invisible'))) {
<button class="btn btn-primary" [dsBtnDisabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()"> <button class="btn btn-primary" [dsBtnDisabled]="form.invalid || registrationVerification && !isRecaptchaCookieAccepted() || disableUntilChecked" (click)="register()">
{{ MESSAGE_PREFIX + '.submit' | translate }} {{ MESSAGE_PREFIX + '.submit' | translate }}
</button> </button>

View File

@@ -107,13 +107,9 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit {
subscriptions: Subscription[] = []; subscriptions: Subscription[] = [];
captchaVersion(): Observable<string> { captchaVersion$: Observable<string>;
return this.googleRecaptchaService.captchaVersion();
}
captchaMode(): Observable<string> { captchaMode$: Observable<string>;
return this.googleRecaptchaService.captchaMode();
}
constructor( constructor(
private epersonRegistrationService: EpersonRegistrationService, private epersonRegistrationService: EpersonRegistrationService,
@@ -135,6 +131,8 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit {
} }
ngOnInit(): void { ngOnInit(): void {
this.captchaVersion$ = this.googleRecaptchaService.captchaVersion();
this.captchaMode$ = this.googleRecaptchaService.captchaMode();
const validators: ValidatorFn[] = [ const validators: ValidatorFn[] = [
Validators.required, Validators.required,
Validators.email, Validators.email,
@@ -191,7 +189,7 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit {
register(tokenV2?) { register(tokenV2?) {
if (!this.form.invalid) { if (!this.form.invalid) {
if (this.registrationVerification) { if (this.registrationVerification) {
this.subscriptions.push(combineLatest([this.captchaVersion(), this.captchaMode()]).pipe( this.subscriptions.push(combineLatest([this.captchaVersion$, this.captchaMode$]).pipe(
switchMap(([captchaVersion, captchaMode]) => { switchMap(([captchaVersion, captchaMode]) => {
if (captchaVersion === 'v3') { if (captchaVersion === 'v3') {
return this.googleRecaptchaService.getRecaptchaToken('register_email'); return this.googleRecaptchaService.getRecaptchaToken('register_email');
@@ -254,7 +252,7 @@ export class RegisterEmailFormComponent implements OnDestroy, OnInit {
*/ */
disableUntilCheckedFcn(): Observable<boolean> { disableUntilCheckedFcn(): Observable<boolean> {
const checked$ = this.checkboxCheckedSubject$.asObservable(); const checked$ = this.checkboxCheckedSubject$.asObservable();
return combineLatest([this.captchaVersion(), this.captchaMode(), checked$]).pipe( return combineLatest([this.captchaVersion$, this.captchaMode$, checked$]).pipe(
// disable if checkbox is not checked or if reCaptcha is not in v2 checkbox mode // disable if checkbox is not checked or if reCaptcha is not in v2 checkbox mode
switchMap(([captchaVersion, captchaMode, checked]) => captchaVersion === 'v2' && captchaMode === 'checkbox' ? of(!checked) : of(false)), switchMap(([captchaVersion, captchaMode, checked]) => captchaVersion === 'v2' && captchaMode === 'checkbox' ? of(!checked) : of(false)),
startWith(true), startWith(true),

View File

@@ -42,7 +42,7 @@
<div class="processed-message"> <div class="processed-message">
<p>{{ 'grant-deny-request-copy.processed' | translate }}</p> <p>{{ 'grant-deny-request-copy.processed' | translate }}</p>
<p class="text-center"> <p class="text-center">
<a routerLink="/home" class="btn btn-primary">{{ 'grant-deny-request-copy.home-page' | translate }}</a> <a routerLink="/home" class="btn btn-primary" role="button" tabindex="0">{{ 'grant-deny-request-copy.home-page' | translate }}</a>
</p> </p>
</div> </div>
} }

View File

@@ -7,7 +7,7 @@
[class.display]="searchExpanded ? 'inline-block' : 'none'" [class.display]="searchExpanded ? 'inline-block' : 'none'"
[tabIndex]="searchExpanded ? 0 : -1" [tabIndex]="searchExpanded ? 0 : -1"
[attr.data-test]="'header-search-box' | dsBrowserOnly"> [attr.data-test]="'header-search-box' | dsBrowserOnly">
<button class="submit-icon btn btn-link btn-link-inline" [attr.aria-label]="'nav.search.button' | translate" type="button" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()" [attr.data-test]="'header-search-icon' | dsBrowserOnly"> <button class="submit-icon btn btn-link btn-link-inline" [attr.aria-label]="'nav.search.button' | translate" type="button" (click)="searchExpanded ? onSubmit(searchForm.value) : expand()" [attr.data-test]="'header-search-icon' | dsBrowserOnly" tabindex="0" role="button">
<em class="fas fa-search fa-lg fa-fw"></em> <em class="fas fa-search fa-lg fa-fw"></em>
</button> </button>
</form> </form>

View File

@@ -8,6 +8,7 @@
<a href="javascript:void(0);" class="dropdownLogin px-0.5" [attr.aria-label]="'nav.login' |translate" <a href="javascript:void(0);" class="dropdownLogin px-0.5" [attr.aria-label]="'nav.login' |translate"
(click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly" (click)="$event.preventDefault()" [attr.data-test]="'login-menu' | dsBrowserOnly"
role="menuitem" role="menuitem"
tabindex="0"
aria-haspopup="menu" aria-haspopup="menu"
aria-controls="loginDropdownMenu" aria-controls="loginDropdownMenu"
[attr.aria-expanded]="loginDrop.isOpen()" [attr.aria-expanded]="loginDrop.isOpen()"
@@ -26,6 +27,7 @@
<div ngbDropdown #loggedInDrop="ngbDropdown" display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut> <div ngbDropdown #loggedInDrop="ngbDropdown" display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="javascript:void(0);" <a href="javascript:void(0);"
role="menuitem" role="menuitem"
tabindex="0"
[attr.aria-label]="'nav.user-profile-menu-and-logout' | translate" [attr.aria-label]="'nav.user-profile-menu-and-logout' | translate"
aria-controls="user-menu-dropdown" aria-controls="user-menu-dropdown"
(click)="$event.preventDefault()" [title]="'nav.user-profile-menu-and-logout' | translate" (click)="$event.preventDefault()" [title]="'nav.user-profile-menu-and-logout' | translate"
@@ -43,12 +45,12 @@
} @else { } @else {
<div data-test="auth-nav"> <div data-test="auth-nav">
@if ((isAuthenticated | async) !== true) { @if ((isAuthenticated | async) !== true) {
<a routerLink="/login" routerLinkActive="active" class="loginLink px-0.5" role="button"> <a routerLink="/login" routerLinkActive="active" class="loginLink px-0.5" role="button" tabindex="0">
{{ 'nav.login' | translate }}<span class="sr-only">(current)</span> {{ 'nav.login' | translate }}<span class="sr-only">(current)</span>
</a> </a>
} }
@if ((isAuthenticated | async)) { @if ((isAuthenticated | async)) {
<a role="button" [attr.aria-label]="'nav.logout' |translate" [title]="'nav.logout' | translate" routerLink="/logout" routerLinkActive="active" class="logoutLink px-1"> <a role="button" [attr.aria-label]="'nav.logout' |translate" [title]="'nav.logout' | translate" routerLink="/logout" routerLinkActive="active" class="logoutLink px-1" role="button" tabindex="0">
<i class="fas fa-sign-out-alt fa-lg fa-fw"></i> <i class="fas fa-sign-out-alt fa-lg fa-fw"></i>
<span class="sr-only">(current)</span> <span class="sr-only">(current)</span>
</a> </a>

View File

@@ -10,7 +10,9 @@
role="tab" role="tab"
[routerLink]="option.routerLink" [routerLink]="option.routerLink"
[queryParams]="option.params" [queryParams]="option.params"
[class.active]="(currentOption$ | async)?.id === option.id"> [class.active]="(currentOption$ | async)?.id === option.id"
role="tab"
tabindex="0">
{{ option.label | translate }} {{ option.label | translate }}
</a> </a>
} }

View File

@@ -3,6 +3,6 @@
@if (title) { @if (title) {
<span class="mb-0">{{ title | translate }}</span> <span class="mb-0">{{ title | translate }}</span>
} }
<a [href]="getHandle()">{{getHandle()}}</a> <a [href]="getHandle()" role="link" tabindex="0">{{getHandle()}}</a>
</p> </p>
} }

View File

@@ -433,4 +433,31 @@ describe('BrowserOrejimeService', () => {
expect(service.orejimeConfig.apps).not.toContain(jasmine.objectContaining({ name: googleAnalytics })); expect(service.orejimeConfig.apps).not.toContain(jasmine.objectContaining({ name: googleAnalytics }));
}); });
}); });
describe('applyUpdateSettingsCallbackToApps', () => {
let user2: EPerson;
let mockApp1, mockApp2;
let updateSettingsSpy;
beforeEach(() => {
user2 = Object.assign(new EPerson(), { uuid: 'test-user' });
mockApp1 = { name: 'app1', callback: jasmine.createSpy('originalCallback1') };
mockApp2 = { name: 'app2', callback: jasmine.createSpy('originalCallback2') };
service.orejimeConfig.apps = [mockApp1, mockApp2];
updateSettingsSpy = spyOn(service, 'updateSettingsForUsers');
});
it('calls updateSettingsForUsers in a debounced manner when a callback is triggered', (done) => {
service.applyUpdateSettingsCallbackToApps(user2);
mockApp1.callback(true);
mockApp2.callback(false);
setTimeout(() => {
expect(updateSettingsSpy).toHaveBeenCalledTimes(1);
expect(updateSettingsSpy).toHaveBeenCalledWith(user2);
done();
}, 400);
});
});
}); });

View File

@@ -192,12 +192,39 @@ export class BrowserOrejimeService extends OrejimeService {
this.translateConfiguration(); this.translateConfiguration();
this.orejimeConfig.apps = this.filterConfigApps(appsToHide); this.orejimeConfig.apps = this.filterConfigApps(appsToHide);
this.lazyOrejime.then(({ init }) => {
this.applyUpdateSettingsCallbackToApps(user);
void this.lazyOrejime.then(({ init }) => {
this.orejimeInstance = init(this.orejimeConfig); this.orejimeInstance = init(this.orejimeConfig);
}); });
}); });
} }
/**
* Applies a debounced callback to update user settings for all apps in the Orejime configuration.
*
* This method modifies the `callback` property of each app in the `orejimeConfig.apps` array.
* It ensures that the `updateSettingsForUsers` method is called in a debounced manner whenever
* a consent change occurs for any app. Additionally, it preserves and invokes the original
* callback for each app if one is defined.
*
* @param {EPerson} user - The authenticated user whose settings are being updated.
*/
applyUpdateSettingsCallbackToApps(user: EPerson) {
const updateSettingsCallback = debounce(() => this.updateSettingsForUsers(user), updateDebounce);
this.orejimeConfig.apps.forEach((app) => {
const originalCallback = app.callback;
app.callback = (consent: boolean) => {
updateSettingsCallback();
if (originalCallback) {
originalCallback(consent);
}
};
});
}
/** /**
* Return saved preferences stored in the orejime cookie * Return saved preferences stored in the orejime cookie
*/ */
@@ -220,7 +247,6 @@ export class BrowserOrejimeService extends OrejimeService {
* @param user The authenticated user * @param user The authenticated user
*/ */
private initializeUser(user: EPerson) { private initializeUser(user: EPerson) {
this.orejimeConfig.callback = debounce((consent, app) => this.updateSettingsForUsers(user), updateDebounce);
this.orejimeConfig.cookieName = this.getStorageName(user.uuid); this.orejimeConfig.cookieName = this.getStorageName(user.uuid);
const anonCookie = this.cookieService.get(ANONYMOUS_STORAGE_NAME_OREJIME); const anonCookie = this.cookieService.get(ANONYMOUS_STORAGE_NAME_OREJIME);
@@ -387,8 +413,10 @@ export class BrowserOrejimeService extends OrejimeService {
* @param user * @param user
*/ */
updateSettingsForUsers(user: EPerson) { updateSettingsForUsers(user: EPerson) {
if (user) {
this.setSettingsForUser(user, this.cookieService.get(this.getStorageName(user.uuid))); this.setSettingsForUser(user, this.cookieService.get(this.getStorageName(user.uuid)));
} }
}
/** /**
* Create the storage name for orejime cookies based on the user's identifier * Create the storage name for orejime cookies based on the user's identifier

View File

@@ -1,15 +1,15 @@
<div class="mt-3" @fadeInOut> <div class="mt-3" @fadeInOut>
@if (isListOfEPerson) { @if (isListOfEPerson) {
<ds-eperson-search-box (search)="onSearch($event)"></ds-eperson-search-box> <ds-eperson-search-box (search)="onSearch($event)"></ds-eperson-search-box>
} } @else {
@if (!isListOfEPerson) {
<ds-group-search-box (search)="onSearch($event)"></ds-group-search-box> <ds-group-search-box (search)="onSearch($event)"></ds-group-search-box>
} }
@if ((getList() | async)?.payload?.totalElements > 0) { @let list = (list$ | async);
@if (list && list.totalElements > 0) {
<ds-pagination <ds-pagination
[paginationOptions]="paginationOptions" [paginationOptions]="paginationOptions"
[collectionSize]="(getList() | async)?.payload?.totalElements" [collectionSize]="list.totalElements"
[retainScrollPosition]="true" [retainScrollPosition]="true"
[hideGear]="true"> [hideGear]="true">
<div class="table-responsive"> <div class="table-responsive">
@@ -22,11 +22,10 @@
</tr> </tr>
</thead> </thead>
<tbody> <tbody>
@for (entry of (getList() | async)?.payload?.page; track entry) { @for (entry of list.page; track entry) {
<tr <tr [class.table-primary]="(entrySelectedId$ | async) === entry.id">
[class.table-primary]="isSelected(entry) | async"> <td>{{ entry.id }}</td>
<td>{{entry.id}}</td> <td>{{ dsoNameService.getName(entry) }}</td>
<td>{{dsoNameService.getName(entry)}}</td>
<td class="text-center"> <td class="text-center">
<button class="btn btn-sm btn-outline-primary" (click)="emitSelect(entry)"> <button class="btn btn-sm btn-outline-primary" (click)="emitSelect(entry)">
{{'resource-policies.form.eperson-group-list.select.btn' | translate}} {{'resource-policies.form.eperson-group-list.select.btn' | translate}}

View File

@@ -13,7 +13,7 @@ import {
} from '@angular/core/testing'; } from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations'; import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core'; import { TranslateModule } from '@ngx-translate/core';
import { cold } from 'jasmine-marbles'; import { hot } from 'jasmine-marbles';
import uniqueId from 'lodash/uniqueId'; import uniqueId from 'lodash/uniqueId';
import { of as observableOf } from 'rxjs'; import { of as observableOf } from 'rxjs';
@@ -49,14 +49,13 @@ const mockDataServiceMap: LazyDataServicesMap = new Map([
[GROUP.value, () => import('../../core/eperson/group-data.service').then(m => m.GroupDataService)], [GROUP.value, () => import('../../core/eperson/group-data.service').then(m => m.GroupDataService)],
]); ]);
describe('EpersonGroupListComponent test suite', () => { describe('EpersonGroupListComponent', () => {
let comp: EpersonGroupListComponent; let comp: EpersonGroupListComponent;
let compAsAny: any; let compAsAny: any;
let fixture: ComponentFixture<EpersonGroupListComponent>; let fixture: ComponentFixture<EpersonGroupListComponent>;
let de;
let groupService: any; let groupService: any;
let epersonService: any; let epersonService: any;
let paginationService; let paginationService: PaginationServiceStub;
const paginationOptions: PaginationComponentOptions = new PaginationComponentOptions(); const paginationOptions: PaginationComponentOptions = new PaginationComponentOptions();
paginationOptions.id = uniqueId('eperson-group-list-pagination-test'); paginationOptions.id = uniqueId('eperson-group-list-pagination-test');
@@ -129,7 +128,6 @@ describe('EpersonGroupListComponent test suite', () => {
})); }));
describe('', () => { describe('', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>; let testFixture: ComponentFixture<TestComponent>;
// synchronous beforeEach // synchronous beforeEach
@@ -139,7 +137,6 @@ describe('EpersonGroupListComponent test suite', () => {
<ds-eperson-group-list [isListOfEPerson]="isListOfEPerson" [initSelected]="initSelected"></ds-eperson-group-list>`; <ds-eperson-group-list [isListOfEPerson]="isListOfEPerson" [initSelected]="initSelected"></ds-eperson-group-list>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>; testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
}); });
afterEach(() => { afterEach(() => {
@@ -167,7 +164,6 @@ describe('EpersonGroupListComponent test suite', () => {
afterEach(() => { afterEach(() => {
comp = null; comp = null;
compAsAny = null; compAsAny = null;
de = null;
fixture.destroy(); fixture.destroy();
}); });
@@ -181,29 +177,25 @@ describe('EpersonGroupListComponent test suite', () => {
}); });
})); }));
it('should init entrySelectedId', fakeAsync(() => { it('should init entrySelectedId', fakeAsync(async () => {
spyOn(comp, 'updateList'); spyOn(comp, 'updateList');
comp.initSelected = EPersonMock.id; comp.initSelected = EPersonMock.id;
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { await fixture.whenStable();
expect(compAsAny.entrySelectedId.value).toBe(EPersonMock.id); expect(comp.entrySelectedId$.value).toBe(EPersonMock.id);
});
})); }));
it('should init the list of eperson', fakeAsync(() => { it('should init the list of eperson', fakeAsync(async () => {
epersonService.searchByScope.and.returnValue(observableOf(epersonPaginatedListRD)); epersonService.searchByScope.and.returnValue(observableOf(epersonPaginatedListRD));
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { await fixture.whenStable();
expect(compAsAny.list$.value).toEqual(epersonPaginatedListRD); expect(comp.list$).toBeObservable(hot('(a|)', {
expect(comp.getList()).toBeObservable(cold('a', { a: epersonPaginatedList,
a: epersonPaginatedListRD,
})); }));
});
})); }));
it('should emit select event', () => { it('should emit select event', () => {
@@ -211,23 +203,13 @@ describe('EpersonGroupListComponent test suite', () => {
comp.emitSelect(EPersonMock); comp.emitSelect(EPersonMock);
expect(comp.select.emit).toHaveBeenCalled(); expect(comp.select.emit).toHaveBeenCalled();
expect(compAsAny.entrySelectedId.value).toBe(EPersonMock.id); expect(comp.entrySelectedId$.value).toBe(EPersonMock.id);
}); });
it('should return true when entry is selected', () => { it('should return the entrySelectedId$ value', () => {
compAsAny.entrySelectedId.next(EPersonMock.id); comp.entrySelectedId$.next(EPersonMock.id);
expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', { expect(comp.entrySelectedId$.value).toBe(EPersonMock.id);
a: true,
}));
});
it('should return false when entry is not selected', () => {
compAsAny.entrySelectedId.next('');
expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', {
a: false,
}));
}); });
}); });
@@ -245,7 +227,6 @@ describe('EpersonGroupListComponent test suite', () => {
afterEach(() => { afterEach(() => {
comp = null; comp = null;
compAsAny = null; compAsAny = null;
de = null;
fixture.destroy(); fixture.destroy();
}); });
@@ -260,27 +241,24 @@ describe('EpersonGroupListComponent test suite', () => {
})); }));
it('should init entrySelectedId', fakeAsync(() => { it('should init entrySelectedId', fakeAsync(async () => {
spyOn(comp, 'updateList'); spyOn(comp, 'updateList');
comp.initSelected = GroupMock.id; comp.initSelected = GroupMock.id;
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { await fixture.whenStable();
expect(compAsAny.entrySelectedId.value).toBe(GroupMock.id); expect(comp.entrySelectedId$.value).toBe(GroupMock.id);
});
})); }));
it('should init the list of group', fakeAsync(() => { it('should init the list of group', fakeAsync(async () => {
groupService.searchGroups.and.returnValue(observableOf(groupPaginatedListRD)); groupService.searchGroups.and.returnValue(observableOf(groupPaginatedListRD));
fixture.detectChanges(); fixture.detectChanges();
fixture.whenStable().then(() => { await fixture.whenStable();
expect(compAsAny.list$.value).toEqual(groupPaginatedListRD); expect(comp.list$).toBeObservable(hot('(a|)', {
expect(comp.getList()).toBeObservable(cold('a', { a: groupPaginatedList,
a: groupPaginatedListRD,
})); }));
});
})); }));
it('should emit select event', () => { it('should emit select event', () => {
@@ -288,27 +266,16 @@ describe('EpersonGroupListComponent test suite', () => {
comp.emitSelect(GroupMock); comp.emitSelect(GroupMock);
expect(comp.select.emit).toHaveBeenCalled(); expect(comp.select.emit).toHaveBeenCalled();
expect(compAsAny.entrySelectedId.value).toBe(GroupMock.id); expect(comp.entrySelectedId$.value).toBe(GroupMock.id);
}); });
it('should return true when entry is selected', () => { it('should return the entrySelectedId$ value', () => {
compAsAny.entrySelectedId.next(EPersonMock.id); comp.entrySelectedId$.next(GroupMock.id);
expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', { expect(comp.entrySelectedId$.value).toBe(GroupMock.id);
a: true,
}));
});
it('should return false when entry is not selected', () => {
compAsAny.entrySelectedId.next('');
expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', {
a: false,
}));
}); });
it('should update list on search triggered', () => { it('should update list on search triggered', () => {
const options: PaginationComponentOptions = comp.paginationOptions;
const event: SearchEvent = { const event: SearchEvent = {
scope: 'metadata', scope: 'metadata',
query: 'test', query: 'test',
@@ -316,7 +283,7 @@ describe('EpersonGroupListComponent test suite', () => {
spyOn(comp, 'updateList'); spyOn(comp, 'updateList');
comp.onSearch(event); comp.onSearch(event);
expect(compAsAny.updateList).toHaveBeenCalledWith('metadata', 'test'); expect(comp.updateList).toHaveBeenCalledWith('metadata', 'test');
}); });
}); });
}); });

View File

@@ -14,9 +14,8 @@ import uniqueId from 'lodash/uniqueId';
import { import {
BehaviorSubject, BehaviorSubject,
Observable, Observable,
Subscription,
} from 'rxjs'; } from 'rxjs';
import { map } from 'rxjs/operators'; import { switchMap } from 'rxjs/operators';
import { import {
APP_DATA_SERVICES_MAP, APP_DATA_SERVICES_MAP,
@@ -25,21 +24,21 @@ import {
import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { FindListOptions } from '../../core/data/find-list-options.model'; import { FindListOptions } from '../../core/data/find-list-options.model';
import { PaginatedList } from '../../core/data/paginated-list.model'; import { PaginatedList } from '../../core/data/paginated-list.model';
import { RemoteData } from '../../core/data/remote-data';
import { EPersonDataService } from '../../core/eperson/eperson-data.service'; import { EPersonDataService } from '../../core/eperson/eperson-data.service';
import { GroupDataService } from '../../core/eperson/group-data.service'; import { GroupDataService } from '../../core/eperson/group-data.service';
import { EPerson } from '../../core/eperson/models/eperson.model';
import { EPERSON } from '../../core/eperson/models/eperson.resource-type'; import { EPERSON } from '../../core/eperson/models/eperson.resource-type';
import { Group } from '../../core/eperson/models/group.model';
import { GROUP } from '../../core/eperson/models/group.resource-type'; import { GROUP } from '../../core/eperson/models/group.resource-type';
import { lazyDataService } from '../../core/lazy-data-service'; import { lazyDataService } from '../../core/lazy-data-service';
import { PaginationService } from '../../core/pagination/pagination.service'; import { PaginationService } from '../../core/pagination/pagination.service';
import { DSpaceObject } from '../../core/shared/dspace-object.model'; import { DSpaceObject } from '../../core/shared/dspace-object.model';
import { getFirstCompletedRemoteData } from '../../core/shared/operators'; import {
getFirstCompletedRemoteData,
getRemoteDataPayload,
} from '../../core/shared/operators';
import { ResourceType } from '../../core/shared/resource-type'; import { ResourceType } from '../../core/shared/resource-type';
import { fadeInOut } from '../animations/fade'; import { fadeInOut } from '../animations/fade';
import {
hasValue,
isNotEmpty,
} from '../empty.util';
import { PaginationComponent } from '../pagination/pagination.component'; import { PaginationComponent } from '../pagination/pagination.component';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model'; import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
import { SearchEvent } from './eperson-group-list-event-type'; import { SearchEvent } from './eperson-group-list-event-type';
@@ -101,21 +100,13 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy {
/** /**
* A list of eperson or group * A list of eperson or group
*/ */
private list$: BehaviorSubject<RemoteData<PaginatedList<DSpaceObject>>> = new BehaviorSubject<RemoteData<PaginatedList<DSpaceObject>>>({} as any); list$: Observable<PaginatedList<EPerson | Group>>;
/** /**
* The eperson or group's id selected * The eperson or group's id selected
* @type {string} * @type {string}
*/ */
private entrySelectedId: BehaviorSubject<string> = new BehaviorSubject<string>(''); entrySelectedId$: BehaviorSubject<string> = new BehaviorSubject('');
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
private subs: Subscription[] = [];
private pageConfigSub: Subscription;
/** /**
* Initialize instance variables and inject the properly UpdateDataServiceImpl * Initialize instance variables and inject the properly UpdateDataServiceImpl
@@ -143,7 +134,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy {
this.paginationOptions.pageSize = 5; this.paginationOptions.pageSize = 5;
if (this.initSelected) { if (this.initSelected) {
this.entrySelectedId.next(this.initSelected); this.entrySelectedId$.next(this.initSelected);
} }
this.updateList(this.currentSearchScope, this.currentSearchQuery); this.updateList(this.currentSearchScope, this.currentSearchQuery);
@@ -158,28 +149,9 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy {
*/ */
emitSelect(entry: DSpaceObject): void { emitSelect(entry: DSpaceObject): void {
this.select.emit(entry); this.select.emit(entry);
this.entrySelectedId.next(entry.id); this.entrySelectedId$.next(entry.id);
} }
/**
* Return the list of eperson or group
*/
getList(): Observable<RemoteData<PaginatedList<DSpaceObject>>> {
return this.list$.asObservable();
}
/**
* Return a boolean representing if a table row is selected
*
* @return {boolean}
*/
isSelected(entry: DSpaceObject): Observable<boolean> {
return this.entrySelectedId.asObservable().pipe(
map((selectedId) => isNotEmpty(selectedId) && selectedId === entry.id),
);
}
/** /**
* Method called on search * Method called on search
*/ */
@@ -194,38 +166,26 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy {
* Retrieve a paginate list of eperson or group * Retrieve a paginate list of eperson or group
*/ */
updateList(scope: string, query: string): void { updateList(scope: string, query: string): void {
if (hasValue(this.pageConfigSub)) { this.list$ = this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions).pipe(
this.pageConfigSub.unsubscribe(); switchMap((paginationOptions) => {
} const options: FindListOptions = Object.assign(new FindListOptions(), {
this.pageConfigSub = this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions)
.subscribe((paginationOptions) => {
const options: FindListOptions = Object.assign({}, new FindListOptions(), {
elementsPerPage: paginationOptions.pageSize, elementsPerPage: paginationOptions.pageSize,
currentPage: paginationOptions.currentPage, currentPage: paginationOptions.currentPage,
}); });
const search$: Observable<RemoteData<PaginatedList<DSpaceObject>>> = this.isListOfEPerson ? return this.isListOfEPerson ?
(this.dataService as EPersonDataService).searchByScope(scope, query, options) : (this.dataService as EPersonDataService).searchByScope(scope, query, options) :
(this.dataService as GroupDataService).searchGroups(query, options); (this.dataService as GroupDataService).searchGroups(query, options);
this.subs.push(search$.pipe(getFirstCompletedRemoteData())
.subscribe((list: RemoteData<PaginatedList<DSpaceObject>>) => {
if (hasValue(this.list$)) {
this.list$.next(list);
}
}), }),
getFirstCompletedRemoteData(),
getRemoteDataPayload(),
); );
});
} }
/** /**
* Unsubscribe from all subscriptions * Unsubscribe from all subscriptions
*/ */
ngOnDestroy(): void { ngOnDestroy(): void {
this.list$ = null;
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
this.paginationService.clearPagination(this.paginationOptions.id); this.paginationService.clearPagination(this.paginationOptions.id);
} }

Some files were not shown because too many files have changed in this diff Show More