diff --git a/package-lock.json b/package-lock.json index 395232425..23bdf0250 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,35 +1,35 @@ { "name": "osf", - "version": "26.16.1", + "version": "26.19.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "osf", - "version": "26.16.1", + "version": "26.19.0", "dependencies": { - "@angular/animations": "^21.2.19", + "@angular/animations": "^21.2.25", "@angular/cdk": "^21.2.14", - "@angular/common": "^21.2.19", - "@angular/compiler": "^21.2.19", - "@angular/core": "^21.2.19", - "@angular/forms": "^21.2.19", - "@angular/platform-browser": "^21.2.19", - "@angular/platform-browser-dynamic": "^21.2.19", - "@angular/platform-server": "^21.2.19", - "@angular/router": "^21.2.19", - "@angular/ssr": "^21.2.20", + "@angular/common": "^21.2.25", + "@angular/compiler": "^21.2.25", + "@angular/core": "^21.2.25", + "@angular/forms": "^21.2.25", + "@angular/platform-browser": "^21.2.25", + "@angular/platform-browser-dynamic": "^21.2.25", + "@angular/platform-server": "^21.2.25", + "@angular/router": "^21.2.25", + "@angular/ssr": "^21.2.24", "@centerforopenscience/markdown-it-atrules": "^0.1.2", "@citation-js/core": "^0.7.21", "@citation-js/plugin-csl": "^0.7.21", - "@fortawesome/fontawesome-free": "^7.2.0", + "@fortawesome/fontawesome-free": "^7.3.1", "@mdit/plugin-img-size": "0.23.1", - "@newrelic/browser-agent": "^1.310.1", + "@newrelic/browser-agent": "^1.323.0", "@ngx-translate/core": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0", "@ngxs/store": "^21.0.0", "@primeuix/themes": "^2.0.3", - "@sentry/angular": "^10.42.0", + "@sentry/angular": "^10.75.3", "@traptitech/markdown-it-katex": "^3.6.0", "ace-builds": "1.43.6", "angular-google-tag-manager": "^1.13.0", @@ -47,29 +47,29 @@ "ngx-markdown-editor": "^5.3.4", "primeflex": "^4.0.0", "primeicons": "^7.0.0", - "primeng": "^21.1.3", + "primeng": "^21.1.10", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, "devDependencies": { - "@angular-eslint/eslint-plugin": "^21.3.0", - "@angular-eslint/eslint-plugin-template": "^21.3.0", - "@angular-eslint/template-parser": "^21.3.0", - "@angular/build": "^21.2.20", - "@angular/cli": "^21.2.20", - "@angular/compiler-cli": "^21.2.19", - "@commitlint/cli": "^20.4.3", - "@commitlint/config-conventional": "^20.4.3", + "@angular-eslint/eslint-plugin": "^21.4.0", + "@angular-eslint/eslint-plugin-template": "^21.4.0", + "@angular-eslint/template-parser": "^21.4.0", + "@angular/build": "^21.2.24", + "@angular/cli": "^21.2.24", + "@angular/compiler-cli": "^21.2.25", + "@commitlint/cli": "^20.5.3", + "@commitlint/config-conventional": "^20.5.3", "@eslint/js": "^10.0.1", "@types/express": "^5.0.1", "@types/gapi": "^0.0.47", "@types/gapi.auth2": "^0.0.61", "@types/markdown-it": "^14.1.2", "@types/node": "^20.17.19", - "@vitest/coverage-v8": "^4.1.9", - "angular-eslint": "^21.3.0", + "@vitest/coverage-v8": "^4.1.11", + "angular-eslint": "^21.4.0", "angularx-qrcode": "^21.0.4", - "eslint": "^10.0.2", + "eslint": "^10.11.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-simple-import-sort": "^12.1.1", @@ -77,12 +77,12 @@ "husky": "^9.1.7", "jsdom": "^29.1.1", "lint-staged": "^16.3.2", - "ng-mocks": "^14.15.3", + "ng-mocks": "^14.18.1", "prettier": "3.8.1", "source-map-explorer": "^2.5.3", "typescript": "~5.9.3", - "typescript-eslint": "^8.56.1", - "vitest": "^4.1.9" + "typescript-eslint": "^8.71.0", + "vitest": "^4.1.11" } }, "node_modules/@algolia/abtesting": { @@ -309,13 +309,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.2102.20", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.20.tgz", - "integrity": "sha512-s7wPFCMrt9mWMr+NWs4T8849snnOE4DcmLb9Set7KtbhV6Z8E7ZASs/wqPOS3KPxLjxTqM6CnkwKhc3Th6DniA==", + "version": "0.2102.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.2102.24.tgz", + "integrity": "sha512-U/d5l/1rfrNSlbGjMTunyzwxh+2vJce3y0r0tIx/NOFZbnYhq+gx+e33mR6oxI4yk/1Jks67ltvstrJuBbVbYg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.20", + "@angular-devkit/core": "21.2.24", "rxjs": "7.8.2" }, "bin": { @@ -328,9 +328,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "21.2.20", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.20.tgz", - "integrity": "sha512-QViRAYFj3jcElWND79hM6y4vTSYhMlJSOjy9nby9JsQaPgetkf039jI2u9Lp+PduE6lysewzf85d1UGsm3eI0A==", + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-21.2.24.tgz", + "integrity": "sha512-QLYLd6OcfQS5qWTlzG1/d8lFTcJDFcsY6sOaGVtrruYiDxrbeM2iJttoIoKzvbNPROFyo3fANVQJOVhytpXWPg==", "dev": true, "license": "MIT", "dependencies": { @@ -356,13 +356,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "21.2.20", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.20.tgz", - "integrity": "sha512-XH0BtcqSwHlyLRzvbZccSEe5Hcl7Yex8fXfj3gMVBB5g+KBUm2OGtGN3lDOl5BtrwrZBVnNNXkg0ehNrWzxVgw==", + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-21.2.24.tgz", + "integrity": "sha512-mLOme2vfbLrHFmMyM6ybPuGCnCvPiemXWdHnptY2Q7Hzum72DabfgtTNsrMcI2F5zrQV2DDQunNjC5cXqtq4/w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.20", + "@angular-devkit/core": "21.2.24", "jsonc-parser": "3.3.1", "magic-string": "0.30.21", "ora": "9.3.0", @@ -375,9 +375,9 @@ } }, "node_modules/@angular-eslint/builder": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.3.1.tgz", - "integrity": "sha512-1f1Lyp5e7OH6txiV224HaY3G1uRCj91OSKq7hT2Vw9NRw6zWFc1anBpDeLVjpL9ptUxzUGIQR5jEV54hOPayoQ==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/builder/-/builder-21.4.0.tgz", + "integrity": "sha512-3kgGmrVaCYbLtDjC8g4BmMBbdz4thsOB8/NYly8JtXM8EuDZEk5Pz6VTRpJR02ARprwayraTTmhyvq6OGBlQ9w==", "dev": true, "license": "MIT", "dependencies": { @@ -391,21 +391,21 @@ } }, "node_modules/@angular-eslint/bundled-angular-compiler": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.3.1.tgz", - "integrity": "sha512-jjbnJPUXQeQBJ8RM+ahlbt4GH2emVN8JvG3AhFbPci1FrqXi9cOOfkbwLmvpoyTli4LF8gy7g4ctFqnlRgqryw==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/bundled-angular-compiler/-/bundled-angular-compiler-21.4.0.tgz", + "integrity": "sha512-/3H4BPbQ1BHJkkrUsfusZtmHc+qiFWBBZ9UDPWah4xZMjflexOK9U4GYeH7nMjcuyqFnIlMMeJJNwNLGt/hmdg==", "dev": true, "license": "MIT" }, "node_modules/@angular-eslint/eslint-plugin": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.3.1.tgz", - "integrity": "sha512-08NNTxwawRLTWPLl8dg1BnXMwimx93y4wMEwx2aWQpJbIt4pmNvwJzd+NgoD/Ag2VdLS/gOMadhJH5fgaYKsPQ==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin/-/eslint-plugin-21.4.0.tgz", + "integrity": "sha512-mow2DMj+xBvGl5t7jzC34R8YfbHbaGNyCNFzpovtl9qc0JbuqLyg6htmt8xb05f8ZjATOr4nz0ESt6HV4c51hw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.3.1", - "@angular-eslint/utils": "21.3.1", + "@angular-eslint/bundled-angular-compiler": "21.4.0", + "@angular-eslint/utils": "21.4.0", "ts-api-utils": "^2.1.0" }, "peerDependencies": { @@ -415,19 +415,19 @@ } }, "node_modules/@angular-eslint/eslint-plugin-template": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.3.1.tgz", - "integrity": "sha512-ndPWJodkcEOu2PVUxlUwyz4D2u3r9KO7veWmStVNOLeNrICJA+nQvrz2BWCu0l48rO0K5ezsy0JFcQDVwE/5mw==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/eslint-plugin-template/-/eslint-plugin-template-21.4.0.tgz", + "integrity": "sha512-sJEHx2WYnvOgPpzP1eHnUdRS06zgKmRxbiIR0JiCcaSen5iv1HlsMieXy//FS9TtNW+abHOy4UtDuGuSPflPFA==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.3.1", - "@angular-eslint/utils": "21.3.1", + "@angular-eslint/bundled-angular-compiler": "21.4.0", + "@angular-eslint/utils": "21.4.0", "aria-query": "5.3.2", "axobject-query": "4.1.0" }, "peerDependencies": { - "@angular-eslint/template-parser": "21.3.1", + "@angular-eslint/template-parser": "21.4.0", "@typescript-eslint/types": "^7.11.0 || ^8.0.0", "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", @@ -435,16 +435,16 @@ } }, "node_modules/@angular-eslint/schematics": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.3.1.tgz", - "integrity": "sha512-1U2u4ZsZvwT30aXRLsIJf6tULIiioo9BtASNsldpYecU3/m/1+F61lCYG79qt7YWbif9KABPYZlFTJUFGN8HWA==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/schematics/-/schematics-21.4.0.tgz", + "integrity": "sha512-crD6Hfxs7x5bN9FCqTZI7uVSiGvprfCS3MCPOpyIQl87bRr/9aNhnicJ3ROUHv+2A713BgPHIgiCII/bxzrfPw==", "dev": true, "license": "MIT", "dependencies": { "@angular-devkit/core": ">= 21.0.0 < 22.0.0", "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", - "@angular-eslint/eslint-plugin": "21.3.1", - "@angular-eslint/eslint-plugin-template": "21.3.1", + "@angular-eslint/eslint-plugin": "21.4.0", + "@angular-eslint/eslint-plugin-template": "21.4.0", "ignore": "7.0.5", "semver": "7.7.4", "strip-json-comments": "3.1.1" @@ -454,13 +454,13 @@ } }, "node_modules/@angular-eslint/template-parser": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.3.1.tgz", - "integrity": "sha512-moERVCTekQKOvR8RMuEOtWSO3VS1qrzA3keI1dPto/JVB8Nqp9w3R5ZpEoXHzh4zgEryosxmPgdi6UczJe2ouQ==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/template-parser/-/template-parser-21.4.0.tgz", + "integrity": "sha512-BaUSLSyS+43fzDoJkTMkGqNdCXq3fGnUZsfXTmrlZPJf5AYFbgAlAPGZXDJyoNWw43fux+DafdlrlKcYUSgSIw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.3.1", + "@angular-eslint/bundled-angular-compiler": "21.4.0", "eslint-scope": "9.1.2" }, "peerDependencies": { @@ -469,13 +469,13 @@ } }, "node_modules/@angular-eslint/utils": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.3.1.tgz", - "integrity": "sha512-Q3SGA1/36phZhmsp1mYrKzp/jcmqofRr861MYn46FaWIKSYXBYRzl+H3FIJKBu5CE36Bggu6hbNpwGPuUp+MCg==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/@angular-eslint/utils/-/utils-21.4.0.tgz", + "integrity": "sha512-7pi+Ga7QmdH5Ig/diau6fR5L4yubgKr9TOjdCg7OeuE/zo0O3osTCNT6JOodzS/iQM1kSCJFDoIBKFeUOttiNw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-eslint/bundled-angular-compiler": "21.3.1" + "@angular-eslint/bundled-angular-compiler": "21.4.0" }, "peerDependencies": { "@typescript-eslint/utils": "^7.11.0 || ^8.0.0", @@ -484,9 +484,9 @@ } }, "node_modules/@angular/animations": { - "version": "21.2.19", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.19.tgz", - "integrity": "sha512-qC0mviselvpKq2ID9bT+USnCsHKfwZAifkUr5A4h/WCzXvUvpVNWp/RPhGcKeOR+seg3EO5GtIZyblwiRlhzrA==", + "version": "21.2.25", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-21.2.25.tgz", + "integrity": "sha512-75Ok3XvrFoP1lAATNqDQFN24RnGmkiOkzcMZ2B0JjYciHqDqp9Sb7v53suwtivW0UXfse4O5FjqMu4wQwzbFng==", "deprecated": "@angular/animations is deprecated. Use `animate.enter` and `animate.leave` instead. For more information see: https://v22.angular.dev/guide/animations.", "license": "MIT", "dependencies": { @@ -496,18 +496,18 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "21.2.19" + "@angular/core": "21.2.25" } }, "node_modules/@angular/build": { - "version": "21.2.20", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.20.tgz", - "integrity": "sha512-Dl9AX8e3mQpAl4RkmvoNnYRoRSpqMLQBqJYf/quupGLxMpQ55mOBhnGfzmoBLyETFhMUd329tvGtOcicPW/hdA==", + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-21.2.24.tgz", + "integrity": "sha512-NurM2W7Q/lCGonkYgSNNxGrh9AYH3LksekYRVxl0KxVKa8Z5x6oMYlLfyI4Lv9Q4C1/x1SXv3xNxgYu/nIW4vA==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.2102.20", + "@angular-devkit/architect": "0.2102.24", "@babel/core": "7.29.7", "@babel/helper-annotate-as-pure": "7.27.3", "@babel/helper-split-export-declaration": "7.24.7", @@ -530,7 +530,7 @@ "semver": "7.7.4", "source-map-support": "0.5.21", "tinyglobby": "0.2.15", - "undici": "7.28.0", + "undici": "7.29.1", "vite": "7.3.6", "watchpack": "2.5.1" }, @@ -550,7 +550,7 @@ "@angular/platform-browser": "^21.0.0", "@angular/platform-server": "^21.0.0", "@angular/service-worker": "^21.0.0", - "@angular/ssr": "^21.2.20", + "@angular/ssr": "^21.2.24", "karma": "^6.4.0", "less": "^4.2.0", "ng-packagr": "^21.0.0", @@ -600,9 +600,9 @@ } }, "node_modules/@angular/build/node_modules/undici": { - "version": "7.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.28.0.tgz", - "integrity": "sha512-cRZYrTDwWznlnRiPjggAGxZXanty6M8RV1ff8Wm4LWXBp7/IG8v5DnOm74DtUBp9OONpK75YlPnIjQqX0dBDtA==", + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.1.tgz", + "integrity": "sha512-RYONW2MeafgYlkVOKYKkA/Ag7BmXqgIWCa8t1m0JcxrQg9pI9lEqRhAOruOBCbAohOa/gkCF+iPi9hrgvTzu6Q==", "dev": true, "license": "MIT", "engines": { @@ -626,19 +626,19 @@ } }, "node_modules/@angular/cli": { - "version": "21.2.20", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.20.tgz", - "integrity": "sha512-ATzRaKSDWIUVHQDU14mO3EVrkoEddfm0b4L/uUJraG2tIevRGYStuzT1jIKlPl8mg+O1cDVIjiqdX114kPHczw==", + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-21.2.24.tgz", + "integrity": "sha512-lx8baI3gssBv+xuwEf5wvAoBi7XSIB1RRuAmSnuAO2E4CAlJmU4eiVhKxd/haXvONH6peYxFGH2N6mSrkkHfIQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.2102.20", - "@angular-devkit/core": "21.2.20", - "@angular-devkit/schematics": "21.2.20", + "@angular-devkit/architect": "0.2102.24", + "@angular-devkit/core": "21.2.24", + "@angular-devkit/schematics": "21.2.24", "@inquirer/prompts": "7.10.1", "@listr2/prompt-adapter-inquirer": "3.0.5", - "@modelcontextprotocol/sdk": "1.26.0", - "@schematics/angular": "21.2.20", + "@modelcontextprotocol/sdk": "1.30.0", + "@schematics/angular": "21.2.24", "@yarnpkg/lockfile": "1.1.0", "algoliasearch": "5.48.1", "ini": "6.0.0", @@ -661,9 +661,9 @@ } }, "node_modules/@angular/common": { - "version": "21.2.19", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.19.tgz", - "integrity": "sha512-Rvo/VXI0kUmfQT7+OeAjv526OJlf/WrLnJq1Tz84Jkyv9bs9SOMTCT6m4+boo8gxVNdrcxvGU3Q0o2jZzKceSg==", + "version": "21.2.25", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-21.2.25.tgz", + "integrity": "sha512-1oxSrFqMyJF6xYXBDrciZSe3A67E5feYQXhmxH3d0hLe0wWM/pFZTvCvSnA65wHj8NTonuI7/HMBPwRKddHoGw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -672,14 +672,14 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/core": "21.2.19", + "@angular/core": "21.2.25", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "21.2.19", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.19.tgz", - "integrity": "sha512-vuF5i1t14ftJiHXVVLgDYLWkT99QuajphcUHy1VZaMX3FiqSGXRV62g+R2RMxL0OJ5C1ai8xTHKPx9n1lQFyFg==", + "version": "21.2.25", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-21.2.25.tgz", + "integrity": "sha512-jR/TiwrHeLEOPT3fVp9q14UkCsBNni8xUIfof8F7JZW4Ze7/LpTLI2OWxajckWmR0+kCKam5RhWeFd5m/F553Q==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -689,9 +689,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "21.2.19", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.19.tgz", - "integrity": "sha512-QxWqUvhTWgyYPPkfpupOUIEa3Y5cbzMilaFgRNpkvFy6teARw5Izsd7RxUbj3Tp3xJOi1MtumNmkxLxmv3iv7A==", + "version": "21.2.25", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-21.2.25.tgz", + "integrity": "sha512-5qMekFc1QNJ7zxTqekOTRnY9WgYSo5nfqED14sou15uScepL+yDM07UrvVsR94CGc78/nhevYWLXbb4iPPZ00Q==", "dev": true, "license": "MIT", "dependencies": { @@ -712,7 +712,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.2.19", + "@angular/compiler": "21.2.25", "typescript": ">=5.9 <6.1" }, "peerDependenciesMeta": { @@ -722,9 +722,9 @@ } }, "node_modules/@angular/core": { - "version": "21.2.19", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.19.tgz", - "integrity": "sha512-PVoXD1kBexOJLkFzKx2zBY/0oZJXGru0eGn2hu0q5n3vkZlYsR1yRolfGenK1gZE48Qibiw/ttg/X/pAIPEZGg==", + "version": "21.2.25", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-21.2.25.tgz", + "integrity": "sha512-cku7EX0igTPnuqEWZ1NiQY4Pn1g2xBljQSk1+bXRKnudJiZCK8e1u2v0byNzWuKwmdz7kKXjz71o224HrtccVA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -733,7 +733,7 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/compiler": "21.2.19", + "@angular/compiler": "21.2.25", "rxjs": "^6.5.3 || ^7.4.0", "zone.js": "~0.15.0 || ~0.16.0" }, @@ -747,9 +747,9 @@ } }, "node_modules/@angular/forms": { - "version": "21.2.19", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.19.tgz", - "integrity": "sha512-tEw8cz2UU6VSB+ReJN86k87nRdLRXGi+8SZhoRl2dFu2iReIO3S6kJAVTH7Y9kdrokqTPhWG+KNEzWgqhdjXOg==", + "version": "21.2.25", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-21.2.25.tgz", + "integrity": "sha512-EQADo64/BaCKO1uGcQyHcYW1y2JwQMOfWQUteBsqFODMwoRQK96VH5Lf/8vUwvV7gvFrlA4FhVXjsv2KVdfPuw==", "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.0.0", @@ -759,16 +759,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.2.19", - "@angular/core": "21.2.19", - "@angular/platform-browser": "21.2.19", + "@angular/common": "21.2.25", + "@angular/core": "21.2.25", + "@angular/platform-browser": "21.2.25", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/platform-browser": { - "version": "21.2.19", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.19.tgz", - "integrity": "sha512-YMguYVhdkV8tr9MvbN+VpMjbdmsYq6g12M9WPvAYM51BkL2iREbWeoXDGmfCw9G0it6+0pMdgrSPFFUFuOqpAQ==", + "version": "21.2.25", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-21.2.25.tgz", + "integrity": "sha512-hQgeKZiKt42P+jcf9j7o3c1GSOwOXrAYX2hXCs4A0+/AQ1neAQ20bjjUAgz1fGC4P3bNdurw1ZLLhMA+ZBKPtg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -777,9 +777,9 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/animations": "21.2.19", - "@angular/common": "21.2.19", - "@angular/core": "21.2.19" + "@angular/animations": "21.2.25", + "@angular/common": "21.2.25", + "@angular/core": "21.2.25" }, "peerDependenciesMeta": { "@angular/animations": { @@ -788,9 +788,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "21.2.19", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.2.19.tgz", - "integrity": "sha512-Ns+0D4S8A6Bypxgeba0QSDdIIVosOUfSfxDxiC0oN/VrgMBiLAbOROTnoNfOaFRgkV2gUzPNPFdHD2Ciflq+bQ==", + "version": "21.2.25", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-21.2.25.tgz", + "integrity": "sha512-ZtMCtLGXaEg+2LoXafnlPOZ2axwlwvy7rxQU8lSgruG9pBib6rL74h1OLUiBWwErkcyEZPx2rnSC6Pt03S9sQA==", "deprecated": "@angular/platform-browser-dynamic is deprecated. Use `@angular/platform-browser` instead.", "license": "MIT", "dependencies": { @@ -800,16 +800,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.2.19", - "@angular/compiler": "21.2.19", - "@angular/core": "21.2.19", - "@angular/platform-browser": "21.2.19" + "@angular/common": "21.2.25", + "@angular/compiler": "21.2.25", + "@angular/core": "21.2.25", + "@angular/platform-browser": "21.2.25" } }, "node_modules/@angular/platform-server": { - "version": "21.2.19", - "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-21.2.19.tgz", - "integrity": "sha512-UVyUIj04LRBoi4KfvxeZYPohzh80nSqamxpb8uzsYFKVSYdZSHDiFaE6OOzj0YST1rs2/LC92cPE2TY7IQIdew==", + "version": "21.2.25", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-21.2.25.tgz", + "integrity": "sha512-v7htAtnYrSqlST9G563WSMTLa5QsWK+4dC1FCnA1STuU35sgsVAaxoa+62MLlt+X7nvKT4X+S9ZDpP8c4hegNA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0", @@ -819,17 +819,17 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.2.19", - "@angular/compiler": "21.2.19", - "@angular/core": "21.2.19", - "@angular/platform-browser": "21.2.19", + "@angular/common": "21.2.25", + "@angular/compiler": "21.2.25", + "@angular/core": "21.2.25", + "@angular/platform-browser": "21.2.25", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/router": { - "version": "21.2.19", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.19.tgz", - "integrity": "sha512-cKO/aq1xEMvgS29jFUc/Y3KsgEzHnwOw6sEL+UQZkZTmGD5YrYv8Yvj05GDMEUYbvDxBd5DixVVEbH38lyQKqA==", + "version": "21.2.25", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-21.2.25.tgz", + "integrity": "sha512-85ZwJCBEpaXBmezusZLlgW2JzEEByzerlWT34I6IK/ZN4AKG8m2B/vlbTkKmHuzCOGWx1L7xFtPg/URkGGKwSA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -838,16 +838,16 @@ "node": "^20.19.0 || ^22.12.0 || >=24.0.0" }, "peerDependencies": { - "@angular/common": "21.2.19", - "@angular/core": "21.2.19", - "@angular/platform-browser": "21.2.19", + "@angular/common": "21.2.25", + "@angular/core": "21.2.25", + "@angular/platform-browser": "21.2.25", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/ssr": { - "version": "21.2.20", - "resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-21.2.20.tgz", - "integrity": "sha512-MZn7Hssm2EYaQ8zWGD+o2fzVs4BLGHLXSg2L7JBmAjlXhSyJeBYw2PUOzSX4gqCNF6wR8ikmJJTel0q8Bo6oow==", + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-21.2.24.tgz", + "integrity": "sha512-BROnqzOJVIgLlPUIXEXrb1eJwbGfel/nFnYKt524oNR2QBv3ErxHe4J2tCX7r4IKBC9B8Jgh06PE/UoDNp3RYg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -1231,6 +1231,30 @@ "specificity": "bin/cli.js" } }, + "node_modules/@cacheable/memory": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@cacheable/memory/-/memory-2.2.0.tgz", + "integrity": "sha512-CTLKqLItRCEixEAewD3/j9DB3/o96gpTPD4eJ1v+DGOlxZRZncRQkGYqqnAGCscYd6RNeXfGeiuCphsPtqyIfQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/utils": "^2.5.0", + "@keyv/bigmap": "^1.3.1", + "hookified": "^1.15.1", + "keyv": "^5.6.0" + } + }, + "node_modules/@cacheable/utils": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@cacheable/utils/-/utils-2.5.0.tgz", + "integrity": "sha512-buipgOVDkkPXNR5+xBpDw7Zk2n1EvU7qBJCNUcL7rhQ//kfpOXPAvQ511Os0vpLYJ1pZnvudNytkQt2hst3wqA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.5.1", + "keyv": "^5.6.0" + } + }, "node_modules/@centerforopenscience/markdown-it-atrules": { "version": "0.1.2", "resolved": "https://registry.npmjs.org/@centerforopenscience/markdown-it-atrules/-/markdown-it-atrules-0.1.2.tgz", @@ -1287,15 +1311,15 @@ } }, "node_modules/@commitlint/cli": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.5.0.tgz", - "integrity": "sha512-yNkyN/tuKTJS3wdVfsZ2tXDM4G4Gi7z+jW54Cki8N8tZqwKBltbIvUUrSbT4hz1bhW/h0CdR+5sCSpXD+wMKaQ==", + "version": "20.5.3", + "resolved": "https://registry.npmjs.org/@commitlint/cli/-/cli-20.5.3.tgz", + "integrity": "sha512-OJdL0EXWD5y9LPa0nr/geOwzaS8BsdaybKkcloB0JgsguGxNv2R+hC2FTPqrAcprg35zF33KOQerY0x8W1aesA==", "dev": true, "license": "MIT", "dependencies": { "@commitlint/format": "^20.5.0", - "@commitlint/lint": "^20.5.0", - "@commitlint/load": "^20.5.0", + "@commitlint/lint": "^20.5.3", + "@commitlint/load": "^20.5.3", "@commitlint/read": "^20.5.0", "@commitlint/types": "^20.5.0", "tinyexec": "^1.0.0", @@ -1435,9 +1459,9 @@ } }, "node_modules/@commitlint/config-conventional": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.5.0.tgz", - "integrity": "sha512-t3Ni88rFw1XMa4nZHgOKJ8fIAT9M2j5TnKyTqJzsxea7FUetlNdYFus9dz+MhIRZmc16P0PPyEfh6X2d/qw8SA==", + "version": "20.5.3", + "resolved": "https://registry.npmjs.org/@commitlint/config-conventional/-/config-conventional-20.5.3.tgz", + "integrity": "sha512-j34Qqeaa152chJgz2ysyk0BCpHenJn1lV0Rx0VXf8k3ccQcED+48EZrzMvo9jLmJUyBrrBwvu89I+2er4gW7QQ==", "dev": true, "license": "MIT", "dependencies": { @@ -1463,18 +1487,14 @@ } }, "node_modules/@commitlint/ensure": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.5.0.tgz", - "integrity": "sha512-IpHqAUesBeW1EDDdjzJeaOxU9tnogLAyXLRBn03SHlj1SGENn2JGZqSWGkFvBJkJzfXAuCNtsoYzax+ZPS+puw==", + "version": "20.5.3", + "resolved": "https://registry.npmjs.org/@commitlint/ensure/-/ensure-20.5.3.tgz", + "integrity": "sha512-4i4AgNvH62owG9MwSiWKrle7HGNpBHHdLnWFIp5fTsHUYe5kRuh15t08L/0pdbbrRk8JKXQxxN4hZQcn+szkrw==", "dev": true, "license": "MIT", "dependencies": { "@commitlint/types": "^20.5.0", - "lodash.camelcase": "^4.3.0", - "lodash.kebabcase": "^4.1.1", - "lodash.snakecase": "^4.1.1", - "lodash.startcase": "^4.4.0", - "lodash.upperfirst": "^4.3.1" + "es-toolkit": "^1.46.0" }, "engines": { "node": ">=v18" @@ -1519,15 +1539,15 @@ } }, "node_modules/@commitlint/lint": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.5.0.tgz", - "integrity": "sha512-jiM3hNUdu04jFBf1VgPdjtIPvbuVfDTBAc6L98AWcoLjF5sYqkulBHBzlVWll4rMF1T5zeQFB6r//a+s+BBKlA==", + "version": "20.5.3", + "resolved": "https://registry.npmjs.org/@commitlint/lint/-/lint-20.5.3.tgz", + "integrity": "sha512-M7JbWBNr2gXKaPc4i/KipsuW1gkDHpj35KPjWtKy3Z+2AQw5wu1gBi1LIO0uoaij67CqY4K8PxPZSGens4evCw==", "dev": true, "license": "MIT", "dependencies": { "@commitlint/is-ignored": "^20.5.0", "@commitlint/parse": "^20.5.0", - "@commitlint/rules": "^20.5.0", + "@commitlint/rules": "^20.5.3", "@commitlint/types": "^20.5.0" }, "engines": { @@ -1535,20 +1555,20 @@ } }, "node_modules/@commitlint/load": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.5.0.tgz", - "integrity": "sha512-sLhhYTL/KxeOTZjjabKDhwidGZan84XKK1+XFkwDYL/4883kIajcz/dZFAhBJmZPtL8+nBx6bnkzA95YxPeDPw==", + "version": "20.5.3", + "resolved": "https://registry.npmjs.org/@commitlint/load/-/load-20.5.3.tgz", + "integrity": "sha512-1FDZWuKyu98Myb8i7Tp31jPU2rZpOwAdYRyJcy2KoGg7Xk2A+bgHN8smhMaaNSNkmE8fwt53BokywZq8Gv/5XQ==", "dev": true, "license": "MIT", "dependencies": { "@commitlint/config-validator": "^20.5.0", "@commitlint/execute-rule": "^20.0.0", - "@commitlint/resolve-extends": "^20.5.0", + "@commitlint/resolve-extends": "^20.5.3", "@commitlint/types": "^20.5.0", "cosmiconfig": "^9.0.1", "cosmiconfig-typescript-loader": "^6.1.0", + "es-toolkit": "^1.46.0", "is-plain-obj": "^4.1.0", - "lodash.mergewith": "^4.6.2", "picocolors": "^1.1.1" }, "engines": { @@ -1598,17 +1618,17 @@ } }, "node_modules/@commitlint/resolve-extends": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.5.0.tgz", - "integrity": "sha512-3SHPWUW2v0tyspCTcfSsYml0gses92l6TlogwzvM2cbxDgmhSRc+fldDjvGkCXJrjSM87BBaWYTPWwwyASZRrg==", + "version": "20.5.3", + "resolved": "https://registry.npmjs.org/@commitlint/resolve-extends/-/resolve-extends-20.5.3.tgz", + "integrity": "sha512-+ogW9v/u9JqpvAgTrLra/YTFo0KkjU6iNblF89pPsj4NebNc+DAWctsludwezI8YnsjBmfHpApSwcXprN/f/ew==", "dev": true, "license": "MIT", "dependencies": { "@commitlint/config-validator": "^20.5.0", "@commitlint/types": "^20.5.0", - "global-directory": "^4.0.1", + "es-toolkit": "^1.46.0", + "global-directory": "^5.0.0", "import-meta-resolve": "^4.0.0", - "lodash.mergewith": "^4.6.2", "resolve-from": "^5.0.0" }, "engines": { @@ -1616,13 +1636,13 @@ } }, "node_modules/@commitlint/rules": { - "version": "20.5.0", - "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.5.0.tgz", - "integrity": "sha512-5NdQXQEdnDPT5pK8O39ZA7HohzPRHEsDGU23cyVCNPQy4WegAbAwrQk3nIu7p2sl3dutPk8RZd91yKTrMTnRkQ==", + "version": "20.5.3", + "resolved": "https://registry.npmjs.org/@commitlint/rules/-/rules-20.5.3.tgz", + "integrity": "sha512-MPlMnb9D3wbszYMp+1hPtuhtPJndRo6I6yfkZVA4+jR8w7Kqp0u2u/Y+gzbaItx5Lltq5rw7FSZQWJMoXUC4NQ==", "dev": true, "license": "MIT", "dependencies": { - "@commitlint/ensure": "^20.5.0", + "@commitlint/ensure": "^20.5.3", "@commitlint/message": "^20.4.3", "@commitlint/to-lines": "^20.0.0", "@commitlint/types": "^20.5.0" @@ -1836,22 +1856,22 @@ } }, "node_modules/@emnapi/core": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.1.tgz", - "integrity": "sha512-RSvbQmHzdKzNsLYa/wHrbc3KN4sYLKAdPZxqiM2HATqv/SBk2/ENSHpvXGaLOMcsAyz0poEGqkmmKYG3OWiJEQ==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/core/-/core-1.11.3.tgz", + "integrity": "sha512-zLpS5asjEb7lq8jYLq37N6XKaE41DIexlY1rF/z4/tIl3wo13Sqm28fRyfIsKZD+NZ8mM5RoKkpW/rBcuoSZSg==", "dev": true, "license": "MIT", "optional": true, "peer": true, "dependencies": { - "@emnapi/wasi-threads": "1.2.2", + "@emnapi/wasi-threads": "1.2.3", "tslib": "^2.4.0" } }, "node_modules/@emnapi/runtime": { - "version": "1.11.1", - "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.1.tgz", - "integrity": "sha512-vgj7R3y3Wgx24IQaGPA/R6YFXLHVMOZ0uVEyIQPaWs+rd1AzfEMXlAC22FYwO1XkKR6NPsq7mUandH8oIRdZFw==", + "version": "1.11.3", + "resolved": "https://registry.npmjs.org/@emnapi/runtime/-/runtime-1.11.3.tgz", + "integrity": "sha512-Xz4Tpyki7XyrpbUK1jR1AhdAdaXyhhY4lZ3neLodmhpuWfy2PAQN5B46sAiU4liOXGLkHypn/qU+jvfWSCYYLA==", "dev": true, "license": "MIT", "optional": true, @@ -1861,9 +1881,9 @@ } }, "node_modules/@emnapi/wasi-threads": { - "version": "1.2.2", - "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.2.tgz", - "integrity": "sha512-c95qOXkHdydNKhscBTebqEC1CVAZpyqOfVfBzQ1qgzyl3gfeldUjIggDbIZgDKsHLgnsM+igH7TJ/eAasaVuMA==", + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/@emnapi/wasi-threads/-/wasi-threads-1.2.3.tgz", + "integrity": "sha512-ELEBe8PsLvvJ6QMr0zLt8ffvOHW/dc1m3CEzNMg7aJUv3bMaoDtw2TXyDAwkYBuroxxuHEwhRTLJSe5sya547g==", "dev": true, "license": "MIT", "optional": true, @@ -2359,9 +2379,9 @@ } }, "node_modules/@eslint/config-helpers": { - "version": "0.5.5", - "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.5.5.tgz", - "integrity": "sha512-eIJYKTCECbP/nsKaaruF6LW967mtbQbsw4JTtSVkUQc9MneSkbrgPJAbKl9nWr0ZeowV8BfsarBmPpBzGelA2w==", + "version": "0.7.0", + "resolved": "https://registry.npmjs.org/@eslint/config-helpers/-/config-helpers-0.7.0.tgz", + "integrity": "sha512-DObd/KKUsU+FaFv4PLxSRenpXfQWmPXXP3pPZ6/K1PCrMu2vQpMDMuQe/BqYeoLcz8ro0bVDF1RxOJgfVEdhUw==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2416,9 +2436,9 @@ } }, "node_modules/@eslint/plugin-kit": { - "version": "0.7.1", - "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.1.tgz", - "integrity": "sha512-rZAP3aVgB9ds9KOeUSL+zZ21hPmo8dh6fnIFwRQj5EAZl9gzR7wxYbYXYysAM8CTqGmUGyp2S4kUdV17MnGuWQ==", + "version": "0.7.3", + "resolved": "https://registry.npmjs.org/@eslint/plugin-kit/-/plugin-kit-0.7.3.tgz", + "integrity": "sha512-IkO+/KEUvwbVpiURZg+P7zF74z5Jxe0UgJxVni+RtoHQ6IZieXaO02kmadomap/q+l6bc/jdPGGqTjhuZnuz1Q==", "dev": true, "license": "Apache-2.0", "dependencies": { @@ -2448,9 +2468,9 @@ } }, "node_modules/@fortawesome/fontawesome-free": { - "version": "7.2.0", - "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.2.0.tgz", - "integrity": "sha512-3DguDv/oUE+7vjMeTSOjCSG+KeawgVQOHrKRnvUuqYh1mfArrh7s+s8hXW3e4RerBA1+Wh+hBqf8sJNpqNrBWg==", + "version": "7.3.1", + "resolved": "https://registry.npmjs.org/@fortawesome/fontawesome-free/-/fontawesome-free-7.3.1.tgz", + "integrity": "sha512-wmglKKPDIkgV3aWlZzWECCPoGIkYCulzBwxG9+w7rc5BGapZ6cPMpoPOT8k36J0Ni7PPX6c/rsoMWfS4d1MUMg==", "license": "(CC-BY-4.0 AND OFL-1.1 AND MIT)", "engines": { "node": ">=6" @@ -2475,42 +2495,56 @@ "optional": true }, "node_modules/@hono/node-server": { - "version": "1.19.17", - "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-1.19.17.tgz", - "integrity": "sha512-dSneS5qhiauZWGDCeK4o695Xd9nUNjviSZCMQrj10eetr8Uln1ucn6bbphOM6UynAMMtNIzZNSpL9vnASJwrPQ==", + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/@hono/node-server/-/node-server-2.1.3.tgz", + "integrity": "sha512-TA//nWMqPhbfdfneACk6t5a9eqbS9lABEPyKn0/xZTah3H3U2XaVg85rJFl0/Fyit0I552YDHgXGVSf3GwqbUw==", "dev": true, "license": "MIT", "engines": { - "node": ">=18.14.1" + "node": ">=20" }, "peerDependencies": { "hono": "^4" } }, "node_modules/@humanfs/core": { - "version": "0.19.1", - "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.1.tgz", - "integrity": "sha512-5DyQ4+1JEUzejeK1JGICcideyfUbGixgS9jNgex5nqkW+cY7WZhxBigmieN5Qnw9ZosSNVC9KQKyb+GUaGyKUA==", + "version": "0.19.2", + "resolved": "https://registry.npmjs.org/@humanfs/core/-/core-0.19.2.tgz", + "integrity": "sha512-UhXNm+CFMWcbChXywFwkmhqjs3PRCmcSa/hfBgLIb7oQ5HNb1wS0icWsGtSAUNgefHeI+eBrA8I1fxmbHsGdvA==", "dev": true, "license": "Apache-2.0", + "dependencies": { + "@humanfs/types": "^0.15.0" + }, "engines": { "node": ">=18.18.0" } }, "node_modules/@humanfs/node": { - "version": "0.16.7", - "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.7.tgz", - "integrity": "sha512-/zUx+yOsIrG4Y43Eh2peDeKCxlRt/gET6aHfaKpuq267qXdYDFViVHfMaLyygZOnl0kGWxFIgsBy8QFuTLUXEQ==", + "version": "0.16.8", + "resolved": "https://registry.npmjs.org/@humanfs/node/-/node-0.16.8.tgz", + "integrity": "sha512-gE1eQNZ3R++kTzFUpdGlpmy8kDZD/MLyHqDwqjkVQI0JMdI1D51sy1H958PNXYkM2rAac7e5/CnIKZrHtPh3BQ==", "dev": true, "license": "Apache-2.0", "dependencies": { - "@humanfs/core": "^0.19.1", + "@humanfs/core": "^0.19.2", + "@humanfs/types": "^0.15.0", "@humanwhocodes/retry": "^0.4.0" }, "engines": { "node": ">=18.18.0" } }, + "node_modules/@humanfs/types": { + "version": "0.15.0", + "resolved": "https://registry.npmjs.org/@humanfs/types/-/types-0.15.0.tgz", + "integrity": "sha512-ZZ1w0aoQkwuUuC7Yf+7sdeaNfqQiiLcSRbfI08oAxqLtpXQr9AIVX7Ay7HLDuiLYAaFPu8oBYNq/QIi9URHJ3Q==", + "dev": true, + "license": "Apache-2.0", + "engines": { + "node": ">=18.18.0" + } + }, "node_modules/@humanwhocodes/module-importer": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@humanwhocodes/module-importer/-/module-importer-1.0.1.tgz", @@ -2962,6 +2996,30 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@keyv/bigmap": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/@keyv/bigmap/-/bigmap-1.3.1.tgz", + "integrity": "sha512-WbzE9sdmQtKy8vrNPa9BRnwZh5UF4s1KTmSK0KUVLo3eff5BlQNNWDnFOouNpKfPKDnms9xynJjsMYjMaT/aFQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hashery": "^1.4.0", + "hookified": "^1.15.0" + }, + "engines": { + "node": ">= 18" + }, + "peerDependencies": { + "keyv": "^5.6.0" + } + }, + "node_modules/@keyv/serialize": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/@keyv/serialize/-/serialize-1.1.1.tgz", + "integrity": "sha512-dXn3FZhPv0US+7dtJsIi2R+c7qWYiReoEh5zUntWCf4oSpMNib8FDhSoed6m3QyZdx5hK7iLFkYk3rNxwt8vTA==", + "dev": true, + "license": "MIT" + }, "node_modules/@kurkle/color": { "version": "0.3.4", "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.4.tgz", @@ -3104,13 +3162,13 @@ } }, "node_modules/@modelcontextprotocol/sdk": { - "version": "1.26.0", - "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.26.0.tgz", - "integrity": "sha512-Y5RmPncpiDtTXDbLKswIJzTqu2hyBKxTNsgKqKclDbhIgg1wgtf1fRuvxgTnRfcnxtvvgbIEcqUOzZrJ6iSReg==", + "version": "1.30.0", + "resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.30.0.tgz", + "integrity": "sha512-xKd8OIzlqNzcqcNumGAa6g+PW2kjD5vrpcKOnfldAUPP3j7lnqMPwlTXQm8gF+UwH72z0lqaRbjr9hqGz0eITA==", "dev": true, "license": "MIT", "dependencies": { - "@hono/node-server": "^1.19.9", + "@hono/node-server": "^1.19.9 || ^2.0.5", "ajv": "^8.17.1", "ajv-formats": "^3.0.1", "content-type": "^1.0.5", @@ -3592,38 +3650,38 @@ } }, "node_modules/@newrelic/browser-agent": { - "version": "1.313.0", - "resolved": "https://registry.npmjs.org/@newrelic/browser-agent/-/browser-agent-1.313.0.tgz", - "integrity": "sha512-3qO7D5H6M1SNbneEcL5d4k4O0Zill7FHHkuZXAIVrVU6VV7QGsJFXGoVxWaFE+UoXr32OqdyfAVutVg9Ald05Q==", + "version": "1.323.0", + "resolved": "https://registry.npmjs.org/@newrelic/browser-agent/-/browser-agent-1.323.0.tgz", + "integrity": "sha512-geevV3bzUXvSPTZT1GadH/6Lv0Sa7NJgQtrBEDI9+IFpbODVJIe9vOeVzAm2GJLnhxCAsQ0pvymVHk+EeccfYg==", "license": "Apache-2.0", "dependencies": { - "@newrelic/rrweb": "1.1.0", + "@newrelic/rrweb": "^1.1.2", "fflate": "0.8.2", - "web-vitals": "4.2.4" + "web-vitals": "6.2.1" }, "engines": { "node": ">=12.17.0 < 13.0.0 || >=13.7.0" } }, "node_modules/@newrelic/rrdom": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@newrelic/rrdom/-/rrdom-1.1.0.tgz", - "integrity": "sha512-NQK9bJMdw2pGitekR44rgvILpzBpdZQrn98+5RGGh7V7+z+k+zwpdFmptHIvcY6Bxej//k7IupFyza1vidg5bw==", - "license": "MIT", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@newrelic/rrdom/-/rrdom-1.1.2.tgz", + "integrity": "sha512-fL49rcPCxT4sgCv6xIvopsq+fVHDRbp8Hb9C7QMVvk3G94wB5UKyVDkI+ubL/5HXLqaX6D06f7SWXJcxmoPZKw==", + "license": "Apache-2.0", "dependencies": { - "@newrelic/rrweb-snapshot": "^1.1.0" + "@newrelic/rrweb-snapshot": "^1.1.2" } }, "node_modules/@newrelic/rrweb": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@newrelic/rrweb/-/rrweb-1.1.0.tgz", - "integrity": "sha512-3QmDsDf0ZTi0upNiSjTKSqmPF7Y/WIm9NvfEYQnqejfowIuV4tgDBt0FePSpgkT0usFGDigXpAGE+VZQi5OqEg==", - "license": "MIT", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@newrelic/rrweb/-/rrweb-1.1.2.tgz", + "integrity": "sha512-OU66VA1PjC9UsFo7NzHgyp2OZIt4QHidbl45tAAUf+JQESwiLF6WvzJvajju7374D7TgZzV+L9cq3f+sIxKFXQ==", + "license": "Apache-2.0", "dependencies": { - "@newrelic/rrdom": "^1.1.0", - "@newrelic/rrweb-snapshot": "^1.1.0", - "@newrelic/rrweb-types": "^1.1.0", - "@newrelic/rrweb-utils": "^1.1.0", + "@newrelic/rrdom": "^1.1.2", + "@newrelic/rrweb-snapshot": "^1.1.2", + "@newrelic/rrweb-types": "^1.1.2", + "@newrelic/rrweb-utils": "^1.1.2", "@types/css-font-loading-module": "0.0.7", "@xstate/fsm": "^1.4.0", "base64-arraybuffer": "^1.0.1", @@ -3631,25 +3689,25 @@ } }, "node_modules/@newrelic/rrweb-snapshot": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@newrelic/rrweb-snapshot/-/rrweb-snapshot-1.1.0.tgz", - "integrity": "sha512-wvtz4RbTjRhLYVq/XPX9rAL6ifeCt6o3pnwUiR3SpUHBSYVGYPJuFQr3tEGh3BWYae3nfzNmOj73tGIc7SbSGQ==", - "license": "MIT", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@newrelic/rrweb-snapshot/-/rrweb-snapshot-1.1.2.tgz", + "integrity": "sha512-iaKS4WSoDGkYdOy3GaqqYneeHz+2GcbQDEl2sR52nNkyEzt6ewiesBYcDCFkGk5QmqoYCQnokry66baJfCyUzw==", + "license": "Apache-2.0", "dependencies": { "postcss": "^8.4.38" } }, "node_modules/@newrelic/rrweb-types": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@newrelic/rrweb-types/-/rrweb-types-1.1.0.tgz", - "integrity": "sha512-R+QhmPYuZdzsYmA4A8huNm+nqWKipbySMO5tAJD/gvfi820k4CJMUYxtc7k3YVuO/DeAjmJwQ8R1KzZNnorZwA==", - "license": "MIT" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@newrelic/rrweb-types/-/rrweb-types-1.1.2.tgz", + "integrity": "sha512-OEQBcT2J4xxArFQ45EYkDVBc9NT3fS7DIgganl4mZS6mZdLnK4UVrR3iSfSiEetIqGp/q6X+hCOqcgJll7NPjQ==", + "license": "Apache-2.0" }, "node_modules/@newrelic/rrweb-utils": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/@newrelic/rrweb-utils/-/rrweb-utils-1.1.0.tgz", - "integrity": "sha512-vMuhV4+8dZxXkaAQM9g0ULa0bUAkeJDMCHds8j82VChdxETJWGXBIlaj/tuQXWGp2kmY6hwhnALy7Gc+xVlHlA==", - "license": "MIT" + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/@newrelic/rrweb-utils/-/rrweb-utils-1.1.2.tgz", + "integrity": "sha512-L1X22OJbJCExp3IEClx6Nh1+sw3O+t79oAgGlBLlkDV/saFSbMpg09K2NLIWpkSxZs7yzouvRSeHh8QiDlA9bg==", + "license": "Apache-2.0" }, "node_modules/@ngx-translate/core": { "version": "17.0.0", @@ -4953,14 +5011,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "21.2.20", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.20.tgz", - "integrity": "sha512-D0MFRofD144Gn+LMPgrRQKazv3sNxfB11kbdU19yN8JyXyRqmB3C8/k3lLA/+LyXVeyLwzIKs7Cw/PSzPUaAxA==", + "version": "21.2.24", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-21.2.24.tgz", + "integrity": "sha512-eGBr4Yhg/A8Lt8QGtWxmXAyKboX8QBwbDG1hSyApQfHAh9iNFbV7mgWSmk0qPWvRJM38s0t3EEkEg79QZ98dWQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "21.2.20", - "@angular-devkit/schematics": "21.2.20", + "@angular-devkit/core": "21.2.24", + "@angular-devkit/schematics": "21.2.24", "jsonc-parser": "3.3.1" }, "engines": { @@ -4969,97 +5027,112 @@ "yarn": ">= 1.13.0" } }, - "node_modules/@sentry-internal/browser-utils": { - "version": "10.49.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-10.49.0.tgz", - "integrity": "sha512-n0QRx0Ysx6mPfIydTkz7VP0FmwM+/EqMZiRqdsU3aTYsngE9GmEDV0OL1bAy6a8N/C1xf9vntkuAtj6N/8Z51w==", + "node_modules/@sentry/angular": { + "version": "10.75.3", + "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-10.75.3.tgz", + "integrity": "sha512-vgwlHt3HuKVB01j2BVpvWSxD+2byw+BqtRCDrlYCEpMjZwm+BhDqGyqxNXpc3DsQIcimlVAQYxkaP5j4vE9viQ==", "license": "MIT", "dependencies": { - "@sentry/core": "10.49.0" + "@sentry/browser": "10.75.3", + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.75.3", + "tslib": "^2.4.1" }, "engines": { "node": ">=18" + }, + "peerDependencies": { + "@angular/common": ">= 14.x <= 22.x", + "@angular/core": ">= 14.x <= 22.x", + "@angular/router": ">= 14.x <= 22.x", + "rxjs": "^6.5.5 || ^7.x" } }, - "node_modules/@sentry-internal/feedback": { - "version": "10.49.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-10.49.0.tgz", - "integrity": "sha512-JNsUBGv0faCFE7MeZUH99Y9lU9qq3LBALbLxpE1x7ngNrQnVYRlcFgdqaD/btNBKr8awjYL8gmcSkHBWskGqLQ==", + "node_modules/@sentry/browser": { + "version": "10.75.3", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.75.3.tgz", + "integrity": "sha512-tqjrG3dnk7LHPDiIxg/5MVF+ONV3VI33fJbAdlSODZD9/i09gXXfp5yoI70HBuasYVMGoCwoUowDfuRoZWEBuA==", "license": "MIT", "dependencies": { - "@sentry/core": "10.49.0" + "@sentry/browser-utils": "10.75.3", + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.75.3", + "@sentry/feedback": "10.75.3", + "@sentry/replay": "10.75.3", + "@sentry/replay-canvas": "10.75.3" }, "engines": { "node": ">=18" } }, - "node_modules/@sentry-internal/replay": { - "version": "10.49.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-10.49.0.tgz", - "integrity": "sha512-IEy4lwHVMiRE3JAcn+kFKjsTgalDOCSTf20SoFd+nkt6rN/k1RDyr4xpdfF//Kj3UdeTmbuibYjK5H/FLhhnGg==", + "node_modules/@sentry/browser-utils": { + "version": "10.75.3", + "resolved": "https://registry.npmjs.org/@sentry/browser-utils/-/browser-utils-10.75.3.tgz", + "integrity": "sha512-T2oQSMMZ5mWDRlyCDx9HtPRzO0qMxYojmHjYwpKvzB2dmk9NFnTS+aGXNYVv2i1/Uz0dbl8RGETBDiFM5UOs+w==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.49.0", - "@sentry/core": "10.49.0" + "@sentry/conventions": "^0.16.0", + "@sentry/core": "10.75.3" }, "engines": { "node": ">=18" } }, - "node_modules/@sentry-internal/replay-canvas": { - "version": "10.49.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-10.49.0.tgz", - "integrity": "sha512-7D/NrgH1Qwx5trDYaaTSSJmCb1yVQQLqFG4G/S9x2ltzl9876lSGJL8UeW8ReNQgF3CDAcwbmm/9aXaVSBUNZA==", + "node_modules/@sentry/conventions": { + "version": "0.16.0", + "resolved": "https://registry.npmjs.org/@sentry/conventions/-/conventions-0.16.0.tgz", + "integrity": "sha512-fO9PLmHdVURcSPUpWCItWAtgKiMwGdJHbovoSEyLplX5sxs2ugvI4CBPTrkkgqhObnZOD0CnWBKDzSVQYBKEyQ==", + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/@sentry/core": { + "version": "10.75.3", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.75.3.tgz", + "integrity": "sha512-QHgzobetM4UgEVbyqx9WPJzRq0YzyNg2CeeoM1Bf84ivYJXXiiVWxjzc0RHk3c0DHY5/6O6nJqos1xVL55dgZA==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "10.49.0", - "@sentry/core": "10.49.0" + "@sentry/conventions": "^0.16.0" }, "engines": { "node": ">=18" } }, - "node_modules/@sentry/angular": { - "version": "10.49.0", - "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-10.49.0.tgz", - "integrity": "sha512-7Cd/fmUUqOTj4kPCVrm3QQVz4a+w/THcKTZvarCkETShhkfvlCf7sPCvB6fNPSWvt3+Ra5sHxA3QKr2S+kvKCQ==", + "node_modules/@sentry/feedback": { + "version": "10.75.3", + "resolved": "https://registry.npmjs.org/@sentry/feedback/-/feedback-10.75.3.tgz", + "integrity": "sha512-F9w4xK1/qd2cDvudyxPF23weFV/+4wDykRg7ay2iapRPqsSgHlHshkq9+OGLqrevhFrPUEizRMimulPuREHUiQ==", "license": "MIT", "dependencies": { - "@sentry/browser": "10.49.0", - "@sentry/core": "10.49.0", - "tslib": "^2.4.1" + "@sentry/core": "10.75.3" }, "engines": { "node": ">=18" - }, - "peerDependencies": { - "@angular/common": ">= 14.x <= 21.x", - "@angular/core": ">= 14.x <= 21.x", - "@angular/router": ">= 14.x <= 21.x", - "rxjs": "^6.5.5 || ^7.x" } }, - "node_modules/@sentry/browser": { - "version": "10.49.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-10.49.0.tgz", - "integrity": "sha512-bGCHc+wK2Dx67YoSbmtlt04alqWfQ+dasD/GVipVOq50gvw/BBIDHTEWRJEjACl+LrvszeY54V+24p8z4IgysA==", + "node_modules/@sentry/replay": { + "version": "10.75.3", + "resolved": "https://registry.npmjs.org/@sentry/replay/-/replay-10.75.3.tgz", + "integrity": "sha512-PSWveKEo/eGqcj7tjgNPLZCOQ5Gd0j0NCvVZvOyUhyZJ+W8rLXg4XrlWAlDGz6fjtK++oO1AOmcbv0Aonq+WlQ==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "10.49.0", - "@sentry-internal/feedback": "10.49.0", - "@sentry-internal/replay": "10.49.0", - "@sentry-internal/replay-canvas": "10.49.0", - "@sentry/core": "10.49.0" + "@sentry/browser-utils": "10.75.3", + "@sentry/core": "10.75.3" }, "engines": { "node": ">=18" } }, - "node_modules/@sentry/core": { - "version": "10.49.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-10.49.0.tgz", - "integrity": "sha512-UaFeum3LUM1mB0d67jvKnqId1yWQjyqmaDV6kWngG03x+jqXb08tJdGpSoxjXZe13jFBbiBL/wKDDYIK7rCK4g==", + "node_modules/@sentry/replay-canvas": { + "version": "10.75.3", + "resolved": "https://registry.npmjs.org/@sentry/replay-canvas/-/replay-canvas-10.75.3.tgz", + "integrity": "sha512-uzmOMJ7O2BGIcfXtiqLrK25LmpbGll2YgJGf5Ulkv2tGSCpXRyC0JHl3mtQcb90AAjB/5/yS1xDAkZLWIEHHSQ==", "license": "MIT", + "dependencies": { + "@sentry/core": "10.75.3", + "@sentry/replay": "10.75.3" + }, "engines": { "node": ">=18" } @@ -5406,17 +5479,17 @@ } }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.58.2.tgz", - "integrity": "sha512-aC2qc5thQahutKjP+cl8cgN9DWe3ZUqVko30CMSZHnFEHyhOYoZSzkGtAI2mcwZ38xeImDucI4dnqsHiOYuuCw==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.71.0.tgz", + "integrity": "sha512-pqcS9c1HxZTHt7End4nXqd0s5lJrrFzrgCkKFJrsbUnaL6M3+6oBFZaslg6Gjsl3argl2DDRFROnXARaZ2e4Nw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.12.2", - "@typescript-eslint/scope-manager": "8.58.2", - "@typescript-eslint/type-utils": "8.58.2", - "@typescript-eslint/utils": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/type-utils": "8.71.0", + "@typescript-eslint/utils": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", "ignore": "^7.0.5", "natural-compare": "^1.4.0", "ts-api-utils": "^2.5.0" @@ -5429,22 +5502,22 @@ "url": "https://opencollective.com/typescript-eslint" }, "peerDependencies": { - "@typescript-eslint/parser": "^8.58.2", + "@typescript-eslint/parser": "^8.71.0", "eslint": "^8.57.0 || ^9.0.0 || ^10.0.0", "typescript": ">=4.8.4 <6.1.0" } }, "node_modules/@typescript-eslint/parser": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.58.2.tgz", - "integrity": "sha512-/Zb/xaIDfxeJnvishjGdcR4jmr7S+bda8PKNhRGdljDM+elXhlvN0FyPSsMnLmJUrVG9aPO6dof80wjMawsASg==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.71.0.tgz", + "integrity": "sha512-CG4nPk1f2zc8yw4pALqHsFYH2hdo+h1T9daSp21+Hnxi9LOE3GT9hAfTKJCBXVNM2GmYs1eMEP615wPoeOgk3A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/scope-manager": "8.58.2", - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2", + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", "debug": "^4.4.3" }, "engines": { @@ -5460,14 +5533,14 @@ } }, "node_modules/@typescript-eslint/project-service": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.58.2.tgz", - "integrity": "sha512-Cq6UfpZZk15+r87BkIh5rDpi38W4b+Sjnb8wQCPPDDweS/LRCFjCyViEbzHk5Ck3f2QDfgmlxqSa7S7clDtlfg==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/project-service/-/project-service-8.71.0.tgz", + "integrity": "sha512-aABjw5rjBacYONVPaPiWOCjJu0vEF4a25iQuodlmQYL1trtLZ0X/y+2Vzl3BKI1odM4LnwLE1oUDXYp1wzx1TQ==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/tsconfig-utils": "^8.58.2", - "@typescript-eslint/types": "^8.58.2", + "@typescript-eslint/tsconfig-utils": "^8.71.0", + "@typescript-eslint/types": "^8.71.0", "debug": "^4.4.3" }, "engines": { @@ -5482,14 +5555,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.58.2.tgz", - "integrity": "sha512-SgmyvDPexWETQek+qzZnrG6844IaO02UVyOLhI4wpo82dpZJY9+6YZCKAMFzXb7qhx37mFK1QcPQ18tud+vo6Q==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.71.0.tgz", + "integrity": "sha512-gWF0BhUcnjZxSpLE8ngS/59n2SB0J3YqRxvX1+2aoRJk9hNtHSLOV+TcarFiOr5ipXm3yc1QrI4c9YZc8zyCxw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2" + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5500,9 +5573,9 @@ } }, "node_modules/@typescript-eslint/tsconfig-utils": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.58.2.tgz", - "integrity": "sha512-3SR+RukipDvkkKp/d0jP0dyzuls3DbGmwDpVEc5wqk5f38KFThakqAAO0XMirWAE+kT00oTauTbzMFGPoAzB0A==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/tsconfig-utils/-/tsconfig-utils-8.71.0.tgz", + "integrity": "sha512-Z1UlWHADEK2Mlb9NpWfDeSjqoZ5EyrOv4R3eQpbkzqn/EwaIdOpXXupEA1+0ZIOSJSZZDBHG0BrQyN8zUG6Pwg==", "dev": true, "license": "MIT", "engines": { @@ -5517,15 +5590,15 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.58.2.tgz", - "integrity": "sha512-Z7EloNR/B389FvabdGeTo2XMs4W9TjtPiO9DAsmT0yom0bwlPyRjkJ1uCdW1DvrrrYP50AJZ9Xc3sByZA9+dcg==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.71.0.tgz", + "integrity": "sha512-i8uO1qbdxeKgRnS5sCRt6On3/nfo2d2DwQe3Yvjx543zLy7r8ySqRuPPiIIXAhS03U0v5NfAFx+rUgxFzKKwNw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2", - "@typescript-eslint/utils": "8.58.2", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/utils": "8.71.0", "debug": "^4.4.3", "ts-api-utils": "^2.5.0" }, @@ -5542,9 +5615,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.58.2.tgz", - "integrity": "sha512-9TukXyATBQf/Jq9AMQXfvurk+G5R2MwfqQGDR2GzGz28HvY/lXNKGhkY+6IOubwcquikWk5cjlgPvD2uAA7htQ==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.71.0.tgz", + "integrity": "sha512-cJ4OoxPGWvFnBTnSZyaU+qJzGTqPTGJY+gDchj6cRyLRdmIdt4rcsE4twj+zPfrNiWuVi38wijHzShL++Z9atQ==", "dev": true, "license": "MIT", "engines": { @@ -5556,16 +5629,16 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.58.2.tgz", - "integrity": "sha512-ELGuoofuhhoCvNbQjFFiobFcGgcDCEm0ThWdmO4Z0UzLqPXS3KFvnEZ+SHewwOYHjM09tkzOWXNTv9u6Gqtyuw==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.71.0.tgz", + "integrity": "sha512-PEEF4G5sLLWAS5BpPrUvms4ySZkiBQQZM4z+3ReI46axK5Vqr/vXBQatJQIZZOYdGyPUAKTtsrWzpqKuU+3DEw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/project-service": "8.58.2", - "@typescript-eslint/tsconfig-utils": "8.58.2", - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/visitor-keys": "8.58.2", + "@typescript-eslint/project-service": "8.71.0", + "@typescript-eslint/tsconfig-utils": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/visitor-keys": "8.71.0", "debug": "^4.4.3", "minimatch": "^10.2.2", "semver": "^7.7.3", @@ -5584,16 +5657,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.58.2.tgz", - "integrity": "sha512-QZfjHNEzPY8+l0+fIXMvuQ2sJlplB4zgDZvA+NmvZsZv3EQwOcc1DuIU1VJUTWZ/RKouBMhDyNaBMx4sWvrzRA==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.71.0.tgz", + "integrity": "sha512-pKR/tEMVrXZG23UFKUn5BQf3zfmfk7KQceI2cGzywZ5nxM5Eu3hEJU1utjWzydtzBbcJAQhHN8iPCxobHpPcZQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.9.1", - "@typescript-eslint/scope-manager": "8.58.2", - "@typescript-eslint/types": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2" + "@typescript-eslint/scope-manager": "8.71.0", + "@typescript-eslint/types": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -5608,13 +5681,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.58.2.tgz", - "integrity": "sha512-f1WO2Lx8a9t8DARmcWAUPJbu0G20bJlj8L4z72K00TMeJAoyLr/tHhI/pzYBLrR4dXWkcxO1cWYZEOX8DKHTqA==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.71.0.tgz", + "integrity": "sha512-8eQ9R218XORK+KLosnf4bu/QsUXvUyVwTbArg7/0NMB1Pu87OJKvj4nhFblkYE8gQV73mW1dx1ptlPCkwRGa7A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.58.2", + "@typescript-eslint/types": "8.71.0", "eslint-visitor-keys": "^5.0.0" }, "engines": { @@ -5652,14 +5725,14 @@ } }, "node_modules/@vitest/coverage-v8": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.9.tgz", - "integrity": "sha512-G9/lgqibheLVBDRuya45EbsEXTYcWoSG+TLg7i2axuzx0Eq62eXn+aWXyaVdV5vKvFSWd6ywcX8hA7la9Pvu8g==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-4.1.11.tgz", + "integrity": "sha512-8MVGEFnJIcdGjcbfKmeq8z0pZHH0JlVtoVZH9Q/qwUp6wyFnEJUBMrw9DCaj+ra3vShGmhavjalMIhPNxZAUcw==", "dev": true, "license": "MIT", "dependencies": { "@bcoe/v8-coverage": "^1.0.2", - "@vitest/utils": "4.1.9", + "@vitest/utils": "4.1.11", "ast-v8-to-istanbul": "^1.0.0", "istanbul-lib-coverage": "^3.2.2", "istanbul-lib-report": "^3.0.1", @@ -5673,8 +5746,8 @@ "url": "https://opencollective.com/vitest" }, "peerDependencies": { - "@vitest/browser": "4.1.9", - "vitest": "4.1.9" + "@vitest/browser": "4.1.11", + "vitest": "4.1.11" }, "peerDependenciesMeta": { "@vitest/browser": { @@ -5683,16 +5756,16 @@ } }, "node_modules/@vitest/expect": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.9.tgz", - "integrity": "sha512-vl/rYsUKcBr3SnQn166+XR5ZQcgMx3DQhFWdfli/cWpLnLUmbxZvyrJZotLFUryib+LtArYMSTJ5RbQ57ZqrlA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-4.1.11.tgz", + "integrity": "sha512-VX2x5vNJXET47KAFzwERI+KRMtTTCSWTfSMKsW7JsUsXV4psq++e3DvZpuTDOpHcxytiDs6p2nhVb2tVDiiUYw==", "dev": true, "license": "MIT", "dependencies": { "@standard-schema/spec": "^1.1.0", "@types/chai": "^5.2.2", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "chai": "^6.2.2", "tinyrainbow": "^3.1.0" }, @@ -5701,13 +5774,13 @@ } }, "node_modules/@vitest/mocker": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.9.tgz", - "integrity": "sha512-EVkXzBjrPGM+cK8/ANWgBrkUCfJfb38/EfTSO8h7pWvKkyPkpWxvR7BkD2MyItMF62C97zAEoqdpUixwR/e+Rw==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-4.1.11.tgz", + "integrity": "sha512-2XJVD55d1o5AZous5CCGKS74g/riOj9odEt2bQpCVZeblHyHdnMeFl4jl0XjU21stf4mbjUkew2eXQZt65g5CQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/spy": "4.1.9", + "@vitest/spy": "4.1.11", "estree-walker": "^3.0.3", "magic-string": "^0.30.21" }, @@ -5728,9 +5801,9 @@ } }, "node_modules/@vitest/pretty-format": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.9.tgz", - "integrity": "sha512-s0iufns3iIFitdgm+YR7g1whCAaGtXz459VS9/PqyKDEEFgYIhsHOQmXgIgDuYCt7DeQmiZT0Qe2OA2p4ZPu5A==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-4.1.11.tgz", + "integrity": "sha512-yiZzPbGTS9Sr/JpFl8zHrcIkAofNbFV6k21vIgQN/cY/oxZeXhJv5sc/MBJ5jFKWmWs+oJHw0UXLZjmf931+Vw==", "dev": true, "license": "MIT", "dependencies": { @@ -5741,13 +5814,13 @@ } }, "node_modules/@vitest/runner": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.9.tgz", - "integrity": "sha512-KXLMDtc7oe70+3mJfGrPUWPesswH+3sTxAMAMl8DG7I8IUQT4XW718dY5ID3vPUcmlu27CcKfY4P3h3I29SLJg==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-4.1.11.tgz", + "integrity": "sha512-LztvUgdwMNJMIkj3hQnnxiC2Xy1zNxq928W/xhjCLaNCzqTZOudjwbQf6v9IntZGPw132i2Lq2rgTRZHD3JHNw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/utils": "4.1.9", + "@vitest/utils": "4.1.11", "pathe": "^2.0.3" }, "funding": { @@ -5755,14 +5828,14 @@ } }, "node_modules/@vitest/snapshot": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.9.tgz", - "integrity": "sha512-Jc7RKGNBo8Z28WYIm0Niej4xdSPByRf6mU58VpHQkd6Zh05rlnA+twjbK5HyeIGHxrzsc3mJgS43uM0CZKzaIA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-4.1.11.tgz", + "integrity": "sha512-pN7ikn1ON7h8ee4gIAp4AzyK+zBtJPzVbqOgu5LCEh4VaJVbPQcgYQYJIMGQPXVeJJq1fnfazis7a5pFNPahog==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/pretty-format": "4.1.11", + "@vitest/utils": "4.1.11", "magic-string": "^0.30.21", "pathe": "^2.0.3" }, @@ -5771,9 +5844,9 @@ } }, "node_modules/@vitest/spy": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.9.tgz", - "integrity": "sha512-fHpsS6mIi+PiEW+vcRVOMkX1oSaPKne3VOclSFICPcGOmfKgXPU5iAah+wcNcj2xPrCCmfq99IDGf+EojhhvhA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-4.1.11.tgz", + "integrity": "sha512-apNa/prQy2qCeywhnixOHPRCgGNhvg7T4Dapfl1GahLp/R+uhBm5cPyFoNVyqsNd2h1nJxL6BqqdIjiABL60YA==", "dev": true, "license": "MIT", "funding": { @@ -5781,13 +5854,13 @@ } }, "node_modules/@vitest/utils": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.9.tgz", - "integrity": "sha512-A51o8ymO5PpqlWNnBP9ZHPXDIpuMtTLlGSjN7la4US+LJzoUMyhwjA5QXlm39JexgwHKW4Xjs8Z2d3dLCXOeuA==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-4.1.11.tgz", + "integrity": "sha512-zTCVGpyFsGWBhllOyKlTw/vnr6D9qxsfSDyfbyZmTyjHw5N/VuvzHpHoQjm2ZJzn4RJgx5w4r7V0er69CmLgPQ==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/pretty-format": "4.1.9", + "@vitest/pretty-format": "4.1.11", "convert-source-map": "^2.0.0", "tinyrainbow": "^3.1.0" }, @@ -5939,19 +6012,19 @@ } }, "node_modules/angular-eslint": { - "version": "21.3.1", - "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-21.3.1.tgz", - "integrity": "sha512-VGQWTyuPAEO/AnZuqHxGBJMYSiZ0tbrHx/OgPCRTKHfbrFU4x+zivS84h9UWoDpDtius1RyD+ZReFjTAEWptiA==", + "version": "21.4.0", + "resolved": "https://registry.npmjs.org/angular-eslint/-/angular-eslint-21.4.0.tgz", + "integrity": "sha512-LH7bWmtJvsubzwPoztnl1pWgI5X0VrfGTUITGSYcwn2J+SXuN/avzrKrxJmhUiIrNvLtfV+18GG6xZS1IGZdKg==", "dev": true, "license": "MIT", "dependencies": { "@angular-devkit/core": ">= 21.0.0 < 22.0.0", "@angular-devkit/schematics": ">= 21.0.0 < 22.0.0", - "@angular-eslint/builder": "21.3.1", - "@angular-eslint/eslint-plugin": "21.3.1", - "@angular-eslint/eslint-plugin-template": "21.3.1", - "@angular-eslint/schematics": "21.3.1", - "@angular-eslint/template-parser": "21.3.1", + "@angular-eslint/builder": "21.4.0", + "@angular-eslint/eslint-plugin": "21.4.0", + "@angular-eslint/eslint-plugin-template": "21.4.0", + "@angular-eslint/schematics": "21.4.0", + "@angular-eslint/template-parser": "21.4.0", "@typescript-eslint/types": "^8.0.0", "@typescript-eslint/utils": "^8.0.0" }, @@ -6141,9 +6214,9 @@ "license": "MIT" }, "node_modules/baseline-browser-mapping": { - "version": "2.11.20", - "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.20.tgz", - "integrity": "sha512-H0ulySigv6icDJ1F7SjtdCD6PrhTpdYCmP0CactWy1+ekh0AFd0o1Wn5T8b+hnTmdBx19u9yhL6wvCylXMY7zw==", + "version": "2.11.27", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.11.27.tgz", + "integrity": "sha512-ElY12DaROGuan+lMmZ8Cvo/ZUbXPe7Enc/9VU/b1T3Kp4dwytRcNdR8DoSJN5SNJT/CuvcCA0DHDVmMOCePdRQ==", "dev": true, "license": "Apache-2.0", "bin": { @@ -6249,9 +6322,9 @@ } }, "node_modules/brace-expansion": { - "version": "5.0.9", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.9.tgz", - "integrity": "sha512-ScQ4IuvIEF1TMlP7Zt+vjJ//9zlPb2SDcxWxM3bk8s6t6GGdJ7KO1dCcTidOPJKePW30LE/2cT7wCyPho9/Wxg==", + "version": "5.0.12", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-5.0.12.tgz", + "integrity": "sha512-YovQ3rzhaLMIrDjNDMkNS01tea93qhEhG5xy8f6+R0l+dw3Ki+5sCoIoI942iuLZTHWogWktgwVDhU09iNEimQ==", "dev": true, "license": "MIT", "dependencies": { @@ -6262,9 +6335,9 @@ } }, "node_modules/browserslist": { - "version": "4.28.8", - "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.8.tgz", - "integrity": "sha512-V2NpofLblG64mfOtSgDhOJESZEGogzDMBv/q+W6oc4LXWP/q75eOXoOaaOu1EOadB9U4Bwx/e0yzbvwKH8zalA==", + "version": "4.29.3", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.29.3.tgz", + "integrity": "sha512-1R4kiYKXGViqEN0CnoDrXc1StD9niAwu+j2dukWzrD4bJgsD4lDmEp0CRbc6E/vYJIfTHwPmwyaKtVSudICdPA==", "dev": true, "funding": [ { @@ -6282,11 +6355,11 @@ ], "license": "MIT", "dependencies": { - "baseline-browser-mapping": "^2.11.12", - "caniuse-lite": "^1.0.30001809", - "electron-to-chromium": "^1.5.402", - "node-releases": "^2.0.53", - "update-browserslist-db": "^1.3.0" + "baseline-browser-mapping": "^2.11.26", + "caniuse-lite": "^1.0.30001813", + "electron-to-chromium": "^1.5.439", + "node-releases": "^2.0.57", + "update-browserslist-db": "^1.3.3" }, "bin": { "browserslist": "cli.js" @@ -6380,6 +6453,20 @@ "node": "20 || >=22" } }, + "node_modules/cacheable": { + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/cacheable/-/cacheable-2.5.0.tgz", + "integrity": "sha512-60cyAOytib/OzBw1JNSoSV/boK1AtHryDIjvVBk7XbN4ugfkM3+Sry7fEjNgPMGgOjuaZPAp8ruZ0Cxafwyq9g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@cacheable/memory": "^2.2.0", + "@cacheable/utils": "^2.5.0", + "hookified": "^1.15.0", + "keyv": "^5.6.0", + "qified": "^0.10.1" + } + }, "node_modules/call-bind-apply-helpers": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", @@ -6430,9 +6517,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001810", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001810.tgz", - "integrity": "sha512-TITQPUkaz+aVk5GL6NhOdwk1aEaNTSDPsGFWrTuhKGtjTF70jL/Oht2W4c6rXUe5fu7Ie19VIahAXHIIiWWNeg==", + "version": "1.0.30001814", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001814.tgz", + "integrity": "sha512-/Uaf1lAzr59XcMpW0o96WoEfr+VXK2OX4U9AgFoiSHsVJ4HppnIFUjtYzsyDH2+tgANaQb2/oxYGwCPapN1FpA==", "dev": true, "funding": [ { @@ -6463,9 +6550,9 @@ "license": "ISC" }, "node_modules/chai": { - "version": "6.2.2", - "resolved": "https://registry.npmjs.org/chai/-/chai-6.2.2.tgz", - "integrity": "sha512-NUPRluOfOiTKBKvWPtSD4PhFvWCqOi0BGStNWs57X9js7XGTprSmFoz5F0tWhR4WPjNeR9jXqdC7/UpSJTnlRg==", + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-6.3.0.tgz", + "integrity": "sha512-XWAtwJ6OHO+tj0EKCs0Y2UamnyOxseZWltU4x2U2wh8g4AigdjwvtUjvLP2tqkA/avxHEtzxNaqGq/YGNwckKg==", "dev": true, "license": "MIT", "engines": { @@ -6814,9 +6901,9 @@ } }, "node_modules/cosmiconfig": { - "version": "9.0.1", - "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.1.tgz", - "integrity": "sha512-hr4ihw+DBqcvrsEDioRO31Z17x71pUYoNe/4h6Z0wB72p7MU7/9gH8Q3s12NFhHPfYBBOV3qyfUxmr/Yn3shnQ==", + "version": "9.0.2", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-9.0.2.tgz", + "integrity": "sha512-gtTZxTDau1wL7Y7zifc2dd8jHSK/k6BTx/2Xp/BpdlAdnlYWFVt7qhJqgwi7637yRwRQ3qL4ZidbB4I8tA5VOg==", "dev": true, "license": "MIT", "dependencies": { @@ -7134,9 +7221,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.420", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.420.tgz", - "integrity": "sha512-2yD6XreGusOfNV+dUcvipJEXc3n/n7fgr7996aszTG+YY5E4mqM4tOq/3uhP129cazL9YHbVWSpc79ePotWtPA==", + "version": "1.5.444", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.444.tgz", + "integrity": "sha512-5ss/uJfoDYDHT0lfJzT6FbcskIzROIOPf0BbbFkGcvDzoJU7i//9GDrwwIHQVmIsrAGiF3ihpADBRIsrEFt1rQ==", "dev": true, "license": "ISC" }, @@ -7227,9 +7314,9 @@ "license": "MIT" }, "node_modules/es-object-atoms": { - "version": "1.1.1", - "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.1.tgz", - "integrity": "sha512-FGgH2h8zKNim9ljj7dankFPcICIK9Cp5bm+c2gQSYePhpaG5+esrLODihIorn+Pe6FGJzWhXQotPv73jTaldXA==", + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/es-object-atoms/-/es-object-atoms-1.1.2.tgz", + "integrity": "sha512-HWcBoN6NileqtSydK2FqHbS/LoDd2pqrnQHLyJzBj4kOp/ky2MWMN694xOfkK8/SnUsW2DH7EfyVlydKCsm1Zw==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -7238,6 +7325,19 @@ "node": ">= 0.4" } }, + "node_modules/es-toolkit": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/es-toolkit/-/es-toolkit-1.52.0.tgz", + "integrity": "sha512-XTNEJQh1tY1ZJVcf6ayP/2n4ZPyaHlW2FWs7xvw5ddPuhUVjLD3olQVQS7kf58JbAB48iL0uL/jerTrjtV3lDA==", + "dev": true, + "license": "MIT", + "workspaces": [ + "docs", + "benchmarks", + "tests/types", + "tests/browser-compat" + ] + }, "node_modules/esbuild": { "version": "0.28.1", "resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.28.1.tgz", @@ -7310,18 +7410,21 @@ } }, "node_modules/eslint": { - "version": "10.2.0", - "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.2.0.tgz", - "integrity": "sha512-+L0vBFYGIpSNIt/KWTpFonPrqYvgKw1eUI5Vn7mEogrQcWtWYtNQ7dNqC+px/J0idT3BAkiWrhfS7k+Tum8TUA==", + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/eslint/-/eslint-10.11.0.tgz", + "integrity": "sha512-P7a6UEEqb9G95MYAtqkmsTbVXIYyzIfl6NGOIJk162PaahFxFyeGcrlXYFSiagECg4sEm8IseJdZBKR3rx6MsQ==", "dev": true, "license": "MIT", + "workspaces": [ + "packages/*" + ], "dependencies": { "@eslint-community/eslint-utils": "^4.8.0", "@eslint-community/regexpp": "^4.12.2", - "@eslint/config-array": "^0.23.4", - "@eslint/config-helpers": "^0.5.4", - "@eslint/core": "^1.2.0", - "@eslint/plugin-kit": "^0.7.0", + "@eslint/config-array": "^0.23.5", + "@eslint/config-helpers": "^0.7.0", + "@eslint/core": "^1.2.1", + "@eslint/plugin-kit": "^0.7.3", "@humanfs/node": "^0.16.6", "@humanwhocodes/module-importer": "^1.0.1", "@humanwhocodes/retry": "^0.4.2", @@ -7336,14 +7439,14 @@ "esquery": "^1.7.0", "esutils": "^2.0.2", "fast-deep-equal": "^3.1.3", - "file-entry-cache": "^8.0.0", + "file-entry-cache": "11.1.5 || >11.1.6 <12", "find-up": "^5.0.0", "glob-parent": "^6.0.2", "ignore": "^5.2.0", "imurmurhash": "^0.1.4", "is-glob": "^4.0.0", "json-stable-stringify-without-jsonify": "^1.0.1", - "minimatch": "^10.2.4", + "minimatch": "^10.2.5", "natural-compare": "^1.4.0", "optionator": "^0.9.3" }, @@ -7704,9 +7807,9 @@ } }, "node_modules/express-rate-limit": { - "version": "8.6.2", - "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.6.2.tgz", - "integrity": "sha512-YH4ru+eOJxQABscKFfRCy9R7x9QFGdezclVMwwgFFndzS2Xnm0uo6B0ABZsLhcpeptGv2qvuJVWlQr9gQZoC3A==", + "version": "8.7.0", + "resolved": "https://registry.npmjs.org/express-rate-limit/-/express-rate-limit-8.7.0.tgz", + "integrity": "sha512-hOwV7WOxXfjRpAM1DSJWZDXx3GhplwD8IfwuwvogD8i1Qnkgosw/H45s4ZnFAUHDAhPjlY9hLBvJhKmGMyY26g==", "dev": true, "license": "MIT", "dependencies": { @@ -7773,9 +7876,9 @@ "license": "MIT" }, "node_modules/fast-uri": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.5.tgz", - "integrity": "sha512-gHwA1O9LDIcKunMKhObS/HimwtehO1nPUECKAu5TpKgaO19fcWEl4bliWe1jWxVFvIXztJjjQ4L8XQ1EU9f7Jw==", + "version": "3.1.8", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.8.tgz", + "integrity": "sha512-GZMtZUTNRpOVIECoXwLNZS5xUGE+mVNbTB8h/7Rwh2TFWcBQiPzTgyZi05BF9UMZKkLJv8XBRJTlU7zg8+ZfMg==", "dev": true, "funding": [ { @@ -7823,16 +7926,13 @@ "license": "MIT" }, "node_modules/file-entry-cache": { - "version": "8.0.0", - "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-8.0.0.tgz", - "integrity": "sha512-XXTUwCvisa5oacNGRP9SfNtYBNAMi+RPwBFmblZEF7N7swHYQS6/Zfk7SRwx4D5j3CH211YNRco1DEMNVfZCnQ==", + "version": "11.1.5", + "resolved": "https://registry.npmjs.org/file-entry-cache/-/file-entry-cache-11.1.5.tgz", + "integrity": "sha512-+PFTHITI08JIGhnNpGNI8T8inUpgZfk3GNEqfT9R2zZV2iFXg3CvqzSl/uEhs7TSGujYRELEANyDvS8Fj7+S7Q==", "dev": true, "license": "MIT", "dependencies": { - "flat-cache": "^4.0.0" - }, - "engines": { - "node": ">=16.0.0" + "flat-cache": "^6.1.23" } }, "node_modules/filelist": { @@ -7853,9 +7953,9 @@ "license": "MIT" }, "node_modules/filelist/node_modules/brace-expansion": { - "version": "2.1.4", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.4.tgz", - "integrity": "sha512-hGfVzPxthbf3+2yjg/RBs60cB0FhqBS/zvdV/4wn4/BmN0bNMMHPc4V/BbFieqf1TKAGGAHnY4eSjajCl0f2Xg==", + "version": "2.1.7", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.1.7.tgz", + "integrity": "sha512-uZbew1NqdmPDTMJ8ah1y+b+9QEJrfkXFk3RcTQw3X0jW/xRUvFKsg1CfQdSYGdTbXZWExtU3J3ccxtnfw1Fi0g==", "dev": true, "license": "MIT", "dependencies": { @@ -7893,23 +7993,21 @@ } }, "node_modules/flat-cache": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-4.0.1.tgz", - "integrity": "sha512-f7ccFPK3SXFHpx15UIGyRJ/FJQctuKZ0zVuN3frBo4HnK3cay9VEW0R6yPYFHC0AgqhukPzKjq22t5DmAyqGyw==", + "version": "6.1.23", + "resolved": "https://registry.npmjs.org/flat-cache/-/flat-cache-6.1.23.tgz", + "integrity": "sha512-f++BY9pTk+983xK1FLzlLpmM0i0z+jHmx3QESGkURMXujQZz1k5wzwX6hjnQ8goaD0B+sYnDK1yZ6MTyZfUaqA==", "dev": true, "license": "MIT", "dependencies": { - "flatted": "^3.2.9", - "keyv": "^4.5.4" - }, - "engines": { - "node": ">=16" + "cacheable": "^2.5.0", + "flatted": "^3.4.2", + "hookified": "^1.15.0" } }, "node_modules/flatted": { - "version": "3.4.2", - "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.2.tgz", - "integrity": "sha512-PjDse7RzhcPkIJwy5t7KPWQSZ9cAbzQXcafsetQoD7sOJRQlGikNbx7yZp2OotDnJyrDcbyRq3Ttb18iYOqkxA==", + "version": "3.4.4", + "resolved": "https://registry.npmjs.org/flatted/-/flatted-3.4.4.tgz", + "integrity": "sha512-5+ybhBZANEJxaH3X5evAFatUxLfEHSr7n6kYJ+1Qd0mUqr4eu9gIf6GDbWHf8RJijHrjjO8G+la14SlL2SeS1Q==", "dev": true, "license": "ISC" }, @@ -8111,31 +8209,21 @@ "license": "BSD-2-Clause" }, "node_modules/global-directory": { - "version": "4.0.1", - "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-4.0.1.tgz", - "integrity": "sha512-wHTUcDUoZ1H5/0iVqEudYW4/kAlN5cZ3j/bXn0Dpbizl9iaUVeWSHqiOjsgk6OW2bkLclbBjzewBz6weQ1zA2Q==", + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/global-directory/-/global-directory-5.0.0.tgz", + "integrity": "sha512-1pgFdhK3J2LeM+dVf2Pd424yHx2ou338lC0ErNP2hPx4j8eW1Sp0XqSjNxtk6Tc4Kr5wlWtSvz8cn2yb7/SG/w==", "dev": true, "license": "MIT", "dependencies": { - "ini": "4.1.1" + "ini": "6.0.0" }, "engines": { - "node": ">=18" + "node": ">=20" }, "funding": { "url": "https://github.com/sponsors/sindresorhus" } }, - "node_modules/global-directory/node_modules/ini": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/ini/-/ini-4.1.1.tgz", - "integrity": "sha512-QQnnxNyfvmHFIsj7gkPcYymR8Jdw/o7mp5ZFihxn6h8Ci6fh3Dx4E1gPjpQEpIuPo9XVNY/ZUwh4BPMjGyL01g==", - "dev": true, - "license": "ISC", - "engines": { - "node": "^14.17.0 || ^16.13.0 || >=18.0.0" - } - }, "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -8193,10 +8281,23 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/hashery": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/hashery/-/hashery-1.5.1.tgz", + "integrity": "sha512-iZyKG96/JwPz1N55vj2Ie2vXbhu440zfUfJvSwEqEbeLluk7NnapfGqa7LH0mOsnDxTF85Mx8/dyR6HfqcbmbQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^1.15.0" + }, + "engines": { + "node": ">=20" + } + }, "node_modules/hasown": { - "version": "2.0.2", - "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", - "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.4.tgz", + "integrity": "sha512-T2UbfbBEF32wiepXIsMlTW9+dDYC6wMh/t/vYA4tuOMKqWz/n3vr1NFSxQiyP+zk2mXsoMA/i/7qV6LKut1t1A==", "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -8206,15 +8307,22 @@ } }, "node_modules/hono": { - "version": "4.13.1", - "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.1.tgz", - "integrity": "sha512-kdJoFVv2xmayw6cY09H7AbMJMt8Jn5jdlEdXsP7AGBdF2DIptVlKlOLKXP41yPip4/a3yQPv9gVcJYI8YY04dw==", + "version": "4.13.12", + "resolved": "https://registry.npmjs.org/hono/-/hono-4.13.12.tgz", + "integrity": "sha512-6E2QDAc9Ick9Sq77ZrGS/dk2WUYni91aufTw6LJKpV7w8kW5/GxVUc650FOADOmlwg3K+f7Pun6XlV+pYmW6gw==", "dev": true, "license": "MIT", "engines": { "node": ">=16.9.0" } }, + "node_modules/hookified": { + "version": "1.15.1", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-1.15.1.tgz", + "integrity": "sha512-MvG/clsADq1GPM2KGo2nyfaWVyn9naPiXrqIe4jYjXNZQt238kWyOGrsyc/DmRAQ+Re6yeo6yX/yoNCG5KAEVg==", + "dev": true, + "license": "MIT" + }, "node_modules/hosted-git-info": { "version": "9.0.3", "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-9.0.3.tgz", @@ -8292,9 +8400,9 @@ } }, "node_modules/http-cache-semantics": { - "version": "4.2.0", - "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.2.0.tgz", - "integrity": "sha512-dTxcvPXqPvXBQpq5dUr6mEMJX4oIEFv6bwom3FDwKRDsuIjjJGANqhBuoAn9c1RQJIdAKav33ED65E2ys+87QQ==", + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/http-cache-semantics/-/http-cache-semantics-4.3.0.tgz", + "integrity": "sha512-M5t5LlJpS1UHMjvwRQVdFHvPISGeLAxNcrWuJkeGh0KxsqCHZ1O3NXZU/8x7cD0BDcGW8kapxMKTvwlqrNkHkA==", "dev": true, "license": "BSD-2-Clause" }, @@ -8505,9 +8613,9 @@ } }, "node_modules/ip-address": { - "version": "10.5.0", - "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.5.0.tgz", - "integrity": "sha512-R5SnVLJmgYYvf2F2ZgwSBnelz5G4q5AxIC277GDfUaNbrZKNANcBC7RHqYYePlszf4kBolVkJauG0ZjHHFh55g==", + "version": "10.7.3", + "resolved": "https://registry.npmjs.org/ip-address/-/ip-address-10.7.3.tgz", + "integrity": "sha512-A1kdq/tSb5QjvKvAMgIoEvDBIgL7qaqVP/jkvSwYYRZ9iEzvPpopxp2wQfu3SuZRHtpHNxMn8Fs0bS+gf5Xmwg==", "dev": true, "license": "MIT", "engines": { @@ -8752,9 +8860,9 @@ } }, "node_modules/jose": { - "version": "6.2.8", - "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.8.tgz", - "integrity": "sha512-Bsdjwm3Qsd/P0jR+BHDe3LytDfY7WBq2HmCCLIwuVRHMuEC9ae7/R474GIUdF1NgCyZjzVo/A9DOiOBtXq8ZoQ==", + "version": "6.2.12", + "resolved": "https://registry.npmjs.org/jose/-/jose-6.2.12.tgz", + "integrity": "sha512-9NiFmJEex0sy2Dk58j2UGBSHgUs2ypF9eZSu4L6vjOX3Dp96Sw1F3uL+H+D1sx02jZZdzUT0HgvCy59CuvXcWw==", "dev": true, "license": "MIT", "funding": { @@ -8769,9 +8877,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.1.tgz", - "integrity": "sha512-CY6crGq313MX8GkwvB7tzgp99vjQxY1++5y10/BKN/GUfHqWaOGQMNZkBvqSzsZKWk/ijwHlWzzkLulsGHhjWQ==", + "version": "4.3.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.3.2.tgz", + "integrity": "sha512-SFNOvSJ+Dgf/9An904Yx+CgSlIPCkIpao4qo51lpee25TIRejdH3rhR4EZMGoNx3/TP3O+wzWuiTFl4sqbltzA==", "dev": true, "funding": [ { @@ -8865,13 +8973,6 @@ "node": ">=6" } }, - "node_modules/json-buffer": { - "version": "3.0.1", - "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", - "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", - "dev": true, - "license": "MIT" - }, "node_modules/json-parse-even-better-errors": { "version": "5.0.0", "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-5.0.0.tgz", @@ -8959,13 +9060,13 @@ } }, "node_modules/keyv": { - "version": "4.5.4", - "resolved": "https://registry.npmjs.org/keyv/-/keyv-4.5.4.tgz", - "integrity": "sha512-oxVHkHR/EJf2CNXnWxRLW6mg7JyCCUcG0DtEGmL2ctUo1PNTin1PUil+r/+4r5MpVgC/fn1kjsx7mjSujKqIpw==", + "version": "5.6.0", + "resolved": "https://registry.npmjs.org/keyv/-/keyv-5.6.0.tgz", + "integrity": "sha512-CYDD3SOtsHtyXeEORYRx2qBtpDJFjRTGXUtmNEMGyzYOKj1TE3tycdlho7kA1Ufx9OYWZzg52QFBGALTirzDSw==", "dev": true, "license": "MIT", "dependencies": { - "json-buffer": "3.0.1" + "@keyv/serialize": "^1.1.1" } }, "node_modules/levn": { @@ -9145,48 +9246,6 @@ "dev": true, "license": "MIT" }, - "node_modules/lodash.camelcase": { - "version": "4.3.0", - "resolved": "https://registry.npmjs.org/lodash.camelcase/-/lodash.camelcase-4.3.0.tgz", - "integrity": "sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.kebabcase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.kebabcase/-/lodash.kebabcase-4.1.1.tgz", - "integrity": "sha512-N8XRTIMMqqDgSy4VLKPnJ/+hpGZN+PHQiJnSenYqPaVV/NCqEogTnAdZLQiGKhxX+JCs8waWq2t1XHWKOmlY8g==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.mergewith": { - "version": "4.6.2", - "resolved": "https://registry.npmjs.org/lodash.mergewith/-/lodash.mergewith-4.6.2.tgz", - "integrity": "sha512-GK3g5RPZWTRSeLSpgP8Xhra+pnjBC56q9FZYe1d5RN3TJ35dbkGy3YqBSMbyCrlbi+CM9Z3Jk5yTL7RCsqboyQ==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.snakecase": { - "version": "4.1.1", - "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", - "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.startcase": { - "version": "4.4.0", - "resolved": "https://registry.npmjs.org/lodash.startcase/-/lodash.startcase-4.4.0.tgz", - "integrity": "sha512-+WKqsK294HMSc2jEbNgpHpd0JfIBhp7rEV4aqXWqFr6AlXov+SlcgB1Fv01y2kGe3Gc8nMW7VA0SrGuSkRfIEg==", - "dev": true, - "license": "MIT" - }, - "node_modules/lodash.upperfirst": { - "version": "4.3.1", - "resolved": "https://registry.npmjs.org/lodash.upperfirst/-/lodash.upperfirst-4.3.1.tgz", - "integrity": "sha512-sReKOYJIJf74dhJONhU4e0/shzi1trVbSWDOhKYE5XV2O+H7Sb2Dihwuc7xWxVl+DgFPyTqIN3zMfT9cq5iWDg==", - "dev": true, - "license": "MIT" - }, "node_modules/log-symbols": { "version": "7.0.1", "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-7.0.1.tgz", @@ -9357,9 +9416,9 @@ } }, "node_modules/markdown-it": { - "version": "14.2.0", - "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.2.0.tgz", - "integrity": "sha512-1TGiQiJVRQ3NPmZH6sx5Cfnmg6GQm9jvC1ch4TK511NjSJvjzKLzn5pPfZRNZkRPZP0HqCioSndqH8v2nRaWVQ==", + "version": "14.3.2", + "resolved": "https://registry.npmjs.org/markdown-it/-/markdown-it-14.3.2.tgz", + "integrity": "sha512-sHHjZ5fJKlgrG4qns2YwVcdNep35h5fERrfkD2YNsb9UFk0UIHarbiTaHKVMlPuWAoiilyK8Fv/jAm11slsY7Q==", "funding": [ { "type": "github", @@ -9373,8 +9432,8 @@ "license": "MIT", "dependencies": { "argparse": "^2.0.1", - "entities": "^4.4.0", - "linkify-it": "^5.0.1", + "entities": "^4.5.0", + "linkify-it": "^5.0.2", "mdurl": "^2.0.0", "punycode.js": "^2.3.1", "uc.micro": "^2.1.0" @@ -9772,9 +9831,9 @@ } }, "node_modules/ng-mocks": { - "version": "14.15.3", - "resolved": "https://registry.npmjs.org/ng-mocks/-/ng-mocks-14.15.3.tgz", - "integrity": "sha512-J0UafeI2K5kc5oDhIinGu+VqT990b0sP2dlKV3JxXYTlaQxQ7Mm0LLrxGz10hsGGJVtn9JEKkhjnpb6oijLT3w==", + "version": "14.18.1", + "resolved": "https://registry.npmjs.org/ng-mocks/-/ng-mocks-14.18.1.tgz", + "integrity": "sha512-XEArPJ06C3G0XmLIS9YMy1Xke+Conn+pAV1IC3hA3AEbFag74kxXN3F/IaMWJC1N1E1UoTPH1SYFBNloBAHAOw==", "dev": true, "license": "MIT", "funding": { @@ -9929,9 +9988,9 @@ } }, "node_modules/node-gyp/node_modules/undici": { - "version": "6.28.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-6.28.0.tgz", - "integrity": "sha512-LIY910g9TI13YS95lrMFrs8Rm/u/irgHeTWoKCoteeJ04CUJ92eEfj0rVn+7VKMPBpUPiUoBKfhNyLI23EE/KA==", + "version": "6.29.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-6.29.0.tgz", + "integrity": "sha512-R+RODBqp6i2pPflGdq+xIOUkl+RNfGgHwoinecKu/JCuf2uO06cOKoDbI2P7Dn6KcswdKwrczbU6IYJ6K8X+wg==", "dev": true, "license": "MIT", "engines": { @@ -9955,9 +10014,9 @@ } }, "node_modules/node-releases": { - "version": "2.0.54", - "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.54.tgz", - "integrity": "sha512-YHs7BmmcsdAI5Ozuf8JZo6PT0mv2GIWC9vMfvUC3dp65M8hn7Ux8CPL+2oBI7juNuj9d0ndhTcznq2ODBps9cQ==", + "version": "2.0.57", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.57.tgz", + "integrity": "sha512-kQK9LGGFiHtrWiNhZtA7Qbw17AQz+dmsEKODRIVTXA9+e5MS/2gZEBhYJt13GrAz5/IOZKddH/0Z3TP/Zgo+yw==", "dev": true, "license": "MIT", "engines": { @@ -10508,9 +10567,9 @@ } }, "node_modules/piscina": { - "version": "5.2.0", - "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.2.0.tgz", - "integrity": "sha512-DszUCKeVN/5G5QKo6jAVHL8fmKnkJvQ0ACiVgY7YGCq3TUB2oznAOayvZPIAdEThvhczkXR+qm3IHsNXpFCYfA==", + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/piscina/-/piscina-5.3.2.tgz", + "integrity": "sha512-vv7l/mM7B9WgrhDaudIqv1x6v4LOBRYYmpWGdWushhpbCmVHp11sUB2v4hj1CAeTGXVxuZFz6xOU1RalAiPKfQ==", "dev": true, "license": "MIT", "engines": { @@ -10654,15 +10713,15 @@ "license": "MIT" }, "node_modules/primeng": { - "version": "21.1.6", - "resolved": "https://registry.npmjs.org/primeng/-/primeng-21.1.6.tgz", - "integrity": "sha512-kPLoixeMPXIXdQO+RnALRmMfSbPQYAeGx1yTltqkj7XbMkSm9Dr7IxqZZUYjUHemCcr2wyjW82G68WjSMDj66w==", + "version": "21.1.10", + "resolved": "https://registry.npmjs.org/primeng/-/primeng-21.1.10.tgz", + "integrity": "sha512-1dNoVrrmdif95iuPwcSyzS0MwBy4yrbB/44s4gp+UeoZU11G9+SNFd6P/3tiiun7bAh2vbe4Uf0juy7DMtnRbA==", "license": "SEE LICENSE IN LICENSE.md", "dependencies": { "@primeuix/motion": "^0.0.10", "@primeuix/styled": "^0.7.4", "@primeuix/styles": "^2.0.3", - "@primeuix/utils": "^0.6.3", + "@primeuix/utils": "^0.7.2", "tslib": "^2.3.0" }, "peerDependencies": { @@ -10675,6 +10734,15 @@ "rxjs": "^6.0.0 || ^7.8.1" } }, + "node_modules/primeng/node_modules/@primeuix/utils": { + "version": "0.7.2", + "resolved": "https://registry.npmjs.org/@primeuix/utils/-/utils-0.7.2.tgz", + "integrity": "sha512-pmEbSfP0Phf9W9RweiM66zXnkn73ZeKyYINElbX3uZ2+stzzaba2svLAl3B1pHVcRw5t43O0VciaGe4ye2EXKw==", + "license": "MIT", + "engines": { + "node": ">=12.11.0" + } + }, "node_modules/proc-log": { "version": "6.1.0", "resolved": "https://registry.npmjs.org/proc-log/-/proc-log-6.1.0.tgz", @@ -10717,6 +10785,26 @@ "node": ">=6" } }, + "node_modules/qified": { + "version": "0.10.1", + "resolved": "https://registry.npmjs.org/qified/-/qified-0.10.1.tgz", + "integrity": "sha512-+Owyggi9IxT1ePKGafcI87ubSmxol6smwJ+RAHDQlx9+9cPwFWDiKFFCPuWhr9ignlGpZ9vDQLw67N4dcTVFEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "hookified": "^2.1.1" + }, + "engines": { + "node": ">=20" + } + }, + "node_modules/qified/node_modules/hookified": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/hookified/-/hookified-2.2.0.tgz", + "integrity": "sha512-p/LgFzRN5FeoD3DLS6bkUapeye6E4SI6yJs6KetENd18S+FBthqYq2amJUWpt5z0EQwwHemidjY5OqJGEKm5uA==", + "dev": true, + "license": "MIT" + }, "node_modules/qrcode": { "version": "1.5.4", "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", @@ -10896,12 +10984,13 @@ } }, "node_modules/qs": { - "version": "6.15.2", - "resolved": "https://registry.npmjs.org/qs/-/qs-6.15.2.tgz", - "integrity": "sha512-Rzq0KEyX/w/tEybncDgdkZrJgVUsUMk3xjh3t5bv3S1HTAtg+uOYt72+ZfwiQwKdysThkTBdL/rTi6HDmX9Ddw==", + "version": "6.16.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.16.0.tgz", + "integrity": "sha512-h6fhOIaRrID2CbEY2fqs+7t+UXZo+MLAnU5gRIq85uFtdiUPCdsApMlHhXogKVM4HM2DVbIjGNTTYH2OcmP1vA==", "license": "BSD-3-Clause", "dependencies": { - "side-channel": "^1.1.0" + "es-define-property": "^1.0.1", + "side-channel": "^1.1.1" }, "engines": { "node": ">=0.6" @@ -11038,9 +11127,9 @@ "license": "MIT" }, "node_modules/rimraf/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -11350,14 +11439,14 @@ } }, "node_modules/side-channel": { - "version": "1.1.0", - "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", - "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.1.tgz", + "integrity": "sha512-6x6dK6zJdpTzF4sQeNYxwtvBzf6Eg4GtlesS94HOvTudUeyK2WXAaIfmDgsyslYrRBeFIlsi54AYsFGUuhmvrQ==", "license": "MIT", "dependencies": { "es-errors": "^1.3.0", - "object-inspect": "^1.13.3", - "side-channel-list": "^1.0.0", + "object-inspect": "^1.13.4", + "side-channel-list": "^1.0.1", "side-channel-map": "^1.0.1", "side-channel-weakmap": "^1.0.2" }, @@ -11589,9 +11678,9 @@ "license": "MIT" }, "node_modules/source-map-explorer/node_modules/brace-expansion": { - "version": "1.1.18", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.18.tgz", - "integrity": "sha512-Edep/X9fGqVNmzKBVsDYIOtD+z1tuezV70LBjdCst9Tqu76lsnvRiZ6oTic1n+/BIwX6QDGAO94PN4N2SADvtw==", + "version": "1.1.21", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", + "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", "dev": true, "license": "MIT", "dependencies": { @@ -12207,16 +12296,16 @@ } }, "node_modules/typescript-eslint": { - "version": "8.58.2", - "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.58.2.tgz", - "integrity": "sha512-V8iSng9mRbdZjl54VJ9NKr6ZB+dW0J3TzRXRGcSbLIej9jV86ZRtlYeTKDR/QLxXykocJ5icNzbsl2+5TzIvcQ==", + "version": "8.71.0", + "resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.71.0.tgz", + "integrity": "sha512-fBdHYiqQ14RW6mOMXD14Svn82ZsCYAoQSzGRzyEjR59S5A2Krh/l7fGTOQ7iCr8gGy/mHVXtEF7s5fgjEdV0Pw==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/eslint-plugin": "8.58.2", - "@typescript-eslint/parser": "8.58.2", - "@typescript-eslint/typescript-estree": "8.58.2", - "@typescript-eslint/utils": "8.58.2" + "@typescript-eslint/eslint-plugin": "8.71.0", + "@typescript-eslint/parser": "8.71.0", + "@typescript-eslint/typescript-estree": "8.71.0", + "@typescript-eslint/utils": "8.71.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -12237,9 +12326,9 @@ "license": "MIT" }, "node_modules/undici": { - "version": "7.29.0", - "resolved": "https://registry.npmjs.org/undici/-/undici-7.29.0.tgz", - "integrity": "sha512-IDxfleLmmbSskfWSUATiN1nfn2rDuvnMOqb5CWR92iIfojA0Ud+ulOAAEQ57LPr9rWmsreUyf5lwyao+7GNNVw==", + "version": "7.30.0", + "resolved": "https://registry.npmjs.org/undici/-/undici-7.30.0.tgz", + "integrity": "sha512-dkrQXeHSaoamnItlYbmzG0wFYrM0ZwDxCIg0A7aKjTyyhh9svRzCNFEzV+Vm05/yehjCzjDZ31KXfGEjYSztDQ==", "dev": true, "license": "MIT", "engines": { @@ -12263,9 +12352,9 @@ } }, "node_modules/update-browserslist-db": { - "version": "1.3.2", - "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.2.tgz", - "integrity": "sha512-UQ+MSxlhRm1bzjhU+DcuXfjFO1FzNtqhK5+9Yvlp90ItDLk5vT932A0rFu619nf7RVS+Y/VeaUW1jaRDqZ8VJw==", + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.3.3.tgz", + "integrity": "sha512-pJ2sYawQS0R/WI928Gj5GlPhTGzbMelq0+4INtSYNDV9ErKJcX6xjGWkoG/VnB3dpUm00zALaqkrUD77pO5TDQ==", "dev": true, "funding": [ { @@ -12398,19 +12487,19 @@ } }, "node_modules/vitest": { - "version": "4.1.9", - "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.9.tgz", - "integrity": "sha512-nE3/LEyc0z87uHYLZebqCUOaJr2hdtuPp7BQ4BosVFnfltxgAvMG08NyrSGlPpOUWvR27c5flSmYFTNr78L9GQ==", + "version": "4.1.11", + "resolved": "https://registry.npmjs.org/vitest/-/vitest-4.1.11.tgz", + "integrity": "sha512-fhACrNXUidIbGSBr5FlbuBkO7VWC1ZyLl0DO4CU2DrQoAPxX84Ysxs+HeGQpii5lZWV1Q4gBZTTu49mF+A6Edw==", "dev": true, "license": "MIT", "dependencies": { - "@vitest/expect": "4.1.9", - "@vitest/mocker": "4.1.9", - "@vitest/pretty-format": "4.1.9", - "@vitest/runner": "4.1.9", - "@vitest/snapshot": "4.1.9", - "@vitest/spy": "4.1.9", - "@vitest/utils": "4.1.9", + "@vitest/expect": "4.1.11", + "@vitest/mocker": "4.1.11", + "@vitest/pretty-format": "4.1.11", + "@vitest/runner": "4.1.11", + "@vitest/snapshot": "4.1.11", + "@vitest/spy": "4.1.11", + "@vitest/utils": "4.1.11", "es-module-lexer": "^2.0.0", "expect-type": "^1.3.0", "magic-string": "^0.30.21", @@ -12438,12 +12527,12 @@ "@edge-runtime/vm": "*", "@opentelemetry/api": "^1.9.0", "@types/node": "^20.0.0 || ^22.0.0 || >=24.0.0", - "@vitest/browser-playwright": "4.1.9", - "@vitest/browser-preview": "4.1.9", - "@vitest/browser-webdriverio": "4.1.9", - "@vitest/coverage-istanbul": "4.1.9", - "@vitest/coverage-v8": "4.1.9", - "@vitest/ui": "4.1.9", + "@vitest/browser-playwright": "4.1.11", + "@vitest/browser-preview": "4.1.11", + "@vitest/browser-webdriverio": "4.1.11", + "@vitest/coverage-istanbul": "4.1.11", + "@vitest/coverage-v8": "4.1.11", + "@vitest/ui": "4.1.11", "happy-dom": "*", "jsdom": "*", "vite": "^6.0.0 || ^7.0.0 || ^8.0.0" @@ -12523,9 +12612,9 @@ "optional": true }, "node_modules/web-vitals": { - "version": "4.2.4", - "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-4.2.4.tgz", - "integrity": "sha512-r4DIlprAGwJ7YM11VZp4R884m0Vmgr6EAKe3P+kO0PPj3Unqyvv59rczf6UiGcb9Z8QxZVcqKNwv/g0WNdWwsw==", + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/web-vitals/-/web-vitals-6.2.1.tgz", + "integrity": "sha512-rLcLXA2sx6+9dE88NHFubwTtGxpK4yYBLj6qHPdFoCaLr0cXGb4efOqtKLlm4loGA4OEKHIQKMKzZkKyOh5ctw==", "license": "Apache-2.0" }, "node_modules/webidl-conversions": { diff --git a/package.json b/package.json index 8cc08cdbc..c3e693568 100644 --- a/package.json +++ b/package.json @@ -31,28 +31,28 @@ }, "private": true, "dependencies": { - "@angular/animations": "^21.2.19", + "@angular/animations": "^21.2.25", "@angular/cdk": "^21.2.14", - "@angular/common": "^21.2.19", - "@angular/compiler": "^21.2.19", - "@angular/core": "^21.2.19", - "@angular/forms": "^21.2.19", - "@angular/platform-browser": "^21.2.19", - "@angular/platform-browser-dynamic": "^21.2.19", - "@angular/platform-server": "^21.2.19", - "@angular/router": "^21.2.19", - "@angular/ssr": "^21.2.20", + "@angular/common": "^21.2.25", + "@angular/compiler": "^21.2.25", + "@angular/core": "^21.2.25", + "@angular/forms": "^21.2.25", + "@angular/platform-browser": "^21.2.25", + "@angular/platform-browser-dynamic": "^21.2.25", + "@angular/platform-server": "^21.2.25", + "@angular/router": "^21.2.25", + "@angular/ssr": "^21.2.24", "@centerforopenscience/markdown-it-atrules": "^0.1.2", "@citation-js/core": "^0.7.21", "@citation-js/plugin-csl": "^0.7.21", - "@fortawesome/fontawesome-free": "^7.2.0", + "@fortawesome/fontawesome-free": "^7.3.1", "@mdit/plugin-img-size": "0.23.1", - "@newrelic/browser-agent": "^1.310.1", + "@newrelic/browser-agent": "^1.323.0", "@ngx-translate/core": "^17.0.0", "@ngx-translate/http-loader": "^17.0.0", "@ngxs/store": "^21.0.0", "@primeuix/themes": "^2.0.3", - "@sentry/angular": "^10.42.0", + "@sentry/angular": "^10.75.3", "@traptitech/markdown-it-katex": "^3.6.0", "ace-builds": "1.43.6", "angular-google-tag-manager": "^1.13.0", @@ -70,29 +70,29 @@ "ngx-markdown-editor": "^5.3.4", "primeflex": "^4.0.0", "primeicons": "^7.0.0", - "primeng": "^21.1.3", + "primeng": "^21.1.10", "rxjs": "~7.8.0", "tslib": "^2.3.0" }, "devDependencies": { - "@angular-eslint/eslint-plugin": "^21.3.0", - "@angular-eslint/eslint-plugin-template": "^21.3.0", - "@angular-eslint/template-parser": "^21.3.0", - "@angular/build": "^21.2.20", - "@angular/cli": "^21.2.20", - "@angular/compiler-cli": "^21.2.19", - "@commitlint/cli": "^20.4.3", - "@commitlint/config-conventional": "^20.4.3", + "@angular-eslint/eslint-plugin": "^21.4.0", + "@angular-eslint/eslint-plugin-template": "^21.4.0", + "@angular-eslint/template-parser": "^21.4.0", + "@angular/build": "^21.2.24", + "@angular/cli": "^21.2.24", + "@angular/compiler-cli": "^21.2.25", + "@commitlint/cli": "^20.5.3", + "@commitlint/config-conventional": "^20.5.3", "@eslint/js": "^10.0.1", "@types/express": "^5.0.1", "@types/gapi": "^0.0.47", "@types/gapi.auth2": "^0.0.61", "@types/markdown-it": "^14.1.2", "@types/node": "^20.17.19", - "@vitest/coverage-v8": "^4.1.9", - "angular-eslint": "^21.3.0", + "@vitest/coverage-v8": "^4.1.11", + "angular-eslint": "^21.4.0", "angularx-qrcode": "^21.0.4", - "eslint": "^10.0.2", + "eslint": "^10.11.0", "eslint-config-prettier": "^10.1.8", "eslint-plugin-prettier": "^5.5.5", "eslint-plugin-simple-import-sort": "^12.1.1", @@ -100,12 +100,15 @@ "husky": "^9.1.7", "jsdom": "^29.1.1", "lint-staged": "^16.3.2", - "ng-mocks": "^14.15.3", + "ng-mocks": "^14.18.1", "prettier": "3.8.1", "source-map-explorer": "^2.5.3", "typescript": "~5.9.3", - "typescript-eslint": "^8.56.1", - "vitest": "^4.1.9" + "typescript-eslint": "^8.71.0", + "vitest": "^4.1.11" + }, + "overrides": { + "piscina": "5.3.2" }, "lint-staged": { "**/*.{ts,html,scss}": [ diff --git a/src/app/app.component.html b/src/app/app.component.html index 3e72ea9b0..c642376d8 100644 --- a/src/app/app.component.html +++ b/src/app/app.component.html @@ -6,5 +6,9 @@

