diff --git a/.eslintrc.json b/.eslintrc.json index 6ee63b269f..929afa80ab 100644 --- a/.eslintrc.json +++ b/.eslintrc.json @@ -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", diff --git a/docker/docker-compose-ci.yml b/docker/docker-compose-ci.yml index 98825605d3..654d1d9bb7 100644 --- a/docker/docker-compose-ci.yml +++ b/docker/docker-compose-ci.yml @@ -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: diff --git a/docker/docker-compose-rest.yml b/docker/docker-compose-rest.yml index e650f09eb5..be037dac3a 100644 --- a/docker/docker-compose-rest.yml +++ b/docker/docker-compose-rest.yml @@ -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: diff --git a/package-lock.json b/package-lock.json index 73c8bf06a9..32ba92c769 100644 --- a/package-lock.json +++ b/package-lock.json @@ -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", diff --git a/package.json b/package.json index 292f7a4abb..13ba71ee88 100644 --- a/package.json +++ b/package.json @@ -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" } diff --git a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html index 9947d775a2..ae36b27c87 100644 --- a/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html +++ b/src/app/access-control/epeople-registry/eperson-form/eperson-form.component.html @@ -30,7 +30,7 @@ } @if (canImpersonate$ | async) { -
+
@if (!isImpersonated) { } @@ -34,7 +34,11 @@ @@ -71,26 +71,26 @@ @if (showPrivacyPolicy) {
  • {{ 'footer.link.privacy-policy' | translate}} + routerLink="info/privacy" role="link" tabindex="0">{{ 'footer.link.privacy-policy' | translate}}
  • } @if (showEndUserAgreement) {
  • {{ 'footer.link.end-user-agreement' | translate}} + routerLink="info/end-user-agreement" role="link" tabindex="0">{{ 'footer.link.end-user-agreement' | translate}}
  • } @if (showSendFeedback$ | async) {
  • {{ 'footer.link.feedback' | translate}} + routerLink="info/feedback" role="link" tabindex="0">{{ 'footer.link.feedback' | translate}}
  • }
    @if (coarLdnEnabled$ | async) {
    - + {{ 'footer.link.coar-notify-support' | translate }} diff --git a/src/app/header/header.component.html b/src/app/header/header.component.html index 67a4a96c23..dc3d7f94b9 100644 --- a/src/app/header/header.component.html +++ b/src/app/header/header.component.html @@ -1,7 +1,7 @@
    - + diff --git a/src/app/home-page/home-news/home-news.component.html b/src/app/home-page/home-news/home-news.component.html index 972c8cc293..06656b9f5b 100644 --- a/src/app/home-page/home-news/home-news.component.html +++ b/src/app/home-page/home-news/home-news.component.html @@ -1,4 +1,4 @@ -
    +
    @@ -14,7 +14,7 @@
  • issue permanent urls and trustworthy identifiers, including optional integrations with handle.net and DataCite DOI
  • Join an international community of leading institutions using DSpace. + target="_blank" role="link" tabindex="0">leading institutions using DSpace.

    diff --git a/src/app/home-page/home-page-routes.ts b/src/app/home-page/home-page-routes.ts index 6e4a8f353f..0440315a2c 100644 --- a/src/app/home-page/home-page-routes.ts +++ b/src/app/home-page/home-page-routes.ts @@ -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, - }, }, ]; diff --git a/src/app/home-page/home-page.component.html b/src/app/home-page/home-page.component.html index 43f7977bf2..b53672768a 100644 --- a/src/app/home-page/home-page.component.html +++ b/src/app/home-page/home-page.component.html @@ -18,9 +18,6 @@ - @if ((site$ | async); as site) { - - } diff --git a/src/app/home-page/home-page.component.ts b/src/app/home-page/home-page.component.ts index 4f05abc96c..03ac44c9f3 100644 --- a/src/app/home-page/home-page.component.ts +++ b/src/app/home-page/home-page.component.ts @@ -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 { diff --git a/src/app/home-page/recent-item-list/recent-item-list.component.html b/src/app/home-page/recent-item-list/recent-item-list.component.html index 4d77e5027e..1a7fc342ed 100644 --- a/src/app/home-page/recent-item-list/recent-item-list.component.html +++ b/src/app/home-page/recent-item-list/recent-item-list.component.html @@ -9,7 +9,7 @@
    } - +
    } @if (itemRD?.hasFailed) { diff --git a/src/app/home-page/top-level-community-list/top-level-community-list.component.html b/src/app/home-page/top-level-community-list/top-level-community-list.component.html index 00446d658a..8b0ee4b853 100644 --- a/src/app/home-page/top-level-community-list/top-level-community-list.component.html +++ b/src/app/home-page/top-level-community-list/top-level-community-list.component.html @@ -8,6 +8,7 @@ diff --git a/src/app/home-page/top-level-community-list/top-level-community-list.component.ts b/src/app/home-page/top-level-community-list/top-level-community-list.component.ts index 4af7bcc483..7f3e384df8 100644 --- a/src/app/home-page/top-level-community-list/top-level-community-list.component.ts +++ b/src/app/home-page/top-level-community-list/top-level-community-list.component.ts @@ -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() { diff --git a/src/app/item-page/alerts/item-alerts.component.html b/src/app/item-page/alerts/item-alerts.component.html index cb69f873bb..0f2205232b 100644 --- a/src/app/item-page/alerts/item-alerts.component.html +++ b/src/app/item-page/alerts/item-alerts.component.html @@ -10,7 +10,7 @@
    {{'item.alerts.withdrawn' | translate}}
    - {{"404.link.home-page" | translate}} + {{"404.link.home-page" | translate}} @if (showReinstateButton$ | async) { {{ 'item.alerts.reinstate-request' | translate}} } diff --git a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.html b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.html index 4aca189e16..5733bbf927 100644 --- a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.html +++ b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.html @@ -1,7 +1,7 @@
    - - + + @for (bundle of (bundles$ | async); track bundle) { diff --git a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts index 3181b0e766..15bd82b82a 100644 --- a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts +++ b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.spec.ts @@ -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, })); diff --git a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts index 6e700fd48e..881de3d6c9 100644 --- a/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts +++ b/src/app/item-page/edit-item-page/item-authorizations/item-authorizations.component.ts @@ -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} */ - private item$: Observable; + item$: Observable; /** * 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; + + 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(); + this.itemName$ = this.getItemName(); } /** - * Return the item's UUID + * Return the item's name */ - getItemUUID(): Observable { - return this.item$.pipe( - map((item: Item) => item.id), - first((UUID: string) => isNotEmpty(UUID)), - ); - } - - /** - * Return the item's name - */ - getItemName(): Observable { + private getItemName(): Observable { 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 { - return this.bundles$.asObservable(); - } - /** * Get all bundles per item * and all the bitstreams per bundle diff --git a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts index ea56a4274a..bd6679d6a1 100644 --- a/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts +++ b/src/app/item-page/edit-item-page/item-bitstreams/item-bitstreams.component.ts @@ -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) => - this.showLoadMoreLink$.next(bundlesPL.pageInfo.currentPage < bundlesPL.pageInfo.totalPages), - ), - map((bundlePage: PaginatedList) => bundlePage.page), - ).subscribe((bundles: Bundle[]) => { - this.bundlesSubject.next([...this.bundlesSubject.getValue(), ...bundles]); + ).subscribe((bundles: PaginatedList) => { + 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) { + 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 diff --git a/src/app/item-page/field-components/collections/collections.component.html b/src/app/item-page/field-components/collections/collections.component.html index f5f42bd272..88d50a1f33 100644 --- a/src/app/item-page/field-components/collections/collections.component.html +++ b/src/app/item-page/field-components/collections/collections.component.html @@ -1,7 +1,7 @@
    @for (collection of (this.collections$ | async); track collection; let last = $last) { - + {{ dsoNameService.getName(collection) }}@if (!last) { } @@ -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}} diff --git a/src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.html b/src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.html index 2093318e66..c07e85d5db 100644 --- a/src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.html +++ b/src/app/item-page/field-components/metadata-uri-values/metadata-uri-values.component.html @@ -1,6 +1,6 @@ @for (mdValue of mdValues; track mdValue; let last = $last) { - + {{ linktext || mdValue.value }}@if (!last) { } diff --git a/src/app/item-page/field-components/metadata-values/metadata-values.component.html b/src/app/item-page/field-components/metadata-values/metadata-values.component.html index 015398f041..60fca0a8b7 100644 --- a/src/app/item-page/field-components/metadata-values/metadata-values.component.html +++ b/src/app/item-page/field-components/metadata-values/metadata-values.component.html @@ -23,14 +23,14 @@ - {{value}} + [queryParams]="getQueryParams(value)" role="link" tabindex="0">{{value}} diff --git a/src/app/item-page/full/full-item-page.component.html b/src/app/item-page/full/full-item-page.component.html index c6c7d90bf4..43ecae9f17 100644 --- a/src/app/item-page/full/full-item-page.component.html +++ b/src/app/item-page/full/full-item-page.component.html @@ -5,7 +5,6 @@
    - @if (!item.isWithdrawn || (isAdmin$|async)) {
    diff --git a/src/app/item-page/full/full-item-page.component.spec.ts b/src/app/item-page/full/full-item-page.component.spec.ts index 0d05510fbf..d38b3ba017 100644 --- a/src/app/item-page/full/full-item-page.component.spec.ts +++ b/src/app/item-page/full/full-item-page.component.spec.ts @@ -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, diff --git a/src/app/item-page/full/full-item-page.component.ts b/src/app/item-page/full/full-item-page.component.ts index 9b982c7a5f..f63447b809 100644 --- a/src/app/item-page/full/full-item-page.component.ts +++ b/src/app/item-page/full/full-item-page.component.ts @@ -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, ], diff --git a/src/app/item-page/item-page-routes.ts b/src/app/item-page/item-page-routes.ts index f6ceefb997..beb6a74857 100644 --- a/src/app/item-page/item-page-routes.ts +++ b/src/app/item-page/item-page-routes.ts @@ -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, diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html index d2f7bc04b6..6695226d5c 100644 --- a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.html @@ -1,16 +1,16 @@

    {{'person.orcid.registry.auth' | translate}}

    - @if ((isLinkedToOrcid() | async)) { + @if ((isOrcidLinked$ | async)) {
    - @if ((hasOrcidAuthorizations() | async)) { + @if ((hasOrcidAuthorizations$ | async)) {
    {{ 'person.page.orcid.granted-authorizations'| translate }}
      - @for (auth of (getOrcidAuthorizations() | async); track auth) { + @for (auth of (profileAuthorizationScopes$ | async); track auth) {
    • {{getAuthorizationDescription(auth) | translate}}
    • @@ -26,16 +26,16 @@
      {{ 'person.page.orcid.missing-authorizations'| translate }}
      - @if ((hasMissingOrcidAuthorizations() | async) !== true) { - + @if ((hasMissingOrcidAuthorizations$ | async) !== true) { + {{'person.page.orcid.no-missing-authorizations-message' | translate}} } - @if ((hasMissingOrcidAuthorizations() | async)) { - + @if ((hasMissingOrcidAuthorizations$ | async)) { + {{'person.page.orcid.missing-authorizations-message' | translate}}
        - @for (auth of (getMissingOrcidAuthorizations() | async); track auth) { + @for (auth of (profileAuthorizationScopes$ | async); track auth) {
      • {{getAuthorizationDescription(auth) | translate }}
      • @@ -48,13 +48,13 @@
    - @if ((onlyAdminCanDisconnectProfileFromOrcid() | async) && (ownerCanDisconnectProfileFromOrcid() | async) !== true) { + @if ((onlyAdminCanDisconnectProfileFromOrcid$ | async) && (ownerCanDisconnectProfileFromOrcid$ | async) !== true) { + [type]="AlertType.Warning" data-test="unlinkOnlyAdmin"> {{ 'person.page.orcid.remove-orcid-message' | translate}} } - @if ((ownerCanDisconnectProfileFromOrcid() | async)) { + @if ((ownerCanDisconnectProfileFromOrcid$ | async)) {
    - @if ((hasMissingOrcidAuthorizations() | async)) { + @if ((hasMissingOrcidAuthorizations$ | async)) {
    - - -
    -
    - @if ((hasOrcidAuthorizations() | async)) { -
    -
    -
    {{ 'person.page.orcid.granted-authorizations'| translate }}
    -
    -
    -
      - @for (auth of (getOrcidAuthorizations() | async); track auth) { -
    • - {{getAuthorizationDescription(auth) | translate}} -
    • - } -
    -
    -
    -
    -
    - } -
    -
    -
    {{ 'person.page.orcid.missing-authorizations'| translate }}
    -
    -
    - @if ((hasMissingOrcidAuthorizations() | async) !== true) { - - {{'person.page.orcid.no-missing-authorizations-message' | translate}} - - } - @if ((hasMissingOrcidAuthorizations() | async)) { - - {{'person.page.orcid.missing-authorizations-message' | translate}} -
      - @for (auth of (getMissingOrcidAuthorizations() | async); track auth) { -
    • - {{getAuthorizationDescription(auth) | translate }} -
    • - } -
    -
    - } -
    -
    -
    -
    -
    - @if ((onlyAdminCanDisconnectProfileFromOrcid() | async) && (ownerCanDisconnectProfileFromOrcid() | async) !== true) { - - {{ 'person.page.orcid.remove-orcid-message' | translate}} - - } - @if ((ownerCanDisconnectProfileFromOrcid() | async)) { -
    -
    - - @if ((hasMissingOrcidAuthorizations() | async)) { - - } -
    -
    - } -
    -
    - - -
    -
    -
    orcid-logo
    -
    - {{ getOrcidNotLinkedMessage() | async }} -
    -
    -
    -
    - -
    -
    -
    -
    - diff --git a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts index 2d39ee25a8..f4ed87f2c6 100644 --- a/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts +++ b/src/app/item-page/orcid-page/orcid-auth/orcid-auth.component.ts @@ -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 = new BehaviorSubject([]); + profileAuthorizationScopes$: BehaviorSubject = new BehaviorSubject([]); + + hasOrcidAuthorizations$: Observable; /** * The list of all orcid authorization scopes missing in the orcid profile */ - missingAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); + missingAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); + + hasMissingOrcidAuthorizations$: Observable; /** * The list of all orcid authorization scopes available */ - orcidAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); + orcidAuthorizationScopes: BehaviorSubject = new BehaviorSubject([]); /** * A boolean representing if unlink operation is processing */ - unlinkProcessing: BehaviorSubject = new BehaviorSubject(false); + unlinkProcessing: BehaviorSubject = new BehaviorSubject(false); /** * A boolean representing if orcid profile is linked */ - private isOrcidLinked$: BehaviorSubject = new BehaviorSubject(false); + isOrcidLinked$: BehaviorSubject = new BehaviorSubject(false); /** * A boolean representing if only admin can disconnect orcid profile */ - private onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject = new BehaviorSubject(false); + onlyAdminCanDisconnectProfileFromOrcid$: BehaviorSubject = new BehaviorSubject(false); /** * A boolean representing if owner can disconnect orcid profile */ - private ownerCanDisconnectProfileFromOrcid$: BehaviorSubject = new BehaviorSubject(false); + ownerCanDisconnectProfileFromOrcid$: BehaviorSubject = new BehaviorSubject(false); /** * An event emitted when orcid profile is unliked successfully */ @Output() unlink: EventEmitter = new EventEmitter(); + 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 { - 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 { - 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 { - return this.profileAuthorizationScopes.asObservable(); - } - - /** - * Return a boolean representing if orcid profile is linked - */ - isLinkedToOrcid(): Observable { - return this.isOrcidLinked$.asObservable(); - } - - getOrcidNotLinkedMessage(): Observable { + 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 { - 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)); } } diff --git a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html index 014f995ae0..b86ddb4461 100644 --- a/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html +++ b/src/app/item-page/orcid-page/orcid-queue/orcid-queue.component.html @@ -5,16 +5,16 @@

    {{ 'person.orcid.registry.queue' | translate }}

    - @if ((processing$ | async) !== true && (getList() | async)?.payload?.totalElements === 0) { + @if ((processing$ | async) !== true && (list$ | async)?.payload?.totalElements === 0) { {{ 'person.page.orcid.sync-queue.empty-message' | translate}} } - @if ((processing$ | async) !== true && (getList() | async)?.payload?.totalElements > 0) { + @if ((processing$ | async) !== true && (list$ | async)?.payload?.totalElements > 0) {
    @@ -26,7 +26,7 @@ - @for (entry of (getList() | async)?.payload?.page; track entry) { + @for (entry of (list$ | async)?.payload?.page; track entry) { - @for (entry of (getList() | async)?.payload?.page; track entry) { - - - + @for (entry of list.page; track entry) { + + + } @@ -19,7 +19,7 @@ @for (header of headers; track header) {
    >> = new BehaviorSubject>>({} as any); + list$: BehaviorSubject>> = new BehaviorSubject>>({} 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>> { - return this.list$.asObservable(); - } - /** * Return the icon class for the queue object type * diff --git a/src/app/item-page/simple/item-page.component.html b/src/app/item-page/simple/item-page.component.html index 2052a36b81..2b9fdebec5 100644 --- a/src/app/item-page/simple/item-page.component.html +++ b/src/app/item-page/simple/item-page.component.html @@ -9,7 +9,6 @@ - @if (!item.isWithdrawn || (isAdmin$|async)) { } diff --git a/src/app/item-page/simple/item-page.component.spec.ts b/src/app/item-page/simple/item-page.component.spec.ts index 7b29711206..6254a3181d 100644 --- a/src/app/item-page/simple/item-page.component.spec.ts +++ b/src/app/item-page/simple/item-page.component.spec.ts @@ -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, diff --git a/src/app/item-page/simple/item-page.component.ts b/src/app/item-page/simple/item-page.component.ts index 9efcd3d9a7..b61ceebfc0 100644 --- a/src/app/item-page/simple/item-page.component.ts +++ b/src/app/item-page/simple/item-page.component.ts @@ -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, diff --git a/src/app/item-page/simple/item-types/publication/publication.component.html b/src/app/item-page/simple/item-types/publication/publication.component.html index 31bb741a9f..673e58c785 100644 --- a/src/app/item-page/simple/item-types/publication/publication.component.html +++ b/src/app/item-page/simple/item-types/publication/publication.component.html @@ -116,7 +116,7 @@ } diff --git a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html index d941bb6327..77d4168a1c 100644 --- a/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html +++ b/src/app/item-page/simple/item-types/untyped-item/untyped-item.component.html @@ -104,7 +104,7 @@ diff --git a/src/app/lookup-by-id/objectnotfound/objectnotfound.component.html b/src/app/lookup-by-id/objectnotfound/objectnotfound.component.html index e1cf58b5b2..e3416f8405 100644 --- a/src/app/lookup-by-id/objectnotfound/objectnotfound.component.html +++ b/src/app/lookup-by-id/objectnotfound/objectnotfound.component.html @@ -3,6 +3,6 @@

    {{missingItem}}


    - {{"404.link.home-page" | translate}} + {{"404.link.home-page" | translate}}

    diff --git a/src/app/navbar/navbar.component.html b/src/app/navbar/navbar.component.html index 479f206f90..f7d48dc050 100644 --- a/src/app/navbar/navbar.component.html +++ b/src/app/navbar/navbar.component.html @@ -1,5 +1,5 @@
    {{entry.id}}{{dsoNameService.getName(entry)}}
    {{ entry.id }}{{ dsoNameService.getName(entry) }} @if (showAdd && this.vocabularyOptions.closed) { } @@ -59,6 +59,8 @@ [(ngModel)]="node.isSelected" [checked]="node.isSelected" (change)="onSelect(node.item)" + role="checkbox" + tabindex="0" > {{node.item.display}} @@ -70,7 +72,9 @@ [ngbTooltip]="node.item?.otherInformation?.note" [openDelay]="500" container="body" - (click)="onSelect(node.item)"> + (click)="onSelect(node.item)" + role="button" + tabindex="0"> {{node.item.display}} } @@ -80,7 +84,11 @@ @@ -95,6 +103,8 @@ [(ngModel)]="node.isSelected" [checked]="node.isSelected" (change)="onSelect(node.item)" + role="checkbox" + tabindex="0" > {{node.item.display}} @@ -106,7 +116,9 @@ [ngbTooltip]="node.item?.otherInformation?.note" [openDelay]="500" container="body" - (click)="onSelect(node.item)"> + (click)="onSelect(node.item)" + role="button" + tabindex="0"> {{node.item.display}} } @@ -114,14 +126,14 @@ diff --git a/src/app/shared/host-window.service.ts b/src/app/shared/host-window.service.ts index 3801a4ef06..07cd29df76 100644 --- a/src/app/shared/host-window.service.ts +++ b/src/app/shared/host-window.service.ts @@ -147,10 +147,10 @@ export class HostWindowService { } isXsOrSm(): Observable { - return observableCombineLatest( + return observableCombineLatest([ this.isXs(), this.isSm(), - ).pipe( + ]).pipe( map(([isXs, isSm]) => isXs || isSm), distinctUntilChanged(), ); diff --git a/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.html b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.html index a8d511a3f5..5a079f91a6 100644 --- a/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.html +++ b/src/app/shared/log-in/methods/log-in-external-provider/log-in-external-provider.component.html @@ -1,3 +1,3 @@ - diff --git a/src/app/shared/log-in/methods/password/log-in-password.component.html b/src/app/shared/log-in/methods/password/log-in-password.component.html index 52685c361c..5eb2088a81 100644 --- a/src/app/shared/log-in/methods/password/log-in-password.component.html +++ b/src/app/shared/log-in/methods/password/log-in-password.component.html @@ -28,18 +28,18 @@ } + [dsBtnDisabled]="!form.valid" role="button" tabindex="0"> {{"login.form.submit" | translate}} @if (canShowDivider$ | async) {
    @if (canRegister$ | async) { {{"login.form.new-user" | translate}} + [attr.data-test]="'register' | dsBrowserOnly" role="menuitem" tabindex="0">{{"login.form.new-user" | translate}} } @if (canForgot$ | async) { {{"login.form.forgot-password" | translate}} + [attr.data-test]="'forgot' | dsBrowserOnly" role="menuitem" tabindex="0">{{"login.form.forgot-password" | translate}} }
    } diff --git a/src/app/shared/menu/menu-item/link-menu-item.component.html b/src/app/shared/menu/menu-item/link-menu-item.component.html index 71eeda2e68..f96084e6e1 100644 --- a/src/app/shared/menu/menu-item/link-menu-item.component.html +++ b/src/app/shared/menu/menu-item/link-menu-item.component.html @@ -8,4 +8,5 @@ (keyup.space)="navigate($event)" (keydown.enter)="navigate($event)" href="javascript:void(0);" + tabindex="0" >{{item.text | translate}} diff --git a/src/app/shared/menu/menu-item/text-menu-item.component.html b/src/app/shared/menu/menu-item/text-menu-item.component.html index ba3cf99a49..e2dd334caf 100644 --- a/src/app/shared/menu/menu-item/text-menu-item.component.html +++ b/src/app/shared/menu/menu-item/text-menu-item.component.html @@ -1 +1 @@ -{{item.text | translate}} +{{item.text | translate}} diff --git a/src/app/shared/mydspace-actions/mydspace-actions.ts b/src/app/shared/mydspace-actions/mydspace-actions.ts index 5100d8122e..73caf4f30c 100644 --- a/src/app/shared/mydspace-actions/mydspace-actions.ts +++ b/src/app/shared/mydspace-actions/mydspace-actions.ts @@ -101,17 +101,25 @@ export abstract class MyDSpaceActionsComponent { return false; }; // This assures that the search cache is empty before reloading mydspace. // See https://github.com/DSpace/dspace-angular/pull/468 + this.invalidateCacheForCurrentSearchUrl(true); + } + + invalidateCacheForCurrentSearchUrl(shouldNavigate = false): void { + const url = decodeURIComponent(this.router.url); this.searchService.getEndpoint().pipe( take(1), tap((cachedHref: string) => this.requestService.removeByHrefSubstring(cachedHref)), - ).subscribe(() => this.router.navigateByUrl(url)); + ).subscribe(() => { + if (shouldNavigate) { + this.router.navigateByUrl(url); + } + }); } /** diff --git a/src/app/shared/mydspace-actions/mydspace-reloadable-actions.spec.ts b/src/app/shared/mydspace-actions/mydspace-reloadable-actions.spec.ts index 1d0d231503..b7a0a20219 100644 --- a/src/app/shared/mydspace-actions/mydspace-reloadable-actions.spec.ts +++ b/src/app/shared/mydspace-actions/mydspace-reloadable-actions.spec.ts @@ -219,6 +219,7 @@ describe('MyDSpaceReloadableActionsComponent', () => { spyOn(component, 'reloadObjectExecution').and.callThrough(); spyOn(component, 'convertReloadedObject').and.callThrough(); spyOn(component.processCompleted, 'emit').and.callThrough(); + spyOn(component, 'invalidateCacheForCurrentSearchUrl').and.callThrough(); (component as any).objectDataService = mockDataService; }); @@ -239,10 +240,11 @@ describe('MyDSpaceReloadableActionsComponent', () => { }); }); - it('should emit the reloaded object in case of success', (done) => { + it('should emit the reloaded object and invalidate cache in case of success', (done) => { component.startActionExecution().subscribe( (result) => { expect(component.processCompleted.emit).toHaveBeenCalledWith({ result: true, reloadedObject: result as any }); + expect(component.invalidateCacheForCurrentSearchUrl).toHaveBeenCalled(); done(); }); }); diff --git a/src/app/shared/mydspace-actions/mydspace-reloadable-actions.ts b/src/app/shared/mydspace-actions/mydspace-reloadable-actions.ts index 28e3ac4040..062bf25741 100644 --- a/src/app/shared/mydspace-actions/mydspace-reloadable-actions.ts +++ b/src/app/shared/mydspace-actions/mydspace-reloadable-actions.ts @@ -105,6 +105,8 @@ export abstract class MyDSpaceReloadableActionsComponent @if (linkType !== linkTypes.None) { - + {{object.value}} } diff --git a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html index 74f77225c1..a49f328ff3 100644 --- a/src/app/shared/object-list/collection-list-element/collection-list-element.component.html +++ b/src/app/shared/object-list/collection-list-element/collection-list-element.component.html @@ -1,6 +1,6 @@
    @if (linkType !== linkTypes.None) { - + {{ dsoNameService.getName(object) }} } diff --git a/src/app/shared/object-list/community-list-element/community-list-element.component.html b/src/app/shared/object-list/community-list-element/community-list-element.component.html index 3c7faf3dab..90cb409819 100644 --- a/src/app/shared/object-list/community-list-element/community-list-element.component.html +++ b/src/app/shared/object-list/community-list-element/community-list-element.component.html @@ -1,6 +1,6 @@
    @if (linkType !== linkTypes.None) { - + {{ dsoNameService.getName(object) }} } diff --git a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html index ecaec7ff64..8c550d0276 100644 --- a/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html +++ b/src/app/shared/object-list/metadata-representation-list-element/plain-text/plain-text-metadata-list-element.component.html @@ -7,7 +7,7 @@ } @if ((mdRepresentation.representationType==='plain_text') && isLink()) { + target="_blank" [href]="mdRepresentation.getValue()" role="link" tabindex="0"> {{mdRepresentation.getValue()}} } @@ -18,7 +18,9 @@ + [queryParams]="getQueryParams()" + role="link" + tabindex="0"> {{mdRepresentation.getValue()}} } diff --git a/src/app/shared/object-list/object-list.component.html b/src/app/shared/object-list/object-list.component.html index 3073623c2e..5670199fc5 100644 --- a/src/app/shared/object-list/object-list.component.html +++ b/src/app/shared/object-list/object-list.component.html @@ -4,6 +4,7 @@ [objects]="objects" [sortOptions]="sortConfig" [hideGear]="hideGear" + [showRSS]="showRSS" [hidePagerWhenSinglePage]="hidePagerWhenSinglePage" [hidePaginationDetail]="hidePaginationDetail" [showPaginator]="showPaginator" diff --git a/src/app/shared/object-list/object-list.component.ts b/src/app/shared/object-list/object-list.component.ts index cf3e0164f6..3c0136fec9 100644 --- a/src/app/shared/object-list/object-list.component.ts +++ b/src/app/shared/object-list/object-list.component.ts @@ -39,7 +39,7 @@ import { SelectableListService } from './selectable-list/selectable-list.service }) export class ObjectListComponent { /** - * The view mode of the this component + * The view mode of this component */ viewMode = ViewMode.ListElement; @@ -70,6 +70,11 @@ export class ObjectListComponent { @Input() selectable = false; @Input() selectionConfig: { repeatable: boolean, listId: string }; + /** + * Whether to show an RSS syndication button for the current search options + */ + @Input() showRSS: SortOptions | boolean = false; + /** * The link type of the listable elements */ diff --git a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html index db647b6e74..cceb69e1ed 100644 --- a/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html +++ b/src/app/shared/object-list/search-result-list-element/item-search-result/item-types/item/item-search-result-list-element.component.html @@ -3,7 +3,7 @@
    @if (linkType !== linkTypes.None) { + [routerLink]="[itemPageRoute]" class="dont-break-out" role="button" tabindex="0"> @@ -28,7 +28,7 @@ @if (linkType !== linkTypes.None) { + [innerHTML]="dsoTitle" role="link" tabindex="0"> } @if (linkType === linkTypes.None) { @if (!hideGear) {
    - +
    } - + @if (showRSS !== false) { + + }
    } - @if (shouldShowBottomPager | async) { + @if (showBottomPager$ | async) {
    @if (showPaginator) { diff --git a/src/app/shared/search/search-export-csv/search-export-csv.component.html b/src/app/shared/search/search-export-csv/search-export-csv.component.html index efc5c5dc4a..1cf8f6b663 100644 --- a/src/app/shared/search/search-export-csv/search-export-csv.component.html +++ b/src/app/shared/search/search-export-csv/search-export-csv.component.html @@ -1,9 +1,22 @@ + +
    +

    {{tooltipMsg | translate}}

    +
    +
    + + +
    +

    {{tooltipMsg | translate}}

    +

    {{exportLimitExceededMsg}}

    +
    +
    + @if (shouldShowButton$ | async) { -} \ No newline at end of file +} diff --git a/src/app/shared/search/search-export-csv/search-export-csv.component.spec.ts b/src/app/shared/search/search-export-csv/search-export-csv.component.spec.ts index 9abdd8d366..f067263712 100644 --- a/src/app/shared/search/search-export-csv/search-export-csv.component.spec.ts +++ b/src/app/shared/search/search-export-csv/search-export-csv.component.spec.ts @@ -9,6 +9,7 @@ import { NgbModule } from '@ng-bootstrap/ng-bootstrap'; import { TranslateModule } from '@ngx-translate/core'; import { of as observableOf } from 'rxjs'; +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { ScriptDataService } from '../../../core/data/processes/script-data.service'; import { getProcessDetailRoute } from '../../../process-page/process-page-routing.paths'; @@ -31,6 +32,7 @@ describe('SearchExportCsvComponent', () => { let authorizationDataService: AuthorizationDataService; let notificationsService; let router; + let configurationDataService: jasmine.SpyObj; const process = Object.assign(new Process(), { processId: 5, scriptName: 'metadata-export-search' }); @@ -45,6 +47,10 @@ describe('SearchExportCsvComponent', () => { ], }); + configurationDataService = jasmine.createSpyObj('ConfigurationDataService', { + findByPropertyName: observableOf({ payload: { value: '500' } }), + }); + function initBeforeEachAsync() { scriptDataService = jasmine.createSpyObj('scriptDataService', { scriptWithNameExistsAndCanExecute: observableOf(true), @@ -64,6 +70,7 @@ describe('SearchExportCsvComponent', () => { { provide: AuthorizationDataService, useValue: authorizationDataService }, { provide: NotificationsService, useValue: notificationsService }, { provide: Router, useValue: router }, + { provide: ConfigurationDataService, useValue: configurationDataService }, ], }).compileComponents(); } diff --git a/src/app/shared/search/search-export-csv/search-export-csv.component.ts b/src/app/shared/search/search-export-csv/search-export-csv.component.ts index 579cabca94..6544770931 100644 --- a/src/app/shared/search/search-export-csv/search-export-csv.component.ts +++ b/src/app/shared/search/search-export-csv/search-export-csv.component.ts @@ -2,7 +2,9 @@ import { AsyncPipe } from '@angular/common'; import { Component, Input, + OnChanges, OnInit, + SimpleChanges, } from '@angular/core'; import { Router } from '@angular/router'; import { NgbTooltipModule } from '@ng-bootstrap/ng-bootstrap'; @@ -18,10 +20,12 @@ import { switchMap, } from 'rxjs/operators'; +import { ConfigurationDataService } from '../../../core/data/configuration-data.service'; import { AuthorizationDataService } from '../../../core/data/feature-authorization/authorization-data.service'; import { FeatureID } from '../../../core/data/feature-authorization/feature-id'; import { ScriptDataService } from '../../../core/data/processes/script-data.service'; import { RemoteData } from '../../../core/data/remote-data'; +import { ConfigurationProperty } from '../../../core/shared/configuration-property.model'; import { getFirstCompletedRemoteData } from '../../../core/shared/operators'; import { getProcessDetailRoute } from '../../../process-page/process-page-routing.paths'; import { Process } from '../../../process-page/processes/process.model'; @@ -43,13 +47,18 @@ import { SearchFilter } from '../models/search-filter.model'; /** * Display a button to export the current search results as csv */ -export class SearchExportCsvComponent implements OnInit { +export class SearchExportCsvComponent implements OnInit, OnChanges { /** * The current configuration of the search */ @Input() searchConfig: PaginatedSearchOptions; + /** + * The total number of items in the search results which can be exported + */ + @Input() total: number; + /** * Observable used to determine whether the button should be shown */ @@ -60,12 +69,18 @@ export class SearchExportCsvComponent implements OnInit { */ tooltipMsg = 'metadata-export-search.tooltip'; + exportLimitExceededKey = 'metadata-export-search.submit.error.limit-exceeded'; + + exportLimitExceededMsg = ''; + + shouldShowWarning$: Observable; + constructor(private scriptDataService: ScriptDataService, private authorizationDataService: AuthorizationDataService, private notificationsService: NotificationsService, private translateService: TranslateService, private router: Router, - ) { + private configurationService: ConfigurationDataService) { } ngOnInit(): void { @@ -75,6 +90,31 @@ export class SearchExportCsvComponent implements OnInit { map((canExecute: boolean) => canExecute), startWith(false), ); + this.shouldShowWarning$ = this.itemExceeds(); + } + + ngOnChanges(changes: SimpleChanges): void { + if (changes.total) { + this.shouldShowWarning$ = this.itemExceeds(); + } + } + + /** + * Checks if the export limit has been exceeded and updates the tooltip accordingly + */ + private itemExceeds(): Observable { + return this.configurationService.findByPropertyName('bulkedit.export.max.items').pipe( + getFirstCompletedRemoteData(), + map((response: RemoteData) => { + const limit = Number(response.payload?.values?.[0]) || 500; + if (limit < this.total) { + this.exportLimitExceededMsg = this.translateService.instant(this.exportLimitExceededKey, { limit: String(limit) }); + return true; + } else { + return false; + } + }), + ); } /** diff --git a/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html index b25a602996..1ed8ba0fc6 100644 --- a/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-authority-filter/search-authority-filter.component.html @@ -13,13 +13,13 @@
    @if ((isLastPage$ | async) !== true) { + (click)="showMore()" href="javascript:void(0);" role="button" tabindex="0"> {{"search.filters.filter.show-more" | translate}} } @if ((currentPage | async) > 1) { + (click)="showFirstPageOnly()" href="javascript:void(0);" role="button" tabindex="0"> {{"search.filters.filter.show-less" | translate}} } diff --git a/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html index c5abc198a6..bd8d2c32fe 100644 --- a/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-boolean-filter/search-boolean-filter.component.html @@ -13,13 +13,13 @@
    @if ((isLastPage$ | async) !== true) { + (click)="showMore()" href="javascript:void(0);" role="button" tabindex="0"> {{"search.filters.filter.show-more" | translate}} } @if ((currentPage | async) > 1) { + (click)="showFirstPageOnly()" href="javascript:void(0);" role="button" tabindex="0"> {{"search.filters.filter.show-less" | translate}} } diff --git a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html index 75387550b2..767de25ae8 100644 --- a/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-facet-filter-options/search-facet-option/search-facet-option.component.html @@ -5,7 +5,7 @@ [queryParams]="addQueryParams$ | async" (click)="announceFilter(); filterService.minimizeAll()">
    } -
    diff --git a/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.html b/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.html index b25a602996..1ed8ba0fc6 100644 --- a/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.html +++ b/src/app/shared/search/search-filters/search-filter/search-text-filter/search-text-filter.component.html @@ -13,13 +13,13 @@
    @if ((isLastPage$ | async) !== true) { + (click)="showMore()" href="javascript:void(0);" role="button" tabindex="0"> {{"search.filters.filter.show-more" | translate}} } @if ((currentPage | async) > 1) { + (click)="showFirstPageOnly()" href="javascript:void(0);" role="button" tabindex="0"> {{"search.filters.filter.show-less" | translate}} } diff --git a/src/app/shared/search/search-filters/search-filters.component.html b/src/app/shared/search/search-filters/search-filters.component.html index 8ebfb4095b..bea2ea9467 100644 --- a/src/app/shared/search/search-filters/search-filters.component.html +++ b/src/app/shared/search/search-filters/search-filters.component.html @@ -5,14 +5,14 @@ } @if ((filters | async)?.hasSucceeded) { -
    +
    @for (filter of (filters | async)?.payload; track filter.name) { }
    } -@if(getFinalFiltersComputed(this.currentConfiguration) !== (filters | async)?.payload?.length) { +@if(getCurrentFiltersComputed(this.currentConfiguration) < (filters | async)?.payload?.length) { } diff --git a/src/app/shared/search/search-filters/search-filters.component.ts b/src/app/shared/search/search-filters/search-filters.component.ts index aee3257614..ed1e83b717 100644 --- a/src/app/shared/search/search-filters/search-filters.component.ts +++ b/src/app/shared/search/search-filters/search-filters.component.ts @@ -251,18 +251,8 @@ export class SearchFiltersComponent implements OnInit { * @param configuration The configuration identifier to get the count for * @returns The number of computed filters, or 0 if none found */ - private getCurrentFiltersComputed(configuration: string) { + getCurrentFiltersComputed(configuration: string): number { const configFilter = this.findConfigInCurrentFilters(configuration); return configFilter?.filtersComputed || 0; } - - /** - * Gets the final number of computed filters for a specific configuration - * @param configuration The configuration identifier to get the count for - * @returns The number of computed filters in the final state, or 0 if none found - */ - getFinalFiltersComputed(configuration: string): number { - const configFilter = this.findConfigInFinalFilters(configuration); - return configFilter?.filtersComputed || 0; - } } diff --git a/src/app/shared/search/search-results/search-results.component.html b/src/app/shared/search/search-results/search-results.component.html index 0026d0ea5b..e1039b8d31 100644 --- a/src/app/shared/search/search-results/search-results.component.html +++ b/src/app/shared/search/search-results/search-results.component.html @@ -15,7 +15,7 @@

    {{ (configuration ? configuration + '.search.results.head' : 'search.results.head') | translate }}

    } @if (showCsvExport) { - + }
    @if (searchResults && searchResults?.hasSucceeded && !searchResults?.isLoading && searchResults?.payload?.page.length > 0) { @@ -25,6 +25,7 @@ [sortConfig]="searchConfig.sort" [objects]="searchResults" [hideGear]="true" + [showRSS]="true" [selectable]="selectable" [selectionConfig]="selectionConfig" [linkType]="linkType" @@ -54,7 +55,7 @@ {{ 'search.results.no-results' | translate }} + queryParamsHandling="merge" role="link" tabindex="0"> {{"search.results.no-results-link" | translate}}
    diff --git a/src/app/shared/starts-with/date/starts-with-date.component.html b/src/app/shared/starts-with/date/starts-with-date.component.html index cd07a898c0..7c80d357a8 100644 --- a/src/app/shared/starts-with/date/starts-with-date.component.html +++ b/src/app/shared/starts-with/date/starts-with-date.component.html @@ -32,7 +32,7 @@
    - +
    diff --git a/src/app/shared/starts-with/text/starts-with-text.component.html b/src/app/shared/starts-with/text/starts-with-text.component.html index 5208427f34..6e70dc122b 100644 --- a/src/app/shared/starts-with/text/starts-with-text.component.html +++ b/src/app/shared/starts-with/text/starts-with-text.component.html @@ -3,7 +3,7 @@
    - +
    {{'browse.startsWith.type_text' | translate}} diff --git a/src/app/shared/testing/eperson.mock.ts b/src/app/shared/testing/eperson.mock.ts index 002ee9326f..36daad1e57 100644 --- a/src/app/shared/testing/eperson.mock.ts +++ b/src/app/shared/testing/eperson.mock.ts @@ -91,3 +91,43 @@ export const EPersonMock2: EPerson = Object.assign(new EPerson(), { ], }, }); + +export const EPersonMockWithNoName: EPerson = Object.assign(new EPerson(), { + handle: null, + groups: [], + netid: 'test@test.com', + lastActive: '2018-05-14T12:25:42.411+0000', + canLogIn: true, + email: 'test@test.com', + requireCertificate: false, + selfRegistered: false, + _links: { + self: { + href: 'https://rest.api/dspace-spring-rest/api/eperson/epersons/testid', + }, + groups: { href: 'https://rest.api/dspace-spring-rest/api/eperson/epersons/testid/groups' }, + }, + id: 'testid', + uuid: 'testid', + type: 'eperson', + metadata: { + 'dc.title': [ + { + language: null, + value: 'User Test', + }, + ], + 'eperson.lastname': [ + { + language: null, + value: 'Test', + }, + ], + 'eperson.language': [ + { + language: null, + value: 'en', + }, + ], + }, +}); diff --git a/src/app/shared/theme-support/themed.component.ts b/src/app/shared/theme-support/themed.component.ts index 84b1163d7b..2c6b9c8836 100644 --- a/src/app/shared/theme-support/themed.component.ts +++ b/src/app/shared/theme-support/themed.component.ts @@ -99,6 +99,9 @@ export abstract class ThemedComponent implements AfterViewInit } initComponentInstance(changes?: SimpleChanges) { + if (hasValue(this.themeSub)) { + this.themeSub.unsubscribe(); + } this.themeSub = this.themeService?.getThemeName$().subscribe(() => { this.renderComponentInstance(changes); }); diff --git a/src/app/shared/truncatable/truncatable-part/truncatable-part.component.html b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.html index fef02ea6d7..a0ec379f54 100644 --- a/src/app/shared/truncatable/truncatable-part/truncatable-part.component.html +++ b/src/app/shared/truncatable/truncatable-part/truncatable-part.component.html @@ -10,6 +10,7 @@ (keyup.Space)="toggle()" role="button" [attr.aria-expanded]="isExpanded" + tabindex="0" > {{ 'item.truncatable-part.show-' + (isExpanded ? 'less' : 'more') | translate }} diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.html b/src/app/statistics-page/statistics-table/statistics-table.component.html index 4a0f87076c..0cc4f5bfbc 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.html +++ b/src/app/statistics-page/statistics-table/statistics-table.component.html @@ -11,7 +11,7 @@ @for (header of headers; track header) {
    - {{ header }} + {{ 'statistics.table.header.' + header | translate }}
    - {{ getLabel(point) | async }} + {{ point.label }} { expect(de.query(By.css('table'))).toBeTruthy(); expect(de.query(By.css('th.views-header')).nativeElement.innerText) - .toEqual('views'); + .toEqual('statistics.table.header.views'); expect(de.query(By.css('th.downloads-header')).nativeElement.innerText) - .toEqual('downloads'); + .toEqual('statistics.table.header.downloads'); expect(de.query(By.css('td.item_1-views-data')).nativeElement.innerText) .toEqual('7'); diff --git a/src/app/statistics-page/statistics-table/statistics-table.component.ts b/src/app/statistics-page/statistics-table/statistics-table.component.ts index 9f59e33fa8..d3cf80330e 100644 --- a/src/app/statistics-page/statistics-table/statistics-table.component.ts +++ b/src/app/statistics-page/statistics-table/statistics-table.component.ts @@ -4,27 +4,11 @@ import { Input, OnInit, } from '@angular/core'; -import { - TranslateModule, - TranslateService, -} from '@ngx-translate/core'; -import { - Observable, - of, -} from 'rxjs'; -import { map } from 'rxjs/operators'; +import { TranslateModule } from '@ngx-translate/core'; import { DSONameService } from '../../core/breadcrumbs/dso-name.service'; import { DSpaceObjectDataService } from '../../core/data/dspace-object-data.service'; -import { - getFinishedRemoteData, - getRemoteDataPayload, -} from '../../core/shared/operators'; -import { - Point, - UsageReport, -} from '../../core/statistics/models/usage-report.model'; -import { isEmpty } from '../../shared/empty.util'; +import { UsageReport } from '../../core/statistics/models/usage-report.model'; /** * Component representing a statistics table for a given usage report. @@ -57,7 +41,6 @@ export class StatisticsTableComponent implements OnInit { constructor( protected dsoService: DSpaceObjectDataService, protected nameService: DSONameService, - private translateService: TranslateService, ) { } @@ -68,23 +51,4 @@ export class StatisticsTableComponent implements OnInit { this.headers = Object.keys(this.report.points[0].values); } } - - /** - * Get the row label to display for a statistics point. - * @param point the statistics point to get the label for - */ - getLabel(point: Point): Observable { - switch (this.report.reportType) { - case 'TotalVisits': - return this.dsoService.findById(point.id).pipe( - getFinishedRemoteData(), - getRemoteDataPayload(), - map((item) => !isEmpty(item) ? this.nameService.getName(item) : this.translateService.instant('statistics.table.no-name')), - ); - case 'TopCities': - case 'topCountries': - default: - return of(point.label); - } - } } diff --git a/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.ts b/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.ts new file mode 100644 index 0000000000..004864cc68 --- /dev/null +++ b/src/app/statistics/angulartics/dspace/view-tracker-resolver.service.ts @@ -0,0 +1,64 @@ +import { Injectable } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveEnd, + Router, + RouterStateSnapshot, +} from '@angular/router'; +import { Angulartics2 } from 'angulartics2'; +import { switchMap } from 'rxjs'; +import { + filter, + take, +} from 'rxjs/operators'; + +import { ReferrerService } from '../../../core/services/referrer.service'; + +/** + * This component triggers a page view statistic + */ +@Injectable({ + providedIn: 'root', +}) +export class ViewTrackerResolverService { + + constructor( + public angulartics2: Angulartics2, + public referrerService: ReferrerService, + public router: Router, + ) { + } + + resolve(routeSnapshot: ActivatedRouteSnapshot, state: RouterStateSnapshot): boolean { + const dsoPath = routeSnapshot.data.dsoPath || 'dso.payload'; // Fetch the resolvers passed via the route data + this.router.events.pipe( + filter(event => event instanceof ResolveEnd), + take(1), + switchMap(() => + this.referrerService.getReferrer().pipe(take(1)))) + .subscribe((referrer: string) => { + this.angulartics2.eventTrack.next({ + action: 'page_view', + properties: { + object: this.getNestedProperty(routeSnapshot.data, dsoPath), + referrer, + }, + }); + }); + return true; + } + + private getNestedProperty(obj: any, path: string) { + const keys = path.split('.'); + let result = obj; + + for (const key of keys) { + if (result && result.hasOwnProperty(key)) { + result = result[key]; + } else { + return undefined; + } + } + return result; + } +} diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.html b/src/app/statistics/angulartics/dspace/view-tracker.component.html deleted file mode 100644 index c0c0ffe181..0000000000 --- a/src/app/statistics/angulartics/dspace/view-tracker.component.html +++ /dev/null @@ -1 +0,0 @@ -  diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.scss b/src/app/statistics/angulartics/dspace/view-tracker.component.scss deleted file mode 100644 index c76cafbe44..0000000000 --- a/src/app/statistics/angulartics/dspace/view-tracker.component.scss +++ /dev/null @@ -1,3 +0,0 @@ -:host { - display: none -} diff --git a/src/app/statistics/angulartics/dspace/view-tracker.component.ts b/src/app/statistics/angulartics/dspace/view-tracker.component.ts deleted file mode 100644 index 801f4fd2cb..0000000000 --- a/src/app/statistics/angulartics/dspace/view-tracker.component.ts +++ /dev/null @@ -1,63 +0,0 @@ -import { - Component, - Input, - OnDestroy, - OnInit, -} from '@angular/core'; -import { Angulartics2 } from 'angulartics2'; -import { Subscription } from 'rxjs'; -import { take } from 'rxjs/operators'; - -import { ReferrerService } from '../../../core/services/referrer.service'; -import { DSpaceObject } from '../../../core/shared/dspace-object.model'; -import { hasValue } from '../../../shared/empty.util'; - -/** - * This component triggers a page view statistic - */ -@Component({ - selector: 'ds-view-tracker', - styleUrls: ['./view-tracker.component.scss'], - templateUrl: './view-tracker.component.html', - standalone: true, -}) -export class ViewTrackerComponent implements OnInit, OnDestroy { - /** - * The DSpaceObject to track a view event about - */ - @Input() object: DSpaceObject; - - /** - * The subscription on this.referrerService.getReferrer() - * @protected - */ - protected sub: Subscription; - - constructor( - public angulartics2: Angulartics2, - public referrerService: ReferrerService, - ) { - } - - ngOnInit(): void { - this.sub = this.referrerService.getReferrer() - .pipe(take(1)) - .subscribe((referrer: string) => { - this.angulartics2.eventTrack.next({ - action: 'page_view', - properties: { - object: this.object, - referrer, - }, - }); - }); - } - - ngOnDestroy(): void { - // unsubscribe in the case that this component is destroyed before - // this.referrerService.getReferrer() has emitted - if (hasValue(this.sub)) { - this.sub.unsubscribe(); - } - } -} diff --git a/src/app/statistics/angulartics/dspace/view-tracker.resolver.ts b/src/app/statistics/angulartics/dspace/view-tracker.resolver.ts new file mode 100644 index 0000000000..78b6bb6f8a --- /dev/null +++ b/src/app/statistics/angulartics/dspace/view-tracker.resolver.ts @@ -0,0 +1,16 @@ +import { inject } from '@angular/core'; +import { + ActivatedRouteSnapshot, + ResolveFn, + RouterStateSnapshot, +} from '@angular/router'; + +import { ViewTrackerResolverService } from './view-tracker-resolver.service'; + +export const viewTrackerResolver: ResolveFn = ( + route: ActivatedRouteSnapshot, + state: RouterStateSnapshot, + viewTrackerResolverService: ViewTrackerResolverService = inject(ViewTrackerResolverService), +): boolean => { + return viewTrackerResolverService.resolve(route, state); +}; diff --git a/src/app/statistics/matomo.service.spec.ts b/src/app/statistics/matomo.service.spec.ts index 51ea814231..be14585c2b 100644 --- a/src/app/statistics/matomo.service.spec.ts +++ b/src/app/statistics/matomo.service.spec.ts @@ -23,6 +23,7 @@ import { createSuccessfulRemoteDataObject$, } from '../shared/remote-data.utils'; import { + MATOMO_ENABLED, MATOMO_SITE_ID, MATOMO_TRACKER_URL, MatomoService, @@ -84,6 +85,9 @@ describe('MatomoService', () => { configService.findByPropertyName.withArgs(MATOMO_TRACKER_URL).and.returnValue( createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(),{ values: ['http://matomo'] })), ); + configService.findByPropertyName.withArgs(MATOMO_ENABLED).and.returnValue( + createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(),{ values: ['true'] })), + ); configService.findByPropertyName.withArgs(MATOMO_SITE_ID).and.returnValue( createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { values: ['1'] }))); orejimeService.getSavedPreferences.and.returnValue(of({ matomo: true })); @@ -102,6 +106,9 @@ describe('MatomoService', () => { configService.findByPropertyName.withArgs(MATOMO_TRACKER_URL).and.returnValue( createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(),{ values: ['http://example.com'] })), ); + configService.findByPropertyName.withArgs(MATOMO_ENABLED).and.returnValue( + createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(),{ values: ['true'] })), + ); configService.findByPropertyName.withArgs(MATOMO_SITE_ID).and.returnValue( createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { values: ['1'] }))); orejimeService.getSavedPreferences.and.returnValue(of({ matomo: true })); @@ -123,6 +130,24 @@ describe('MatomoService', () => { expect(matomoInitializer.initializeTracker).not.toHaveBeenCalled(); }); + it('should not initialize tracker if matomo is disabled', () => { + environment.production = true; + environment.matomo = { trackerUrl: '' }; + configService.findByPropertyName.withArgs(MATOMO_TRACKER_URL).and.returnValue( + createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(),{ values: ['http://example.com'] })), + ); + configService.findByPropertyName.withArgs(MATOMO_ENABLED).and.returnValue( + createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(),{ values: ['false'] })), + ); + configService.findByPropertyName.withArgs(MATOMO_SITE_ID).and.returnValue( + createSuccessfulRemoteDataObject$(Object.assign(new ConfigurationProperty(), { values: ['1'] }))); + orejimeService.getSavedPreferences.and.returnValue(of({ matomo: true })); + + service.init(); + + expect(matomoInitializer.initializeTracker).not.toHaveBeenCalled(); + }); + describe('with visitorId set', () => { beforeEach(() => { matomoTracker.getVisitorId.and.returnValue(Promise.resolve('12345')); diff --git a/src/app/statistics/matomo.service.ts b/src/app/statistics/matomo.service.ts index 88fc7f9476..a30e82f93a 100644 --- a/src/app/statistics/matomo.service.ts +++ b/src/app/statistics/matomo.service.ts @@ -77,10 +77,10 @@ export class MatomoService { preferences$ .pipe( tap(preferences => this.changeMatomoConsent(preferences?.matomo)), - switchMap(_ => combineLatest([this.getSiteId$(), this.getTrackerUrl$()])), + switchMap(_ => combineLatest([this.isMatomoEnabled$(), this.getSiteId$(), this.getTrackerUrl$()])), ) - .subscribe(([siteId, trackerUrl]) => { - if (siteId && trackerUrl) { + .subscribe(([isMatomoEnabled, siteId, trackerUrl]) => { + if (isMatomoEnabled && siteId && trackerUrl) { this.matomoInitializer.initializeTracker({ siteId, trackerUrl }); } }); diff --git a/src/app/submission/edit/submission-edit.component.spec.ts b/src/app/submission/edit/submission-edit.component.spec.ts index 59f9883f19..29d872f542 100644 --- a/src/app/submission/edit/submission-edit.component.spec.ts +++ b/src/app/submission/edit/submission-edit.component.spec.ts @@ -105,6 +105,10 @@ describe('SubmissionEditComponent Component', () => { }); afterEach(() => { + if (fixture) { + // Ensure Angular cleans up the component properly + fixture.destroy(); + } comp = null; fixture = null; router = null; diff --git a/src/app/submission/edit/submission-edit.component.ts b/src/app/submission/edit/submission-edit.component.ts index 822818ee24..c2a914c85d 100644 --- a/src/app/submission/edit/submission-edit.component.ts +++ b/src/app/submission/edit/submission-edit.component.ts @@ -36,7 +36,7 @@ import { isNotNull, } from '../../shared/empty.util'; import { NotificationsService } from '../../shared/notifications/notifications.service'; -import { SubmissionFormComponent } from '../form/submission-form.component'; +import { ThemedSubmissionFormComponent } from '../form/themed-submission-form.component'; import { SubmissionError } from '../objects/submission-error.model'; import { SubmissionService } from '../submission.service'; import parseSectionErrors from '../utils/parseSectionErrors'; @@ -50,7 +50,7 @@ import parseSectionErrors from '../utils/parseSectionErrors'; templateUrl: './submission-edit.component.html', standalone: true, imports: [ - SubmissionFormComponent, + ThemedSubmissionFormComponent, ], }) export class SubmissionEditComponent implements OnDestroy, OnInit { diff --git a/src/app/submission/form/footer/submission-form-footer.component.html b/src/app/submission/form/footer/submission-form-footer.component.html index 1001d4c619..671888e824 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.html +++ b/src/app/submission/form/footer/submission-form-footer.component.html @@ -1,6 +1,6 @@ @if (!!submissionId) { -
    -
    +
    +
    @if ((showDepositAndDiscard | async)) {
    -
    +
    @if ((hasUnsavedModification | async) !== true && (processingSaveStatus | async) !== true && (processingDepositStatus | async) !== true) { {{'submission.general.info.saved' | translate}} diff --git a/src/app/submission/form/footer/submission-form-footer.component.ts b/src/app/submission/form/footer/submission-form-footer.component.ts index 8645003783..a61e2599a2 100644 --- a/src/app/submission/form/footer/submission-form-footer.component.ts +++ b/src/app/submission/form/footer/submission-form-footer.component.ts @@ -24,7 +24,7 @@ import { SubmissionService } from '../../submission.service'; * This component represents submission form footer bar. */ @Component({ - selector: 'ds-submission-form-footer', + selector: 'ds-base-submission-form-footer', styleUrls: ['./submission-form-footer.component.scss'], templateUrl: './submission-form-footer.component.html', standalone: true, diff --git a/src/app/submission/form/footer/themed-submission-form-footer.component.ts b/src/app/submission/form/footer/themed-submission-form-footer.component.ts new file mode 100644 index 0000000000..82240abcf5 --- /dev/null +++ b/src/app/submission/form/footer/themed-submission-form-footer.component.ts @@ -0,0 +1,33 @@ +import { + Component, + Input, +} from '@angular/core'; + +import { ThemedComponent } from '../../../shared/theme-support/themed.component'; +import { SubmissionFormFooterComponent } from './submission-form-footer.component'; + +@Component({ + selector: 'ds-submission-form-footer', + styleUrls: [], + templateUrl: '../../../shared/theme-support/themed.component.html', + standalone: true, + imports: [SubmissionFormFooterComponent], +}) +export class ThemedSubmissionFormFooterComponent extends ThemedComponent { + @Input() submissionId: string; + + protected inAndOutputNames: (keyof SubmissionFormFooterComponent & keyof this)[] = ['submissionId']; + + protected getComponentName(): string { + return 'SubmissionFormFooterComponent'; + } + + protected importThemedComponent(themeName: string): Promise { + return import(`../../../../themes/${themeName}/app/submission/form/footer/submission-form-footer.component`); + } + + protected importUnthemedComponent(): Promise { + return import(`./submission-form-footer.component`); + } + +} diff --git a/src/app/submission/form/section-add/submission-form-section-add.component.html b/src/app/submission/form/section-add/submission-form-section-add.component.html index 5f2a26b8a1..21c6322700 100644 --- a/src/app/submission/form/section-add/submission-form-section-add.component.html +++ b/src/app/submission/form/section-add/submission-form-section-add.component.html @@ -2,12 +2,12 @@ #sectionAdd="ngbDropdown" placement="bottom-right" class="d-inline-block" - [ngClass]="{'w-100': windowService.isXs()}"> + [ngClass]="{'w-100': isXs$}"> @if (hasSections$ | async) { @@ -15,7 +15,7 @@
    + [ngClass]="{'w-100': (isXs$ | async)}"> @if ((hasSections$ | async) !== true) {