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-simple-import-sort",
"eslint-plugin-import-newlines",
"eslint-plugin-jsonc",
"dspace-angular-ts",
"dspace-angular-html"
],
@@ -303,10 +302,13 @@
"*.json5"
],
"extends": [
"plugin:jsonc/recommended-with-jsonc"
"plugin:jsonc/recommended-with-json5"
],
"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",
"jsonc/comma-dangle": [
"error",

View File

@@ -93,7 +93,10 @@ services:
volumes:
# Keep Solr data directory between reboots
- 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:
- /bin/bash
- '-c'
@@ -111,7 +114,8 @@ services:
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
exec solr -f
chown -R solr:solr /var/solr
runuser -u solr -- solr-foreground
volumes:
assetstore:
pgdata:

View File

@@ -97,11 +97,16 @@ services:
volumes:
# Keep Solr data directory between reboots
- 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
# * 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:
# 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`
# * Third, ensure all new folders are owned by "solr" user
# * Finally, start Solr as the "solr" user via the provided solr-foreground script
entrypoint:
- /bin/bash
- '-c'
@@ -119,7 +124,8 @@ services:
cp -r /opt/solr/server/solr/configsets/qaevent/* qaevent
precreate-core suggestion /opt/solr/server/solr/configsets/suggestion
cp -r /opt/solr/server/solr/configsets/suggestion/* suggestion
exec solr -f
chown -R solr:solr /var/solr
runuser -u solr -- solr-foreground
volumes:
assetstore:
pgdata:

195
package-lock.json generated
View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -19,7 +19,7 @@ import {
TranslateModule,
TranslateService,
} from '@ngx-translate/core';
import { combineLatest } from 'rxjs';
import { Observable } from 'rxjs';
import { take } from 'rxjs/operators';
import { MetadataField } from '../../../../core/metadata/metadata-field.model';
@@ -109,6 +109,8 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
*/
@Output() submitForm: EventEmitter<any> = new EventEmitter();
activeMetadataField$: Observable<MetadataField>;
constructor(public registryService: RegistryService,
private formBuilderService: FormBuilderService,
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
*/
ngOnInit() {
combineLatest([
this.translateService.get(`${this.messagePrefix}.element`),
this.translateService.get(`${this.messagePrefix}.qualifier`),
this.translateService.get(`${this.messagePrefix}.scopenote`),
]).subscribe(([element, qualifier, scopenote]) => {
ngOnInit(): void {
this.activeMetadataField$ = this.registryService.getActiveMetadataField();
this.element = new DynamicInputModel({
id: 'element',
label: element,
label: this.translateService.instant(`${this.messagePrefix}.element`),
name: 'element',
validators: {
required: null,
@@ -140,7 +138,7 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
});
this.qualifier = new DynamicInputModel({
id: 'qualifier',
label: qualifier,
label: this.translateService.instant(`${this.messagePrefix}.qualifier`),
name: 'qualifier',
validators: {
pattern: '^[^. ,]*$',
@@ -154,14 +152,13 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
});
this.scopeNote = new DynamicTextAreaModel({
id: 'scopeNote',
label: scopenote,
label: this.translateService.instant(`${this.messagePrefix}.scopenote`),
name: 'scopeNote',
required: false,
rows: 5,
});
this.formModel = [
new DynamicFormGroupModel(
{
new DynamicFormGroupModel({
id: 'metadatadatafieldgroup',
group:[this.element, this.qualifier, this.scopeNote],
}),
@@ -182,7 +179,6 @@ export class MetadataFieldFormComponent implements OnInit, OnDestroy {
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 { ServerCheckGuard } from './core/server-check/server-check.guard';
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 { provideSuggestionNotificationsState } from './notifications/provide-suggestion-notifications-state';
import { ThemedPageErrorComponent } from './page-error/themed-page-error.component';
import { ThemedPageInternalServerErrorComponent } from './page-internal-server-error/themed-page-internal-server-error.component';
import { ThemedPageNotFoundComponent } from './pagenotfound/themed-pagenotfound.component';
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 { SUGGESTION_MODULE_PATH } from './suggestions-page/suggestions-page-routing-paths';
@@ -61,9 +63,17 @@ export const APP_ROUTES: Route[] = [
path: 'home',
loadChildren: () => import('./home-page/home-page-routes')
.then((m) => m.ROUTES),
data: { showBreadcrumbs: false, enableRSS: true },
data: {
showBreadcrumbs: false,
enableRSS: true,
dsoPath: 'site',
},
providers: [provideSuggestionNotificationsState()],
canActivate: [endUserAgreementCurrentUserGuard],
resolve: {
site: homePageResolver,
tracking: viewTrackerResolver,
},
},
{
path: 'community-list',
@@ -99,14 +109,12 @@ export const APP_ROUTES: Route[] = [
path: COMMUNITY_MODULE_PATH,
loadChildren: () => import('./community-page/community-page-routes')
.then((m) => m.ROUTES),
data: { enableRSS: true },
canActivate: [endUserAgreementCurrentUserGuard],
},
{
path: COLLECTION_MODULE_PATH,
loadChildren: () => import('./collection-page/collection-page-routes')
.then((m) => m.ROUTES),
data: { enableRSS: true },
canActivate: [endUserAgreementCurrentUserGuard],
},
{

View File

@@ -12,7 +12,7 @@
}
<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 #activeBreadcrumb let-text="text">

View File

@@ -20,6 +20,8 @@
<a class="btn btn-primary"
[routerLink]="['/search']"
[queryParams]="queryParams"
[queryParamsHandling]="'merge'">
[queryParamsHandling]="'merge'"
role="link"
tabindex="0">
{{ 'browse.taxonomy.button' | translate }}</a>
</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 { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component';
import { MenuRoute } from '../shared/menu/menu-route.model';
import { viewTrackerResolver } from '../statistics/angulartics/dspace/view-tracker.resolver';
import { collectionPageResolver } from './collection-page.resolver';
import { collectionPageAdministratorGuard } from './collection-page-administrator.guard';
import {
@@ -99,6 +100,7 @@ export const ROUTES: Route[] = [
data: {
breadcrumbKey: 'collection.search',
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>
@if (collectionRD?.payload; as collection) {
<div>
<ds-view-tracker [object]="collection"></ds-view-tracker>
<div class="d-flex flex-row border-bottom mb-4 pb-4">
<header class="comcol-header me-auto">
<!-- 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 { PaginationComponentOptions } from '../shared/pagination/pagination-component-options.model';
import { VarDirective } from '../shared/utils/var.directive';
import { ViewTrackerComponent } from '../statistics/angulartics/dspace/view-tracker.component';
import { getCollectionPageRoute } from './collection-page-routing-paths';
@Component({
@@ -64,7 +63,6 @@ import { getCollectionPageRoute } from './collection-page-routing-paths';
ErrorComponent,
ThemedLoadingComponent,
TranslateModule,
ViewTrackerComponent,
VarDirective,
AsyncPipe,
ComcolPageHeaderComponent,

View File

@@ -12,7 +12,7 @@
<div class="align-middle my-auto">
@if ((dataSource.loading$ | async) !== true) {
<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 }}
</button>
}
@@ -34,7 +34,11 @@
<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) }"
(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'}}"
aria-hidden="true"></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">
<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>
@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>
@@ -88,7 +92,7 @@
<span class="fa fa-chevron-right"></span>
</span>
<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>
</div>
<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 { ComcolSearchSectionComponent } from '../shared/comcol/sections/comcol-search-section/comcol-search-section.component';
import { MenuRoute } from '../shared/menu/menu-route.model';
import { viewTrackerResolver } from '../statistics/angulartics/dspace/view-tracker.resolver';
import { communityPageResolver } from './community-page.resolver';
import { communityPageAdministratorGuard } from './community-page-administrator.guard';
import {
@@ -70,6 +71,9 @@ export const ROUTES: Route[] = [
data: {
menuRoute: MenuRoute.COMMUNITY_PAGE,
},
resolve: {
tracking: viewTrackerResolver,
},
children: [
{
path: '',
@@ -86,6 +90,7 @@ export const ROUTES: Route[] = [
data: {
breadcrumbKey: 'community.search',
menuRoute: MenuRoute.COMMUNITY_PAGE,
enableRSS: true,
},
},
{

View File

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

View File

@@ -38,7 +38,6 @@ import { hasValue } from '../shared/empty.util';
import { ErrorComponent } from '../shared/error/error.component';
import { ThemedLoadingComponent } from '../shared/loading/themed-loading.component';
import { VarDirective } from '../shared/utils/var.directive';
import { ViewTrackerComponent } from '../statistics/angulartics/dspace/view-tracker.component';
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 { 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,
ComcolPageHeaderComponent,
AsyncPipe,
ViewTrackerComponent,
VarDirective,
RouterOutlet,
RouterModule,

View File

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

View File

@@ -163,12 +163,25 @@ export class BitstreamDataService extends IdentifiableDataService<Bitstream> imp
sendRequest(this.requestService),
take(1),
).subscribe(() => {
this.requestService.removeByHrefSubstring(bitstream.self + '/format');
this.deleteFormatCache(bitstream);
});
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
* optional sequenceId or filename, with a list of {@link FollowLinkConfig}, to automatically

View File

@@ -31,6 +31,7 @@ import {
import {
EPersonMock,
EPersonMock2,
EPersonMockWithNoName,
} from '../../shared/testing/eperson.mock';
import { GroupMock } from '../../shared/testing/group-mock';
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', () => {
beforeEach(() => {
spyOn(halService, 'getEndpoint').and.callFake((linkPath: string) => {

View File

@@ -269,7 +269,8 @@ export class EPersonDataService extends IdentifiableDataService<EPerson> impleme
* @param newEPerson
*/
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) {
operations = [...operations, {
op: 'replace', path: '/email', value: newEPerson.email,

View File

@@ -1,34 +1,26 @@
import { CommonModule } from '@angular/common';
import { Component } from '@angular/core';
import { TestBed } from '@angular/core/testing';
import {
Router,
UrlTree,
} from '@angular/router';
import { RouterTestingModule } from '@angular/router/testing';
import { RouterModule } from '@angular/router';
import { Angulartics2 } from 'angulartics2';
import {
combineLatest as observableCombineLatest,
Observable,
of as observableOf,
} from 'rxjs';
import { map } from 'rxjs/operators';
import { of as observableOf } from 'rxjs';
import { TestScheduler } from 'rxjs/testing';
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 { 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 { SearchFilterConfig } from '../../../shared/search/models/search-filter-config.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 { 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 { CommunityDataService } from '../../data/community-data.service';
import { DSpaceObjectDataService } from '../../data/dspace-object-data.service';
import { RemoteData } from '../../data/remote-data';
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 { RouteService } from '../../services/route.service';
import { HALEndpointService } from '../hal-endpoint.service';
@@ -36,7 +28,8 @@ import { ViewMode } from '../view-mode.model';
import { SearchService } from './search.service';
import { SearchConfigurationService } from './search-configuration.service';
import anything = jasmine.anything;
import SpyObj = jasmine.SpyObj;
import { Component } from '@angular/core';
@Component({
template: '',
@@ -47,94 +40,38 @@ class DummyComponent {
}
describe('SearchService', () => {
describe('By default', () => {
let searchService: SearchService;
const router = new RouterStub();
const route = new ActivatedRouteStub();
const searchConfigService = { paginationID: 'page-id' };
let service: SearchService;
let halService: HALEndpointServiceStub;
let paginationService: PaginationServiceStub;
let remoteDataBuildService: RemoteDataBuildService;
let requestService: SpyObj<RequestService>;
let routeService: RouteService;
let searchConfigService: SearchConfigurationServiceStub;
let testScheduler: TestScheduler;
let msToLive: number;
let remoteDataTimestamp: number;
beforeEach(() => {
halService = new HALEndpointServiceStub(environment.rest.baseUrl);
paginationService = new PaginationServiceStub();
remoteDataBuildService = getMockRemoteDataBuildService();
requestService = getMockRequestService();
searchConfigService = new SearchConfigurationServiceStub();
initTestData();
TestBed.configureTestingModule({
imports: [
CommonModule,
RouterTestingModule.withRoutes([
{ path: 'search', component: DummyComponent, pathMatch: 'full' },
]),
DummyComponent,
RouterModule.forRoot([]),
],
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: RequestService, useValue: requestService },
{ provide: RemoteDataBuildService, useValue: remoteDataBuildService },
{ provide: HALEndpointService, useValue: halService },
{ provide: CommunityDataService, useValue: {} },
{ provide: DSpaceObjectDataService, useValue: {} },
{ provide: PaginationService, useValue: paginationService },
{ provide: SearchConfigurationService, useValue: searchConfigService },
@@ -142,84 +79,263 @@ describe('SearchService', () => {
SearchService,
],
});
searchService = TestBed.inject(SearchService);
service = TestBed.inject(SearchService);
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', () => {
searchService.setViewMode(ViewMode.ListElement);
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], { page: 1 }, { view: ViewMode.ListElement },
);
service.setViewMode(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', () => {
searchService.setViewMode(ViewMode.GridElement);
expect(paginationService.updateRouteWithUrl).toHaveBeenCalledWith('page-id', ['/search'], { page: 1 }, { view: ViewMode.GridElement },
);
service.setViewMode(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', () => {
let viewMode = ViewMode.GridElement;
testScheduler.run(({ expectObservable }) => {
spyOn(routeService, 'getQueryParamMap').and.returnValue(observableOf(new Map([
['view', ViewMode.ListElement],
])));
searchService.getViewMode().subscribe((mode) => viewMode = mode);
expect(viewMode).toEqual(ViewMode.ListElement);
expectObservable(service.getViewMode()).toBe('(a|)', {
a: ViewMode.ListElement,
});
});
});
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([
['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', () => {
const endPoint = 'http://endpoint.com/test/test';
const searchOptions = new PaginatedSearchOptions({});
describe('search', () => {
let remoteDataMocks: Record<string, RemoteData<SearchObjects<any>>>;
beforeEach(() => {
spyOn((searchService as any).halService, 'getEndpoint').and.returnValue(observableOf(endPoint));
spyOn((searchService as any).rdb, 'buildFromHref').and.callThrough();
/* eslint-disable no-empty,@typescript-eslint/no-empty-function */
searchService.search(searchOptions).subscribe((t) => {
}); // subscribe to make sure all methods are called
/* eslint-enable no-empty,@typescript-eslint/no-empty-function */
remoteDataMocks = {
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 SearchObjects(), 200),
SuccessStale: new RemoteData(remoteDataTimestamp, msToLive, remoteDataTimestamp, RequestEntryState.SuccessStale, undefined, new SearchObjects(), 200),
};
});
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', () => {
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', () => {
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', () => {
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', () => {
it('should add the encoded filterQuery to the args list', () => {
jasmine.getEnv().allowRespy(true);
const spyRequest = spyOn((searchService as any), 'request').and.stub();
spyOn(requestService, 'send').and.returnValue(true);
const searchFilterConfig = new SearchFilterConfig();
searchFilterConfig._links = {
describe('getFacetValuesFor', () => {
let remoteDataMocks: Record<string, RemoteData<FacetValues>>;
let filterConfig: SearchFilterConfig;
beforeEach(() => {
remoteDataMocks = {
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: {
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 {
distinctUntilChanged,
map,
skipWhile,
switchMap,
take,
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>>> {
const href$ = this.getEndpoint(searchOptions);
let startTime: number;
href$.pipe(
take(1),
map((href: string) => {
@@ -191,6 +193,7 @@ export class SearchService {
searchOptions: searchOptions,
});
startTime = new Date().getTime();
this.requestService.send(request, useCachedVersionIfAvailable);
});
@@ -198,7 +201,13 @@ export class SearchService {
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;
},
});
const startTime = new Date().getTime();
this.requestService.send(request, useCachedVersionIfAvailable);
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>) => {
if (facetValuesRD.hasSucceeded) {
const appliedFilters: AppliedFilter[] = (facetValuesRD.payload.appliedFilters ?? [])

View File

@@ -7,7 +7,7 @@
<a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail>
@@ -48,7 +48,7 @@
<div class="text-center">
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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>

View File

@@ -7,7 +7,7 @@
<a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail>
@@ -48,7 +48,7 @@
<div class="text-center">
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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>

View File

@@ -7,7 +7,7 @@
<a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail>
@@ -54,7 +54,7 @@
<div class="text-center">
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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>

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) {
<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>
</a>
@@ -26,7 +26,7 @@
<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"
[innerHTML]="dsoTitle"></a>
[innerHTML]="dsoTitle" role="link" tabindex="0"></a>
}
@if (linkType === linkTypes.None) {
<span

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) {
<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>
</a>
@@ -26,7 +26,7 @@
<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"
[innerHTML]="dsoTitle"></a>
[innerHTML]="dsoTitle" role="link" tabindex="0"></a>
}
@if (linkType === linkTypes.None) {
<span

View File

@@ -3,7 +3,7 @@
<div class="col-3 col-md-2">
@if (linkType !== linkTypes.None) {
<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>
</a>
@@ -24,7 +24,7 @@
@if (linkType !== linkTypes.None) {
<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"
[innerHTML]="dsoTitle"></a>
[innerHTML]="dsoTitle" role="link" tabindex="0"></a>
}
@if (linkType === linkTypes.None) {
<span

View File

@@ -53,7 +53,7 @@
[label]="'journalissue.page.keyword'">
</ds-generic-item-page-field>
<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}}
</a>
</div>

View File

@@ -36,7 +36,7 @@
[label]="'journalvolume.page.description'">
</ds-generic-item-page-field>
<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}}
</a>
</div>

View File

@@ -35,7 +35,7 @@
[label]="'journal.page.description'">
</ds-generic-item-page-field>
<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}}
</a>
</div>

View File

@@ -7,7 +7,7 @@
<a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail>
@@ -56,7 +56,7 @@
<div class="text-center">
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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>

View File

@@ -7,7 +7,7 @@
<a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail>
@@ -47,7 +47,7 @@
<div class="text-center">
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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>

View File

@@ -7,7 +7,7 @@
<a
[target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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>
<ds-thumbnail [thumbnail]="dso?.thumbnail | async" [limitWidth]="false">
</ds-thumbnail>
@@ -40,7 +40,7 @@
<div class="text-center">
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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>

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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"
[defaultImage]="'assets/images/orgunit-placeholder.svg'"
[alt]="'thumbnail.orgunit.alt'"
@@ -32,7 +32,7 @@
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[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) {
<span

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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"
[defaultImage]="'assets/images/person-placeholder.svg'"
[alt]="'thumbnail.person.alt'"
@@ -32,7 +32,7 @@
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[attr.rel]="(linkType === linkTypes.ExternalLink) ? 'noopener noreferrer' : null"
[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) {
<span

View File

@@ -4,7 +4,7 @@
@if (linkType !== linkTypes.None) {
<a [target]="(linkType === linkTypes.ExternalLink) ? '_blank' : '_self'"
[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"
[defaultImage]="'assets/images/project-placeholder.svg'"
[alt]="'thumbnail.project.alt'"
@@ -32,7 +32,7 @@
<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"
[innerHTML]="dsoTitle"></a>
[innerHTML]="dsoTitle" role="link" tabindex="0"></a>
}
@if (linkType === linkTypes.None) {
<span

View File

@@ -56,7 +56,7 @@
[label]="'orgunit.page.description'">
</ds-generic-item-page-field>
<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}}
</a>
</div>

View File

@@ -52,7 +52,7 @@
[label]="'person.page.name'">
</ds-generic-item-page-field>
<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}}
</a>
</div>

View File

@@ -64,7 +64,7 @@
[label]="'project.page.keyword'">
</ds-generic-item-page-field>
<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}}
</a>
</div>

View File

@@ -26,7 +26,7 @@
<h5 class="text-uppercase">Footer Content</h5>
<ul class="list-unstyled mb-0">
<li>
<a routerLink="./" class="">Suspendisse potenti</a>
<a routerLink="./" class="" role="link" tabindex="0">Suspendisse potenti</a>
</li>
</ul>
</div>
@@ -53,14 +53,14 @@
<div class="content-container">
<p class="m-0">
<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'} }}
<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>
<ul class="footer-info list-unstyled d-flex justify-content-center mb-0">
<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}}
</button>
</li>
@@ -71,26 +71,26 @@
@if (showPrivacyPolicy) {
<li>
<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>
}
@if (showEndUserAgreement) {
<li>
<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>
}
@if (showSendFeedback$ | async) {
<li>
<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>
}
</ul>
</div>
@if (coarLdnEnabled$ | async) {
<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" />
{{ 'footer.link.coar-notify-support' | translate }}
</a>

View File

@@ -1,7 +1,7 @@
<header>
<div class="container">
<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"/>
</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="d-flex flex-wrap">
<div>
@@ -14,7 +14,7 @@
<li>issue permanent urls and trustworthy identifiers, including optional integrations with handle.net and DataCite DOI</li>
</ul>
<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>
</div>
</div>

View File

@@ -2,7 +2,6 @@ import { Route } from '@angular/router';
import { LinkMenuItemModel } from '../shared/menu/menu-item/models/link.model';
import { MenuItemType } from '../shared/menu/menu-item-type.model';
import { homePageResolver } from './home-page.resolver';
import { ThemedHomePageComponent } from './themed-home-page.component';
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>
<ng-template #homeContent>
@if ((site$ | async); as site) {
<ds-view-tracker [object]="site"></ds-view-tracker>
}
<ds-search-form [inPlaceSearch]="false"
[searchPlaceholder]="'home.search-form.placeholder' | translate">
</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 { ThemedSearchFormComponent } from '../shared/search-form/themed-search-form.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 { ThemedHomeNewsComponent } from './home-news/themed-home-news.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'],
templateUrl: './home-page.component.html',
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 {

View File

@@ -9,7 +9,7 @@
</ds-listable-object-component-loader>
</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>
}
@if (itemRD?.hasFailed) {

View File

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

View File

@@ -65,9 +65,10 @@ export class TopLevelCommunityListComponent implements OnInit, OnDestroy {
pageId = 'tl';
/**
* The sorting configuration
* The sorting configuration for the community list itself, and the optional RSS feed button
*/
sortConfig: SortOptions;
rssSortConfig: SortOptions;
/**
* 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.currentPage = 1;
this.sortConfig = new SortOptions('dc.title', SortDirection.ASC);
this.rssSortConfig = new SortOptions('dc.date.accessioned', SortDirection.DESC);
}
ngOnInit() {

View File

@@ -10,7 +10,7 @@
<div class="d-flex justify-content-between flex-wrap">
<span class="align-self-center">{{'item.alerts.withdrawn' | translate}}</span>
<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) {
<a class="btn btn-primary btn-sm" (click)="openReinstateModal()">{{ 'item.alerts.reinstate-request' | translate}}</a>
}

View File

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

View File

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

View File

@@ -30,7 +30,6 @@ import {
map,
switchMap,
take,
tap,
} from 'rxjs/operators';
import { AlertComponent } from 'src/app/shared/alert/alert.component';
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(
getFirstSucceededRemoteData(),
getRemoteDataPayload(),
tap((bundlesPL: PaginatedList<Bundle>) =>
this.showLoadMoreLink$.next(bundlesPL.pageInfo.currentPage < bundlesPL.pageInfo.totalPages),
),
map((bundlePage: PaginatedList<Bundle>) => bundlePage.page),
).subscribe((bundles: Bundle[]) => {
this.bundlesSubject.next([...this.bundlesSubject.getValue(), ...bundles]);
).subscribe((bundles: PaginatedList<Bundle>) => {
this.updateBundles(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

View File

@@ -1,7 +1,7 @@
<ds-metadata-field-wrapper [label]="label | translate">
<div class="collections">
@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 [innerHTML]="separator"></span>
}
@@ -21,6 +21,8 @@
class="load-more-btn btn btn-sm btn-outline-secondary"
role="button"
href="javascript:void(0);"
role="button"
tabindex="0"
>
{{'item.page.collections.load-more' | translate}}
</a>

View File

@@ -1,6 +1,6 @@
<ds-metadata-field-wrapper [label]="label | translate">
@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) {
<span [innerHTML]="separator"></span>
}

View File

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

View File

@@ -5,7 +5,6 @@
<div>
<ds-item-alerts [item]="item"></ds-item-alerts>
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker>
@if (!item.isWithdrawn || (isAdmin$|async)) {
<div class="full-item-info">
<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 { TruncatePipe } from '../../shared/utils/truncate.pipe';
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 { CollectionsComponent } from '../field-components/collections/collections.component';
import { ThemedItemPageTitleFieldComponent } from '../simple/field-components/specific-field/title/themed-item-page-field.component';
@@ -162,7 +161,6 @@ describe('FullItemPageComponent', () => {
ThemedLoadingComponent,
ThemedItemPageTitleFieldComponent,
DsoEditMenuComponent,
ViewTrackerComponent,
ThemedItemAlertsComponent,
CollectionsComponent,
ThemedFullFileSectionComponent,

View File

@@ -42,7 +42,6 @@ import { hasValue } from '../../shared/empty.util';
import { ErrorComponent } from '../../shared/error/error.component';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
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 { CollectionsComponent } from '../field-components/collections/collections.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,
DsoEditMenuComponent,
ItemVersionsNoticeComponent,
ViewTrackerComponent,
ThemedItemAlertsComponent,
VarDirective,
],

View File

@@ -5,6 +5,7 @@ import { accessTokenResolver } from '../core/auth/access-token.resolver';
import { authenticatedGuard } from '../core/auth/authenticated.guard';
import { itemBreadcrumbResolver } from '../core/breadcrumbs/item-breadcrumb.resolver';
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 { UploadBitstreamComponent } from './bitstreams/upload/upload-bitstream.component';
import { ThemedFullItemPageComponent } from './full/themed-full-item-page.component';
@@ -38,7 +39,9 @@ export const ROUTES: Route[] = [
data: {
menuRoute: MenuRoute.ITEM_PAGE,
},
resolve: {
tracking: viewTrackerResolver,
},
},
{
path: 'full',
@@ -46,7 +49,9 @@ export const ROUTES: Route[] = [
data: {
menuRoute: MenuRoute.ITEM_PAGE,
},
resolve: {
tracking: viewTrackerResolver,
},
},
{
path: ITEM_EDIT_PATH,

View File

@@ -1,16 +1,16 @@
<div class="container mb-5">
<h1>{{'person.orcid.registry.auth' | translate}}</h1>
@if ((isLinkedToOrcid() | async)) {
@if ((isOrcidLinked$ | async)) {
<div data-test="orcidLinked">
<div class="row">
@if ((hasOrcidAuthorizations() | async)) {
@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) {
@for (auth of (profileAuthorizationScopes$ | async); track auth) {
<li data-test="orcidAuthorization">
{{getAuthorizationDescription(auth) | translate}}
</li>
@@ -26,16 +26,16 @@
<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">
@if ((hasMissingOrcidAuthorizations$ | async) !== true) {
<ds-alert [type]="AlertType.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">
@if ((hasMissingOrcidAuthorizations$ | async)) {
<ds-alert [type]="AlertType.Warning" data-test="missingOrcidAuthorizations">
{{'person.page.orcid.missing-authorizations-message' | translate}}
<ul>
@for (auth of (getMissingOrcidAuthorizations() | async); track auth) {
@for (auth of (profileAuthorizationScopes$ | async); track auth) {
<li data-test="missingOrcidAuthorization">
{{getAuthorizationDescription(auth) | translate }}
</li>
@@ -48,13 +48,13 @@
</div>
</div>
</div>
@if ((onlyAdminCanDisconnectProfileFromOrcid() | async) && (ownerCanDisconnectProfileFromOrcid() | async) !== true) {
@if ((onlyAdminCanDisconnectProfileFromOrcid$ | async) && (ownerCanDisconnectProfileFromOrcid$ | async) !== true) {
<ds-alert
[type]="'alert-warning'" data-test="unlinkOnlyAdmin">
[type]="AlertType.Warning" data-test="unlinkOnlyAdmin">
{{ 'person.page.orcid.remove-orcid-message' | translate}}
</ds-alert>
}
@if ((ownerCanDisconnectProfileFromOrcid() | async)) {
@if ((ownerCanDisconnectProfileFromOrcid$ | async)) {
<div class="row" data-test="unlinkOwner">
<div class="col">
<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>
}
</button>
@if ((hasMissingOrcidAuthorizations() | async)) {
@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>
@@ -83,7 +83,7 @@
<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>
<ds-alert [type]="AlertType.Info">{{ getOrcidNotLinkedMessage() }}</ds-alert>
</div>
</div>
<div class="row">
@@ -97,103 +97,3 @@
</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 { getFirstCompletedRemoteData } from '../../../core/shared/operators';
import { AlertComponent } from '../../../shared/alert/alert.component';
import { AlertType } from '../../../shared/alert/alert-type';
import { BtnDisabledDirective } from '../../../shared/btn-disabled.directive';
import { NotificationsService } from '../../../shared/notifications/notifications.service';
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
*/
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
*/
missingAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject<string[]>([]);
missingAuthorizationScopes: BehaviorSubject<string[]> = new BehaviorSubject([]);
hasMissingOrcidAuthorizations$: Observable<boolean>;
/**
* 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
*/
unlinkProcessing: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
unlinkProcessing: BehaviorSubject<boolean> = new BehaviorSubject(false);
/**
* 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
*/
private onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject<boolean>(false);
onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject<boolean> = new BehaviorSubject(false);
/**
* 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
*/
@Output() unlink: EventEmitter<void> = new EventEmitter<void>();
readonly AlertType = AlertType;
constructor(
private orcidAuthService: OrcidAuthService,
private translateService: TranslateService,
@@ -106,6 +113,8 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
this.orcidAuthorizationScopes.next(scopes);
this.initOrcidAuthSettings();
});
this.hasOrcidAuthorizations$ = this.hasOrcidAuthorizations();
this.hasMissingOrcidAuthorizations$ = this.hasMissingOrcidAuthorizations();
}
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
*/
hasOrcidAuthorizations(): Observable<boolean> {
return this.profileAuthorizationScopes.asObservable().pipe(
return this.profileAuthorizationScopes$.pipe(
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
*/
@@ -139,26 +141,12 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
);
}
/**
* 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> {
getOrcidNotLinkedMessage(): string {
const orcid = this.item.firstMetadataValue('person.identifier.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 {
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 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
*/
@@ -243,7 +224,7 @@ export class OrcidAuthComponent implements OnInit, OnChanges {
}
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">
<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
[type]="AlertTypeEnum.Info">
{{ 'person.page.orcid.sync-queue.empty-message' | translate}}
</ds-alert>
}
@if ((processing$ | async) !== true && (getList() | async)?.payload?.totalElements > 0) {
@if ((processing$ | async) !== true && (list$ | async)?.payload?.totalElements > 0) {
<ds-pagination
[paginationOptions]="paginationOptions"
[collectionSize]="(getList() | async)?.payload?.totalElements"
[collectionSize]="(list$ | async)?.payload?.totalElements"
[retainScrollPosition]="false" [hideGear]="true" (paginationChange)="updateList()">
<div class="table-responsive">
<table id="groups" class="table table-sm table-striped table-hover table-bordered">
@@ -26,7 +26,7 @@
</tr>
</thead>
<tbody>
@for (entry of (getList() | async)?.payload?.page; track entry) {
@for (entry of (list$ | async)?.payload?.page; track entry) {
<tr data-test="orcidQueueElementRow">
<td style="width: 15%" class="text-center align-middle">
<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
*/
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
* @type {AlertType}
*/
AlertTypeEnum = AlertType;
readonly AlertTypeEnum = AlertType;
/**
* 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
*

View File

@@ -9,7 +9,6 @@
<ds-qa-event-notification [item]="item"></ds-qa-event-notification>
<ds-notify-requests-status [itemUuid]="item.uuid"></ds-notify-requests-status>
<ds-item-versions-notice [item]="item"></ds-item-versions-notice>
<ds-view-tracker [object]="item"></ds-view-tracker>
@if (!item.isWithdrawn || (isAdmin$|async)) {
<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 { createPaginatedList } from '../../shared/testing/utils.test';
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 { ItemVersionsComponent } from '../versions/item-versions.component';
import { ItemVersionsNoticeComponent } from '../versions/notice/item-versions-notice.component';
@@ -142,7 +141,6 @@ describe('ItemPageComponent', () => {
remove: { imports: [
ThemedItemAlertsComponent,
ItemVersionsNoticeComponent,
ViewTrackerComponent,
ListableObjectComponentLoaderComponent,
ItemVersionsComponent,
ErrorComponent,

View File

@@ -51,7 +51,6 @@ import { ErrorComponent } from '../../shared/error/error.component';
import { ThemedLoadingComponent } from '../../shared/loading/themed-loading.component';
import { ListableObjectComponentLoaderComponent } from '../../shared/object-collection/shared/listable-object/listable-object-component-loader.component';
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 { getItemPageRoute } from '../item-page-routing-paths';
import { ItemVersionsComponent } from '../versions/item-versions.component';
@@ -76,7 +75,6 @@ import { QaEventNotificationComponent } from './qa-event-notification/qa-event-n
VarDirective,
ThemedItemAlertsComponent,
ItemVersionsNoticeComponent,
ViewTrackerComponent,
ListableObjectComponentLoaderComponent,
ItemVersionsComponent,
ErrorComponent,

View File

@@ -116,7 +116,7 @@
</ds-geospatial-item-page-field>
}
<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}}
</a>
</div>

View File

@@ -104,7 +104,7 @@
<ds-item-page-cc-license-field [item]="object" [variant]="'full'">
</ds-item-page-cc-license-field>
<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}}
</a>
</div>

View File

@@ -3,6 +3,6 @@
<h2><small><em>{{missingItem}}</em></small></h2>
<br />
<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>
</div>

View File

@@ -1,5 +1,5 @@
<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"
[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 -->

View File

@@ -5,6 +5,6 @@
<p>{{"error-page." + code | translate}}</p>
<br/>
<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>
</div>

View File

@@ -5,6 +5,6 @@
<p>{{"404.help" | translate}}</p>
<br/>
<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>
</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) {
<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) {
<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 { DateValueInputComponent } from './date-value-input/date-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';
/**
@@ -30,7 +31,7 @@ import { StringValueInputComponent } from './string-value-input/string-value-inp
useFactory: controlContainerFactory,
deps: [[new Optional(), NgForm]] }],
standalone: true,
imports: [StringValueInputComponent, DateValueInputComponent, FileValueInputComponent, BooleanValueInputComponent],
imports: [StringValueInputComponent, DateValueInputComponent, FileValueInputComponent, BooleanValueInputComponent, IntegerValueInputComponent],
})
export class ParameterValueInputComponent {
@Input() index: number;

View File

@@ -62,20 +62,20 @@ class="fas fa-plus pe-2"></i>{{'process.overview.new' | translate}}</button>
</div>
<div class="modal-body">
@if ((processBulkDeleteService.isProcessing$() | async) !== true) {
<div>{{'process.overview.delete.body' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</div>
}
@if (processBulkDeleteService.isProcessing$() |async) {
@let isProcessing = (isProcessing$ | async);
@if (isProcessing) {
<div class="alert alert-info">
<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>
</div>
} @else {
<div>{{'process.overview.delete.body' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}</div>
}
<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>
<button id="delete-confirm" class="btn btn-danger"
[dsBtnDisabled]="processBulkDeleteService.isProcessing$() |async"
[dsBtnDisabled]="isProcessing"
(click)="deleteSelected()">{{ 'process.overview.delete' | translate: {count: processBulkDeleteService.getAmountOfSelectedProcesses()} }}
</button>
</div>

View File

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

View File

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

View File

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

View File

@@ -11,10 +11,7 @@ import {
TranslateService,
} from '@ngx-translate/core';
import { UiSwitchModule } from 'ngx-ui-switch';
import {
BehaviorSubject,
Observable,
} from 'rxjs';
import { BehaviorSubject } from 'rxjs';
import {
map,
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.
*/

View File

@@ -54,15 +54,15 @@
</ds-alert>
}
@if (isRecaptchaCookieAccepted() && (googleRecaptchaService.captchaVersion() | async) === 'v2') {
@if (isRecaptchaCookieAccepted() && (captchaVersion$ | async) === 'v2') {
<div class="my-3">
<ds-google-recaptcha [captchaMode]="(googleRecaptchaService.captchaMode() | async)"
<ds-google-recaptcha [captchaMode]="(captchaMode$ | async)"
(executeRecaptcha)="register($event)" (checkboxChecked)="onCheckboxChecked($event)"
(showNotification)="showNotification($event)"></ds-google-recaptcha>
</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()">
{{ MESSAGE_PREFIX + '.submit' | translate }}
</button>

View File

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

View File

@@ -42,7 +42,7 @@
<div class="processed-message">
<p>{{ 'grant-deny-request-copy.processed' | translate }}</p>
<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>
</div>
}

View File

@@ -7,7 +7,7 @@
[class.display]="searchExpanded ? 'inline-block' : 'none'"
[tabIndex]="searchExpanded ? 0 : -1"
[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>
</button>
</form>

View File

@@ -8,6 +8,7 @@
<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"
role="menuitem"
tabindex="0"
aria-haspopup="menu"
aria-controls="loginDropdownMenu"
[attr.aria-expanded]="loginDrop.isOpen()"
@@ -26,6 +27,7 @@
<div ngbDropdown #loggedInDrop="ngbDropdown" display="dynamic" placement="bottom-right" class="d-inline-block" @fadeInOut>
<a href="javascript:void(0);"
role="menuitem"
tabindex="0"
[attr.aria-label]="'nav.user-profile-menu-and-logout' | translate"
aria-controls="user-menu-dropdown"
(click)="$event.preventDefault()" [title]="'nav.user-profile-menu-and-logout' | translate"
@@ -43,12 +45,12 @@
} @else {
<div data-test="auth-nav">
@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>
</a>
}
@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>
<span class="sr-only">(current)</span>
</a>

View File

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

View File

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

View File

@@ -433,4 +433,31 @@ describe('BrowserOrejimeService', () => {
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.orejimeConfig.apps = this.filterConfigApps(appsToHide);
this.lazyOrejime.then(({ init }) => {
this.applyUpdateSettingsCallbackToApps(user);
void this.lazyOrejime.then(({ init }) => {
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
*/
@@ -220,7 +247,6 @@ export class BrowserOrejimeService extends OrejimeService {
* @param user The authenticated user
*/
private initializeUser(user: EPerson) {
this.orejimeConfig.callback = debounce((consent, app) => this.updateSettingsForUsers(user), updateDebounce);
this.orejimeConfig.cookieName = this.getStorageName(user.uuid);
const anonCookie = this.cookieService.get(ANONYMOUS_STORAGE_NAME_OREJIME);
@@ -387,8 +413,10 @@ export class BrowserOrejimeService extends OrejimeService {
* @param user
*/
updateSettingsForUsers(user: EPerson) {
if (user) {
this.setSettingsForUser(user, this.cookieService.get(this.getStorageName(user.uuid)));
}
}
/**
* Create the storage name for orejime cookies based on the user's identifier

View File

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

View File

@@ -13,7 +13,7 @@ import {
} from '@angular/core/testing';
import { NoopAnimationsModule } from '@angular/platform-browser/animations';
import { TranslateModule } from '@ngx-translate/core';
import { cold } from 'jasmine-marbles';
import { hot } from 'jasmine-marbles';
import uniqueId from 'lodash/uniqueId';
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)],
]);
describe('EpersonGroupListComponent test suite', () => {
describe('EpersonGroupListComponent', () => {
let comp: EpersonGroupListComponent;
let compAsAny: any;
let fixture: ComponentFixture<EpersonGroupListComponent>;
let de;
let groupService: any;
let epersonService: any;
let paginationService;
let paginationService: PaginationServiceStub;
const paginationOptions: PaginationComponentOptions = new PaginationComponentOptions();
paginationOptions.id = uniqueId('eperson-group-list-pagination-test');
@@ -129,7 +128,6 @@ describe('EpersonGroupListComponent test suite', () => {
}));
describe('', () => {
let testComp: TestComponent;
let testFixture: ComponentFixture<TestComponent>;
// synchronous beforeEach
@@ -139,7 +137,6 @@ describe('EpersonGroupListComponent test suite', () => {
<ds-eperson-group-list [isListOfEPerson]="isListOfEPerson" [initSelected]="initSelected"></ds-eperson-group-list>`;
testFixture = createTestComponent(html, TestComponent) as ComponentFixture<TestComponent>;
testComp = testFixture.componentInstance;
});
afterEach(() => {
@@ -167,7 +164,6 @@ describe('EpersonGroupListComponent test suite', () => {
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
@@ -181,29 +177,25 @@ describe('EpersonGroupListComponent test suite', () => {
});
}));
it('should init entrySelectedId', fakeAsync(() => {
it('should init entrySelectedId', fakeAsync(async () => {
spyOn(comp, 'updateList');
comp.initSelected = EPersonMock.id;
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(compAsAny.entrySelectedId.value).toBe(EPersonMock.id);
});
await fixture.whenStable();
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));
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(compAsAny.list$.value).toEqual(epersonPaginatedListRD);
expect(comp.getList()).toBeObservable(cold('a', {
a: epersonPaginatedListRD,
await fixture.whenStable();
expect(comp.list$).toBeObservable(hot('(a|)', {
a: epersonPaginatedList,
}));
});
}));
it('should emit select event', () => {
@@ -211,23 +203,13 @@ describe('EpersonGroupListComponent test suite', () => {
comp.emitSelect(EPersonMock);
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', () => {
compAsAny.entrySelectedId.next(EPersonMock.id);
it('should return the entrySelectedId$ value', () => {
comp.entrySelectedId$.next(EPersonMock.id);
expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', {
a: true,
}));
});
it('should return false when entry is not selected', () => {
compAsAny.entrySelectedId.next('');
expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', {
a: false,
}));
expect(comp.entrySelectedId$.value).toBe(EPersonMock.id);
});
});
@@ -245,7 +227,6 @@ describe('EpersonGroupListComponent test suite', () => {
afterEach(() => {
comp = null;
compAsAny = null;
de = null;
fixture.destroy();
});
@@ -260,27 +241,24 @@ describe('EpersonGroupListComponent test suite', () => {
}));
it('should init entrySelectedId', fakeAsync(() => {
it('should init entrySelectedId', fakeAsync(async () => {
spyOn(comp, 'updateList');
comp.initSelected = GroupMock.id;
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(compAsAny.entrySelectedId.value).toBe(GroupMock.id);
});
await fixture.whenStable();
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));
fixture.detectChanges();
fixture.whenStable().then(() => {
expect(compAsAny.list$.value).toEqual(groupPaginatedListRD);
expect(comp.getList()).toBeObservable(cold('a', {
a: groupPaginatedListRD,
await fixture.whenStable();
expect(comp.list$).toBeObservable(hot('(a|)', {
a: groupPaginatedList,
}));
});
}));
it('should emit select event', () => {
@@ -288,27 +266,16 @@ describe('EpersonGroupListComponent test suite', () => {
comp.emitSelect(GroupMock);
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', () => {
compAsAny.entrySelectedId.next(EPersonMock.id);
it('should return the entrySelectedId$ value', () => {
comp.entrySelectedId$.next(GroupMock.id);
expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', {
a: true,
}));
});
it('should return false when entry is not selected', () => {
compAsAny.entrySelectedId.next('');
expect(comp.isSelected(EPersonMock)).toBeObservable(cold('a', {
a: false,
}));
expect(comp.entrySelectedId$.value).toBe(GroupMock.id);
});
it('should update list on search triggered', () => {
const options: PaginationComponentOptions = comp.paginationOptions;
const event: SearchEvent = {
scope: 'metadata',
query: 'test',
@@ -316,7 +283,7 @@ describe('EpersonGroupListComponent test suite', () => {
spyOn(comp, 'updateList');
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 {
BehaviorSubject,
Observable,
Subscription,
} from 'rxjs';
import { map } from 'rxjs/operators';
import { switchMap } from 'rxjs/operators';
import {
APP_DATA_SERVICES_MAP,
@@ -25,21 +24,21 @@ import {
import { DSONameService } from '../../core/breadcrumbs/dso-name.service';
import { FindListOptions } from '../../core/data/find-list-options.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 { 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 { Group } from '../../core/eperson/models/group.model';
import { GROUP } from '../../core/eperson/models/group.resource-type';
import { lazyDataService } from '../../core/lazy-data-service';
import { PaginationService } from '../../core/pagination/pagination.service';
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 { fadeInOut } from '../animations/fade';
import {
hasValue,
isNotEmpty,
} from '../empty.util';
import { PaginationComponent } from '../pagination/pagination.component';
import { PaginationComponentOptions } from '../pagination/pagination-component-options.model';
import { SearchEvent } from './eperson-group-list-event-type';
@@ -101,21 +100,13 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy {
/**
* 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
* @type {string}
*/
private entrySelectedId: BehaviorSubject<string> = new BehaviorSubject<string>('');
/**
* Array to track all subscriptions and unsubscribe them onDestroy
* @type {Array}
*/
private subs: Subscription[] = [];
private pageConfigSub: Subscription;
entrySelectedId$: BehaviorSubject<string> = new BehaviorSubject('');
/**
* Initialize instance variables and inject the properly UpdateDataServiceImpl
@@ -143,7 +134,7 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy {
this.paginationOptions.pageSize = 5;
if (this.initSelected) {
this.entrySelectedId.next(this.initSelected);
this.entrySelectedId$.next(this.initSelected);
}
this.updateList(this.currentSearchScope, this.currentSearchQuery);
@@ -158,28 +149,9 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy {
*/
emitSelect(entry: DSpaceObject): void {
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
*/
@@ -194,38 +166,26 @@ export class EpersonGroupListComponent implements OnInit, OnDestroy {
* Retrieve a paginate list of eperson or group
*/
updateList(scope: string, query: string): void {
if (hasValue(this.pageConfigSub)) {
this.pageConfigSub.unsubscribe();
}
this.pageConfigSub = this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions)
.subscribe((paginationOptions) => {
const options: FindListOptions = Object.assign({}, new FindListOptions(), {
this.list$ = this.paginationService.getCurrentPagination(this.paginationOptions.id, this.paginationOptions).pipe(
switchMap((paginationOptions) => {
const options: FindListOptions = Object.assign(new FindListOptions(), {
elementsPerPage: paginationOptions.pageSize,
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 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
*/
ngOnDestroy(): void {
this.list$ = null;
this.subs
.filter((subscription) => hasValue(subscription))
.forEach((subscription) => subscription.unsubscribe());
this.paginationService.clearPagination(this.paginationOptions.id);
}

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