{{ 'maintenance.title' | translate }}

{{ 'maintenance.message' | translate }}

+

+ {{ 'maintenance.details' | translate }} + https://status.cos.io/ +

} diff --git a/src/app/app.component.scss b/src/app/app.component.scss index 62deb123b..da0b60fd8 100644 --- a/src/app/app.component.scss +++ b/src/app/app.component.scss @@ -1,6 +1,6 @@ .maintenance-overlay { position: fixed; inset: 0; - z-index: 2000; + z-index: 3100; background: var(--white); } diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index ace56b109..36b92bc58 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -67,6 +67,14 @@ export const routes: Routes = [ (mod) => mod.ChooseRepositoryComponent ), }, + { + path: 'resend', + loadComponent: () => + import('./features/auth/pages/resend/resend-confirmation.component').then( + (mod) => mod.ResendConfirmationComponent + ), + data: { skipBreadcrumbs: true }, + }, { path: 'search', loadComponent: () => import('./features/search/search.component').then((mod) => mod.SearchComponent), diff --git a/src/app/core/enums/maintenance-status.enum.ts b/src/app/core/enums/maintenance-status.enum.ts new file mode 100644 index 000000000..775e082a4 --- /dev/null +++ b/src/app/core/enums/maintenance-status.enum.ts @@ -0,0 +1,5 @@ +export enum MaintenanceStatus { + Active = 'active', + Inactive = 'inactive', + Unknown = 'unknown', +} diff --git a/src/app/core/services/auth.service.spec.ts b/src/app/core/services/auth.service.spec.ts new file mode 100644 index 000000000..642191d49 --- /dev/null +++ b/src/app/core/services/auth.service.spec.ts @@ -0,0 +1,104 @@ +import { CookieService } from 'ngx-cookie-service'; +import { MockProvider } from 'ng-mocks'; + +import { PLATFORM_ID } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; + +import { ENVIRONMENT } from '@core/provider/environment.provider'; +import { WINDOW } from '@core/provider/window.provider'; +import { JsonApiService } from '@osf/shared/services/json-api.service'; +import { LoaderService } from '@osf/shared/services/loader.service'; + +import { CookieServiceMock } from '@testing/providers/cookie-service.mock'; +import { JsonApiServiceMock } from '@testing/providers/json-api.service.mock'; +import { LoaderServiceMock } from '@testing/providers/loader-service.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +import { AuthService } from './auth.service'; + +describe('AuthService', () => { + const webUrl = 'https://web.test'; + const casUrl = 'https://cas.test'; + const origin = 'https://osf.test'; + + let service: AuthService; + let loaderService: LoaderServiceMock; + let locationMock: { href: string; pathname: string; origin: string }; + + function setup(overrides: { pathname?: string; href?: string; isBrowser?: boolean } = {}) { + const pathname = overrides.pathname ?? '/dashboard'; + locationMock = { + href: overrides.href ?? `${origin}${pathname}`, + pathname, + origin, + }; + + loaderService = new LoaderServiceMock(); + + TestBed.configureTestingModule({ + providers: [ + AuthService, + provideMockStore(), + MockProvider(JsonApiService, JsonApiServiceMock.simple()), + MockProvider(CookieService, CookieServiceMock.simple()), + MockProvider(LoaderService, loaderService), + MockProvider(WINDOW, { location: locationMock } as Window), + MockProvider(ENVIRONMENT, { webUrl, casUrl, apiDomainUrl: 'https://api.test' }), + MockProvider(PLATFORM_ID, overrides.isBrowser === false ? 'server' : 'browser'), + ], + }); + + service = TestBed.inject(AuthService); + } + + function nextFromServiceParam(href: string): string | null { + const serviceParam = new URL(href).searchParams.get('service'); + return serviceParam ? new URL(serviceParam).searchParams.get('next') : null; + } + + it('should use the current page as next when signing in from a regular page', () => { + const href = `${origin}/project/abc`; + setup({ pathname: '/project/abc', href }); + + service.navigateToSignIn(); + + expect(loaderService.show).toHaveBeenCalled(); + expect(nextFromServiceParam(locationMock.href)).toBe(href); + }); + + it('should use home as next when signing in from reset password', () => { + setup({ + pathname: '/resetpassword/user-1/token-1', + href: `${origin}/resetpassword/user-1/token-1`, + }); + + service.navigateToSignIn(); + + expect(nextFromServiceParam(locationMock.href)).toBe(`${webUrl}/`); + }); + + it('should use home as next when signing in from forgot password with orcid', () => { + setup({ pathname: '/forgotpassword', href: `${origin}/forgotpassword` }); + + service.navigateToOrcidSignIn(); + + expect(new URL(locationMock.href).searchParams.get('next')).toBe(`${webUrl}/`); + }); + + it('should use home as next when signing in from register with institution', () => { + setup({ pathname: '/register', href: `${origin}/register` }); + + service.navigateToInstitutionSignIn(); + + expect(new URL(locationMock.href).searchParams.get('next')).toBe(`${webUrl}/`); + }); + + it('should not redirect to sign in when not in the browser', () => { + setup({ isBrowser: false, href: `${origin}/dashboard` }); + + service.navigateToSignIn(); + + expect(loaderService.show).not.toHaveBeenCalled(); + expect(locationMock.href).toBe(`${origin}/dashboard`); + }); +}); diff --git a/src/app/core/services/auth.service.ts b/src/app/core/services/auth.service.ts index 09f73bbb8..70a3328e6 100644 --- a/src/app/core/services/auth.service.ts +++ b/src/app/core/services/auth.service.ts @@ -7,6 +7,7 @@ import { inject, Injectable, PLATFORM_ID } from '@angular/core'; import { SignUpModel } from '@core/models/sign-up.model'; import { ENVIRONMENT } from '@core/provider/environment.provider'; +import { WINDOW } from '@core/provider/window.provider'; import { ClearCurrentUser } from '@osf/core/store/user'; import { urlParam } from '@osf/shared/helpers/url-param.helper'; import { JsonApiService } from '@osf/shared/services/json-api.service'; @@ -20,6 +21,7 @@ export class AuthService { private readonly cookieService = inject(CookieService); private readonly loaderService = inject(LoaderService); private readonly environment = inject(ENVIRONMENT); + private readonly window = inject(WINDOW); private readonly platformId = inject(PLATFORM_ID); private readonly actions = createDispatchMap({ clearCurrentUser: ClearCurrentUser }); @@ -43,12 +45,12 @@ export class AuthService { this.loaderService.show(); const serviceUrl = new URL(`${this.webUrl}/login`); - serviceUrl.searchParams.set('next', window.location.href); + serviceUrl.searchParams.set('next', this.getPostLoginRedirectUrl()); const loginUrl = new URL(`${this.casUrl}/login`); loginUrl.searchParams.set('service', serviceUrl.toString()); - window.location.href = loginUrl.toString(); + this.window.location.href = loginUrl.toString(); } navigateToOrcidSignIn(): void { @@ -59,10 +61,10 @@ export class AuthService { const loginUrl = `${this.casUrl}/login?${urlParam({ redirectOrcid: 'true', service: `${this.webUrl}/login`, - next: window.location.href, + next: this.getPostLoginRedirectUrl(), })}`; - window.location.href = loginUrl; + this.window.location.href = loginUrl; } navigateToInstitutionSignIn(): void { @@ -73,10 +75,10 @@ export class AuthService { const loginUrl = `${this.casUrl}/login?${urlParam({ campaign: 'institution', service: `${this.webUrl}/login`, - next: window.location.href, + next: this.getPostLoginRedirectUrl(), })}`; - window.location.href = loginUrl; + this.window.location.href = loginUrl; } logout(nextUrl?: string): void { @@ -85,7 +87,7 @@ export class AuthService { if (isPlatformBrowser(this.platformId)) { this.cookieService.deleteAll(); - window.location.href = `${this.webUrl}/logout/?next=${encodeURIComponent(nextUrl || `${window.location.origin}/`)}`; + this.window.location.href = `${this.webUrl}/logout/?next=${encodeURIComponent(nextUrl || `${this.window.location.origin}/`)}`; } } @@ -117,4 +119,28 @@ export class AuthService { return this.jsonApiService.post(baseUrl, body); } + + resendConfirmationUrl(email: string) { + const baseUrl = `${this.apiUrl}/resend_confirmation/`; + const body = { + data: { + attributes: { + email: email, + }, + }, + }; + + return this.jsonApiService.post(baseUrl, body); + } + + private getPostLoginRedirectUrl(): string { + const pathname = this.window.location.pathname; + const excludedPaths = ['/resetpassword', '/forgotpassword', '/register', '/resend']; + + if (excludedPaths.some((path) => pathname === path || pathname.startsWith(`${path}/`))) { + return `${this.webUrl}/`; + } + + return this.window.location.href; + } } diff --git a/src/app/core/services/maintenance-mode.service.spec.ts b/src/app/core/services/maintenance-mode.service.spec.ts new file mode 100644 index 000000000..7c82286ec --- /dev/null +++ b/src/app/core/services/maintenance-mode.service.spec.ts @@ -0,0 +1,146 @@ +import { HttpTestingController, TestRequest } from '@angular/common/http/testing'; +import { TestBed } from '@angular/core/testing'; + +import { BYPASS_ERROR_INTERCEPTOR } from '@core/interceptors/error-interceptor.tokens'; +import { MaintenanceResponse } from '@core/models/maintenance-response.model'; +import { ENVIRONMENT } from '@core/provider/environment.provider'; + +import { provideOSFCore, provideOSFHttp } from '@testing/osf.testing.provider'; + +import { MaintenanceModeService } from './maintenance-mode.service'; + +describe('MaintenanceModeService', () => { + let service: MaintenanceModeService; + let httpMock: HttpTestingController; + + const apiUrl = 'http://localhost:8000/v2/'; + const pollIntervalMs = 5 * 60 * 1_000; + const maintenanceOn: MaintenanceResponse = { meta: { maintenance_mode: true } }; + const maintenanceOff: MaintenanceResponse = { meta: { maintenance_mode: false } }; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [provideOSFCore(), provideOSFHttp()], + }); + service = TestBed.inject(MaintenanceModeService); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + service.ngOnDestroy(); + httpMock.verify(); + vi.useRealTimers(); + }); + + function expectStatusRequest(): TestRequest { + const req = httpMock.expectOne(apiUrl); + expect(req.request.method).toBe('GET'); + expect(req.request.context.get(BYPASS_ERROR_INTERCEPTOR)).toBe(true); + return req; + } + + function flushPoll(response: MaintenanceResponse | null, status = 200): void { + vi.advanceTimersByTime(0); + const req = expectStatusRequest(); + if (status === 200) { + req.flush(response ?? {}); + return; + } + req.flush(response, { status, statusText: 'Error' }); + } + + it('should GET /v2/ with the error interceptor bypassed on checkOnce', () => { + service.checkOnce(); + expectStatusRequest().flush(maintenanceOff); + expect(service.isActive()).toBe(false); + }); + + it('should activate when /v2/ succeeds with maintenance_mode true', () => { + service.checkOnce(); + expectStatusRequest().flush(maintenanceOn); + expect(service.isActive()).toBe(true); + }); + + it('should not activate when /v2/ succeeds without maintenance_mode', () => { + service.checkOnce(); + expectStatusRequest().flush(maintenanceOff); + expect(service.isActive()).toBe(false); + }); + + it('should activate when /v2/ returns 503 with maintenance_mode true', () => { + service.checkOnce(); + expectStatusRequest().flush(maintenanceOn, { status: 503, statusText: 'Service Unavailable' }); + expect(service.isActive()).toBe(true); + }); + + it('should not activate when /v2/ returns 503 without maintenance_mode', () => { + service.checkOnce(); + expectStatusRequest().flush(null, { status: 503, statusText: 'Service Unavailable' }); + expect(service.isActive()).toBe(false); + }); + + it('should not activate when the /v2/ request fails', () => { + service.checkOnce(); + expectStatusRequest().error(new ProgressEvent('error')); + expect(service.isActive()).toBe(false); + }); + + it('should deactivate when a poll receives a successful non-maintenance response', () => { + vi.useFakeTimers(); + service.activate(); + expect(service.isActive()).toBe(true); + flushPoll(maintenanceOff); + expect(service.isActive()).toBe(false); + }); + + it('should stay active when a poll request fails', () => { + vi.useFakeTimers(); + service.activate(); + flushPoll(null, 502); + expect(service.isActive()).toBe(true); + }); + + it('should not start a second poll when already active', () => { + vi.useFakeTimers(); + service.activate(); + service.activate(); + flushPoll(maintenanceOn); + vi.advanceTimersByTime(pollIntervalMs); + expectStatusRequest().flush(maintenanceOn); + }); + + it('should stop polling on destroy', () => { + vi.useFakeTimers(); + service.activate(); + flushPoll(maintenanceOn); + service.ngOnDestroy(); + vi.advanceTimersByTime(pollIntervalMs); + httpMock.expectNone(apiUrl); + }); + + describe('when maintenance is forced by config', () => { + beforeEach(() => { + TestBed.inject(ENVIRONMENT).maintenance = true; + }); + + it('should activate on checkOnce without requesting /v2/', () => { + service.checkOnce(); + httpMock.expectNone(apiUrl); + expect(service.isActive()).toBe(true); + }); + + it('should not start polling on activate', () => { + vi.useFakeTimers(); + service.activate(); + vi.advanceTimersByTime(pollIntervalMs); + httpMock.expectNone(apiUrl); + expect(service.isActive()).toBe(true); + }); + + it('should stay active on deactivate', () => { + service.checkOnce(); + service.deactivate(); + expect(service.isActive()).toBe(true); + }); + }); +}); diff --git a/src/app/core/services/maintenance-mode.service.ts b/src/app/core/services/maintenance-mode.service.ts index 5cc350a8a..bb92be21c 100644 --- a/src/app/core/services/maintenance-mode.service.ts +++ b/src/app/core/services/maintenance-mode.service.ts @@ -1,8 +1,9 @@ import { catchError, map, Observable, of, Subscription, switchMap, timer } from 'rxjs'; -import { HttpClient, HttpContext } from '@angular/common/http'; +import { HttpClient, HttpContext, HttpErrorResponse } from '@angular/common/http'; import { inject, Injectable, OnDestroy, signal } from '@angular/core'; +import { MaintenanceStatus } from '@core/enums/maintenance-status.enum'; import { MaintenanceResponse } from '@core/models/maintenance-response.model'; import { ENVIRONMENT } from '@core/provider/environment.provider'; @@ -23,13 +24,22 @@ export class MaintenanceModeService implements OnDestroy { readonly isActive = this._isActive.asReadonly(); + private get isForcedByConfig(): boolean { + return this.environment.maintenance === true; + } + /** * Check for maintenance mode upon application startup. * If the application is in maintenance mode, activate the service and start polling for when maintenance mode ends. */ checkOnce(): void { - this.checkMaintenanceStatus().subscribe((isMaintenance) => { - if (isMaintenance) { + if (this.isForcedByConfig) { + this._isActive.set(true); + return; + } + + this.checkMaintenanceStatus().subscribe((status) => { + if (status === MaintenanceStatus.Active) { this.activate(); } }); @@ -37,13 +47,17 @@ export class MaintenanceModeService implements OnDestroy { activate(): void { this._isActive.set(true); - if (this.pollingSubscription) { + if (this.isForcedByConfig || this.pollingSubscription) { return; } this.startPolling(); } deactivate(): void { + if (this.isForcedByConfig) { + return; + } + this._isActive.set(false); this.stopPolling(); } @@ -55,8 +69,8 @@ export class MaintenanceModeService implements OnDestroy { private startPolling(): void { this.pollingSubscription = timer(0, this.POLL_INTERVAL_MS) .pipe(switchMap(() => this.checkMaintenanceStatus())) - .subscribe((isMaintenance) => { - if (!isMaintenance) { + .subscribe((status) => { + if (status === MaintenanceStatus.Inactive) { this.deactivate(); } }); @@ -67,12 +81,21 @@ export class MaintenanceModeService implements OnDestroy { this.pollingSubscription = null; } - private checkMaintenanceStatus(): Observable { + private checkMaintenanceStatus(): Observable { return this.http .get(`${this.environment.apiDomainUrl}/v2/`, { context: this.bypassContext }) .pipe( - map((response) => response.meta?.maintenance_mode === true), - catchError(() => of(true)) + map((response) => + response.meta?.maintenance_mode === true ? MaintenanceStatus.Active : MaintenanceStatus.Inactive + ), + catchError((error: HttpErrorResponse) => of(this.statusFromError(error))) ); } + + private statusFromError(error: HttpErrorResponse): MaintenanceStatus { + const response = error.error as MaintenanceResponse | null; + return error.status === 503 && response?.meta?.maintenance_mode === true + ? MaintenanceStatus.Active + : MaintenanceStatus.Unknown; + } } diff --git a/src/app/features/auth/models/resend-confirmation.model.ts b/src/app/features/auth/models/resend-confirmation.model.ts new file mode 100644 index 000000000..9dfc48661 --- /dev/null +++ b/src/app/features/auth/models/resend-confirmation.model.ts @@ -0,0 +1,3 @@ +import { FormControl, FormGroup } from '@angular/forms'; + +export type ResendConfirmationFormGroupType = FormGroup<{ email: FormControl }>; diff --git a/src/app/features/auth/pages/resend/resend-confirmation.component.html b/src/app/features/auth/pages/resend/resend-confirmation.component.html new file mode 100644 index 000000000..f283449cf --- /dev/null +++ b/src/app/features/auth/pages/resend/resend-confirmation.component.html @@ -0,0 +1,31 @@ +
+
+

{{ 'auth.resendConfirmation.title' | translate }}

+

{{ 'auth.resendConfirmation.description' | translate }}

+ +
+ + + +
+
+ +
+ @if (message(); as msg) { + + {{ msg.content | translate }} + + } +
+
diff --git a/src/app/features/auth/pages/resend/resend-confirmation.component.scss b/src/app/features/auth/pages/resend/resend-confirmation.component.scss new file mode 100644 index 000000000..45db00704 --- /dev/null +++ b/src/app/features/auth/pages/resend/resend-confirmation.component.scss @@ -0,0 +1,17 @@ +@use "styles/mixins" as mix; + +:host { + @include mix.flex-center; + flex: 1; + background: var(--gradient-3); +} + +.resend-confirmation-container { + @include mix.flex-column; + background: var(--white); + border-radius: mix.rem(12px); + box-shadow: 0 2px 4px var(--grey-outline); + max-width: mix.rem(448px); + gap: mix.rem(24px); + flex: 1; +} diff --git a/src/app/features/auth/pages/resend/resend-confirmation.component.spec.ts b/src/app/features/auth/pages/resend/resend-confirmation.component.spec.ts new file mode 100644 index 000000000..f5877b0de --- /dev/null +++ b/src/app/features/auth/pages/resend/resend-confirmation.component.spec.ts @@ -0,0 +1,75 @@ +import { MockComponent, MockProvider } from 'ng-mocks'; + +import { ComponentFixture, TestBed } from '@angular/core/testing'; + +import { AuthService } from '@core/services/auth.service'; +import { TextInputComponent } from '@osf/shared/components/text-input/text-input.component'; + +import { provideOSFCore } from '@testing/osf.testing.provider'; +import { AuthServiceMock, AuthServiceMockType } from '@testing/providers/auth-service.mock'; + +import { ResendConfirmationComponent } from './resend-confirmation.component'; + +describe('ResendConfirmationComponent', () => { + let component: ResendConfirmationComponent; + let fixture: ComponentFixture; + let authService: AuthServiceMockType; + + beforeEach(() => { + authService = AuthServiceMock.simple(); + + TestBed.configureTestingModule({ + imports: [ResendConfirmationComponent, MockComponent(TextInputComponent)], + providers: [provideOSFCore(), MockProvider(AuthService, authService)], + }); + + fixture = TestBed.createComponent(ResendConfirmationComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should create', () => { + expect(component).toBeTruthy(); + }); + + it('should not call resendConfirmationUrl when form is invalid', () => { + component.resendConfirmationForm.setValue({ email: '' }); + + component.onSubmit(); + + expect(authService.resendConfirmationUrl).not.toHaveBeenCalled(); + expect(component.message()).toBeNull(); + }); + + it('should not call resendConfirmationUrl when email is not a valid email address', () => { + component.resendConfirmationForm.setValue({ email: 'not-an-email' }); + + component.onSubmit(); + + expect(authService.resendConfirmationUrl).not.toHaveBeenCalled(); + expect(component.message()).toBeNull(); + }); + + it('should call resendConfirmationUrl, reset the form, and set the success message when form is valid', () => { + component.resendConfirmationForm.setValue({ email: 'user@example.com' }); + + component.onSubmit(); + + expect(authService.resendConfirmationUrl).toHaveBeenCalledWith('user@example.com'); + expect(component.resendConfirmationForm.getRawValue()).toEqual({ email: null }); + expect(component.message()).toEqual({ + severity: 'success', + content: 'auth.resendConfirmation.messages.success', + }); + }); + + it('should clear the message on onCloseMessage', () => { + component.resendConfirmationForm.setValue({ email: 'user@example.com' }); + component.onSubmit(); + expect(component.message()).not.toBeNull(); + + component.onCloseMessage(); + + expect(component.message()).toBeNull(); + }); +}); diff --git a/src/app/features/auth/pages/resend/resend-confirmation.component.ts b/src/app/features/auth/pages/resend/resend-confirmation.component.ts new file mode 100644 index 000000000..f2713271e --- /dev/null +++ b/src/app/features/auth/pages/resend/resend-confirmation.component.ts @@ -0,0 +1,54 @@ +import { TranslatePipe } from '@ngx-translate/core'; + +import { Button } from 'primeng/button'; +import { Message } from 'primeng/message'; + +import { Component, inject, signal } from '@angular/core'; +import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; + +import { AuthService } from '@core/services/auth.service'; +import { MessageInfo } from '@osf/features/auth/models'; +import { ResendConfirmationFormGroupType } from '@osf/features/auth/models/resend-confirmation.model'; +import { TextInputComponent } from '@osf/shared/components/text-input/text-input.component'; +import { InputLimits } from '@osf/shared/constants/input-limits.const'; +import { CustomValidators } from '@osf/shared/helpers/custom-form-validators.helper'; + +@Component({ + selector: 'osf-resend-confirmation', + imports: [ReactiveFormsModule, Button, Message, TextInputComponent, TranslatePipe], + templateUrl: './resend-confirmation.component.html', + styleUrl: './resend-confirmation.component.scss', +}) +export class ResendConfirmationComponent { + private readonly fb = inject(FormBuilder); + private readonly authService = inject(AuthService); + + readonly emailLimit = InputLimits.email.maxLength; + + resendConfirmationForm: ResendConfirmationFormGroupType = this.fb.group({ + email: ['', [CustomValidators.requiredTrimmed(), Validators.email]], + }); + + message = signal(null); + + onSubmit(): void { + if (this.resendConfirmationForm.invalid) { + return; + } + + const emailForm = this.resendConfirmationForm.getRawValue(); + + this.authService.resendConfirmationUrl(emailForm.email).subscribe(() => { + this.resendConfirmationForm.reset(); + + this.message.set({ + severity: 'success', + content: 'auth.resendConfirmation.messages.success', + }); + }); + } + + onCloseMessage(): void { + this.message.set(null); + } +} diff --git a/src/app/features/auth/pages/reset-password/reset-password.component.spec.ts b/src/app/features/auth/pages/reset-password/reset-password.component.spec.ts index 18d523a3c..ce9fd240f 100644 --- a/src/app/features/auth/pages/reset-password/reset-password.component.spec.ts +++ b/src/app/features/auth/pages/reset-password/reset-password.component.spec.ts @@ -5,8 +5,11 @@ import { ActivatedRoute } from '@angular/router'; import { AuthService } from '@core/services/auth.service'; import { PasswordInputHintComponent } from '@osf/shared/components/password-input-hint/password-input-hint.component'; +import { LoaderService } from '@osf/shared/services/loader.service'; import { provideOSFCore } from '@testing/osf.testing.provider'; +import { AuthServiceMock, AuthServiceMockType } from '@testing/providers/auth-service.mock'; +import { LoaderServiceMock } from '@testing/providers/loader-service.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { ResetPasswordComponent } from './reset-password.component'; @@ -14,13 +17,24 @@ import { ResetPasswordComponent } from './reset-password.component'; describe('ResetPasswordComponent', () => { let component: ResetPasswordComponent; let fixture: ComponentFixture; + let authService: AuthServiceMockType; + let loaderService: LoaderServiceMock; + + const validPassword = 'Password1!'; beforeEach(() => { - const mockRoute = ActivatedRouteMockBuilder.create().withQueryParams({}) as Partial; + authService = AuthServiceMock.simple(); + loaderService = new LoaderServiceMock(); + const mockRoute = ActivatedRouteMockBuilder.create().withParams({ userId: 'user-1', token: 'token-1' }).build(); TestBed.configureTestingModule({ imports: [ResetPasswordComponent, MockComponent(PasswordInputHintComponent)], - providers: [provideOSFCore(), MockProvider(AuthService), MockProvider(ActivatedRoute, mockRoute)], + providers: [ + provideOSFCore(), + MockProvider(AuthService, authService), + MockProvider(LoaderService, loaderService), + MockProvider(ActivatedRoute, mockRoute), + ], }); fixture = TestBed.createComponent(ResetPasswordComponent); @@ -31,4 +45,47 @@ describe('ResetPasswordComponent', () => { it('should create', () => { expect(component).toBeTruthy(); }); + + it('should not reset password when form is invalid', () => { + component.onSubmit(); + + expect(authService.resetPassword).not.toHaveBeenCalled(); + expect(loaderService.show).not.toHaveBeenCalled(); + }); + + it('should show mismatch error when passwords differ and both fields are dirty', () => { + component.resetPasswordForm.setValue({ + newPassword: validPassword, + confirmNewPassword: 'Different1!', + }); + component.resetPasswordForm.get('newPassword')?.markAsDirty(); + component.resetPasswordForm.get('confirmNewPassword')?.markAsDirty(); + fixture.detectChanges(); + + expect(component.isMismatchError).toBe(true); + expect(fixture.nativeElement.querySelector('p-message')).toBeTruthy(); + }); + + it('should reset password, toggle loader, and show success', () => { + component.resetPasswordForm.setValue({ + newPassword: validPassword, + confirmNewPassword: validPassword, + }); + + component.onSubmit(); + fixture.detectChanges(); + + expect(loaderService.show).toHaveBeenCalled(); + expect(authService.resetPassword).toHaveBeenCalledWith('user-1', 'token-1', validPassword); + expect(loaderService.hide).toHaveBeenCalled(); + expect(component.isFormSubmitted()).toBe(true); + expect(fixture.nativeElement.querySelector('.reset-password-container')).toBeNull(); + expect(fixture.nativeElement.querySelector('.message-container')).toBeTruthy(); + }); + + it('should navigate to sign in', () => { + component.backToSignIn(); + + expect(authService.navigateToSignIn).toHaveBeenCalled(); + }); }); diff --git a/src/app/features/auth/pages/reset-password/reset-password.component.ts b/src/app/features/auth/pages/reset-password/reset-password.component.ts index 558b7574b..8ec5fe58a 100644 --- a/src/app/features/auth/pages/reset-password/reset-password.component.ts +++ b/src/app/features/auth/pages/reset-password/reset-password.component.ts @@ -4,6 +4,8 @@ import { Button } from 'primeng/button'; import { Message } from 'primeng/message'; import { Password } from 'primeng/password'; +import { finalize } from 'rxjs'; + import { Component, inject, signal } from '@angular/core'; import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; @@ -12,6 +14,7 @@ import { AuthService } from '@core/services/auth.service'; import { PasswordInputHintComponent } from '@osf/shared/components/password-input-hint/password-input-hint.component'; import { CustomValidators } from '@osf/shared/helpers/custom-form-validators.helper'; import { PASSWORD_REGEX } from '@osf/shared/helpers/password.helper'; +import { LoaderService } from '@osf/shared/services/loader.service'; import { ResetPasswordFormGroupType } from '../../models'; @@ -25,6 +28,7 @@ export class ResetPasswordComponent { private readonly fb = inject(FormBuilder); private readonly route = inject(ActivatedRoute); private readonly authService = inject(AuthService); + private readonly loaderService = inject(LoaderService); isFormSubmitted = signal(false); passwordRegex = PASSWORD_REGEX; @@ -59,9 +63,11 @@ export class ResetPasswordComponent { const token = this.route.snapshot.params['token']; const newPassword = this.resetPasswordForm.getRawValue().newPassword; - this.authService.resetPassword(userId, token, newPassword).subscribe(() => { - this.isFormSubmitted.set(true); - }); + this.loaderService.show(); + this.authService + .resetPassword(userId, token, newPassword) + .pipe(finalize(() => this.loaderService.hide())) + .subscribe(() => this.isFormSubmitted.set(true)); } backToSignIn() { diff --git a/src/app/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component.html b/src/app/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component.html index ba6a67499..3287a2377 100644 --- a/src/app/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component.html +++ b/src/app/features/preprints/components/preprint-details/preprint-doi-section/preprint-doi-section.component.html @@ -15,12 +15,11 @@

{{ 'preprints.details.doi.title' | translate: { documentType: preprintProvid (onChange)="selectPreprintVersion($event.value)" /> @if (preprintValue?.preprintDoiLink) { - @if (preprintValue?.preprintDoiCreated) { - - {{ preprintValue?.preprintDoiLink }} - - } @else { -

{{ preprintValue?.preprintDoiLink }}

+ + {{ preprintValue?.preprintDoiLink }} + + + @if (!preprintValue?.preprintDoiCreated) {

{{ 'preprints.details.doi.pendingDoiMinted' | translate }}

} } @else { diff --git a/src/app/features/preprints/pages/preprints-landing/preprints-landing.component.spec.ts b/src/app/features/preprints/pages/preprints-landing/preprints-landing.component.spec.ts index 85341ecde..6056481ce 100644 --- a/src/app/features/preprints/pages/preprints-landing/preprints-landing.component.spec.ts +++ b/src/app/features/preprints/pages/preprints-landing/preprints-landing.component.spec.ts @@ -4,7 +4,7 @@ import { MockComponents, MockPipe, MockProvider } from 'ng-mocks'; import { TitleCasePipe } from '@angular/common'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; @@ -15,6 +15,7 @@ import { PREPRINT_PROVIDER_SHORT_INFO_MOCK } from '@testing/mocks/preprint-provi import { SUBJECTS_MOCK } from '@testing/mocks/subject.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; import { BrandServiceMock, BrandServiceMockType } from '@testing/providers/brand-service.mock'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -62,6 +63,7 @@ describe('PreprintsLandingComponent', () => { provideOSFCore(), MockProvider(BrandService, brandServiceMock), MockProvider(Router, routerMock), + MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build()), provideMockStore({ signals: [ { diff --git a/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.spec.ts b/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.spec.ts index 846a7b0a8..a8931935f 100644 --- a/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.spec.ts +++ b/src/app/features/project/overview/components/project-overview-metadata/project-overview-metadata.component.spec.ts @@ -5,7 +5,7 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { Mock } from 'vitest'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { UserSelectors } from '@osf/core/store/user'; import { @@ -34,6 +34,7 @@ import { FetchSelectedSubjects, SubjectsSelectors } from '@osf/shared/stores/sub import { MOCK_PROJECT_OVERVIEW } from '@testing/mocks/project-overview.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; import { mergeSignalOverrides, provideMockStore } from '@testing/providers/store-provider.mock'; @@ -116,6 +117,7 @@ describe('ProjectOverviewMetadataComponent', () => { provideOSFCore(), MockProvider(MetadataRecordsService, metadataRecordsService), MockProvider(Router, mockRouter), + MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build()), provideMockStore({ signals: signals, }), diff --git a/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.html b/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.html index 59c97dc04..3e4446cc3 100644 --- a/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.html +++ b/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.html @@ -28,7 +28,7 @@

} @@ -36,7 +36,7 @@

icon="fas fa-trash" severity="danger" text - (click)="handleDisconnectAccount()" + (onClick)="handleDisconnectAccount()" (keydown.enter)="handleDisconnectAccount()" > @@ -63,7 +63,7 @@

diff --git a/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.ts b/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.ts index d4061482a..a11418d30 100644 --- a/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.ts +++ b/src/app/features/project/project-addons/components/configure-addon/configure-addon.component.ts @@ -2,7 +2,6 @@ import { createDispatchMap, select, Store } from '@ngxs/store'; import { TranslatePipe } from '@ngx-translate/core'; -import { BreadcrumbModule } from 'primeng/breadcrumb'; import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; import { Skeleton } from 'primeng/skeleton'; @@ -19,7 +18,7 @@ import { PLATFORM_ID, signal, } from '@angular/core'; -import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FormControl } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ENVIRONMENT } from '@core/provider/environment.provider'; @@ -46,18 +45,7 @@ import { AddonDialogService } from '../../services/addon-dialog.service'; @Component({ selector: 'osf-configure-addon', - imports: [ - SubHeaderComponent, - TranslatePipe, - Button, - RouterLink, - Card, - ReactiveFormsModule, - FormsModule, - Skeleton, - BreadcrumbModule, - StorageItemSelectorComponent, - ], + imports: [SubHeaderComponent, TranslatePipe, Button, RouterLink, Card, Skeleton, StorageItemSelectorComponent], templateUrl: './configure-addon.component.html', styleUrl: './configure-addon.component.scss', providers: [AddonDialogService], diff --git a/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.html b/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.html index b24357152..3cc02b29e 100644 --- a/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.html +++ b/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.html @@ -5,7 +5,7 @@ class="btn-full-width" [label]="'common.buttons.cancel' | translate" severity="info" - (click)="dialogRef.close()" + (onClick)="dialogRef.close()" [disabled]="isSubmitting()" data-test-addon-cancel-button /> diff --git a/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.scss b/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.spec.ts b/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.spec.ts index 00acc6dca..14d29483a 100644 --- a/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.spec.ts +++ b/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.spec.ts @@ -1,25 +1,187 @@ +import { Store } from '@ngxs/store'; + +import { MockProvider } from 'ng-mocks'; + +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { Mock, Mocked } from 'vitest'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { OperationNames } from '@osf/shared/enums/operation-names.enum'; +import { OperationInvocationRequestJsonApi } from '@osf/shared/models/addons/addon-operations-json-api.model'; +import { AuthorizedAccountModel } from '@osf/shared/models/addons/authorized-account.model'; +import { AddonOperationInvocationService } from '@osf/shared/services/addons/addon-operation-invocation.service'; +import { AddonsSelectors, CreateAddonOperationInvocation } from '@osf/shared/stores/addons'; + +import { MOCK_ADDON } from '@testing/mocks/addon.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; +import { AddonOperationInvocationServiceMockFactory } from '@testing/providers/addon-operation-invocation.service.mock'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; import { ConfirmAccountConnectionModalComponent } from './confirm-account-connection-modal.component'; -describe.skip('ConfirmAccountConnectionModalComponent', () => { +describe('ConfirmAccountConnectionModalComponent', () => { let component: ConfirmAccountConnectionModalComponent; let fixture: ComponentFixture; + let store: Store; + let dialogRef: DynamicDialogRef; + let operationInvocationService: Mocked; + + const selectedAccount: AuthorizedAccountModel = { + ...MOCK_ADDON, + id: 'account-1', + displayName: 'Google Drive', + type: 'authorized-storage-accounts', + authUrl: null, + authorizedCapabilities: ['ACCESS'], + authorizedOperationNames: [OperationNames.LIST_ROOT_ITEMS], + credentialsAvailable: true, + apiBaseUrl: 'https://www.googleapis.com', + defaultRootFolder: '', + oauthToken: 'token', + accountOwnerId: 'owner-1', + externalStorageServiceId: 'service-1', + }; + + const invocationPayload = { + data: { + type: 'addon-operation-invocations', + attributes: { + invocation_status: null, + operation_name: OperationNames.LIST_ROOT_ITEMS, + operation_kwargs: {}, + operation_result: {}, + created: null, + modified: null, + }, + relationships: {}, + }, + } as OperationInvocationRequestJsonApi; + + const defaultSignals: SignalOverride[] = [ + { selector: AddonsSelectors.getOperationInvocationSubmitting, value: false }, + ]; + + interface SetupOverrides extends BaseSetupOverrides { + message?: string; + omitMessage?: boolean; + selectedAccount?: AuthorizedAccountModel | null; + isGoogleDrive?: boolean; + } + + function setup(overrides: SetupOverrides = {}) { + const account = overrides.selectedAccount === undefined ? selectedAccount : overrides.selectedAccount; + const data = { + ...(overrides.omitMessage ? {} : { message: overrides.message ?? 'Connect this account?' }), + selectedAccount: account, + isGoogleDrive: overrides.isGoogleDrive ?? false, + }; + const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides); + operationInvocationService = AddonOperationInvocationServiceMockFactory(); + operationInvocationService.createInitialOperationInvocationPayload.mockReturnValue(invocationPayload); - beforeEach(() => { TestBed.configureTestingModule({ imports: [ConfirmAccountConnectionModalComponent], - providers: [provideOSFCore()], + providers: [ + provideOSFCore(), + provideDynamicDialogRefMock(), + MockProvider(DynamicDialogConfig, { data }), + MockProvider(AddonOperationInvocationService, operationInvocationService), + provideMockStore({ signals }), + ], }); + store = TestBed.inject(Store); + dialogRef = TestBed.inject(DynamicDialogRef); fixture = TestBed.createComponent(ConfirmAccountConnectionModalComponent); component = fixture.componentInstance; fixture.detectChanges(); + } + + it('should read the message from dialog config', () => { + setup(); + + expect(component.dialogMessage).toBe('Connect this account?'); + }); + + it('should use an empty message when dialog data omits it', () => { + setup({ omitMessage: true }); + + expect(component.dialogMessage).toBe(''); + }); + + it('should expose the submitting state from the store', () => { + setup({ + selectorOverrides: [{ selector: AddonsSelectors.getOperationInvocationSubmitting, value: true }], + }); + + expect(component.isSubmitting()).toBe(true); + }); + + it('should render the confirmation message', () => { + setup(); + + expect(fixture.nativeElement.textContent).toContain('Connect this account?'); + }); + + it('should disable cancel while the connection is submitting', () => { + setup({ + selectorOverrides: [{ selector: AddonsSelectors.getOperationInvocationSubmitting, value: true }], + }); + + const buttons = fixture.nativeElement.querySelectorAll('button'); + expect(buttons[0].disabled).toBe(true); + }); + + it('should not connect when the selected account is missing', () => { + setup({ selectedAccount: null }); + (store.dispatch as Mock).mockClear(); + + component.handleConnectAddonAccount(); + + expect(operationInvocationService.createInitialOperationInvocationPayload).not.toHaveBeenCalled(); + expect(store.dispatch).not.toHaveBeenCalled(); + expect(dialogRef.close).not.toHaveBeenCalled(); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should close with success for Google Drive without creating an invocation', () => { + setup({ isGoogleDrive: true }); + (store.dispatch as Mock).mockClear(); + + component.handleConnectAddonAccount(); + + expect(dialogRef.close).toHaveBeenCalledWith({ success: true }); + expect(operationInvocationService.createInitialOperationInvocationPayload).not.toHaveBeenCalled(); + expect(store.dispatch).not.toHaveBeenCalled(); + }); + + it('should create a root-items invocation and close with success', () => { + setup(); + (store.dispatch as Mock).mockClear(); + + component.handleConnectAddonAccount(); + + expect(operationInvocationService.createInitialOperationInvocationPayload).toHaveBeenCalledWith( + OperationNames.LIST_ROOT_ITEMS, + selectedAccount + ); + expect(store.dispatch).toHaveBeenCalledWith(new CreateAddonOperationInvocation(invocationPayload)); + expect(dialogRef.close).toHaveBeenCalledWith({ success: true }); + }); + + it('should close without a result when cancel is clicked', () => { + setup(); + + const buttons = fixture.nativeElement.querySelectorAll('button'); + buttons[0].click(); + + expect(dialogRef.close).toHaveBeenCalledWith(); }); }); diff --git a/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.ts b/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.ts index ceb0de46f..f44314113 100644 --- a/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.ts +++ b/src/app/features/project/project-addons/components/confirm-account-connection-modal/confirm-account-connection-modal.component.ts @@ -6,7 +6,6 @@ import { Button } from 'primeng/button'; import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; import { ChangeDetectionStrategy, Component, inject } from '@angular/core'; -import { ReactiveFormsModule } from '@angular/forms'; import { OperationNames } from '@osf/shared/enums/operation-names.enum'; import { AddonOperationInvocationService } from '@osf/shared/services/addons/addon-operation-invocation.service'; @@ -14,21 +13,19 @@ import { AddonsSelectors, CreateAddonOperationInvocation } from '@osf/shared/sto @Component({ selector: 'osf-confirm-account-connection-modal', - imports: [Button, ReactiveFormsModule, TranslatePipe], + imports: [Button, TranslatePipe], templateUrl: './confirm-account-connection-modal.component.html', - styleUrl: './confirm-account-connection-modal.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class ConfirmAccountConnectionModalComponent { - private dialogConfig = inject(DynamicDialogConfig); - private operationInvocationService = inject(AddonOperationInvocationService); - dialogRef = inject(DynamicDialogRef); + private readonly dialogConfig = inject(DynamicDialogConfig); + private readonly operationInvocationService = inject(AddonOperationInvocationService); + readonly dialogRef = inject(DynamicDialogRef); + dialogMessage = this.dialogConfig.data.message || ''; - isSubmitting = select(AddonsSelectors.getOperationInvocationSubmitting); + readonly isSubmitting = select(AddonsSelectors.getOperationInvocationSubmitting); - actions = createDispatchMap({ - createAddonOperationInvocation: CreateAddonOperationInvocation, - }); + private readonly actions = createDispatchMap({ createAddonOperationInvocation: CreateAddonOperationInvocation }); handleConnectAddonAccount(): void { const selectedAccount = this.dialogConfig.data.selectedAccount; diff --git a/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.html b/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.html index ee683bd95..9ce5f1cb3 100644 --- a/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.html +++ b/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.html @@ -70,7 +70,7 @@

{{ loginOrChooseAccountText() }}

[label]="'common.buttons.back' | translate" severity="info" class="w-7rem btn-full-width" - (click)="activateCallback(AddonStepperValue.TERMS)" + (onClick)="activateCallback(AddonStepperValue.TERMS)" data-test-addon-back-button > @@ -78,14 +78,14 @@

{{ loginOrChooseAccountText() }}

@@ -134,25 +134,27 @@

{{ 'settings.addons.connectAddon.chooseExistingAccount' | trans

{{ 'settings.addons.connectAddon.configure' | translate }} {{ addon()?.displayName }}

- + @if (chosenAccountId()) { + + }
diff --git a/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.spec.ts b/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.spec.ts index 0cef8ccfe..c8904dafd 100644 --- a/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.spec.ts +++ b/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.spec.ts @@ -46,6 +46,5 @@ describe.skip('ConnectAddonComponent', () => { it('should create and initialize with addon data from router state', () => { expect(component).toBeTruthy(); expect(component['addon']()).toEqual(mockAddon); - expect(component['terms']().length).toBeGreaterThan(0); }); }); diff --git a/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.ts b/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.ts index b7907d439..3e9ba666f 100644 --- a/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.ts +++ b/src/app/features/project/project-addons/components/connect-configured-addon/connect-configured-addon.component.ts @@ -3,15 +3,12 @@ import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { Button } from 'primeng/button'; -import { DialogModule } from 'primeng/dialog'; -import { DynamicDialogModule } from 'primeng/dynamicdialog'; import { RadioButtonModule } from 'primeng/radiobutton'; import { StepPanel, StepPanels, Stepper } from 'primeng/stepper'; -import { TableModule } from 'primeng/table'; import { isPlatformBrowser } from '@angular/common'; import { Component, computed, DestroyRef, inject, PLATFORM_ID, signal, viewChild } from '@angular/core'; -import { FormControl, FormsModule, ReactiveFormsModule } from '@angular/forms'; +import { FormControl, FormsModule } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ENVIRONMENT } from '@core/provider/environment.provider'; @@ -25,7 +22,6 @@ import { OperationNames } from '@osf/shared/enums/operation-names.enum'; import { ProjectAddonsStepperValue } from '@osf/shared/enums/profile-addons-stepper.enum'; import { getAddonTypeString } from '@osf/shared/helpers/addon-type.helper'; import { AddonModel } from '@osf/shared/models/addons/addon.model'; -import { AddonTerm } from '@osf/shared/models/addons/addon-utils.model'; import { AuthorizedAccountModel } from '@osf/shared/models/addons/authorized-account.model'; import { AuthorizedAddonRequestJsonApi } from '@osf/shared/models/addons/authorized-addon-json-api.model'; import { AddonFormService } from '@osf/shared/services/addons/addon-form.service'; @@ -40,11 +36,9 @@ import { GetAuthorizedCitationAddons, GetAuthorizedLinkAddons, GetAuthorizedStorageAddons, - UpdateAuthorizedAddon, - UpdateConfiguredAddon, } from '@osf/shared/stores/addons'; -import { AddonConfigMap } from '../../models'; +import { AddonConfigMap } from '../../models/addon-config-actions.model'; import { AddonDialogService } from '../../services'; @Component({ @@ -55,17 +49,13 @@ import { AddonDialogService } from '../../services'; StepPanels, Stepper, Button, - TableModule, RouterLink, FormsModule, - ReactiveFormsModule, TranslatePipe, RadioButtonModule, StorageItemSelectorComponent, AddonTermsComponent, AddonSetupAccountFormComponent, - DialogModule, - DynamicDialogModule, ], templateUrl: './connect-configured-addon.component.html', providers: [AddonDialogService], @@ -93,7 +83,6 @@ export class ConnectConfiguredAddonComponent { readonly stepper = viewChild(Stepper); accountNameControl = new FormControl(''); - terms = signal([]); addon = signal(null); addonAuthUrl = signal('/settings/addons'); currentAuthorizedAddonAccounts = signal([]); @@ -129,8 +118,6 @@ export class ConnectConfiguredAddonComponent { getAuthorizedLinkAddons: GetAuthorizedLinkAddons, createAuthorizedAddon: CreateAuthorizedAddon, createConfiguredAddon: CreateConfiguredAddon, - updateConfiguredAddon: UpdateConfiguredAddon, - updateAuthorizedAddon: UpdateAuthorizedAddon, createAddonOperationInvocation: CreateAddonOperationInvocation, }); diff --git a/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.html b/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.html index 7fdcbf053..b77742e4e 100644 --- a/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.html +++ b/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.html @@ -12,7 +12,7 @@ class="btn-full-width" [label]="'common.buttons.cancel' | translate" severity="info" - (click)="dialogRef.close()" + (onClick)="dialogRef.close()" [disabled]="isSubmitting()" data-test-addon-cancel-button /> diff --git a/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.scss b/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.scss deleted file mode 100644 index e69de29bb..000000000 diff --git a/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.spec.ts b/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.spec.ts index 9540ea0b4..1f6c9c712 100644 --- a/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.spec.ts +++ b/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.spec.ts @@ -1,25 +1,164 @@ +import { Store } from '@ngxs/store'; + +import { MockProvider } from 'ng-mocks'; + +import { DynamicDialogConfig, DynamicDialogRef } from 'primeng/dynamicdialog'; + +import { Mock } from 'vitest'; + import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ConfiguredAddonType } from '@osf/shared/enums/addon-type.enum'; +import { ConfiguredAddonModel } from '@osf/shared/models/addons/configured-addon.model'; +import { AddonsSelectors, DeleteConfiguredAddon } from '@osf/shared/stores/addons'; + +import { MOCK_CONFIGURED_ADDON } from '@testing/mocks/configured-addon.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; +import { provideDynamicDialogRefMock } from '@testing/providers/dynamic-dialog-ref.mock'; +import { + BaseSetupOverrides, + mergeSignalOverrides, + provideMockStore, + SignalOverride, +} from '@testing/providers/store-provider.mock'; import { DisconnectAddonModalComponent } from './disconnect-addon-modal.component'; -describe.skip('DisconnectAddonModalComponent', () => { +describe('DisconnectAddonModalComponent', () => { let component: DisconnectAddonModalComponent; let fixture: ComponentFixture; + let store: Store; + let dialogRef: DynamicDialogRef; + + const storageAddon: ConfiguredAddonModel = { + ...MOCK_CONFIGURED_ADDON, + type: ConfiguredAddonType.STORAGE, + }; + + const linkAddon: ConfiguredAddonModel = { + ...MOCK_CONFIGURED_ADDON, + type: ConfiguredAddonType.LINK, + }; + + const defaultSignals: SignalOverride[] = [ + { selector: AddonsSelectors.getDeleteStorageAddonSubmitting, value: false }, + { selector: AddonsSelectors.getSelectedStorageItem, value: { itemName: 'Research Folder' } }, + ]; + + interface SetupOverrides extends BaseSetupOverrides { + addon?: ConfiguredAddonModel | null; + message?: string; + omitMessage?: boolean; + detectChanges?: boolean; + } + + function setup(overrides: SetupOverrides = {}) { + const addon = overrides.addon === undefined ? storageAddon : overrides.addon; + const data = overrides.omitMessage ? { addon } : { addon, message: overrides.message ?? 'Disconnect this addon?' }; + const signals = mergeSignalOverrides(defaultSignals, overrides.selectorOverrides); - beforeEach(() => { TestBed.configureTestingModule({ imports: [DisconnectAddonModalComponent], - providers: [provideOSFCore()], + providers: [ + provideOSFCore(), + provideDynamicDialogRefMock(), + MockProvider(DynamicDialogConfig, { data }), + provideMockStore({ signals }), + ], }); + store = TestBed.inject(Store); + dialogRef = TestBed.inject(DynamicDialogRef); fixture = TestBed.createComponent(DisconnectAddonModalComponent); component = fixture.componentInstance; - fixture.detectChanges(); + + if (overrides.detectChanges !== false) { + fixture.detectChanges(); + } + } + + it('should read addon and message from dialog config', () => { + setup(); + + expect(component.addon).toEqual(storageAddon); + expect(component.dialogMessage).toBe('Disconnect this addon?'); + }); + + it('should use an empty message when dialog data omits it', () => { + setup({ omitMessage: true }); + + expect(component.dialogMessage).toBe(''); + }); + + it('should label the selected item as a folder for storage addons', () => { + setup(); + + expect(component.selectedItemLabel()).toBe('settings.addons.configureAddon.selectedFolder'); + }); + + it('should label the selected item as a linked item for link addons', () => { + setup({ addon: linkAddon }); + + expect(component.selectedItemLabel()).toBe('settings.addons.configureAddon.linkedItem'); }); - it('should create', () => { - expect(component).toBeTruthy(); + it('should expose submitting state and the selected folder from the store', () => { + setup({ + selectorOverrides: [ + { selector: AddonsSelectors.getDeleteStorageAddonSubmitting, value: true }, + { selector: AddonsSelectors.getSelectedStorageItem, value: { itemName: 'Shared Drive' } }, + ], + }); + + expect(component.isSubmitting()).toBe(true); + expect(component.selectedFolder()?.itemName).toBe('Shared Drive'); + }); + + it('should render the message, account name, and selected folder', () => { + setup(); + + const text = fixture.nativeElement.textContent; + expect(text).toContain('Disconnect this addon?'); + expect(text).toContain(storageAddon.displayName); + expect(text).toContain('Research Folder'); + }); + + it('should disable actions while disconnect is submitting', () => { + setup({ + selectorOverrides: [{ selector: AddonsSelectors.getDeleteStorageAddonSubmitting, value: true }], + }); + + const buttons = fixture.nativeElement.querySelectorAll('button'); + expect(buttons[0].disabled).toBe(true); + expect(buttons[1].disabled).toBe(true); + }); + + it('should delete the configured addon and close with success', () => { + setup(); + (store.dispatch as Mock).mockClear(); + + component.handleDisconnectAddonAccount(); + + expect(store.dispatch).toHaveBeenCalledWith(new DeleteConfiguredAddon(storageAddon.id, storageAddon.type)); + expect(dialogRef.close).toHaveBeenCalledWith({ success: true }); + }); + + it('should not delete when the addon is missing', () => { + setup({ addon: null, detectChanges: false }); + (store.dispatch as Mock).mockClear(); + + component.handleDisconnectAddonAccount(); + + expect(store.dispatch).not.toHaveBeenCalled(); + expect(dialogRef.close).not.toHaveBeenCalled(); + }); + + it('should close without a result when cancel is clicked', () => { + setup(); + + const buttons = fixture.nativeElement.querySelectorAll('button'); + buttons[0].click(); + + expect(dialogRef.close).toHaveBeenCalledWith(); }); }); diff --git a/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.ts b/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.ts index 9395b4b6d..1cba5e35d 100644 --- a/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.ts +++ b/src/app/features/project/project-addons/components/disconnect-addon-modal/disconnect-addon-modal.component.ts @@ -15,25 +15,27 @@ import { AddonsSelectors, DeleteConfiguredAddon } from '@osf/shared/stores/addon selector: 'osf-disconnect-addon-modal', imports: [Button, TranslatePipe], templateUrl: './disconnect-addon-modal.component.html', - styleUrl: './disconnect-addon-modal.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, }) export class DisconnectAddonModalComponent { - private dialogConfig = inject(DynamicDialogConfig); - dialogRef = inject(DynamicDialogRef); + private readonly dialogConfig = inject(DynamicDialogConfig); + readonly dialogRef = inject(DynamicDialogRef); + addon = this.dialogConfig.data.addon; dialogMessage = this.dialogConfig.data.message || ''; - isSubmitting = select(AddonsSelectors.getDeleteStorageAddonSubmitting); - selectedFolder = select(AddonsSelectors.getSelectedStorageItem); - selectedItemLabel = computed(() => { + + private readonly actions = createDispatchMap({ deleteConfiguredAddon: DeleteConfiguredAddon }); + + readonly isSubmitting = select(AddonsSelectors.getDeleteStorageAddonSubmitting); + readonly selectedFolder = select(AddonsSelectors.getSelectedStorageItem); + + readonly selectedItemLabel = computed(() => { const addonType = getAddonTypeString(this.addon); return addonType === AddonType.LINK ? 'settings.addons.configureAddon.linkedItem' : 'settings.addons.configureAddon.selectedFolder'; }); - actions = createDispatchMap({ deleteConfiguredAddon: DeleteConfiguredAddon }); - handleDisconnectAddonAccount(): void { if (!this.addon) return; diff --git a/src/app/features/project/project-addons/components/index.ts b/src/app/features/project/project-addons/components/index.ts deleted file mode 100644 index e7ac9e46a..000000000 --- a/src/app/features/project/project-addons/components/index.ts +++ /dev/null @@ -1,4 +0,0 @@ -export { ConfigureAddonComponent } from './configure-addon/configure-addon.component'; -export { ConfirmAccountConnectionModalComponent } from './confirm-account-connection-modal/confirm-account-connection-modal.component'; -export { ConnectConfiguredAddonComponent } from './connect-configured-addon/connect-configured-addon.component'; -export { DisconnectAddonModalComponent } from './disconnect-addon-modal/disconnect-addon-modal.component'; diff --git a/src/app/features/project/project-addons/models/addon-config-actions.model.ts b/src/app/features/project/project-addons/models/addon-config-actions.model.ts index aa4b2645d..b6dc4c7be 100644 --- a/src/app/features/project/project-addons/models/addon-config-actions.model.ts +++ b/src/app/features/project/project-addons/models/addon-config-actions.model.ts @@ -2,7 +2,9 @@ import { Observable } from 'rxjs'; import { AuthorizedAccountModel } from '@osf/shared/models/addons/authorized-account.model'; -export interface AddonConfigActions { +export type AddonConfigMap = Record; + +interface AddonConfigActions { getAddons: () => Observable; getAuthorizedAddons: () => AuthorizedAccountModel[]; } diff --git a/src/app/features/project/project-addons/models/addon-config-map.type.ts b/src/app/features/project/project-addons/models/addon-config-map.type.ts deleted file mode 100644 index 1873fd2a9..000000000 --- a/src/app/features/project/project-addons/models/addon-config-map.type.ts +++ /dev/null @@ -1,3 +0,0 @@ -import { AddonConfigActions } from '.'; - -export type AddonConfigMap = Record; diff --git a/src/app/features/project/project-addons/models/index.ts b/src/app/features/project/project-addons/models/index.ts deleted file mode 100644 index 488a1c42c..000000000 --- a/src/app/features/project/project-addons/models/index.ts +++ /dev/null @@ -1,2 +0,0 @@ -export * from './addon-config-actions.model'; -export * from './addon-config-map.type'; diff --git a/src/app/features/project/project-addons/project-addons.component.ts b/src/app/features/project/project-addons/project-addons.component.ts index c26e7b0e6..a6dc4828f 100644 --- a/src/app/features/project/project-addons/project-addons.component.ts +++ b/src/app/features/project/project-addons/project-addons.component.ts @@ -21,7 +21,7 @@ import { untracked, } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; -import { FormControl, FormsModule } from '@angular/forms'; +import { FormControl } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { UserSelectors } from '@core/store/user'; @@ -42,7 +42,6 @@ import { AddonsQueryParamsService } from '@shared/services/addons-query-params.s import { AddonsSelectors, ClearConfiguredAddons, - DeleteAuthorizedAddon, GetAddonsResourceReference, GetAddonsUserReference, GetCitationAddons, @@ -67,7 +66,6 @@ import { CurrentResourceSelectors } from '@shared/stores/current-resource'; TabPanels, Tabs, TranslatePipe, - FormsModule, LoadingSpinnerComponent, SelectComponent, ], @@ -76,15 +74,16 @@ import { CurrentResourceSelectors } from '@shared/stores/current-resource'; changeDetection: ChangeDetectionStrategy.OnPush, }) export class ProjectAddonsComponent implements OnInit { - private route = inject(ActivatedRoute); - private destroyRef = inject(DestroyRef); - private queryParamsService = inject(AddonsQueryParamsService); - private platformId = inject(PLATFORM_ID); - private isBrowser = isPlatformBrowser(this.platformId); + private readonly route = inject(ActivatedRoute); + private readonly destroyRef = inject(DestroyRef); + private readonly queryParamsService = inject(AddonsQueryParamsService); + private readonly platformId = inject(PLATFORM_ID); + private readonly isBrowser = isPlatformBrowser(this.platformId); readonly tabOptions = ADDON_TAB_OPTIONS; readonly AddonTabValue = AddonTabValue; readonly defaultTabValue = AddonTabValue.ALL_ADDONS; + searchControl = new FormControl(''); searchValue = signal(''); selectedCategory = signal(AddonCategory.EXTERNAL_STORAGE_SERVICES); @@ -127,16 +126,6 @@ export class ProjectAddonsComponent implements OnInit { return ADDON_CATEGORY_OPTIONS; }); - isAddonsLoading = computed(() => { - return ( - this.isStorageAddonsLoading() || - this.isCitationAddonsLoading() || - this.isLinkAddonsLoading() || - this.isRedirectAddonsLoading() || - this.isUserReferenceLoading() || - this.isCurrentUserLoading() - ); - }); isConfiguredAddonsLoading = computed(() => { let categoryLoading; @@ -175,9 +164,7 @@ export class ProjectAddonsComponent implements OnInit { } }); - isAllAddonsTabLoading = computed(() => { - return this.currentAddonsLoading() || this.isConfiguredAddonsLoading(); - }); + isAllAddonsTabLoading = computed(() => this.currentAddonsLoading() || this.isConfiguredAddonsLoading()); actions = createDispatchMap({ getStorageAddons: GetStorageAddons, @@ -189,7 +176,6 @@ export class ProjectAddonsComponent implements OnInit { getConfiguredLinkAddons: GetConfiguredLinkAddons, getAddonsUserReference: GetAddonsUserReference, getAddonsResourceReference: GetAddonsResourceReference, - deleteAuthorizedAddon: DeleteAuthorizedAddon, clearConfiguredAddons: ClearConfiguredAddons, }); @@ -233,9 +219,7 @@ export class ProjectAddonsComponent implements OnInit { } }); - resourceReferenceId = computed(() => { - return this.addonsResourceReference()[0]?.id; - }); + resourceReferenceId = computed(() => this.addonsResourceReference()[0]?.id); currentAction = computed(() => { switch (this.selectedCategory()) { diff --git a/src/app/features/registries/components/new-registration/new-registration.component.html b/src/app/features/registries/components/new-registration/new-registration.component.html index b6bbed2da..25a9aab44 100644 --- a/src/app/features/registries/components/new-registration/new-registration.component.html +++ b/src/app/features/registries/components/new-registration/new-registration.component.html @@ -2,15 +2,45 @@
-
-

- {{ 'registries.new.registrySubmissionInfo' | translate }} - {{ 'common.links.clickHere' | translate }} - {{ 'registries.new.hostedRegistriesLink' | translate }} -

-
+
+ +
+

{{ 'registries.new.policy.intro' | translate }}

+

{{ 'registries.new.policy.preregistration' | translate }}

+ +
+

{{ 'registries.new.policy.acceptableTitle' | translate }}

+
    +
  • {{ 'registries.new.policy.acceptable.notBegun' | translate }}
  • +
  • {{ 'registries.new.policy.acceptable.underway' | translate }}
  • +
  • {{ 'registries.new.policy.acceptable.concluded' | translate }}
  • +
+
+ +
+

{{ 'registries.new.policy.unacceptableTitle' | translate }}

+
    +
  • {{ 'registries.new.policy.unacceptable.papers' | translate }}
  • +
  • {{ 'registries.new.policy.unacceptable.otherOutputs' | translate }}
  • +
+
+ +

{{ 'registries.new.policy.alternative' | translate }}

+
+ +
+ + +
+
-
@if (!isProjectReadOnly()) {

{{ 'registries.new.steps.title' | translate }} 1

@@ -80,9 +110,12 @@

{{ 'registries.new.steps.title' | translate }} {{ fromProject()
diff --git a/src/app/features/registries/components/new-registration/new-registration.component.spec.ts b/src/app/features/registries/components/new-registration/new-registration.component.spec.ts index eb0d3cc64..a6a5b5e3d 100644 --- a/src/app/features/registries/components/new-registration/new-registration.component.spec.ts +++ b/src/app/features/registries/components/new-registration/new-registration.component.spec.ts @@ -159,14 +159,16 @@ describe('NewRegistrationComponent', () => { expect(component.draftForm.get('project')?.validator).toBeNull(); }); - it('should dispatch createDraft and navigate when form is valid', () => { + it('should dispatch createDraft and navigate when form is valid and policy is acknowledged', () => { setup(); component.draftForm.patchValue({ providerSchema: 'schema-1', project: 'proj-1' }); component.fromProject.set(true); + component.policyAcknowledged.set(true); (store.dispatch as Mock).mockClear(); component.createDraft(); + expect(component.isCreateDraftDisabled()).toBe(false); expect(store.dispatch).toHaveBeenCalledWith( new CreateDraft({ registrationSchemaId: 'schema-1', provider: 'prov-1', projectId: 'proj-1' }) ); @@ -176,10 +178,25 @@ describe('NewRegistrationComponent', () => { it('should not dispatch createDraft when form is invalid', () => { setup(); component.draftForm.patchValue({ providerSchema: '' }); + component.policyAcknowledged.set(true); (store.dispatch as Mock).mockClear(); component.createDraft(); + expect(component.isCreateDraftDisabled()).toBe(true); + expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(CreateDraft)); + }); + + it('should not dispatch createDraft when policy is not acknowledged', () => { + setup(); + component.draftForm.patchValue({ providerSchema: 'schema-1', project: 'proj-1' }); + component.fromProject.set(true); + component.policyAcknowledged.set(false); + (store.dispatch as Mock).mockClear(); + + component.createDraft(); + + expect(component.isCreateDraftDisabled()).toBe(true); expect(store.dispatch).not.toHaveBeenCalledWith(expect.any(CreateDraft)); }); diff --git a/src/app/features/registries/components/new-registration/new-registration.component.ts b/src/app/features/registries/components/new-registration/new-registration.component.ts index 95cdc1ae6..6b1d8e66c 100644 --- a/src/app/features/registries/components/new-registration/new-registration.component.ts +++ b/src/app/features/registries/components/new-registration/new-registration.component.ts @@ -4,13 +4,15 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { Card } from 'primeng/card'; +import { Checkbox } from 'primeng/checkbox'; import { Select } from 'primeng/select'; +import { Tooltip } from 'primeng/tooltip'; -import { debounceTime, distinctUntilChanged, filter, Subject, take } from 'rxjs'; +import { debounceTime, distinctUntilChanged, filter, map, Subject, take } from 'rxjs'; import { ChangeDetectionStrategy, Component, computed, DestroyRef, effect, inject, signal } from '@angular/core'; -import { takeUntilDestroyed, toObservable } from '@angular/core/rxjs-interop'; -import { FormBuilder, ReactiveFormsModule, Validators } from '@angular/forms'; +import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop'; +import { FormBuilder, FormsModule, ReactiveFormsModule, Validators } from '@angular/forms'; import { ActivatedRoute, Router } from '@angular/router'; import { UserSelectors } from '@core/store/user'; @@ -23,7 +25,18 @@ import { CreateDraft, GetProjects, GetProviderSchemas, RegistriesSelectors } fro @Component({ selector: 'osf-new-registration', - imports: [Button, Card, Select, ReactiveFormsModule, LoadingSpinnerComponent, SubHeaderComponent, TranslatePipe], + imports: [ + Button, + Card, + Checkbox, + Select, + Tooltip, + FormsModule, + ReactiveFormsModule, + LoadingSpinnerComponent, + SubHeaderComponent, + TranslatePipe, + ], templateUrl: './new-registration.component.html', styleUrl: './new-registration.component.scss', changeDetection: ChangeDetectionStrategy.OnPush, @@ -58,11 +71,17 @@ export class NewRegistrationComponent { private readonly filter$ = new Subject(); readonly fromProject = signal(this.projectId !== undefined); + readonly policyAcknowledged = signal(false); readonly draftForm = this.fb.group({ providerSchema: ['', Validators.required], project: [this.projectId || ''], }); + readonly isFormValid = toSignal(this.draftForm.statusChanges.pipe(map(() => this.draftForm.valid)), { + initialValue: this.draftForm.valid, + }); + readonly isCreateDraftDisabled = computed(() => !this.isFormValid() || !this.policyAcknowledged()); + constructor() { this.loadInitialData(); this.setupDefaultSchema(); @@ -82,7 +101,7 @@ export class NewRegistrationComponent { } createDraft() { - if (this.draftForm.invalid) { + if (this.isCreateDraftDisabled()) { return; } diff --git a/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.scss b/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.scss index 4e458d78a..ce7c4536b 100644 --- a/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.scss +++ b/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.scss @@ -13,8 +13,3 @@ width: 100%; } } - -.folders-list { - border: 1px solid var(--grey-2); - border-radius: 0.5rem; -} diff --git a/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.spec.ts b/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.spec.ts index 1729311f2..39e8068bb 100644 --- a/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.spec.ts +++ b/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.spec.ts @@ -34,6 +34,5 @@ describe.skip('ConnectAddonComponent', () => { it('should create and initialize with addon data from router state', () => { expect(component).toBeTruthy(); expect(component['addon']()).toEqual(MOCK_ADDON); - expect(component['terms']().length).toBeGreaterThan(0); }); }); diff --git a/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.ts b/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.ts index 95d65c98a..97bd9b8d3 100644 --- a/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.ts +++ b/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.ts @@ -4,11 +4,9 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { StepPanel, StepPanels, Stepper } from 'primeng/stepper'; -import { TableModule } from 'primeng/table'; import { isPlatformBrowser } from '@angular/common'; import { Component, computed, DestroyRef, effect, inject, PLATFORM_ID, signal, viewChild } from '@angular/core'; -import { FormsModule, ReactiveFormsModule } from '@angular/forms'; import { Router, RouterLink } from '@angular/router'; import { AddonSetupAccountFormComponent } from '@osf/shared/components/addons/addon-setup-account-form/addon-setup-account-form.component'; @@ -18,7 +16,6 @@ import { AddonServiceNames } from '@osf/shared/enums/addon-service-names.enum'; import { AddonType } from '@osf/shared/enums/addon-type.enum'; import { ProjectAddonsStepperValue } from '@osf/shared/enums/profile-addons-stepper.enum'; import { getAddonTypeString, isAuthorizedAddon } from '@osf/shared/helpers/addon-type.helper'; -import { AddonTerm } from '@osf/shared/models/addons/addon-utils.model'; import { AuthorizedAddonRequestJsonApi } from '@osf/shared/models/addons/authorized-addon-json-api.model'; import { AddonOAuthService } from '@osf/shared/services/addons/addon-oauth.service'; import { ToastService } from '@osf/shared/services/toast.service'; @@ -34,10 +31,7 @@ import { AddonsSelectors, CreateAuthorizedAddon, UpdateAuthorizedAddon } from '@ StepPanels, Stepper, Button, - TableModule, RouterLink, - FormsModule, - ReactiveFormsModule, TranslatePipe, AddonTermsComponent, AddonSetupAccountFormComponent, @@ -57,7 +51,6 @@ export class ConnectAddonComponent { readonly AddonType = AddonType; readonly ProjectAddonsStepperValue = ProjectAddonsStepperValue; - terms = signal([]); addon = signal(null); addonAuthUrl = signal('/settings/addons'); diff --git a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.html b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.html index 0ed868bfd..40e23d2af 100644 --- a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.html +++ b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.html @@ -1,21 +1,23 @@
- - - {{ item.label | translate }} - - + @if (!isGoogleFilePicker()) { + + + {{ item.label | translate }} + + + }

@@ -112,7 +114,7 @@

{{ 'settings.addons.configureAddon.selectFolder' | translate }}

{{ 'settings.addons.configureAddon.noFolders' | translate }}

} - @if (showLoadMoreButton() && !isOperationInvocationSubmitting()) { + @if (showLoadMoreButton()) {
.table-cell:first-child { - min-width: 0; - max-width: 95%; - } } &-row:last-child { diff --git a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts index 26291fadc..2b5c683cd 100644 --- a/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts +++ b/src/app/shared/components/addons/storage-item-selector/storage-item-selector.component.ts @@ -91,8 +91,6 @@ export class StorageItemSelectorComponent implements OnInit { operationInvokeWithCursor = output(); save = output(); cancelSelection = output(); - readonly OperationNames = OperationNames; - readonly StorageItemType = StorageItemType; hasInputChanged = signal(false); hasFolderChanged = signal(false); hasResourceTypeChanged = signal(false); @@ -159,19 +157,8 @@ export class StorageItemSelectorComponent implements OnInit { } ngOnInit(): void { - this.initializeFormState(); - this.setupAccountNameTracking(); - } - - private initializeFormState(): void { this.initialResourceType.set(this.selectedResourceType()); - this.resetChangeFlags(); - } - - private resetChangeFlags(): void { - this.hasInputChanged.set(false); - this.hasFolderChanged.set(false); - this.hasResourceTypeChanged.set(false); + this.setupAccountNameTracking(); } private setupAccountNameTracking(): void { @@ -271,7 +258,7 @@ export class StorageItemSelectorComponent implements OnInit { id: itemId, label: itemName, state: { - operationName: mayContainRootCandidates ? OperationNames.LIST_CHILD_ITEMS : OperationNames.GET_ITEM_INFO, + operationName: OperationNames.LIST_CHILD_ITEMS, }, }; diff --git a/src/app/shared/components/google-file-picker/google-file-picker.component.html b/src/app/shared/components/google-file-picker/google-file-picker.component.html index a894d8383..7bafac660 100644 --- a/src/app/shared/components/google-file-picker/google-file-picker.component.html +++ b/src/app/shared/components/google-file-picker/google-file-picker.component.html @@ -1,4 +1,4 @@ -
+
@if (isFolderPicker()) {
{ let pickerBuilderMock: ReturnType['pickerBuilderMock']; let pickerSetVisibleMock: ReturnType['pickerSetVisibleMock']; + const registeredPickerCallback = (): ((data: GoogleFilePickerModel) => void) | undefined => { + const callback = pickerBuilderMock.setCallback.mock.calls.at(-1)?.[0]; + return typeof callback === 'function' ? callback : undefined; + }; + const rootFolder: StorageItem = { itemId: 'root-folder-id', itemName: 'Root Folder', }; - const setup = (options?: { accountId?: string; isFolderPicker?: boolean; googleFilePickerApiKey?: string }) => { + const setup = (options?: { + accountId?: string; + isFolderPicker?: boolean; + googleFilePickerApiKey?: string; + oauthToken?: string; + detectChanges?: boolean; + }) => { sentryMock = SentryMock.simple(); googlePickerDownloadServiceMock = GoogleFilePickerDownloadServiceMockBuilder.create().build(); ({ pickerBuilderMock, pickerSetVisibleMock } = setupGooglePickerMock()); + const authorizedStorageAddons = options?.oauthToken + ? [{ id: options.accountId ?? '', oauthToken: options.oauthToken }] + : []; + TestBed.configureTestingModule({ imports: [GoogleFilePickerComponent], providers: [ provideOSFCore(), - provideMockStore(), + provideMockStore({ + signals: [{ selector: AddonsSelectors.getAuthorizedStorageAddons, value: signal(authorizedStorageAddons) }], + }), { provide: SENTRY_TOKEN, useValue: sentryMock }, MockProvider(GoogleFilePickerDownloadService, googlePickerDownloadServiceMock), MockProvider(ENVIRONMENT, { @@ -66,7 +85,9 @@ describe('GoogleFilePickerComponent', () => { fixture.componentRef.setInput('rootFolder', rootFolder); fixture.componentRef.setInput('accountId', options?.accountId ?? ''); fixture.componentRef.setInput('currentAddonType', AddonType.STORAGE); - fixture.detectChanges(); + if (options?.detectChanges !== false) { + fixture.detectChanges(); + } }; it('should create', () => { @@ -78,8 +99,6 @@ describe('GoogleFilePickerComponent', () => { it('should disable picker when configuration is missing', () => { setup({ googleFilePickerApiKey: '' }); - component.ngOnInit(); - expect(component.isGFPDisabled()).toBe(true); expect(googlePickerDownloadServiceMock.loadScript).not.toHaveBeenCalled(); }); @@ -87,49 +106,44 @@ describe('GoogleFilePickerComponent', () => { it('should initialize and set folder picker visible on init', () => { setup({ isFolderPicker: true }); - component.ngOnInit(); - expect(googlePickerDownloadServiceMock.loadScript).toHaveBeenCalled(); expect(googlePickerDownloadServiceMock.loadGapiModules).toHaveBeenCalled(); expect(component.visible()).toBe(true); }); it('should capture Sentry error when script loading fails', () => { - setup(); + setup({ detectChanges: false }); const error = new Error('script fail'); googlePickerDownloadServiceMock.loadScript.mockReturnValue(throwError(() => error)); - component.ngOnInit(); + fixture.detectChanges(); expect(sentryMock.captureException).toHaveBeenCalledWith(error, { tags: { feature: 'google-picker load' } }); }); it('should capture Sentry error when gapi modules loading fails', () => { - setup(); + setup({ detectChanges: false }); const error = new Error('gapi fail'); googlePickerDownloadServiceMock.loadGapiModules.mockReturnValue(throwError(() => error)); - component.ngOnInit(); + fixture.detectChanges(); expect(sentryMock.captureException).toHaveBeenCalledWith(error, { tags: { feature: 'google-picker auth' } }); }); it('should dispatch token action and open picker for account id', () => { - setup({ accountId: 'account-1' }); - vi.spyOn(store, 'selectSnapshot').mockReturnValue('oauth-token'); + setup({ accountId: 'account-1', oauthToken: 'oauth-token' }); - component.ngOnInit(); component.createPicker(); expect(store.dispatch).toHaveBeenCalledWith(new GetAuthorizedStorageOauthToken('account-1', AddonType.STORAGE)); - expect(component.accessToken()).toBe('oauth-token'); expect(component.isGFPDisabled()).toBe(false); expect(pickerBuilderMock.setOAuthToken).toHaveBeenCalledWith('oauth-token'); expect(pickerSetVisibleMock).toHaveBeenCalledWith(true); }); it('should send selected item to handleFolderSelection on PICKED action', () => { - setup(); + setup({ accountId: 'account-1', oauthToken: 'oauth-token' }); const handleFolderSelection = vi.fn(); fixture.componentRef.setInput('handleFolderSelection', handleFolderSelection); fixture.detectChanges(); @@ -139,24 +153,26 @@ describe('GoogleFilePickerComponent', () => { id: 42, }; - component.pickerCallback({ + component.createPicker(); + registeredPickerCallback()?.({ action: 'picked', docs: [selectedDoc], }); expect(handleFolderSelection).toHaveBeenCalledWith({ itemName: 'Google Doc', - itemId: 42, + itemId: '42', }); }); it('should ignore callback when action is not PICKED', () => { - setup(); + setup({ accountId: 'account-1', oauthToken: 'oauth-token' }); const handleFolderSelection = vi.fn(); fixture.componentRef.setInput('handleFolderSelection', handleFolderSelection); fixture.detectChanges(); - component.pickerCallback({ + component.createPicker(); + registeredPickerCallback()?.({ action: 'cancel', docs: [{ name: 'Google Doc', id: 42 }], }); diff --git a/src/app/shared/components/google-file-picker/google-file-picker.component.ts b/src/app/shared/components/google-file-picker/google-file-picker.component.ts index 04fd0a080..bba91005b 100644 --- a/src/app/shared/components/google-file-picker/google-file-picker.component.ts +++ b/src/app/shared/components/google-file-picker/google-file-picker.component.ts @@ -1,17 +1,29 @@ -import { Store } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; import { TranslatePipe, TranslateService } from '@ngx-translate/core'; import { Button } from 'primeng/button'; -import { ChangeDetectionStrategy, Component, effect, inject, input, OnInit, signal } from '@angular/core'; +import { catchError, EMPTY, switchMap } from 'rxjs'; + +import { + ChangeDetectionStrategy, + Component, + computed, + DestroyRef, + effect, + inject, + input, + OnInit, + signal, +} from '@angular/core'; +import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { ENVIRONMENT } from '@core/provider/environment.provider'; import { SENTRY_TOKEN } from '@core/provider/sentry.provider'; import { AddonType } from '@osf/shared/enums/addon-type.enum'; import { GoogleFilePickerDownloadService } from '@osf/shared/services/google-file-picker.download.service'; import { StorageItem } from '@shared/models/addons/storage-item.model'; -import { GoogleFileDataModel } from '@shared/models/files/google-file-data.model'; import { GoogleFilePickerModel } from '@shared/models/files/google-file-picker.model'; import { AddonsSelectors, GetAuthorizedStorageOauthToken } from '@shared/stores/addons'; @@ -24,114 +36,112 @@ import { AddonsSelectors, GetAuthorizedStorageOauthToken } from '@shared/stores/ }) export class GoogleFilePickerComponent implements OnInit { private readonly Sentry = inject(SENTRY_TOKEN); - private readonly store = inject(Store); + private readonly destroyRef = inject(DestroyRef); private readonly environment = inject(ENVIRONMENT); private readonly translateService = inject(TranslateService); private readonly googlePicker = inject(GoogleFilePickerDownloadService); private readonly apiKey = this.environment.googleFilePickerApiKey; private readonly appId = this.environment.googleFilePickerAppId; - private parentId = ''; - private isMultipleSelect!: boolean; - private title!: string; - - isFolderPicker = input.required(); - rootFolder = input(null); - accountId = input(''); - handleFolderSelection = input<(folder: StorageItem) => void>(); - currentAddonType = input(AddonType.STORAGE); - - accessToken = signal(null); - visible = signal(false); - isGFPDisabled = signal(true); - - private get isPickerConfigured() { - return !!this.apiKey && !!this.appId; - } + private readonly isPickerConfigured = !!this.apiKey && !!this.appId; + + private readonly authorizedStorageAddons = select(AddonsSelectors.getAuthorizedStorageAddons); + private readonly actions = createDispatchMap({ + getAuthorizedStorageOauthToken: GetAuthorizedStorageOauthToken, + }); + + readonly isFolderPicker = input.required(); + readonly rootFolder = input(null); + readonly accountId = input(''); + readonly handleFolderSelection = input<(folder: StorageItem) => void>(); + readonly currentAddonType = input(AddonType.STORAGE); + + private readonly scriptsLoaded = signal(false); + + private readonly accessToken = computed(() => { + const accountId = this.accountId(); + return this.authorizedStorageAddons()?.find((addon) => addon.id === accountId)?.oauthToken || null; + }); + + readonly visible = computed(() => this.isFolderPicker() && this.scriptsLoaded()); + readonly isGFPDisabled = computed(() => !this.isPickerConfigured || !this.scriptsLoaded() || !this.accessToken()); constructor() { effect(() => { - const isReady = !this.isGFPDisabled(); - const hasRootFolder = !!this.rootFolder(); - const isFilePicker = !this.isFolderPicker(); - - if (isReady && hasRootFolder && isFilePicker) { - this.createPicker(); + if (!this.isPickerConfigured || !this.accountId()) { + return; } + + this.loadOauthToken(); }); } ngOnInit(): void { if (!this.isPickerConfigured) { - this.isGFPDisabled.set(true); return; } - this.parentId = this.isFolderPicker() ? '' : this.rootFolder()?.itemId || ''; - this.title = this.isFolderPicker() - ? this.translateService.instant('settings.addons.configureAddon.googleFilePicker.rootFolderTitle') - : this.translateService.instant('settings.addons.configureAddon.googleFilePicker.fileFolderTitle'); - this.isMultipleSelect = !this.isFolderPicker(); - - this.googlePicker.loadScript().subscribe({ - next: () => { - this.googlePicker.loadGapiModules().subscribe({ - next: () => { - this.initializePicker(); - this.loadOauthToken(); - }, - error: (err) => this.Sentry.captureException(err, { tags: { feature: 'google-picker auth' } }), - }); - }, - error: (err) => this.Sentry.captureException(err, { tags: { feature: 'google-picker load' } }), - }); + this.googlePicker + .loadScript() + .pipe( + catchError((err) => { + this.Sentry.captureException(err, { tags: { feature: 'google-picker load' } }); + return EMPTY; + }), + switchMap(() => this.googlePicker.loadGapiModules()), + takeUntilDestroyed(this.destroyRef) + ) + .subscribe({ + next: () => this.scriptsLoaded.set(true), + error: (err) => this.Sentry.captureException(err, { tags: { feature: 'google-picker auth' } }), + }); } createPicker(): void { - if (!this.isPickerConfigured) return; + if (this.isGFPDisabled()) { + return; + } this.refreshOauthTokenAndOpenPicker(); } private refreshOauthTokenAndOpenPicker(): void { - if (!this.accountId()) { - this.openPickerWithCurrentToken(); - return; - } - - this.store.dispatch(new GetAuthorizedStorageOauthToken(this.accountId(), this.currentAddonType())).subscribe({ - complete: () => { - this.accessToken.set( - this.store.selectSnapshot(AddonsSelectors.getAuthorizedStorageAddonOauthToken(this.accountId())) - ); - this.isGFPDisabled.set(!this.accessToken()); - this.openPickerWithCurrentToken(); - }, - error: () => { - this.openPickerWithCurrentToken(); - }, - }); + this.actions + .getAuthorizedStorageOauthToken(this.accountId(), this.currentAddonType()) + .pipe(takeUntilDestroyed(this.destroyRef)) + .subscribe({ + complete: () => this.openPickerWithCurrentToken(), + error: () => undefined, + }); } private openPickerWithCurrentToken(): void { const google = window.google; + if (!google?.picker) { + return; + } + + const isFolderPicker = this.isFolderPicker(); + const titleKey = isFolderPicker + ? 'settings.addons.configureAddon.googleFilePicker.rootFolderTitle' + : 'settings.addons.configureAddon.googleFilePicker.fileFolderTitle'; const googlePickerView = new google.picker.DocsView(google.picker.ViewId.DOCS); googlePickerView.setSelectFolderEnabled(true); - if (this.isFolderPicker()) { + if (isFolderPicker) { googlePickerView.setMimeTypes('application/vnd.google-apps.folder'); } googlePickerView.setIncludeFolders(true); - googlePickerView.setParent(this.parentId); + googlePickerView.setParent(isFolderPicker ? '' : this.rootFolder()?.itemId || ''); const pickerBuilder = new google.picker.PickerBuilder() .setDeveloperKey(this.apiKey) - .setAppId(this.appId) + .setAppId(String(this.appId)) .addView(googlePickerView) - .setTitle(this.title) + .setTitle(this.translateService.instant(titleKey)) .setOAuthToken(this.accessToken()) .setCallback(this.pickerCallback.bind(this)); - if (this.isMultipleSelect) { + if (!isFolderPicker) { pickerBuilder.enableFeature(google.picker.Feature.MULTISELECT_ENABLED); } @@ -139,37 +149,21 @@ export class GoogleFilePickerComponent implements OnInit { picker.setVisible(true); } - private initializePicker() { - if (this.isFolderPicker()) { - this.visible.set(true); - } - } - private loadOauthToken(): void { - if (this.accountId()) { - this.store.dispatch(new GetAuthorizedStorageOauthToken(this.accountId(), this.currentAddonType())).subscribe({ - complete: () => { - this.accessToken.set( - this.store.selectSnapshot(AddonsSelectors.getAuthorizedStorageAddonOauthToken(this.accountId())) - ); - this.isGFPDisabled.set(!this.accessToken()); - }, - }); - } + this.actions.getAuthorizedStorageOauthToken(this.accountId(), this.currentAddonType()); } - private filePickerCallback(data: GoogleFileDataModel) { - this.handleFolderSelection()?.( - Object({ - itemName: data.name, - itemId: data.id, - }) - ); - } + private pickerCallback(data: GoogleFilePickerModel) { + if (data.action !== window.google.picker.Action.PICKED) { + return; + } - pickerCallback(data: GoogleFilePickerModel) { - if (data.action === window.google.picker.Action.PICKED) { - this.filePickerCallback(data.docs[0]); + const handleFolderSelection = this.handleFolderSelection(); + for (const selectedFile of data.docs ?? []) { + handleFolderSelection?.({ + itemName: selectedFile.name, + itemId: String(selectedFile.id), + }); } } } diff --git a/src/app/shared/components/registration-card/registration-card.component.spec.ts b/src/app/shared/components/registration-card/registration-card.component.spec.ts index 1f639bbbb..aaeac5aa2 100644 --- a/src/app/shared/components/registration-card/registration-card.component.spec.ts +++ b/src/app/shared/components/registration-card/registration-card.component.spec.ts @@ -6,7 +6,7 @@ import { Mock } from 'vitest'; import { signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; -import { Router } from '@angular/router'; +import { ActivatedRoute, Router } from '@angular/router'; import { CreateSchemaResponse, FetchAllSchemaResponses, RegistriesSelectors } from '@osf/features/registries/store'; import { RegistrationReviewStates } from '@osf/shared/enums/registration-review-states.enum'; @@ -16,6 +16,7 @@ import { RegistrationCard } from '@shared/models/registration/registration-card. import { MOCK_REGISTRATION } from '@testing/mocks/registration.mock'; import { provideOSFCore } from '@testing/osf.testing.provider'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder, RouterMockType } from '@testing/providers/router-provider.mock'; import { BaseSetupOverrides, @@ -59,6 +60,7 @@ describe('RegistrationCardComponent', () => { providers: [ provideOSFCore(), MockProvider(Router, routerMock), + MockProvider(ActivatedRoute, ActivatedRouteMockBuilder.create().build()), provideMockStore({ signals: mergeSignalOverrides(defaultSignals, overrides.selectorOverrides) }), ], }); diff --git a/src/app/shared/mappers/registration/page-schema.mapper.spec.ts b/src/app/shared/mappers/registration/page-schema.mapper.spec.ts new file mode 100644 index 000000000..93ed01558 --- /dev/null +++ b/src/app/shared/mappers/registration/page-schema.mapper.spec.ts @@ -0,0 +1,156 @@ +import { BlockType } from '@osf/shared/enums/block-type.enum'; +import { FieldType } from '@osf/shared/enums/field-type.enum'; +import { SchemaBlocksResponseJsonApi } from '@osf/shared/models/registration/schema-blocks-json-api.model'; + +import { PageSchemaMapper } from './page-schema.mapper'; + +type SchemaBlock = SchemaBlocksResponseJsonApi['data'][number]; + +function block(id: string, blockType: BlockType, overrides: Partial = {}): SchemaBlock { + return { + id, + type: 'schema-blocks', + attributes: { + block_type: blockType, + display_text: '', + example_text: '', + help_text: '', + index: 0, + registration_response_key: null, + required: false, + schema_block_group_key: id, + ...overrides, + }, + }; +} + +function response(blocks: SchemaBlock[]): SchemaBlocksResponseJsonApi { + return { data: blocks, meta: { total: blocks.length } }; +} + +describe('PageSchemaMapper', () => { + it('should return no pages when the response has no blocks', () => { + expect(PageSchemaMapper.fromSchemaBlocksResponse(response([]))).toEqual([]); + }); + + it('should map a page heading and attach its question', () => { + const pages = PageSchemaMapper.fromSchemaBlocksResponse( + response([ + block('page-1', BlockType.PageHeading, { display_text: 'Study & design', help_text: 'Help' }), + block('q-1', BlockType.QuestionLabel, { + display_text: 'Research questions', + schema_block_group_key: 'g1', + }), + block('input-1', BlockType.LongTextInput, { + registration_response_key: 'q1', + required: true, + schema_block_group_key: 'g1', + }), + ]) + ); + + expect(pages).toEqual([ + { + id: 'page-1', + title: 'Study & design', + helpText: 'Help', + questions: [ + { + id: 'q-1', + displayText: 'Research questions', + helpText: '', + exampleText: '', + required: true, + groupKey: 'g1', + responseKey: 'q1', + fieldType: FieldType.TextArea, + }, + ], + }, + ]); + }); + + it('should attach section questions to the section', () => { + const pages = PageSchemaMapper.fromSchemaBlocksResponse( + response([ + block('page-1', BlockType.PageHeading, { display_text: 'Methods' }), + block('section-1', BlockType.SectionHeading, { display_text: 'Analysis' }), + block('q-1', BlockType.QuestionLabel, { display_text: 'Data processing', schema_block_group_key: 'g1' }), + block('input-1', BlockType.ShortTextInput, { + registration_response_key: 'q2', + required: true, + schema_block_group_key: 'g1', + }), + ]) + ); + + expect(pages[0].questions).toEqual([]); + expect(pages[0].sections).toEqual([ + { + id: 'section-1', + title: 'Analysis', + helpText: '', + questions: [ + { + id: 'q-1', + displayText: 'Data processing', + helpText: '', + exampleText: '', + required: true, + groupKey: 'g1', + responseKey: 'q2', + fieldType: FieldType.Text, + }, + ], + }, + ]); + }); + + it('should wrap questions without a page heading into one page', () => { + const pages = PageSchemaMapper.fromSchemaBlocksResponse( + response([ + block('q-1', BlockType.QuestionLabel, { display_text: 'Research questions', schema_block_group_key: 'g1' }), + block('input-1', BlockType.LongTextInput, { + registration_response_key: 'q1', + required: true, + schema_block_group_key: 'g1', + }), + block('q-2', BlockType.QuestionLabel, { display_text: 'Upload files', schema_block_group_key: 'g2' }), + block('input-2', BlockType.FileInput, { + registration_response_key: '53-7', + required: false, + schema_block_group_key: 'g2', + }), + ]) + ); + + expect(pages).toEqual([ + { + id: 'q-1', + title: '', + questions: [ + { + id: 'q-1', + displayText: 'Research questions', + helpText: '', + exampleText: '', + required: true, + groupKey: 'g1', + responseKey: 'q1', + fieldType: FieldType.TextArea, + }, + { + id: 'q-2', + displayText: 'Upload files', + helpText: '', + exampleText: '', + required: false, + groupKey: 'g2', + responseKey: '53-7', + fieldType: FieldType.File, + }, + ], + }, + ]); + }); +}); diff --git a/src/app/shared/mappers/registration/page-schema.mapper.ts b/src/app/shared/mappers/registration/page-schema.mapper.ts index c42851d43..dc3f2b474 100644 --- a/src/app/shared/mappers/registration/page-schema.mapper.ts +++ b/src/app/shared/mappers/registration/page-schema.mapper.ts @@ -7,6 +7,7 @@ import { SchemaBlocksResponseJsonApi } from '@shared/models/registration/schema- export class PageSchemaMapper { static fromSchemaBlocksResponse(response: SchemaBlocksResponseJsonApi): PageSchema[] { const pages: PageSchema[] = []; + const unpagedQuestions: Question[] = []; let currentPage!: PageSchema; let currentQuestion: Question | null = null; let currentSection: Section | null = null; @@ -75,6 +76,8 @@ export class PageSchemaMapper { } else if (currentPage) { currentPage.questions = currentPage.questions || []; currentPage.questions.push(currentQuestion); + } else { + unpagedQuestions.push(currentQuestion); } break; @@ -94,6 +97,8 @@ export class PageSchemaMapper { } else if (currentPage) { currentPage.questions = currentPage.questions || []; currentPage.questions.push(currentQuestion); + } else { + unpagedQuestions.push(currentQuestion); } break; @@ -151,6 +156,14 @@ export class PageSchemaMapper { } }); + if (!pages.length && unpagedQuestions.length) { + pages.push({ + id: unpagedQuestions[0].id, + title: '', + questions: unpagedQuestions, + }); + } + return pages; } } diff --git a/src/app/shared/models/environment.model.ts b/src/app/shared/models/environment.model.ts index 184fe4ce4..fcc33126d 100644 --- a/src/app/shared/models/environment.model.ts +++ b/src/app/shared/models/environment.model.ts @@ -65,4 +65,5 @@ export interface EnvironmentModel { */ googleFilePickerAppId: number; throttleToken: string; + maintenance?: boolean; } diff --git a/src/assets/config/template.json b/src/assets/config/template.json index 826cb39c4..f74a71179 100644 --- a/src/assets/config/template.json +++ b/src/assets/config/template.json @@ -27,5 +27,6 @@ "newRelicLoaderConfigTrustKey": "", "newRelicLoaderConfigAgentID": "", "newRelicLoaderConfigLicenseKey": "", - "newRelicLoaderConfigApplicationID": "" + "newRelicLoaderConfigApplicationID": "", + "maintenance": false } diff --git a/src/assets/i18n/en.json b/src/assets/i18n/en.json index c041a41cc..26437b779 100644 --- a/src/assets/i18n/en.json +++ b/src/assets/i18n/en.json @@ -240,7 +240,8 @@ "required": "Password is required." } }, - "resetPassword": "Reset password" + "resetPassword": "Reset password", + "resendConfirmation": "Resend" }, "forgotPassword": { "description": "Enter your email address and we'll send a link to reset your password", @@ -249,6 +250,13 @@ }, "title": "Forgot Your Password?" }, + "resendConfirmation": { + "description": "Enter your email address and we'll resend your confirmation link.", + "messages": { + "success": "Thanks. Check your email for confirmation link." + }, + "title": "Resend Confirmation Email" + }, "resetPassword": { "success": { "backToSignin": "Back to Sign In", @@ -2180,9 +2188,25 @@ "addNewRegistry": "Add New Registration", "createDraft": "Create draft", "createdSuccessfully": "Draft created successfully", - "hostedRegistriesLink": "Learn more about other hosted registries.", + "policy": { + "acceptable": { + "concluded": "Plans for research that has concluded, with disclosure that they are submitted retrospectively", + "notBegun": "Plans for research that has not yet begun", + "underway": "Plans for research that is underway, with explanation of the current state of the study" + }, + "acceptableTitle": "Acceptable submissions to the registries on OSF include:", + "alternative": "Seek alternative paper sharing services and repositories for these uses.", + "checkbox": "I understand and confirm that I am submitting a research plan, and I am not submitting a paper, preprint, or other research output that is not associated with a research plan.", + "createDraftTooltip": "Read the information above. Click the checkbox if you understand and acknowledge that you are submitting a research plan.", + "intro": "Registration on OSF is for submitting research plans.", + "preregistration": "Ideally, registrations are submitted prior to observing the outcomes of the research, and include a description of what the research is, how it will be done, and how it will be analyzed. These are preregistrations.", + "unacceptable": { + "otherOutputs": "Any other research outputs (e.g., data) that are not a part of the research plan", + "papers": "Papers or preprints, including those reporting research outcomes, commentaries, proofs, or other theoretical pieces" + }, + "unacceptableTitle": "Unacceptable submissions to the registries on OSF include:" + }, "registryClosedForSubmissions": "This registry is closed for new submissions. Please start a new registration with a different registry.", - "registrySubmissionInfo": "You are submitting to OSF Registries.", "selectProject": "Select your project", "steps": { "existingProjectQuestion": "Do you have content for registration in an existing OSF project?", @@ -2877,6 +2901,7 @@ } }, "maintenance": { + "details": "For more details, please visit", "message": "Please come back later.", "title": "The OSF is currently down for scheduled maintenance." }, diff --git a/src/testing/providers/auth-service.mock.ts b/src/testing/providers/auth-service.mock.ts index 6bd0cd2d9..f79b5a234 100644 --- a/src/testing/providers/auth-service.mock.ts +++ b/src/testing/providers/auth-service.mock.ts @@ -15,6 +15,7 @@ export type AuthServiceMockType = Partial & { register: Mock; forgotPassword: Mock; resetPassword: Mock; + resendConfirmationUrl: Mock; }; export const AuthServiceMock = { @@ -30,6 +31,7 @@ export const AuthServiceMock = { register: vi.fn().mockReturnValue(of({})), forgotPassword: vi.fn().mockReturnValue(of({})), resetPassword: vi.fn().mockReturnValue(of({})), + resendConfirmationUrl: vi.fn().mockReturnValue(of({})), }; }, };