From 8bf4632f4d9e60230c321d112aa98a051ac8cdfd Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 24 Nov 2025 18:24:59 +0200 Subject: [PATCH 01/11] fix(ssr): setup ssr --- angular.json | 7 +- package-lock.json | 150 +- package.json | 8 +- src/app/app.config.server.ts | 12 + src/app/app.routes.server.ts | 8 + src/assets/js/newrelic/newrelic.snippet.js | 3319 -------------------- src/index.html | 1 - src/main.server.ts | 8 + src/main.ts | 4 +- src/server.ts | 66 + tsconfig.app.json | 4 +- 11 files changed, 174 insertions(+), 3413 deletions(-) create mode 100644 src/app/app.config.server.ts create mode 100644 src/app/app.routes.server.ts delete mode 100644 src/assets/js/newrelic/newrelic.snippet.js create mode 100644 src/main.server.ts create mode 100644 src/server.ts diff --git a/angular.json b/angular.json index 5108d40ce..fc5c3f7cb 100644 --- a/angular.json +++ b/angular.json @@ -59,7 +59,12 @@ "node_modules/ngx-markdown-editor/assets/highlight.js/highlight.min.js", "node_modules/ngx-markdown-editor/assets/marked.min.js", "src/assets/js/ace/snippetsMarkdown.js" - ] + ], + "server": "src/main.server.ts", + "outputMode": "server", + "ssr": { + "entry": "src/server.ts" + } }, "configurations": { "production": { diff --git a/package-lock.json b/package-lock.json index 9699293ab..844d2cbf7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "osf", - "version": "25.2.0", + "version": "25.3.0", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "osf", - "version": "25.2.0", + "version": "25.3.0", "dependencies": { "@angular/animations": "^19.2.0", "@angular/cdk": "^19.2.1", @@ -16,7 +16,9 @@ "@angular/forms": "^19.2.0", "@angular/platform-browser": "^19.2.0", "@angular/platform-browser-dynamic": "^19.2.0", + "@angular/platform-server": "^19.2.0", "@angular/router": "^19.2.0", + "@angular/ssr": "^19.2.17", "@citation-js/core": "^0.7.18", "@citation-js/plugin-csl": "^0.7.18", "@fortawesome/fontawesome-free": "^6.7.2", @@ -33,6 +35,7 @@ "cedar-embeddable-editor": "1.2.2", "chart.js": "^4.4.9", "diff": "^8.0.2", + "express": "^4.18.2", "markdown-it": "^14.1.0", "markdown-it-video": "^0.6.3", "ngx-captcha": "^13.0.0", @@ -55,10 +58,12 @@ "@commitlint/cli": "^19.7.1", "@commitlint/config-conventional": "^19.7.1", "@compodoc/compodoc": "^1.1.26", + "@types/express": "^4.17.17", "@types/gapi": "^0.0.47", "@types/gapi.auth2": "^0.0.61", "@types/jest": "^29.5.14", "@types/markdown-it": "^14.1.2", + "@types/node": "^18.18.0", "angular-eslint": "19.1.0", "angularx-qrcode": "^19.0.0", "eslint": "^9.20.0", @@ -851,6 +856,26 @@ "@angular/platform-browser": "19.2.15" } }, + "node_modules/@angular/platform-server": { + "version": "19.2.15", + "resolved": "https://registry.npmjs.org/@angular/platform-server/-/platform-server-19.2.15.tgz", + "integrity": "sha512-VKuEmzFylYLnFjjFTctnbckgYdXEyt3wU0AwT3uuLrSU/3EgfHlqd33ONuYaIxSRES81GaLcV9cc9uiZYT2QMg==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0", + "xhr2": "^0.2.0" + }, + "engines": { + "node": "^18.19.1 || ^20.11.1 || >=22.0.0" + }, + "peerDependencies": { + "@angular/common": "19.2.15", + "@angular/compiler": "19.2.15", + "@angular/core": "19.2.15", + "@angular/platform-browser": "19.2.15", + "rxjs": "^6.5.3 || ^7.4.0" + } + }, "node_modules/@angular/router": { "version": "19.2.15", "resolved": "https://registry.npmjs.org/@angular/router/-/router-19.2.15.tgz", @@ -869,6 +894,26 @@ "rxjs": "^6.5.3 || ^7.4.0" } }, + "node_modules/@angular/ssr": { + "version": "19.2.19", + "resolved": "https://registry.npmjs.org/@angular/ssr/-/ssr-19.2.19.tgz", + "integrity": "sha512-7HqC3K99DdzDakB/4mkqGqY6REQNMxskU1VVkH9D7SthZSuxhWIMVBojVhBDd+JOUYiyQlwEGMBevbrgbtfKlQ==", + "license": "MIT", + "dependencies": { + "tslib": "^2.3.0" + }, + "peerDependencies": { + "@angular/common": "^19.0.0 || ^19.2.0-next.0", + "@angular/core": "^19.0.0 || ^19.2.0-next.0", + "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0", + "@angular/router": "^19.0.0 || ^19.2.0-next.0" + }, + "peerDependenciesMeta": { + "@angular/platform-server": { + "optional": true + } + } + }, "node_modules/@arr/every": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/@arr/every/-/every-1.0.1.tgz", @@ -8219,13 +8264,13 @@ "license": "MIT" }, "node_modules/@types/node": { - "version": "24.5.2", - "resolved": "https://registry.npmjs.org/@types/node/-/node-24.5.2.tgz", - "integrity": "sha512-FYxk1I7wPv3K2XBaoyH2cTnocQEu8AOZ60hPbsyukMPLv5/5qr7V1i8PLHdl6Zf87I+xZXFvPCXYjiTFq+YSDQ==", + "version": "18.19.130", + "resolved": "https://registry.npmjs.org/@types/node/-/node-18.19.130.tgz", + "integrity": "sha512-GRaXQx6jGfL8sKfaIDD6OupbIHBr9jv7Jnaml9tB7l4v068PAOXqfcujMMo5PhbIs6ggR1XODELqahT2R8v0fg==", "dev": true, "license": "MIT", "dependencies": { - "undici-types": "~7.12.0" + "undici-types": "~5.26.4" } }, "node_modules/@types/node-forge": { @@ -9128,7 +9173,6 @@ "version": "1.3.8", "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", - "dev": true, "license": "MIT", "dependencies": { "mime-types": "~2.1.34", @@ -9142,7 +9186,6 @@ "version": "0.6.3", "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -9562,7 +9605,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==", - "dev": true, "license": "MIT" }, "node_modules/array-ify": { @@ -10364,7 +10406,6 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -10515,7 +10556,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/call-bind-apply-helpers/-/call-bind-apply-helpers-1.0.2.tgz", "integrity": "sha512-Sp1ablJ0ivDkSzjcaJdxEunN5/XvksFJ2sMBFfq6x0ryhQV/2b/KwFe21cMpmHtPOSij8K99/wSfoEuTObmuMQ==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -10529,7 +10569,6 @@ "version": "1.0.4", "resolved": "https://registry.npmjs.org/call-bound/-/call-bound-1.0.4.tgz", "integrity": "sha512-+ys997U96po4Kx/ABpBCqhA9EuxJaQWDQg7295H4hBphv3IZg0boBKuwYpt4YXp6MZ5AmZQnU/tyMTlRpaSejg==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -11225,7 +11264,6 @@ "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", - "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "5.2.1" @@ -11238,7 +11276,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -11259,7 +11296,6 @@ "version": "1.0.5", "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -11321,7 +11357,6 @@ "version": "0.7.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.7.1.tgz", "integrity": "sha512-6DnInpx7SJ2AK3+CTUE/ZM0vWTUboZCegxhC2xiIydHR9jNuTAASBrfEpHhiGOZw/nX51bHt6YQl8jsGo4y/0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -11331,7 +11366,6 @@ "version": "1.0.6", "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==", - "dev": true, "license": "MIT" }, "node_modules/copy-anything": { @@ -11903,7 +11937,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -11913,7 +11946,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8", @@ -12097,7 +12129,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/dunder-proto/-/dunder-proto-1.0.1.tgz", "integrity": "sha512-KIN/nDJBQRcXw0MLVhZE9iQHmG68qAVIBg9CqmUYjmQIhgij9U5MFvrqkUL5FbtyyzZuOeOt0zdeRe4UY7ct+A==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.1", @@ -12126,7 +12157,6 @@ "version": "1.1.1", "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==", - "dev": true, "license": "MIT" }, "node_modules/ejs": { @@ -12414,7 +12444,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/es-define-property/-/es-define-property-1.0.1.tgz", "integrity": "sha512-e3nRfgfUZ4rNGL232gUgX06QNyyez04KdjFrF+LTRoOXmrOgFKDg4BCdsjW8EnT69eqdYGmRpJwiPVYNrCaW3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12424,7 +12453,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/es-errors/-/es-errors-1.3.0.tgz", "integrity": "sha512-Zf5H2Kxt2xjTvbJvP2ZWLEICxA6j+hAmMzIlypy4xcBg1vKVnx89Wy0GbS+kf5cwCVFFzdCFh2XSCFNULS6csw==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -12441,7 +12469,6 @@ "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==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0" @@ -12572,7 +12599,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==", - "dev": true, "license": "MIT" }, "node_modules/escape-string-regexp": { @@ -13171,7 +13197,6 @@ "version": "1.8.1", "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -13278,7 +13303,6 @@ "version": "4.21.2", "resolved": "https://registry.npmjs.org/express/-/express-4.21.2.tgz", "integrity": "sha512-28HqgMZAmih1Czt9ny7qr6ek2qddF4FclbMzwhCREB6OFfH+rXAnuNCwo1/wFvrtbgsQDb4kSbX9de9lFbrXnA==", - "dev": true, "license": "MIT", "dependencies": { "accepts": "~1.3.8", @@ -13325,7 +13349,6 @@ "version": "1.20.3", "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.3.tgz", "integrity": "sha512-7rAxByjUMqQ3/bHJy7D6OGXvx/MMc4IqBn/X0fcM1QUcAItpZrBEYhWGem+tzXH90c+G01ypMcYJBO9Y30203g==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -13350,7 +13373,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -13360,14 +13382,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/express/node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -13377,7 +13397,6 @@ "version": "1.3.1", "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.3.1.tgz", "integrity": "sha512-6BN9trH7bp3qvnrRyzsBz+g3lZxTNZTbVO2EV1CS0WIcDbawYVdYvGflME/9QP0h0pYlCDBCTjYa9nZzMDpyxQ==", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -13396,7 +13415,6 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -13406,7 +13424,6 @@ "version": "0.4.24", "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", - "dev": true, "license": "MIT", "dependencies": { "safer-buffer": ">= 2.1.2 < 3" @@ -13419,7 +13436,6 @@ "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -13429,7 +13445,6 @@ "version": "6.13.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.13.0.tgz", "integrity": "sha512-+38qI9SOr8tfZ4QmJNplMUxqjbe7LKvvZgWdExBOmd+egZTtjLB67Gu0HRX3u/XOq7UU2Nx6nsjvS16Z9uwfpg==", - "dev": true, "license": "BSD-3-Clause", "dependencies": { "side-channel": "^1.0.6" @@ -13445,7 +13460,6 @@ "version": "2.5.2", "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.2.tgz", "integrity": "sha512-8zGqypfENjCIqGhgXToC8aB2r7YrBX+AQAfIPs/Mlk+BtPTztOvTS01NRW/3Eh60J+a48lt8qsCzirQ6loCVfA==", - "dev": true, "license": "MIT", "dependencies": { "bytes": "3.1.2", @@ -13461,7 +13475,6 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", - "dev": true, "funding": [ { "type": "github", @@ -13482,7 +13495,6 @@ "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -13507,7 +13519,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -13517,7 +13528,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -13527,7 +13537,6 @@ "version": "1.6.18", "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", - "dev": true, "license": "MIT", "dependencies": { "media-typer": "0.3.0", @@ -13940,7 +13949,6 @@ "version": "0.2.0", "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -14031,7 +14039,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/ljharb" @@ -14105,7 +14112,6 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.3.0.tgz", "integrity": "sha512-9fSjSaos/fRIVIp+xSJlE6lfwhES7LNtKaCBIamHsjr2na1BiABJPo0mOjjz8GJDURarmCPGqaiVg5mfjb98CQ==", - "dev": true, "license": "MIT", "dependencies": { "call-bind-apply-helpers": "^1.0.2", @@ -14140,7 +14146,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/get-proto/-/get-proto-1.0.1.tgz", "integrity": "sha512-sTSfBjoXBp89JvIKIefqw7U2CCebsc74kiY6awiGogKtoSGbgjYE/G/+l9sF3MWFPNc9IcoOC4ODfKHfxFmp0g==", - "dev": true, "license": "MIT", "dependencies": { "dunder-proto": "^1.0.1", @@ -14370,7 +14375,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", "integrity": "sha512-ZUKRh6/kUFoAiTAtTYPZJ3hw9wNxx+BIBOijnlG9PnrJsCcSjs1wyyD6vJpaYtgnzDrKYRSqf3OO6Rfa93xsRg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -14514,7 +14518,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.1.0.tgz", "integrity": "sha512-1cDNdwJ2Jaohmb3sg4OmKaMBwuC48sYni5HUw2DvsC8LjGTLK9h+eb1X6RyuOHe4hT0ULCW68iomhjUoKUqlPQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -14543,7 +14546,6 @@ "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", - "dev": true, "license": "MIT", "dependencies": { "function-bind": "^1.1.2" @@ -14768,7 +14770,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", - "dev": true, "license": "MIT", "dependencies": { "depd": "2.0.0", @@ -14785,7 +14786,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -15208,7 +15208,6 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", - "dev": true, "license": "ISC" }, "node_modules/ini": { @@ -18508,7 +18507,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", "integrity": "sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -18566,7 +18564,6 @@ "version": "1.0.3", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.3.tgz", "integrity": "sha512-gaNvAS7TZ897/rVaZ0nMtAyxNyi/pdbjbAwUpFQpN70GqnVfOiXpeUUMKRBmzXaSQ8DdTX4/0ms62r2K+hE6mQ==", - "dev": true, "license": "MIT", "funding": { "url": "https://github.com/sponsors/sindresorhus" @@ -18593,7 +18590,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -18630,7 +18626,6 @@ "version": "1.6.0", "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", - "dev": true, "license": "MIT", "bin": { "mime": "cli.js" @@ -18643,7 +18638,6 @@ "version": "1.52.0", "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -18653,7 +18647,6 @@ "version": "2.1.35", "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", - "dev": true, "license": "MIT", "dependencies": { "mime-db": "1.52.0" @@ -18982,7 +18975,6 @@ "version": "2.1.3", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", - "dev": true, "license": "MIT" }, "node_modules/msgpackr": { @@ -19545,7 +19537,6 @@ "version": "1.13.4", "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.4.tgz", "integrity": "sha512-W67iLl4J2EXEGTbfeHCffrjDfitvLANg0UlX3wFUUSTx92KXRFegMHUVgSqE+wvhAbi4WqjGg9czysTV2Epbew==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4" @@ -19649,7 +19640,6 @@ "version": "2.4.1", "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", - "dev": true, "license": "MIT", "dependencies": { "ee-first": "1.1.1" @@ -20181,7 +20171,6 @@ "version": "1.3.3", "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -20262,7 +20251,6 @@ "version": "0.1.12", "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.12.tgz", "integrity": "sha512-RA1GjUVMnvYFxuqovrEqZoxxW5NUZqbwKtYz/Tt7nXerk0LbLblQmrsgdeOxV5SFHf0UDggjS/bSeOZwt1pmEQ==", - "dev": true, "license": "MIT" }, "node_modules/path-type": { @@ -20735,7 +20723,6 @@ "version": "2.0.7", "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", - "dev": true, "license": "MIT", "dependencies": { "forwarded": "0.2.0", @@ -20749,7 +20736,6 @@ "version": "1.9.1", "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.10" @@ -21086,7 +21072,6 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -21676,7 +21661,6 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==", - "devOptional": true, "license": "MIT" }, "node_modules/sass": { @@ -21990,7 +21974,6 @@ "version": "1.16.2", "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.16.2.tgz", "integrity": "sha512-VqpjJZKadQB/PEbEwvFdO43Ax5dFBZ2UECszz8bQ7pi7wt//PWe1P6MN7eCnjsatYtBT6EuiClbjSWP2WrIoTw==", - "dev": true, "license": "MIT", "dependencies": { "encodeurl": "~2.0.0", @@ -22006,7 +21989,6 @@ "version": "2.6.9", "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", - "dev": true, "license": "MIT", "dependencies": { "ms": "2.0.0" @@ -22016,14 +21998,12 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==", - "dev": true, "license": "MIT" }, "node_modules/serve-static/node_modules/encodeurl": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-2.0.0.tgz", "integrity": "sha512-Q0n9HRi4m6JuGIV1eFlmvJB7ZEVxu93IrMyiMsGC0lrMJMWzRgx6WGquyfQgZVb31vhGgXnfmPNNXmxnOkRBrg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -22033,7 +22013,6 @@ "version": "0.5.2", "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.6" @@ -22043,7 +22022,6 @@ "version": "0.19.0", "resolved": "https://registry.npmjs.org/send/-/send-0.19.0.tgz", "integrity": "sha512-dW41u5VfLXu8SJh5bwRmyYUbAoSB3c9uQh6L8h/KtsFREPWpbX1lrljJo186Jc4nmci/sGUZ9a0a0J2zgfq2hw==", - "dev": true, "license": "MIT", "dependencies": { "debug": "2.6.9", @@ -22068,7 +22046,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -22078,7 +22055,6 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -22144,7 +22120,6 @@ "version": "1.2.0", "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==", - "dev": true, "license": "ISC" }, "node_modules/shallow-clone": { @@ -22200,7 +22175,6 @@ "version": "1.1.0", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.1.0.tgz", "integrity": "sha512-ZX99e6tRweoUXqR+VBrslhda51Nh5MTQwou5tnUDgbtyM0dBgmhEDtWGP/xbKn6hqfPRHujUNwz5fy/wbbhnpw==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -22220,7 +22194,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/side-channel-list/-/side-channel-list-1.0.0.tgz", "integrity": "sha512-FCLHtRD/gnpCiCHEiJLOwdmFP+wzCmDEkc9y7NsYxeF4u7Btsn1ZuwgwJGxImImHicJArLP4R0yX4c2KCrMrTA==", - "dev": true, "license": "MIT", "dependencies": { "es-errors": "^1.3.0", @@ -22237,7 +22210,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/side-channel-map/-/side-channel-map-1.0.1.tgz", "integrity": "sha512-VCjCNfgMsby3tTdo02nbjtM/ewra6jPHmpThenkTYh8pG9ucZ/1P8So4u4FGBek/BjpOVsDCMoLA/iuBKIFXRA==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -22256,7 +22228,6 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/side-channel-weakmap/-/side-channel-weakmap-1.0.2.tgz", "integrity": "sha512-WPS/HvHQTYnHisLo9McqBHOJk2FkHO/tlpvldyrnem4aeQp4hai3gythswg6p01oSoTl58rcpiFAjF2br2Ak2A==", - "dev": true, "license": "MIT", "dependencies": { "call-bound": "^1.0.2", @@ -23729,7 +23700,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", - "dev": true, "license": "MIT", "engines": { "node": ">=0.6" @@ -24344,9 +24314,9 @@ } }, "node_modules/undici-types": { - "version": "7.12.0", - "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.12.0.tgz", - "integrity": "sha512-goOacqME2GYyOZZfb5Lgtu+1IDmAlAEu5xnD3+xTzS10hT0vzpf0SPjkXwAw9Jm+4n/mQGDP3LO8CPbYROeBfQ==", + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==", "dev": true, "license": "MIT" }, @@ -24454,7 +24424,6 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -24523,7 +24492,6 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.4.0" @@ -24590,7 +24558,6 @@ "version": "1.1.2", "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", - "dev": true, "license": "MIT", "engines": { "node": ">= 0.8" @@ -25888,6 +25855,15 @@ } } }, + "node_modules/xhr2": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/xhr2/-/xhr2-0.2.1.tgz", + "integrity": "sha512-sID0rrVCqkVNUn8t6xuv9+6FViXjUVXq8H5rWOH2rz9fDNQEd4g0EA2XlcEdJXRz5BMEn4O1pJFdT+z4YHhoWw==", + "license": "MIT", + "engines": { + "node": ">= 6" + } + }, "node_modules/xml-name-validator": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/xml-name-validator/-/xml-name-validator-4.0.0.tgz", diff --git a/package.json b/package.json index 30c9c6215..0e512e990 100644 --- a/package.json +++ b/package.json @@ -28,7 +28,8 @@ "test:coverage": "jest --coverage && npm run test:display", "test:check-coverage-thresholds": "node .github/scripts/check-coverage-thresholds.js", "test:display": "node .github/counter/counter.test.display.js", - "watch": "ng build --watch --configuration development" + "watch": "ng build --watch --configuration development", + "serve:ssr:osf": "node dist/osf/server/server.mjs" }, "private": true, "dependencies": { @@ -40,7 +41,9 @@ "@angular/forms": "^19.2.0", "@angular/platform-browser": "^19.2.0", "@angular/platform-browser-dynamic": "^19.2.0", + "@angular/platform-server": "^19.2.0", "@angular/router": "^19.2.0", + "@angular/ssr": "^19.2.17", "@citation-js/core": "^0.7.18", "@citation-js/plugin-csl": "^0.7.18", "@fortawesome/fontawesome-free": "^6.7.2", @@ -57,6 +60,7 @@ "cedar-embeddable-editor": "1.2.2", "chart.js": "^4.4.9", "diff": "^8.0.2", + "express": "^4.18.2", "markdown-it": "^14.1.0", "markdown-it-video": "^0.6.3", "ngx-captcha": "^13.0.0", @@ -79,10 +83,12 @@ "@commitlint/cli": "^19.7.1", "@commitlint/config-conventional": "^19.7.1", "@compodoc/compodoc": "^1.1.26", + "@types/express": "^4.17.17", "@types/gapi": "^0.0.47", "@types/gapi.auth2": "^0.0.61", "@types/jest": "^29.5.14", "@types/markdown-it": "^14.1.2", + "@types/node": "^18.18.0", "angular-eslint": "19.1.0", "angularx-qrcode": "^19.0.0", "eslint": "^9.20.0", diff --git a/src/app/app.config.server.ts b/src/app/app.config.server.ts new file mode 100644 index 000000000..1f4702fcd --- /dev/null +++ b/src/app/app.config.server.ts @@ -0,0 +1,12 @@ +import { ApplicationConfig, mergeApplicationConfig } from '@angular/core'; +import { provideServerRendering } from '@angular/platform-server'; +import { provideServerRouting } from '@angular/ssr'; + +import { appConfig } from './app.config'; +import { serverRoutes } from './app.routes.server'; + +const serverConfig: ApplicationConfig = { + providers: [provideServerRendering(), provideServerRouting(serverRoutes)], +}; + +export const config = mergeApplicationConfig(appConfig, serverConfig); diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts new file mode 100644 index 000000000..28c14c584 --- /dev/null +++ b/src/app/app.routes.server.ts @@ -0,0 +1,8 @@ +import { RenderMode, ServerRoute } from '@angular/ssr'; + +export const serverRoutes: ServerRoute[] = [ + { + path: '**', + renderMode: RenderMode.Prerender, + }, +]; diff --git a/src/assets/js/newrelic/newrelic.snippet.js b/src/assets/js/newrelic/newrelic.snippet.js deleted file mode 100644 index e63e4fc1d..000000000 --- a/src/assets/js/newrelic/newrelic.snippet.js +++ /dev/null @@ -1,3319 +0,0 @@ -window.NREUM || (NREUM = {}); -NREUM.init = { - distributed_tracing: { enabled: true }, - performance: { capture_measures: true }, - privacy: { cookies_enabled: true }, - ajax: { deny_list: ['bam.nr-data.net'] }, -}; - -NREUM.loader_config = { - accountID: '772413', - trustKey: '772413', - agentID: '1835137194', - licenseKey: '704513e63b', - applicationID: '1835137194', -}; -NREUM.info = { - beacon: 'bam.nr-data.net', - errorBeacon: 'bam.nr-data.net', - licenseKey: '704513e63b', - applicationID: '1835137194', - sa: 1, -}; /*! For license information please see nr-loader-spa-1.301.0.min.js.LICENSE.txt */ -(() => { - var e, - t, - r = { - 384: (e, t, r) => { - 'use strict'; - r.d(t, { NT: () => a, US: () => d, Zm: () => s, bQ: () => u, dV: () => c, pV: () => l }); - var n = r(6154), - i = r(1863), - o = r(1910); - const a = { beacon: 'bam.nr-data.net', errorBeacon: 'bam.nr-data.net' }; - function s() { - return n.gm.NREUM || (n.gm.NREUM = {}), void 0 === n.gm.newrelic && (n.gm.newrelic = n.gm.NREUM), n.gm.NREUM; - } - function c() { - let e = s(); - return ( - e.o || - ((e.o = { - ST: n.gm.setTimeout, - SI: n.gm.setImmediate || n.gm.setInterval, - CT: n.gm.clearTimeout, - XHR: n.gm.XMLHttpRequest, - REQ: n.gm.Request, - EV: n.gm.Event, - PR: n.gm.Promise, - MO: n.gm.MutationObserver, - FETCH: n.gm.fetch, - WS: n.gm.WebSocket, - }), - (0, o.i)(...Object.values(e.o))), - e - ); - } - function u(e, t) { - let r = s(); - (r.initializedAgents ??= {}), - (t.initializedAt = { ms: (0, i.t)(), date: new Date() }), - (r.initializedAgents[e] = t); - } - function d(e, t) { - s()[e] = t; - } - function l() { - return ( - (function () { - let e = s(); - const t = e.info || {}; - e.info = { beacon: a.beacon, errorBeacon: a.errorBeacon, ...t }; - })(), - (function () { - let e = s(); - const t = e.init || {}; - e.init = { ...t }; - })(), - c(), - (function () { - let e = s(); - const t = e.loader_config || {}; - e.loader_config = { ...t }; - })(), - s() - ); - } - }, - 782: (e, t, r) => { - 'use strict'; - r.d(t, { T: () => n }); - const n = r(860).K7.pageViewTiming; - }, - 860: (e, t, r) => { - 'use strict'; - r.d(t, { - $J: () => d, - K7: () => c, - P3: () => u, - XX: () => i, - Yy: () => s, - df: () => o, - qY: () => n, - v4: () => a, - }); - const n = 'events', - i = 'jserrors', - o = 'browser/blobs', - a = 'rum', - s = 'browser/logs', - c = { - ajax: 'ajax', - genericEvents: 'generic_events', - jserrors: i, - logging: 'logging', - metrics: 'metrics', - pageAction: 'page_action', - pageViewEvent: 'page_view_event', - pageViewTiming: 'page_view_timing', - sessionReplay: 'session_replay', - sessionTrace: 'session_trace', - softNav: 'soft_navigations', - spa: 'spa', - }, - u = { - [c.pageViewEvent]: 1, - [c.pageViewTiming]: 2, - [c.metrics]: 3, - [c.jserrors]: 4, - [c.spa]: 5, - [c.ajax]: 6, - [c.sessionTrace]: 7, - [c.softNav]: 8, - [c.sessionReplay]: 9, - [c.logging]: 10, - [c.genericEvents]: 11, - }, - d = { - [c.pageViewEvent]: a, - [c.pageViewTiming]: n, - [c.ajax]: n, - [c.spa]: n, - [c.softNav]: n, - [c.metrics]: i, - [c.jserrors]: i, - [c.sessionTrace]: o, - [c.sessionReplay]: o, - [c.logging]: s, - [c.genericEvents]: 'ins', - }; - }, - 944: (e, t, r) => { - 'use strict'; - r.d(t, { R: () => i }); - var n = r(3241); - function i(e, t) { - 'function' == typeof console.debug && - (console.debug( - 'New Relic Warning: https://github.com/newrelic/newrelic-browser-agent/blob/main/docs/warning-codes.md#'.concat( - e - ), - t - ), - (0, n.W)({ - agentIdentifier: null, - drained: null, - type: 'data', - name: 'warn', - feature: 'warn', - data: { code: e, secondary: t }, - })); - } - }, - 993: (e, t, r) => { - 'use strict'; - r.d(t, { A$: () => o, ET: () => a, TZ: () => s, p_: () => i }); - var n = r(860); - const i = { ERROR: 'ERROR', WARN: 'WARN', INFO: 'INFO', DEBUG: 'DEBUG', TRACE: 'TRACE' }, - o = { OFF: 0, ERROR: 1, WARN: 2, INFO: 3, DEBUG: 4, TRACE: 5 }, - a = 'log', - s = n.K7.logging; - }, - 1687: (e, t, r) => { - 'use strict'; - r.d(t, { Ak: () => u, Ze: () => f, x3: () => d }); - var n = r(3241), - i = r(7836), - o = r(3606), - a = r(860), - s = r(2646); - const c = {}; - function u(e, t) { - const r = { staged: !1, priority: a.P3[t] || 0 }; - l(e), c[e].get(t) || c[e].set(t, r); - } - function d(e, t) { - e && c[e] && (c[e].get(t) && c[e].delete(t), p(e, t, !1), c[e].size && h(e)); - } - function l(e) { - if (!e) throw new Error('agentIdentifier required'); - c[e] || (c[e] = new Map()); - } - function f(e = '', t = 'feature', r = !1) { - if ((l(e), !e || !c[e].get(t) || r)) return p(e, t); - (c[e].get(t).staged = !0), h(e); - } - function h(e) { - const t = Array.from(c[e]); - t.every(([e, t]) => t.staged) && - (t.sort((e, t) => e[1].priority - t[1].priority), - t.forEach(([t]) => { - c[e].delete(t), p(e, t); - })); - } - function p(e, t, r = !0) { - const a = e ? i.ee.get(e) : i.ee, - c = o.i.handlers; - if (!a.aborted && a.backlog && c) { - if (((0, n.W)({ agentIdentifier: e, type: 'lifecycle', name: 'drain', feature: t }), r)) { - const e = a.backlog[t], - r = c[t]; - if (r) { - for (let t = 0; e && t < e.length; ++t) g(e[t], r); - Object.entries(r).forEach(([e, t]) => { - Object.values(t || {}).forEach((t) => { - t[0]?.on && t[0]?.context() instanceof s.y && t[0].on(e, t[1]); - }); - }); - } - } - a.isolatedBacklog || delete c[t], (a.backlog[t] = null), a.emit('drain-' + t, []); - } - } - function g(e, t) { - var r = e[1]; - Object.values(t[r] || {}).forEach((t) => { - var r = e[0]; - if (t[0] === r) { - var n = t[1], - i = e[3], - o = e[2]; - n.apply(i, o); - } - }); - } - }, - 1741: (e, t, r) => { - 'use strict'; - r.d(t, { W: () => o }); - var n = r(944), - i = r(4261); - class o { - #e(e, ...t) { - if (this[e] !== o.prototype[e]) return this[e](...t); - (0, n.R)(35, e); - } - addPageAction(e, t) { - return this.#e(i.hG, e, t); - } - register(e) { - return this.#e(i.eY, e); - } - recordCustomEvent(e, t) { - return this.#e(i.fF, e, t); - } - setPageViewName(e, t) { - return this.#e(i.Fw, e, t); - } - setCustomAttribute(e, t, r) { - return this.#e(i.cD, e, t, r); - } - noticeError(e, t) { - return this.#e(i.o5, e, t); - } - setUserId(e) { - return this.#e(i.Dl, e); - } - setApplicationVersion(e) { - return this.#e(i.nb, e); - } - setErrorHandler(e) { - return this.#e(i.bt, e); - } - addRelease(e, t) { - return this.#e(i.k6, e, t); - } - log(e, t) { - return this.#e(i.$9, e, t); - } - start() { - return this.#e(i.d3); - } - finished(e) { - return this.#e(i.BL, e); - } - recordReplay() { - return this.#e(i.CH); - } - pauseReplay() { - return this.#e(i.Tb); - } - addToTrace(e) { - return this.#e(i.U2, e); - } - setCurrentRouteName(e) { - return this.#e(i.PA, e); - } - interaction(e) { - return this.#e(i.dT, e); - } - wrapLogger(e, t, r) { - return this.#e(i.Wb, e, t, r); - } - measure(e, t) { - return this.#e(i.V1, e, t); - } - } - }, - 1863: (e, t, r) => { - 'use strict'; - function n() { - return Math.floor(performance.now()); - } - r.d(t, { t: () => n }); - }, - 1910: (e, t, r) => { - 'use strict'; - r.d(t, { i: () => o }); - var n = r(944); - const i = new Map(); - function o(...e) { - return e.every((e) => { - if (i.has(e)) return i.get(e); - const t = 'function' == typeof e && e.toString().includes('[native code]'); - return t || (0, n.R)(64, e?.name || e?.toString()), i.set(e, t), t; - }); - } - }, - 2555: (e, t, r) => { - 'use strict'; - r.d(t, { D: () => s, f: () => a }); - var n = r(384), - i = r(8122); - const o = { - beacon: n.NT.beacon, - errorBeacon: n.NT.errorBeacon, - licenseKey: void 0, - applicationID: void 0, - sa: void 0, - queueTime: void 0, - applicationTime: void 0, - ttGuid: void 0, - user: void 0, - account: void 0, - product: void 0, - extra: void 0, - jsAttributes: {}, - userAttributes: void 0, - atts: void 0, - transactionName: void 0, - tNamePlain: void 0, - }; - function a(e) { - try { - return !!e.licenseKey && !!e.errorBeacon && !!e.applicationID; - } catch (e) { - return !1; - } - } - const s = (e) => (0, i.a)(e, o); - }, - 2614: (e, t, r) => { - 'use strict'; - r.d(t, { BB: () => a, H3: () => n, g: () => u, iL: () => c, tS: () => s, uh: () => i, wk: () => o }); - const n = 'NRBA', - i = 'SESSION', - o = 144e5, - a = 18e5, - s = { - STARTED: 'session-started', - PAUSE: 'session-pause', - RESET: 'session-reset', - RESUME: 'session-resume', - UPDATE: 'session-update', - }, - c = { SAME_TAB: 'same-tab', CROSS_TAB: 'cross-tab' }, - u = { OFF: 0, FULL: 1, ERROR: 2 }; - }, - 2646: (e, t, r) => { - 'use strict'; - r.d(t, { y: () => n }); - class n { - constructor(e) { - this.contextId = e; - } - } - }, - 2843: (e, t, r) => { - 'use strict'; - r.d(t, { u: () => i }); - var n = r(3878); - function i(e, t = !1, r, i) { - (0, n.DD)( - 'visibilitychange', - function () { - if (t) return void ('hidden' === document.visibilityState && e()); - e(document.visibilityState); - }, - r, - i - ); - } - }, - 3241: (e, t, r) => { - 'use strict'; - r.d(t, { W: () => o }); - var n = r(6154); - const i = 'newrelic'; - function o(e = {}) { - try { - n.gm.dispatchEvent(new CustomEvent(i, { detail: e })); - } catch (e) {} - } - }, - 3304: (e, t, r) => { - 'use strict'; - r.d(t, { A: () => o }); - var n = r(7836); - const i = () => { - const e = new WeakSet(); - return (t, r) => { - if ('object' == typeof r && null !== r) { - if (e.has(r)) return; - e.add(r); - } - return r; - }; - }; - function o(e) { - try { - return JSON.stringify(e, i()) ?? ''; - } catch (e) { - try { - n.ee.emit('internal-error', [e]); - } catch (e) {} - return ''; - } - } - }, - 3333: (e, t, r) => { - 'use strict'; - r.d(t, { - $v: () => d, - TZ: () => n, - Xh: () => c, - Zp: () => i, - kd: () => u, - mq: () => s, - nf: () => a, - qN: () => o, - }); - const n = r(860).K7.genericEvents, - i = ['auxclick', 'click', 'copy', 'keydown', 'paste', 'scrollend'], - o = ['focus', 'blur'], - a = 4, - s = 1e3, - c = 2e3, - u = ['PageAction', 'UserAction', 'BrowserPerformance'], - d = { RESOURCES: 'experimental.resources' }; - }, - 3434: (e, t, r) => { - 'use strict'; - r.d(t, { Jt: () => o, YM: () => u }); - var n = r(7836), - i = r(5607); - const o = 'nr@original:'.concat(i.W), - a = 50; - var s = Object.prototype.hasOwnProperty, - c = !1; - function u(e, t) { - return ( - e || (e = n.ee), - (r.inPlace = function (e, t, n, i, o) { - n || (n = ''); - const a = '-' === n.charAt(0); - for (let s = 0; s < t.length; s++) { - const c = t[s], - u = e[c]; - l(u) || (e[c] = r(u, a ? c + n : n, i, c, o)); - } - }), - (r.flag = o), - r - ); - function r(t, r, n, c, u) { - return l(t) - ? t - : (r || (r = ''), - (nrWrapper[o] = t), - (function (e, t, r) { - if (Object.defineProperty && Object.keys) - try { - return ( - Object.keys(e).forEach(function (r) { - Object.defineProperty(t, r, { - get: function () { - return e[r]; - }, - set: function (t) { - return (e[r] = t), t; - }, - }); - }), - t - ); - } catch (e) { - d([e], r); - } - for (var n in e) s.call(e, n) && (t[n] = e[n]); - })(t, nrWrapper, e), - nrWrapper); - function nrWrapper() { - var o, s, l, f; - let h; - try { - (s = this), (o = [...arguments]), (l = 'function' == typeof n ? n(o, s) : n || {}); - } catch (t) { - d([t, '', [o, s, c], l], e); - } - i(r + 'start', [o, s, c], l, u); - const p = performance.now(); - let g; - try { - return (f = t.apply(s, o)), (g = performance.now()), f; - } catch (e) { - throw ((g = performance.now()), i(r + 'err', [o, s, e], l, u), (h = e), h); - } finally { - const e = g - p, - t = { start: p, end: g, duration: e, isLongTask: e >= a, methodName: c, thrownError: h }; - t.isLongTask && i('long-task', [t, s], l, u), i(r + 'end', [o, s, f], l, u); - } - } - } - function i(r, n, i, o) { - if (!c || t) { - var a = c; - c = !0; - try { - e.emit(r, n, i, t, o); - } catch (t) { - d([t, r, n, i], e); - } - c = a; - } - } - } - function d(e, t) { - t || (t = n.ee); - try { - t.emit('internal-error', e); - } catch (e) {} - } - function l(e) { - return !(e && 'function' == typeof e && e.apply && !e[o]); - } - }, - 3496: (e, t, r) => { - 'use strict'; - function n(e) { - return !e || !(!e.licenseKey || !e.applicationID); - } - function i(e, t) { - return !e || (e.licenseKey === t.info.licenseKey && e.applicationID === t.info.applicationID); - } - r.d(t, { A: () => i, I: () => n }); - }, - 3606: (e, t, r) => { - 'use strict'; - r.d(t, { i: () => o }); - var n = r(9908); - o.on = a; - var i = (o.handlers = {}); - function o(e, t, r, o) { - a(o || n.d, i, e, t, r); - } - function a(e, t, r, i, o) { - o || (o = 'feature'), e || (e = n.d); - var a = (t[o] = t[o] || {}); - (a[r] = a[r] || []).push([e, i]); - } - }, - 3738: (e, t, r) => { - 'use strict'; - r.d(t, { - He: () => i, - Kp: () => s, - Lc: () => u, - Rz: () => d, - TZ: () => n, - bD: () => o, - d3: () => a, - jx: () => l, - sl: () => f, - uP: () => c, - }); - const n = r(860).K7.sessionTrace, - i = 'bstResource', - o = 'resource', - a = '-start', - s = '-end', - c = 'fn' + a, - u = 'fn' + s, - d = 'pushState', - l = 1e3, - f = 3e4; - }, - 3785: (e, t, r) => { - 'use strict'; - r.d(t, { R: () => c, b: () => u }); - var n = r(9908), - i = r(1863), - o = r(860), - a = r(8154), - s = r(993); - function c(e, t, r = {}, c = s.p_.INFO, u, d = (0, i.t)()) { - (0, n.p)(a.xV, ['API/logging/'.concat(c.toLowerCase(), '/called')], void 0, o.K7.metrics, e), - (0, n.p)(s.ET, [d, t, r, c, u], void 0, o.K7.logging, e); - } - function u(e) { - return 'string' == typeof e && Object.values(s.p_).some((t) => t === e.toUpperCase().trim()); - } - }, - 3878: (e, t, r) => { - 'use strict'; - function n(e, t) { - return { capture: e, passive: !1, signal: t }; - } - function i(e, t, r = !1, i) { - window.addEventListener(e, t, n(r, i)); - } - function o(e, t, r = !1, i) { - document.addEventListener(e, t, n(r, i)); - } - r.d(t, { DD: () => o, jT: () => n, sp: () => i }); - }, - 3962: (e, t, r) => { - 'use strict'; - r.d(t, { - AM: () => a, - O2: () => l, - OV: () => o, - Qu: () => f, - TZ: () => c, - ih: () => h, - pP: () => s, - t1: () => d, - tC: () => i, - wD: () => u, - }); - var n = r(860); - const i = ['click', 'keydown', 'submit'], - o = 'popstate', - a = 'api', - s = 'initialPageLoad', - c = n.K7.softNav, - u = 5e3, - d = 500, - l = { INITIAL_PAGE_LOAD: '', ROUTE_CHANGE: 1, UNSPECIFIED: 2 }, - f = { INTERACTION: 1, AJAX: 2, CUSTOM_END: 3, CUSTOM_TRACER: 4 }, - h = { IP: 'in progress', PF: 'pending finish', FIN: 'finished', CAN: 'cancelled' }; - }, - 4234: (e, t, r) => { - 'use strict'; - r.d(t, { W: () => o }); - var n = r(7836), - i = r(1687); - class o { - constructor(e, t) { - (this.agentIdentifier = e), (this.ee = n.ee.get(e)), (this.featureName = t), (this.blocked = !1); - } - deregisterDrain() { - (0, i.x3)(this.agentIdentifier, this.featureName); - } - } - }, - 4261: (e, t, r) => { - 'use strict'; - r.d(t, { - $9: () => d, - BL: () => c, - CH: () => p, - Dl: () => R, - Fw: () => w, - PA: () => v, - Pl: () => n, - Tb: () => f, - U2: () => a, - V1: () => E, - Wb: () => T, - bt: () => y, - cD: () => b, - d3: () => x, - dT: () => u, - eY: () => g, - fF: () => h, - hG: () => o, - hw: () => i, - k6: () => s, - nb: () => m, - o5: () => l, - }); - const n = 'api-', - i = n + 'ixn-', - o = 'addPageAction', - a = 'addToTrace', - s = 'addRelease', - c = 'finished', - u = 'interaction', - d = 'log', - l = 'noticeError', - f = 'pauseReplay', - h = 'recordCustomEvent', - p = 'recordReplay', - g = 'register', - m = 'setApplicationVersion', - v = 'setCurrentRouteName', - b = 'setCustomAttribute', - y = 'setErrorHandler', - w = 'setPageViewName', - R = 'setUserId', - x = 'start', - T = 'wrapLogger', - E = 'measure'; - }, - 5205: (e, t, r) => { - 'use strict'; - r.d(t, { j: () => O }); - var n = r(384), - i = r(1741); - var o = r(2555), - a = r(3333); - const s = (e) => { - if (!e || 'string' != typeof e) return !1; - try { - document.createDocumentFragment().querySelector(e); - } catch { - return !1; - } - return !0; - }; - var c = r(2614), - u = r(944), - d = r(8122); - const l = '[data-nr-mask]', - f = (e) => - (0, d.a)( - e, - (() => { - const e = { - feature_flags: [], - experimental: { resources: !1 }, - mask_selector: '*', - block_selector: '[data-nr-block]', - mask_input_options: { - color: !1, - date: !1, - 'datetime-local': !1, - email: !1, - month: !1, - number: !1, - range: !1, - search: !1, - tel: !1, - text: !1, - time: !1, - url: !1, - week: !1, - textarea: !1, - select: !1, - password: !0, - }, - }; - return { - ajax: { deny_list: void 0, block_internal: !0, enabled: !0, autoStart: !0 }, - api: { allow_registered_children: !0, duplicate_registered_data: !1 }, - distributed_tracing: { - enabled: void 0, - exclude_newrelic_header: void 0, - cors_use_newrelic_header: void 0, - cors_use_tracecontext_headers: void 0, - allowed_origins: void 0, - }, - get feature_flags() { - return e.feature_flags; - }, - set feature_flags(t) { - e.feature_flags = t; - }, - generic_events: { enabled: !0, autoStart: !0 }, - harvest: { interval: 30 }, - jserrors: { enabled: !0, autoStart: !0 }, - logging: { enabled: !0, autoStart: !0 }, - metrics: { enabled: !0, autoStart: !0 }, - obfuscate: void 0, - page_action: { enabled: !0 }, - page_view_event: { enabled: !0, autoStart: !0 }, - page_view_timing: { enabled: !0, autoStart: !0 }, - performance: { - capture_marks: !1, - capture_measures: !1, - capture_detail: !0, - resources: { - get enabled() { - return e.feature_flags.includes(a.$v.RESOURCES) || e.experimental.resources; - }, - set enabled(t) { - e.experimental.resources = t; - }, - asset_types: [], - first_party_domains: [], - ignore_newrelic: !0, - }, - }, - privacy: { cookies_enabled: !0 }, - proxy: { assets: void 0, beacon: void 0 }, - session: { expiresMs: c.wk, inactiveMs: c.BB }, - session_replay: { - autoStart: !0, - enabled: !1, - preload: !1, - sampling_rate: 10, - error_sampling_rate: 100, - collect_fonts: !1, - inline_images: !1, - fix_stylesheets: !0, - mask_all_inputs: !0, - get mask_text_selector() { - return e.mask_selector; - }, - set mask_text_selector(t) { - s(t) - ? (e.mask_selector = ''.concat(t, ',').concat(l)) - : '' === t || null === t - ? (e.mask_selector = l) - : (0, u.R)(5, t); - }, - get block_class() { - return 'nr-block'; - }, - get ignore_class() { - return 'nr-ignore'; - }, - get mask_text_class() { - return 'nr-mask'; - }, - get block_selector() { - return e.block_selector; - }, - set block_selector(t) { - s(t) ? (e.block_selector += ','.concat(t)) : '' !== t && (0, u.R)(6, t); - }, - get mask_input_options() { - return e.mask_input_options; - }, - set mask_input_options(t) { - t && 'object' == typeof t ? (e.mask_input_options = { ...t, password: !0 }) : (0, u.R)(7, t); - }, - }, - session_trace: { enabled: !0, autoStart: !0 }, - soft_navigations: { enabled: !0, autoStart: !0 }, - spa: { enabled: !0, autoStart: !0 }, - ssl: void 0, - user_actions: { enabled: !0, elementAttributes: ['id', 'className', 'tagName', 'type'] }, - }; - })() - ); - var h = r(6154), - p = r(9324); - let g = 0; - const m = { buildEnv: p.F3, distMethod: p.Xs, version: p.xv, originTime: h.WN }, - v = { - appMetadata: {}, - customTransaction: void 0, - denyList: void 0, - disabled: !1, - entityManager: void 0, - harvester: void 0, - isolatedBacklog: !1, - isRecording: !1, - loaderType: void 0, - maxBytes: 3e4, - obfuscator: void 0, - onerror: void 0, - ptid: void 0, - releaseIds: {}, - session: void 0, - timeKeeper: void 0, - jsAttributesMetadata: { bytes: 0 }, - get harvestCount() { - return ++g; - }, - }, - b = (e) => { - const t = (0, d.a)(e, v), - r = Object.keys(m).reduce( - (e, t) => ((e[t] = { value: m[t], writable: !1, configurable: !0, enumerable: !0 }), e), - {} - ); - return Object.defineProperties(t, r); - }; - var y = r(5701); - const w = (e) => { - const t = e.startsWith('http'); - (e += '/'), (r.p = t ? e : 'https://' + e); - }; - var R = r(7836), - x = r(3241); - const T = { - accountID: void 0, - trustKey: void 0, - agentID: void 0, - licenseKey: void 0, - applicationID: void 0, - xpid: void 0, - }, - E = (e) => (0, d.a)(e, T), - A = new Set(); - function O(e, t = {}, r, a) { - let { init: s, info: c, loader_config: u, runtime: d = {}, exposed: l = !0 } = t; - if (!c) { - const e = (0, n.pV)(); - (s = e.init), (c = e.info), (u = e.loader_config); - } - (e.init = f(s || {})), - (e.loader_config = E(u || {})), - (c.jsAttributes ??= {}), - h.bv && (c.jsAttributes.isWorker = !0), - (e.info = (0, o.D)(c)); - const p = e.init, - g = [c.beacon, c.errorBeacon]; - A.has(e.agentIdentifier) || - (p.proxy.assets && (w(p.proxy.assets), g.push(p.proxy.assets)), - p.proxy.beacon && g.push(p.proxy.beacon), - (e.beacons = [...g]), - (function (e) { - const t = (0, n.pV)(); - Object.getOwnPropertyNames(i.W.prototype).forEach((r) => { - const n = i.W.prototype[r]; - if ('function' != typeof n || 'constructor' === n) return; - let o = t[r]; - e[r] && - !1 !== e.exposed && - 'micro-agent' !== e.runtime?.loaderType && - (t[r] = (...t) => { - const n = e[r](...t); - return o ? o(...t) : n; - }); - }); - })(e), - (0, n.US)('activatedFeatures', y.B), - (e.runSoftNavOverSpa &&= !0 === p.soft_navigations.enabled && p.feature_flags.includes('soft_nav'))), - (d.denyList = [...(p.ajax.deny_list || []), ...(p.ajax.block_internal ? g : [])]), - (d.ptid = e.agentIdentifier), - (d.loaderType = r), - (e.runtime = b(d)), - A.has(e.agentIdentifier) || - ((e.ee = R.ee.get(e.agentIdentifier)), - (e.exposed = l), - (0, x.W)({ - agentIdentifier: e.agentIdentifier, - drained: !!y.B?.[e.agentIdentifier], - type: 'lifecycle', - name: 'initialize', - feature: void 0, - data: e.config, - })), - A.add(e.agentIdentifier); - } - }, - 5270: (e, t, r) => { - 'use strict'; - r.d(t, { Aw: () => a, SR: () => o, rF: () => s }); - var n = r(384), - i = r(7767); - function o(e) { - return !!(0, n.dV)().o.MO && (0, i.V)(e) && !0 === e?.session_trace.enabled; - } - function a(e) { - return !0 === e?.session_replay.preload && o(e); - } - function s(e, t) { - try { - if ('string' == typeof t?.type) { - if ('password' === t.type.toLowerCase()) return '*'.repeat(e?.length || 0); - if (void 0 !== t?.dataset?.nrUnmask || t?.classList?.contains('nr-unmask')) return e; - } - } catch (e) {} - return 'string' == typeof e ? e.replace(/[\S]/g, '*') : '*'.repeat(e?.length || 0); - } - }, - 5289: (e, t, r) => { - 'use strict'; - r.d(t, { GG: () => o, Qr: () => s, sB: () => a }); - var n = r(3878); - function i() { - return 'undefined' == typeof document || 'complete' === document.readyState; - } - function o(e, t) { - if (i()) return e(); - (0, n.sp)('load', e, t); - } - function a(e) { - if (i()) return e(); - (0, n.DD)('DOMContentLoaded', e); - } - function s(e) { - if (i()) return e(); - (0, n.sp)('popstate', e); - } - }, - 5607: (e, t, r) => { - 'use strict'; - r.d(t, { W: () => n }); - const n = (0, r(9566).bz)(); - }, - 5701: (e, t, r) => { - 'use strict'; - r.d(t, { B: () => o, t: () => a }); - var n = r(3241); - const i = new Set(), - o = {}; - function a(e, t) { - const r = t.agentIdentifier; - (o[r] ??= {}), - e && - 'object' == typeof e && - (i.has(r) || - (t.ee.emit('rumresp', [e]), - (o[r] = e), - i.add(r), - (0, n.W)({ - agentIdentifier: r, - loaded: !0, - drained: !0, - type: 'lifecycle', - name: 'load', - feature: void 0, - data: e, - }))); - } - }, - 6154: (e, t, r) => { - 'use strict'; - r.d(t, { - A4: () => s, - OF: () => d, - RI: () => i, - WN: () => h, - bv: () => o, - gm: () => a, - lR: () => f, - m: () => u, - mw: () => c, - sb: () => l, - }); - var n = r(1863); - const i = 'undefined' != typeof window && !!window.document, - o = - 'undefined' != typeof WorkerGlobalScope && - (('undefined' != typeof self && - self instanceof WorkerGlobalScope && - self.navigator instanceof WorkerNavigator) || - ('undefined' != typeof globalThis && - globalThis instanceof WorkerGlobalScope && - globalThis.navigator instanceof WorkerNavigator)), - a = i - ? window - : 'undefined' != typeof WorkerGlobalScope && - (('undefined' != typeof self && self instanceof WorkerGlobalScope && self) || - ('undefined' != typeof globalThis && globalThis instanceof WorkerGlobalScope && globalThis)), - s = 'complete' === a?.document?.readyState, - c = Boolean('hidden' === a?.document?.visibilityState), - u = '' + a?.location, - d = /iPad|iPhone|iPod/.test(a.navigator?.userAgent), - l = d && 'undefined' == typeof SharedWorker, - f = (() => { - const e = a.navigator?.userAgent?.match(/Firefox[/\s](\d+\.\d+)/); - return Array.isArray(e) && e.length >= 2 ? +e[1] : 0; - })(), - h = Date.now() - (0, n.t)(); - }, - 6344: (e, t, r) => { - 'use strict'; - r.d(t, { - BB: () => d, - G4: () => o, - Qb: () => l, - TZ: () => i, - Ug: () => a, - _s: () => s, - bc: () => u, - yP: () => c, - }); - var n = r(2614); - const i = r(860).K7.sessionReplay, - o = { RECORD: 'recordReplay', PAUSE: 'pauseReplay', ERROR_DURING_REPLAY: 'errorDuringReplay' }, - a = 0.12, - s = { DomContentLoaded: 0, Load: 1, FullSnapshot: 2, IncrementalSnapshot: 3, Meta: 4, Custom: 5 }, - c = { [n.g.ERROR]: 15e3, [n.g.FULL]: 3e5, [n.g.OFF]: 0 }, - u = { - RESET: { message: 'Session was reset', sm: 'Reset' }, - IMPORT: { message: 'Recorder failed to import', sm: 'Import' }, - TOO_MANY: { message: '429: Too Many Requests', sm: 'Too-Many' }, - TOO_BIG: { message: 'Payload was too large', sm: 'Too-Big' }, - CROSS_TAB: { message: 'Session Entity was set to OFF on another tab', sm: 'Cross-Tab' }, - ENTITLEMENTS: { message: 'Session Replay is not allowed and will not be started', sm: 'Entitlement' }, - }, - d = 5e3, - l = { - API: 'api', - RESUME: 'resume', - SWITCH_TO_FULL: 'switchToFull', - INITIALIZE: 'initialize', - PRELOAD: 'preload', - }; - }, - 6389: (e, t, r) => { - 'use strict'; - function n(e, t = 500, r = {}) { - const n = r?.leading || !1; - let i; - return (...r) => { - n && - void 0 === i && - (e.apply(this, r), - (i = setTimeout(() => { - i = clearTimeout(i); - }, t))), - n || - (clearTimeout(i), - (i = setTimeout(() => { - e.apply(this, r); - }, t))); - }; - } - function i(e) { - let t = !1; - return (...r) => { - t || ((t = !0), e.apply(this, r)); - }; - } - r.d(t, { J: () => i, s: () => n }); - }, - 6630: (e, t, r) => { - 'use strict'; - r.d(t, { T: () => n }); - const n = r(860).K7.pageViewEvent; - }, - 6774: (e, t, r) => { - 'use strict'; - r.d(t, { T: () => n }); - const n = r(860).K7.jserrors; - }, - 7295: (e, t, r) => { - 'use strict'; - r.d(t, { Xv: () => a, gX: () => i, iW: () => o }); - var n = []; - function i(e) { - if (!e || o(e)) return !1; - if (0 === n.length) return !0; - for (var t = 0; t < n.length; t++) { - var r = n[t]; - if ('*' === r.hostname) return !1; - if (s(r.hostname, e.hostname) && c(r.pathname, e.pathname)) return !1; - } - return !0; - } - function o(e) { - return void 0 === e.hostname; - } - function a(e) { - if (((n = []), e && e.length)) - for (var t = 0; t < e.length; t++) { - let r = e[t]; - if (!r) continue; - 0 === r.indexOf('http://') ? (r = r.substring(7)) : 0 === r.indexOf('https://') && (r = r.substring(8)); - const i = r.indexOf('/'); - let o, a; - i > 0 ? ((o = r.substring(0, i)), (a = r.substring(i))) : ((o = r), (a = '')); - let [s] = o.split(':'); - n.push({ hostname: s, pathname: a }); - } - } - function s(e, t) { - return !(e.length > t.length) && t.indexOf(e) === t.length - e.length; - } - function c(e, t) { - return ( - 0 === e.indexOf('/') && (e = e.substring(1)), - 0 === t.indexOf('/') && (t = t.substring(1)), - '' === e || e === t - ); - } - }, - 7378: (e, t, r) => { - 'use strict'; - r.d(t, { - $p: () => x, - BR: () => b, - Kp: () => R, - L3: () => y, - Lc: () => c, - NC: () => o, - SG: () => d, - TZ: () => i, - U6: () => p, - UT: () => m, - d3: () => w, - dT: () => f, - e5: () => E, - gx: () => v, - l9: () => l, - oW: () => h, - op: () => g, - rw: () => u, - tH: () => A, - uP: () => s, - wW: () => T, - xq: () => a, - }); - var n = r(384); - const i = r(860).K7.spa, - o = ['click', 'submit', 'keypress', 'keydown', 'keyup', 'change'], - a = 999, - s = 'fn-start', - c = 'fn-end', - u = 'cb-start', - d = 'api-ixn-', - l = 'remaining', - f = 'interaction', - h = 'spaNode', - p = 'jsonpNode', - g = 'fetch-start', - m = 'fetch-done', - v = 'fetch-body-', - b = 'jsonp-end', - y = (0, n.dV)().o.ST, - w = '-start', - R = '-end', - x = '-body', - T = 'cb' + R, - E = 'jsTime', - A = 'fetch'; - }, - 7485: (e, t, r) => { - 'use strict'; - r.d(t, { D: () => i }); - var n = r(6154); - function i(e) { - if (0 === (e || '').indexOf('data:')) return { protocol: 'data' }; - try { - const t = new URL(e, location.href), - r = { - port: t.port, - hostname: t.hostname, - pathname: t.pathname, - search: t.search, - protocol: t.protocol.slice(0, t.protocol.indexOf(':')), - sameOrigin: t.protocol === n.gm?.location?.protocol && t.host === n.gm?.location?.host, - }; - return ( - (r.port && '' !== r.port) || - ('http:' === t.protocol && (r.port = '80'), 'https:' === t.protocol && (r.port = '443')), - r.pathname && '' !== r.pathname - ? r.pathname.startsWith('/') || (r.pathname = '/'.concat(r.pathname)) - : (r.pathname = '/'), - r - ); - } catch (e) { - return {}; - } - } - }, - 7699: (e, t, r) => { - 'use strict'; - r.d(t, { It: () => i, No: () => n, qh: () => a, uh: () => o }); - const n = 16e3, - i = 1e6, - o = 'NR_CONTAINER_AGENT', - a = 'SESSION_ERROR'; - }, - 7767: (e, t, r) => { - 'use strict'; - r.d(t, { V: () => i }); - var n = r(6154); - const i = (e) => n.RI && !0 === e?.privacy.cookies_enabled; - }, - 7836: (e, t, r) => { - 'use strict'; - r.d(t, { P: () => s, ee: () => c }); - var n = r(384), - i = r(8990), - o = r(2646), - a = r(5607); - const s = 'nr@context:'.concat(a.W), - c = (function e(t, r) { - var n = {}, - a = {}, - d = {}, - l = !1; - try { - l = 16 === r.length && u.initializedAgents?.[r]?.runtime.isolatedBacklog; - } catch (e) {} - var f = { - on: p, - addEventListener: p, - removeEventListener: function (e, t) { - var r = n[e]; - if (!r) return; - for (var i = 0; i < r.length; i++) r[i] === t && r.splice(i, 1); - }, - emit: function (e, r, n, i, o) { - !1 !== o && (o = !0); - if (c.aborted && !i) return; - t && o && t.emit(e, r, n); - var s = h(n); - g(e).forEach((e) => { - e.apply(s, r); - }); - var u = v()[a[e]]; - u && u.push([f, e, r, s]); - return s; - }, - get: m, - listeners: g, - context: h, - buffer: function (e, t) { - const r = v(); - if (((t = t || 'feature'), f.aborted)) return; - Object.entries(e || {}).forEach(([e, n]) => { - (a[n] = t), t in r || (r[t] = []); - }); - }, - abort: function () { - (f._aborted = !0), - Object.keys(f.backlog).forEach((e) => { - delete f.backlog[e]; - }); - }, - isBuffering: function (e) { - return !!v()[a[e]]; - }, - debugId: r, - backlog: l ? {} : t && 'object' == typeof t.backlog ? t.backlog : {}, - isolatedBacklog: l, - }; - return ( - Object.defineProperty(f, 'aborted', { - get: () => { - let e = f._aborted || !1; - return e || (t && (e = t.aborted), e); - }, - }), - f - ); - function h(e) { - return e && e instanceof o.y ? e : e ? (0, i.I)(e, s, () => new o.y(s)) : new o.y(s); - } - function p(e, t) { - n[e] = g(e).concat(t); - } - function g(e) { - return n[e] || []; - } - function m(t) { - return (d[t] = d[t] || e(f, t)); - } - function v() { - return f.backlog; - } - })(void 0, 'globalEE'), - u = (0, n.Zm)(); - u.ee || (u.ee = c); - }, - 8122: (e, t, r) => { - 'use strict'; - r.d(t, { a: () => i }); - var n = r(944); - function i(e, t) { - try { - if (!e || 'object' != typeof e) return (0, n.R)(3); - if (!t || 'object' != typeof t) return (0, n.R)(4); - const r = Object.create(Object.getPrototypeOf(t), Object.getOwnPropertyDescriptors(t)), - o = 0 === Object.keys(r).length ? e : r; - for (let a in o) - if (void 0 !== e[a]) - try { - if (null === e[a]) { - r[a] = null; - continue; - } - Array.isArray(e[a]) && Array.isArray(t[a]) - ? (r[a] = Array.from(new Set([...e[a], ...t[a]]))) - : 'object' == typeof e[a] && 'object' == typeof t[a] - ? (r[a] = i(e[a], t[a])) - : (r[a] = e[a]); - } catch (e) { - r[a] || (0, n.R)(1, e); - } - return r; - } catch (e) { - (0, n.R)(2, e); - } - } - }, - 8139: (e, t, r) => { - 'use strict'; - r.d(t, { u: () => f }); - var n = r(7836), - i = r(3434), - o = r(8990), - a = r(6154); - const s = {}, - c = a.gm.XMLHttpRequest, - u = 'addEventListener', - d = 'removeEventListener', - l = 'nr@wrapped:'.concat(n.P); - function f(e) { - var t = (function (e) { - return (e || n.ee).get('events'); - })(e); - if (s[t.debugId]++) return t; - s[t.debugId] = 1; - var r = (0, i.YM)(t, !0); - function f(e) { - r.inPlace(e, [u, d], '-', p); - } - function p(e, t) { - return e[1]; - } - return ( - 'getPrototypeOf' in Object && (a.RI && h(document, f), c && h(c.prototype, f), h(a.gm, f)), - t.on(u + '-start', function (e, t) { - var n = e[1]; - if (null !== n && ('function' == typeof n || 'object' == typeof n) && 'newrelic' !== e[0]) { - var i = (0, o.I)(n, l, function () { - var e = { - object: function () { - if ('function' != typeof n.handleEvent) return; - return n.handleEvent.apply(n, arguments); - }, - function: n, - }[typeof n]; - return e ? r(e, 'fn-', null, e.name || 'anonymous') : n; - }); - this.wrapped = e[1] = i; - } - }), - t.on(d + '-start', function (e) { - e[1] = this.wrapped || e[1]; - }), - t - ); - } - function h(e, t, ...r) { - let n = e; - for (; 'object' == typeof n && !Object.prototype.hasOwnProperty.call(n, u); ) n = Object.getPrototypeOf(n); - n && t(n, ...r); - } - }, - 8154: (e, t, r) => { - 'use strict'; - r.d(t, { z_: () => o, XG: () => s, TZ: () => n, rs: () => i, xV: () => a }); - r(6154), r(9566), r(384); - const n = r(860).K7.metrics, - i = 'sm', - o = 'cm', - a = 'storeSupportabilityMetrics', - s = 'storeEventMetrics'; - }, - 8374: (e, t, r) => { - r.nc = (() => { - try { - return document?.currentScript?.nonce; - } catch (e) {} - return ''; - })(); - }, - 8990: (e, t, r) => { - 'use strict'; - r.d(t, { I: () => i }); - var n = Object.prototype.hasOwnProperty; - function i(e, t, r) { - if (n.call(e, t)) return e[t]; - var i = r(); - if (Object.defineProperty && Object.keys) - try { - return Object.defineProperty(e, t, { value: i, writable: !0, enumerable: !1 }), i; - } catch (e) {} - return (e[t] = i), i; - } - }, - 9300: (e, t, r) => { - 'use strict'; - r.d(t, { T: () => n }); - const n = r(860).K7.ajax; - }, - 9324: (e, t, r) => { - 'use strict'; - r.d(t, { AJ: () => a, F3: () => i, Xs: () => o, Yq: () => s, xv: () => n }); - const n = '1.301.0', - i = 'PROD', - o = 'CDN', - a = '@newrelic/rrweb', - s = '1.0.1'; - }, - 9566: (e, t, r) => { - 'use strict'; - r.d(t, { LA: () => s, ZF: () => c, bz: () => a, el: () => u }); - var n = r(6154); - const i = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; - function o(e, t) { - return e ? 15 & e[t] : (16 * Math.random()) | 0; - } - function a() { - const e = n.gm?.crypto || n.gm?.msCrypto; - let t, - r = 0; - return ( - e && e.getRandomValues && (t = e.getRandomValues(new Uint8Array(30))), - i - .split('') - .map((e) => ('x' === e ? o(t, r++).toString(16) : 'y' === e ? ((3 & o()) | 8).toString(16) : e)) - .join('') - ); - } - function s(e) { - const t = n.gm?.crypto || n.gm?.msCrypto; - let r, - i = 0; - t && t.getRandomValues && (r = t.getRandomValues(new Uint8Array(e))); - const a = []; - for (var s = 0; s < e; s++) a.push(o(r, i++).toString(16)); - return a.join(''); - } - function c() { - return s(16); - } - function u() { - return s(32); - } - }, - 9908: (e, t, r) => { - 'use strict'; - r.d(t, { d: () => n, p: () => i }); - var n = r(7836).ee.get('handle'); - function i(e, t, r, i, o) { - o ? (o.buffer([e], i), o.emit(e, t, r)) : (n.buffer([e], i), n.emit(e, t, r)); - } - }, - }, - n = {}; - function i(e) { - var t = n[e]; - if (void 0 !== t) return t.exports; - var o = (n[e] = { exports: {} }); - return r[e](o, o.exports, i), o.exports; - } - (i.m = r), - (i.d = (e, t) => { - for (var r in t) i.o(t, r) && !i.o(e, r) && Object.defineProperty(e, r, { enumerable: !0, get: t[r] }); - }), - (i.f = {}), - (i.e = (e) => Promise.all(Object.keys(i.f).reduce((t, r) => (i.f[r](e, t), t), []))), - (i.u = (e) => ({ 212: 'nr-spa-compressor', 249: 'nr-spa-recorder', 478: 'nr-spa' })[e] + '-1.301.0.min.js'), - (i.o = (e, t) => Object.prototype.hasOwnProperty.call(e, t)), - (e = {}), - (t = 'NRBA-1.301.0.PROD:'), - (i.l = (r, n, o, a) => { - if (e[r]) e[r].push(n); - else { - var s, c; - if (void 0 !== o) - for (var u = document.getElementsByTagName('script'), d = 0; d < u.length; d++) { - var l = u[d]; - if (l.getAttribute('src') == r || l.getAttribute('data-webpack') == t + o) { - s = l; - break; - } - } - if (!s) { - c = !0; - var f = { - 478: 'sha512-7qHClSVBtoyiwRvCkgyaF5Ps0RAENoPwjauK0I+0bYyBYefdZVshGSe8JQWh/Wexd7hFuUui5xp+2xn5U1a3ZA==', - 249: 'sha512-KuEP0gQ0mAldYT/AqNp3NW4kVb0kLtfIlXaPSFq4WQRFf8vKVNnSqiafso/bWR75halLwFsgmJtdLJEzntVZoQ==', - 212: 'sha512-fqWEILwVJyfYV9/SedvSjCZ6hDRNjOvwYfN73wxZtahaztcFZ2cr3Ns172tBGIDQeWO25QmSlihZm+awv8ma/w==', - }; - ((s = document.createElement('script')).charset = 'utf-8'), - i.nc && s.setAttribute('nonce', i.nc), - s.setAttribute('data-webpack', t + o), - (s.src = r), - 0 !== s.src.indexOf(window.location.origin + '/') && (s.crossOrigin = 'anonymous'), - f[a] && (s.integrity = f[a]); - } - e[r] = [n]; - var h = (t, n) => { - (s.onerror = s.onload = null), clearTimeout(p); - var i = e[r]; - if ((delete e[r], s.parentNode && s.parentNode.removeChild(s), i && i.forEach((e) => e(n)), t)) return t(n); - }, - p = setTimeout(h.bind(null, void 0, { type: 'timeout', target: s }), 12e4); - (s.onerror = h.bind(null, s.onerror)), (s.onload = h.bind(null, s.onload)), c && document.head.appendChild(s); - } - }), - (i.r = (e) => { - 'undefined' != typeof Symbol && - Symbol.toStringTag && - Object.defineProperty(e, Symbol.toStringTag, { value: 'Module' }), - Object.defineProperty(e, '__esModule', { value: !0 }); - }), - (i.p = 'https://js-agent.newrelic.com/'), - (() => { - var e = { 38: 0, 788: 0 }; - i.f.j = (t, r) => { - var n = i.o(e, t) ? e[t] : void 0; - if (0 !== n) - if (n) r.push(n[2]); - else { - var o = new Promise((r, i) => (n = e[t] = [r, i])); - r.push((n[2] = o)); - var a = i.p + i.u(t), - s = new Error(); - i.l( - a, - (r) => { - if (i.o(e, t) && (0 !== (n = e[t]) && (e[t] = void 0), n)) { - var o = r && ('load' === r.type ? 'missing' : r.type), - a = r && r.target && r.target.src; - (s.message = 'Loading chunk ' + t + ' failed.\n(' + o + ': ' + a + ')'), - (s.name = 'ChunkLoadError'), - (s.type = o), - (s.request = a), - n[1](s); - } - }, - 'chunk-' + t, - t - ); - } - }; - var t = (t, r) => { - var n, - o, - [a, s, c] = r, - u = 0; - if (a.some((t) => 0 !== e[t])) { - for (n in s) i.o(s, n) && (i.m[n] = s[n]); - if (c) c(i); - } - for (t && t(r); u < a.length; u++) (o = a[u]), i.o(e, o) && e[o] && e[o][0](), (e[o] = 0); - }, - r = (self['webpackChunk:NRBA-1.301.0.PROD'] = self['webpackChunk:NRBA-1.301.0.PROD'] || []); - r.forEach(t.bind(null, 0)), (r.push = t.bind(null, r.push.bind(r))); - })(), - (() => { - 'use strict'; - i(8374); - var e = i(9566), - t = i(1741); - class r extends t.W { - agentIdentifier = (0, e.LA)(16); - } - var n = i(860); - const o = Object.values(n.K7); - var a = i(5205); - var s = i(9908), - c = i(1863), - u = i(4261), - d = i(3241), - l = i(944), - f = i(5701), - h = i(8154); - function p(e, t, i, o) { - const a = o || i; - !a || - (a[e] && a[e] !== r.prototype[e]) || - (a[e] = function () { - (0, s.p)(h.xV, ['API/' + e + '/called'], void 0, n.K7.metrics, i.ee), - (0, d.W)({ - agentIdentifier: i.agentIdentifier, - drained: !!f.B?.[i.agentIdentifier], - type: 'data', - name: 'api', - feature: u.Pl + e, - data: {}, - }); - try { - return t.apply(this, arguments); - } catch (e) { - (0, l.R)(23, e); - } - }); - } - function g(e, t, r, n, i) { - const o = e.info; - null === r ? delete o.jsAttributes[t] : (o.jsAttributes[t] = r), - (i || null === r) && (0, s.p)(u.Pl + n, [(0, c.t)(), t, r], void 0, 'session', e.ee); - } - var m = i(1687), - v = i(4234), - b = i(5289), - y = i(6154), - w = i(5270), - R = i(7767), - x = i(6389), - T = i(7699); - class E extends v.W { - constructor(e, t) { - super(e.agentIdentifier, t), - (this.agentRef = e), - (this.abortHandler = void 0), - (this.featAggregate = void 0), - (this.onAggregateImported = void 0), - (this.deferred = Promise.resolve()), - !1 === e.init[this.featureName].autoStart - ? (this.deferred = new Promise((t, r) => { - this.ee.on( - 'manual-start-all', - (0, x.J)(() => { - (0, m.Ak)(e.agentIdentifier, this.featureName), t(); - }) - ); - })) - : (0, m.Ak)(e.agentIdentifier, t); - } - importAggregator(e, t, r = {}) { - if (this.featAggregate) return; - let n; - this.onAggregateImported = new Promise((e) => { - n = e; - }); - const o = async () => { - let o; - await this.deferred; - try { - if ((0, R.V)(e.init)) { - const { setupAgentSession: t } = await i.e(478).then(i.bind(i, 8766)); - o = t(e); - } - } catch (e) { - (0, l.R)(20, e), - this.ee.emit('internal-error', [e]), - (0, s.p)(T.qh, [e], void 0, this.featureName, this.ee); - } - try { - if (!this.#t(this.featureName, o, e.init)) - return (0, m.Ze)(this.agentIdentifier, this.featureName), void n(!1); - const { Aggregate: i } = await t(); - (this.featAggregate = new i(e, r)), - e.runtime.harvester.initializedAggregates.push(this.featAggregate), - n(!0); - } catch (e) { - (0, l.R)(34, e), - this.abortHandler?.(), - (0, m.Ze)(this.agentIdentifier, this.featureName, !0), - n(!1), - this.ee && this.ee.abort(); - } - }; - y.RI ? (0, b.GG)(() => o(), !0) : o(); - } - #t(e, t, r) { - if (this.blocked) return !1; - switch (e) { - case n.K7.sessionReplay: - return (0, w.SR)(r) && !!t; - case n.K7.sessionTrace: - return !!t; - default: - return !0; - } - } - } - var A = i(6630), - O = i(2614); - class S extends E { - static featureName = A.T; - constructor(e) { - var t; - super(e, A.T), - this.setupInspectionEvents(e.agentIdentifier), - (t = e), - p( - u.Fw, - function (e, r) { - 'string' == typeof e && - ('/' !== e.charAt(0) && (e = '/' + e), - (t.runtime.customTransaction = (r || 'http://custom.transaction') + e), - (0, s.p)(u.Pl + u.Fw, [(0, c.t)()], void 0, void 0, t.ee)); - }, - t - ), - this.ee.on('api-send-rum', (e, t) => (0, s.p)('send-rum', [e, t], void 0, this.featureName, this.ee)), - this.importAggregator(e, () => i.e(478).then(i.bind(i, 1983))); - } - setupInspectionEvents(e) { - const t = (t, r) => { - t && - (0, d.W)({ - agentIdentifier: e, - timeStamp: t.timeStamp, - loaded: 'complete' === t.target.readyState, - type: 'window', - name: r, - data: t.target.location + '', - }); - }; - (0, b.sB)((e) => { - t(e, 'DOMContentLoaded'); - }), - (0, b.GG)((e) => { - t(e, 'load'); - }), - (0, b.Qr)((e) => { - t(e, 'navigate'); - }), - this.ee.on(O.tS.UPDATE, (t, r) => { - (0, d.W)({ agentIdentifier: e, type: 'lifecycle', name: 'session', data: r }); - }); - } - } - var N = i(384); - var _ = i(2843), - I = i(3878), - P = i(782); - class j extends E { - static featureName = P.T; - constructor(e) { - super(e, P.T), - y.RI && - ((0, _.u)(() => (0, s.p)('docHidden', [(0, c.t)()], void 0, P.T, this.ee), !0), - (0, I.sp)('pagehide', () => (0, s.p)('winPagehide', [(0, c.t)()], void 0, P.T, this.ee)), - this.importAggregator(e, () => i.e(478).then(i.bind(i, 9917)))); - } - } - class C extends E { - static featureName = h.TZ; - constructor(e) { - super(e, h.TZ), - y.RI && - document.addEventListener('securitypolicyviolation', (e) => { - (0, s.p)(h.xV, ['Generic/CSPViolation/Detected'], void 0, this.featureName, this.ee); - }), - this.importAggregator(e, () => i.e(478).then(i.bind(i, 6555))); - } - } - var k = i(6774), - L = i(3304); - class H { - constructor(e, t, r, n, i) { - (this.name = 'UncaughtError'), - (this.message = 'string' == typeof e ? e : (0, L.A)(e)), - (this.sourceURL = t), - (this.line = r), - (this.column = n), - (this.__newrelic = i); - } - } - function M(e) { - return U(e) - ? e - : new H( - void 0 !== e?.message ? e.message : e, - e?.filename || e?.sourceURL, - e?.lineno || e?.line, - e?.colno || e?.col, - e?.__newrelic, - e?.cause - ); - } - function D(e) { - const t = 'Unhandled Promise Rejection: '; - if (!e?.reason) return; - if (U(e.reason)) { - try { - e.reason.message.startsWith(t) || (e.reason.message = t + e.reason.message); - } catch (e) {} - return M(e.reason); - } - const r = M(e.reason); - return (r.message || '').startsWith(t) || (r.message = t + r.message), r; - } - function K(e) { - if (e.error instanceof SyntaxError && !/:\d+$/.test(e.error.stack?.trim())) { - const t = new H(e.message, e.filename, e.lineno, e.colno, e.error.__newrelic, e.cause); - return (t.name = SyntaxError.name), t; - } - return U(e.error) ? e.error : M(e); - } - function U(e) { - return e instanceof Error && !!e.stack; - } - function F(e, t, r, i, o = (0, c.t)()) { - 'string' == typeof e && (e = new Error(e)), - (0, s.p)('err', [e, o, !1, t, r.runtime.isRecording, void 0, i], void 0, n.K7.jserrors, r.ee), - (0, s.p)('uaErr', [], void 0, n.K7.genericEvents, r.ee); - } - var W = i(3496), - B = i(993), - G = i(3785); - function V(e, { customAttributes: t = {}, level: r = B.p_.INFO } = {}, n, i, o = (0, c.t)()) { - (0, G.R)(n.ee, e, t, r, i, o); - } - function z(e, t, r, i, o = (0, c.t)()) { - (0, s.p)(u.Pl + u.hG, [o, e, t, i], void 0, n.K7.genericEvents, r.ee); - } - function Z(e) { - p( - u.eY, - function (t) { - return (function (e, t) { - const r = {}; - let i, o; - (0, l.R)(54, 'newrelic.register'), e.init.api.allow_registered_children || (i = () => (0, l.R)(55)); - (t && (0, W.I)(t)) || (i = () => (0, l.R)(48, t)); - const a = { - addPageAction: (n, i = {}) => { - u(z, [n, { ...r, ...i }, e], t); - }, - log: (n, i = {}) => { - u(V, [n, { ...i, customAttributes: { ...r, ...(i.customAttributes || {}) } }, e], t); - }, - noticeError: (n, i = {}) => { - u(F, [n, { ...r, ...i }, e], t); - }, - setApplicationVersion: (e) => { - r['application.version'] = e; - }, - setCustomAttribute: (e, t) => { - r[e] = t; - }, - setUserId: (e) => { - r['enduser.id'] = e; - }, - metadata: { - customAttributes: r, - target: t, - get connected() { - return o || Promise.reject(new Error('Failed to connect')); - }, - }, - }; - i - ? i() - : (o = new Promise((n, i) => { - try { - const o = e.runtime?.entityManager; - let s = !!o?.get().entityGuid, - c = o?.getEntityGuidFor(t.licenseKey, t.applicationID), - u = !!c; - if (s && u) (t.entityGuid = c), n(a); - else { - const d = setTimeout(() => i(new Error('Failed to connect - Timeout')), 15e3); - function l(r) { - (0, W.A)(r, e) - ? (s ||= !0) - : t.licenseKey === r.licenseKey && - t.applicationID === r.applicationID && - ((u = !0), (t.entityGuid = r.entityGuid)), - s && u && (clearTimeout(d), e.ee.removeEventListener('entity-added', l), n(a)); - } - e.ee.emit('api-send-rum', [r, t]), e.ee.on('entity-added', l); - } - } catch (f) { - i(f); - } - })); - const u = async (t, r, a) => { - if (i) return i(); - const u = (0, c.t)(); - (0, s.p)(h.xV, ['API/register/'.concat(t.name, '/called')], void 0, n.K7.metrics, e.ee); - try { - await o; - const n = e.init.api.duplicate_registered_data; - (!0 === n || (Array.isArray(n) && n.includes(a.entityGuid))) && t(...r, void 0, u), - t(...r, a.entityGuid, u); - } catch (e) { - (0, l.R)(50, e); - } - }; - return a; - })(e, t); - }, - e - ); - } - class q extends E { - static featureName = k.T; - constructor(e) { - var t; - super(e, k.T), - (t = e), - p(u.o5, (e, r) => F(e, r, t), t), - (function (e) { - p( - u.bt, - function (t) { - e.runtime.onerror = t; - }, - e - ); - })(e), - (function (e) { - let t = 0; - p( - u.k6, - function (e, r) { - ++t > 10 || (this.runtime.releaseIds[e.slice(-200)] = ('' + r).slice(-200)); - }, - e - ); - })(e), - Z(e); - try { - this.removeOnAbort = new AbortController(); - } catch (e) {} - this.ee.on('internal-error', (t, r) => { - this.abortHandler && - (0, s.p)('ierr', [M(t), (0, c.t)(), !0, {}, e.runtime.isRecording, r], void 0, this.featureName, this.ee); - }), - y.gm.addEventListener( - 'unhandledrejection', - (t) => { - this.abortHandler && - (0, s.p)( - 'err', - [D(t), (0, c.t)(), !1, { unhandledPromiseRejection: 1 }, e.runtime.isRecording], - void 0, - this.featureName, - this.ee - ); - }, - (0, I.jT)(!1, this.removeOnAbort?.signal) - ), - y.gm.addEventListener( - 'error', - (t) => { - this.abortHandler && - (0, s.p)('err', [K(t), (0, c.t)(), !1, {}, e.runtime.isRecording], void 0, this.featureName, this.ee); - }, - (0, I.jT)(!1, this.removeOnAbort?.signal) - ), - (this.abortHandler = this.#r), - this.importAggregator(e, () => i.e(478).then(i.bind(i, 2176))); - } - #r() { - this.removeOnAbort?.abort(), (this.abortHandler = void 0); - } - } - var X = i(8990); - let Y = 1; - function Q(e) { - const t = typeof e; - return !e || ('object' !== t && 'function' !== t) - ? -1 - : e === y.gm - ? 0 - : (0, X.I)(e, 'nr@id', function () { - return Y++; - }); - } - function J(e) { - if ('string' == typeof e && e.length) return e.length; - if ('object' == typeof e) { - if ('undefined' != typeof ArrayBuffer && e instanceof ArrayBuffer && e.byteLength) return e.byteLength; - if ('undefined' != typeof Blob && e instanceof Blob && e.size) return e.size; - if (!('undefined' != typeof FormData && e instanceof FormData)) - try { - return (0, L.A)(e).length; - } catch (e) { - return; - } - } - } - var ee = i(8139), - te = i(7836), - re = i(3434); - const ne = {}, - ie = ['open', 'send']; - function oe(e) { - var t = e || te.ee; - const r = (function (e) { - return (e || te.ee).get('xhr'); - })(t); - if (void 0 === y.gm.XMLHttpRequest) return r; - if (ne[r.debugId]++) return r; - (ne[r.debugId] = 1), (0, ee.u)(t); - var n = (0, re.YM)(r), - i = y.gm.XMLHttpRequest, - o = y.gm.MutationObserver, - a = y.gm.Promise, - s = y.gm.setInterval, - c = 'readystatechange', - u = ['onload', 'onerror', 'onabort', 'onloadstart', 'onloadend', 'onprogress', 'ontimeout'], - d = [], - f = (y.gm.XMLHttpRequest = function (e) { - const t = new i(e), - o = r.context(t); - try { - r.emit('new-xhr', [t], o), - t.addEventListener( - c, - ((a = o), - function () { - var e = this; - e.readyState > 3 && !a.resolved && ((a.resolved = !0), r.emit('xhr-resolved', [], e)), - n.inPlace(e, u, 'fn-', b); - }), - (0, I.jT)(!1) - ); - } catch (e) { - (0, l.R)(15, e); - try { - r.emit('internal-error', [e]); - } catch (e) {} - } - var a; - return t; - }); - function h(e, t) { - n.inPlace(t, ['onreadystatechange'], 'fn-', b); - } - if ( - ((function (e, t) { - for (var r in e) t[r] = e[r]; - })(i, f), - (f.prototype = i.prototype), - n.inPlace(f.prototype, ie, '-xhr-', b), - r.on('send-xhr-start', function (e, t) { - h(e, t), - (function (e) { - d.push(e), o && (p ? p.then(v) : s ? s(v) : ((g = -g), (m.data = g))); - })(t); - }), - r.on('open-xhr-start', h), - o) - ) { - var p = a && a.resolve(); - if (!s && !a) { - var g = 1, - m = document.createTextNode(g); - new o(v).observe(m, { characterData: !0 }); - } - } else - t.on('fn-end', function (e) { - (e[0] && e[0].type === c) || v(); - }); - function v() { - for (var e = 0; e < d.length; e++) h(0, d[e]); - d.length && (d = []); - } - function b(e, t) { - return t; - } - return r; - } - var ae = 'fetch-', - se = ae + 'body-', - ce = ['arrayBuffer', 'blob', 'json', 'text', 'formData'], - ue = y.gm.Request, - de = y.gm.Response, - le = 'prototype'; - const fe = {}; - function he(e) { - const t = (function (e) { - return (e || te.ee).get('fetch'); - })(e); - if (!(ue && de && y.gm.fetch)) return t; - if (fe[t.debugId]++) return t; - function r(e, r, n) { - var i = e[r]; - 'function' == typeof i && - (e[r] = function () { - var e, - r = [...arguments], - o = {}; - t.emit(n + 'before-start', [r], o), o[te.P] && o[te.P].dt && (e = o[te.P].dt); - var a = i.apply(this, r); - return ( - t.emit(n + 'start', [r, e], a), - a.then( - function (e) { - return t.emit(n + 'end', [null, e], a), e; - }, - function (e) { - throw (t.emit(n + 'end', [e], a), e); - } - ) - ); - }); - } - return ( - (fe[t.debugId] = 1), - ce.forEach((e) => { - r(ue[le], e, se), r(de[le], e, se); - }), - r(y.gm, 'fetch', ae), - t.on(ae + 'end', function (e, r) { - var n = this; - if (r) { - var i = r.headers.get('content-length'); - null !== i && (n.rxSize = i), t.emit(ae + 'done', [null, r], n); - } else t.emit(ae + 'done', [e], n); - }), - t - ); - } - var pe = i(7485); - class ge { - constructor(e) { - this.agentRef = e; - } - generateTracePayload(t) { - const r = this.agentRef.loader_config; - if (!this.shouldGenerateTrace(t) || !r) return null; - var n = (r.accountID || '').toString() || null, - i = (r.agentID || '').toString() || null, - o = (r.trustKey || '').toString() || null; - if (!n || !i) return null; - var a = (0, e.ZF)(), - s = (0, e.el)(), - c = Date.now(), - u = { spanId: a, traceId: s, timestamp: c }; - return ( - (t.sameOrigin || (this.isAllowedOrigin(t) && this.useTraceContextHeadersForCors())) && - ((u.traceContextParentHeader = this.generateTraceContextParentHeader(a, s)), - (u.traceContextStateHeader = this.generateTraceContextStateHeader(a, c, n, i, o))), - ((t.sameOrigin && !this.excludeNewrelicHeader()) || - (!t.sameOrigin && this.isAllowedOrigin(t) && this.useNewrelicHeaderForCors())) && - (u.newrelicHeader = this.generateTraceHeader(a, s, c, n, i, o)), - u - ); - } - generateTraceContextParentHeader(e, t) { - return '00-' + t + '-' + e + '-01'; - } - generateTraceContextStateHeader(e, t, r, n, i) { - return i + '@nr=0-1-' + r + '-' + n + '-' + e + '----' + t; - } - generateTraceHeader(e, t, r, n, i, o) { - if (!('function' == typeof y.gm?.btoa)) return null; - var a = { v: [0, 1], d: { ty: 'Browser', ac: n, ap: i, id: e, tr: t, ti: r } }; - return o && n !== o && (a.d.tk = o), btoa((0, L.A)(a)); - } - shouldGenerateTrace(e) { - return this.agentRef.init?.distributed_tracing?.enabled && this.isAllowedOrigin(e); - } - isAllowedOrigin(e) { - var t = !1; - const r = this.agentRef.init?.distributed_tracing; - if (e.sameOrigin) t = !0; - else if (r?.allowed_origins instanceof Array) - for (var n = 0; n < r.allowed_origins.length; n++) { - var i = (0, pe.D)(r.allowed_origins[n]); - if (e.hostname === i.hostname && e.protocol === i.protocol && e.port === i.port) { - t = !0; - break; - } - } - return t; - } - excludeNewrelicHeader() { - var e = this.agentRef.init?.distributed_tracing; - return !!e && !!e.exclude_newrelic_header; - } - useNewrelicHeaderForCors() { - var e = this.agentRef.init?.distributed_tracing; - return !!e && !1 !== e.cors_use_newrelic_header; - } - useTraceContextHeadersForCors() { - var e = this.agentRef.init?.distributed_tracing; - return !!e && !!e.cors_use_tracecontext_headers; - } - } - var me = i(9300), - ve = i(7295); - function be(e) { - return 'string' == typeof e - ? e - : e instanceof (0, N.dV)().o.REQ - ? e.url - : y.gm?.URL && e instanceof URL - ? e.href - : void 0; - } - var ye = ['load', 'error', 'abort', 'timeout'], - we = ye.length, - Re = (0, N.dV)().o.REQ, - xe = (0, N.dV)().o.XHR; - const Te = 'X-NewRelic-App-Data'; - class Ee extends E { - static featureName = me.T; - constructor(e) { - super(e, me.T), (this.dt = new ge(e)), (this.handler = (e, t, r, n) => (0, s.p)(e, t, r, n, this.ee)); - try { - const e = { xmlhttprequest: 'xhr', fetch: 'fetch', beacon: 'beacon' }; - y.gm?.performance?.getEntriesByType('resource').forEach((t) => { - if (t.initiatorType in e && 0 !== t.responseStatus) { - const r = { status: t.responseStatus }, - i = { rxSize: t.transferSize, duration: Math.floor(t.duration), cbTime: 0 }; - Ae(r, t.name), - this.handler('xhr', [r, i, t.startTime, t.responseEnd, e[t.initiatorType]], void 0, n.K7.ajax); - } - }); - } catch (e) {} - he(this.ee), - oe(this.ee), - (function (e, t, r, i) { - function o(e) { - var t = this; - (t.totalCbs = 0), - (t.called = 0), - (t.cbTime = 0), - (t.end = E), - (t.ended = !1), - (t.xhrGuids = {}), - (t.lastSize = null), - (t.loadCaptureCalled = !1), - (t.params = this.params || {}), - (t.metrics = this.metrics || {}), - (t.latestLongtaskEnd = 0), - e.addEventListener( - 'load', - function (r) { - A(t, e); - }, - (0, I.jT)(!1) - ), - y.lR || - e.addEventListener( - 'progress', - function (e) { - t.lastSize = e.loaded; - }, - (0, I.jT)(!1) - ); - } - function a(e) { - (this.params = { method: e[0] }), Ae(this, e[1]), (this.metrics = {}); - } - function u(t, r) { - e.loader_config.xpid && this.sameOrigin && r.setRequestHeader('X-NewRelic-ID', e.loader_config.xpid); - var n = i.generateTracePayload(this.parsedOrigin); - if (n) { - var o = !1; - n.newrelicHeader && (r.setRequestHeader('newrelic', n.newrelicHeader), (o = !0)), - n.traceContextParentHeader && - (r.setRequestHeader('traceparent', n.traceContextParentHeader), - n.traceContextStateHeader && r.setRequestHeader('tracestate', n.traceContextStateHeader), - (o = !0)), - o && (this.dt = n); - } - } - function d(e, r) { - var n = this.metrics, - i = e[0], - o = this; - if (n && i) { - var a = J(i); - a && (n.txSize = a); - } - (this.startTime = (0, c.t)()), - (this.body = i), - (this.listener = function (e) { - try { - 'abort' !== e.type || o.loadCaptureCalled || (o.params.aborted = !0), - ('load' !== e.type || - (o.called === o.totalCbs && - (o.onloadCalled || 'function' != typeof r.onload) && - 'function' == typeof o.end)) && - o.end(r); - } catch (e) { - try { - t.emit('internal-error', [e]); - } catch (e) {} - } - }); - for (var s = 0; s < we; s++) r.addEventListener(ye[s], this.listener, (0, I.jT)(!1)); - } - function l(e, t, r) { - (this.cbTime += e), - t ? (this.onloadCalled = !0) : (this.called += 1), - this.called !== this.totalCbs || - (!this.onloadCalled && 'function' == typeof r.onload) || - 'function' != typeof this.end || - this.end(r); - } - function f(e, t) { - var r = '' + Q(e) + !!t; - this.xhrGuids && !this.xhrGuids[r] && ((this.xhrGuids[r] = !0), (this.totalCbs += 1)); - } - function p(e, t) { - var r = '' + Q(e) + !!t; - this.xhrGuids && this.xhrGuids[r] && (delete this.xhrGuids[r], (this.totalCbs -= 1)); - } - function g() { - this.endTime = (0, c.t)(); - } - function m(e, r) { - r instanceof xe && 'load' === e[0] && t.emit('xhr-load-added', [e[1], e[2]], r); - } - function v(e, r) { - r instanceof xe && 'load' === e[0] && t.emit('xhr-load-removed', [e[1], e[2]], r); - } - function b(e, t, r) { - t instanceof xe && - ('onload' === r && (this.onload = !0), - ('load' === (e[0] && e[0].type) || this.onload) && (this.xhrCbStart = (0, c.t)())); - } - function w(e, r) { - this.xhrCbStart && t.emit('xhr-cb-time', [(0, c.t)() - this.xhrCbStart, this.onload, r], r); - } - function R(e) { - var t, - r = e[1] || {}; - if ( - ('string' == typeof e[0] - ? 0 === (t = e[0]).length && y.RI && (t = '' + y.gm.location.href) - : e[0] && e[0].url - ? (t = e[0].url) - : y.gm?.URL && e[0] && e[0] instanceof URL - ? (t = e[0].href) - : 'function' == typeof e[0].toString && (t = e[0].toString()), - 'string' == typeof t && 0 !== t.length) - ) { - t && ((this.parsedOrigin = (0, pe.D)(t)), (this.sameOrigin = this.parsedOrigin.sameOrigin)); - var n = i.generateTracePayload(this.parsedOrigin); - if (n && (n.newrelicHeader || n.traceContextParentHeader)) - if (e[0] && e[0].headers) s(e[0].headers, n) && (this.dt = n); - else { - var o = {}; - for (var a in r) o[a] = r[a]; - (o.headers = new Headers(r.headers || {})), - s(o.headers, n) && (this.dt = n), - e.length > 1 ? (e[1] = o) : e.push(o); - } - } - function s(e, t) { - var r = !1; - return ( - t.newrelicHeader && (e.set('newrelic', t.newrelicHeader), (r = !0)), - t.traceContextParentHeader && - (e.set('traceparent', t.traceContextParentHeader), - t.traceContextStateHeader && e.set('tracestate', t.traceContextStateHeader), - (r = !0)), - r - ); - } - } - function x(e, t) { - (this.params = {}), - (this.metrics = {}), - (this.startTime = (0, c.t)()), - (this.dt = t), - e.length >= 1 && (this.target = e[0]), - e.length >= 2 && (this.opts = e[1]); - var r = this.opts || {}, - n = this.target; - Ae(this, be(n)); - var i = ('' + ((n && n instanceof Re && n.method) || r.method || 'GET')).toUpperCase(); - (this.params.method = i), (this.body = r.body), (this.txSize = J(r.body) || 0); - } - function T(e, t) { - if (((this.endTime = (0, c.t)()), this.params || (this.params = {}), (0, ve.iW)(this.params))) return; - let i; - (this.params.status = t ? t.status : 0), - 'string' == typeof this.rxSize && this.rxSize.length > 0 && (i = +this.rxSize); - const o = { txSize: this.txSize, rxSize: i, duration: (0, c.t)() - this.startTime }; - r('xhr', [this.params, o, this.startTime, this.endTime, 'fetch'], this, n.K7.ajax); - } - function E(e) { - const t = this.params, - i = this.metrics; - if (!this.ended) { - this.ended = !0; - for (let t = 0; t < we; t++) e.removeEventListener(ye[t], this.listener, !1); - t.aborted || - (0, ve.iW)(t) || - ((i.duration = (0, c.t)() - this.startTime), - this.loadCaptureCalled || 4 !== e.readyState ? null == t.status && (t.status = 0) : A(this, e), - (i.cbTime = this.cbTime), - r('xhr', [t, i, this.startTime, this.endTime, 'xhr'], this, n.K7.ajax)); - } - } - function A(e, r) { - e.params.status = r.status; - var i = (function (e, t) { - var r = e.responseType; - return 'json' === r && null !== t - ? t - : 'arraybuffer' === r || 'blob' === r || 'json' === r - ? J(e.response) - : 'text' === r || '' === r || void 0 === r - ? J(e.responseText) - : void 0; - })(r, e.lastSize); - if ((i && (e.metrics.rxSize = i), e.sameOrigin && r.getAllResponseHeaders().indexOf(Te) >= 0)) { - var o = r.getResponseHeader(Te); - o && - ((0, s.p)(h.rs, ['Ajax/CrossApplicationTracing/Header/Seen'], void 0, n.K7.metrics, t), - (e.params.cat = o.split(', ').pop())); - } - e.loadCaptureCalled = !0; - } - t.on('new-xhr', o), - t.on('open-xhr-start', a), - t.on('open-xhr-end', u), - t.on('send-xhr-start', d), - t.on('xhr-cb-time', l), - t.on('xhr-load-added', f), - t.on('xhr-load-removed', p), - t.on('xhr-resolved', g), - t.on('addEventListener-end', m), - t.on('removeEventListener-end', v), - t.on('fn-end', w), - t.on('fetch-before-start', R), - t.on('fetch-start', x), - t.on('fn-start', b), - t.on('fetch-done', T); - })(e, this.ee, this.handler, this.dt), - this.importAggregator(e, () => i.e(478).then(i.bind(i, 3845))); - } - } - function Ae(e, t) { - var r = (0, pe.D)(t), - n = e.params || e; - (n.hostname = r.hostname), - (n.port = r.port), - (n.protocol = r.protocol), - (n.host = r.hostname + ':' + r.port), - (n.pathname = r.pathname), - (e.parsedOrigin = r), - (e.sameOrigin = r.sameOrigin); - } - const Oe = {}, - Se = ['pushState', 'replaceState']; - function Ne(e) { - const t = (function (e) { - return (e || te.ee).get('history'); - })(e); - return !y.RI || Oe[t.debugId]++ || ((Oe[t.debugId] = 1), (0, re.YM)(t).inPlace(window.history, Se, '-')), t; - } - var _e = i(3738); - function Ie(e) { - p( - u.BL, - function (t = Date.now()) { - const r = t - y.WN; - r < 0 && (0, l.R)(62, t), - (0, s.p)(h.XG, [u.BL, { time: r }], void 0, n.K7.metrics, e.ee), - e.addToTrace({ name: u.BL, start: t, origin: 'nr' }), - (0, s.p)(u.Pl + u.hG, [r, u.BL], void 0, n.K7.genericEvents, e.ee); - }, - e - ); - } - const { He: Pe, bD: je, d3: Ce, Kp: ke, TZ: Le, Lc: He, uP: Me, Rz: De } = _e; - class Ke extends E { - static featureName = Le; - constructor(e) { - var t; - super(e, Le), - (t = e), - p( - u.U2, - function (e) { - if (!(e && 'object' == typeof e && e.name && e.start)) return; - const r = { n: e.name, s: e.start - y.WN, e: (e.end || e.start) - y.WN, o: e.origin || '', t: 'api' }; - r.s < 0 || r.e < 0 || r.e < r.s - ? (0, l.R)(61, { start: r.s, end: r.e }) - : (0, s.p)('bstApi', [r], void 0, n.K7.sessionTrace, t.ee); - }, - t - ), - Ie(e); - if (!(0, R.V)(e.init)) return void this.deregisterDrain(); - const r = this.ee; - let o; - Ne(r), - (this.eventsEE = (0, ee.u)(r)), - this.eventsEE.on(Me, function (e, t) { - this.bstStart = (0, c.t)(); - }), - this.eventsEE.on(He, function (e, t) { - (0, s.p)('bst', [e[0], t, this.bstStart, (0, c.t)()], void 0, n.K7.sessionTrace, r); - }), - r.on(De + Ce, function (e) { - (this.time = (0, c.t)()), (this.startPath = location.pathname + location.hash); - }), - r.on(De + ke, function (e) { - (0, s.p)( - 'bstHist', - [location.pathname + location.hash, this.startPath, this.time], - void 0, - n.K7.sessionTrace, - r - ); - }); - try { - (o = new PerformanceObserver((e) => { - const t = e.getEntries(); - (0, s.p)(Pe, [t], void 0, n.K7.sessionTrace, r); - })), - o.observe({ type: je, buffered: !0 }); - } catch (e) {} - this.importAggregator(e, () => i.e(478).then(i.bind(i, 6974)), { resourceObserver: o }); - } - } - var Ue = i(6344); - class Fe extends E { - static featureName = Ue.TZ; - #n; - recorder; - constructor(e) { - var t; - let r; - super(e, Ue.TZ), - (t = e), - p( - u.CH, - function () { - (0, s.p)(u.CH, [], void 0, n.K7.sessionReplay, t.ee); - }, - t - ), - (function (e) { - p( - u.Tb, - function () { - (0, s.p)(u.Tb, [], void 0, n.K7.sessionReplay, e.ee); - }, - e - ); - })(e); - try { - r = JSON.parse(localStorage.getItem(''.concat(O.H3, '_').concat(O.uh))); - } catch (e) {} - (0, w.SR)(e.init) && this.ee.on(Ue.G4.RECORD, () => this.#i()), - this.#o(r) && - this.importRecorder().then((e) => { - e.startRecording(Ue.Qb.PRELOAD, r?.sessionReplayMode); - }), - this.importAggregator(this.agentRef, () => i.e(478).then(i.bind(i, 6167)), this), - this.ee.on('err', (e) => { - this.blocked || - (this.agentRef.runtime.isRecording && - ((this.errorNoticed = !0), - (0, s.p)(Ue.G4.ERROR_DURING_REPLAY, [e], void 0, this.featureName, this.ee))); - }); - } - #o(e) { - return ( - (e && (e.sessionReplayMode === O.g.FULL || e.sessionReplayMode === O.g.ERROR)) || - (0, w.Aw)(this.agentRef.init) - ); - } - importRecorder() { - return this.recorder - ? Promise.resolve(this.recorder) - : ((this.#n ??= Promise.all([i.e(478), i.e(249)]) - .then(i.bind(i, 4866)) - .then(({ Recorder: e }) => ((this.recorder = new e(this)), this.recorder)) - .catch((e) => { - throw (this.ee.emit('internal-error', [e]), (this.blocked = !0), e); - })), - this.#n); - } - #i() { - this.blocked || - (this.featAggregate - ? this.featAggregate.mode !== O.g.FULL && this.featAggregate.initializeRecording(O.g.FULL, !0, Ue.Qb.API) - : this.importRecorder().then(() => { - this.recorder.startRecording(Ue.Qb.API, O.g.FULL); - })); - } - } - var We = i(3962); - function Be(e) { - const t = e.ee.get('tracer'); - function r() {} - p( - u.dT, - function (e) { - return new r().get('object' == typeof e ? e : {}); - }, - e - ); - const i = (r.prototype = { - createTracer: function (r, i) { - var o = {}, - a = this, - d = 'function' == typeof i; - return ( - (0, s.p)(h.xV, ['API/createTracer/called'], void 0, n.K7.metrics, e.ee), - e.runSoftNavOverSpa || (0, s.p)(u.hw + 'tracer', [(0, c.t)(), r, o], a, n.K7.spa, e.ee), - function () { - if ((t.emit((d ? '' : 'no-') + 'fn-start', [(0, c.t)(), a, d], o), d)) - try { - return i.apply(this, arguments); - } catch (e) { - const r = 'string' == typeof e ? new Error(e) : e; - throw (t.emit('fn-err', [arguments, this, r], o), r); - } finally { - t.emit('fn-end', [(0, c.t)()], o); - } - } - ); - }, - }); - ['actionText', 'setName', 'setAttribute', 'save', 'ignore', 'onEnd', 'getContext', 'end', 'get'].forEach( - (t) => { - p.apply(this, [ - t, - function () { - return ( - (0, s.p)( - u.hw + t, - [(0, c.t)(), ...arguments], - this, - e.runSoftNavOverSpa ? n.K7.softNav : n.K7.spa, - e.ee - ), - this - ); - }, - e, - i, - ]); - } - ), - p( - u.PA, - function () { - e.runSoftNavOverSpa - ? (0, s.p)(u.hw + 'routeName', [performance.now(), ...arguments], void 0, n.K7.softNav, e.ee) - : (0, s.p)(u.Pl + 'routeName', [(0, c.t)(), ...arguments], this, n.K7.spa, e.ee); - }, - e - ); - } - class Ge extends E { - static featureName = We.TZ; - constructor(e) { - if ((super(e, We.TZ), Be(e), !y.RI || !(0, N.dV)().o.MO)) return; - const t = Ne(this.ee); - try { - this.removeOnAbort = new AbortController(); - } catch (e) {} - We.tC.forEach((e) => { - (0, I.sp)( - e, - (e) => { - a(e); - }, - !0, - this.removeOnAbort?.signal - ); - }); - const r = () => (0, s.p)('newURL', [(0, c.t)(), '' + window.location], void 0, this.featureName, this.ee); - t.on('pushState-end', r), - t.on('replaceState-end', r), - (0, I.sp)( - We.OV, - (e) => { - a(e), (0, s.p)('newURL', [e.timeStamp, '' + window.location], void 0, this.featureName, this.ee); - }, - !0, - this.removeOnAbort?.signal - ); - let n = !1; - const o = new ((0, N.dV)().o.MO)((e, t) => { - n || - ((n = !0), - requestAnimationFrame(() => { - (0, s.p)('newDom', [(0, c.t)()], void 0, this.featureName, this.ee), (n = !1); - })); - }), - a = (0, x.s)( - (e) => { - (0, s.p)('newUIEvent', [e], void 0, this.featureName, this.ee), - o.observe(document.body, { attributes: !0, childList: !0, subtree: !0, characterData: !0 }); - }, - 100, - { leading: !0 } - ); - (this.abortHandler = function () { - this.removeOnAbort?.abort(), o.disconnect(), (this.abortHandler = void 0); - }), - this.importAggregator(e, () => i.e(478).then(i.bind(i, 4393)), { domObserver: o }); - } - } - var Ve = i(7378); - const ze = {}, - Ze = ['appendChild', 'insertBefore', 'replaceChild']; - function qe(e) { - const t = (function (e) { - return (e || te.ee).get('jsonp'); - })(e); - if (!y.RI || ze[t.debugId]) return t; - ze[t.debugId] = !0; - var r = (0, re.YM)(t), - n = /[?&](?:callback|cb)=([^&#]+)/, - i = /(.*)\.([^.]+)/, - o = /^(\w+)(\.|$)(.*)$/; - function a(e, t) { - if (!e) return t; - const r = e.match(o), - n = r[1]; - return a(r[3], t[n]); - } - return ( - r.inPlace(Node.prototype, Ze, 'dom-'), - t.on('dom-start', function (e) { - !(function (e) { - if (!e || 'string' != typeof e.nodeName || 'script' !== e.nodeName.toLowerCase()) return; - if ('function' != typeof e.addEventListener) return; - var o = ((s = e.src), (c = s.match(n)), c ? c[1] : null); - var s, c; - if (!o) return; - var u = (function (e) { - var t = e.match(i); - if (t && t.length >= 3) return { key: t[2], parent: a(t[1], window) }; - return { key: e, parent: window }; - })(o); - if ('function' != typeof u.parent[u.key]) return; - var d = {}; - function l() { - t.emit('jsonp-end', [], d), - e.removeEventListener('load', l, (0, I.jT)(!1)), - e.removeEventListener('error', f, (0, I.jT)(!1)); - } - function f() { - t.emit('jsonp-error', [], d), - t.emit('jsonp-end', [], d), - e.removeEventListener('load', l, (0, I.jT)(!1)), - e.removeEventListener('error', f, (0, I.jT)(!1)); - } - r.inPlace(u.parent, [u.key], 'cb-', d), - e.addEventListener('load', l, (0, I.jT)(!1)), - e.addEventListener('error', f, (0, I.jT)(!1)), - t.emit('new-jsonp', [e.src], d); - })(e[0]); - }), - t - ); - } - const Xe = {}; - function Ye(e) { - const t = (function (e) { - return (e || te.ee).get('promise'); - })(e); - if (Xe[t.debugId]) return t; - Xe[t.debugId] = !0; - var r = t.context, - n = (0, re.YM)(t), - i = y.gm.Promise; - return ( - i && - (function () { - function e(r) { - var o = t.context(), - a = n(r, 'executor-', o, null, !1); - const s = Reflect.construct(i, [a], e); - return ( - (t.context(s).getCtx = function () { - return o; - }), - s - ); - } - (y.gm.Promise = e), - Object.defineProperty(e, 'name', { value: 'Promise' }), - (e.toString = function () { - return i.toString(); - }), - Object.setPrototypeOf(e, i), - ['all', 'race'].forEach(function (r) { - const n = i[r]; - e[r] = function (e) { - let i = !1; - [...(e || [])].forEach((e) => { - this.resolve(e).then(a('all' === r), a(!1)); - }); - const o = n.apply(this, arguments); - return o; - function a(e) { - return function () { - t.emit('propagate', [null, !i], o, !1, !1), (i = i || !e); - }; - } - }; - }), - ['resolve', 'reject'].forEach(function (r) { - const n = i[r]; - e[r] = function (e) { - const r = n.apply(this, arguments); - return e !== r && t.emit('propagate', [e, !0], r, !1, !1), r; - }; - }), - (e.prototype = i.prototype); - const o = i.prototype.then; - (i.prototype.then = function (...e) { - var i = this, - a = r(i); - (a.promise = i), (e[0] = n(e[0], 'cb-', a, null, !1)), (e[1] = n(e[1], 'cb-', a, null, !1)); - const s = o.apply(this, e); - return (a.nextPromise = s), t.emit('propagate', [i, !0], s, !1, !1), s; - }), - (i.prototype.then[re.Jt] = o), - t.on('executor-start', function (e) { - (e[0] = n(e[0], 'resolve-', this, null, !1)), (e[1] = n(e[1], 'resolve-', this, null, !1)); - }), - t.on('executor-err', function (e, t, r) { - e[1](r); - }), - t.on('cb-end', function (e, r, n) { - t.emit('propagate', [n, !0], this.nextPromise, !1, !1); - }), - t.on('propagate', function (e, r, n) { - (this.getCtx && !r) || - (this.getCtx = function () { - if (e instanceof Promise) var r = t.context(e); - return r && r.getCtx ? r.getCtx() : this; - }); - }); - })(), - t - ); - } - const Qe = {}, - $e = 'setTimeout', - Je = 'setInterval', - et = 'clearTimeout', - tt = '-start', - rt = [$e, 'setImmediate', Je, et, 'clearImmediate']; - function nt(e) { - const t = (function (e) { - return (e || te.ee).get('timer'); - })(e); - if (Qe[t.debugId]++) return t; - Qe[t.debugId] = 1; - var r = (0, re.YM)(t); - return ( - r.inPlace(y.gm, rt.slice(0, 2), $e + '-'), - r.inPlace(y.gm, rt.slice(2, 3), Je + '-'), - r.inPlace(y.gm, rt.slice(3), et + '-'), - t.on(Je + tt, function (e, t, n) { - e[0] = r(e[0], 'fn-', null, n); - }), - t.on($e + tt, function (e, t, n) { - (this.method = n), (this.timerDuration = isNaN(e[1]) ? 0 : +e[1]), (e[0] = r(e[0], 'fn-', this, n)); - }), - t - ); - } - const it = {}; - function ot(e) { - const t = (function (e) { - return (e || te.ee).get('mutation'); - })(e); - if (!y.RI || it[t.debugId]) return t; - it[t.debugId] = !0; - var r = (0, re.YM)(t), - n = y.gm.MutationObserver; - return ( - n && - ((window.MutationObserver = function (e) { - return this instanceof n ? new n(r(e, 'fn-')) : n.apply(this, arguments); - }), - (MutationObserver.prototype = n.prototype)), - t - ); - } - const { TZ: at, d3: st, Kp: ct, $p: ut, wW: dt, e5: lt, tH: ft, uP: ht, rw: pt, Lc: gt } = Ve; - class mt extends E { - static featureName = at; - constructor(e) { - if ((super(e, at), Be(e), !y.RI)) return; - try { - this.removeOnAbort = new AbortController(); - } catch (e) {} - let t, - r = 0; - const n = this.ee.get('tracer'), - o = qe(this.ee), - a = Ye(this.ee), - u = nt(this.ee), - d = oe(this.ee), - l = this.ee.get('events'), - f = he(this.ee), - h = Ne(this.ee), - p = ot(this.ee); - function g(e, t) { - h.emit('newURL', ['' + window.location, t]); - } - function m() { - r++, (t = window.location.hash), (this[ht] = (0, c.t)()); - } - function v() { - r--, window.location.hash !== t && g(0, !0); - var e = (0, c.t)(); - (this[lt] = ~~this[lt] + e - this[ht]), (this[gt] = e); - } - function b(e, t) { - e.on(t, function () { - this[t] = (0, c.t)(); - }); - } - this.ee.on(ht, m), - a.on(pt, m), - o.on(pt, m), - this.ee.on(gt, v), - a.on(dt, v), - o.on(dt, v), - this.ee.on('fn-err', (...t) => { - t[2]?.__newrelic?.[e.agentIdentifier] || - (0, s.p)('function-err', [...t], void 0, this.featureName, this.ee); - }), - this.ee.buffer([ht, gt, 'xhr-resolved'], this.featureName), - l.buffer([ht], this.featureName), - u.buffer(['setTimeout' + ct, 'clearTimeout' + st, ht], this.featureName), - d.buffer([ht, 'new-xhr', 'send-xhr' + st], this.featureName), - f.buffer([ft + st, ft + '-done', ft + ut + st, ft + ut + ct], this.featureName), - h.buffer(['newURL'], this.featureName), - p.buffer([ht], this.featureName), - a.buffer(['propagate', pt, dt, 'executor-err', 'resolve' + st], this.featureName), - n.buffer([ht, 'no-' + ht], this.featureName), - o.buffer(['new-jsonp', 'cb-start', 'jsonp-error', 'jsonp-end'], this.featureName), - b(f, ft + st), - b(f, ft + '-done'), - b(o, 'new-jsonp'), - b(o, 'jsonp-end'), - b(o, 'cb-start'), - h.on('pushState-end', g), - h.on('replaceState-end', g), - window.addEventListener('hashchange', g, (0, I.jT)(!0, this.removeOnAbort?.signal)), - window.addEventListener('load', g, (0, I.jT)(!0, this.removeOnAbort?.signal)), - window.addEventListener( - 'popstate', - function () { - g(0, r > 1); - }, - (0, I.jT)(!0, this.removeOnAbort?.signal) - ), - (this.abortHandler = this.#r), - this.importAggregator(e, () => i.e(478).then(i.bind(i, 5592))); - } - #r() { - this.removeOnAbort?.abort(), (this.abortHandler = void 0); - } - } - var vt = i(3333); - class bt extends E { - static featureName = vt.TZ; - constructor(e) { - super(e, vt.TZ); - const t = [ - e.init.page_action.enabled, - e.init.performance.capture_marks, - e.init.performance.capture_measures, - e.init.user_actions.enabled, - e.init.performance.resources.enabled, - ]; - var r; - if ( - ((r = e), - p(u.hG, (e, t) => z(e, t, r), r), - (function (e) { - p( - u.fF, - function () { - (0, s.p)(u.Pl + u.fF, [(0, c.t)(), ...arguments], void 0, n.K7.genericEvents, e.ee); - }, - e - ); - })(e), - Ie(e), - Z(e), - (function (e) { - p( - u.V1, - function (t, r) { - const i = (0, c.t)(), - { start: o, end: a, customAttributes: d } = r || {}, - f = { customAttributes: d || {} }; - if ('object' != typeof f.customAttributes || 'string' != typeof t || 0 === t.length) - return void (0, l.R)(57); - const h = (e, t) => - null == e ? t : 'number' == typeof e ? e : e instanceof PerformanceMark ? e.startTime : Number.NaN; - if (((f.start = h(o, 0)), (f.end = h(a, i)), Number.isNaN(f.start) || Number.isNaN(f.end))) - (0, l.R)(57); - else { - if (((f.duration = f.end - f.start), !(f.duration < 0))) - return (0, s.p)(u.Pl + u.V1, [f, t], void 0, n.K7.genericEvents, e.ee), f; - (0, l.R)(58); - } - }, - e - ); - })(e), - y.RI) - ) { - if ( - (e.init.user_actions.enabled && - (vt.Zp.forEach((e) => (0, I.sp)(e, (e) => (0, s.p)('ua', [e], void 0, this.featureName, this.ee), !0)), - vt.qN.forEach((e) => { - const t = (0, x.s)( - (e) => { - (0, s.p)('ua', [e], void 0, this.featureName, this.ee); - }, - 500, - { leading: !0 } - ); - (0, I.sp)(e, t); - })), - e.init.performance.resources.enabled && - y.gm.PerformanceObserver?.supportedEntryTypes.includes('resource')) - ) { - new PerformanceObserver((e) => { - e.getEntries().forEach((e) => { - (0, s.p)('browserPerformance.resource', [e], void 0, this.featureName, this.ee); - }); - }).observe({ type: 'resource', buffered: !0 }); - } - const a = Ne(this.ee); - function d() { - a.emit('navChange'); - } - a.on('pushState-end', d), - a.on('replaceState-end', d), - window.addEventListener('hashchange', d, (0, I.jT)(!0, this.removeOnAbort?.signal)), - window.addEventListener('popstate', d, (0, I.jT)(!0, this.removeOnAbort?.signal)); - } - try { - this.removeOnAbort = new AbortController(); - } catch (f) {} - function o(t) { - const r = (0, pe.D)(t); - return e.beacons.includes(r.hostname + ':' + r.port); - } - (this.abortHandler = () => { - this.removeOnAbort?.abort(), (this.abortHandler = void 0); - }), - y.gm.addEventListener( - 'error', - () => { - (0, s.p)('uaErr', [], void 0, n.K7.genericEvents, this.ee); - }, - (0, I.jT)(!1, this.removeOnAbort?.signal) - ), - he(this.ee), - oe(this.ee), - this.ee.on('open-xhr-start', (e, t) => { - o(e[1]) || - t.addEventListener('readystatechange', () => { - 2 === t.readyState && (0, s.p)('uaXhr', [], void 0, n.K7.genericEvents, this.ee); - }); - }), - this.ee.on('fetch-start', (e) => { - e.length >= 1 && !o(be(e[0])) && (0, s.p)('uaXhr', [], void 0, n.K7.genericEvents, this.ee); - }), - t.some((e) => e) ? this.importAggregator(e, () => i.e(478).then(i.bind(i, 8019))) : this.deregisterDrain(); - } - } - var yt = i(2646); - const wt = new Map(); - function Rt(e, t, r, n) { - if ('object' != typeof t || !t || 'string' != typeof r || !r || 'function' != typeof t[r]) return (0, l.R)(29); - const i = (function (e) { - return (e || te.ee).get('logger'); - })(e), - o = (0, re.YM)(i), - a = new yt.y(te.P); - (a.level = n.level), (a.customAttributes = n.customAttributes); - const s = t[r]?.[re.Jt] || t[r]; - return wt.set(s, a), o.inPlace(t, [r], 'wrap-logger-', () => wt.get(s)), i; - } - var xt = i(1910); - class Tt extends E { - static featureName = B.TZ; - constructor(e) { - var t; - super(e, B.TZ), - (t = e), - p(u.$9, (e, r) => V(e, r, t), t), - (function (e) { - p( - u.Wb, - (t, r, { customAttributes: n = {}, level: i = B.p_.INFO } = {}) => { - Rt(e.ee, t, r, { customAttributes: n, level: i }); - }, - e - ); - })(e), - Z(e); - const r = this.ee; - ['log', 'error', 'warn', 'info', 'debug', 'trace'].forEach((e) => { - (0, xt.i)(y.gm.console[e]), Rt(r, y.gm.console, e, { level: 'log' === e ? 'info' : e }); - }), - this.ee.on('wrap-logger-end', function ([e]) { - const { level: t, customAttributes: n } = this; - (0, G.R)(r, e, n, t); - }), - this.importAggregator(e, () => i.e(478).then(i.bind(i, 5288))); - } - } - new (class extends r { - constructor(e) { - var t; - (super(), y.gm) - ? ((this.features = {}), - (0, N.bQ)(this.agentIdentifier, this), - (this.desiredFeatures = new Set(e.features || [])), - this.desiredFeatures.add(S), - (this.runSoftNavOverSpa = [...this.desiredFeatures].some((e) => e.featureName === n.K7.softNav)), - (0, a.j)(this, e, e.loaderType || 'agent'), - (t = this), - p( - u.cD, - function (e, r, n = !1) { - if ('string' == typeof e) { - if (['string', 'number', 'boolean'].includes(typeof r) || null === r) return g(t, e, r, u.cD, n); - (0, l.R)(40, typeof r); - } else (0, l.R)(39, typeof e); - }, - t - ), - (function (e) { - p( - u.Dl, - function (t) { - if ('string' == typeof t || null === t) return g(e, 'enduser.id', t, u.Dl, !0); - (0, l.R)(41, typeof t); - }, - e - ); - })(this), - (function (e) { - p( - u.nb, - function (t) { - if ('string' == typeof t || null === t) return g(e, 'application.version', t, u.nb, !1); - (0, l.R)(42, typeof t); - }, - e - ); - })(this), - (function (e) { - p( - u.d3, - function () { - e.ee.emit('manual-start-all'); - }, - e - ); - })(this), - this.run()) - : (0, l.R)(21); - } - get config() { - return { info: this.info, init: this.init, loader_config: this.loader_config, runtime: this.runtime }; - } - get api() { - return this; - } - run() { - try { - const e = (function (e) { - const t = {}; - return ( - o.forEach((r) => { - t[r] = !!e[r]?.enabled; - }), - t - ); - })(this.init), - t = [...this.desiredFeatures]; - t.sort((e, t) => n.P3[e.featureName] - n.P3[t.featureName]), - t.forEach((t) => { - if (!e[t.featureName] && t.featureName !== n.K7.pageViewEvent) return; - if (this.runSoftNavOverSpa && t.featureName === n.K7.spa) return; - if (!this.runSoftNavOverSpa && t.featureName === n.K7.softNav) return; - const r = (function (e) { - switch (e) { - case n.K7.ajax: - return [n.K7.jserrors]; - case n.K7.sessionTrace: - return [n.K7.ajax, n.K7.pageViewEvent]; - case n.K7.sessionReplay: - return [n.K7.sessionTrace]; - case n.K7.pageViewTiming: - return [n.K7.pageViewEvent]; - default: - return []; - } - })(t.featureName).filter((e) => !(e in this.features)); - r.length > 0 && (0, l.R)(36, { targetFeature: t.featureName, missingDependencies: r }), - (this.features[t.featureName] = new t(this)); - }); - } catch (e) { - (0, l.R)(22, e); - for (const e in this.features) this.features[e].abortHandler?.(); - const t = (0, N.Zm)(); - delete t.initializedAgents[this.agentIdentifier]?.features, delete this.sharedAggregator; - return t.ee.get(this.agentIdentifier).abort(), !1; - } - } - })({ features: [Ee, S, j, Ke, Fe, C, q, bt, Tt, Ge, mt], loaderType: 'spa' }); - })(); -})(); diff --git a/src/index.html b/src/index.html index 717c99e2b..0bb3b9a03 100644 --- a/src/index.html +++ b/src/index.html @@ -8,7 +8,6 @@ - - From 03bcb73bd74717d4c1c3c81eab27172f8a55b12a Mon Sep 17 00:00:00 2001 From: nsemets Date: Tue, 2 Dec 2025 14:16:36 +0200 Subject: [PATCH 03/11] feat(ssr): updated ssr issues for window and document --- package-lock.json | 93 +++++++++---------- .../components/nav-menu/nav-menu.component.ts | 5 +- src/app/core/guards/auth.guard.ts | 5 +- src/app/core/guards/view-only.guard.ts | 7 +- src/app/core/helpers/nav-menu.helper.ts | 11 +-- .../core/interceptors/error.interceptor.ts | 5 +- .../interceptors/view-only.interceptor.ts | 5 +- src/app/core/models/route-context.model.ts | 6 +- .../features/analytics/analytics.component.ts | 5 +- .../add-to-collection.component.ts | 38 ++++---- .../collections-discover.component.ts | 12 ++- .../file-keywords/file-keywords.component.ts | 5 +- .../file-metadata/file-metadata.component.ts | 5 +- .../file-resource-metadata.component.ts | 5 +- .../file-detail/file-detail.component.ts | 7 +- .../files/pages/files/files.component.ts | 7 +- .../preprint-services.component.html | 2 +- .../create-new-version.component.spec.ts | 19 +--- .../create-new-version.component.ts | 19 ++-- .../landing/preprints-landing.component.ts | 5 +- ...eprint-provider-discover.component.spec.ts | 18 +--- .../preprint-provider-discover.component.ts | 19 ++-- ...eprint-provider-overview.component.spec.ts | 17 +--- .../preprint-provider-overview.component.ts | 19 ++-- .../submit-preprint-stepper.component.spec.ts | 21 +---- .../submit-preprint-stepper.component.ts | 19 ++-- .../update-preprint-stepper.component.spec.ts | 21 +---- .../update-preprint-stepper.component.ts | 19 ++-- .../files-widget/files-widget.component.ts | 7 +- .../overview/project-overview.component.ts | 5 +- .../features/project/wiki/wiki.component.ts | 10 +- .../registry-provider-hero.component.ts | 12 ++- .../registry-components.component.ts | 5 +- .../registry-overview.component.ts | 5 +- .../registry-wiki/registry-wiki.component.ts | 5 +- .../file-menu/file-menu.component.ts | 6 +- .../files-tree/files-tree.component.ts | 23 +++-- .../resource-citations.component.ts | 5 +- src/app/shared/helpers/browser-tab.helper.ts | 22 ----- src/app/shared/helpers/header-style.helper.ts | 17 ---- src/app/shared/helpers/view-only.helper.ts | 43 --------- .../shared/mappers/view-only-links.mapper.ts | 18 +++- .../view-only-link-response.model.ts | 21 +---- .../activity-log-url-builder.service.ts | 9 +- .../services/addons/addon-oauth.service.ts | 18 +++- src/app/shared/services/analytics.service.ts | 20 +++- src/app/shared/services/brand.service.ts | 25 ++++- .../shared/services/browser-tab.service.ts | 41 ++++++++ .../services/datacite/datacite.service.ts | 21 +++-- .../google-file-picker.download.service.ts | 14 ++- .../shared/services/header-style.service.ts | 32 +++++++ src/app/shared/services/meta-tags.service.ts | 19 +++- .../services/view-only-links.service.ts | 14 +-- src/app/shared/services/view-only.service.ts | 53 +++++++++++ 54 files changed, 495 insertions(+), 394 deletions(-) delete mode 100644 src/app/shared/helpers/browser-tab.helper.ts delete mode 100644 src/app/shared/helpers/header-style.helper.ts delete mode 100644 src/app/shared/helpers/view-only.helper.ts create mode 100644 src/app/shared/services/browser-tab.service.ts create mode 100644 src/app/shared/services/header-style.service.ts create mode 100644 src/app/shared/services/view-only.service.ts diff --git a/package-lock.json b/package-lock.json index 2d3b71bcf..3783664a7 100644 --- a/package-lock.json +++ b/package-lock.json @@ -5006,9 +5006,9 @@ } }, "node_modules/@istanbuljs/load-nyc-config/node_modules/js-yaml": { - "version": "3.14.1", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.1.tgz", - "integrity": "sha512-okMH7OXXJ7YrN9Ok3/SXrnu4iX9yOk+25nqX4imS2npuvTYDmo/QEZoqwZkYaIDk3jVvBOTOIEgEhaLOynBS9g==", + "version": "3.14.2", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-3.14.2.tgz", + "integrity": "sha512-PMSmkqxr106Xa156c2M265Z+FTrPl+oxd/rgOQy2tijQeK5TxQ43psO1ZCwhVOSdnn+RzkzlRz/eY4BgJBYVpg==", "dev": true, "license": "MIT", "dependencies": { @@ -6719,9 +6719,9 @@ } }, "node_modules/@npmcli/package-json/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -10175,37 +10175,28 @@ } }, "node_modules/body-parser": { - "version": "2.2.0", - "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.0.tgz", - "integrity": "sha512-02qvAaxv8tp7fBa/mw1ga98OGm+eCbqzJOKoRt70sLmfEEi+jyBYVTDGfCL/k06/4EMk/z01gCe7HoCH/f2LTg==", + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-2.2.1.tgz", + "integrity": "sha512-nfDwkulwiZYQIGwxdy0RUmowMhKcFVcYXUU7m4QlKYim1rUtg83xm2yjZ40QjDuc291AJjjeSc9b++AWHSgSHw==", "dev": true, "license": "MIT", "dependencies": { "bytes": "^3.1.2", "content-type": "^1.0.5", - "debug": "^4.4.0", + "debug": "^4.4.3", "http-errors": "^2.0.0", - "iconv-lite": "^0.6.3", + "iconv-lite": "^0.7.0", "on-finished": "^2.4.1", "qs": "^6.14.0", - "raw-body": "^3.0.0", - "type-is": "^2.0.0" + "raw-body": "^3.0.1", + "type-is": "^2.0.1" }, "engines": { "node": ">=18" - } - }, - "node_modules/body-parser/node_modules/iconv-lite": { - "version": "0.6.3", - "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz", - "integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==", - "dev": true, - "license": "MIT", - "dependencies": { - "safer-buffer": ">= 2.1.2 < 3.0.0" }, - "engines": { - "node": ">=0.10.0" + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/express" } }, "node_modules/bonjour-service": { @@ -10446,9 +10437,9 @@ } }, "node_modules/cacache/node_modules/glob": { - "version": "10.4.5", - "resolved": "https://registry.npmjs.org/glob/-/glob-10.4.5.tgz", - "integrity": "sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==", + "version": "10.5.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-10.5.0.tgz", + "integrity": "sha512-DfXN8DfhJ7NH3Oe7cFmu3NCu1wKbkReJ8TorzSAFbSKrlNaQSKfIzqYqVY8zlbs2NLBbWpRiU52GX2PbaBVNkg==", "dev": true, "license": "ISC", "dependencies": { @@ -10507,11 +10498,11 @@ } }, "node_modules/cacache/node_modules/tar": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", - "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", @@ -14205,15 +14196,15 @@ } }, "node_modules/glob": { - "version": "11.0.3", - "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.3.tgz", - "integrity": "sha512-2Nim7dha1KVkaiF4q6Dj+ngPPMdfvLJEOpZk/jKiUAkqKebpGAWQXAq9z1xu9HKu5lWfqw/FASuccEjyznjPaA==", + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.1.0.tgz", + "integrity": "sha512-vuNwKSaKiqm7g0THUBu2x7ckSs3XJLXE+2ssL7/MfTGPLLcrJQ/4Uq1CjPTtO5cCIiRxqvN6Twy1qOwhL0Xjcw==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "foreground-child": "^3.3.1", "jackspeak": "^4.1.1", - "minimatch": "^10.0.3", + "minimatch": "^10.1.1", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", "path-scurry": "^2.0.0" @@ -14266,11 +14257,11 @@ "license": "BSD-2-Clause" }, "node_modules/glob/node_modules/minimatch": { - "version": "10.0.3", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.3.tgz", - "integrity": "sha512-IPZ167aShDZZUMdRk66cyQAW3qr0WzbHkPdMYa8bzZhlHhO3jALbKdxcaak7W9FfT2rZNpQuUu4Od7ILEpXSaw==", + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.1.1.tgz", + "integrity": "sha512-enIvLvRAFZYXJzkCYG5RKmPfrFArdLv+R+lbQ53BmIMLIry74bjKzX6iHAm8WYamJkhSSEabrWN5D97XnKObjQ==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/brace-expansion": "^5.0.0" }, @@ -17289,9 +17280,9 @@ "license": "MIT" }, "node_modules/js-yaml": { - "version": "4.1.0", - "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", - "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.1.tgz", + "integrity": "sha512-qQKT4zQxXl8lLwBtHMWwaTcGfFOZviOJet3Oy/xmGk2gZH677CJM9EvtfdSkgWcATZhj/55JZ0rmy3myCT5lsA==", "dev": true, "license": "MIT", "dependencies": { @@ -19226,9 +19217,9 @@ } }, "node_modules/node-forge": { - "version": "1.3.1", - "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", - "integrity": "sha512-dPEtOeMvF9VMcYV/1Wb8CPoVAXtp6MKMlcbAt4ddqmGqUJ6fQZFXkNZNkNlfevtNkGtaSoXf/vNNNSvgrdXwtA==", + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.2.tgz", + "integrity": "sha512-6xKiQ+cph9KImrRh0VsjH2d8/GXA4FIMlgU4B757iI1ApvcyA9VlouP0yZJha01V+huImO+kKMU7ih+2+E14fw==", "dev": true, "license": "(BSD-3-Clause OR GPL-2.0)", "engines": { @@ -19297,11 +19288,11 @@ } }, "node_modules/node-gyp/node_modules/tar": { - "version": "7.5.1", - "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.1.tgz", - "integrity": "sha512-nlGpxf+hv0v7GkWBK2V9spgactGOp0qvfWRxUMjqHyzrt3SgwE48DIv/FhqPHJYLHpgW1opq3nERbz5Anq7n1g==", + "version": "7.5.2", + "resolved": "https://registry.npmjs.org/tar/-/tar-7.5.2.tgz", + "integrity": "sha512-7NyxrTE4Anh8km8iEy7o0QYPs+0JKBTj5ZaqHg6B39erLg0qYXN3BijtShwbsNSvQ+LN75+KV+C4QR/f6Gwnpg==", "dev": true, - "license": "ISC", + "license": "BlueOak-1.0.0", "dependencies": { "@isaacs/fs-minipass": "^4.0.0", "chownr": "^3.0.0", diff --git a/src/app/core/components/nav-menu/nav-menu.component.ts b/src/app/core/components/nav-menu/nav-menu.component.ts index 67b555500..40c3d87a2 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.ts +++ b/src/app/core/components/nav-menu/nav-menu.component.ts @@ -21,8 +21,8 @@ import { UserSelectors } from '@core/store/user'; import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; import { ReviewPermissions } from '@osf/shared/enums/review-permissions.enum'; -import { getViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; import { WrapFnPipe } from '@osf/shared/pipes/wrap-fn.pipe'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { CurrentResourceSelectors, GetResourceDetails } from '@osf/shared/stores/current-resource'; @Component({ @@ -38,6 +38,7 @@ export class NavMenuComponent { private readonly route = inject(ActivatedRoute); private readonly authService = inject(AuthService); private readonly platformId = inject(PLATFORM_ID); + private readonly viewOnlyService = inject(ViewOnlyService); private readonly isAuthenticated = select(UserSelectors.isAuthenticated); private readonly currentResource = select(CurrentResourceSelectors.getCurrentResource); @@ -70,7 +71,7 @@ export class NavMenuComponent { this.provider()?.permissions?.includes(ReviewPermissions.ViewSubmissions), isCollections: this.isCollectionsRoute() || false, currentUrl: this.router.url, - isViewOnly: !!getViewOnlyParam(this.router), + viewOnly: this.viewOnlyService.getViewOnlyParam(this.router), permissions: this.currentResource()?.permissions, }; diff --git a/src/app/core/guards/auth.guard.ts b/src/app/core/guards/auth.guard.ts index d20108e71..5b497e01b 100644 --- a/src/app/core/guards/auth.guard.ts +++ b/src/app/core/guards/auth.guard.ts @@ -7,12 +7,13 @@ import { CanActivateFn, Router } from '@angular/router'; import { AuthService } from '@core/services/auth.service'; import { GetCurrentUser, UserSelectors } from '@osf/core/store/user'; -import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; export const authGuard: CanActivateFn = () => { const store = inject(Store); const router = inject(Router); const authService = inject(AuthService); + const viewOnlyHelper = inject(ViewOnlyService); const isAuthenticated = store.selectSnapshot(UserSelectors.isAuthenticated); @@ -20,7 +21,7 @@ export const authGuard: CanActivateFn = () => { return true; } - if (hasViewOnlyParam(router)) { + if (viewOnlyHelper.hasViewOnlyParam(router)) { return true; } diff --git a/src/app/core/guards/view-only.guard.ts b/src/app/core/guards/view-only.guard.ts index f987d2500..cb53224af 100644 --- a/src/app/core/guards/view-only.guard.ts +++ b/src/app/core/guards/view-only.guard.ts @@ -2,12 +2,13 @@ import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; import { VIEW_ONLY_EXCLUDED_ROUTES } from '@core/constants/view-only-excluded-routes.const'; -import { getViewOnlyParam, hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; export const viewOnlyGuard: CanActivateFn = (route) => { const router = inject(Router); + const viewOnlyHelper = inject(ViewOnlyService); - if (!hasViewOnlyParam(router)) { + if (!viewOnlyHelper.hasViewOnlyParam(router)) { return true; } @@ -23,7 +24,7 @@ export const viewOnlyGuard: CanActivateFn = (route) => { const urlSegments = router.url.split('/'); const resourceId = urlSegments[1]; - const viewOnlyParam = getViewOnlyParam(router); + const viewOnlyParam = viewOnlyHelper.getViewOnlyParam(router); if (resourceId && viewOnlyParam) { router.navigate([resourceId, 'overview'], { diff --git a/src/app/core/helpers/nav-menu.helper.ts b/src/app/core/helpers/nav-menu.helper.ts index b126d2dc7..4b6a79954 100644 --- a/src/app/core/helpers/nav-menu.helper.ts +++ b/src/app/core/helpers/nav-menu.helper.ts @@ -8,7 +8,6 @@ import { } from '@core/constants/nav-items.constant'; import { RouteContext } from '@core/models/route-context.model'; import { UserPermissions } from '@osf/shared/enums/user-permissions.enum'; -import { getViewOnlyParamFromUrl } from '@osf/shared/helpers/view-only.helper'; import { CustomMenuItem } from '../models/custom-menu-item.model'; @@ -96,7 +95,7 @@ function updateProjectMenuItem(item: CustomMenuItem, ctx: RouteContext): CustomM if (hasProject) { let menuItems = PROJECT_MENU_ITEMS; - if (ctx.isViewOnly) { + if (ctx.viewOnly) { const allowedViewOnlyItems = VIEW_ONLY_PROJECT_MENU_ITEMS; menuItems = PROJECT_MENU_ITEMS.filter((menuItem) => allowedViewOnlyItems.includes(menuItem.id || '')); } @@ -124,7 +123,7 @@ function updateProjectMenuItem(item: CustomMenuItem, ctx: RouteContext): CustomM items: menuItems.map((menuItem) => ({ ...menuItem, routerLink: [ctx.resourceId as string, menuItem.routerLink], - queryParams: ctx.isViewOnly ? { view_only: getViewOnlyParamFromUrl(ctx.currentUrl) } : undefined, + queryParams: ctx.viewOnly ? { view_only: ctx.viewOnly } : undefined, })), }; } @@ -143,7 +142,7 @@ function updateRegistryMenuItem(item: CustomMenuItem, ctx: RouteContext): Custom if (hasRegistry) { let menuItems = REGISTRATION_MENU_ITEMS; - if (ctx.isViewOnly) { + if (ctx.viewOnly) { const allowedViewOnlyItems = VIEW_ONLY_REGISTRY_MENU_ITEMS; menuItems = REGISTRATION_MENU_ITEMS.filter((menuItem) => allowedViewOnlyItems.includes(menuItem.id || '')); } @@ -165,7 +164,7 @@ function updateRegistryMenuItem(item: CustomMenuItem, ctx: RouteContext): Custom return { ...menuItem, routerLink: [ctx.resourceId as string, menuItem.routerLink], - queryParams: ctx.isViewOnly ? { view_only: getViewOnlyParamFromUrl(ctx.currentUrl) } : undefined, + queryParams: ctx.viewOnly ? { view_only: ctx.viewOnly } : undefined, }; }), }; @@ -199,7 +198,7 @@ function updatePreprintMenuItem(item: CustomMenuItem, ctx: RouteContext): Custom items: PREPRINT_MENU_ITEMS.map((menuItem) => ({ ...menuItem, routerLink: ['preprints', ctx.providerId, ctx.resourceId as string], - queryParams: ctx.isViewOnly ? { view_only: getViewOnlyParamFromUrl(ctx.currentUrl) } : undefined, + queryParams: ctx.viewOnly ? { view_only: ctx.viewOnly } : undefined, })), }; } diff --git a/src/app/core/interceptors/error.interceptor.ts b/src/app/core/interceptors/error.interceptor.ts index 12d40b185..2aa288554 100644 --- a/src/app/core/interceptors/error.interceptor.ts +++ b/src/app/core/interceptors/error.interceptor.ts @@ -9,9 +9,9 @@ import { Router } from '@angular/router'; import { ERROR_MESSAGES } from '@core/constants/error-messages'; import { SENTRY_TOKEN } from '@core/provider/sentry.provider'; import { AuthService } from '@core/services/auth.service'; -import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; import { LoaderService } from '@osf/shared/services/loader.service'; import { ToastService } from '@osf/shared/services/toast.service'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { BYPASS_ERROR_INTERCEPTOR } from './error-interceptor.tokens'; @@ -22,6 +22,7 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => { const authService = inject(AuthService); const sentry = inject(SENTRY_TOKEN); const platformId = inject(PLATFORM_ID); + const viewOnlyHelper = inject(ViewOnlyService); return next(req).pipe( catchError((error: HttpErrorResponse) => { @@ -52,7 +53,7 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => { } if (error.status === 401) { - if (!hasViewOnlyParam(router)) { + if (!viewOnlyHelper.hasViewOnlyParam(router)) { authService.logout(); } return throwError(() => error); diff --git a/src/app/core/interceptors/view-only.interceptor.ts b/src/app/core/interceptors/view-only.interceptor.ts index e77c731ab..6622eee13 100644 --- a/src/app/core/interceptors/view-only.interceptor.ts +++ b/src/app/core/interceptors/view-only.interceptor.ts @@ -4,15 +4,16 @@ import { HttpEvent, HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angul import { inject } from '@angular/core'; import { Router } from '@angular/router'; -import { getViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; export const viewOnlyInterceptor: HttpInterceptorFn = ( req: HttpRequest, next: HttpHandlerFn ): Observable> => { const router = inject(Router); + const viewOnlyHelper = inject(ViewOnlyService); - const viewOnlyParam = getViewOnlyParam(router); + const viewOnlyParam = viewOnlyHelper.getViewOnlyParam(router); if (!req.url.includes('/api.crossref.org/funders') && viewOnlyParam) { if (req.url.includes('view_only=')) { diff --git a/src/app/core/models/route-context.model.ts b/src/app/core/models/route-context.model.ts index fa6f20a7b..d0707d9a0 100644 --- a/src/app/core/models/route-context.model.ts +++ b/src/app/core/models/route-context.model.ts @@ -3,15 +3,15 @@ import { UserPermissions } from '@osf/shared/enums/user-permissions.enum'; export interface RouteContext { resourceId: string | undefined; providerId?: string; - isProject: boolean; wikiPageVisible?: boolean; + isProject: boolean; isRegistry: boolean; isPreprint: boolean; + isCollections: boolean; preprintReviewsPageVisible?: boolean; registrationModerationPageVisible?: boolean; collectionModerationPageVisible?: boolean; - isCollections: boolean; currentUrl?: string; - isViewOnly?: boolean; + viewOnly?: string | null; permissions?: UserPermissions[]; } diff --git a/src/app/features/analytics/analytics.component.ts b/src/app/features/analytics/analytics.component.ts index f27d9b343..a0409c215 100644 --- a/src/app/features/analytics/analytics.component.ts +++ b/src/app/features/analytics/analytics.component.ts @@ -28,8 +28,8 @@ import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-link-message/view-only-link-message.component'; import { IS_WEB } from '@osf/shared/helpers/breakpoints.tokens'; import { Primitive } from '@osf/shared/helpers/types.helper'; -import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; import { DatasetInput } from '@osf/shared/models/charts/dataset-input'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { AnalyticsKpiComponent } from './components'; import { DATE_RANGE_OPTIONS } from './constants'; @@ -66,11 +66,12 @@ export class AnalyticsComponent implements OnInit { private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); private readonly translateService = inject(TranslateService); + private readonly viewOnlyService = inject(ViewOnlyService); readonly resourceId = toSignal(this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined)); readonly resourceType = toSignal(this.route.data.pipe(map((params) => params['resourceType'])) ?? of(undefined)); - hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); analytics = select(AnalyticsSelectors.getMetrics(this.resourceId())); relatedCounts = select(AnalyticsSelectors.getRelatedCounts(this.resourceId())); diff --git a/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts b/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts index f9be9fb74..60378f381 100644 --- a/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts +++ b/src/app/features/collections/components/add-to-collection/add-to-collection.component.ts @@ -21,26 +21,22 @@ import { FormGroup } from '@angular/forms'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { UserSelectors } from '@core/store/user'; -import { AddToCollectionSteps } from '@osf/features/collections/enums'; -import { - ClearAddToCollectionState, - CreateCollectionSubmission, -} from '@osf/features/collections/store/add-to-collection'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; +import { CanDeactivateComponent } from '@osf/shared/models/can-deactivate.interface'; import { BrandService } from '@osf/shared/services/brand.service'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; -import { HeaderStyleHelper } from '@shared/helpers/header-style.helper'; -import { CanDeactivateComponent } from '@shared/models/can-deactivate.interface'; -import { CollectionsSelectors, GetCollectionProvider } from '@shared/stores/collections'; -import { ProjectsSelectors } from '@shared/stores/projects/projects.selectors'; +import { HeaderStyleService } from '@osf/shared/services/header-style.service'; +import { CollectionsSelectors, GetCollectionProvider } from '@osf/shared/stores/collections'; +import { ProjectsSelectors } from '@osf/shared/stores/projects'; -import { - AddToCollectionConfirmationDialogComponent, - CollectionMetadataStepComponent, - ProjectContributorsStepComponent, - ProjectMetadataStepComponent, - SelectProjectStepComponent, -} from './index'; +import { AddToCollectionSteps } from '../../enums'; +import { ClearAddToCollectionState, CreateCollectionSubmission } from '../../store/add-to-collection'; + +import { AddToCollectionConfirmationDialogComponent } from './add-to-collection-confirmation-dialog/add-to-collection-confirmation-dialog.component'; +import { CollectionMetadataStepComponent } from './collection-metadata-step/collection-metadata-step.component'; +import { ProjectContributorsStepComponent } from './project-contributors-step/project-contributors-step.component'; +import { ProjectMetadataStepComponent } from './project-metadata-step/project-metadata-step.component'; +import { SelectProjectStepComponent } from './select-project-step/select-project-step.component'; @Component({ selector: 'osf-add-to-collection-form', @@ -64,6 +60,8 @@ export class AddToCollectionComponent implements CanDeactivateComponent { private readonly route = inject(ActivatedRoute); private readonly destroyRef = inject(DestroyRef); private readonly customDialogService = inject(CustomDialogService); + private readonly brandService = inject(BrandService); + private readonly headerStyleHelper = inject(HeaderStyleService); readonly AddToCollectionSteps = AddToCollectionSteps; @@ -160,8 +158,8 @@ export class AddToCollectionComponent implements CanDeactivateComponent { const provider = this.collectionProvider(); if (provider && provider.brand) { - BrandService.applyBranding(provider.brand); - HeaderStyleHelper.applyHeaderStyles(provider.brand.secondaryColor, provider.brand.backgroundColor || ''); + this.brandService.applyBranding(provider.brand); + this.headerStyleHelper.applyHeaderStyles(provider.brand.secondaryColor, provider.brand.backgroundColor || ''); } }); } @@ -171,8 +169,8 @@ export class AddToCollectionComponent implements CanDeactivateComponent { this.actions.clearAddToCollectionState(); this.allowNavigation.set(false); - HeaderStyleHelper.resetToDefaults(); - BrandService.resetBranding(); + this.headerStyleHelper.resetToDefaults(); + this.brandService.resetBranding(); }); } diff --git a/src/app/features/collections/components/collections-discover/collections-discover.component.ts b/src/app/features/collections/components/collections-discover/collections-discover.component.ts index 61ff5ddc8..cd7929102 100644 --- a/src/app/features/collections/components/collections-discover/collections-discover.component.ts +++ b/src/app/features/collections/components/collections-discover/collections-discover.component.ts @@ -14,10 +14,10 @@ import { ActivatedRoute, Router, RouterLink } from '@angular/router'; import { ClearCurrentProvider } from '@core/store/provider'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; -import { HeaderStyleHelper } from '@osf/shared/helpers/header-style.helper'; import { CollectionsFilters } from '@osf/shared/models/collections/collections-filters.model'; import { BrandService } from '@osf/shared/services/brand.service'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { ClearCollections, ClearCollectionSubmissions, @@ -54,6 +54,8 @@ export class CollectionsDiscoverComponent { private customDialogService = inject(CustomDialogService); private querySyncService = inject(CollectionsQuerySyncService); private destroyRef = inject(DestroyRef); + private brandService = inject(BrandService); + private headerStyleHelper = inject(HeaderStyleService); searchControl = new FormControl(''); providerId = signal(''); @@ -118,8 +120,8 @@ export class CollectionsDiscoverComponent { const provider = this.collectionProvider(); if (provider && provider.brand) { - BrandService.applyBranding(provider.brand); - HeaderStyleHelper.applyHeaderStyles(provider.brand.secondaryColor, provider.brand.backgroundColor || ''); + this.brandService.applyBranding(provider.brand); + this.headerStyleHelper.applyHeaderStyles(provider.brand.secondaryColor, provider.brand.backgroundColor || ''); } }); @@ -151,8 +153,8 @@ export class CollectionsDiscoverComponent { this.destroyRef.onDestroy(() => { this.actions.clearCollections(); - HeaderStyleHelper.resetToDefaults(); - BrandService.resetBranding(); + this.headerStyleHelper.resetToDefaults(); + this.brandService.resetBranding(); }); } diff --git a/src/app/features/files/components/file-keywords/file-keywords.component.ts b/src/app/features/files/components/file-keywords/file-keywords.component.ts index 83bf1893e..3e1cb8384 100644 --- a/src/app/features/files/components/file-keywords/file-keywords.component.ts +++ b/src/app/features/files/components/file-keywords/file-keywords.component.ts @@ -14,7 +14,7 @@ import { Router } from '@angular/router'; import { InputLimits } from '@osf/shared/constants/input-limits.const'; import { CustomValidators } from '@osf/shared/helpers/custom-form-validators.helper'; -import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { FilesSelectors, UpdateTags } from '../../store'; @@ -29,12 +29,13 @@ export class FileKeywordsComponent { private readonly actions = createDispatchMap({ updateTags: UpdateTags }); private readonly destroyRef = inject(DestroyRef); private readonly router = inject(Router); + private readonly viewOnlyService = inject(ViewOnlyService); readonly tags = select(FilesSelectors.getFileTags); readonly isTagsLoading = select(FilesSelectors.isFileTagsLoading); readonly file = select(FilesSelectors.getOpenedFile); readonly hasWriteAccess = select(FilesSelectors.hasWriteAccess); - readonly hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + readonly hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); keywordControl = new FormControl('', { nonNullable: true, diff --git a/src/app/features/files/components/file-metadata/file-metadata.component.ts b/src/app/features/files/components/file-metadata/file-metadata.component.ts index eee47d70a..b6b05db70 100644 --- a/src/app/features/files/components/file-metadata/file-metadata.component.ts +++ b/src/app/features/files/components/file-metadata/file-metadata.component.ts @@ -13,8 +13,8 @@ import { ActivatedRoute, Router } from '@angular/router'; import { ENVIRONMENT } from '@core/provider/environment.provider'; import { languageCodes } from '@osf/shared/constants/language.const'; -import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { LanguageCodeModel } from '@shared/models/language-code.model'; import { FileMetadataFields } from '../../constants'; @@ -35,11 +35,12 @@ export class FileMetadataComponent { private readonly router = inject(Router); private readonly customDialogService = inject(CustomDialogService); private readonly environment = inject(ENVIRONMENT); + private readonly viewOnlyService = inject(ViewOnlyService); fileMetadata = select(FilesSelectors.getFileCustomMetadata); isLoading = select(FilesSelectors.isFileMetadataLoading); hasWriteAccess = select(FilesSelectors.hasWriteAccess); - hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); readonly languageCodes = languageCodes; diff --git a/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts index e554a4734..ccf29d1fa 100644 --- a/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts +++ b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts @@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component, computed, inject, input } from '@an import { Router } from '@angular/router'; import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component'; -import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { FilesSelectors } from '../../store'; @@ -22,11 +22,12 @@ import { FilesSelectors } from '../../store'; }) export class FileResourceMetadataComponent { private readonly router = inject(Router); + private readonly viewOnlyService = inject(ViewOnlyService); resourceType = input('nodes'); resourceMetadata = select(FilesSelectors.getResourceMetadata); contributors = select(FilesSelectors.getContributors); isResourceMetadataLoading = select(FilesSelectors.isResourceMetadataLoading); isResourceContributorsLoading = select(FilesSelectors.isResourceContributorsLoading); - hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); } diff --git a/src/app/features/files/pages/file-detail/file-detail.component.ts b/src/app/features/files/pages/file-detail/file-detail.component.ts index ec366758d..ab60f3a5f 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.ts +++ b/src/app/features/files/pages/file-detail/file-detail.component.ts @@ -44,11 +44,11 @@ import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header import { MetadataResourceEnum } from '@osf/shared/enums/metadata-resource.enum'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { pathJoin } from '@osf/shared/helpers/path-join.helper'; -import { getViewOnlyParam, hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { DataciteService } from '@osf/shared/services/datacite/datacite.service'; import { MetaTagsService } from '@osf/shared/services/meta-tags.service'; import { ToastService } from '@osf/shared/services/toast.service'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { FileDetailsModel } from '@shared/models/files/file.model'; import { MetadataTabsModel } from '@shared/models/metadata-tabs.model'; @@ -107,6 +107,7 @@ export class FileDetailComponent { private readonly metaTags = inject(MetaTagsService); private readonly datePipe = inject(DatePipe); + private readonly viewOnlyService = inject(ViewOnlyService); private readonly translateService = inject(TranslateService); private readonly environment = inject(ENVIRONMENT); private readonly clipboard = inject(Clipboard); @@ -144,7 +145,7 @@ export class FileDetailComponent { isFileRevisionLoading = select(FilesSelectors.isFileRevisionsLoading); hasWriteAccess = select(FilesSelectors.hasWriteAccess); - hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); safeLink: SafeResourceUrl | null = null; resourceId = ''; @@ -472,7 +473,7 @@ export class FileDetailComponent { if (version) downloadUrlObj.searchParams.set('version', version); if (this.hasViewOnly()) { - const viewOnlyParam = getViewOnlyParam(); + const viewOnlyParam = this.viewOnlyService.getViewOnlyParam(); if (viewOnlyParam) downloadUrlObj.searchParams.set('view_only', viewOnlyParam); } diff --git a/src/app/features/files/pages/files/files.component.ts b/src/app/features/files/pages/files/files.component.ts index 38e8620cd..7bd2d5a91 100644 --- a/src/app/features/files/pages/files/files.component.ts +++ b/src/app/features/files/pages/files/files.component.ts @@ -67,11 +67,11 @@ import { SupportedFeature } from '@osf/shared/enums/addon-supported-features.enu import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { UserPermissions } from '@osf/shared/enums/user-permissions.enum'; -import { getViewOnlyParamFromUrl, hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { FilesService } from '@osf/shared/services/files.service'; import { ToastService } from '@osf/shared/services/toast.service'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { CurrentResourceSelectors, GetResourceDetails } from '@osf/shared/stores/current-resource'; import { ConfiguredAddonModel } from '@shared/models/addons/configured-addon.model'; import { StorageItem } from '@shared/models/addons/storage-item.model'; @@ -129,6 +129,7 @@ export class FilesComponent { private readonly environment = inject(ENVIRONMENT); private readonly customConfirmationService = inject(CustomConfirmationService); private readonly toastService = inject(ToastService); + private readonly viewOnlyService = inject(ViewOnlyService); private readonly webUrl = this.environment.webUrl; private readonly apiDomainUrl = this.environment.apiDomainUrl; @@ -229,7 +230,7 @@ export class FilesComponent { this.activeRoute.parent?.parent?.snapshot.data['resourceType'] || ResourceType.Project ); - readonly hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + readonly hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); readonly canEdit = computed(() => { const details = this.resourceDetails(); @@ -621,7 +622,7 @@ export class FilesComponent { navigateToFile(file: FileModel) { const extras = this.hasViewOnly() - ? { queryParams: { view_only: getViewOnlyParamFromUrl(this.router.url) } } + ? { queryParams: { view_only: this.viewOnlyService.getViewOnlyParamFromUrl(this.router.url) } } : undefined; const url = this.router.serializeUrl(this.router.createUrlTree(['/', file.guid], extras)); diff --git a/src/app/features/preprints/components/preprint-services/preprint-services.component.html b/src/app/features/preprints/components/preprint-services/preprint-services.component.html index c4f3cc58a..b2bb4246f 100644 --- a/src/app/features/preprints/components/preprint-services/preprint-services.component.html +++ b/src/app/features/preprints/components/preprint-services/preprint-services.component.html @@ -7,7 +7,7 @@

{{ 'preprints.services.title' | translate }}

@for (preprintProvider of preprintProvidersToAdvertise(); track preprintProvider.id) { { const mockPreprintId = 'test_preprint_123'; beforeEach(async () => { - jest.spyOn(BrowserTabHelper, 'updateTabStyles').mockImplementation(() => {}); - jest.spyOn(BrowserTabHelper, 'resetToDefaults').mockImplementation(() => {}); - jest.spyOn(HeaderStyleHelper, 'applyHeaderStyles').mockImplementation(() => {}); - jest.spyOn(HeaderStyleHelper, 'resetToDefaults').mockImplementation(() => {}); - jest.spyOn(BrandService, 'applyBranding').mockImplementation(() => {}); - jest.spyOn(BrandService, 'resetBranding').mockImplementation(() => {}); - routerMock = RouterMockBuilder.create().withNavigate(jest.fn().mockResolvedValue(true)).build(); routeMock = ActivatedRouteMockBuilder.create() .withParams({ providerId: mockProviderId, preprintId: mockPreprintId }) @@ -63,9 +56,11 @@ describe('CreateNewVersionComponent', () => { providers: [ TranslationServiceMock, MockProvider(BrandService), + MockProvider(BrowserTabService), + MockProvider(HeaderStyleService), MockProvider(Router, routerMock), MockProvider(ActivatedRoute, routeMock), - { provide: IS_WEB, useValue: of(true) }, + MockProvider(IS_WEB, of(true)), provideMockStore({ signals: [ { @@ -101,10 +96,6 @@ describe('CreateNewVersionComponent', () => { jest.restoreAllMocks(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - it('should initialize with correct default values', () => { expect(component.PreprintSteps).toBe(PreprintSteps); expect(component.newVersionSteps).toBe(createNewVersionStepsConst); diff --git a/src/app/features/preprints/pages/create-new-version/create-new-version.component.ts b/src/app/features/preprints/pages/create-new-version/create-new-version.component.ts index d59bfa609..26e2700a0 100644 --- a/src/app/features/preprints/pages/create-new-version/create-new-version.component.ts +++ b/src/app/features/preprints/pages/create-new-version/create-new-version.component.ts @@ -23,11 +23,11 @@ import { ActivatedRoute, Router } from '@angular/router'; import { ResetState } from '@osf/features/files/store'; import { StepperComponent } from '@osf/shared/components/stepper/stepper.component'; import { IS_WEB } from '@osf/shared/helpers/breakpoints.tokens'; -import { BrowserTabHelper } from '@osf/shared/helpers/browser-tab.helper'; -import { HeaderStyleHelper } from '@osf/shared/helpers/header-style.helper'; import { CanDeactivateComponent } from '@osf/shared/models/can-deactivate.interface'; import { StepOption } from '@osf/shared/models/step-option.model'; import { BrandService } from '@osf/shared/services/brand.service'; +import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; +import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { FileStepComponent, ReviewStepComponent } from '../../components'; import { createNewVersionStepsConst } from '../../constants'; @@ -48,6 +48,9 @@ export class CreateNewVersionComponent implements OnInit, OnDestroy, CanDeactiva private route = inject(ActivatedRoute); private router = inject(Router); + private brandService = inject(BrandService); + private headerStyleHelper = inject(HeaderStyleService); + private browserTabHelper = inject(BrowserTabService); private providerId = toSignal(this.route.params.pipe(map((params) => params['providerId'])) ?? of(undefined)); private preprintId = toSignal(this.route.params.pipe(map((params) => params['preprintId'])) ?? of(undefined)); @@ -75,13 +78,13 @@ export class CreateNewVersionComponent implements OnInit, OnDestroy, CanDeactiva if (provider) { this.actions.setSelectedPreprintProviderId(provider.id); - BrandService.applyBranding(provider.brand); - HeaderStyleHelper.applyHeaderStyles( + this.brandService.applyBranding(provider.brand); + this.headerStyleHelper.applyHeaderStyles( provider.brand.primaryColor, provider.brand.secondaryColor, provider.brand.heroBackgroundImageUrl ); - BrowserTabHelper.updateTabStyles(provider.faviconUrl, provider.name); + this.browserTabHelper.updateTabStyles(provider.faviconUrl, provider.name); } }); } @@ -98,9 +101,9 @@ export class CreateNewVersionComponent implements OnInit, OnDestroy, CanDeactiva } ngOnDestroy() { - HeaderStyleHelper.resetToDefaults(); - BrandService.resetBranding(); - BrowserTabHelper.resetToDefaults(); + this.headerStyleHelper.resetToDefaults(); + this.brandService.resetBranding(); + this.browserTabHelper.resetToDefaults(); this.actions.resetState(); } diff --git a/src/app/features/preprints/pages/landing/preprints-landing.component.ts b/src/app/features/preprints/pages/landing/preprints-landing.component.ts index b2d04393e..b6bcfc29d 100644 --- a/src/app/features/preprints/pages/landing/preprints-landing.component.ts +++ b/src/app/features/preprints/pages/landing/preprints-landing.component.ts @@ -47,6 +47,7 @@ export class PreprintsLandingComponent implements OnInit, OnDestroy { searchControl = new FormControl(''); private readonly environment = inject(ENVIRONMENT); + private readonly brandService = inject(BrandService); readonly supportEmail = this.environment.supportEmail; private readonly OSF_PROVIDER_ID = this.environment.defaultProvider; @@ -69,7 +70,7 @@ export class PreprintsLandingComponent implements OnInit, OnDestroy { const provider = this.osfPreprintProvider(); if (provider) { - BrandService.applyBranding(provider.brand); + this.brandService.applyBranding(provider.brand); } }); } @@ -81,7 +82,7 @@ export class PreprintsLandingComponent implements OnInit, OnDestroy { } ngOnDestroy() { - BrandService.resetBranding(); + this.brandService.resetBranding(); } redirectToSearchPageWithValue() { diff --git a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts index 819db4e62..21ccc8848 100644 --- a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts +++ b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.spec.ts @@ -5,9 +5,9 @@ import { FormControl } from '@angular/forms'; import { ActivatedRoute } from '@angular/router'; import { GlobalSearchComponent } from '@osf/shared/components/global-search/global-search.component'; -import { BrowserTabHelper } from '@osf/shared/helpers/browser-tab.helper'; -import { HeaderStyleHelper } from '@osf/shared/helpers/header-style.helper'; import { BrandService } from '@osf/shared/services/brand.service'; +import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; +import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { PreprintProviderHeroComponent } from '../../components'; import { PreprintProviderDetails } from '../../models'; @@ -29,13 +29,6 @@ describe('PreprintProviderDiscoverComponent', () => { const mockProviderId = 'osf'; beforeEach(async () => { - jest.spyOn(BrowserTabHelper, 'updateTabStyles').mockImplementation(() => {}); - jest.spyOn(BrowserTabHelper, 'resetToDefaults').mockImplementation(() => {}); - jest.spyOn(HeaderStyleHelper, 'applyHeaderStyles').mockImplementation(() => {}); - jest.spyOn(HeaderStyleHelper, 'resetToDefaults').mockImplementation(() => {}); - jest.spyOn(BrandService, 'applyBranding').mockImplementation(() => {}); - jest.spyOn(BrandService, 'resetBranding').mockImplementation(() => {}); - routeMock = ActivatedRouteMockBuilder.create() .withParams({ providerId: mockProviderId }) .withQueryParams({}) @@ -48,6 +41,9 @@ describe('PreprintProviderDiscoverComponent', () => { ...MockComponents(PreprintProviderHeroComponent, GlobalSearchComponent), ], providers: [ + MockProvider(BrandService), + MockProvider(BrowserTabService), + MockProvider(HeaderStyleService), MockProvider(ActivatedRoute, routeMock), provideMockStore({ signals: [ @@ -69,10 +65,6 @@ describe('PreprintProviderDiscoverComponent', () => { fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - it('should initialize with correct default values', () => { expect(component.providerId).toBe(mockProviderId); expect(component.classes).toBe('flex-1 flex flex-column w-full h-full'); diff --git a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts index 298f4624f..577947d2f 100644 --- a/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts +++ b/src/app/features/preprints/pages/preprint-provider-discover/preprint-provider-discover.component.ts @@ -6,9 +6,9 @@ import { ActivatedRoute } from '@angular/router'; import { GlobalSearchComponent } from '@osf/shared/components/global-search/global-search.component'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; -import { BrowserTabHelper } from '@osf/shared/helpers/browser-tab.helper'; -import { HeaderStyleHelper } from '@osf/shared/helpers/header-style.helper'; import { BrandService } from '@osf/shared/services/brand.service'; +import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; +import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { SetDefaultFilterValue, SetResourceType } from '@osf/shared/stores/global-search'; import { PreprintProviderHeroComponent } from '../../components'; @@ -25,6 +25,9 @@ export class PreprintProviderDiscoverComponent implements OnInit, OnDestroy { @HostBinding('class') classes = 'flex-1 flex flex-column w-full h-full'; private readonly activatedRoute = inject(ActivatedRoute); + private readonly brandService = inject(BrandService); + private readonly headerStyleHelper = inject(HeaderStyleService); + private readonly browserTabHelper = inject(BrowserTabService); private actions = createDispatchMap({ getPreprintProviderById: GetPreprintProviderById, @@ -50,21 +53,21 @@ export class PreprintProviderDiscoverComponent implements OnInit, OnDestroy { this.actions.setResourceType(ResourceType.Preprint); this.defaultSearchFiltersInitialized.set(true); - BrandService.applyBranding(provider.brand); - HeaderStyleHelper.applyHeaderStyles( + this.brandService.applyBranding(provider.brand); + this.headerStyleHelper.applyHeaderStyles( provider.brand.primaryColor, provider.brand.secondaryColor, provider.brand.heroBackgroundImageUrl ); - BrowserTabHelper.updateTabStyles(provider.faviconUrl, provider.name); + this.browserTabHelper.updateTabStyles(provider.faviconUrl, provider.name); } }, }); } ngOnDestroy() { - HeaderStyleHelper.resetToDefaults(); - BrandService.resetBranding(); - BrowserTabHelper.resetToDefaults(); + this.headerStyleHelper.resetToDefaults(); + this.brandService.resetBranding(); + this.browserTabHelper.resetToDefaults(); } } diff --git a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts index 363c14880..8c307f9ab 100644 --- a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts +++ b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.spec.ts @@ -3,9 +3,9 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; -import { BrowserTabHelper } from '@osf/shared/helpers/browser-tab.helper'; -import { HeaderStyleHelper } from '@osf/shared/helpers/header-style.helper'; import { BrandService } from '@osf/shared/services/brand.service'; +import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; +import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { AdvisoryBoardComponent, @@ -36,13 +36,6 @@ describe('PreprintProviderOverviewComponent', () => { const mockProviderId = 'osf'; beforeEach(async () => { - jest.spyOn(BrowserTabHelper, 'updateTabStyles').mockImplementation(() => {}); - jest.spyOn(BrowserTabHelper, 'resetToDefaults').mockImplementation(() => {}); - jest.spyOn(HeaderStyleHelper, 'applyHeaderStyles').mockImplementation(() => {}); - jest.spyOn(HeaderStyleHelper, 'resetToDefaults').mockImplementation(() => {}); - jest.spyOn(BrandService, 'applyBranding').mockImplementation(() => {}); - jest.spyOn(BrandService, 'resetBranding').mockImplementation(() => {}); - routerMock = RouterMockBuilder.create().withNavigate(jest.fn().mockResolvedValue(true)).build(); routeMock = ActivatedRouteMockBuilder.create() .withParams({ providerId: mockProviderId }) @@ -62,6 +55,8 @@ describe('PreprintProviderOverviewComponent', () => { ], providers: [ MockProvider(BrandService), + MockProvider(BrowserTabService), + MockProvider(HeaderStyleService), MockProvider(Router, routerMock), MockProvider(ActivatedRoute, routeMock), provideMockStore({ @@ -92,10 +87,6 @@ describe('PreprintProviderOverviewComponent', () => { fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - it('should initialize with correct default values', () => { expect(component.preprintProvider).toBeDefined(); expect(component.isPreprintProviderLoading).toBeDefined(); diff --git a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts index 05cc5d4bc..c38ab13f4 100644 --- a/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts +++ b/src/app/features/preprints/pages/preprint-provider-overview/preprint-provider-overview.component.ts @@ -6,9 +6,9 @@ import { ChangeDetectionStrategy, Component, effect, inject, OnDestroy, OnInit } import { toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router } from '@angular/router'; -import { BrowserTabHelper } from '@osf/shared/helpers/browser-tab.helper'; -import { HeaderStyleHelper } from '@osf/shared/helpers/header-style.helper'; import { BrandService } from '@osf/shared/services/brand.service'; +import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; +import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { AdvisoryBoardComponent, @@ -37,6 +37,9 @@ import { export class PreprintProviderOverviewComponent implements OnInit, OnDestroy { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); + private readonly brandService = inject(BrandService); + private readonly headerStyleHelper = inject(HeaderStyleService); + private readonly browserTabHelper = inject(BrowserTabService); private providerId = toSignal(this.route.params.pipe(map((params) => params['providerId'])) ?? of(undefined)); private actions = createDispatchMap({ @@ -54,13 +57,13 @@ export class PreprintProviderOverviewComponent implements OnInit, OnDestroy { const provider = this.preprintProvider(); if (provider) { - BrandService.applyBranding(provider.brand); - HeaderStyleHelper.applyHeaderStyles( + this.brandService.applyBranding(provider.brand); + this.headerStyleHelper.applyHeaderStyles( provider.brand.primaryColor, provider.brand.secondaryColor, provider.brand.heroBackgroundImageUrl ); - BrowserTabHelper.updateTabStyles(provider.faviconUrl, provider.name); + this.browserTabHelper.updateTabStyles(provider.faviconUrl, provider.name); } }); } @@ -71,9 +74,9 @@ export class PreprintProviderOverviewComponent implements OnInit, OnDestroy { } ngOnDestroy() { - HeaderStyleHelper.resetToDefaults(); - BrandService.resetBranding(); - BrowserTabHelper.resetToDefaults(); + this.headerStyleHelper.resetToDefaults(); + this.brandService.resetBranding(); + this.browserTabHelper.resetToDefaults(); } redirectToDiscoverPageWithValue(searchValue: string) { diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts index c20849a1c..50920be2a 100644 --- a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.spec.ts @@ -2,16 +2,15 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { StepperComponent } from '@osf/shared/components/stepper/stepper.component'; import { IS_WEB } from '@osf/shared/helpers/breakpoints.tokens'; -import { BrowserTabHelper } from '@osf/shared/helpers/browser-tab.helper'; -import { HeaderStyleHelper } from '@osf/shared/helpers/header-style.helper'; import { StepOption } from '@osf/shared/models/step-option.model'; import { BrandService } from '@osf/shared/services/brand.service'; +import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; +import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { AuthorAssertionsStepComponent, @@ -45,13 +44,6 @@ describe('SubmitPreprintStepperComponent', () => { const mockProviderId = 'osf'; beforeEach(async () => { - jest.spyOn(BrowserTabHelper, 'updateTabStyles').mockImplementation(() => {}); - jest.spyOn(BrowserTabHelper, 'resetToDefaults').mockImplementation(() => {}); - jest.spyOn(HeaderStyleHelper, 'applyHeaderStyles').mockImplementation(() => {}); - jest.spyOn(HeaderStyleHelper, 'resetToDefaults').mockImplementation(() => {}); - jest.spyOn(BrandService, 'applyBranding').mockImplementation(() => {}); - jest.spyOn(BrandService, 'resetBranding').mockImplementation(() => {}); - routerMock = RouterMockBuilder.create().withNavigate(jest.fn().mockResolvedValue(true)).build(); routeMock = ActivatedRouteMockBuilder.create() .withParams({ providerId: mockProviderId }) @@ -75,9 +67,11 @@ describe('SubmitPreprintStepperComponent', () => { ], providers: [ MockProvider(BrandService), + MockProvider(BrowserTabService), + MockProvider(HeaderStyleService), MockProvider(Router, routerMock), MockProvider(ActivatedRoute, routeMock), - { provide: IS_WEB, useValue: of(true) }, + MockProvider(IS_WEB, of(true)), provideMockStore({ signals: [ { @@ -95,7 +89,6 @@ describe('SubmitPreprintStepperComponent', () => { ], }), ], - schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(SubmitPreprintStepperComponent); @@ -103,10 +96,6 @@ describe('SubmitPreprintStepperComponent', () => { fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - it('should initialize with correct default values', () => { expect(component.SubmitStepsEnum).toBe(PreprintSteps); expect(component.classes).toBe('flex-1 flex flex-column w-full'); diff --git a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts index 63e47a8c8..bf3e1cb35 100644 --- a/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts +++ b/src/app/features/preprints/pages/submit-preprint-stepper/submit-preprint-stepper.component.ts @@ -24,11 +24,11 @@ import { ActivatedRoute } from '@angular/router'; import { ResetState } from '@osf/features/files/store'; import { StepperComponent } from '@osf/shared/components/stepper/stepper.component'; import { IS_WEB } from '@osf/shared/helpers/breakpoints.tokens'; -import { BrowserTabHelper } from '@osf/shared/helpers/browser-tab.helper'; -import { HeaderStyleHelper } from '@osf/shared/helpers/header-style.helper'; import { CanDeactivateComponent } from '@osf/shared/models/can-deactivate.interface'; import { StepOption } from '@osf/shared/models/step-option.model'; import { BrandService } from '@osf/shared/services/brand.service'; +import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; +import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { AuthorAssertionsStepComponent, @@ -65,6 +65,9 @@ export class SubmitPreprintStepperComponent implements OnInit, OnDestroy, CanDea @HostBinding('class') classes = 'flex-1 flex flex-column w-full'; private readonly route = inject(ActivatedRoute); + private readonly brandService = inject(BrandService); + private readonly headerStyleHelper = inject(HeaderStyleService); + private readonly browserTabHelper = inject(BrowserTabService); private providerId = toSignal(this.route.params.pipe(map((params) => params['providerId'])) ?? of(undefined)); @@ -111,13 +114,13 @@ export class SubmitPreprintStepperComponent implements OnInit, OnDestroy, CanDea if (provider) { this.actions.setSelectedPreprintProviderId(provider.id); - BrandService.applyBranding(provider.brand); - HeaderStyleHelper.applyHeaderStyles( + this.brandService.applyBranding(provider.brand); + this.headerStyleHelper.applyHeaderStyles( provider.brand.primaryColor, provider.brand.secondaryColor, provider.brand.heroBackgroundImageUrl ); - BrowserTabHelper.updateTabStyles(provider.faviconUrl, provider.name); + this.browserTabHelper.updateTabStyles(provider.faviconUrl, provider.name); } }); } @@ -131,9 +134,9 @@ export class SubmitPreprintStepperComponent implements OnInit, OnDestroy, CanDea } ngOnDestroy() { - HeaderStyleHelper.resetToDefaults(); - BrandService.resetBranding(); - BrowserTabHelper.resetToDefaults(); + this.headerStyleHelper.resetToDefaults(); + this.brandService.resetBranding(); + this.browserTabHelper.resetToDefaults(); this.actions.deletePreprint(); this.actions.resetState(); } diff --git a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts index 0e399ebe9..7a57296bf 100644 --- a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts +++ b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.spec.ts @@ -2,16 +2,15 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; -import { NO_ERRORS_SCHEMA } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; import { ActivatedRoute, Router } from '@angular/router'; import { StepperComponent } from '@osf/shared/components/stepper/stepper.component'; import { IS_WEB } from '@osf/shared/helpers/breakpoints.tokens'; -import { BrowserTabHelper } from '@osf/shared/helpers/browser-tab.helper'; -import { HeaderStyleHelper } from '@osf/shared/helpers/header-style.helper'; import { StepOption } from '@osf/shared/models/step-option.model'; import { BrandService } from '@osf/shared/services/brand.service'; +import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; +import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { AuthorAssertionsStepComponent, @@ -48,13 +47,6 @@ describe('UpdatePreprintStepperComponent', () => { const mockPreprintId = 'test_preprint_123'; beforeEach(async () => { - jest.spyOn(BrowserTabHelper, 'updateTabStyles').mockImplementation(() => {}); - jest.spyOn(BrowserTabHelper, 'resetToDefaults').mockImplementation(() => {}); - jest.spyOn(HeaderStyleHelper, 'applyHeaderStyles').mockImplementation(() => {}); - jest.spyOn(HeaderStyleHelper, 'resetToDefaults').mockImplementation(() => {}); - jest.spyOn(BrandService, 'applyBranding').mockImplementation(() => {}); - jest.spyOn(BrandService, 'resetBranding').mockImplementation(() => {}); - routerMock = RouterMockBuilder.create().withNavigate(jest.fn().mockResolvedValue(true)).build(); routeMock = ActivatedRouteMockBuilder.create() .withParams({ providerId: mockProviderId, preprintId: mockPreprintId }) @@ -77,9 +69,11 @@ describe('UpdatePreprintStepperComponent', () => { ], providers: [ MockProvider(BrandService), + MockProvider(BrowserTabService), + MockProvider(HeaderStyleService), MockProvider(Router, routerMock), MockProvider(ActivatedRoute, routeMock), - { provide: IS_WEB, useValue: of(true) }, + MockProvider(IS_WEB, of(true)), provideMockStore({ signals: [ { @@ -101,7 +95,6 @@ describe('UpdatePreprintStepperComponent', () => { ], }), ], - schemas: [NO_ERRORS_SCHEMA], }).compileComponents(); fixture = TestBed.createComponent(UpdatePreprintStepperComponent); @@ -109,10 +102,6 @@ describe('UpdatePreprintStepperComponent', () => { fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - it('should initialize with correct default values', () => { expect(component.PreprintSteps).toBe(PreprintSteps); expect(component.classes).toBe('flex-1 flex flex-column w-full'); diff --git a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.ts b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.ts index 963db1228..b22cfbd3b 100644 --- a/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.ts +++ b/src/app/features/preprints/pages/update-preprint-stepper/update-preprint-stepper.component.ts @@ -25,9 +25,9 @@ import { ResetState } from '@osf/features/files/store'; import { StepperComponent } from '@osf/shared/components/stepper/stepper.component'; import { UserPermissions } from '@osf/shared/enums/user-permissions.enum'; import { IS_WEB } from '@osf/shared/helpers/breakpoints.tokens'; -import { BrowserTabHelper } from '@osf/shared/helpers/browser-tab.helper'; -import { HeaderStyleHelper } from '@osf/shared/helpers/header-style.helper'; import { BrandService } from '@osf/shared/services/brand.service'; +import { BrowserTabService } from '@osf/shared/services/browser-tab.service'; +import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { CanDeactivateComponent } from '@shared/models/can-deactivate.interface'; import { StepOption } from '@shared/models/step-option.model'; @@ -66,6 +66,9 @@ export class UpdatePreprintStepperComponent implements OnInit, OnDestroy, CanDea @HostBinding('class') classes = 'flex-1 flex flex-column w-full'; private readonly route = inject(ActivatedRoute); + private readonly brandService = inject(BrandService); + private readonly headerStyleHelper = inject(HeaderStyleService); + private readonly browserTabHelper = inject(BrowserTabService); private providerId = toSignal(this.route.params.pipe(map((params) => params['providerId'])) ?? of(undefined)); private preprintId = toSignal(this.route.params.pipe(map((params) => params['preprintId'])) ?? of(undefined)); @@ -138,13 +141,13 @@ export class UpdatePreprintStepperComponent implements OnInit, OnDestroy, CanDea if (provider) { this.actions.setSelectedPreprintProviderId(provider.id); - BrandService.applyBranding(provider.brand); - HeaderStyleHelper.applyHeaderStyles( + this.brandService.applyBranding(provider.brand); + this.headerStyleHelper.applyHeaderStyles( provider.brand.primaryColor, provider.brand.secondaryColor, provider.brand.heroBackgroundImageUrl ); - BrowserTabHelper.updateTabStyles(provider.faviconUrl, provider.name); + this.browserTabHelper.updateTabStyles(provider.faviconUrl, provider.name); } }); } @@ -165,9 +168,9 @@ export class UpdatePreprintStepperComponent implements OnInit, OnDestroy, CanDea } ngOnDestroy() { - HeaderStyleHelper.resetToDefaults(); - BrandService.resetBranding(); - BrowserTabHelper.resetToDefaults(); + this.headerStyleHelper.resetToDefaults(); + this.brandService.resetBranding(); + this.browserTabHelper.resetToDefaults(); this.actions.resetState(); } diff --git a/src/app/features/project/overview/components/files-widget/files-widget.component.ts b/src/app/features/project/overview/components/files-widget/files-widget.component.ts index be019d008..605e5825d 100644 --- a/src/app/features/project/overview/components/files-widget/files-widget.component.ts +++ b/src/app/features/project/overview/components/files-widget/files-widget.component.ts @@ -32,7 +32,6 @@ import { import { FilesTreeComponent } from '@osf/shared/components/files-tree/files-tree.component'; import { SelectComponent } from '@osf/shared/components/select/select.component'; import { Primitive } from '@osf/shared/helpers/types.helper'; -import { getViewOnlyParamFromUrl, hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; import { ConfiguredAddonModel } from '@osf/shared/models/addons/configured-addon.model'; import { FileModel } from '@osf/shared/models/files/file.model'; import { FileFolderModel } from '@osf/shared/models/files/file-folder.model'; @@ -40,6 +39,7 @@ import { FileLabelModel } from '@osf/shared/models/files/file-label.model'; import { NodeShortInfoModel } from '@osf/shared/models/nodes/node-with-children.model'; import { ProjectModel } from '@osf/shared/models/projects/projects.models'; import { SelectOption } from '@osf/shared/models/select-option.model'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; @Component({ selector: 'osf-files-widget', @@ -57,6 +57,7 @@ export class FilesWidgetComponent { private readonly environment = inject(ENVIRONMENT); private readonly destroyRef = inject(DestroyRef); + private readonly viewOnlyService = inject(ViewOnlyService); readonly files = select(FilesSelectors.getFiles); readonly filesTotalCount = select(FilesSelectors.getFilesTotalCount); @@ -90,7 +91,7 @@ export class FilesWidgetComponent { return []; }); - readonly hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + readonly hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); private readonly actions = createDispatchMap({ getFiles: GetFiles, @@ -220,7 +221,7 @@ export class FilesWidgetComponent { navigateToFile(file: FileModel) { const extras = this.hasViewOnly() - ? { queryParams: { view_only: getViewOnlyParamFromUrl(this.router.url) } } + ? { queryParams: { view_only: this.viewOnlyService.getViewOnlyParamFromUrl(this.router.url) } } : undefined; const url = this.router.serializeUrl(this.router.createUrlTree(['/', file.guid], extras)); diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index b4ee62c9a..4c5d5ea83 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -30,9 +30,9 @@ import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-link-message/view-only-link-message.component'; import { Mode } from '@osf/shared/enums/mode.enum'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; -import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { ToastService } from '@osf/shared/services/toast.service'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { GetActivityLogs } from '@osf/shared/stores/activity-logs'; import { AddonsSelectors, @@ -98,6 +98,7 @@ export class ProjectOverviewComponent implements OnInit { private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); private readonly toastService = inject(ToastService); + private readonly viewOnlyService = inject(ViewOnlyService); private readonly customDialogService = inject(CustomDialogService); readonly analyticsService = inject(AnalyticsService); @@ -164,7 +165,7 @@ export class ProjectOverviewComponent implements OnInit { this.submissionReviewStatus() !== SubmissionReviewStatus.Rejected ); - hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); filesRootOption = computed(() => ({ value: this.currentProject()?.id ?? '', diff --git a/src/app/features/project/wiki/wiki.component.ts b/src/app/features/project/wiki/wiki.component.ts index d57a8cd9f..bd35d6329 100644 --- a/src/app/features/project/wiki/wiki.component.ts +++ b/src/app/features/project/wiki/wiki.component.ts @@ -1,6 +1,6 @@ import { createDispatchMap, select } from '@ngxs/store'; -import { TranslatePipe, TranslateService } from '@ngx-translate/core'; +import { TranslatePipe } from '@ngx-translate/core'; import { Button } from 'primeng/button'; import { ButtonGroupModule } from 'primeng/buttongroup'; @@ -17,9 +17,9 @@ import { EditSectionComponent } from '@osf/shared/components/wiki/edit-section/e import { ViewSectionComponent } from '@osf/shared/components/wiki/view-section/view-section.component'; import { WikiListComponent } from '@osf/shared/components/wiki/wiki-list/wiki-list.component'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; -import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; import { WikiModes } from '@osf/shared/models/wiki/wiki.model'; import { ToastService } from '@osf/shared/services/toast.service'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; import { ClearWiki, @@ -61,8 +61,8 @@ export class WikiComponent { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); - private toastService = inject(ToastService); - private readonly translateService = inject(TranslateService); + private readonly toastService = inject(ToastService); + private readonly viewOnlyService = inject(ViewOnlyService); WikiModes = WikiModes; homeWikiName = 'Home'; @@ -81,7 +81,7 @@ export class WikiComponent { isWikiVersionLoading = select(WikiSelectors.getWikiVersionsLoading); isCompareVersionLoading = select(WikiSelectors.getCompareVersionsLoading); isAnonymous = select(WikiSelectors.isWikiAnonymous); - hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); hasWriteAccess = select(CurrentResourceSelectors.hasWriteAccess); hasAdminAccess = select(CurrentResourceSelectors.hasAdminAccess); diff --git a/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.ts b/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.ts index 52f770e9f..3ba019880 100644 --- a/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.ts +++ b/src/app/features/registries/components/registry-provider-hero/registry-provider-hero.component.ts @@ -10,9 +10,9 @@ import { Router } from '@angular/router'; import { PreprintsHelpDialogComponent } from '@osf/features/preprints/components'; import { SearchInputComponent } from '@osf/shared/components/search-input/search-input.component'; -import { HeaderStyleHelper } from '@osf/shared/helpers/header-style.helper'; import { BrandService } from '@osf/shared/services/brand.service'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { HeaderStyleService } from '@osf/shared/services/header-style.service'; import { RegistryProviderDetails } from '@shared/models/provider/registry-provider.model'; import { DecodeHtmlPipe } from '@shared/pipes/decode-html.pipe'; @@ -26,6 +26,8 @@ import { DecodeHtmlPipe } from '@shared/pipes/decode-html.pipe'; export class RegistryProviderHeroComponent implements OnDestroy { private readonly router = inject(Router); private readonly customDialogService = inject(CustomDialogService); + private readonly brandService = inject(BrandService); + private readonly headerStyleHelper = inject(HeaderStyleService); private readonly WHITE = '#ffffff'; searchControl = input(new FormControl()); @@ -42,8 +44,8 @@ export class RegistryProviderHeroComponent implements OnDestroy { const provider = this.provider(); if (provider?.brand) { - BrandService.applyBranding(provider.brand); - HeaderStyleHelper.applyHeaderStyles( + this.brandService.applyBranding(provider.brand); + this.headerStyleHelper.applyHeaderStyles( this.WHITE, provider.brand.primaryColor, provider.brand.heroBackgroundImageUrl @@ -53,8 +55,8 @@ export class RegistryProviderHeroComponent implements OnDestroy { } ngOnDestroy() { - HeaderStyleHelper.resetToDefaults(); - BrandService.resetBranding(); + this.headerStyleHelper.resetToDefaults(); + this.brandService.resetBranding(); } openHelpDialog() { diff --git a/src/app/features/registry/pages/registry-components/registry-components.component.ts b/src/app/features/registry/pages/registry-components/registry-components.component.ts index 55c7ec4ce..7e3bd800f 100644 --- a/src/app/features/registry/pages/registry-components/registry-components.component.ts +++ b/src/app/features/registry/pages/registry-components/registry-components.component.ts @@ -8,7 +8,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-link-message/view-only-link-message.component'; -import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { RegistrationLinksCardComponent } from '../../components'; import { GetRegistryComponents, RegistryComponentsSelectors } from '../../store/registry-components'; @@ -29,12 +29,13 @@ import { GetRegistryComponents, RegistryComponentsSelectors } from '../../store/ export class RegistryComponentsComponent implements OnInit { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); + private readonly viewOnlyService = inject(ViewOnlyService); private registryId = signal(''); actions = createDispatchMap({ getRegistryComponents: GetRegistryComponents }); - hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); registryComponents = select(RegistryComponentsSelectors.getRegistryComponents); registryComponentsLoading = select(RegistryComponentsSelectors.getRegistryComponentsLoading); diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts index 2638f3f5a..664318f9b 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts @@ -28,10 +28,10 @@ import { RegistrationReviewStates } from '@osf/shared/enums/registration-review- import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { RevisionReviewStates } from '@osf/shared/enums/revision-review-states.enum'; import { toCamelCase } from '@osf/shared/helpers/camel-case'; -import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { LoaderService } from '@osf/shared/services/loader.service'; import { ToastService } from '@osf/shared/services/toast.service'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { GetBookmarksCollectionId } from '@osf/shared/stores/bookmarks'; import { ContributorsSelectors, GetBibliographicContributors } from '@osf/shared/stores/contributors'; import { SchemaResponse } from '@shared/models/registration/schema-response.model'; @@ -83,6 +83,7 @@ export class RegistryOverviewComponent { private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); private readonly toastService = inject(ToastService); + private readonly viewOnlyService = inject(ViewOnlyService); private readonly customDialogService = inject(CustomDialogService); private readonly loaderService = inject(LoaderService); @@ -140,7 +141,7 @@ export class RegistryOverviewComponent { revisionId: string | null = null; isModeration = false; - hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); get isInitialState(): boolean { return this.registry()?.reviewsState === RegistrationReviewStates.Initial; diff --git a/src/app/features/registry/pages/registry-wiki/registry-wiki.component.ts b/src/app/features/registry/pages/registry-wiki/registry-wiki.component.ts index 322808dfd..c186b4555 100644 --- a/src/app/features/registry/pages/registry-wiki/registry-wiki.component.ts +++ b/src/app/features/registry/pages/registry-wiki/registry-wiki.component.ts @@ -17,8 +17,8 @@ import { CompareSectionComponent } from '@osf/shared/components/wiki/compare-sec import { ViewSectionComponent } from '@osf/shared/components/wiki/view-section/view-section.component'; import { WikiListComponent } from '@osf/shared/components/wiki/wiki-list/wiki-list.component'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; -import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; import { WikiModes } from '@osf/shared/models/wiki/wiki.model'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { ClearWiki, GetCompareVersionContent, @@ -52,6 +52,7 @@ export class RegistryWikiComponent { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); + private readonly viewOnlyService = inject(ViewOnlyService); WikiModes = WikiModes; wikiModes = select(WikiSelectors.getWikiModes); @@ -65,7 +66,7 @@ export class RegistryWikiComponent { isWikiVersionLoading = select(WikiSelectors.getWikiVersionsLoading); componentsWikiList = select(WikiSelectors.getComponentsWikiList); - hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); readonly resourceId = this.route.parent?.snapshot.params['id']; diff --git a/src/app/shared/components/file-menu/file-menu.component.ts b/src/app/shared/components/file-menu/file-menu.component.ts index 6135fb4bc..66752d347 100644 --- a/src/app/shared/components/file-menu/file-menu.component.ts +++ b/src/app/shared/components/file-menu/file-menu.component.ts @@ -8,8 +8,8 @@ import { Component, computed, inject, input, output, viewChild } from '@angular/ import { Router } from '@angular/router'; import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum'; -import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; import { MenuManagerService } from '@osf/shared/services/menu-manager.service'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { FileMenuAction, FileMenuData, FileMenuFlags } from '@shared/models/files/file-menu-action.model'; @Component({ @@ -21,12 +21,14 @@ import { FileMenuAction, FileMenuData, FileMenuFlags } from '@shared/models/file export class FileMenuComponent { private router = inject(Router); private menuManager = inject(MenuManagerService); + private viewOnlyService = inject(ViewOnlyService); + isFolder = input(false); allowedActions = input({} as FileMenuFlags); menu = viewChild.required('menu'); action = output(); - hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); private readonly allMenuItems: MenuItem[] = [ { diff --git a/src/app/shared/components/files-tree/files-tree.component.ts b/src/app/shared/components/files-tree/files-tree.component.ts index d01320220..8055d0fb6 100644 --- a/src/app/shared/components/files-tree/files-tree.component.ts +++ b/src/app/shared/components/files-tree/files-tree.component.ts @@ -6,7 +6,7 @@ import { PrimeTemplate } from 'primeng/api'; import { Tree, TreeNodeDropEvent, TreeNodeSelectEvent, TreeScrollIndexChangeEvent } from 'primeng/tree'; import { Clipboard } from '@angular/cdk/clipboard'; -import { DatePipe } from '@angular/common'; +import { DatePipe, isPlatformBrowser } from '@angular/common'; import { AfterViewInit, ChangeDetectionStrategy, @@ -20,6 +20,7 @@ import { input, OnDestroy, output, + PLATFORM_ID, signal, viewChild, } from '@angular/core'; @@ -34,7 +35,6 @@ import { embedDynamicJs, embedStaticHtml } from '@osf/features/files/constants'; import { StopPropagationDirective } from '@osf/shared/directives/stop-propagation.directive'; import { FileKind } from '@osf/shared/enums/file-kind.enum'; import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum'; -import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; import { FilesMapper } from '@osf/shared/mappers/files/files.mapper'; import { FileSizePipe } from '@osf/shared/pipes/file-size.pipe'; import { CustomConfirmationService } from '@osf/shared/services/custom-confirmation.service'; @@ -42,6 +42,7 @@ import { CustomDialogService } from '@osf/shared/services/custom-dialog.service' import { DataciteService } from '@osf/shared/services/datacite/datacite.service'; import { FilesService } from '@osf/shared/services/files.service'; import { ToastService } from '@osf/shared/services/toast.service'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { FileModel } from '@shared/models/files/file.model'; import { FileFolderModel } from '@shared/models/files/file-folder.model'; import { FileLabelModel } from '@shared/models/files/file-label.model'; @@ -77,9 +78,11 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { readonly customConfirmationService = inject(CustomConfirmationService); readonly customDialogService = inject(CustomDialogService); readonly dataciteService = inject(DataciteService); + private readonly viewOnlyService = inject(ViewOnlyService); private readonly destroyRef = inject(DestroyRef); private readonly environment = inject(ENVIRONMENT); + private readonly platformId = inject(PLATFORM_ID); readonly clipboard = inject(Clipboard); files = input.required(); @@ -119,7 +122,7 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { isDragOver = signal(false); isLoadingMore = signal(false); - hasViewOnly = computed(() => hasViewOnlyParam(this.router) || this.viewOnly()); + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router) || this.viewOnly()); visibleFilesCount = computed((): number => { const height = parseInt(this.scrollHeight(), 10); return Math.ceil(height / this.virtualScrollItemSize); @@ -374,19 +377,25 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { } downloadFile(link: string): void { - window.open(link)?.focus(); + if (isPlatformBrowser(this.platformId)) { + window.open(link)?.focus(); + } } openLink(link: string): void { - window.location.href = link; + if (isPlatformBrowser(this.platformId)) { + window.location.href = link; + } } openLinkNewTab(link: string): void { - window.open(link, '_blank', 'noopener,noreferrer'); + if (isPlatformBrowser(this.platformId)) { + window.open(link, '_blank', 'noopener,noreferrer'); + } } downloadFolder(downloadLink: string): void { - if (downloadLink) { + if (isPlatformBrowser(this.platformId) && downloadLink) { const link = this.filesService.getFolderDownloadLink(downloadLink); window.open(link, '_blank')?.focus(); } diff --git a/src/app/shared/components/resource-citations/resource-citations.component.ts b/src/app/shared/components/resource-citations/resource-citations.component.ts index 406c82261..96e39530c 100644 --- a/src/app/shared/components/resource-citations/resource-citations.component.ts +++ b/src/app/shared/components/resource-citations/resource-citations.component.ts @@ -28,8 +28,8 @@ import { FormControl, ReactiveFormsModule } from '@angular/forms'; import { Router } from '@angular/router'; import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; -import { hasViewOnlyParam } from '@osf/shared/helpers/view-only.helper'; import { ToastService } from '@osf/shared/services/toast.service'; +import { ViewOnlyService } from '@osf/shared/services/view-only.service'; import { CitationStyle } from '@shared/models/citations/citation-style.model'; import { CustomOption } from '@shared/models/select-option.model'; import { @@ -71,6 +71,7 @@ export class ResourceCitationsComponent { private readonly clipboard = inject(Clipboard); private readonly toastService = inject(ToastService); + private readonly viewOnlyService = inject(ViewOnlyService); private readonly filterSubject = new Subject(); customCitationChange = output(); @@ -90,7 +91,7 @@ export class ResourceCitationsComponent { ); customCitationInput = new FormControl(''); - readonly hasViewOnly = computed(() => hasViewOnlyParam(this.router)); + readonly hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); actions = createDispatchMap({ getDefaultCitations: GetDefaultCitations, diff --git a/src/app/shared/helpers/browser-tab.helper.ts b/src/app/shared/helpers/browser-tab.helper.ts deleted file mode 100644 index f0df8afef..000000000 --- a/src/app/shared/helpers/browser-tab.helper.ts +++ /dev/null @@ -1,22 +0,0 @@ -export class BrowserTabHelper { - private static readonly DEFAULT_FAVICON = '/favicon.ico'; - private static readonly DEFAULT_TITLE = 'OSF'; - - static updateTabStyles(faviconUrl: string, title: string) { - if (faviconUrl) { - const faviconElement = document.querySelector("link[rel*='icon']") as HTMLLinkElement; - faviconElement.href = faviconUrl; - } - - if (title) { - document.title = title; - } - } - - static resetToDefaults() { - const faviconElement = document.querySelector("link[rel*='icon']") as HTMLLinkElement; - faviconElement.href = this.DEFAULT_FAVICON; - - document.title = this.DEFAULT_TITLE; - } -} diff --git a/src/app/shared/helpers/header-style.helper.ts b/src/app/shared/helpers/header-style.helper.ts deleted file mode 100644 index e40447b77..000000000 --- a/src/app/shared/helpers/header-style.helper.ts +++ /dev/null @@ -1,17 +0,0 @@ -export class HeaderStyleHelper { - static applyHeaderStyles(textColor: string, backgroundColor?: string, backgroundImageUrl?: string) { - const root = document.documentElement; - - root.style.setProperty('--header-color', textColor); - root.style.setProperty('--header-background-color', backgroundColor || ''); - root.style.setProperty('--header-background-image-url', `url(${backgroundImageUrl || ''})`); - } - - static resetToDefaults() { - const root = document.documentElement; - - root.style.setProperty('--header-color', ''); - root.style.setProperty('--header-background-color', ''); - root.style.setProperty('--header-background-image-url', ''); - } -} diff --git a/src/app/shared/helpers/view-only.helper.ts b/src/app/shared/helpers/view-only.helper.ts deleted file mode 100644 index 020ddee30..000000000 --- a/src/app/shared/helpers/view-only.helper.ts +++ /dev/null @@ -1,43 +0,0 @@ -import { Router } from '@angular/router'; - -export function hasViewOnlyParam(router: Router): boolean { - const currentUrl = router.url; - const routerParams = new URLSearchParams(currentUrl.split('?')[1] || ''); - - if (typeof window !== 'undefined' && window.location?.search) { - const windowParams = new URLSearchParams(window.location.search); - return routerParams.has('view_only') || windowParams.has('view_only'); - } - - return routerParams.has('view_only'); -} - -export function getViewOnlyParam(router?: Router): string | null { - let currentUrl = ''; - - if (router) { - currentUrl = router.url; - } - - const routerParams = new URLSearchParams(currentUrl.split('?')[1] || ''); - - if (typeof window !== 'undefined' && window.location?.search) { - const windowParams = new URLSearchParams(window.location.search); - return routerParams.get('view_only') || windowParams.get('view_only'); - } - - return routerParams.get('view_only'); -} - -export function getViewOnlyParamFromUrl(currentUrl?: string): string | null { - if (!currentUrl) return null; - - const routerParams = new URLSearchParams(currentUrl.split('?')[1] || ''); - - if (typeof window !== 'undefined' && window.location?.search) { - const windowParams = new URLSearchParams(window.location.search); - return routerParams.get('view_only') || windowParams.get('view_only'); - } - - return routerParams.get('view_only'); -} diff --git a/src/app/shared/mappers/view-only-links.mapper.ts b/src/app/shared/mappers/view-only-links.mapper.ts index e31b339cc..354649c88 100644 --- a/src/app/shared/mappers/view-only-links.mapper.ts +++ b/src/app/shared/mappers/view-only-links.mapper.ts @@ -5,19 +5,23 @@ import { } from '../models/view-only-links/view-only-link.model'; import { ViewOnlyLinkJsonApi, - ViewOnlyLinksResponseJsonApi, + ViewOnlyLinksResponsesJsonApi, } from '../models/view-only-links/view-only-link-response.model'; import { UserMapper } from './user'; export class ViewOnlyLinksMapper { - static fromResponse(response: ViewOnlyLinksResponseJsonApi, projectId: string): PaginatedViewOnlyLinksModel { + static fromResponse( + response: ViewOnlyLinksResponsesJsonApi, + projectId: string, + webUrl: string + ): PaginatedViewOnlyLinksModel { const items: ViewOnlyLinkModel[] = response.data.map((item) => { const creator = UserMapper.getUserInfo(item.embeds.creator); return { id: item.id, - link: `${document.baseURI}${projectId}/overview?view_only=${item.attributes.key}`, + link: `${webUrl}/${projectId}/overview?view_only=${item.attributes.key}`, dateCreated: item.attributes.date_created, key: item.attributes.key, name: item.attributes.name, @@ -46,13 +50,17 @@ export class ViewOnlyLinksMapper { }; } - static fromSingleResponse(response: ViewOnlyLinkJsonApi, projectId: string): PaginatedViewOnlyLinksModel { + static fromSingleResponse( + response: ViewOnlyLinkJsonApi, + projectId: string, + webUrl: string + ): PaginatedViewOnlyLinksModel { const item = response; const creator = UserMapper.getUserInfo(item.embeds.creator); const mappedItem: ViewOnlyLinkModel = { id: item.id, - link: `${document.baseURI}${projectId}/overview?view_only=${item.attributes.key}`, + link: `${webUrl}/${projectId}/overview?view_only=${item.attributes.key}`, dateCreated: item.attributes.date_created, key: item.attributes.key, name: item.attributes.name, diff --git a/src/app/shared/models/view-only-links/view-only-link-response.model.ts b/src/app/shared/models/view-only-links/view-only-link-response.model.ts index 06bbfb1a9..75567f68d 100644 --- a/src/app/shared/models/view-only-links/view-only-link-response.model.ts +++ b/src/app/shared/models/view-only-links/view-only-link-response.model.ts @@ -1,12 +1,9 @@ -import { MetaJsonApi } from '../common/json-api.model'; +import { ResponseJsonApi } from '../common/json-api.model'; import { BaseNodeDataJsonApi } from '../nodes/base-node-data-json-api.model'; import { UserDataErrorResponseJsonApi } from '../user/user-json-api.model'; -export interface ViewOnlyLinksResponseJsonApi { - data: ViewOnlyLinkJsonApi[]; - links: PaginationLinksJsonApi; - meta: MetaJsonApi; -} +export type ViewOnlyLinksResponsesJsonApi = ResponseJsonApi; +export type ViewOnlyLinksResponseJsonApi = ResponseJsonApi; export interface ViewOnlyLinkJsonApi { id: string; @@ -24,15 +21,3 @@ export interface ViewOnlyLinkJsonApi { }; }; } - -export interface LinkWithMetaJsonApi { - href: string; - meta: Record; -} - -interface PaginationLinksJsonApi { - first: string | null; - last: string | null; - prev: string | null; - next: string | null; -} diff --git a/src/app/shared/services/activity-logs/activity-log-url-builder.service.ts b/src/app/shared/services/activity-logs/activity-log-url-builder.service.ts index 761d70e53..e13408b3e 100644 --- a/src/app/shared/services/activity-logs/activity-log-url-builder.service.ts +++ b/src/app/shared/services/activity-logs/activity-log-url-builder.service.ts @@ -1,4 +1,5 @@ -import { Injectable } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; +import { inject, Injectable, PLATFORM_ID } from '@angular/core'; import { ActivityLog } from '@osf/shared/models/activity-logs/activity-logs.model'; @@ -6,6 +7,8 @@ import { ActivityLog } from '@osf/shared/models/activity-logs/activity-logs.mode providedIn: 'root', }) export class ActivityLogUrlBuilderService { + private readonly platformId = inject(PLATFORM_ID); + buildAHrefElement(url: string | undefined, value: string): string { const safeUrl = url || ''; const relativeUrl = this.toRelativeUrl(safeUrl); @@ -147,6 +150,10 @@ export class ActivityLogUrlBuilderService { private toRelativeUrl(url: string): string { if (!url) return ''; + if (!isPlatformBrowser(this.platformId)) { + return url; + } + try { const parser = document.createElement('a'); parser.href = url; diff --git a/src/app/shared/services/addons/addon-oauth.service.ts b/src/app/shared/services/addons/addon-oauth.service.ts index d95fa3f48..3ebfaae3c 100644 --- a/src/app/shared/services/addons/addon-oauth.service.ts +++ b/src/app/shared/services/addons/addon-oauth.service.ts @@ -1,6 +1,7 @@ import { createDispatchMap, select } from '@ngxs/store'; -import { DestroyRef, inject, Injectable, signal } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; +import { DestroyRef, inject, Injectable, PLATFORM_ID, signal } from '@angular/core'; import { takeUntilDestroyed } from '@angular/core/rxjs-interop'; import { OAuthCallbacks } from '@osf/shared/models/addons/addon-utils.models'; @@ -12,6 +13,7 @@ import { AddonsSelectors, DeleteAuthorizedAddon, GetAuthorizedStorageOauthToken }) export class AddonOAuthService { private destroyRef = inject(DestroyRef); + private platformId = inject(PLATFORM_ID); private pendingOauth = signal(false); private createdAddon = signal(null); @@ -33,7 +35,9 @@ export class AddonOAuthService { this.addonTypeString.set(addonTypeString); this.callbacks.set(callbacks); - document.addEventListener('visibilitychange', this.boundOnVisibilityChange); + if (isPlatformBrowser(this.platformId)) { + document.addEventListener('visibilitychange', this.boundOnVisibilityChange); + } } stopOAuthTracking(): void { @@ -41,7 +45,7 @@ export class AddonOAuthService { } private onVisibilityChange(): void { - if (document.visibilityState === 'visible' && this.pendingOauth()) { + if (isPlatformBrowser(this.platformId) && document.visibilityState === 'visible' && this.pendingOauth()) { this.checkOauthSuccess(); } } @@ -71,7 +75,9 @@ export class AddonOAuthService { private completeOauthFlow(updatedAddon?: AuthorizedAccountModel): void { this.pendingOauth.set(false); - document.removeEventListener('visibilitychange', this.boundOnVisibilityChange); + if (isPlatformBrowser(this.platformId)) { + document.removeEventListener('visibilitychange', this.boundOnVisibilityChange); + } if (updatedAddon && this.callbacks()?.onSuccess) { const originalAddon = this.createdAddon(); @@ -87,7 +93,9 @@ export class AddonOAuthService { private cleanupService(): void { this.cleanupIncompleteOAuthAddon(); - document.removeEventListener('visibilitychange', this.boundOnVisibilityChange); + if (isPlatformBrowser(this.platformId)) { + document.removeEventListener('visibilitychange', this.boundOnVisibilityChange); + } this.resetServiceData(); } diff --git a/src/app/shared/services/analytics.service.ts b/src/app/shared/services/analytics.service.ts index e5b428fa8..f76aabbd9 100644 --- a/src/app/shared/services/analytics.service.ts +++ b/src/app/shared/services/analytics.service.ts @@ -1,6 +1,7 @@ import { Observable } from 'rxjs'; -import { inject, Injectable } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; +import { inject, Injectable, PLATFORM_ID } from '@angular/core'; import { ENVIRONMENT } from '@core/provider/environment.provider'; import { CurrentResource } from '@osf/shared/models/current-resource.model'; @@ -10,6 +11,7 @@ import { JsonApiService } from '@osf/shared/services/json-api.service'; export class AnalyticsService { private readonly jsonApiService = inject(JsonApiService); private readonly environment = inject(ENVIRONMENT); + private readonly platformId = inject(PLATFORM_ID); get apiDomainUrl() { return `${this.environment.apiDomainUrl}/_/metrics/events/counted_usage/`; @@ -20,7 +22,17 @@ export class AnalyticsService { const attributes = Object.fromEntries( Object.entries(all_attrs).filter(([_, value]: [unknown, unknown]) => typeof value !== 'undefined') ); - const pageTitle = document.title === 'OSF' ? `OSF | ${resource.title}` : document.title; + + let pageTitle = 'OSF'; + let pageUrl = ''; + let refererUrl = ''; + + if (isPlatformBrowser(this.platformId)) { + pageTitle = document.title === 'OSF' ? `OSF | ${resource.title}` : document.title; + pageUrl = document.URL; + refererUrl = document.referrer; + } + return { data: { type: 'counted-usage', @@ -28,9 +40,9 @@ export class AnalyticsService { ...attributes, action_labels: ['web', 'view'], pageview_info: { - page_url: document.URL, + page_url: pageUrl, page_title: pageTitle, - referer_url: document.referrer, + referer_url: refererUrl, route_name: `angular-osf-web.${routeName}`, }, }, diff --git a/src/app/shared/services/brand.service.ts b/src/app/shared/services/brand.service.ts index 72740585d..3d236ae32 100644 --- a/src/app/shared/services/brand.service.ts +++ b/src/app/shared/services/brand.service.ts @@ -1,8 +1,21 @@ +import { DOCUMENT, isPlatformBrowser } from '@angular/common'; +import { inject, Injectable, PLATFORM_ID } from '@angular/core'; + import { BrandModel } from '../models/brand/brand.model'; +@Injectable({ + providedIn: 'root', +}) export class BrandService { - static applyBranding(brand: BrandModel): void { - const root = document.documentElement; + private readonly document = inject(DOCUMENT); + private readonly platformId = inject(PLATFORM_ID); + + applyBranding(brand: BrandModel): void { + if (!isPlatformBrowser(this.platformId)) { + return; + } + + const root = this.document.documentElement; root.style.setProperty('--branding-primary-color', brand.primaryColor); root.style.setProperty('--branding-secondary-color', brand.secondaryColor); @@ -11,8 +24,12 @@ export class BrandService { root.style.setProperty('--branding-hero-background-image-url', `url(${brand.heroBackgroundImageUrl})`); } - static resetBranding(): void { - const root = document.documentElement; + resetBranding(): void { + if (!isPlatformBrowser(this.platformId)) { + return; + } + + const root = this.document.documentElement; root.style.setProperty('--branding-primary-color', ''); root.style.setProperty('--branding-secondary-color', ''); diff --git a/src/app/shared/services/browser-tab.service.ts b/src/app/shared/services/browser-tab.service.ts new file mode 100644 index 000000000..9c6a419c6 --- /dev/null +++ b/src/app/shared/services/browser-tab.service.ts @@ -0,0 +1,41 @@ +import { DOCUMENT, isPlatformBrowser } from '@angular/common'; +import { inject, Injectable, PLATFORM_ID } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class BrowserTabService { + private static readonly DEFAULT_FAVICON = '/favicon.ico'; + private static readonly DEFAULT_TITLE = 'OSF'; + + private readonly document = inject(DOCUMENT); + private readonly platformId = inject(PLATFORM_ID); + + updateTabStyles(faviconUrl: string, title: string) { + if (!isPlatformBrowser(this.platformId)) { + return; + } + + if (faviconUrl) { + const faviconElement = this.document.querySelector("link[rel*='icon']") as HTMLLinkElement; + if (faviconElement) { + faviconElement.href = faviconUrl; + } + } + + if (title) { + this.document.title = title; + } + } + + resetToDefaults() { + if (!isPlatformBrowser(this.platformId)) { + return; + } + + const faviconElement = this.document.querySelector("link[rel*='icon']") as HTMLLinkElement; + if (faviconElement) { + faviconElement.href = BrowserTabService.DEFAULT_FAVICON; + } + + this.document.title = BrowserTabService.DEFAULT_TITLE; + } +} diff --git a/src/app/shared/services/datacite/datacite.service.ts b/src/app/shared/services/datacite/datacite.service.ts index 935333b5c..38ed9f53d 100644 --- a/src/app/shared/services/datacite/datacite.service.ts +++ b/src/app/shared/services/datacite/datacite.service.ts @@ -1,7 +1,8 @@ import { EMPTY, filter, map, Observable, of, switchMap, take } from 'rxjs'; +import { isPlatformBrowser } from '@angular/common'; import { HttpClient, HttpContext } from '@angular/common/http'; -import { inject, Injectable } from '@angular/core'; +import { inject, Injectable, PLATFORM_ID } from '@angular/core'; import { BYPASS_ERROR_INTERCEPTOR } from '@core/interceptors/error-interceptor.tokens'; import { ENVIRONMENT } from '@core/provider/environment.provider'; @@ -15,6 +16,7 @@ import { IdentifiersResponseJsonApi } from '@osf/shared/models/identifiers/ident export class DataciteService { private readonly http: HttpClient = inject(HttpClient); private readonly environment = inject(ENVIRONMENT); + private readonly platformId = inject(PLATFORM_ID); get apiDomainUrl() { return this.environment.apiDomainUrl; @@ -74,31 +76,38 @@ export class DataciteService { /** * Internal helper to log a specific Datacite event for a given DOI. + * Only tracks in browser environment using sendBeacon with HTTP POST fallback. * * @param event - The Datacite event type (VIEW or DOWNLOAD). * @param doi - The DOI (Digital Object Identifier) of the resource. - * @returns An Observable that completes when the HTTP POST is sent, - * or EMPTY if DOI or repo ID is missing. + * @returns An Observable that completes when the tracking request is sent, + * or EMPTY if DOI, repo ID is missing, or not in browser. */ private logActivity(event: DataciteEvent, doi: string): Observable { if (!doi || !this.dataciteTrackerRepoId) { return EMPTY; } + + if (!isPlatformBrowser(this.platformId)) { + return EMPTY; + } + const payload = { n: event, u: window.location.href, i: this.dataciteTrackerRepoId, p: doi, }; + const success = navigator.sendBeacon(this.dataciteTrackerAddress, JSON.stringify(payload)); + if (success) { return of(void 0); } else { - const headers = { - 'Content-Type': 'application/json', - }; + const headers = { 'Content-Type': 'application/json' }; const context = new HttpContext(); context.set(BYPASS_ERROR_INTERCEPTOR, true); + return this.http .post(this.dataciteTrackerAddress, payload, { headers, diff --git a/src/app/shared/services/google-file-picker.download.service.ts b/src/app/shared/services/google-file-picker.download.service.ts index 76be19951..0973f17a6 100644 --- a/src/app/shared/services/google-file-picker.download.service.ts +++ b/src/app/shared/services/google-file-picker.download.service.ts @@ -1,7 +1,7 @@ import { Observable, Subscriber } from 'rxjs'; -import { DOCUMENT } from '@angular/common'; -import { Inject, Injectable } from '@angular/core'; +import { DOCUMENT, isPlatformBrowser } from '@angular/common'; +import { Inject, Injectable, PLATFORM_ID } from '@angular/core'; /** * Injectable service to load the Google Picker API script dynamically. @@ -21,7 +21,10 @@ export class GoogleFilePickerDownloadService { * * @param document - The Angular-injected reference to the global `document`. */ - constructor(@Inject(DOCUMENT) private document: Document) {} + constructor( + @Inject(DOCUMENT) private document: Document, + @Inject(PLATFORM_ID) private platformId: string + ) {} /** * Dynamically loads the Google Picker script if it hasn't already been loaded. @@ -59,6 +62,11 @@ export class GoogleFilePickerDownloadService { */ public loadGapiModules(): Observable { return new Observable((observer: Subscriber) => { + if (!isPlatformBrowser(this.platformId) || !window.gapi) { + observer.error('GAPI not available'); + return; + } + window.gapi.load('client:picker', { callback: () => { observer.next(); diff --git a/src/app/shared/services/header-style.service.ts b/src/app/shared/services/header-style.service.ts new file mode 100644 index 000000000..4bf8c70d2 --- /dev/null +++ b/src/app/shared/services/header-style.service.ts @@ -0,0 +1,32 @@ +import { DOCUMENT, isPlatformBrowser } from '@angular/common'; +import { inject, Injectable, PLATFORM_ID } from '@angular/core'; + +@Injectable({ providedIn: 'root' }) +export class HeaderStyleService { + private readonly document = inject(DOCUMENT); + private readonly platformId = inject(PLATFORM_ID); + + applyHeaderStyles(textColor: string, backgroundColor?: string, backgroundImageUrl?: string) { + if (!isPlatformBrowser(this.platformId)) { + return; + } + + const root = this.document.documentElement; + + root.style.setProperty('--header-color', textColor); + root.style.setProperty('--header-background-color', backgroundColor || ''); + root.style.setProperty('--header-background-image-url', `url(${backgroundImageUrl || ''})`); + } + + resetToDefaults() { + if (!isPlatformBrowser(this.platformId)) { + return; + } + + const root = this.document.documentElement; + + root.style.setProperty('--header-color', ''); + root.style.setProperty('--header-background-color', ''); + root.style.setProperty('--header-background-image-url', ''); + } +} diff --git a/src/app/shared/services/meta-tags.service.ts b/src/app/shared/services/meta-tags.service.ts index 8cea2c55d..dffa846be 100644 --- a/src/app/shared/services/meta-tags.service.ts +++ b/src/app/shared/services/meta-tags.service.ts @@ -1,7 +1,7 @@ import { catchError, map, Observable, of, switchMap, tap } from 'rxjs'; -import { DOCUMENT } from '@angular/common'; -import { DestroyRef, effect, Inject, inject, Injectable, signal } from '@angular/core'; +import { DOCUMENT, isPlatformBrowser } from '@angular/common'; +import { DestroyRef, effect, Inject, inject, Injectable, PLATFORM_ID, signal } from '@angular/core'; import { Meta, MetaDefinition, Title } from '@angular/platform-browser'; import { ENVIRONMENT } from '@core/provider/environment.provider'; @@ -22,6 +22,7 @@ export class MetaTagsService { private readonly metadataRecords: MetadataRecordsService = inject(MetadataRecordsService); private readonly environment = inject(ENVIRONMENT); private readonly prerenderReady = inject(PrerenderReadyService); + private readonly platformId = inject(PLATFORM_ID); get webUrl() { return this.environment.webUrl; @@ -79,6 +80,12 @@ export class MetaTagsService { } clearMetaTags(): void { + if (!isPlatformBrowser(this.platformId)) { + this.areMetaTagsApplied.set(false); + this.prerenderReady.setNotReady(); + return; + } + const elementsToRemove = this.document.querySelectorAll(`.${this.metaTagClass}`); if (elementsToRemove.length === 0) { @@ -245,6 +252,10 @@ export class MetaTagsService { } private applyHeadTags(headTags: HeadTagDef[]): void { + if (!isPlatformBrowser(this.platformId)) { + return; + } + headTags.forEach((tag) => { if (tag.type === 'meta') { this.meta.addTag(tag.attrs); @@ -282,6 +293,10 @@ export class MetaTagsService { } private dispatchZoteroEvent(): void { + if (!isPlatformBrowser(this.platformId)) { + return; + } + const event = new Event('ZoteroItemUpdated', { bubbles: true, cancelable: true, diff --git a/src/app/shared/services/view-only-links.service.ts b/src/app/shared/services/view-only-links.service.ts index 32da38445..23cc23764 100644 --- a/src/app/shared/services/view-only-links.service.ts +++ b/src/app/shared/services/view-only-links.service.ts @@ -3,7 +3,6 @@ import { map, Observable } from 'rxjs'; import { inject, Injectable } from '@angular/core'; import { ENVIRONMENT } from '@core/provider/environment.provider'; -import { JsonApiResponse } from '@shared/models/common/json-api.model'; import { ResourceType } from '../enums/resource-type.enum'; import { ViewOnlyLinksMapper } from '../mappers/view-only-links.mapper'; @@ -11,6 +10,7 @@ import { PaginatedViewOnlyLinksModel } from '../models/view-only-links/view-only import { ViewOnlyLinkJsonApi, ViewOnlyLinksResponseJsonApi, + ViewOnlyLinksResponsesJsonApi, } from '../models/view-only-links/view-only-link-response.model'; import { JsonApiService } from './json-api.service'; @@ -36,8 +36,8 @@ export class ViewOnlyLinksService { const params: Record = { 'embed[]': ['creator', 'nodes'] }; return this.jsonApiService - .get(`${this.apiUrl}/${resourcePath}/${projectId}/view_only_links/`, params) - .pipe(map((response) => ViewOnlyLinksMapper.fromResponse(response, projectId))); + .get(`${this.apiUrl}/${resourcePath}/${projectId}/view_only_links/`, params) + .pipe(map((response) => ViewOnlyLinksMapper.fromResponse(response, projectId, this.environment.webUrl))); } createViewOnlyLink( @@ -50,10 +50,10 @@ export class ViewOnlyLinksService { const params: Record = { 'embed[]': ['creator', 'nodes'] }; return this.jsonApiService - .post< - JsonApiResponse - >(`${this.apiUrl}/${resourcePath}/${projectId}/view_only_links/`, data, params) - .pipe(map((response) => ViewOnlyLinksMapper.fromSingleResponse(response.data, projectId))); + .post(`${this.apiUrl}/${resourcePath}/${projectId}/view_only_links/`, data, params) + .pipe( + map((response) => ViewOnlyLinksMapper.fromSingleResponse(response.data, projectId, this.environment.webUrl)) + ); } deleteLink(projectId: string, resourceType: ResourceType, linkId: string): Observable { diff --git a/src/app/shared/services/view-only.service.ts b/src/app/shared/services/view-only.service.ts new file mode 100644 index 000000000..80caf7bde --- /dev/null +++ b/src/app/shared/services/view-only.service.ts @@ -0,0 +1,53 @@ +import { isPlatformBrowser } from '@angular/common'; +import { inject, Injectable, PLATFORM_ID } from '@angular/core'; +import { Router } from '@angular/router'; + +import { WINDOW } from '@core/provider/window.provider'; + +@Injectable({ providedIn: 'root' }) +export class ViewOnlyService { + private readonly platformId = inject(PLATFORM_ID); + private readonly window = inject(WINDOW); + + hasViewOnlyParam(router: Router): boolean { + const currentUrl = router.url; + const routerParams = new URLSearchParams(currentUrl.split('?')[1] || ''); + + if (isPlatformBrowser(this.platformId) && this.window.location?.search) { + const windowParams = new URLSearchParams(this.window.location.search); + return routerParams.has('view_only') || windowParams.has('view_only'); + } + + return routerParams.has('view_only'); + } + + getViewOnlyParam(router?: Router): string | null { + let currentUrl = ''; + + if (router) { + currentUrl = router.url; + } + + const routerParams = new URLSearchParams(currentUrl.split('?')[1] || ''); + + if (isPlatformBrowser(this.platformId) && this.window.location?.search) { + const windowParams = new URLSearchParams(this.window.location.search); + return routerParams.get('view_only') || windowParams.get('view_only'); + } + + return routerParams.get('view_only'); + } + + getViewOnlyParamFromUrl(currentUrl?: string): string | null { + if (!currentUrl) return null; + + const routerParams = new URLSearchParams(currentUrl.split('?')[1] || ''); + + if (isPlatformBrowser(this.platformId) && this.window.location?.search) { + const windowParams = new URLSearchParams(this.window.location.search); + return routerParams.get('view_only') || windowParams.get('view_only'); + } + + return routerParams.get('view_only'); + } +} From 4ca1496bfdb14a32c75290176f6ecac288e23658 Mon Sep 17 00:00:00 2001 From: nsemets Date: Tue, 2 Dec 2025 19:31:05 +0200 Subject: [PATCH 04/11] fix(ssr): fixed and clean up code for ssr --- src/app/app.component.ts | 17 ++++++------ src/app/app.config.ts | 2 ++ src/app/app.routes.ts | 6 +---- .../components/nav-menu/nav-menu.component.ts | 4 +-- src/app/core/guards/view-only.guard.ts | 4 +-- .../core/interceptors/error.interceptor.ts | 4 +-- .../interceptors/view-only.interceptor.ts | 4 +-- .../features/analytics/analytics.component.ts | 4 +-- .../file-keywords/file-keywords.component.ts | 4 +-- .../file-metadata/file-metadata.component.ts | 4 +-- .../file-resource-metadata.component.ts | 4 +-- .../file-detail/file-detail.component.ts | 4 +-- .../files/pages/files/files.component.ts | 4 +-- .../files-widget/files-widget.component.ts | 4 +-- .../project-overview.component.spec.ts | 3 --- .../overview/project-overview.component.ts | 9 +++---- .../connect-configured-addon.component.html | 2 +- src/app/features/project/project.component.ts | 7 +++-- src/app/features/project/project.routes.ts | 3 ++- .../features/project/wiki/wiki.component.ts | 4 +-- .../registry-components.component.ts | 4 +-- .../registry-overview.component.html | 2 +- .../registry-overview.component.ts | 27 +++++++------------ .../registry-wiki/registry-wiki.component.ts | 4 +-- src/app/features/registry/registry.routes.ts | 3 ++- .../connect-addon.component.html | 2 +- .../file-menu/file-menu.component.ts | 4 +-- .../files-tree/files-tree.component.ts | 4 +-- .../global-search/global-search.component.ts | 12 ++------- .../resource-citations.component.ts | 4 +-- ...ce.ts => view-only-link-helper.service.ts} | 2 +- src/main.ts | 6 ++--- 32 files changed, 74 insertions(+), 97 deletions(-) rename src/app/shared/services/{view-only.service.ts => view-only-link-helper.service.ts} (97%) diff --git a/src/app/app.component.ts b/src/app/app.component.ts index 083c98d29..03eee262a 100644 --- a/src/app/app.component.ts +++ b/src/app/app.component.ts @@ -1,6 +1,6 @@ -import { Actions, createDispatchMap, ofActionSuccessful, select } from '@ngxs/store'; +import { createDispatchMap, select } from '@ngxs/store'; -import { take, timer } from 'rxjs'; +import { switchMap, timer } from 'rxjs'; import { isPlatformBrowser } from '@angular/common'; import { ChangeDetectionStrategy, Component, DestroyRef, effect, inject, OnInit, PLATFORM_ID } from '@angular/core'; @@ -39,7 +39,6 @@ export class AppComponent implements OnInit { private readonly customDialogService = inject(CustomDialogService); private readonly router = inject(Router); private readonly environment = inject(ENVIRONMENT); - private readonly actions$ = inject(Actions); private readonly actions = createDispatchMap({ getCurrentUser: GetCurrentUser, getEmails: GetEmails }); private readonly platformId = inject(PLATFORM_ID); private readonly isBrowser = isPlatformBrowser(this.platformId); @@ -56,11 +55,13 @@ export class AppComponent implements OnInit { } ngOnInit(): void { - this.actions.getCurrentUser(); - - this.actions$.pipe(ofActionSuccessful(GetCurrentUser), take(1)).subscribe(() => { - this.actions.getEmails(); - }); + this.actions + .getCurrentUser() + .pipe( + takeUntilDestroyed(this.destroyRef), + switchMap(() => this.actions.getEmails()) + ) + .subscribe(); if (this.isBrowser) { this.router.events.pipe(takeUntilDestroyed(this.destroyRef)).subscribe((event) => { diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 27b701593..2926885c4 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -8,6 +8,7 @@ import { DialogService } from 'primeng/dynamicdialog'; import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http'; import { ApplicationConfig, ErrorHandler, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; +import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; import { provideAnimations } from '@angular/platform-browser/animations'; import { provideRouter, withInMemoryScrolling } from '@angular/router'; @@ -52,6 +53,7 @@ export const appConfig: ApplicationConfig = { provideRouter(routes, withInMemoryScrolling({ scrollPositionRestoration: 'top', anchorScrolling: 'enabled' })), provideStore(STATES), provideZoneChangeDetection({ eventCoalescing: true }), + provideClientHydration(withEventReplay()), SENTRY_PROVIDER, ], }; diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index 5a1fb18ab..e0c0676bc 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -15,7 +15,6 @@ import { LicensesHandlers, ProjectsHandlers, ProvidersHandlers } from './feature import { FilesHandlers } from './features/registries/store/handlers/files.handlers'; import { LicensesService } from './shared/services/licenses.service'; import { BookmarksState } from './shared/stores/bookmarks'; -import { ProjectsState } from './shared/stores/projects'; export const routes: Routes = [ { @@ -35,7 +34,6 @@ export const routes: Routes = [ import('./features/home/pages/dashboard/dashboard.component').then((mod) => mod.DashboardComponent), data: { skipBreadcrumbs: true }, canActivate: [authGuard], - providers: [provideStates([ProjectsState])], }, { path: 'register', @@ -69,7 +67,7 @@ export const routes: Routes = [ loadComponent: () => import('./features/my-projects/my-projects.component').then((mod) => mod.MyProjectsComponent), canActivate: [authGuard], - providers: [provideStates([BookmarksState, ProjectsState])], + providers: [provideStates([BookmarksState])], }, { path: 'my-registrations', @@ -192,13 +190,11 @@ export const routes: Routes = [ path: ':id', canMatch: [isProjectGuard], loadChildren: () => import('./features/project/project.routes').then((m) => m.projectRoutes), - providers: [provideStates([ProjectsState, BookmarksState])], }, { path: ':id', canMatch: [isRegistryGuard], loadChildren: () => import('./features/registry/registry.routes').then((m) => m.registryRoutes), - providers: [provideStates([BookmarksState])], }, { path: '**', diff --git a/src/app/core/components/nav-menu/nav-menu.component.ts b/src/app/core/components/nav-menu/nav-menu.component.ts index 40c3d87a2..dfd71016d 100644 --- a/src/app/core/components/nav-menu/nav-menu.component.ts +++ b/src/app/core/components/nav-menu/nav-menu.component.ts @@ -22,7 +22,7 @@ import { IconComponent } from '@osf/shared/components/icon/icon.component'; import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; import { ReviewPermissions } from '@osf/shared/enums/review-permissions.enum'; import { WrapFnPipe } from '@osf/shared/pipes/wrap-fn.pipe'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { CurrentResourceSelectors, GetResourceDetails } from '@osf/shared/stores/current-resource'; @Component({ @@ -38,7 +38,7 @@ export class NavMenuComponent { private readonly route = inject(ActivatedRoute); private readonly authService = inject(AuthService); private readonly platformId = inject(PLATFORM_ID); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); private readonly isAuthenticated = select(UserSelectors.isAuthenticated); private readonly currentResource = select(CurrentResourceSelectors.getCurrentResource); diff --git a/src/app/core/guards/view-only.guard.ts b/src/app/core/guards/view-only.guard.ts index cb53224af..6410c406d 100644 --- a/src/app/core/guards/view-only.guard.ts +++ b/src/app/core/guards/view-only.guard.ts @@ -2,11 +2,11 @@ import { inject } from '@angular/core'; import { CanActivateFn, Router } from '@angular/router'; import { VIEW_ONLY_EXCLUDED_ROUTES } from '@core/constants/view-only-excluded-routes.const'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; export const viewOnlyGuard: CanActivateFn = (route) => { const router = inject(Router); - const viewOnlyHelper = inject(ViewOnlyService); + const viewOnlyHelper = inject(ViewOnlyLinkHelperService); if (!viewOnlyHelper.hasViewOnlyParam(router)) { return true; diff --git a/src/app/core/interceptors/error.interceptor.ts b/src/app/core/interceptors/error.interceptor.ts index 2aa288554..b9717ad71 100644 --- a/src/app/core/interceptors/error.interceptor.ts +++ b/src/app/core/interceptors/error.interceptor.ts @@ -11,7 +11,7 @@ import { SENTRY_TOKEN } from '@core/provider/sentry.provider'; import { AuthService } from '@core/services/auth.service'; import { LoaderService } from '@osf/shared/services/loader.service'; import { ToastService } from '@osf/shared/services/toast.service'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { BYPASS_ERROR_INTERCEPTOR } from './error-interceptor.tokens'; @@ -22,7 +22,7 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => { const authService = inject(AuthService); const sentry = inject(SENTRY_TOKEN); const platformId = inject(PLATFORM_ID); - const viewOnlyHelper = inject(ViewOnlyService); + const viewOnlyHelper = inject(ViewOnlyLinkHelperService); return next(req).pipe( catchError((error: HttpErrorResponse) => { diff --git a/src/app/core/interceptors/view-only.interceptor.ts b/src/app/core/interceptors/view-only.interceptor.ts index 6622eee13..fe3ae6fa1 100644 --- a/src/app/core/interceptors/view-only.interceptor.ts +++ b/src/app/core/interceptors/view-only.interceptor.ts @@ -4,14 +4,14 @@ import { HttpEvent, HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angul import { inject } from '@angular/core'; import { Router } from '@angular/router'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; export const viewOnlyInterceptor: HttpInterceptorFn = ( req: HttpRequest, next: HttpHandlerFn ): Observable> => { const router = inject(Router); - const viewOnlyHelper = inject(ViewOnlyService); + const viewOnlyHelper = inject(ViewOnlyLinkHelperService); const viewOnlyParam = viewOnlyHelper.getViewOnlyParam(router); diff --git a/src/app/features/analytics/analytics.component.ts b/src/app/features/analytics/analytics.component.ts index a0409c215..29494fd25 100644 --- a/src/app/features/analytics/analytics.component.ts +++ b/src/app/features/analytics/analytics.component.ts @@ -29,7 +29,7 @@ import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-l import { IS_WEB } from '@osf/shared/helpers/breakpoints.tokens'; import { Primitive } from '@osf/shared/helpers/types.helper'; import { DatasetInput } from '@osf/shared/models/charts/dataset-input'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { AnalyticsKpiComponent } from './components'; import { DATE_RANGE_OPTIONS } from './constants'; @@ -66,7 +66,7 @@ export class AnalyticsComponent implements OnInit { private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); private readonly translateService = inject(TranslateService); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); readonly resourceId = toSignal(this.route.parent?.params.pipe(map((params) => params['id'])) ?? of(undefined)); readonly resourceType = toSignal(this.route.data.pipe(map((params) => params['resourceType'])) ?? of(undefined)); diff --git a/src/app/features/files/components/file-keywords/file-keywords.component.ts b/src/app/features/files/components/file-keywords/file-keywords.component.ts index 3e1cb8384..dc8fbabf4 100644 --- a/src/app/features/files/components/file-keywords/file-keywords.component.ts +++ b/src/app/features/files/components/file-keywords/file-keywords.component.ts @@ -14,7 +14,7 @@ import { Router } from '@angular/router'; import { InputLimits } from '@osf/shared/constants/input-limits.const'; import { CustomValidators } from '@osf/shared/helpers/custom-form-validators.helper'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { FilesSelectors, UpdateTags } from '../../store'; @@ -29,7 +29,7 @@ export class FileKeywordsComponent { private readonly actions = createDispatchMap({ updateTags: UpdateTags }); private readonly destroyRef = inject(DestroyRef); private readonly router = inject(Router); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); readonly tags = select(FilesSelectors.getFileTags); readonly isTagsLoading = select(FilesSelectors.isFileTagsLoading); diff --git a/src/app/features/files/components/file-metadata/file-metadata.component.ts b/src/app/features/files/components/file-metadata/file-metadata.component.ts index b6b05db70..ab68392f5 100644 --- a/src/app/features/files/components/file-metadata/file-metadata.component.ts +++ b/src/app/features/files/components/file-metadata/file-metadata.component.ts @@ -14,7 +14,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { ENVIRONMENT } from '@core/provider/environment.provider'; import { languageCodes } from '@osf/shared/constants/language.const'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { LanguageCodeModel } from '@shared/models/language-code.model'; import { FileMetadataFields } from '../../constants'; @@ -35,7 +35,7 @@ export class FileMetadataComponent { private readonly router = inject(Router); private readonly customDialogService = inject(CustomDialogService); private readonly environment = inject(ENVIRONMENT); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); fileMetadata = select(FilesSelectors.getFileCustomMetadata); isLoading = select(FilesSelectors.isFileMetadataLoading); diff --git a/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts index ccf29d1fa..6f1e8fa07 100644 --- a/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts +++ b/src/app/features/files/components/file-resource-metadata/file-resource-metadata.component.ts @@ -9,7 +9,7 @@ import { ChangeDetectionStrategy, Component, computed, inject, input } from '@an import { Router } from '@angular/router'; import { ContributorsListComponent } from '@osf/shared/components/contributors-list/contributors-list.component'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { FilesSelectors } from '../../store'; @@ -22,7 +22,7 @@ import { FilesSelectors } from '../../store'; }) export class FileResourceMetadataComponent { private readonly router = inject(Router); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); resourceType = input('nodes'); resourceMetadata = select(FilesSelectors.getResourceMetadata); diff --git a/src/app/features/files/pages/file-detail/file-detail.component.ts b/src/app/features/files/pages/file-detail/file-detail.component.ts index ab60f3a5f..9dc06ff04 100644 --- a/src/app/features/files/pages/file-detail/file-detail.component.ts +++ b/src/app/features/files/pages/file-detail/file-detail.component.ts @@ -48,7 +48,7 @@ import { CustomConfirmationService } from '@osf/shared/services/custom-confirmat import { DataciteService } from '@osf/shared/services/datacite/datacite.service'; import { MetaTagsService } from '@osf/shared/services/meta-tags.service'; import { ToastService } from '@osf/shared/services/toast.service'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { FileDetailsModel } from '@shared/models/files/file.model'; import { MetadataTabsModel } from '@shared/models/metadata-tabs.model'; @@ -107,7 +107,7 @@ export class FileDetailComponent { private readonly metaTags = inject(MetaTagsService); private readonly datePipe = inject(DatePipe); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); private readonly translateService = inject(TranslateService); private readonly environment = inject(ENVIRONMENT); private readonly clipboard = inject(Clipboard); diff --git a/src/app/features/files/pages/files/files.component.ts b/src/app/features/files/pages/files/files.component.ts index 569d14891..a7dc18e84 100644 --- a/src/app/features/files/pages/files/files.component.ts +++ b/src/app/features/files/pages/files/files.component.ts @@ -71,7 +71,7 @@ import { CustomConfirmationService } from '@osf/shared/services/custom-confirmat import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { FilesService } from '@osf/shared/services/files.service'; import { ToastService } from '@osf/shared/services/toast.service'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { CurrentResourceSelectors, GetResourceDetails } from '@osf/shared/stores/current-resource'; import { ConfiguredAddonModel } from '@shared/models/addons/configured-addon.model'; import { StorageItem } from '@shared/models/addons/storage-item.model'; @@ -129,7 +129,7 @@ export class FilesComponent { private readonly environment = inject(ENVIRONMENT); private readonly customConfirmationService = inject(CustomConfirmationService); private readonly toastService = inject(ToastService); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); private readonly webUrl = this.environment.webUrl; private readonly apiDomainUrl = this.environment.apiDomainUrl; diff --git a/src/app/features/project/overview/components/files-widget/files-widget.component.ts b/src/app/features/project/overview/components/files-widget/files-widget.component.ts index 057969e19..02e401c3a 100644 --- a/src/app/features/project/overview/components/files-widget/files-widget.component.ts +++ b/src/app/features/project/overview/components/files-widget/files-widget.component.ts @@ -39,7 +39,7 @@ import { FileLabelModel } from '@osf/shared/models/files/file-label.model'; import { NodeShortInfoModel } from '@osf/shared/models/nodes/node-with-children.model'; import { ProjectModel } from '@osf/shared/models/projects/projects.models'; import { SelectOption } from '@osf/shared/models/select-option.model'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; @Component({ selector: 'osf-files-widget', @@ -57,7 +57,7 @@ export class FilesWidgetComponent { private readonly environment = inject(ENVIRONMENT); private readonly destroyRef = inject(DestroyRef); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); readonly files = select(FilesSelectors.getFiles); readonly filesTotalCount = select(FilesSelectors.getFilesTotalCount); diff --git a/src/app/features/project/overview/project-overview.component.spec.ts b/src/app/features/project/overview/project-overview.component.spec.ts index f6bb02721..3cbbd1ee7 100644 --- a/src/app/features/project/overview/project-overview.component.spec.ts +++ b/src/app/features/project/overview/project-overview.component.spec.ts @@ -15,7 +15,6 @@ import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/ import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-link-message/view-only-link-message.component'; import { Mode } from '@osf/shared/enums/mode.enum'; -import { AnalyticsService } from '@osf/shared/services/analytics.service'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { ToastService } from '@osf/shared/services/toast.service'; import { AddonsSelectors, ClearConfiguredAddons } from '@osf/shared/stores/addons'; @@ -40,7 +39,6 @@ import { ClearProjectOverview, GetComponents, GetProjectById, ProjectOverviewSel import { MOCK_PROJECT_OVERVIEW } from '@testing/mocks/project-overview.mock'; import { OSFTestingModule } from '@testing/osf.testing.module'; -import { AnalyticsServiceMockFactory } from '@testing/providers/analytics.service.mock'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; @@ -118,7 +116,6 @@ describe('ProjectOverviewComponent', () => { MockProvider(ActivatedRoute, activatedRouteMock), MockProvider(CustomDialogService, customDialogServiceMock), MockProvider(ToastService, toastService), - MockProvider(AnalyticsService, AnalyticsServiceMockFactory()), ], }).compileComponents(); diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index b6a578023..ecab1819e 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -34,7 +34,7 @@ import { Mode } from '@osf/shared/enums/mode.enum'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { ToastService } from '@osf/shared/services/toast.service'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { AddonsSelectors, ClearConfiguredAddons, @@ -47,7 +47,6 @@ import { ClearCollections, CollectionsSelectors, GetCollectionProvider } from '@ import { CurrentResourceSelectors, GetResourceWithChildren } from '@osf/shared/stores/current-resource'; import { GetLinkedResources } from '@osf/shared/stores/node-links'; import { ClearWiki, GetHomeWiki } from '@osf/shared/stores/wiki'; -import { AnalyticsService } from '@shared/services/analytics.service'; import { CitationAddonCardComponent } from './components/citation-addon-card/citation-addon-card.component'; import { FilesWidgetComponent } from './components/files-widget/files-widget.component'; @@ -99,9 +98,8 @@ export class ProjectOverviewComponent implements OnInit { private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); private readonly toastService = inject(ToastService); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); private readonly customDialogService = inject(CustomDialogService); - readonly analyticsService = inject(AnalyticsService); submissions = select(CollectionsModerationSelectors.getCollectionSubmissions); collectionProvider = select(CollectionsSelectors.getCollectionProvider); @@ -155,6 +153,7 @@ export class ProjectOverviewComponent implements OnInit { }); submissionReviewStatus = computed(() => this.currentReviewAction()?.toState); + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); showDecisionButton = computed( () => @@ -163,8 +162,6 @@ export class ProjectOverviewComponent implements OnInit { this.submissionReviewStatus() !== SubmissionReviewStatus.Rejected ); - hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); - filesRootOption = computed(() => ({ value: this.currentProject()?.id ?? '', label: this.currentProject()?.title ?? '', 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 b3a940436..7e7ca253f 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 @@ -183,7 +183,7 @@

{{ 'settings.addons.connectAddon.oauthDescription' | translate }}

-
+ {{ 'settings.addons.connectAddon.startOauth' | translate }} diff --git a/src/app/features/project/project.component.ts b/src/app/features/project/project.component.ts index 0071a1733..47407f439 100644 --- a/src/app/features/project/project.component.ts +++ b/src/app/features/project/project.component.ts @@ -54,7 +54,7 @@ export class ProjectComponent implements OnDestroy { private readonly prerenderReady = inject(PrerenderReadyService); private readonly router = inject(Router); private readonly analyticsService = inject(AnalyticsService); - currentResource = select(CurrentResourceSelectors.getCurrentResource); + private readonly currentResource = select(CurrentResourceSelectors.getCurrentResource); readonly identifiersForDatacite$ = toObservable(select(ProjectOverviewSelectors.getIdentifiers)).pipe( map((identifiers) => (identifiers?.length ? { identifiers } : null)) @@ -69,7 +69,8 @@ export class ProjectComponent implements OnDestroy { readonly institutions = select(ProjectOverviewSelectors.getInstitutions); readonly isInstitutionsLoading = select(ProjectOverviewSelectors.isInstitutionsLoading); - private projectId = toSignal(this.route.params.pipe(map((params) => params['id']))); + private readonly lastMetaTagsProjectId = signal(null); + private readonly projectId = toSignal(this.route.params.pipe(map((params) => params['id']))); private readonly allDataLoaded = computed( () => @@ -80,8 +81,6 @@ export class ProjectComponent implements OnDestroy { !!this.currentProject() ); - private readonly lastMetaTagsProjectId = signal(null); - private readonly actions = createDispatchMap({ getProject: GetProjectById, getLicense: GetProjectLicense, diff --git a/src/app/features/project/project.routes.ts b/src/app/features/project/project.routes.ts index 9ae274792..870ee6e8b 100644 --- a/src/app/features/project/project.routes.ts +++ b/src/app/features/project/project.routes.ts @@ -5,6 +5,7 @@ import { Routes } from '@angular/router'; import { viewOnlyGuard } from '@core/guards/view-only.guard'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { ActivityLogsState } from '@osf/shared/stores/activity-logs'; +import { BookmarksState } from '@osf/shared/stores/bookmarks'; import { CitationsState } from '@osf/shared/stores/citations'; import { CollectionsState } from '@osf/shared/stores/collections'; import { DuplicatesState } from '@osf/shared/stores/duplicates'; @@ -24,7 +25,7 @@ export const projectRoutes: Routes = [ { path: '', loadComponent: () => import('../project/project.component').then((mod) => mod.ProjectComponent), - providers: [provideStates([ProjectOverviewState])], + providers: [provideStates([BookmarksState, ProjectOverviewState])], children: [ { path: '', diff --git a/src/app/features/project/wiki/wiki.component.ts b/src/app/features/project/wiki/wiki.component.ts index bd35d6329..b1dc6fef5 100644 --- a/src/app/features/project/wiki/wiki.component.ts +++ b/src/app/features/project/wiki/wiki.component.ts @@ -19,7 +19,7 @@ import { WikiListComponent } from '@osf/shared/components/wiki/wiki-list/wiki-li import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { WikiModes } from '@osf/shared/models/wiki/wiki.model'; import { ToastService } from '@osf/shared/services/toast.service'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { CurrentResourceSelectors } from '@osf/shared/stores/current-resource'; import { ClearWiki, @@ -62,7 +62,7 @@ export class WikiComponent { private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); private readonly toastService = inject(ToastService); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); WikiModes = WikiModes; homeWikiName = 'Home'; diff --git a/src/app/features/registry/pages/registry-components/registry-components.component.ts b/src/app/features/registry/pages/registry-components/registry-components.component.ts index 7e3bd800f..41e877221 100644 --- a/src/app/features/registry/pages/registry-components/registry-components.component.ts +++ b/src/app/features/registry/pages/registry-components/registry-components.component.ts @@ -8,7 +8,7 @@ import { ActivatedRoute, Router } from '@angular/router'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-link-message/view-only-link-message.component'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { RegistrationLinksCardComponent } from '../../components'; import { GetRegistryComponents, RegistryComponentsSelectors } from '../../store/registry-components'; @@ -29,7 +29,7 @@ import { GetRegistryComponents, RegistryComponentsSelectors } from '../../store/ export class RegistryComponentsComponent implements OnInit { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); private registryId = signal(''); diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.html b/src/app/features/registry/pages/registry-overview/registry-overview.component.html index 724c807b4..e94064a8a 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.html +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.html @@ -33,7 +33,7 @@ } @else {
- @if (schemaResponse() && !schemaResponse()?.isOriginalResponse && !isInitialState) { + @if (schemaResponse() && !schemaResponse()?.isOriginalResponse && !isInitialState()) {
diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts index 664318f9b..3d9a38e97 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.ts +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.ts @@ -20,6 +20,7 @@ import { import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router } from '@angular/router'; +import { CreateSchemaResponse } from '@osf/features/registries/store'; import { DataResourcesComponent } from '@osf/shared/components/data-resources/data-resources.component'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; @@ -28,13 +29,13 @@ import { RegistrationReviewStates } from '@osf/shared/enums/registration-review- import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { RevisionReviewStates } from '@osf/shared/enums/revision-review-states.enum'; import { toCamelCase } from '@osf/shared/helpers/camel-case'; +import { SchemaResponse } from '@osf/shared/models/registration/schema-response.model'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { LoaderService } from '@osf/shared/services/loader.service'; import { ToastService } from '@osf/shared/services/toast.service'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { GetBookmarksCollectionId } from '@osf/shared/stores/bookmarks'; import { ContributorsSelectors, GetBibliographicContributors } from '@osf/shared/stores/contributors'; -import { SchemaResponse } from '@shared/models/registration/schema-response.model'; import { ArchivingMessageComponent, @@ -47,7 +48,6 @@ import { RegistryMakeDecisionComponent } from '../../components/registry-make-de import { RegistryOverviewMetadataComponent } from '../../components/registry-overview-metadata/registry-overview-metadata.component'; import { WithdrawnMessageComponent } from '../../components/withdrawn-message/withdrawn-message.component'; import { - CreateSchemaResponse, GetRegistryById, GetRegistryReviewActions, GetRegistrySchemaResponses, @@ -83,7 +83,7 @@ export class RegistryOverviewComponent { private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); private readonly toastService = inject(ToastService); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); private readonly customDialogService = inject(CustomDialogService); private readonly loaderService = inject(LoaderService); @@ -104,8 +104,14 @@ export class RegistryOverviewComponent { readonly hasWriteAccess = select(RegistrySelectors.hasWriteAccess); readonly hasAdminAccess = select(RegistrySelectors.hasAdminAccess); + isModeration = false; + revisionId: string | null = null; revisionInProgress: SchemaResponse | undefined; + selectedRevisionIndex = signal(0); + hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); + showToolbar = computed(() => !this.registry()?.archiving && !this.registry()?.withdrawn); + isInitialState = computed(() => this.registry()?.reviewsState === RegistrationReviewStates.Initial); canMakeDecision = computed(() => !this.registry()?.archiving && !this.registry()?.withdrawn && this.isModeration); isRootRegistration = computed(() => { @@ -124,10 +130,6 @@ export class RegistryOverviewComponent { return index !== null ? schemaResponses[index] : null; }); - readonly selectedRevisionIndex = signal(0); - - showToolbar = computed(() => !this.registry()?.archiving && !this.registry()?.withdrawn); - private readonly actions = createDispatchMap({ getRegistryById: GetRegistryById, getBookmarksId: GetBookmarksCollectionId, @@ -138,15 +140,6 @@ export class RegistryOverviewComponent { getBibliographicContributors: GetBibliographicContributors, }); - revisionId: string | null = null; - isModeration = false; - - hasViewOnly = computed(() => this.viewOnlyService.hasViewOnlyParam(this.router)); - - get isInitialState(): boolean { - return this.registry()?.reviewsState === RegistrationReviewStates.Initial; - } - constructor() { effect(() => { const registry = this.registry(); diff --git a/src/app/features/registry/pages/registry-wiki/registry-wiki.component.ts b/src/app/features/registry/pages/registry-wiki/registry-wiki.component.ts index c186b4555..2a8d92633 100644 --- a/src/app/features/registry/pages/registry-wiki/registry-wiki.component.ts +++ b/src/app/features/registry/pages/registry-wiki/registry-wiki.component.ts @@ -18,7 +18,7 @@ import { ViewSectionComponent } from '@osf/shared/components/wiki/view-section/v import { WikiListComponent } from '@osf/shared/components/wiki/wiki-list/wiki-list.component'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; import { WikiModes } from '@osf/shared/models/wiki/wiki.model'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { ClearWiki, GetCompareVersionContent, @@ -52,7 +52,7 @@ export class RegistryWikiComponent { private readonly route = inject(ActivatedRoute); private readonly router = inject(Router); private readonly destroyRef = inject(DestroyRef); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); WikiModes = WikiModes; wikiModes = select(WikiSelectors.getWikiModes); diff --git a/src/app/features/registry/registry.routes.ts b/src/app/features/registry/registry.routes.ts index db19cd0af..752cbf703 100644 --- a/src/app/features/registry/registry.routes.ts +++ b/src/app/features/registry/registry.routes.ts @@ -4,6 +4,7 @@ import { Routes } from '@angular/router'; import { viewOnlyGuard } from '@core/guards/view-only.guard'; import { ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { BookmarksState } from '@osf/shared/stores/bookmarks'; import { CitationsState } from '@osf/shared/stores/citations'; import { DuplicatesState } from '@osf/shared/stores/duplicates'; import { RegistrationProviderState } from '@osf/shared/stores/registration-provider'; @@ -23,7 +24,7 @@ export const registryRoutes: Routes = [ { path: '', component: RegistryComponent, - providers: [provideStates([RegistryState, RegistrationProviderState])], + providers: [provideStates([BookmarksState, RegistryState, RegistrationProviderState])], children: [ { path: '', diff --git a/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.html b/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.html index 4d23698e6..d67a91b8f 100644 --- a/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.html +++ b/src/app/features/settings/settings-addons/components/connect-addon/connect-addon.component.html @@ -78,7 +78,7 @@

{{ 'settings.addons.connectAddon.oauthDescription' | translate }}

- + {{ 'settings.addons.connectAddon.startOauth' | translate }}

diff --git a/src/app/shared/components/file-menu/file-menu.component.ts b/src/app/shared/components/file-menu/file-menu.component.ts index 66752d347..08ae3b5e2 100644 --- a/src/app/shared/components/file-menu/file-menu.component.ts +++ b/src/app/shared/components/file-menu/file-menu.component.ts @@ -9,7 +9,7 @@ import { Router } from '@angular/router'; import { FileMenuType } from '@osf/shared/enums/file-menu-type.enum'; import { MenuManagerService } from '@osf/shared/services/menu-manager.service'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { FileMenuAction, FileMenuData, FileMenuFlags } from '@shared/models/files/file-menu-action.model'; @Component({ @@ -21,7 +21,7 @@ import { FileMenuAction, FileMenuData, FileMenuFlags } from '@shared/models/file export class FileMenuComponent { private router = inject(Router); private menuManager = inject(MenuManagerService); - private viewOnlyService = inject(ViewOnlyService); + private viewOnlyService = inject(ViewOnlyLinkHelperService); isFolder = input(false); allowedActions = input({} as FileMenuFlags); diff --git a/src/app/shared/components/files-tree/files-tree.component.ts b/src/app/shared/components/files-tree/files-tree.component.ts index 0fc1021af..c094a6ec6 100644 --- a/src/app/shared/components/files-tree/files-tree.component.ts +++ b/src/app/shared/components/files-tree/files-tree.component.ts @@ -42,7 +42,7 @@ import { CustomDialogService } from '@osf/shared/services/custom-dialog.service' import { DataciteService } from '@osf/shared/services/datacite/datacite.service'; import { FilesService } from '@osf/shared/services/files.service'; import { ToastService } from '@osf/shared/services/toast.service'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { FileModel } from '@shared/models/files/file.model'; import { FileFolderModel } from '@shared/models/files/file-folder.model'; import { FileLabelModel } from '@shared/models/files/file-label.model'; @@ -78,7 +78,7 @@ export class FilesTreeComponent implements OnDestroy, AfterViewInit { readonly customConfirmationService = inject(CustomConfirmationService); readonly customDialogService = inject(CustomDialogService); readonly dataciteService = inject(DataciteService); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); private readonly destroyRef = inject(DestroyRef); private readonly environment = inject(ENVIRONMENT); diff --git a/src/app/shared/components/global-search/global-search.component.ts b/src/app/shared/components/global-search/global-search.component.ts index 8032c0126..d49d78328 100644 --- a/src/app/shared/components/global-search/global-search.component.ts +++ b/src/app/shared/components/global-search/global-search.component.ts @@ -145,11 +145,7 @@ export class GlobalSearchComponent implements OnInit, OnDestroy { this.actions.setResourceType(resourceTab); this.updateUrlWithTab(resourceTab); - this.actions.fetchResources().subscribe({ - next: () => { - this.updateUrlWithFilterOptions(this.filterOptions()); - }, - }); + this.actions.fetchResources().subscribe(() => this.updateUrlWithFilterOptions(this.filterOptions())); } onSortChanged(sortBy: string): void { @@ -158,11 +154,7 @@ export class GlobalSearchComponent implements OnInit, OnDestroy { } onPageChanged(link: string): void { - this.actions.getResourcesByLink(link).subscribe({ - next: () => { - this.scrollToTop(); - }, - }); + this.actions.getResourcesByLink(link).subscribe(() => this.scrollToTop()); } scrollToTop() { diff --git a/src/app/shared/components/resource-citations/resource-citations.component.ts b/src/app/shared/components/resource-citations/resource-citations.component.ts index 96e39530c..fadb05b38 100644 --- a/src/app/shared/components/resource-citations/resource-citations.component.ts +++ b/src/app/shared/components/resource-citations/resource-citations.component.ts @@ -29,7 +29,7 @@ import { Router } from '@angular/router'; import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; import { ToastService } from '@osf/shared/services/toast.service'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { CitationStyle } from '@shared/models/citations/citation-style.model'; import { CustomOption } from '@shared/models/select-option.model'; import { @@ -71,7 +71,7 @@ export class ResourceCitationsComponent { private readonly clipboard = inject(Clipboard); private readonly toastService = inject(ToastService); - private readonly viewOnlyService = inject(ViewOnlyService); + private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); private readonly filterSubject = new Subject(); customCitationChange = output(); diff --git a/src/app/shared/services/view-only.service.ts b/src/app/shared/services/view-only-link-helper.service.ts similarity index 97% rename from src/app/shared/services/view-only.service.ts rename to src/app/shared/services/view-only-link-helper.service.ts index 80caf7bde..e600722d6 100644 --- a/src/app/shared/services/view-only.service.ts +++ b/src/app/shared/services/view-only-link-helper.service.ts @@ -5,7 +5,7 @@ import { Router } from '@angular/router'; import { WINDOW } from '@core/provider/window.provider'; @Injectable({ providedIn: 'root' }) -export class ViewOnlyService { +export class ViewOnlyLinkHelperService { private readonly platformId = inject(PLATFORM_ID); private readonly window = inject(WINDOW); diff --git a/src/main.ts b/src/main.ts index aba4076a9..67d73f19f 100644 --- a/src/main.ts +++ b/src/main.ts @@ -1,11 +1,9 @@ -import { bootstrapApplication, provideClientHydration, withEventReplay } from '@angular/platform-browser'; +import { bootstrapApplication } from '@angular/platform-browser'; import { AppComponent } from '@osf/app.component'; import { appConfig } from '@osf/app.config'; -bootstrapApplication(AppComponent, { - providers: [...appConfig.providers, provideClientHydration(withEventReplay())], -}).catch((err) => +bootstrapApplication(AppComponent, appConfig).catch((err) => // eslint-disable-next-line no-console console.error(err) ); From 7e4a52645b82ba28c6dcc0f36a98f8f2e480ec37 Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 3 Dec 2025 17:15:17 +0200 Subject: [PATCH 05/11] fix(ssr): fixed preprint, project, registration overview pages for ssr --- src/app/app.routes.server.ts | 50 ++++++++++++++++++- src/app/core/guards/auth.guard.ts | 18 +++---- src/app/core/guards/is-file.guard.ts | 23 +++------ src/app/core/guards/is-project.guard.ts | 29 ++--------- src/app/core/guards/is-registry.guard.ts | 29 ++--------- .../guards/redirect-if-logged-in.guard.ts | 16 ++---- src/app/core/theme/semantic.ts | 10 +++- .../general-information.component.ts | 19 ++++++- .../preprint-details.component.ts | 12 +++-- .../files-widget/files-widget.component.ts | 8 ++- .../project-recent-activity.component.ts | 20 +++++++- .../overview/project-overview.component.ts | 20 +++++--- .../features/registry/registry.component.ts | 18 ++++--- .../socials-share-button.component.scss | 4 ++ 14 files changed, 162 insertions(+), 114 deletions(-) diff --git a/src/app/app.routes.server.ts b/src/app/app.routes.server.ts index 4d2479f9d..a46f725ba 100644 --- a/src/app/app.routes.server.ts +++ b/src/app/app.routes.server.ts @@ -10,7 +10,55 @@ export const serverRoutes: ServerRoute[] = [ renderMode: RenderMode.Server, }, { - path: '**', + path: 'forbidden', + renderMode: RenderMode.Server, + }, + { + path: 'request-access/:id', + renderMode: RenderMode.Server, + }, + { + path: 'not-found', + renderMode: RenderMode.Server, + }, + { + path: 'preprints/:providerId/:id', renderMode: RenderMode.Server, }, + { + path: 'dashboard', + renderMode: RenderMode.Client, + }, + { + path: 'my-projects', + renderMode: RenderMode.Client, + }, + { + path: 'my-registrations', + renderMode: RenderMode.Client, + }, + { + path: 'my-preprints', + renderMode: RenderMode.Client, + }, + { + path: 'profile', + renderMode: RenderMode.Client, + }, + { + path: 'settings/**', + renderMode: RenderMode.Client, + }, + { + path: ':id/overview', + renderMode: RenderMode.Server, + }, + { + path: ':id', + renderMode: RenderMode.Server, + }, + { + path: '**', + renderMode: RenderMode.Client, + }, ]; diff --git a/src/app/core/guards/auth.guard.ts b/src/app/core/guards/auth.guard.ts index 5b497e01b..e0a7b5155 100644 --- a/src/app/core/guards/auth.guard.ts +++ b/src/app/core/guards/auth.guard.ts @@ -7,27 +7,21 @@ import { CanActivateFn, Router } from '@angular/router'; import { AuthService } from '@core/services/auth.service'; import { GetCurrentUser, UserSelectors } from '@osf/core/store/user'; -import { ViewOnlyService } from '@osf/shared/services/view-only.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; export const authGuard: CanActivateFn = () => { const store = inject(Store); const router = inject(Router); const authService = inject(AuthService); - const viewOnlyHelper = inject(ViewOnlyService); - - const isAuthenticated = store.selectSnapshot(UserSelectors.isAuthenticated); - - if (isAuthenticated) { - return true; - } + const viewOnlyHelper = inject(ViewOnlyLinkHelperService); if (viewOnlyHelper.hasViewOnlyParam(router)) { return true; } return store.dispatch(GetCurrentUser).pipe( - switchMap(() => { - return store.select(UserSelectors.isAuthenticated).pipe( + switchMap(() => + store.select(UserSelectors.isAuthenticated).pipe( take(1), map((isAuthenticated) => { if (!isAuthenticated) { @@ -37,7 +31,7 @@ export const authGuard: CanActivateFn = () => { return true; }) - ); - }) + ) + ) ); }; diff --git a/src/app/core/guards/is-file.guard.ts b/src/app/core/guards/is-file.guard.ts index 24e8f3ad4..482e257f4 100644 --- a/src/app/core/guards/is-file.guard.ts +++ b/src/app/core/guards/is-file.guard.ts @@ -13,43 +13,32 @@ export const isFileGuard: CanMatchFn = (route: Route, segments: UrlSegment[]) => const store = inject(Store); const router = inject(Router); const platformId = inject(PLATFORM_ID); + const isBrowser = isPlatformBrowser(platformId); const id = segments[0]?.path; const isMetadataPath = segments[1]?.path === 'metadata'; let viewOnly: string | null = null; - if (isPlatformBrowser(platformId)) { + + if (isBrowser) { const urlObj = new URL(window.location.href); viewOnly = urlObj.searchParams.get('view_only'); } else { const routerUrl = router.url; const queryParams = routerUrl.split('?')[1]; + if (queryParams) { const params = new URLSearchParams(queryParams); viewOnly = params.get('view_only'); } } + const extras = viewOnly ? { queryParams: { view_only: viewOnly } } : {}; if (!id) { return false; } - const currentResource = store.selectSnapshot(CurrentResourceSelectors.getCurrentResource); - if (currentResource && currentResource.id === id) { - if (currentResource.type === CurrentResourceType.Files) { - if (isMetadataPath) { - return true; - } - if (currentResource.parentId) { - router.navigate(['/', currentResource.parentId, 'files', id], extras); - return false; - } - } - - return currentResource.type === CurrentResourceType.Files; - } - return store.dispatch(new GetResource(id)).pipe( switchMap(() => store.select(CurrentResourceSelectors.getCurrentResource)), map((resource) => { @@ -61,11 +50,13 @@ export const isFileGuard: CanMatchFn = (route: Route, segments: UrlSegment[]) => if (isMetadataPath) { return true; } + if (resource.parentId) { router.navigate(['/', resource.parentId, 'files', id], extras); return false; } } + return resource.type === CurrentResourceType.Files; }) ); diff --git a/src/app/core/guards/is-project.guard.ts b/src/app/core/guards/is-project.guard.ts index e41a461af..f8921a40d 100644 --- a/src/app/core/guards/is-project.guard.ts +++ b/src/app/core/guards/is-project.guard.ts @@ -19,32 +19,6 @@ export const isProjectGuard: CanMatchFn = (route: Route, segments: UrlSegment[]) return false; } - const currentResource = store.selectSnapshot(CurrentResourceSelectors.getCurrentResource); - const currentUser = store.selectSnapshot(UserSelectors.getCurrentUser); - - if (currentResource && !id.startsWith(currentResource.id)) { - if (currentResource.type === CurrentResourceType.Projects && currentResource.parentId) { - router.navigate(['/', currentResource.parentId, 'files', id], { queryParamsHandling: 'preserve' }); - return true; - } - - if (currentResource.type === CurrentResourceType.Preprints && currentResource.parentId) { - router.navigate(['/preprints', currentResource.parentId, id]); - return true; - } - - if (currentResource.type === CurrentResourceType.Users) { - if (currentUser && currentUser.id === currentResource.id) { - router.navigate(['/profile']); - } else { - router.navigate(['/user', id]); - } - return false; - } - - return currentResource.type === CurrentResourceType.Projects; - } - return store.dispatch(new GetResource(id)).pipe( switchMap(() => store.select(CurrentResourceSelectors.getCurrentResource)), map((resource) => { @@ -63,11 +37,14 @@ export const isProjectGuard: CanMatchFn = (route: Route, segments: UrlSegment[]) } if (resource.type === CurrentResourceType.Users) { + const currentUser = store.selectSnapshot(UserSelectors.getCurrentUser); + if (currentUser && currentUser.id === resource.id) { router.navigate(['/profile']); } else { router.navigate(['/user', id]); } + return false; } diff --git a/src/app/core/guards/is-registry.guard.ts b/src/app/core/guards/is-registry.guard.ts index 9c9369019..a187a25b9 100644 --- a/src/app/core/guards/is-registry.guard.ts +++ b/src/app/core/guards/is-registry.guard.ts @@ -19,32 +19,6 @@ export const isRegistryGuard: CanMatchFn = (route: Route, segments: UrlSegment[] return false; } - const currentResource = store.selectSnapshot(CurrentResourceSelectors.getCurrentResource); - const currentUser = store.selectSnapshot(UserSelectors.getCurrentUser); - - if (currentResource && !id.startsWith(currentResource.id)) { - if (currentResource.type === CurrentResourceType.Registrations && currentResource.parentId) { - router.navigate(['/', currentResource.parentId, 'files', id], { queryParamsHandling: 'preserve' }); - return true; - } - - if (currentResource.type === CurrentResourceType.Preprints && currentResource.parentId) { - router.navigate(['/preprints', currentResource.parentId, id]); - return true; - } - - if (currentResource.type === CurrentResourceType.Users) { - if (currentUser && currentUser.id === currentResource.id) { - router.navigate(['/profile']); - } else { - router.navigate(['/user', id]); - } - return false; - } - - return currentResource.type === CurrentResourceType.Registrations; - } - return store.dispatch(new GetResource(id)).pipe( switchMap(() => store.select(CurrentResourceSelectors.getCurrentResource)), map((resource) => { @@ -63,11 +37,14 @@ export const isRegistryGuard: CanMatchFn = (route: Route, segments: UrlSegment[] } if (resource.type === CurrentResourceType.Users) { + const currentUser = store.selectSnapshot(UserSelectors.getCurrentUser); + if (currentUser && currentUser.id === resource.id) { router.navigate(['/profile']); } else { router.navigate(['/user', id]); } + return false; } diff --git a/src/app/core/guards/redirect-if-logged-in.guard.ts b/src/app/core/guards/redirect-if-logged-in.guard.ts index b89bcb1eb..9e6232945 100644 --- a/src/app/core/guards/redirect-if-logged-in.guard.ts +++ b/src/app/core/guards/redirect-if-logged-in.guard.ts @@ -11,25 +11,19 @@ export const redirectIfLoggedInGuard: CanActivateFn = () => { const store = inject(Store); const router = inject(Router); - const isAuthenticated = store.selectSnapshot(UserSelectors.isAuthenticated); - - if (isAuthenticated) { - router.navigate(['/dashboard']); - return false; - } - return store.dispatch(GetCurrentUser).pipe( - switchMap(() => { - return store.select(UserSelectors.isAuthenticated).pipe( + switchMap(() => + store.select(UserSelectors.isAuthenticated).pipe( take(1), map((isAuthenticated) => { if (isAuthenticated) { router.navigate(['/dashboard']); return false; } + return true; }) - ); - }) + ) + ) ); }; diff --git a/src/app/core/theme/semantic.ts b/src/app/core/theme/semantic.ts index e5e87d8d1..7c4225fb6 100644 --- a/src/app/core/theme/semantic.ts +++ b/src/app/core/theme/semantic.ts @@ -25,7 +25,15 @@ export const semantic = { function getCssVariableValue(variableName: string): string { if (typeof document === 'undefined' || typeof getComputedStyle === 'undefined') { - return ''; + const fallbackColors: Record = { + '--pr-blue-1': '#337ab7', + '--bg-blue-3': '#f1f8fd', + '--green-1': '#357935', + '--red-1': '#b73333', + '--blue-1': '#3792b1', + }; + + return fallbackColors[variableName] || ''; } return getComputedStyle(document.documentElement).getPropertyValue(variableName).trim(); diff --git a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.ts b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.ts index 29eb2571b..8228a9cd1 100644 --- a/src/app/features/preprints/components/preprint-details/general-information/general-information.component.ts +++ b/src/app/features/preprints/components/preprint-details/general-information/general-information.component.ts @@ -5,7 +5,18 @@ import { TranslatePipe } from '@ngx-translate/core'; import { Card } from 'primeng/card'; import { Skeleton } from 'primeng/skeleton'; -import { ChangeDetectionStrategy, Component, computed, effect, inject, input, OnDestroy, output } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + input, + OnDestroy, + output, + PLATFORM_ID, +} from '@angular/core'; import { FormsModule } from '@angular/forms'; import { ENVIRONMENT } from '@core/provider/environment.provider'; @@ -48,6 +59,8 @@ import { PreprintDoiSectionComponent } from '../preprint-doi-section/preprint-do }) export class GeneralInformationComponent implements OnDestroy { private readonly environment = inject(ENVIRONMENT); + private readonly platformId = inject(PLATFORM_ID); + private readonly isBrowser = isPlatformBrowser(this.platformId); readonly ApplicabilityStatus = ApplicabilityStatus; readonly PreregLinkInfo = PreregLinkInfo; @@ -87,7 +100,9 @@ export class GeneralInformationComponent implements OnDestroy { } ngOnDestroy(): void { - this.actions.resetContributorsState(); + if (this.isBrowser) { + this.actions.resetContributorsState(); + } } handleLoadMoreContributors(): void { diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts b/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts index 9bf3f3088..ac9522746 100644 --- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts +++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.ts @@ -7,7 +7,7 @@ import { Skeleton } from 'primeng/skeleton'; import { catchError, EMPTY, filter, map, of } from 'rxjs'; -import { DatePipe } from '@angular/common'; +import { DatePipe, isPlatformBrowser } from '@angular/common'; import { HttpErrorResponse } from '@angular/common/http'; import { ChangeDetectionStrategy, @@ -19,6 +19,7 @@ import { inject, OnDestroy, OnInit, + PLATFORM_ID, } from '@angular/core'; import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; @@ -101,6 +102,8 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy { private readonly datePipe = inject(DatePipe); private readonly dataciteService = inject(DataciteService); private readonly prerenderReady = inject(PrerenderReadyService); + private readonly platformId = inject(PLATFORM_ID); + private readonly isBrowser = isPlatformBrowser(this.platformId); private readonly environment = inject(ENVIRONMENT); @@ -305,8 +308,11 @@ export class PreprintDetailsComponent implements OnInit, OnDestroy { } ngOnDestroy() { - this.actions.resetState(); - this.actions.clearCurrentProvider(); + if (this.isBrowser) { + this.actions.resetState(); + this.actions.clearCurrentProvider(); + } + this.helpScoutService.unsetResourceType(); } diff --git a/src/app/features/project/overview/components/files-widget/files-widget.component.ts b/src/app/features/project/overview/components/files-widget/files-widget.component.ts index 02e401c3a..8b6c926b4 100644 --- a/src/app/features/project/overview/components/files-widget/files-widget.component.ts +++ b/src/app/features/project/overview/components/files-widget/files-widget.component.ts @@ -6,6 +6,7 @@ import { Button } from 'primeng/button'; import { Skeleton } from 'primeng/skeleton'; import { TabsModule } from 'primeng/tabs'; +import { isPlatformBrowser } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -15,6 +16,7 @@ import { inject, input, model, + PLATFORM_ID, signal, } from '@angular/core'; import { ActivatedRoute, Router } from '@angular/router'; @@ -58,6 +60,8 @@ export class FilesWidgetComponent { private readonly environment = inject(ENVIRONMENT); private readonly destroyRef = inject(DestroyRef); private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); + private readonly platformId = inject(PLATFORM_ID); + private readonly isBrowser = isPlatformBrowser(this.platformId); readonly files = select(FilesSelectors.getFiles); readonly filesTotalCount = select(FilesSelectors.getFilesTotalCount); @@ -150,7 +154,9 @@ export class FilesWidgetComponent { }); this.destroyRef.onDestroy(() => { - this.actions.resetState(); + if (this.isBrowser) { + this.actions.resetState(); + } }); } diff --git a/src/app/features/project/overview/components/project-recent-activity/project-recent-activity.component.ts b/src/app/features/project/overview/components/project-recent-activity/project-recent-activity.component.ts index 5b4305004..0f531e93e 100644 --- a/src/app/features/project/overview/components/project-recent-activity/project-recent-activity.component.ts +++ b/src/app/features/project/overview/components/project-recent-activity/project-recent-activity.component.ts @@ -4,7 +4,18 @@ import { TranslatePipe } from '@ngx-translate/core'; import { PaginatorState } from 'primeng/paginator'; -import { ChangeDetectionStrategy, Component, computed, effect, input, OnDestroy, signal } from '@angular/core'; +import { isPlatformBrowser } from '@angular/common'; +import { + ChangeDetectionStrategy, + Component, + computed, + effect, + inject, + input, + OnDestroy, + PLATFORM_ID, + signal, +} from '@angular/core'; import { RecentActivityListComponent } from '@osf/shared/components/recent-activity/recent-activity-list.component'; import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; @@ -19,6 +30,9 @@ import { ActivityLogsSelectors, ClearActivityLogs, GetActivityLogs } from '@osf/ export class ProjectRecentActivityComponent implements OnDestroy { projectId = input(); + private readonly platformId = inject(PLATFORM_ID); + private readonly isBrowser = isPlatformBrowser(this.platformId); + pageSize = signal(5); currentPage = signal(1); @@ -42,7 +56,9 @@ export class ProjectRecentActivityComponent implements OnDestroy { } ngOnDestroy(): void { - this.actions.clearActivityLogsStore(); + if (this.isBrowser) { + this.actions.clearActivityLogsStore(); + } } onPageChange(event: PaginatorState) { diff --git a/src/app/features/project/overview/project-overview.component.ts b/src/app/features/project/overview/project-overview.component.ts index ecab1819e..5f2f09c15 100644 --- a/src/app/features/project/overview/project-overview.component.ts +++ b/src/app/features/project/overview/project-overview.component.ts @@ -7,6 +7,7 @@ import { Message } from 'primeng/message'; import { map, of } from 'rxjs'; +import { isPlatformBrowser } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -16,6 +17,7 @@ import { HostBinding, inject, OnInit, + PLATFORM_ID, } from '@angular/core'; import { takeUntilDestroyed, toSignal } from '@angular/core/rxjs-interop'; import { ActivatedRoute, Router, RouterLink } from '@angular/router'; @@ -100,6 +102,8 @@ export class ProjectOverviewComponent implements OnInit { private readonly toastService = inject(ToastService); private readonly viewOnlyService = inject(ViewOnlyLinkHelperService); private readonly customDialogService = inject(CustomDialogService); + private readonly platformId = inject(PLATFORM_ID); + private readonly isBrowser = isPlatformBrowser(this.platformId); submissions = select(CollectionsModerationSelectors.getCollectionSubmissions); collectionProvider = select(CollectionsSelectors.getCollectionProvider); @@ -283,12 +287,14 @@ export class ProjectOverviewComponent implements OnInit { } private setupCleanup(): void { - this.destroyRef.onDestroy(() => { - this.actions.clearProjectOverview(); - this.actions.clearWiki(); - this.actions.clearCollections(); - this.actions.clearCollectionModeration(); - this.actions.clearConfiguredAddons(); - }); + if (this.isBrowser) { + this.destroyRef.onDestroy(() => { + this.actions.clearProjectOverview(); + this.actions.clearWiki(); + this.actions.clearCollections(); + this.actions.clearCollectionModeration(); + this.actions.clearConfiguredAddons(); + }); + } } } diff --git a/src/app/features/registry/registry.component.ts b/src/app/features/registry/registry.component.ts index edd9ae630..ddc1d89d9 100644 --- a/src/app/features/registry/registry.component.ts +++ b/src/app/features/registry/registry.component.ts @@ -2,7 +2,7 @@ import { createDispatchMap, select } from '@ngxs/store'; import { filter, map } from 'rxjs'; -import { DatePipe } from '@angular/common'; +import { DatePipe, isPlatformBrowser } from '@angular/common'; import { ChangeDetectionStrategy, Component, @@ -12,6 +12,7 @@ import { HostBinding, inject, OnDestroy, + PLATFORM_ID, signal, } from '@angular/core'; import { takeUntilDestroyed, toObservable, toSignal } from '@angular/core/rxjs-interop'; @@ -47,10 +48,13 @@ export class RegistryComponent implements OnDestroy { private readonly dataciteService = inject(DataciteService); private readonly destroyRef = inject(DestroyRef); private readonly route = inject(ActivatedRoute); + private readonly router = inject(Router); private readonly helpScoutService = inject(HelpScoutService); private readonly environment = inject(ENVIRONMENT); private readonly prerenderReady = inject(PrerenderReadyService); - readonly analyticsService = inject(AnalyticsService); + private readonly analyticsService = inject(AnalyticsService); + private readonly platformId = inject(PLATFORM_ID); + private readonly isBrowser = isPlatformBrowser(this.platformId); private readonly actions = createDispatchMap({ getRegistryWithRelatedData: GetRegistryWithRelatedData, @@ -71,6 +75,8 @@ export class RegistryComponent implements OnDestroy { readonly license = select(RegistrySelectors.getLicense); readonly isLicenseLoading = select(RegistrySelectors.isLicenseLoading); + private readonly lastMetaTagsRegistryId = signal(null); + private readonly allDataLoaded = computed( () => !this.isRegistryLoading() && @@ -79,9 +85,6 @@ export class RegistryComponent implements OnDestroy { !!this.registry() ); - private readonly lastMetaTagsRegistryId = signal(null); - readonly router = inject(Router); - constructor() { this.prerenderReady.setNotReady(); this.helpScoutService.setResourceType('registration'); @@ -127,7 +130,10 @@ export class RegistryComponent implements OnDestroy { } ngOnDestroy(): void { - this.actions.clearCurrentProvider(); + if (this.isBrowser) { + this.actions.clearCurrentProvider(); + } + this.helpScoutService.unsetResourceType(); } diff --git a/src/app/shared/components/socials-share-button/socials-share-button.component.scss b/src/app/shared/components/socials-share-button/socials-share-button.component.scss index 5a4fc67ca..d2373875e 100644 --- a/src/app/shared/components/socials-share-button/socials-share-button.component.scss +++ b/src/app/shared/components/socials-share-button/socials-share-button.component.scss @@ -1,3 +1,7 @@ +:host { + display: flex; +} + .social-link { background-color: var(--pr-blue-1); border-radius: 0.25rem; From 7e133b2357fd5cc2d9a1408dfcde1606f2c8ea5e Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 3 Dec 2025 17:55:53 +0200 Subject: [PATCH 06/11] fix(ssr): updated cookies for ssr --- src/app/core/interceptors/auth.interceptor.ts | 43 ++++++++++++------- 1 file changed, 28 insertions(+), 15 deletions(-) diff --git a/src/app/core/interceptors/auth.interceptor.ts b/src/app/core/interceptors/auth.interceptor.ts index 8c5308cf2..d73511118 100644 --- a/src/app/core/interceptors/auth.interceptor.ts +++ b/src/app/core/interceptors/auth.interceptor.ts @@ -4,7 +4,7 @@ import { Observable } from 'rxjs'; import { isPlatformBrowser } from '@angular/common'; import { HttpEvent, HttpHandlerFn, HttpInterceptorFn, HttpRequest } from '@angular/common/http'; -import { inject, PLATFORM_ID } from '@angular/core'; +import { inject, PLATFORM_ID, REQUEST } from '@angular/core'; export const authInterceptor: HttpInterceptorFn = ( req: HttpRequest, @@ -12,29 +12,42 @@ export const authInterceptor: HttpInterceptorFn = ( ): Observable> => { const cookieService = inject(CookieService); const platformId = inject(PLATFORM_ID); + const serverRequest = inject(REQUEST, { optional: true }); - const csrfToken = isPlatformBrowser(platformId) ? cookieService.get('api-csrf') : null; - - if (!req.url.includes('/api.crossref.org/funders')) { - const headers: Record = {}; + if (req.url.includes('/api.crossref.org/funders')) { + return next(req); + } - headers['Accept'] = req.responseType === 'text' ? '*/*' : 'application/vnd.api+json;version=2.20'; + const headers: Record = {}; + headers['Accept'] = req.responseType === 'text' ? '*/*' : 'application/vnd.api+json;version=2.20'; - if (!req.headers.has('Content-Type')) { - headers['Content-Type'] = 'application/vnd.api+json'; - } + if (!req.headers.has('Content-Type')) { + headers['Content-Type'] = 'application/vnd.api+json'; + } + if (isPlatformBrowser(platformId)) { + const csrfToken = cookieService.get('api-csrf'); if (csrfToken) { headers['X-CSRFToken'] = csrfToken; } - const authReq = req.clone({ - setHeaders: headers, - withCredentials: true, - }); + const authReq = req.clone({ setHeaders: headers, withCredentials: true }); return next(authReq); - } else { - return next(req); } + + if (serverRequest) { + const cookieHeader = serverRequest.headers.get('cookie') || ''; + if (cookieHeader) { + headers['Cookie'] = cookieHeader; + const csrfMatch = cookieHeader.match(/api-csrf=([^;]+)/); + if (csrfMatch) { + headers['X-CSRFToken'] = csrfMatch[1]; + } + } + } + + const authReq = req.clone({ setHeaders: headers }); + + return next(authReq); }; From 4f223e0a10de9ce8bb3e7ef4c5d73bf9a00c3f75 Mon Sep 17 00:00:00 2001 From: nsemets Date: Wed, 3 Dec 2025 18:07:22 +0200 Subject: [PATCH 07/11] fix(meta-tags): removed unnecessary condition --- src/app/shared/services/meta-tags.service.ts | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/app/shared/services/meta-tags.service.ts b/src/app/shared/services/meta-tags.service.ts index dffa846be..3bfcb37eb 100644 --- a/src/app/shared/services/meta-tags.service.ts +++ b/src/app/shared/services/meta-tags.service.ts @@ -252,10 +252,6 @@ export class MetaTagsService { } private applyHeadTags(headTags: HeadTagDef[]): void { - if (!isPlatformBrowser(this.platformId)) { - return; - } - headTags.forEach((tag) => { if (tag.type === 'meta') { this.meta.addTag(tag.attrs); From c90e72f94de59818b973b0295daeec28b99bfaa6 Mon Sep 17 00:00:00 2001 From: nsemets Date: Thu, 4 Dec 2025 15:41:13 +0200 Subject: [PATCH 08/11] fix(routes): returned project state --- src/app/app.routes.ts | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/app.routes.ts b/src/app/app.routes.ts index e0c0676bc..e908cd10a 100644 --- a/src/app/app.routes.ts +++ b/src/app/app.routes.ts @@ -15,6 +15,7 @@ import { LicensesHandlers, ProjectsHandlers, ProvidersHandlers } from './feature import { FilesHandlers } from './features/registries/store/handlers/files.handlers'; import { LicensesService } from './shared/services/licenses.service'; import { BookmarksState } from './shared/stores/bookmarks'; +import { ProjectsState } from './shared/stores/projects'; export const routes: Routes = [ { @@ -34,6 +35,7 @@ export const routes: Routes = [ import('./features/home/pages/dashboard/dashboard.component').then((mod) => mod.DashboardComponent), data: { skipBreadcrumbs: true }, canActivate: [authGuard], + providers: [provideStates([ProjectsState])], }, { path: 'register', @@ -67,7 +69,7 @@ export const routes: Routes = [ loadComponent: () => import('./features/my-projects/my-projects.component').then((mod) => mod.MyProjectsComponent), canActivate: [authGuard], - providers: [provideStates([BookmarksState])], + providers: [provideStates([BookmarksState, ProjectsState])], }, { path: 'my-registrations', From 2a058dbc5e209ef7302f1dc7bb9f33ecb2378fc2 Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 8 Dec 2025 11:21:53 +0200 Subject: [PATCH 09/11] fix(newrelic): returned new relic back --- src/assets/js/newrelic/newrelic.snippet.js | 3319 ++++++++++++++++++++ src/index.html | 1 + 2 files changed, 3320 insertions(+) create mode 100644 src/assets/js/newrelic/newrelic.snippet.js diff --git a/src/assets/js/newrelic/newrelic.snippet.js b/src/assets/js/newrelic/newrelic.snippet.js new file mode 100644 index 000000000..e63e4fc1d --- /dev/null +++ b/src/assets/js/newrelic/newrelic.snippet.js @@ -0,0 +1,3319 @@ +window.NREUM || (NREUM = {}); +NREUM.init = { + distributed_tracing: { enabled: true }, + performance: { capture_measures: true }, + privacy: { cookies_enabled: true }, + ajax: { deny_list: ['bam.nr-data.net'] }, +}; + +NREUM.loader_config = { + accountID: '772413', + trustKey: '772413', + agentID: '1835137194', + licenseKey: '704513e63b', + applicationID: '1835137194', +}; +NREUM.info = { + beacon: 'bam.nr-data.net', + errorBeacon: 'bam.nr-data.net', + licenseKey: '704513e63b', + applicationID: '1835137194', + sa: 1, +}; /*! For license information please see nr-loader-spa-1.301.0.min.js.LICENSE.txt */ +(() => { + var e, + t, + r = { + 384: (e, t, r) => { + 'use strict'; + r.d(t, { NT: () => a, US: () => d, Zm: () => s, bQ: () => u, dV: () => c, pV: () => l }); + var n = r(6154), + i = r(1863), + o = r(1910); + const a = { beacon: 'bam.nr-data.net', errorBeacon: 'bam.nr-data.net' }; + function s() { + return n.gm.NREUM || (n.gm.NREUM = {}), void 0 === n.gm.newrelic && (n.gm.newrelic = n.gm.NREUM), n.gm.NREUM; + } + function c() { + let e = s(); + return ( + e.o || + ((e.o = { + ST: n.gm.setTimeout, + SI: n.gm.setImmediate || n.gm.setInterval, + CT: n.gm.clearTimeout, + XHR: n.gm.XMLHttpRequest, + REQ: n.gm.Request, + EV: n.gm.Event, + PR: n.gm.Promise, + MO: n.gm.MutationObserver, + FETCH: n.gm.fetch, + WS: n.gm.WebSocket, + }), + (0, o.i)(...Object.values(e.o))), + e + ); + } + function u(e, t) { + let r = s(); + (r.initializedAgents ??= {}), + (t.initializedAt = { ms: (0, i.t)(), date: new Date() }), + (r.initializedAgents[e] = t); + } + function d(e, t) { + s()[e] = t; + } + function l() { + return ( + (function () { + let e = s(); + const t = e.info || {}; + e.info = { beacon: a.beacon, errorBeacon: a.errorBeacon, ...t }; + })(), + (function () { + let e = s(); + const t = e.init || {}; + e.init = { ...t }; + })(), + c(), + (function () { + let e = s(); + const t = e.loader_config || {}; + e.loader_config = { ...t }; + })(), + s() + ); + } + }, + 782: (e, t, r) => { + 'use strict'; + r.d(t, { T: () => n }); + const n = r(860).K7.pageViewTiming; + }, + 860: (e, t, r) => { + 'use strict'; + r.d(t, { + $J: () => d, + K7: () => c, + P3: () => u, + XX: () => i, + Yy: () => s, + df: () => o, + qY: () => n, + v4: () => a, + }); + const n = 'events', + i = 'jserrors', + o = 'browser/blobs', + a = 'rum', + s = 'browser/logs', + c = { + ajax: 'ajax', + genericEvents: 'generic_events', + jserrors: i, + logging: 'logging', + metrics: 'metrics', + pageAction: 'page_action', + pageViewEvent: 'page_view_event', + pageViewTiming: 'page_view_timing', + sessionReplay: 'session_replay', + sessionTrace: 'session_trace', + softNav: 'soft_navigations', + spa: 'spa', + }, + u = { + [c.pageViewEvent]: 1, + [c.pageViewTiming]: 2, + [c.metrics]: 3, + [c.jserrors]: 4, + [c.spa]: 5, + [c.ajax]: 6, + [c.sessionTrace]: 7, + [c.softNav]: 8, + [c.sessionReplay]: 9, + [c.logging]: 10, + [c.genericEvents]: 11, + }, + d = { + [c.pageViewEvent]: a, + [c.pageViewTiming]: n, + [c.ajax]: n, + [c.spa]: n, + [c.softNav]: n, + [c.metrics]: i, + [c.jserrors]: i, + [c.sessionTrace]: o, + [c.sessionReplay]: o, + [c.logging]: s, + [c.genericEvents]: 'ins', + }; + }, + 944: (e, t, r) => { + 'use strict'; + r.d(t, { R: () => i }); + var n = r(3241); + function i(e, t) { + 'function' == typeof console.debug && + (console.debug( + 'New Relic Warning: https://github.com/newrelic/newrelic-browser-agent/blob/main/docs/warning-codes.md#'.concat( + e + ), + t + ), + (0, n.W)({ + agentIdentifier: null, + drained: null, + type: 'data', + name: 'warn', + feature: 'warn', + data: { code: e, secondary: t }, + })); + } + }, + 993: (e, t, r) => { + 'use strict'; + r.d(t, { A$: () => o, ET: () => a, TZ: () => s, p_: () => i }); + var n = r(860); + const i = { ERROR: 'ERROR', WARN: 'WARN', INFO: 'INFO', DEBUG: 'DEBUG', TRACE: 'TRACE' }, + o = { OFF: 0, ERROR: 1, WARN: 2, INFO: 3, DEBUG: 4, TRACE: 5 }, + a = 'log', + s = n.K7.logging; + }, + 1687: (e, t, r) => { + 'use strict'; + r.d(t, { Ak: () => u, Ze: () => f, x3: () => d }); + var n = r(3241), + i = r(7836), + o = r(3606), + a = r(860), + s = r(2646); + const c = {}; + function u(e, t) { + const r = { staged: !1, priority: a.P3[t] || 0 }; + l(e), c[e].get(t) || c[e].set(t, r); + } + function d(e, t) { + e && c[e] && (c[e].get(t) && c[e].delete(t), p(e, t, !1), c[e].size && h(e)); + } + function l(e) { + if (!e) throw new Error('agentIdentifier required'); + c[e] || (c[e] = new Map()); + } + function f(e = '', t = 'feature', r = !1) { + if ((l(e), !e || !c[e].get(t) || r)) return p(e, t); + (c[e].get(t).staged = !0), h(e); + } + function h(e) { + const t = Array.from(c[e]); + t.every(([e, t]) => t.staged) && + (t.sort((e, t) => e[1].priority - t[1].priority), + t.forEach(([t]) => { + c[e].delete(t), p(e, t); + })); + } + function p(e, t, r = !0) { + const a = e ? i.ee.get(e) : i.ee, + c = o.i.handlers; + if (!a.aborted && a.backlog && c) { + if (((0, n.W)({ agentIdentifier: e, type: 'lifecycle', name: 'drain', feature: t }), r)) { + const e = a.backlog[t], + r = c[t]; + if (r) { + for (let t = 0; e && t < e.length; ++t) g(e[t], r); + Object.entries(r).forEach(([e, t]) => { + Object.values(t || {}).forEach((t) => { + t[0]?.on && t[0]?.context() instanceof s.y && t[0].on(e, t[1]); + }); + }); + } + } + a.isolatedBacklog || delete c[t], (a.backlog[t] = null), a.emit('drain-' + t, []); + } + } + function g(e, t) { + var r = e[1]; + Object.values(t[r] || {}).forEach((t) => { + var r = e[0]; + if (t[0] === r) { + var n = t[1], + i = e[3], + o = e[2]; + n.apply(i, o); + } + }); + } + }, + 1741: (e, t, r) => { + 'use strict'; + r.d(t, { W: () => o }); + var n = r(944), + i = r(4261); + class o { + #e(e, ...t) { + if (this[e] !== o.prototype[e]) return this[e](...t); + (0, n.R)(35, e); + } + addPageAction(e, t) { + return this.#e(i.hG, e, t); + } + register(e) { + return this.#e(i.eY, e); + } + recordCustomEvent(e, t) { + return this.#e(i.fF, e, t); + } + setPageViewName(e, t) { + return this.#e(i.Fw, e, t); + } + setCustomAttribute(e, t, r) { + return this.#e(i.cD, e, t, r); + } + noticeError(e, t) { + return this.#e(i.o5, e, t); + } + setUserId(e) { + return this.#e(i.Dl, e); + } + setApplicationVersion(e) { + return this.#e(i.nb, e); + } + setErrorHandler(e) { + return this.#e(i.bt, e); + } + addRelease(e, t) { + return this.#e(i.k6, e, t); + } + log(e, t) { + return this.#e(i.$9, e, t); + } + start() { + return this.#e(i.d3); + } + finished(e) { + return this.#e(i.BL, e); + } + recordReplay() { + return this.#e(i.CH); + } + pauseReplay() { + return this.#e(i.Tb); + } + addToTrace(e) { + return this.#e(i.U2, e); + } + setCurrentRouteName(e) { + return this.#e(i.PA, e); + } + interaction(e) { + return this.#e(i.dT, e); + } + wrapLogger(e, t, r) { + return this.#e(i.Wb, e, t, r); + } + measure(e, t) { + return this.#e(i.V1, e, t); + } + } + }, + 1863: (e, t, r) => { + 'use strict'; + function n() { + return Math.floor(performance.now()); + } + r.d(t, { t: () => n }); + }, + 1910: (e, t, r) => { + 'use strict'; + r.d(t, { i: () => o }); + var n = r(944); + const i = new Map(); + function o(...e) { + return e.every((e) => { + if (i.has(e)) return i.get(e); + const t = 'function' == typeof e && e.toString().includes('[native code]'); + return t || (0, n.R)(64, e?.name || e?.toString()), i.set(e, t), t; + }); + } + }, + 2555: (e, t, r) => { + 'use strict'; + r.d(t, { D: () => s, f: () => a }); + var n = r(384), + i = r(8122); + const o = { + beacon: n.NT.beacon, + errorBeacon: n.NT.errorBeacon, + licenseKey: void 0, + applicationID: void 0, + sa: void 0, + queueTime: void 0, + applicationTime: void 0, + ttGuid: void 0, + user: void 0, + account: void 0, + product: void 0, + extra: void 0, + jsAttributes: {}, + userAttributes: void 0, + atts: void 0, + transactionName: void 0, + tNamePlain: void 0, + }; + function a(e) { + try { + return !!e.licenseKey && !!e.errorBeacon && !!e.applicationID; + } catch (e) { + return !1; + } + } + const s = (e) => (0, i.a)(e, o); + }, + 2614: (e, t, r) => { + 'use strict'; + r.d(t, { BB: () => a, H3: () => n, g: () => u, iL: () => c, tS: () => s, uh: () => i, wk: () => o }); + const n = 'NRBA', + i = 'SESSION', + o = 144e5, + a = 18e5, + s = { + STARTED: 'session-started', + PAUSE: 'session-pause', + RESET: 'session-reset', + RESUME: 'session-resume', + UPDATE: 'session-update', + }, + c = { SAME_TAB: 'same-tab', CROSS_TAB: 'cross-tab' }, + u = { OFF: 0, FULL: 1, ERROR: 2 }; + }, + 2646: (e, t, r) => { + 'use strict'; + r.d(t, { y: () => n }); + class n { + constructor(e) { + this.contextId = e; + } + } + }, + 2843: (e, t, r) => { + 'use strict'; + r.d(t, { u: () => i }); + var n = r(3878); + function i(e, t = !1, r, i) { + (0, n.DD)( + 'visibilitychange', + function () { + if (t) return void ('hidden' === document.visibilityState && e()); + e(document.visibilityState); + }, + r, + i + ); + } + }, + 3241: (e, t, r) => { + 'use strict'; + r.d(t, { W: () => o }); + var n = r(6154); + const i = 'newrelic'; + function o(e = {}) { + try { + n.gm.dispatchEvent(new CustomEvent(i, { detail: e })); + } catch (e) {} + } + }, + 3304: (e, t, r) => { + 'use strict'; + r.d(t, { A: () => o }); + var n = r(7836); + const i = () => { + const e = new WeakSet(); + return (t, r) => { + if ('object' == typeof r && null !== r) { + if (e.has(r)) return; + e.add(r); + } + return r; + }; + }; + function o(e) { + try { + return JSON.stringify(e, i()) ?? ''; + } catch (e) { + try { + n.ee.emit('internal-error', [e]); + } catch (e) {} + return ''; + } + } + }, + 3333: (e, t, r) => { + 'use strict'; + r.d(t, { + $v: () => d, + TZ: () => n, + Xh: () => c, + Zp: () => i, + kd: () => u, + mq: () => s, + nf: () => a, + qN: () => o, + }); + const n = r(860).K7.genericEvents, + i = ['auxclick', 'click', 'copy', 'keydown', 'paste', 'scrollend'], + o = ['focus', 'blur'], + a = 4, + s = 1e3, + c = 2e3, + u = ['PageAction', 'UserAction', 'BrowserPerformance'], + d = { RESOURCES: 'experimental.resources' }; + }, + 3434: (e, t, r) => { + 'use strict'; + r.d(t, { Jt: () => o, YM: () => u }); + var n = r(7836), + i = r(5607); + const o = 'nr@original:'.concat(i.W), + a = 50; + var s = Object.prototype.hasOwnProperty, + c = !1; + function u(e, t) { + return ( + e || (e = n.ee), + (r.inPlace = function (e, t, n, i, o) { + n || (n = ''); + const a = '-' === n.charAt(0); + for (let s = 0; s < t.length; s++) { + const c = t[s], + u = e[c]; + l(u) || (e[c] = r(u, a ? c + n : n, i, c, o)); + } + }), + (r.flag = o), + r + ); + function r(t, r, n, c, u) { + return l(t) + ? t + : (r || (r = ''), + (nrWrapper[o] = t), + (function (e, t, r) { + if (Object.defineProperty && Object.keys) + try { + return ( + Object.keys(e).forEach(function (r) { + Object.defineProperty(t, r, { + get: function () { + return e[r]; + }, + set: function (t) { + return (e[r] = t), t; + }, + }); + }), + t + ); + } catch (e) { + d([e], r); + } + for (var n in e) s.call(e, n) && (t[n] = e[n]); + })(t, nrWrapper, e), + nrWrapper); + function nrWrapper() { + var o, s, l, f; + let h; + try { + (s = this), (o = [...arguments]), (l = 'function' == typeof n ? n(o, s) : n || {}); + } catch (t) { + d([t, '', [o, s, c], l], e); + } + i(r + 'start', [o, s, c], l, u); + const p = performance.now(); + let g; + try { + return (f = t.apply(s, o)), (g = performance.now()), f; + } catch (e) { + throw ((g = performance.now()), i(r + 'err', [o, s, e], l, u), (h = e), h); + } finally { + const e = g - p, + t = { start: p, end: g, duration: e, isLongTask: e >= a, methodName: c, thrownError: h }; + t.isLongTask && i('long-task', [t, s], l, u), i(r + 'end', [o, s, f], l, u); + } + } + } + function i(r, n, i, o) { + if (!c || t) { + var a = c; + c = !0; + try { + e.emit(r, n, i, t, o); + } catch (t) { + d([t, r, n, i], e); + } + c = a; + } + } + } + function d(e, t) { + t || (t = n.ee); + try { + t.emit('internal-error', e); + } catch (e) {} + } + function l(e) { + return !(e && 'function' == typeof e && e.apply && !e[o]); + } + }, + 3496: (e, t, r) => { + 'use strict'; + function n(e) { + return !e || !(!e.licenseKey || !e.applicationID); + } + function i(e, t) { + return !e || (e.licenseKey === t.info.licenseKey && e.applicationID === t.info.applicationID); + } + r.d(t, { A: () => i, I: () => n }); + }, + 3606: (e, t, r) => { + 'use strict'; + r.d(t, { i: () => o }); + var n = r(9908); + o.on = a; + var i = (o.handlers = {}); + function o(e, t, r, o) { + a(o || n.d, i, e, t, r); + } + function a(e, t, r, i, o) { + o || (o = 'feature'), e || (e = n.d); + var a = (t[o] = t[o] || {}); + (a[r] = a[r] || []).push([e, i]); + } + }, + 3738: (e, t, r) => { + 'use strict'; + r.d(t, { + He: () => i, + Kp: () => s, + Lc: () => u, + Rz: () => d, + TZ: () => n, + bD: () => o, + d3: () => a, + jx: () => l, + sl: () => f, + uP: () => c, + }); + const n = r(860).K7.sessionTrace, + i = 'bstResource', + o = 'resource', + a = '-start', + s = '-end', + c = 'fn' + a, + u = 'fn' + s, + d = 'pushState', + l = 1e3, + f = 3e4; + }, + 3785: (e, t, r) => { + 'use strict'; + r.d(t, { R: () => c, b: () => u }); + var n = r(9908), + i = r(1863), + o = r(860), + a = r(8154), + s = r(993); + function c(e, t, r = {}, c = s.p_.INFO, u, d = (0, i.t)()) { + (0, n.p)(a.xV, ['API/logging/'.concat(c.toLowerCase(), '/called')], void 0, o.K7.metrics, e), + (0, n.p)(s.ET, [d, t, r, c, u], void 0, o.K7.logging, e); + } + function u(e) { + return 'string' == typeof e && Object.values(s.p_).some((t) => t === e.toUpperCase().trim()); + } + }, + 3878: (e, t, r) => { + 'use strict'; + function n(e, t) { + return { capture: e, passive: !1, signal: t }; + } + function i(e, t, r = !1, i) { + window.addEventListener(e, t, n(r, i)); + } + function o(e, t, r = !1, i) { + document.addEventListener(e, t, n(r, i)); + } + r.d(t, { DD: () => o, jT: () => n, sp: () => i }); + }, + 3962: (e, t, r) => { + 'use strict'; + r.d(t, { + AM: () => a, + O2: () => l, + OV: () => o, + Qu: () => f, + TZ: () => c, + ih: () => h, + pP: () => s, + t1: () => d, + tC: () => i, + wD: () => u, + }); + var n = r(860); + const i = ['click', 'keydown', 'submit'], + o = 'popstate', + a = 'api', + s = 'initialPageLoad', + c = n.K7.softNav, + u = 5e3, + d = 500, + l = { INITIAL_PAGE_LOAD: '', ROUTE_CHANGE: 1, UNSPECIFIED: 2 }, + f = { INTERACTION: 1, AJAX: 2, CUSTOM_END: 3, CUSTOM_TRACER: 4 }, + h = { IP: 'in progress', PF: 'pending finish', FIN: 'finished', CAN: 'cancelled' }; + }, + 4234: (e, t, r) => { + 'use strict'; + r.d(t, { W: () => o }); + var n = r(7836), + i = r(1687); + class o { + constructor(e, t) { + (this.agentIdentifier = e), (this.ee = n.ee.get(e)), (this.featureName = t), (this.blocked = !1); + } + deregisterDrain() { + (0, i.x3)(this.agentIdentifier, this.featureName); + } + } + }, + 4261: (e, t, r) => { + 'use strict'; + r.d(t, { + $9: () => d, + BL: () => c, + CH: () => p, + Dl: () => R, + Fw: () => w, + PA: () => v, + Pl: () => n, + Tb: () => f, + U2: () => a, + V1: () => E, + Wb: () => T, + bt: () => y, + cD: () => b, + d3: () => x, + dT: () => u, + eY: () => g, + fF: () => h, + hG: () => o, + hw: () => i, + k6: () => s, + nb: () => m, + o5: () => l, + }); + const n = 'api-', + i = n + 'ixn-', + o = 'addPageAction', + a = 'addToTrace', + s = 'addRelease', + c = 'finished', + u = 'interaction', + d = 'log', + l = 'noticeError', + f = 'pauseReplay', + h = 'recordCustomEvent', + p = 'recordReplay', + g = 'register', + m = 'setApplicationVersion', + v = 'setCurrentRouteName', + b = 'setCustomAttribute', + y = 'setErrorHandler', + w = 'setPageViewName', + R = 'setUserId', + x = 'start', + T = 'wrapLogger', + E = 'measure'; + }, + 5205: (e, t, r) => { + 'use strict'; + r.d(t, { j: () => O }); + var n = r(384), + i = r(1741); + var o = r(2555), + a = r(3333); + const s = (e) => { + if (!e || 'string' != typeof e) return !1; + try { + document.createDocumentFragment().querySelector(e); + } catch { + return !1; + } + return !0; + }; + var c = r(2614), + u = r(944), + d = r(8122); + const l = '[data-nr-mask]', + f = (e) => + (0, d.a)( + e, + (() => { + const e = { + feature_flags: [], + experimental: { resources: !1 }, + mask_selector: '*', + block_selector: '[data-nr-block]', + mask_input_options: { + color: !1, + date: !1, + 'datetime-local': !1, + email: !1, + month: !1, + number: !1, + range: !1, + search: !1, + tel: !1, + text: !1, + time: !1, + url: !1, + week: !1, + textarea: !1, + select: !1, + password: !0, + }, + }; + return { + ajax: { deny_list: void 0, block_internal: !0, enabled: !0, autoStart: !0 }, + api: { allow_registered_children: !0, duplicate_registered_data: !1 }, + distributed_tracing: { + enabled: void 0, + exclude_newrelic_header: void 0, + cors_use_newrelic_header: void 0, + cors_use_tracecontext_headers: void 0, + allowed_origins: void 0, + }, + get feature_flags() { + return e.feature_flags; + }, + set feature_flags(t) { + e.feature_flags = t; + }, + generic_events: { enabled: !0, autoStart: !0 }, + harvest: { interval: 30 }, + jserrors: { enabled: !0, autoStart: !0 }, + logging: { enabled: !0, autoStart: !0 }, + metrics: { enabled: !0, autoStart: !0 }, + obfuscate: void 0, + page_action: { enabled: !0 }, + page_view_event: { enabled: !0, autoStart: !0 }, + page_view_timing: { enabled: !0, autoStart: !0 }, + performance: { + capture_marks: !1, + capture_measures: !1, + capture_detail: !0, + resources: { + get enabled() { + return e.feature_flags.includes(a.$v.RESOURCES) || e.experimental.resources; + }, + set enabled(t) { + e.experimental.resources = t; + }, + asset_types: [], + first_party_domains: [], + ignore_newrelic: !0, + }, + }, + privacy: { cookies_enabled: !0 }, + proxy: { assets: void 0, beacon: void 0 }, + session: { expiresMs: c.wk, inactiveMs: c.BB }, + session_replay: { + autoStart: !0, + enabled: !1, + preload: !1, + sampling_rate: 10, + error_sampling_rate: 100, + collect_fonts: !1, + inline_images: !1, + fix_stylesheets: !0, + mask_all_inputs: !0, + get mask_text_selector() { + return e.mask_selector; + }, + set mask_text_selector(t) { + s(t) + ? (e.mask_selector = ''.concat(t, ',').concat(l)) + : '' === t || null === t + ? (e.mask_selector = l) + : (0, u.R)(5, t); + }, + get block_class() { + return 'nr-block'; + }, + get ignore_class() { + return 'nr-ignore'; + }, + get mask_text_class() { + return 'nr-mask'; + }, + get block_selector() { + return e.block_selector; + }, + set block_selector(t) { + s(t) ? (e.block_selector += ','.concat(t)) : '' !== t && (0, u.R)(6, t); + }, + get mask_input_options() { + return e.mask_input_options; + }, + set mask_input_options(t) { + t && 'object' == typeof t ? (e.mask_input_options = { ...t, password: !0 }) : (0, u.R)(7, t); + }, + }, + session_trace: { enabled: !0, autoStart: !0 }, + soft_navigations: { enabled: !0, autoStart: !0 }, + spa: { enabled: !0, autoStart: !0 }, + ssl: void 0, + user_actions: { enabled: !0, elementAttributes: ['id', 'className', 'tagName', 'type'] }, + }; + })() + ); + var h = r(6154), + p = r(9324); + let g = 0; + const m = { buildEnv: p.F3, distMethod: p.Xs, version: p.xv, originTime: h.WN }, + v = { + appMetadata: {}, + customTransaction: void 0, + denyList: void 0, + disabled: !1, + entityManager: void 0, + harvester: void 0, + isolatedBacklog: !1, + isRecording: !1, + loaderType: void 0, + maxBytes: 3e4, + obfuscator: void 0, + onerror: void 0, + ptid: void 0, + releaseIds: {}, + session: void 0, + timeKeeper: void 0, + jsAttributesMetadata: { bytes: 0 }, + get harvestCount() { + return ++g; + }, + }, + b = (e) => { + const t = (0, d.a)(e, v), + r = Object.keys(m).reduce( + (e, t) => ((e[t] = { value: m[t], writable: !1, configurable: !0, enumerable: !0 }), e), + {} + ); + return Object.defineProperties(t, r); + }; + var y = r(5701); + const w = (e) => { + const t = e.startsWith('http'); + (e += '/'), (r.p = t ? e : 'https://' + e); + }; + var R = r(7836), + x = r(3241); + const T = { + accountID: void 0, + trustKey: void 0, + agentID: void 0, + licenseKey: void 0, + applicationID: void 0, + xpid: void 0, + }, + E = (e) => (0, d.a)(e, T), + A = new Set(); + function O(e, t = {}, r, a) { + let { init: s, info: c, loader_config: u, runtime: d = {}, exposed: l = !0 } = t; + if (!c) { + const e = (0, n.pV)(); + (s = e.init), (c = e.info), (u = e.loader_config); + } + (e.init = f(s || {})), + (e.loader_config = E(u || {})), + (c.jsAttributes ??= {}), + h.bv && (c.jsAttributes.isWorker = !0), + (e.info = (0, o.D)(c)); + const p = e.init, + g = [c.beacon, c.errorBeacon]; + A.has(e.agentIdentifier) || + (p.proxy.assets && (w(p.proxy.assets), g.push(p.proxy.assets)), + p.proxy.beacon && g.push(p.proxy.beacon), + (e.beacons = [...g]), + (function (e) { + const t = (0, n.pV)(); + Object.getOwnPropertyNames(i.W.prototype).forEach((r) => { + const n = i.W.prototype[r]; + if ('function' != typeof n || 'constructor' === n) return; + let o = t[r]; + e[r] && + !1 !== e.exposed && + 'micro-agent' !== e.runtime?.loaderType && + (t[r] = (...t) => { + const n = e[r](...t); + return o ? o(...t) : n; + }); + }); + })(e), + (0, n.US)('activatedFeatures', y.B), + (e.runSoftNavOverSpa &&= !0 === p.soft_navigations.enabled && p.feature_flags.includes('soft_nav'))), + (d.denyList = [...(p.ajax.deny_list || []), ...(p.ajax.block_internal ? g : [])]), + (d.ptid = e.agentIdentifier), + (d.loaderType = r), + (e.runtime = b(d)), + A.has(e.agentIdentifier) || + ((e.ee = R.ee.get(e.agentIdentifier)), + (e.exposed = l), + (0, x.W)({ + agentIdentifier: e.agentIdentifier, + drained: !!y.B?.[e.agentIdentifier], + type: 'lifecycle', + name: 'initialize', + feature: void 0, + data: e.config, + })), + A.add(e.agentIdentifier); + } + }, + 5270: (e, t, r) => { + 'use strict'; + r.d(t, { Aw: () => a, SR: () => o, rF: () => s }); + var n = r(384), + i = r(7767); + function o(e) { + return !!(0, n.dV)().o.MO && (0, i.V)(e) && !0 === e?.session_trace.enabled; + } + function a(e) { + return !0 === e?.session_replay.preload && o(e); + } + function s(e, t) { + try { + if ('string' == typeof t?.type) { + if ('password' === t.type.toLowerCase()) return '*'.repeat(e?.length || 0); + if (void 0 !== t?.dataset?.nrUnmask || t?.classList?.contains('nr-unmask')) return e; + } + } catch (e) {} + return 'string' == typeof e ? e.replace(/[\S]/g, '*') : '*'.repeat(e?.length || 0); + } + }, + 5289: (e, t, r) => { + 'use strict'; + r.d(t, { GG: () => o, Qr: () => s, sB: () => a }); + var n = r(3878); + function i() { + return 'undefined' == typeof document || 'complete' === document.readyState; + } + function o(e, t) { + if (i()) return e(); + (0, n.sp)('load', e, t); + } + function a(e) { + if (i()) return e(); + (0, n.DD)('DOMContentLoaded', e); + } + function s(e) { + if (i()) return e(); + (0, n.sp)('popstate', e); + } + }, + 5607: (e, t, r) => { + 'use strict'; + r.d(t, { W: () => n }); + const n = (0, r(9566).bz)(); + }, + 5701: (e, t, r) => { + 'use strict'; + r.d(t, { B: () => o, t: () => a }); + var n = r(3241); + const i = new Set(), + o = {}; + function a(e, t) { + const r = t.agentIdentifier; + (o[r] ??= {}), + e && + 'object' == typeof e && + (i.has(r) || + (t.ee.emit('rumresp', [e]), + (o[r] = e), + i.add(r), + (0, n.W)({ + agentIdentifier: r, + loaded: !0, + drained: !0, + type: 'lifecycle', + name: 'load', + feature: void 0, + data: e, + }))); + } + }, + 6154: (e, t, r) => { + 'use strict'; + r.d(t, { + A4: () => s, + OF: () => d, + RI: () => i, + WN: () => h, + bv: () => o, + gm: () => a, + lR: () => f, + m: () => u, + mw: () => c, + sb: () => l, + }); + var n = r(1863); + const i = 'undefined' != typeof window && !!window.document, + o = + 'undefined' != typeof WorkerGlobalScope && + (('undefined' != typeof self && + self instanceof WorkerGlobalScope && + self.navigator instanceof WorkerNavigator) || + ('undefined' != typeof globalThis && + globalThis instanceof WorkerGlobalScope && + globalThis.navigator instanceof WorkerNavigator)), + a = i + ? window + : 'undefined' != typeof WorkerGlobalScope && + (('undefined' != typeof self && self instanceof WorkerGlobalScope && self) || + ('undefined' != typeof globalThis && globalThis instanceof WorkerGlobalScope && globalThis)), + s = 'complete' === a?.document?.readyState, + c = Boolean('hidden' === a?.document?.visibilityState), + u = '' + a?.location, + d = /iPad|iPhone|iPod/.test(a.navigator?.userAgent), + l = d && 'undefined' == typeof SharedWorker, + f = (() => { + const e = a.navigator?.userAgent?.match(/Firefox[/\s](\d+\.\d+)/); + return Array.isArray(e) && e.length >= 2 ? +e[1] : 0; + })(), + h = Date.now() - (0, n.t)(); + }, + 6344: (e, t, r) => { + 'use strict'; + r.d(t, { + BB: () => d, + G4: () => o, + Qb: () => l, + TZ: () => i, + Ug: () => a, + _s: () => s, + bc: () => u, + yP: () => c, + }); + var n = r(2614); + const i = r(860).K7.sessionReplay, + o = { RECORD: 'recordReplay', PAUSE: 'pauseReplay', ERROR_DURING_REPLAY: 'errorDuringReplay' }, + a = 0.12, + s = { DomContentLoaded: 0, Load: 1, FullSnapshot: 2, IncrementalSnapshot: 3, Meta: 4, Custom: 5 }, + c = { [n.g.ERROR]: 15e3, [n.g.FULL]: 3e5, [n.g.OFF]: 0 }, + u = { + RESET: { message: 'Session was reset', sm: 'Reset' }, + IMPORT: { message: 'Recorder failed to import', sm: 'Import' }, + TOO_MANY: { message: '429: Too Many Requests', sm: 'Too-Many' }, + TOO_BIG: { message: 'Payload was too large', sm: 'Too-Big' }, + CROSS_TAB: { message: 'Session Entity was set to OFF on another tab', sm: 'Cross-Tab' }, + ENTITLEMENTS: { message: 'Session Replay is not allowed and will not be started', sm: 'Entitlement' }, + }, + d = 5e3, + l = { + API: 'api', + RESUME: 'resume', + SWITCH_TO_FULL: 'switchToFull', + INITIALIZE: 'initialize', + PRELOAD: 'preload', + }; + }, + 6389: (e, t, r) => { + 'use strict'; + function n(e, t = 500, r = {}) { + const n = r?.leading || !1; + let i; + return (...r) => { + n && + void 0 === i && + (e.apply(this, r), + (i = setTimeout(() => { + i = clearTimeout(i); + }, t))), + n || + (clearTimeout(i), + (i = setTimeout(() => { + e.apply(this, r); + }, t))); + }; + } + function i(e) { + let t = !1; + return (...r) => { + t || ((t = !0), e.apply(this, r)); + }; + } + r.d(t, { J: () => i, s: () => n }); + }, + 6630: (e, t, r) => { + 'use strict'; + r.d(t, { T: () => n }); + const n = r(860).K7.pageViewEvent; + }, + 6774: (e, t, r) => { + 'use strict'; + r.d(t, { T: () => n }); + const n = r(860).K7.jserrors; + }, + 7295: (e, t, r) => { + 'use strict'; + r.d(t, { Xv: () => a, gX: () => i, iW: () => o }); + var n = []; + function i(e) { + if (!e || o(e)) return !1; + if (0 === n.length) return !0; + for (var t = 0; t < n.length; t++) { + var r = n[t]; + if ('*' === r.hostname) return !1; + if (s(r.hostname, e.hostname) && c(r.pathname, e.pathname)) return !1; + } + return !0; + } + function o(e) { + return void 0 === e.hostname; + } + function a(e) { + if (((n = []), e && e.length)) + for (var t = 0; t < e.length; t++) { + let r = e[t]; + if (!r) continue; + 0 === r.indexOf('http://') ? (r = r.substring(7)) : 0 === r.indexOf('https://') && (r = r.substring(8)); + const i = r.indexOf('/'); + let o, a; + i > 0 ? ((o = r.substring(0, i)), (a = r.substring(i))) : ((o = r), (a = '')); + let [s] = o.split(':'); + n.push({ hostname: s, pathname: a }); + } + } + function s(e, t) { + return !(e.length > t.length) && t.indexOf(e) === t.length - e.length; + } + function c(e, t) { + return ( + 0 === e.indexOf('/') && (e = e.substring(1)), + 0 === t.indexOf('/') && (t = t.substring(1)), + '' === e || e === t + ); + } + }, + 7378: (e, t, r) => { + 'use strict'; + r.d(t, { + $p: () => x, + BR: () => b, + Kp: () => R, + L3: () => y, + Lc: () => c, + NC: () => o, + SG: () => d, + TZ: () => i, + U6: () => p, + UT: () => m, + d3: () => w, + dT: () => f, + e5: () => E, + gx: () => v, + l9: () => l, + oW: () => h, + op: () => g, + rw: () => u, + tH: () => A, + uP: () => s, + wW: () => T, + xq: () => a, + }); + var n = r(384); + const i = r(860).K7.spa, + o = ['click', 'submit', 'keypress', 'keydown', 'keyup', 'change'], + a = 999, + s = 'fn-start', + c = 'fn-end', + u = 'cb-start', + d = 'api-ixn-', + l = 'remaining', + f = 'interaction', + h = 'spaNode', + p = 'jsonpNode', + g = 'fetch-start', + m = 'fetch-done', + v = 'fetch-body-', + b = 'jsonp-end', + y = (0, n.dV)().o.ST, + w = '-start', + R = '-end', + x = '-body', + T = 'cb' + R, + E = 'jsTime', + A = 'fetch'; + }, + 7485: (e, t, r) => { + 'use strict'; + r.d(t, { D: () => i }); + var n = r(6154); + function i(e) { + if (0 === (e || '').indexOf('data:')) return { protocol: 'data' }; + try { + const t = new URL(e, location.href), + r = { + port: t.port, + hostname: t.hostname, + pathname: t.pathname, + search: t.search, + protocol: t.protocol.slice(0, t.protocol.indexOf(':')), + sameOrigin: t.protocol === n.gm?.location?.protocol && t.host === n.gm?.location?.host, + }; + return ( + (r.port && '' !== r.port) || + ('http:' === t.protocol && (r.port = '80'), 'https:' === t.protocol && (r.port = '443')), + r.pathname && '' !== r.pathname + ? r.pathname.startsWith('/') || (r.pathname = '/'.concat(r.pathname)) + : (r.pathname = '/'), + r + ); + } catch (e) { + return {}; + } + } + }, + 7699: (e, t, r) => { + 'use strict'; + r.d(t, { It: () => i, No: () => n, qh: () => a, uh: () => o }); + const n = 16e3, + i = 1e6, + o = 'NR_CONTAINER_AGENT', + a = 'SESSION_ERROR'; + }, + 7767: (e, t, r) => { + 'use strict'; + r.d(t, { V: () => i }); + var n = r(6154); + const i = (e) => n.RI && !0 === e?.privacy.cookies_enabled; + }, + 7836: (e, t, r) => { + 'use strict'; + r.d(t, { P: () => s, ee: () => c }); + var n = r(384), + i = r(8990), + o = r(2646), + a = r(5607); + const s = 'nr@context:'.concat(a.W), + c = (function e(t, r) { + var n = {}, + a = {}, + d = {}, + l = !1; + try { + l = 16 === r.length && u.initializedAgents?.[r]?.runtime.isolatedBacklog; + } catch (e) {} + var f = { + on: p, + addEventListener: p, + removeEventListener: function (e, t) { + var r = n[e]; + if (!r) return; + for (var i = 0; i < r.length; i++) r[i] === t && r.splice(i, 1); + }, + emit: function (e, r, n, i, o) { + !1 !== o && (o = !0); + if (c.aborted && !i) return; + t && o && t.emit(e, r, n); + var s = h(n); + g(e).forEach((e) => { + e.apply(s, r); + }); + var u = v()[a[e]]; + u && u.push([f, e, r, s]); + return s; + }, + get: m, + listeners: g, + context: h, + buffer: function (e, t) { + const r = v(); + if (((t = t || 'feature'), f.aborted)) return; + Object.entries(e || {}).forEach(([e, n]) => { + (a[n] = t), t in r || (r[t] = []); + }); + }, + abort: function () { + (f._aborted = !0), + Object.keys(f.backlog).forEach((e) => { + delete f.backlog[e]; + }); + }, + isBuffering: function (e) { + return !!v()[a[e]]; + }, + debugId: r, + backlog: l ? {} : t && 'object' == typeof t.backlog ? t.backlog : {}, + isolatedBacklog: l, + }; + return ( + Object.defineProperty(f, 'aborted', { + get: () => { + let e = f._aborted || !1; + return e || (t && (e = t.aborted), e); + }, + }), + f + ); + function h(e) { + return e && e instanceof o.y ? e : e ? (0, i.I)(e, s, () => new o.y(s)) : new o.y(s); + } + function p(e, t) { + n[e] = g(e).concat(t); + } + function g(e) { + return n[e] || []; + } + function m(t) { + return (d[t] = d[t] || e(f, t)); + } + function v() { + return f.backlog; + } + })(void 0, 'globalEE'), + u = (0, n.Zm)(); + u.ee || (u.ee = c); + }, + 8122: (e, t, r) => { + 'use strict'; + r.d(t, { a: () => i }); + var n = r(944); + function i(e, t) { + try { + if (!e || 'object' != typeof e) return (0, n.R)(3); + if (!t || 'object' != typeof t) return (0, n.R)(4); + const r = Object.create(Object.getPrototypeOf(t), Object.getOwnPropertyDescriptors(t)), + o = 0 === Object.keys(r).length ? e : r; + for (let a in o) + if (void 0 !== e[a]) + try { + if (null === e[a]) { + r[a] = null; + continue; + } + Array.isArray(e[a]) && Array.isArray(t[a]) + ? (r[a] = Array.from(new Set([...e[a], ...t[a]]))) + : 'object' == typeof e[a] && 'object' == typeof t[a] + ? (r[a] = i(e[a], t[a])) + : (r[a] = e[a]); + } catch (e) { + r[a] || (0, n.R)(1, e); + } + return r; + } catch (e) { + (0, n.R)(2, e); + } + } + }, + 8139: (e, t, r) => { + 'use strict'; + r.d(t, { u: () => f }); + var n = r(7836), + i = r(3434), + o = r(8990), + a = r(6154); + const s = {}, + c = a.gm.XMLHttpRequest, + u = 'addEventListener', + d = 'removeEventListener', + l = 'nr@wrapped:'.concat(n.P); + function f(e) { + var t = (function (e) { + return (e || n.ee).get('events'); + })(e); + if (s[t.debugId]++) return t; + s[t.debugId] = 1; + var r = (0, i.YM)(t, !0); + function f(e) { + r.inPlace(e, [u, d], '-', p); + } + function p(e, t) { + return e[1]; + } + return ( + 'getPrototypeOf' in Object && (a.RI && h(document, f), c && h(c.prototype, f), h(a.gm, f)), + t.on(u + '-start', function (e, t) { + var n = e[1]; + if (null !== n && ('function' == typeof n || 'object' == typeof n) && 'newrelic' !== e[0]) { + var i = (0, o.I)(n, l, function () { + var e = { + object: function () { + if ('function' != typeof n.handleEvent) return; + return n.handleEvent.apply(n, arguments); + }, + function: n, + }[typeof n]; + return e ? r(e, 'fn-', null, e.name || 'anonymous') : n; + }); + this.wrapped = e[1] = i; + } + }), + t.on(d + '-start', function (e) { + e[1] = this.wrapped || e[1]; + }), + t + ); + } + function h(e, t, ...r) { + let n = e; + for (; 'object' == typeof n && !Object.prototype.hasOwnProperty.call(n, u); ) n = Object.getPrototypeOf(n); + n && t(n, ...r); + } + }, + 8154: (e, t, r) => { + 'use strict'; + r.d(t, { z_: () => o, XG: () => s, TZ: () => n, rs: () => i, xV: () => a }); + r(6154), r(9566), r(384); + const n = r(860).K7.metrics, + i = 'sm', + o = 'cm', + a = 'storeSupportabilityMetrics', + s = 'storeEventMetrics'; + }, + 8374: (e, t, r) => { + r.nc = (() => { + try { + return document?.currentScript?.nonce; + } catch (e) {} + return ''; + })(); + }, + 8990: (e, t, r) => { + 'use strict'; + r.d(t, { I: () => i }); + var n = Object.prototype.hasOwnProperty; + function i(e, t, r) { + if (n.call(e, t)) return e[t]; + var i = r(); + if (Object.defineProperty && Object.keys) + try { + return Object.defineProperty(e, t, { value: i, writable: !0, enumerable: !1 }), i; + } catch (e) {} + return (e[t] = i), i; + } + }, + 9300: (e, t, r) => { + 'use strict'; + r.d(t, { T: () => n }); + const n = r(860).K7.ajax; + }, + 9324: (e, t, r) => { + 'use strict'; + r.d(t, { AJ: () => a, F3: () => i, Xs: () => o, Yq: () => s, xv: () => n }); + const n = '1.301.0', + i = 'PROD', + o = 'CDN', + a = '@newrelic/rrweb', + s = '1.0.1'; + }, + 9566: (e, t, r) => { + 'use strict'; + r.d(t, { LA: () => s, ZF: () => c, bz: () => a, el: () => u }); + var n = r(6154); + const i = 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'; + function o(e, t) { + return e ? 15 & e[t] : (16 * Math.random()) | 0; + } + function a() { + const e = n.gm?.crypto || n.gm?.msCrypto; + let t, + r = 0; + return ( + e && e.getRandomValues && (t = e.getRandomValues(new Uint8Array(30))), + i + .split('') + .map((e) => ('x' === e ? o(t, r++).toString(16) : 'y' === e ? ((3 & o()) | 8).toString(16) : e)) + .join('') + ); + } + function s(e) { + const t = n.gm?.crypto || n.gm?.msCrypto; + let r, + i = 0; + t && t.getRandomValues && (r = t.getRandomValues(new Uint8Array(e))); + const a = []; + for (var s = 0; s < e; s++) a.push(o(r, i++).toString(16)); + return a.join(''); + } + function c() { + return s(16); + } + function u() { + return s(32); + } + }, + 9908: (e, t, r) => { + 'use strict'; + r.d(t, { d: () => n, p: () => i }); + var n = r(7836).ee.get('handle'); + function i(e, t, r, i, o) { + o ? (o.buffer([e], i), o.emit(e, t, r)) : (n.buffer([e], i), n.emit(e, t, r)); + } + }, + }, + n = {}; + function i(e) { + var t = n[e]; + if (void 0 !== t) return t.exports; + var o = (n[e] = { exports: {} }); + return r[e](o, o.exports, i), o.exports; + } + (i.m = r), + (i.d = (e, t) => { + for (var r in t) i.o(t, r) && !i.o(e, r) && Object.defineProperty(e, r, { enumerable: !0, get: t[r] }); + }), + (i.f = {}), + (i.e = (e) => Promise.all(Object.keys(i.f).reduce((t, r) => (i.f[r](e, t), t), []))), + (i.u = (e) => ({ 212: 'nr-spa-compressor', 249: 'nr-spa-recorder', 478: 'nr-spa' })[e] + '-1.301.0.min.js'), + (i.o = (e, t) => Object.prototype.hasOwnProperty.call(e, t)), + (e = {}), + (t = 'NRBA-1.301.0.PROD:'), + (i.l = (r, n, o, a) => { + if (e[r]) e[r].push(n); + else { + var s, c; + if (void 0 !== o) + for (var u = document.getElementsByTagName('script'), d = 0; d < u.length; d++) { + var l = u[d]; + if (l.getAttribute('src') == r || l.getAttribute('data-webpack') == t + o) { + s = l; + break; + } + } + if (!s) { + c = !0; + var f = { + 478: 'sha512-7qHClSVBtoyiwRvCkgyaF5Ps0RAENoPwjauK0I+0bYyBYefdZVshGSe8JQWh/Wexd7hFuUui5xp+2xn5U1a3ZA==', + 249: 'sha512-KuEP0gQ0mAldYT/AqNp3NW4kVb0kLtfIlXaPSFq4WQRFf8vKVNnSqiafso/bWR75halLwFsgmJtdLJEzntVZoQ==', + 212: 'sha512-fqWEILwVJyfYV9/SedvSjCZ6hDRNjOvwYfN73wxZtahaztcFZ2cr3Ns172tBGIDQeWO25QmSlihZm+awv8ma/w==', + }; + ((s = document.createElement('script')).charset = 'utf-8'), + i.nc && s.setAttribute('nonce', i.nc), + s.setAttribute('data-webpack', t + o), + (s.src = r), + 0 !== s.src.indexOf(window.location.origin + '/') && (s.crossOrigin = 'anonymous'), + f[a] && (s.integrity = f[a]); + } + e[r] = [n]; + var h = (t, n) => { + (s.onerror = s.onload = null), clearTimeout(p); + var i = e[r]; + if ((delete e[r], s.parentNode && s.parentNode.removeChild(s), i && i.forEach((e) => e(n)), t)) return t(n); + }, + p = setTimeout(h.bind(null, void 0, { type: 'timeout', target: s }), 12e4); + (s.onerror = h.bind(null, s.onerror)), (s.onload = h.bind(null, s.onload)), c && document.head.appendChild(s); + } + }), + (i.r = (e) => { + 'undefined' != typeof Symbol && + Symbol.toStringTag && + Object.defineProperty(e, Symbol.toStringTag, { value: 'Module' }), + Object.defineProperty(e, '__esModule', { value: !0 }); + }), + (i.p = 'https://js-agent.newrelic.com/'), + (() => { + var e = { 38: 0, 788: 0 }; + i.f.j = (t, r) => { + var n = i.o(e, t) ? e[t] : void 0; + if (0 !== n) + if (n) r.push(n[2]); + else { + var o = new Promise((r, i) => (n = e[t] = [r, i])); + r.push((n[2] = o)); + var a = i.p + i.u(t), + s = new Error(); + i.l( + a, + (r) => { + if (i.o(e, t) && (0 !== (n = e[t]) && (e[t] = void 0), n)) { + var o = r && ('load' === r.type ? 'missing' : r.type), + a = r && r.target && r.target.src; + (s.message = 'Loading chunk ' + t + ' failed.\n(' + o + ': ' + a + ')'), + (s.name = 'ChunkLoadError'), + (s.type = o), + (s.request = a), + n[1](s); + } + }, + 'chunk-' + t, + t + ); + } + }; + var t = (t, r) => { + var n, + o, + [a, s, c] = r, + u = 0; + if (a.some((t) => 0 !== e[t])) { + for (n in s) i.o(s, n) && (i.m[n] = s[n]); + if (c) c(i); + } + for (t && t(r); u < a.length; u++) (o = a[u]), i.o(e, o) && e[o] && e[o][0](), (e[o] = 0); + }, + r = (self['webpackChunk:NRBA-1.301.0.PROD'] = self['webpackChunk:NRBA-1.301.0.PROD'] || []); + r.forEach(t.bind(null, 0)), (r.push = t.bind(null, r.push.bind(r))); + })(), + (() => { + 'use strict'; + i(8374); + var e = i(9566), + t = i(1741); + class r extends t.W { + agentIdentifier = (0, e.LA)(16); + } + var n = i(860); + const o = Object.values(n.K7); + var a = i(5205); + var s = i(9908), + c = i(1863), + u = i(4261), + d = i(3241), + l = i(944), + f = i(5701), + h = i(8154); + function p(e, t, i, o) { + const a = o || i; + !a || + (a[e] && a[e] !== r.prototype[e]) || + (a[e] = function () { + (0, s.p)(h.xV, ['API/' + e + '/called'], void 0, n.K7.metrics, i.ee), + (0, d.W)({ + agentIdentifier: i.agentIdentifier, + drained: !!f.B?.[i.agentIdentifier], + type: 'data', + name: 'api', + feature: u.Pl + e, + data: {}, + }); + try { + return t.apply(this, arguments); + } catch (e) { + (0, l.R)(23, e); + } + }); + } + function g(e, t, r, n, i) { + const o = e.info; + null === r ? delete o.jsAttributes[t] : (o.jsAttributes[t] = r), + (i || null === r) && (0, s.p)(u.Pl + n, [(0, c.t)(), t, r], void 0, 'session', e.ee); + } + var m = i(1687), + v = i(4234), + b = i(5289), + y = i(6154), + w = i(5270), + R = i(7767), + x = i(6389), + T = i(7699); + class E extends v.W { + constructor(e, t) { + super(e.agentIdentifier, t), + (this.agentRef = e), + (this.abortHandler = void 0), + (this.featAggregate = void 0), + (this.onAggregateImported = void 0), + (this.deferred = Promise.resolve()), + !1 === e.init[this.featureName].autoStart + ? (this.deferred = new Promise((t, r) => { + this.ee.on( + 'manual-start-all', + (0, x.J)(() => { + (0, m.Ak)(e.agentIdentifier, this.featureName), t(); + }) + ); + })) + : (0, m.Ak)(e.agentIdentifier, t); + } + importAggregator(e, t, r = {}) { + if (this.featAggregate) return; + let n; + this.onAggregateImported = new Promise((e) => { + n = e; + }); + const o = async () => { + let o; + await this.deferred; + try { + if ((0, R.V)(e.init)) { + const { setupAgentSession: t } = await i.e(478).then(i.bind(i, 8766)); + o = t(e); + } + } catch (e) { + (0, l.R)(20, e), + this.ee.emit('internal-error', [e]), + (0, s.p)(T.qh, [e], void 0, this.featureName, this.ee); + } + try { + if (!this.#t(this.featureName, o, e.init)) + return (0, m.Ze)(this.agentIdentifier, this.featureName), void n(!1); + const { Aggregate: i } = await t(); + (this.featAggregate = new i(e, r)), + e.runtime.harvester.initializedAggregates.push(this.featAggregate), + n(!0); + } catch (e) { + (0, l.R)(34, e), + this.abortHandler?.(), + (0, m.Ze)(this.agentIdentifier, this.featureName, !0), + n(!1), + this.ee && this.ee.abort(); + } + }; + y.RI ? (0, b.GG)(() => o(), !0) : o(); + } + #t(e, t, r) { + if (this.blocked) return !1; + switch (e) { + case n.K7.sessionReplay: + return (0, w.SR)(r) && !!t; + case n.K7.sessionTrace: + return !!t; + default: + return !0; + } + } + } + var A = i(6630), + O = i(2614); + class S extends E { + static featureName = A.T; + constructor(e) { + var t; + super(e, A.T), + this.setupInspectionEvents(e.agentIdentifier), + (t = e), + p( + u.Fw, + function (e, r) { + 'string' == typeof e && + ('/' !== e.charAt(0) && (e = '/' + e), + (t.runtime.customTransaction = (r || 'http://custom.transaction') + e), + (0, s.p)(u.Pl + u.Fw, [(0, c.t)()], void 0, void 0, t.ee)); + }, + t + ), + this.ee.on('api-send-rum', (e, t) => (0, s.p)('send-rum', [e, t], void 0, this.featureName, this.ee)), + this.importAggregator(e, () => i.e(478).then(i.bind(i, 1983))); + } + setupInspectionEvents(e) { + const t = (t, r) => { + t && + (0, d.W)({ + agentIdentifier: e, + timeStamp: t.timeStamp, + loaded: 'complete' === t.target.readyState, + type: 'window', + name: r, + data: t.target.location + '', + }); + }; + (0, b.sB)((e) => { + t(e, 'DOMContentLoaded'); + }), + (0, b.GG)((e) => { + t(e, 'load'); + }), + (0, b.Qr)((e) => { + t(e, 'navigate'); + }), + this.ee.on(O.tS.UPDATE, (t, r) => { + (0, d.W)({ agentIdentifier: e, type: 'lifecycle', name: 'session', data: r }); + }); + } + } + var N = i(384); + var _ = i(2843), + I = i(3878), + P = i(782); + class j extends E { + static featureName = P.T; + constructor(e) { + super(e, P.T), + y.RI && + ((0, _.u)(() => (0, s.p)('docHidden', [(0, c.t)()], void 0, P.T, this.ee), !0), + (0, I.sp)('pagehide', () => (0, s.p)('winPagehide', [(0, c.t)()], void 0, P.T, this.ee)), + this.importAggregator(e, () => i.e(478).then(i.bind(i, 9917)))); + } + } + class C extends E { + static featureName = h.TZ; + constructor(e) { + super(e, h.TZ), + y.RI && + document.addEventListener('securitypolicyviolation', (e) => { + (0, s.p)(h.xV, ['Generic/CSPViolation/Detected'], void 0, this.featureName, this.ee); + }), + this.importAggregator(e, () => i.e(478).then(i.bind(i, 6555))); + } + } + var k = i(6774), + L = i(3304); + class H { + constructor(e, t, r, n, i) { + (this.name = 'UncaughtError'), + (this.message = 'string' == typeof e ? e : (0, L.A)(e)), + (this.sourceURL = t), + (this.line = r), + (this.column = n), + (this.__newrelic = i); + } + } + function M(e) { + return U(e) + ? e + : new H( + void 0 !== e?.message ? e.message : e, + e?.filename || e?.sourceURL, + e?.lineno || e?.line, + e?.colno || e?.col, + e?.__newrelic, + e?.cause + ); + } + function D(e) { + const t = 'Unhandled Promise Rejection: '; + if (!e?.reason) return; + if (U(e.reason)) { + try { + e.reason.message.startsWith(t) || (e.reason.message = t + e.reason.message); + } catch (e) {} + return M(e.reason); + } + const r = M(e.reason); + return (r.message || '').startsWith(t) || (r.message = t + r.message), r; + } + function K(e) { + if (e.error instanceof SyntaxError && !/:\d+$/.test(e.error.stack?.trim())) { + const t = new H(e.message, e.filename, e.lineno, e.colno, e.error.__newrelic, e.cause); + return (t.name = SyntaxError.name), t; + } + return U(e.error) ? e.error : M(e); + } + function U(e) { + return e instanceof Error && !!e.stack; + } + function F(e, t, r, i, o = (0, c.t)()) { + 'string' == typeof e && (e = new Error(e)), + (0, s.p)('err', [e, o, !1, t, r.runtime.isRecording, void 0, i], void 0, n.K7.jserrors, r.ee), + (0, s.p)('uaErr', [], void 0, n.K7.genericEvents, r.ee); + } + var W = i(3496), + B = i(993), + G = i(3785); + function V(e, { customAttributes: t = {}, level: r = B.p_.INFO } = {}, n, i, o = (0, c.t)()) { + (0, G.R)(n.ee, e, t, r, i, o); + } + function z(e, t, r, i, o = (0, c.t)()) { + (0, s.p)(u.Pl + u.hG, [o, e, t, i], void 0, n.K7.genericEvents, r.ee); + } + function Z(e) { + p( + u.eY, + function (t) { + return (function (e, t) { + const r = {}; + let i, o; + (0, l.R)(54, 'newrelic.register'), e.init.api.allow_registered_children || (i = () => (0, l.R)(55)); + (t && (0, W.I)(t)) || (i = () => (0, l.R)(48, t)); + const a = { + addPageAction: (n, i = {}) => { + u(z, [n, { ...r, ...i }, e], t); + }, + log: (n, i = {}) => { + u(V, [n, { ...i, customAttributes: { ...r, ...(i.customAttributes || {}) } }, e], t); + }, + noticeError: (n, i = {}) => { + u(F, [n, { ...r, ...i }, e], t); + }, + setApplicationVersion: (e) => { + r['application.version'] = e; + }, + setCustomAttribute: (e, t) => { + r[e] = t; + }, + setUserId: (e) => { + r['enduser.id'] = e; + }, + metadata: { + customAttributes: r, + target: t, + get connected() { + return o || Promise.reject(new Error('Failed to connect')); + }, + }, + }; + i + ? i() + : (o = new Promise((n, i) => { + try { + const o = e.runtime?.entityManager; + let s = !!o?.get().entityGuid, + c = o?.getEntityGuidFor(t.licenseKey, t.applicationID), + u = !!c; + if (s && u) (t.entityGuid = c), n(a); + else { + const d = setTimeout(() => i(new Error('Failed to connect - Timeout')), 15e3); + function l(r) { + (0, W.A)(r, e) + ? (s ||= !0) + : t.licenseKey === r.licenseKey && + t.applicationID === r.applicationID && + ((u = !0), (t.entityGuid = r.entityGuid)), + s && u && (clearTimeout(d), e.ee.removeEventListener('entity-added', l), n(a)); + } + e.ee.emit('api-send-rum', [r, t]), e.ee.on('entity-added', l); + } + } catch (f) { + i(f); + } + })); + const u = async (t, r, a) => { + if (i) return i(); + const u = (0, c.t)(); + (0, s.p)(h.xV, ['API/register/'.concat(t.name, '/called')], void 0, n.K7.metrics, e.ee); + try { + await o; + const n = e.init.api.duplicate_registered_data; + (!0 === n || (Array.isArray(n) && n.includes(a.entityGuid))) && t(...r, void 0, u), + t(...r, a.entityGuid, u); + } catch (e) { + (0, l.R)(50, e); + } + }; + return a; + })(e, t); + }, + e + ); + } + class q extends E { + static featureName = k.T; + constructor(e) { + var t; + super(e, k.T), + (t = e), + p(u.o5, (e, r) => F(e, r, t), t), + (function (e) { + p( + u.bt, + function (t) { + e.runtime.onerror = t; + }, + e + ); + })(e), + (function (e) { + let t = 0; + p( + u.k6, + function (e, r) { + ++t > 10 || (this.runtime.releaseIds[e.slice(-200)] = ('' + r).slice(-200)); + }, + e + ); + })(e), + Z(e); + try { + this.removeOnAbort = new AbortController(); + } catch (e) {} + this.ee.on('internal-error', (t, r) => { + this.abortHandler && + (0, s.p)('ierr', [M(t), (0, c.t)(), !0, {}, e.runtime.isRecording, r], void 0, this.featureName, this.ee); + }), + y.gm.addEventListener( + 'unhandledrejection', + (t) => { + this.abortHandler && + (0, s.p)( + 'err', + [D(t), (0, c.t)(), !1, { unhandledPromiseRejection: 1 }, e.runtime.isRecording], + void 0, + this.featureName, + this.ee + ); + }, + (0, I.jT)(!1, this.removeOnAbort?.signal) + ), + y.gm.addEventListener( + 'error', + (t) => { + this.abortHandler && + (0, s.p)('err', [K(t), (0, c.t)(), !1, {}, e.runtime.isRecording], void 0, this.featureName, this.ee); + }, + (0, I.jT)(!1, this.removeOnAbort?.signal) + ), + (this.abortHandler = this.#r), + this.importAggregator(e, () => i.e(478).then(i.bind(i, 2176))); + } + #r() { + this.removeOnAbort?.abort(), (this.abortHandler = void 0); + } + } + var X = i(8990); + let Y = 1; + function Q(e) { + const t = typeof e; + return !e || ('object' !== t && 'function' !== t) + ? -1 + : e === y.gm + ? 0 + : (0, X.I)(e, 'nr@id', function () { + return Y++; + }); + } + function J(e) { + if ('string' == typeof e && e.length) return e.length; + if ('object' == typeof e) { + if ('undefined' != typeof ArrayBuffer && e instanceof ArrayBuffer && e.byteLength) return e.byteLength; + if ('undefined' != typeof Blob && e instanceof Blob && e.size) return e.size; + if (!('undefined' != typeof FormData && e instanceof FormData)) + try { + return (0, L.A)(e).length; + } catch (e) { + return; + } + } + } + var ee = i(8139), + te = i(7836), + re = i(3434); + const ne = {}, + ie = ['open', 'send']; + function oe(e) { + var t = e || te.ee; + const r = (function (e) { + return (e || te.ee).get('xhr'); + })(t); + if (void 0 === y.gm.XMLHttpRequest) return r; + if (ne[r.debugId]++) return r; + (ne[r.debugId] = 1), (0, ee.u)(t); + var n = (0, re.YM)(r), + i = y.gm.XMLHttpRequest, + o = y.gm.MutationObserver, + a = y.gm.Promise, + s = y.gm.setInterval, + c = 'readystatechange', + u = ['onload', 'onerror', 'onabort', 'onloadstart', 'onloadend', 'onprogress', 'ontimeout'], + d = [], + f = (y.gm.XMLHttpRequest = function (e) { + const t = new i(e), + o = r.context(t); + try { + r.emit('new-xhr', [t], o), + t.addEventListener( + c, + ((a = o), + function () { + var e = this; + e.readyState > 3 && !a.resolved && ((a.resolved = !0), r.emit('xhr-resolved', [], e)), + n.inPlace(e, u, 'fn-', b); + }), + (0, I.jT)(!1) + ); + } catch (e) { + (0, l.R)(15, e); + try { + r.emit('internal-error', [e]); + } catch (e) {} + } + var a; + return t; + }); + function h(e, t) { + n.inPlace(t, ['onreadystatechange'], 'fn-', b); + } + if ( + ((function (e, t) { + for (var r in e) t[r] = e[r]; + })(i, f), + (f.prototype = i.prototype), + n.inPlace(f.prototype, ie, '-xhr-', b), + r.on('send-xhr-start', function (e, t) { + h(e, t), + (function (e) { + d.push(e), o && (p ? p.then(v) : s ? s(v) : ((g = -g), (m.data = g))); + })(t); + }), + r.on('open-xhr-start', h), + o) + ) { + var p = a && a.resolve(); + if (!s && !a) { + var g = 1, + m = document.createTextNode(g); + new o(v).observe(m, { characterData: !0 }); + } + } else + t.on('fn-end', function (e) { + (e[0] && e[0].type === c) || v(); + }); + function v() { + for (var e = 0; e < d.length; e++) h(0, d[e]); + d.length && (d = []); + } + function b(e, t) { + return t; + } + return r; + } + var ae = 'fetch-', + se = ae + 'body-', + ce = ['arrayBuffer', 'blob', 'json', 'text', 'formData'], + ue = y.gm.Request, + de = y.gm.Response, + le = 'prototype'; + const fe = {}; + function he(e) { + const t = (function (e) { + return (e || te.ee).get('fetch'); + })(e); + if (!(ue && de && y.gm.fetch)) return t; + if (fe[t.debugId]++) return t; + function r(e, r, n) { + var i = e[r]; + 'function' == typeof i && + (e[r] = function () { + var e, + r = [...arguments], + o = {}; + t.emit(n + 'before-start', [r], o), o[te.P] && o[te.P].dt && (e = o[te.P].dt); + var a = i.apply(this, r); + return ( + t.emit(n + 'start', [r, e], a), + a.then( + function (e) { + return t.emit(n + 'end', [null, e], a), e; + }, + function (e) { + throw (t.emit(n + 'end', [e], a), e); + } + ) + ); + }); + } + return ( + (fe[t.debugId] = 1), + ce.forEach((e) => { + r(ue[le], e, se), r(de[le], e, se); + }), + r(y.gm, 'fetch', ae), + t.on(ae + 'end', function (e, r) { + var n = this; + if (r) { + var i = r.headers.get('content-length'); + null !== i && (n.rxSize = i), t.emit(ae + 'done', [null, r], n); + } else t.emit(ae + 'done', [e], n); + }), + t + ); + } + var pe = i(7485); + class ge { + constructor(e) { + this.agentRef = e; + } + generateTracePayload(t) { + const r = this.agentRef.loader_config; + if (!this.shouldGenerateTrace(t) || !r) return null; + var n = (r.accountID || '').toString() || null, + i = (r.agentID || '').toString() || null, + o = (r.trustKey || '').toString() || null; + if (!n || !i) return null; + var a = (0, e.ZF)(), + s = (0, e.el)(), + c = Date.now(), + u = { spanId: a, traceId: s, timestamp: c }; + return ( + (t.sameOrigin || (this.isAllowedOrigin(t) && this.useTraceContextHeadersForCors())) && + ((u.traceContextParentHeader = this.generateTraceContextParentHeader(a, s)), + (u.traceContextStateHeader = this.generateTraceContextStateHeader(a, c, n, i, o))), + ((t.sameOrigin && !this.excludeNewrelicHeader()) || + (!t.sameOrigin && this.isAllowedOrigin(t) && this.useNewrelicHeaderForCors())) && + (u.newrelicHeader = this.generateTraceHeader(a, s, c, n, i, o)), + u + ); + } + generateTraceContextParentHeader(e, t) { + return '00-' + t + '-' + e + '-01'; + } + generateTraceContextStateHeader(e, t, r, n, i) { + return i + '@nr=0-1-' + r + '-' + n + '-' + e + '----' + t; + } + generateTraceHeader(e, t, r, n, i, o) { + if (!('function' == typeof y.gm?.btoa)) return null; + var a = { v: [0, 1], d: { ty: 'Browser', ac: n, ap: i, id: e, tr: t, ti: r } }; + return o && n !== o && (a.d.tk = o), btoa((0, L.A)(a)); + } + shouldGenerateTrace(e) { + return this.agentRef.init?.distributed_tracing?.enabled && this.isAllowedOrigin(e); + } + isAllowedOrigin(e) { + var t = !1; + const r = this.agentRef.init?.distributed_tracing; + if (e.sameOrigin) t = !0; + else if (r?.allowed_origins instanceof Array) + for (var n = 0; n < r.allowed_origins.length; n++) { + var i = (0, pe.D)(r.allowed_origins[n]); + if (e.hostname === i.hostname && e.protocol === i.protocol && e.port === i.port) { + t = !0; + break; + } + } + return t; + } + excludeNewrelicHeader() { + var e = this.agentRef.init?.distributed_tracing; + return !!e && !!e.exclude_newrelic_header; + } + useNewrelicHeaderForCors() { + var e = this.agentRef.init?.distributed_tracing; + return !!e && !1 !== e.cors_use_newrelic_header; + } + useTraceContextHeadersForCors() { + var e = this.agentRef.init?.distributed_tracing; + return !!e && !!e.cors_use_tracecontext_headers; + } + } + var me = i(9300), + ve = i(7295); + function be(e) { + return 'string' == typeof e + ? e + : e instanceof (0, N.dV)().o.REQ + ? e.url + : y.gm?.URL && e instanceof URL + ? e.href + : void 0; + } + var ye = ['load', 'error', 'abort', 'timeout'], + we = ye.length, + Re = (0, N.dV)().o.REQ, + xe = (0, N.dV)().o.XHR; + const Te = 'X-NewRelic-App-Data'; + class Ee extends E { + static featureName = me.T; + constructor(e) { + super(e, me.T), (this.dt = new ge(e)), (this.handler = (e, t, r, n) => (0, s.p)(e, t, r, n, this.ee)); + try { + const e = { xmlhttprequest: 'xhr', fetch: 'fetch', beacon: 'beacon' }; + y.gm?.performance?.getEntriesByType('resource').forEach((t) => { + if (t.initiatorType in e && 0 !== t.responseStatus) { + const r = { status: t.responseStatus }, + i = { rxSize: t.transferSize, duration: Math.floor(t.duration), cbTime: 0 }; + Ae(r, t.name), + this.handler('xhr', [r, i, t.startTime, t.responseEnd, e[t.initiatorType]], void 0, n.K7.ajax); + } + }); + } catch (e) {} + he(this.ee), + oe(this.ee), + (function (e, t, r, i) { + function o(e) { + var t = this; + (t.totalCbs = 0), + (t.called = 0), + (t.cbTime = 0), + (t.end = E), + (t.ended = !1), + (t.xhrGuids = {}), + (t.lastSize = null), + (t.loadCaptureCalled = !1), + (t.params = this.params || {}), + (t.metrics = this.metrics || {}), + (t.latestLongtaskEnd = 0), + e.addEventListener( + 'load', + function (r) { + A(t, e); + }, + (0, I.jT)(!1) + ), + y.lR || + e.addEventListener( + 'progress', + function (e) { + t.lastSize = e.loaded; + }, + (0, I.jT)(!1) + ); + } + function a(e) { + (this.params = { method: e[0] }), Ae(this, e[1]), (this.metrics = {}); + } + function u(t, r) { + e.loader_config.xpid && this.sameOrigin && r.setRequestHeader('X-NewRelic-ID', e.loader_config.xpid); + var n = i.generateTracePayload(this.parsedOrigin); + if (n) { + var o = !1; + n.newrelicHeader && (r.setRequestHeader('newrelic', n.newrelicHeader), (o = !0)), + n.traceContextParentHeader && + (r.setRequestHeader('traceparent', n.traceContextParentHeader), + n.traceContextStateHeader && r.setRequestHeader('tracestate', n.traceContextStateHeader), + (o = !0)), + o && (this.dt = n); + } + } + function d(e, r) { + var n = this.metrics, + i = e[0], + o = this; + if (n && i) { + var a = J(i); + a && (n.txSize = a); + } + (this.startTime = (0, c.t)()), + (this.body = i), + (this.listener = function (e) { + try { + 'abort' !== e.type || o.loadCaptureCalled || (o.params.aborted = !0), + ('load' !== e.type || + (o.called === o.totalCbs && + (o.onloadCalled || 'function' != typeof r.onload) && + 'function' == typeof o.end)) && + o.end(r); + } catch (e) { + try { + t.emit('internal-error', [e]); + } catch (e) {} + } + }); + for (var s = 0; s < we; s++) r.addEventListener(ye[s], this.listener, (0, I.jT)(!1)); + } + function l(e, t, r) { + (this.cbTime += e), + t ? (this.onloadCalled = !0) : (this.called += 1), + this.called !== this.totalCbs || + (!this.onloadCalled && 'function' == typeof r.onload) || + 'function' != typeof this.end || + this.end(r); + } + function f(e, t) { + var r = '' + Q(e) + !!t; + this.xhrGuids && !this.xhrGuids[r] && ((this.xhrGuids[r] = !0), (this.totalCbs += 1)); + } + function p(e, t) { + var r = '' + Q(e) + !!t; + this.xhrGuids && this.xhrGuids[r] && (delete this.xhrGuids[r], (this.totalCbs -= 1)); + } + function g() { + this.endTime = (0, c.t)(); + } + function m(e, r) { + r instanceof xe && 'load' === e[0] && t.emit('xhr-load-added', [e[1], e[2]], r); + } + function v(e, r) { + r instanceof xe && 'load' === e[0] && t.emit('xhr-load-removed', [e[1], e[2]], r); + } + function b(e, t, r) { + t instanceof xe && + ('onload' === r && (this.onload = !0), + ('load' === (e[0] && e[0].type) || this.onload) && (this.xhrCbStart = (0, c.t)())); + } + function w(e, r) { + this.xhrCbStart && t.emit('xhr-cb-time', [(0, c.t)() - this.xhrCbStart, this.onload, r], r); + } + function R(e) { + var t, + r = e[1] || {}; + if ( + ('string' == typeof e[0] + ? 0 === (t = e[0]).length && y.RI && (t = '' + y.gm.location.href) + : e[0] && e[0].url + ? (t = e[0].url) + : y.gm?.URL && e[0] && e[0] instanceof URL + ? (t = e[0].href) + : 'function' == typeof e[0].toString && (t = e[0].toString()), + 'string' == typeof t && 0 !== t.length) + ) { + t && ((this.parsedOrigin = (0, pe.D)(t)), (this.sameOrigin = this.parsedOrigin.sameOrigin)); + var n = i.generateTracePayload(this.parsedOrigin); + if (n && (n.newrelicHeader || n.traceContextParentHeader)) + if (e[0] && e[0].headers) s(e[0].headers, n) && (this.dt = n); + else { + var o = {}; + for (var a in r) o[a] = r[a]; + (o.headers = new Headers(r.headers || {})), + s(o.headers, n) && (this.dt = n), + e.length > 1 ? (e[1] = o) : e.push(o); + } + } + function s(e, t) { + var r = !1; + return ( + t.newrelicHeader && (e.set('newrelic', t.newrelicHeader), (r = !0)), + t.traceContextParentHeader && + (e.set('traceparent', t.traceContextParentHeader), + t.traceContextStateHeader && e.set('tracestate', t.traceContextStateHeader), + (r = !0)), + r + ); + } + } + function x(e, t) { + (this.params = {}), + (this.metrics = {}), + (this.startTime = (0, c.t)()), + (this.dt = t), + e.length >= 1 && (this.target = e[0]), + e.length >= 2 && (this.opts = e[1]); + var r = this.opts || {}, + n = this.target; + Ae(this, be(n)); + var i = ('' + ((n && n instanceof Re && n.method) || r.method || 'GET')).toUpperCase(); + (this.params.method = i), (this.body = r.body), (this.txSize = J(r.body) || 0); + } + function T(e, t) { + if (((this.endTime = (0, c.t)()), this.params || (this.params = {}), (0, ve.iW)(this.params))) return; + let i; + (this.params.status = t ? t.status : 0), + 'string' == typeof this.rxSize && this.rxSize.length > 0 && (i = +this.rxSize); + const o = { txSize: this.txSize, rxSize: i, duration: (0, c.t)() - this.startTime }; + r('xhr', [this.params, o, this.startTime, this.endTime, 'fetch'], this, n.K7.ajax); + } + function E(e) { + const t = this.params, + i = this.metrics; + if (!this.ended) { + this.ended = !0; + for (let t = 0; t < we; t++) e.removeEventListener(ye[t], this.listener, !1); + t.aborted || + (0, ve.iW)(t) || + ((i.duration = (0, c.t)() - this.startTime), + this.loadCaptureCalled || 4 !== e.readyState ? null == t.status && (t.status = 0) : A(this, e), + (i.cbTime = this.cbTime), + r('xhr', [t, i, this.startTime, this.endTime, 'xhr'], this, n.K7.ajax)); + } + } + function A(e, r) { + e.params.status = r.status; + var i = (function (e, t) { + var r = e.responseType; + return 'json' === r && null !== t + ? t + : 'arraybuffer' === r || 'blob' === r || 'json' === r + ? J(e.response) + : 'text' === r || '' === r || void 0 === r + ? J(e.responseText) + : void 0; + })(r, e.lastSize); + if ((i && (e.metrics.rxSize = i), e.sameOrigin && r.getAllResponseHeaders().indexOf(Te) >= 0)) { + var o = r.getResponseHeader(Te); + o && + ((0, s.p)(h.rs, ['Ajax/CrossApplicationTracing/Header/Seen'], void 0, n.K7.metrics, t), + (e.params.cat = o.split(', ').pop())); + } + e.loadCaptureCalled = !0; + } + t.on('new-xhr', o), + t.on('open-xhr-start', a), + t.on('open-xhr-end', u), + t.on('send-xhr-start', d), + t.on('xhr-cb-time', l), + t.on('xhr-load-added', f), + t.on('xhr-load-removed', p), + t.on('xhr-resolved', g), + t.on('addEventListener-end', m), + t.on('removeEventListener-end', v), + t.on('fn-end', w), + t.on('fetch-before-start', R), + t.on('fetch-start', x), + t.on('fn-start', b), + t.on('fetch-done', T); + })(e, this.ee, this.handler, this.dt), + this.importAggregator(e, () => i.e(478).then(i.bind(i, 3845))); + } + } + function Ae(e, t) { + var r = (0, pe.D)(t), + n = e.params || e; + (n.hostname = r.hostname), + (n.port = r.port), + (n.protocol = r.protocol), + (n.host = r.hostname + ':' + r.port), + (n.pathname = r.pathname), + (e.parsedOrigin = r), + (e.sameOrigin = r.sameOrigin); + } + const Oe = {}, + Se = ['pushState', 'replaceState']; + function Ne(e) { + const t = (function (e) { + return (e || te.ee).get('history'); + })(e); + return !y.RI || Oe[t.debugId]++ || ((Oe[t.debugId] = 1), (0, re.YM)(t).inPlace(window.history, Se, '-')), t; + } + var _e = i(3738); + function Ie(e) { + p( + u.BL, + function (t = Date.now()) { + const r = t - y.WN; + r < 0 && (0, l.R)(62, t), + (0, s.p)(h.XG, [u.BL, { time: r }], void 0, n.K7.metrics, e.ee), + e.addToTrace({ name: u.BL, start: t, origin: 'nr' }), + (0, s.p)(u.Pl + u.hG, [r, u.BL], void 0, n.K7.genericEvents, e.ee); + }, + e + ); + } + const { He: Pe, bD: je, d3: Ce, Kp: ke, TZ: Le, Lc: He, uP: Me, Rz: De } = _e; + class Ke extends E { + static featureName = Le; + constructor(e) { + var t; + super(e, Le), + (t = e), + p( + u.U2, + function (e) { + if (!(e && 'object' == typeof e && e.name && e.start)) return; + const r = { n: e.name, s: e.start - y.WN, e: (e.end || e.start) - y.WN, o: e.origin || '', t: 'api' }; + r.s < 0 || r.e < 0 || r.e < r.s + ? (0, l.R)(61, { start: r.s, end: r.e }) + : (0, s.p)('bstApi', [r], void 0, n.K7.sessionTrace, t.ee); + }, + t + ), + Ie(e); + if (!(0, R.V)(e.init)) return void this.deregisterDrain(); + const r = this.ee; + let o; + Ne(r), + (this.eventsEE = (0, ee.u)(r)), + this.eventsEE.on(Me, function (e, t) { + this.bstStart = (0, c.t)(); + }), + this.eventsEE.on(He, function (e, t) { + (0, s.p)('bst', [e[0], t, this.bstStart, (0, c.t)()], void 0, n.K7.sessionTrace, r); + }), + r.on(De + Ce, function (e) { + (this.time = (0, c.t)()), (this.startPath = location.pathname + location.hash); + }), + r.on(De + ke, function (e) { + (0, s.p)( + 'bstHist', + [location.pathname + location.hash, this.startPath, this.time], + void 0, + n.K7.sessionTrace, + r + ); + }); + try { + (o = new PerformanceObserver((e) => { + const t = e.getEntries(); + (0, s.p)(Pe, [t], void 0, n.K7.sessionTrace, r); + })), + o.observe({ type: je, buffered: !0 }); + } catch (e) {} + this.importAggregator(e, () => i.e(478).then(i.bind(i, 6974)), { resourceObserver: o }); + } + } + var Ue = i(6344); + class Fe extends E { + static featureName = Ue.TZ; + #n; + recorder; + constructor(e) { + var t; + let r; + super(e, Ue.TZ), + (t = e), + p( + u.CH, + function () { + (0, s.p)(u.CH, [], void 0, n.K7.sessionReplay, t.ee); + }, + t + ), + (function (e) { + p( + u.Tb, + function () { + (0, s.p)(u.Tb, [], void 0, n.K7.sessionReplay, e.ee); + }, + e + ); + })(e); + try { + r = JSON.parse(localStorage.getItem(''.concat(O.H3, '_').concat(O.uh))); + } catch (e) {} + (0, w.SR)(e.init) && this.ee.on(Ue.G4.RECORD, () => this.#i()), + this.#o(r) && + this.importRecorder().then((e) => { + e.startRecording(Ue.Qb.PRELOAD, r?.sessionReplayMode); + }), + this.importAggregator(this.agentRef, () => i.e(478).then(i.bind(i, 6167)), this), + this.ee.on('err', (e) => { + this.blocked || + (this.agentRef.runtime.isRecording && + ((this.errorNoticed = !0), + (0, s.p)(Ue.G4.ERROR_DURING_REPLAY, [e], void 0, this.featureName, this.ee))); + }); + } + #o(e) { + return ( + (e && (e.sessionReplayMode === O.g.FULL || e.sessionReplayMode === O.g.ERROR)) || + (0, w.Aw)(this.agentRef.init) + ); + } + importRecorder() { + return this.recorder + ? Promise.resolve(this.recorder) + : ((this.#n ??= Promise.all([i.e(478), i.e(249)]) + .then(i.bind(i, 4866)) + .then(({ Recorder: e }) => ((this.recorder = new e(this)), this.recorder)) + .catch((e) => { + throw (this.ee.emit('internal-error', [e]), (this.blocked = !0), e); + })), + this.#n); + } + #i() { + this.blocked || + (this.featAggregate + ? this.featAggregate.mode !== O.g.FULL && this.featAggregate.initializeRecording(O.g.FULL, !0, Ue.Qb.API) + : this.importRecorder().then(() => { + this.recorder.startRecording(Ue.Qb.API, O.g.FULL); + })); + } + } + var We = i(3962); + function Be(e) { + const t = e.ee.get('tracer'); + function r() {} + p( + u.dT, + function (e) { + return new r().get('object' == typeof e ? e : {}); + }, + e + ); + const i = (r.prototype = { + createTracer: function (r, i) { + var o = {}, + a = this, + d = 'function' == typeof i; + return ( + (0, s.p)(h.xV, ['API/createTracer/called'], void 0, n.K7.metrics, e.ee), + e.runSoftNavOverSpa || (0, s.p)(u.hw + 'tracer', [(0, c.t)(), r, o], a, n.K7.spa, e.ee), + function () { + if ((t.emit((d ? '' : 'no-') + 'fn-start', [(0, c.t)(), a, d], o), d)) + try { + return i.apply(this, arguments); + } catch (e) { + const r = 'string' == typeof e ? new Error(e) : e; + throw (t.emit('fn-err', [arguments, this, r], o), r); + } finally { + t.emit('fn-end', [(0, c.t)()], o); + } + } + ); + }, + }); + ['actionText', 'setName', 'setAttribute', 'save', 'ignore', 'onEnd', 'getContext', 'end', 'get'].forEach( + (t) => { + p.apply(this, [ + t, + function () { + return ( + (0, s.p)( + u.hw + t, + [(0, c.t)(), ...arguments], + this, + e.runSoftNavOverSpa ? n.K7.softNav : n.K7.spa, + e.ee + ), + this + ); + }, + e, + i, + ]); + } + ), + p( + u.PA, + function () { + e.runSoftNavOverSpa + ? (0, s.p)(u.hw + 'routeName', [performance.now(), ...arguments], void 0, n.K7.softNav, e.ee) + : (0, s.p)(u.Pl + 'routeName', [(0, c.t)(), ...arguments], this, n.K7.spa, e.ee); + }, + e + ); + } + class Ge extends E { + static featureName = We.TZ; + constructor(e) { + if ((super(e, We.TZ), Be(e), !y.RI || !(0, N.dV)().o.MO)) return; + const t = Ne(this.ee); + try { + this.removeOnAbort = new AbortController(); + } catch (e) {} + We.tC.forEach((e) => { + (0, I.sp)( + e, + (e) => { + a(e); + }, + !0, + this.removeOnAbort?.signal + ); + }); + const r = () => (0, s.p)('newURL', [(0, c.t)(), '' + window.location], void 0, this.featureName, this.ee); + t.on('pushState-end', r), + t.on('replaceState-end', r), + (0, I.sp)( + We.OV, + (e) => { + a(e), (0, s.p)('newURL', [e.timeStamp, '' + window.location], void 0, this.featureName, this.ee); + }, + !0, + this.removeOnAbort?.signal + ); + let n = !1; + const o = new ((0, N.dV)().o.MO)((e, t) => { + n || + ((n = !0), + requestAnimationFrame(() => { + (0, s.p)('newDom', [(0, c.t)()], void 0, this.featureName, this.ee), (n = !1); + })); + }), + a = (0, x.s)( + (e) => { + (0, s.p)('newUIEvent', [e], void 0, this.featureName, this.ee), + o.observe(document.body, { attributes: !0, childList: !0, subtree: !0, characterData: !0 }); + }, + 100, + { leading: !0 } + ); + (this.abortHandler = function () { + this.removeOnAbort?.abort(), o.disconnect(), (this.abortHandler = void 0); + }), + this.importAggregator(e, () => i.e(478).then(i.bind(i, 4393)), { domObserver: o }); + } + } + var Ve = i(7378); + const ze = {}, + Ze = ['appendChild', 'insertBefore', 'replaceChild']; + function qe(e) { + const t = (function (e) { + return (e || te.ee).get('jsonp'); + })(e); + if (!y.RI || ze[t.debugId]) return t; + ze[t.debugId] = !0; + var r = (0, re.YM)(t), + n = /[?&](?:callback|cb)=([^&#]+)/, + i = /(.*)\.([^.]+)/, + o = /^(\w+)(\.|$)(.*)$/; + function a(e, t) { + if (!e) return t; + const r = e.match(o), + n = r[1]; + return a(r[3], t[n]); + } + return ( + r.inPlace(Node.prototype, Ze, 'dom-'), + t.on('dom-start', function (e) { + !(function (e) { + if (!e || 'string' != typeof e.nodeName || 'script' !== e.nodeName.toLowerCase()) return; + if ('function' != typeof e.addEventListener) return; + var o = ((s = e.src), (c = s.match(n)), c ? c[1] : null); + var s, c; + if (!o) return; + var u = (function (e) { + var t = e.match(i); + if (t && t.length >= 3) return { key: t[2], parent: a(t[1], window) }; + return { key: e, parent: window }; + })(o); + if ('function' != typeof u.parent[u.key]) return; + var d = {}; + function l() { + t.emit('jsonp-end', [], d), + e.removeEventListener('load', l, (0, I.jT)(!1)), + e.removeEventListener('error', f, (0, I.jT)(!1)); + } + function f() { + t.emit('jsonp-error', [], d), + t.emit('jsonp-end', [], d), + e.removeEventListener('load', l, (0, I.jT)(!1)), + e.removeEventListener('error', f, (0, I.jT)(!1)); + } + r.inPlace(u.parent, [u.key], 'cb-', d), + e.addEventListener('load', l, (0, I.jT)(!1)), + e.addEventListener('error', f, (0, I.jT)(!1)), + t.emit('new-jsonp', [e.src], d); + })(e[0]); + }), + t + ); + } + const Xe = {}; + function Ye(e) { + const t = (function (e) { + return (e || te.ee).get('promise'); + })(e); + if (Xe[t.debugId]) return t; + Xe[t.debugId] = !0; + var r = t.context, + n = (0, re.YM)(t), + i = y.gm.Promise; + return ( + i && + (function () { + function e(r) { + var o = t.context(), + a = n(r, 'executor-', o, null, !1); + const s = Reflect.construct(i, [a], e); + return ( + (t.context(s).getCtx = function () { + return o; + }), + s + ); + } + (y.gm.Promise = e), + Object.defineProperty(e, 'name', { value: 'Promise' }), + (e.toString = function () { + return i.toString(); + }), + Object.setPrototypeOf(e, i), + ['all', 'race'].forEach(function (r) { + const n = i[r]; + e[r] = function (e) { + let i = !1; + [...(e || [])].forEach((e) => { + this.resolve(e).then(a('all' === r), a(!1)); + }); + const o = n.apply(this, arguments); + return o; + function a(e) { + return function () { + t.emit('propagate', [null, !i], o, !1, !1), (i = i || !e); + }; + } + }; + }), + ['resolve', 'reject'].forEach(function (r) { + const n = i[r]; + e[r] = function (e) { + const r = n.apply(this, arguments); + return e !== r && t.emit('propagate', [e, !0], r, !1, !1), r; + }; + }), + (e.prototype = i.prototype); + const o = i.prototype.then; + (i.prototype.then = function (...e) { + var i = this, + a = r(i); + (a.promise = i), (e[0] = n(e[0], 'cb-', a, null, !1)), (e[1] = n(e[1], 'cb-', a, null, !1)); + const s = o.apply(this, e); + return (a.nextPromise = s), t.emit('propagate', [i, !0], s, !1, !1), s; + }), + (i.prototype.then[re.Jt] = o), + t.on('executor-start', function (e) { + (e[0] = n(e[0], 'resolve-', this, null, !1)), (e[1] = n(e[1], 'resolve-', this, null, !1)); + }), + t.on('executor-err', function (e, t, r) { + e[1](r); + }), + t.on('cb-end', function (e, r, n) { + t.emit('propagate', [n, !0], this.nextPromise, !1, !1); + }), + t.on('propagate', function (e, r, n) { + (this.getCtx && !r) || + (this.getCtx = function () { + if (e instanceof Promise) var r = t.context(e); + return r && r.getCtx ? r.getCtx() : this; + }); + }); + })(), + t + ); + } + const Qe = {}, + $e = 'setTimeout', + Je = 'setInterval', + et = 'clearTimeout', + tt = '-start', + rt = [$e, 'setImmediate', Je, et, 'clearImmediate']; + function nt(e) { + const t = (function (e) { + return (e || te.ee).get('timer'); + })(e); + if (Qe[t.debugId]++) return t; + Qe[t.debugId] = 1; + var r = (0, re.YM)(t); + return ( + r.inPlace(y.gm, rt.slice(0, 2), $e + '-'), + r.inPlace(y.gm, rt.slice(2, 3), Je + '-'), + r.inPlace(y.gm, rt.slice(3), et + '-'), + t.on(Je + tt, function (e, t, n) { + e[0] = r(e[0], 'fn-', null, n); + }), + t.on($e + tt, function (e, t, n) { + (this.method = n), (this.timerDuration = isNaN(e[1]) ? 0 : +e[1]), (e[0] = r(e[0], 'fn-', this, n)); + }), + t + ); + } + const it = {}; + function ot(e) { + const t = (function (e) { + return (e || te.ee).get('mutation'); + })(e); + if (!y.RI || it[t.debugId]) return t; + it[t.debugId] = !0; + var r = (0, re.YM)(t), + n = y.gm.MutationObserver; + return ( + n && + ((window.MutationObserver = function (e) { + return this instanceof n ? new n(r(e, 'fn-')) : n.apply(this, arguments); + }), + (MutationObserver.prototype = n.prototype)), + t + ); + } + const { TZ: at, d3: st, Kp: ct, $p: ut, wW: dt, e5: lt, tH: ft, uP: ht, rw: pt, Lc: gt } = Ve; + class mt extends E { + static featureName = at; + constructor(e) { + if ((super(e, at), Be(e), !y.RI)) return; + try { + this.removeOnAbort = new AbortController(); + } catch (e) {} + let t, + r = 0; + const n = this.ee.get('tracer'), + o = qe(this.ee), + a = Ye(this.ee), + u = nt(this.ee), + d = oe(this.ee), + l = this.ee.get('events'), + f = he(this.ee), + h = Ne(this.ee), + p = ot(this.ee); + function g(e, t) { + h.emit('newURL', ['' + window.location, t]); + } + function m() { + r++, (t = window.location.hash), (this[ht] = (0, c.t)()); + } + function v() { + r--, window.location.hash !== t && g(0, !0); + var e = (0, c.t)(); + (this[lt] = ~~this[lt] + e - this[ht]), (this[gt] = e); + } + function b(e, t) { + e.on(t, function () { + this[t] = (0, c.t)(); + }); + } + this.ee.on(ht, m), + a.on(pt, m), + o.on(pt, m), + this.ee.on(gt, v), + a.on(dt, v), + o.on(dt, v), + this.ee.on('fn-err', (...t) => { + t[2]?.__newrelic?.[e.agentIdentifier] || + (0, s.p)('function-err', [...t], void 0, this.featureName, this.ee); + }), + this.ee.buffer([ht, gt, 'xhr-resolved'], this.featureName), + l.buffer([ht], this.featureName), + u.buffer(['setTimeout' + ct, 'clearTimeout' + st, ht], this.featureName), + d.buffer([ht, 'new-xhr', 'send-xhr' + st], this.featureName), + f.buffer([ft + st, ft + '-done', ft + ut + st, ft + ut + ct], this.featureName), + h.buffer(['newURL'], this.featureName), + p.buffer([ht], this.featureName), + a.buffer(['propagate', pt, dt, 'executor-err', 'resolve' + st], this.featureName), + n.buffer([ht, 'no-' + ht], this.featureName), + o.buffer(['new-jsonp', 'cb-start', 'jsonp-error', 'jsonp-end'], this.featureName), + b(f, ft + st), + b(f, ft + '-done'), + b(o, 'new-jsonp'), + b(o, 'jsonp-end'), + b(o, 'cb-start'), + h.on('pushState-end', g), + h.on('replaceState-end', g), + window.addEventListener('hashchange', g, (0, I.jT)(!0, this.removeOnAbort?.signal)), + window.addEventListener('load', g, (0, I.jT)(!0, this.removeOnAbort?.signal)), + window.addEventListener( + 'popstate', + function () { + g(0, r > 1); + }, + (0, I.jT)(!0, this.removeOnAbort?.signal) + ), + (this.abortHandler = this.#r), + this.importAggregator(e, () => i.e(478).then(i.bind(i, 5592))); + } + #r() { + this.removeOnAbort?.abort(), (this.abortHandler = void 0); + } + } + var vt = i(3333); + class bt extends E { + static featureName = vt.TZ; + constructor(e) { + super(e, vt.TZ); + const t = [ + e.init.page_action.enabled, + e.init.performance.capture_marks, + e.init.performance.capture_measures, + e.init.user_actions.enabled, + e.init.performance.resources.enabled, + ]; + var r; + if ( + ((r = e), + p(u.hG, (e, t) => z(e, t, r), r), + (function (e) { + p( + u.fF, + function () { + (0, s.p)(u.Pl + u.fF, [(0, c.t)(), ...arguments], void 0, n.K7.genericEvents, e.ee); + }, + e + ); + })(e), + Ie(e), + Z(e), + (function (e) { + p( + u.V1, + function (t, r) { + const i = (0, c.t)(), + { start: o, end: a, customAttributes: d } = r || {}, + f = { customAttributes: d || {} }; + if ('object' != typeof f.customAttributes || 'string' != typeof t || 0 === t.length) + return void (0, l.R)(57); + const h = (e, t) => + null == e ? t : 'number' == typeof e ? e : e instanceof PerformanceMark ? e.startTime : Number.NaN; + if (((f.start = h(o, 0)), (f.end = h(a, i)), Number.isNaN(f.start) || Number.isNaN(f.end))) + (0, l.R)(57); + else { + if (((f.duration = f.end - f.start), !(f.duration < 0))) + return (0, s.p)(u.Pl + u.V1, [f, t], void 0, n.K7.genericEvents, e.ee), f; + (0, l.R)(58); + } + }, + e + ); + })(e), + y.RI) + ) { + if ( + (e.init.user_actions.enabled && + (vt.Zp.forEach((e) => (0, I.sp)(e, (e) => (0, s.p)('ua', [e], void 0, this.featureName, this.ee), !0)), + vt.qN.forEach((e) => { + const t = (0, x.s)( + (e) => { + (0, s.p)('ua', [e], void 0, this.featureName, this.ee); + }, + 500, + { leading: !0 } + ); + (0, I.sp)(e, t); + })), + e.init.performance.resources.enabled && + y.gm.PerformanceObserver?.supportedEntryTypes.includes('resource')) + ) { + new PerformanceObserver((e) => { + e.getEntries().forEach((e) => { + (0, s.p)('browserPerformance.resource', [e], void 0, this.featureName, this.ee); + }); + }).observe({ type: 'resource', buffered: !0 }); + } + const a = Ne(this.ee); + function d() { + a.emit('navChange'); + } + a.on('pushState-end', d), + a.on('replaceState-end', d), + window.addEventListener('hashchange', d, (0, I.jT)(!0, this.removeOnAbort?.signal)), + window.addEventListener('popstate', d, (0, I.jT)(!0, this.removeOnAbort?.signal)); + } + try { + this.removeOnAbort = new AbortController(); + } catch (f) {} + function o(t) { + const r = (0, pe.D)(t); + return e.beacons.includes(r.hostname + ':' + r.port); + } + (this.abortHandler = () => { + this.removeOnAbort?.abort(), (this.abortHandler = void 0); + }), + y.gm.addEventListener( + 'error', + () => { + (0, s.p)('uaErr', [], void 0, n.K7.genericEvents, this.ee); + }, + (0, I.jT)(!1, this.removeOnAbort?.signal) + ), + he(this.ee), + oe(this.ee), + this.ee.on('open-xhr-start', (e, t) => { + o(e[1]) || + t.addEventListener('readystatechange', () => { + 2 === t.readyState && (0, s.p)('uaXhr', [], void 0, n.K7.genericEvents, this.ee); + }); + }), + this.ee.on('fetch-start', (e) => { + e.length >= 1 && !o(be(e[0])) && (0, s.p)('uaXhr', [], void 0, n.K7.genericEvents, this.ee); + }), + t.some((e) => e) ? this.importAggregator(e, () => i.e(478).then(i.bind(i, 8019))) : this.deregisterDrain(); + } + } + var yt = i(2646); + const wt = new Map(); + function Rt(e, t, r, n) { + if ('object' != typeof t || !t || 'string' != typeof r || !r || 'function' != typeof t[r]) return (0, l.R)(29); + const i = (function (e) { + return (e || te.ee).get('logger'); + })(e), + o = (0, re.YM)(i), + a = new yt.y(te.P); + (a.level = n.level), (a.customAttributes = n.customAttributes); + const s = t[r]?.[re.Jt] || t[r]; + return wt.set(s, a), o.inPlace(t, [r], 'wrap-logger-', () => wt.get(s)), i; + } + var xt = i(1910); + class Tt extends E { + static featureName = B.TZ; + constructor(e) { + var t; + super(e, B.TZ), + (t = e), + p(u.$9, (e, r) => V(e, r, t), t), + (function (e) { + p( + u.Wb, + (t, r, { customAttributes: n = {}, level: i = B.p_.INFO } = {}) => { + Rt(e.ee, t, r, { customAttributes: n, level: i }); + }, + e + ); + })(e), + Z(e); + const r = this.ee; + ['log', 'error', 'warn', 'info', 'debug', 'trace'].forEach((e) => { + (0, xt.i)(y.gm.console[e]), Rt(r, y.gm.console, e, { level: 'log' === e ? 'info' : e }); + }), + this.ee.on('wrap-logger-end', function ([e]) { + const { level: t, customAttributes: n } = this; + (0, G.R)(r, e, n, t); + }), + this.importAggregator(e, () => i.e(478).then(i.bind(i, 5288))); + } + } + new (class extends r { + constructor(e) { + var t; + (super(), y.gm) + ? ((this.features = {}), + (0, N.bQ)(this.agentIdentifier, this), + (this.desiredFeatures = new Set(e.features || [])), + this.desiredFeatures.add(S), + (this.runSoftNavOverSpa = [...this.desiredFeatures].some((e) => e.featureName === n.K7.softNav)), + (0, a.j)(this, e, e.loaderType || 'agent'), + (t = this), + p( + u.cD, + function (e, r, n = !1) { + if ('string' == typeof e) { + if (['string', 'number', 'boolean'].includes(typeof r) || null === r) return g(t, e, r, u.cD, n); + (0, l.R)(40, typeof r); + } else (0, l.R)(39, typeof e); + }, + t + ), + (function (e) { + p( + u.Dl, + function (t) { + if ('string' == typeof t || null === t) return g(e, 'enduser.id', t, u.Dl, !0); + (0, l.R)(41, typeof t); + }, + e + ); + })(this), + (function (e) { + p( + u.nb, + function (t) { + if ('string' == typeof t || null === t) return g(e, 'application.version', t, u.nb, !1); + (0, l.R)(42, typeof t); + }, + e + ); + })(this), + (function (e) { + p( + u.d3, + function () { + e.ee.emit('manual-start-all'); + }, + e + ); + })(this), + this.run()) + : (0, l.R)(21); + } + get config() { + return { info: this.info, init: this.init, loader_config: this.loader_config, runtime: this.runtime }; + } + get api() { + return this; + } + run() { + try { + const e = (function (e) { + const t = {}; + return ( + o.forEach((r) => { + t[r] = !!e[r]?.enabled; + }), + t + ); + })(this.init), + t = [...this.desiredFeatures]; + t.sort((e, t) => n.P3[e.featureName] - n.P3[t.featureName]), + t.forEach((t) => { + if (!e[t.featureName] && t.featureName !== n.K7.pageViewEvent) return; + if (this.runSoftNavOverSpa && t.featureName === n.K7.spa) return; + if (!this.runSoftNavOverSpa && t.featureName === n.K7.softNav) return; + const r = (function (e) { + switch (e) { + case n.K7.ajax: + return [n.K7.jserrors]; + case n.K7.sessionTrace: + return [n.K7.ajax, n.K7.pageViewEvent]; + case n.K7.sessionReplay: + return [n.K7.sessionTrace]; + case n.K7.pageViewTiming: + return [n.K7.pageViewEvent]; + default: + return []; + } + })(t.featureName).filter((e) => !(e in this.features)); + r.length > 0 && (0, l.R)(36, { targetFeature: t.featureName, missingDependencies: r }), + (this.features[t.featureName] = new t(this)); + }); + } catch (e) { + (0, l.R)(22, e); + for (const e in this.features) this.features[e].abortHandler?.(); + const t = (0, N.Zm)(); + delete t.initializedAgents[this.agentIdentifier]?.features, delete this.sharedAggregator; + return t.ee.get(this.agentIdentifier).abort(), !1; + } + } + })({ features: [Ee, S, j, Ke, Fe, C, q, bt, Tt, Ge, mt], loaderType: 'spa' }); + })(); +})(); diff --git a/src/index.html b/src/index.html index 5b82e0161..ff649bd7f 100644 --- a/src/index.html +++ b/src/index.html @@ -8,6 +8,7 @@ + From 8b36affdb251e07598759cf3ba76bce1764aed73 Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 8 Dec 2025 16:02:11 +0200 Subject: [PATCH 10/11] fix(tests): added unit tests for guards and ssr components --- src/app/core/guards/auth.guard.spec.ts | 165 ++++- src/app/core/guards/is-file.guard.spec.ts | 492 ++++++++++++++ src/app/core/guards/is-project.guard.spec.ts | 444 ++++++++++++ src/app/core/guards/is-registry.guard.spec.ts | 444 ++++++++++++ .../redirect-if-logged-in.guard.spec.ts | 133 +++- .../guards/registration-moderation.guard.ts | 2 + .../preprint-details.component.spec.ts | 185 ++++- .../project-overview.component.spec.ts | 128 ++++ .../registry-overview.component.spec.ts | 636 ++++++++++++++++-- 9 files changed, 2512 insertions(+), 117 deletions(-) create mode 100644 src/app/core/guards/is-file.guard.spec.ts create mode 100644 src/app/core/guards/is-project.guard.spec.ts create mode 100644 src/app/core/guards/is-registry.guard.spec.ts diff --git a/src/app/core/guards/auth.guard.spec.ts b/src/app/core/guards/auth.guard.spec.ts index cc5dfcdb8..88910e9ed 100644 --- a/src/app/core/guards/auth.guard.spec.ts +++ b/src/app/core/guards/auth.guard.spec.ts @@ -1,41 +1,164 @@ -import { inject } from '@angular/core'; +import { MockProvider } from 'ng-mocks'; + +import { of } from 'rxjs'; + +import { runInInjectionContext } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; import { AuthService } from '@core/services/auth.service'; +import { GetCurrentUser, UserSelectors } from '@osf/core/store/user'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { authGuard } from './auth.guard'; -jest.mock('@angular/core', () => ({ - ...jest.requireActual('@angular/core'), - inject: jest.fn(), -})); +import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; -describe.skip('authGuard (functional)', () => { - let mockAuthService: jest.Mocked; +describe('authGuard', () => { + let router: Router; + let authService: AuthService; + let viewOnlyHelper: ViewOnlyLinkHelperService; beforeEach(() => { - mockAuthService = { - isAuthenticated: jest.fn(), - } as unknown as jest.Mocked; + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [], + actions: [], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().withUrl('/test').build(), + }, + MockProvider(AuthService, { + navigateToSignIn: jest.fn(), + }), + MockProvider(ViewOnlyLinkHelperService, { + hasViewOnlyParam: jest.fn(), + }), + ], + }); + + router = TestBed.inject(Router); + authService = TestBed.inject(AuthService); + viewOnlyHelper = TestBed.inject(ViewOnlyLinkHelperService); + jest.clearAllMocks(); }); - it('should return true when user is authenticated', () => { - (inject as jest.Mock).mockImplementation((token) => { - if (token === AuthService) return mockAuthService; - }); + it('should return true when view-only param exists', () => { + jest.spyOn(viewOnlyHelper, 'hasViewOnlyParam').mockReturnValue(true); - const result = authGuard({} as any, {} as any); + const result = runInInjectionContext(TestBed, () => { + return authGuard({} as any, {} as any); + }); expect(result).toBe(true); + expect(viewOnlyHelper.hasViewOnlyParam).toHaveBeenCalledWith(router); + expect(authService.navigateToSignIn).not.toHaveBeenCalled(); }); - it('should navigate to sign-in and return false when user is not authenticated', () => { - (inject as jest.Mock).mockImplementation((token) => { - if (token === AuthService) return mockAuthService; + it('should return true when user is authenticated', (done) => { + jest.spyOn(viewOnlyHelper, 'hasViewOnlyParam').mockReturnValue(false); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: UserSelectors.isAuthenticated, + value: true, + }, + ], + actions: [ + { + action: GetCurrentUser, + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().withUrl('/test').build(), + }, + MockProvider(AuthService, { + navigateToSignIn: jest.fn(), + }), + MockProvider(ViewOnlyLinkHelperService, { + hasViewOnlyParam: jest.fn().mockReturnValue(false), + }), + ], }); - const result = authGuard({} as any, {} as any); + router = TestBed.inject(Router); + authService = TestBed.inject(AuthService); - expect(mockAuthService.navigateToSignIn).toHaveBeenCalled(); - expect(result).toBe(false); + runInInjectionContext(TestBed, () => { + const result = authGuard({} as any, {} as any); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(true); + expect(authService.navigateToSignIn).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(true); + done(); + } + }); + }); + + it('should navigate to sign-in and return false when user is not authenticated', (done) => { + jest.spyOn(viewOnlyHelper, 'hasViewOnlyParam').mockReturnValue(false); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: UserSelectors.isAuthenticated, + value: false, + }, + ], + actions: [ + { + action: GetCurrentUser, + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().withUrl('/test').build(), + }, + MockProvider(AuthService, { + navigateToSignIn: jest.fn(), + }), + MockProvider(ViewOnlyLinkHelperService, { + hasViewOnlyParam: jest.fn().mockReturnValue(false), + }), + ], + }); + + router = TestBed.inject(Router); + authService = TestBed.inject(AuthService); + + runInInjectionContext(TestBed, () => { + const result = authGuard({} as any, {} as any); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(authService.navigateToSignIn).toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); }); }); diff --git a/src/app/core/guards/is-file.guard.spec.ts b/src/app/core/guards/is-file.guard.spec.ts new file mode 100644 index 000000000..512fbd45a --- /dev/null +++ b/src/app/core/guards/is-file.guard.spec.ts @@ -0,0 +1,492 @@ +import { of } from 'rxjs'; + +import { PLATFORM_ID, runInInjectionContext } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; +import { CurrentResource } from '@osf/shared/models/current-resource.model'; +import { CurrentResourceSelectors, GetResource } from '@osf/shared/stores/current-resource'; + +import { isFileGuard } from './is-file.guard'; + +import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('isFileGuard', () => { + let router: Router; + + const createMockResource = (overrides?: Partial): CurrentResource => ({ + id: 'file-id', + type: CurrentResourceType.Files, + permissions: [], + ...overrides, + }); + + const createMockSegments = (path: string, secondPath?: string) => { + const segments = [{ path }] as any[]; + if (secondPath) { + segments.push({ path: secondPath }); + } + return segments; + }; + + beforeEach(() => { + Object.defineProperty(window, 'location', { + writable: true, + value: { + href: 'http://localhost/test', + }, + }); + + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [], + actions: [], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().withUrl('/test').build(), + }, + { + provide: PLATFORM_ID, + useValue: 'browser', + }, + ], + }); + + router = TestBed.inject(Router); + jest.clearAllMocks(); + }); + + it('should return false when id is missing', () => { + const result = runInInjectionContext(TestBed, () => { + return isFileGuard({} as any, []); + }); + + expect(result).toBe(false); + }); + + it('should return false when resource is not found', (done) => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: null, + }, + ], + actions: [ + { + action: new GetResource('file-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().withUrl('/test').build(), + }, + { + provide: PLATFORM_ID, + useValue: 'browser', + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isFileGuard({} as any, createMockSegments('file-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); + + it('should return false when resource id does not match exactly', (done) => { + const resource = createMockResource({ id: 'different-id' }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('file-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().withUrl('/test').build(), + }, + { + provide: PLATFORM_ID, + useValue: 'browser', + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isFileGuard({} as any, createMockSegments('file-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); + + it('should return true for Files with metadata path', (done) => { + const resource = createMockResource({ + id: 'file-id', + type: CurrentResourceType.Files, + }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('file-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().withUrl('/test').build(), + }, + { + provide: PLATFORM_ID, + useValue: 'browser', + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isFileGuard({} as any, createMockSegments('file-id', 'metadata')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(true); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(true); + done(); + } + }); + }); + + it('should navigate and return false for Files with parentId', (done) => { + const resource = createMockResource({ + id: 'file-id', + type: CurrentResourceType.Files, + parentId: 'parent-id', + }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('file-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().withUrl('/test').build(), + }, + { + provide: PLATFORM_ID, + useValue: 'browser', + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isFileGuard({} as any, createMockSegments('file-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).toHaveBeenCalledWith(['/', 'parent-id', 'files', 'file-id'], {}); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); + + it('should return true for Files without parentId', (done) => { + const resource = createMockResource({ + id: 'file-id', + type: CurrentResourceType.Files, + }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('file-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().withUrl('/test').build(), + }, + { + provide: PLATFORM_ID, + useValue: 'browser', + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isFileGuard({} as any, createMockSegments('file-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(true); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(true); + done(); + } + }); + }); + + it('should return false for other resource types', (done) => { + const resource = createMockResource({ + id: 'resource-id', + type: CurrentResourceType.Projects, + }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('resource-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().withUrl('/test').build(), + }, + { + provide: PLATFORM_ID, + useValue: 'browser', + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isFileGuard({} as any, createMockSegments('resource-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); + + it('should include view_only param in navigation when present in router.url (server-side)', (done) => { + const resource = createMockResource({ + id: 'file-id', + type: CurrentResourceType.Files, + parentId: 'parent-id', + }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('file-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().withUrl('/test?view_only=abc123').build(), + }, + { + provide: PLATFORM_ID, + useValue: 'server', + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isFileGuard({} as any, createMockSegments('file-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).toHaveBeenCalledWith(['/', 'parent-id', 'files', 'file-id'], { + queryParams: { view_only: 'abc123' }, + }); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); + + it('should include view_only param in navigation when present in window.location (browser)', (done) => { + const resource = createMockResource({ + id: 'file-id', + type: CurrentResourceType.Files, + parentId: 'parent-id', + }); + + Object.defineProperty(window, 'location', { + writable: true, + value: { + href: 'http://localhost/test?view_only=xyz789', + }, + }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('file-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().withUrl('/test').build(), + }, + { + provide: PLATFORM_ID, + useValue: 'browser', + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isFileGuard({} as any, createMockSegments('file-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).toHaveBeenCalledWith(['/', 'parent-id', 'files', 'file-id'], { + queryParams: { view_only: 'xyz789' }, + }); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); +}); diff --git a/src/app/core/guards/is-project.guard.spec.ts b/src/app/core/guards/is-project.guard.spec.ts new file mode 100644 index 000000000..41df9a57c --- /dev/null +++ b/src/app/core/guards/is-project.guard.spec.ts @@ -0,0 +1,444 @@ +import { of } from 'rxjs'; + +import { runInInjectionContext } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { UserSelectors } from '@core/store/user'; +import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; +import { CurrentResource } from '@osf/shared/models/current-resource.model'; +import { CurrentResourceSelectors, GetResource } from '@osf/shared/stores/current-resource'; + +import { isProjectGuard } from './is-project.guard'; + +import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('isProjectGuard', () => { + let router: Router; + + const createMockResource = (overrides?: Partial): CurrentResource => ({ + id: 'test-id', + type: CurrentResourceType.Projects, + permissions: [], + ...overrides, + }); + + const createMockSegments = (path: string) => [{ path }] as any[]; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [], + actions: [], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + jest.clearAllMocks(); + }); + + it('should return false when id is missing', () => { + const result = runInInjectionContext(TestBed, () => { + return isProjectGuard({} as any, []); + }); + + expect(result).toBe(false); + }); + + it('should return false when resource is not found', (done) => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: null, + }, + ], + actions: [ + { + action: new GetResource('test-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isProjectGuard({} as any, createMockSegments('test-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); + + it('should return false when id does not start with resource.id', (done) => { + const resource = createMockResource({ id: 'different-id' }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('test-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isProjectGuard({} as any, createMockSegments('test-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); + + it('should navigate and return true for Projects with parentId', (done) => { + const resource = createMockResource({ + id: 'parent-id', + type: CurrentResourceType.Projects, + parentId: 'parent-id', + }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('parent-id/child-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isProjectGuard({} as any, createMockSegments('parent-id/child-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(true); + expect(router.navigate).toHaveBeenCalledWith(['/', 'parent-id', 'files', 'parent-id/child-id'], { + queryParamsHandling: 'preserve', + }); + done(); + }); + } else { + expect(result).toBe(true); + done(); + } + }); + }); + + it('should return true for Projects without parentId', (done) => { + const resource = createMockResource({ + id: 'project-id', + type: CurrentResourceType.Projects, + }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('project-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isProjectGuard({} as any, createMockSegments('project-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(true); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(true); + done(); + } + }); + }); + + it('should navigate and return true for Preprints with parentId', (done) => { + const resource = createMockResource({ + id: 'parent-id', + type: CurrentResourceType.Preprints, + parentId: 'parent-id', + }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('parent-id/child-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isProjectGuard({} as any, createMockSegments('parent-id/child-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(true); + expect(router.navigate).toHaveBeenCalledWith(['/preprints', 'parent-id', 'parent-id/child-id']); + done(); + }); + } else { + expect(result).toBe(true); + done(); + } + }); + }); + + it('should navigate to profile and return false for Users when current user matches', (done) => { + const resource = createMockResource({ + id: 'user-id', + type: CurrentResourceType.Users, + }); + const currentUser = { id: 'user-id' }; + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + { + selector: UserSelectors.getCurrentUser, + value: currentUser, + }, + ], + actions: [ + { + action: new GetResource('user-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isProjectGuard({} as any, createMockSegments('user-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).toHaveBeenCalledWith(['/profile']); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); + + it('should navigate to user page and return false for Users when current user does not match', (done) => { + const resource = createMockResource({ + id: 'user-id', + type: CurrentResourceType.Users, + }); + const currentUser = { id: 'different-user-id' }; + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + { + selector: UserSelectors.getCurrentUser, + value: currentUser, + }, + ], + actions: [ + { + action: new GetResource('user-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isProjectGuard({} as any, createMockSegments('user-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).toHaveBeenCalledWith(['/user', 'user-id']); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); + + it('should return false for other resource types', (done) => { + const resource = createMockResource({ + id: 'resource-id', + type: CurrentResourceType.Files, + }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('resource-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isProjectGuard({} as any, createMockSegments('resource-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); +}); diff --git a/src/app/core/guards/is-registry.guard.spec.ts b/src/app/core/guards/is-registry.guard.spec.ts new file mode 100644 index 000000000..60346f3ed --- /dev/null +++ b/src/app/core/guards/is-registry.guard.spec.ts @@ -0,0 +1,444 @@ +import { of } from 'rxjs'; + +import { runInInjectionContext } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; +import { Router } from '@angular/router'; + +import { UserSelectors } from '@core/store/user'; +import { CurrentResourceType } from '@osf/shared/enums/resource-type.enum'; +import { CurrentResource } from '@osf/shared/models/current-resource.model'; +import { CurrentResourceSelectors, GetResource } from '@osf/shared/stores/current-resource'; + +import { isRegistryGuard } from './is-registry.guard'; + +import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; + +describe('isRegistryGuard', () => { + let router: Router; + + const createMockResource = (overrides?: Partial): CurrentResource => ({ + id: 'test-id', + type: CurrentResourceType.Registrations, + permissions: [], + ...overrides, + }); + + const createMockSegments = (path: string) => [{ path }] as any[]; + + beforeEach(() => { + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [], + actions: [], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + jest.clearAllMocks(); + }); + + it('should return false when id is missing', () => { + const result = runInInjectionContext(TestBed, () => { + return isRegistryGuard({} as any, []); + }); + + expect(result).toBe(false); + }); + + it('should return false when resource is not found', (done) => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: null, + }, + ], + actions: [ + { + action: new GetResource('test-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isRegistryGuard({} as any, createMockSegments('test-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); + + it('should return false when id does not start with resource.id', (done) => { + const resource = createMockResource({ id: 'different-id' }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('test-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isRegistryGuard({} as any, createMockSegments('test-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); + + it('should navigate and return true for Registrations with parentId', (done) => { + const resource = createMockResource({ + id: 'parent-id', + type: CurrentResourceType.Registrations, + parentId: 'parent-id', + }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('parent-id/child-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isRegistryGuard({} as any, createMockSegments('parent-id/child-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(true); + expect(router.navigate).toHaveBeenCalledWith(['/', 'parent-id', 'files', 'parent-id/child-id'], { + queryParamsHandling: 'preserve', + }); + done(); + }); + } else { + expect(result).toBe(true); + done(); + } + }); + }); + + it('should return true for Registrations without parentId', (done) => { + const resource = createMockResource({ + id: 'registration-id', + type: CurrentResourceType.Registrations, + }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('registration-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isRegistryGuard({} as any, createMockSegments('registration-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(true); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(true); + done(); + } + }); + }); + + it('should navigate and return true for Preprints with parentId', (done) => { + const resource = createMockResource({ + id: 'parent-id', + type: CurrentResourceType.Preprints, + parentId: 'parent-id', + }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('parent-id/child-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isRegistryGuard({} as any, createMockSegments('parent-id/child-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(true); + expect(router.navigate).toHaveBeenCalledWith(['/preprints', 'parent-id', 'parent-id/child-id']); + done(); + }); + } else { + expect(result).toBe(true); + done(); + } + }); + }); + + it('should navigate to profile and return false for Users when current user matches', (done) => { + const resource = createMockResource({ + id: 'user-id', + type: CurrentResourceType.Users, + }); + const currentUser = { id: 'user-id' }; + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + { + selector: UserSelectors.getCurrentUser, + value: currentUser, + }, + ], + actions: [ + { + action: new GetResource('user-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isRegistryGuard({} as any, createMockSegments('user-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).toHaveBeenCalledWith(['/profile']); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); + + it('should navigate to user page and return false for Users when current user does not match', (done) => { + const resource = createMockResource({ + id: 'user-id', + type: CurrentResourceType.Users, + }); + const currentUser = { id: 'different-user-id' }; + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + { + selector: UserSelectors.getCurrentUser, + value: currentUser, + }, + ], + actions: [ + { + action: new GetResource('user-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isRegistryGuard({} as any, createMockSegments('user-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).toHaveBeenCalledWith(['/user', 'user-id']); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); + + it('should return false for other resource types', (done) => { + const resource = createMockResource({ + id: 'resource-id', + type: CurrentResourceType.Files, + }); + + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: CurrentResourceSelectors.getCurrentResource, + value: resource, + }, + ], + actions: [ + { + action: new GetResource('resource-id'), + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); + + runInInjectionContext(TestBed, () => { + const result = isRegistryGuard({} as any, createMockSegments('resource-id')); + + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); + }); +}); diff --git a/src/app/core/guards/redirect-if-logged-in.guard.spec.ts b/src/app/core/guards/redirect-if-logged-in.guard.spec.ts index f3855b273..a12976489 100644 --- a/src/app/core/guards/redirect-if-logged-in.guard.spec.ts +++ b/src/app/core/guards/redirect-if-logged-in.guard.spec.ts @@ -1,51 +1,120 @@ +import { of } from 'rxjs'; + +import { runInInjectionContext } from '@angular/core'; +import { TestBed } from '@angular/core/testing'; import { Router } from '@angular/router'; -import { AuthService } from '@core/services/auth.service'; +import { GetCurrentUser, UserSelectors } from '@osf/core/store/user'; import { redirectIfLoggedInGuard } from './redirect-if-logged-in.guard'; -jest.mock('@angular/core', () => ({ - ...(jest.requireActual('@angular/core') as any), - inject: jest.fn(), -})); - -const inject = jest.requireMock('@angular/core').inject as jest.Mock; - -describe.skip('redirectIfLoggedInGuard', () => { - const mockAuthService = { - isAuthenticated: jest.fn(), - }; +import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; +import { provideMockStore } from '@testing/providers/store-provider.mock'; - const mockRouter = { - navigate: jest.fn(), - }; +describe('redirectIfLoggedInGuard', () => { + let router: Router; beforeEach(() => { - jest.clearAllMocks(); - inject.mockImplementation((token) => { - if (token === AuthService) return mockAuthService; - if (token === Router) return mockRouter; - return null; + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [], + actions: [], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], }); + + router = TestBed.inject(Router); + jest.clearAllMocks(); }); - it('should return false and call router.navigate if user is authenticated', () => { - mockAuthService.isAuthenticated.mockReturnValue(true); + it('should navigate to dashboard and return false when user is authenticated', (done) => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: UserSelectors.isAuthenticated, + value: true, + }, + ], + actions: [ + { + action: GetCurrentUser, + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); - const result = redirectIfLoggedInGuard({} as any, {} as any); + runInInjectionContext(TestBed, () => { + const result = redirectIfLoggedInGuard({} as any, {} as any); - expect(mockAuthService.isAuthenticated).toHaveBeenCalled(); - expect(mockRouter.navigate).toHaveBeenCalledWith(['/dashboard']); - expect(result).toBeUndefined(); + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(false); + expect(router.navigate).toHaveBeenCalledWith(['/dashboard']); + done(); + }); + } else { + expect(result).toBe(false); + done(); + } + }); }); - it('should return true and not call router.navigate if user is not authenticated', () => { - mockAuthService.isAuthenticated.mockReturnValue(false); + it('should return true and not navigate when user is not authenticated', (done) => { + TestBed.resetTestingModule(); + TestBed.configureTestingModule({ + providers: [ + provideMockStore({ + selectors: [ + { + selector: UserSelectors.isAuthenticated, + value: false, + }, + ], + actions: [ + { + action: GetCurrentUser, + value: of(true), + }, + ], + }), + { + provide: Router, + useValue: RouterMockBuilder.create().build(), + }, + ], + }); + + router = TestBed.inject(Router); - const result = redirectIfLoggedInGuard({} as any, {} as any); + runInInjectionContext(TestBed, () => { + const result = redirectIfLoggedInGuard({} as any, {} as any); - expect(mockAuthService.isAuthenticated).toHaveBeenCalled(); - expect(mockRouter.navigate).not.toHaveBeenCalled(); - expect(result).toBe(true); + if (typeof result === 'object' && 'subscribe' in result) { + result.subscribe((value) => { + expect(value).toBe(true); + expect(router.navigate).not.toHaveBeenCalled(); + done(); + }); + } else { + expect(result).toBe(true); + done(); + } + }); }); }); diff --git a/src/app/core/guards/registration-moderation.guard.ts b/src/app/core/guards/registration-moderation.guard.ts index b9286fa5b..89fd02224 100644 --- a/src/app/core/guards/registration-moderation.guard.ts +++ b/src/app/core/guards/registration-moderation.guard.ts @@ -16,7 +16,9 @@ export const registrationModerationGuard: CanActivateFn = (route) => { if (provider?.reviewsWorkflow) { return true; } + const id = route.params['providerId']; + return store.dispatch(new GetRegistryProvider(id)).pipe( switchMap(() => { return store.select(RegistrationProviderSelectors.getBrandedProvider).pipe( diff --git a/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts b/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts index 80a825706..1fbcfc53f 100644 --- a/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts +++ b/src/app/features/preprints/pages/preprint-details/preprint-details.component.spec.ts @@ -1,10 +1,17 @@ +import { Store } from '@ngxs/store'; + import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; +import { HttpTestingController } from '@angular/common/http/testing'; +import { PLATFORM_ID } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideServerRendering } from '@angular/platform-server'; import { ActivatedRoute, Router } from '@angular/router'; +import { HelpScoutService } from '@core/services/help-scout.service'; +import { PrerenderReadyService } from '@core/services/prerender-ready.service'; import { UserPermissions } from '@osf/shared/enums/user-permissions.enum'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { DataciteService } from '@osf/shared/services/datacite/datacite.service'; @@ -30,13 +37,18 @@ import { PreprintProvidersSelectors } from '../../store/preprint-providers'; import { PreprintDetailsComponent } from './preprint-details.component'; import { MOCK_CONTRIBUTOR } from '@testing/mocks/contributors.mock'; +import { DataciteMockFactory } from '@testing/mocks/datacite.service.mock'; import { PREPRINT_MOCK } from '@testing/mocks/preprint.mock'; import { PREPRINT_PROVIDER_DETAILS_MOCK } from '@testing/mocks/preprint-provider-details'; import { PREPRINT_REQUEST_MOCK } from '@testing/mocks/preprint-request.mock'; import { REVIEW_ACTION_MOCK } from '@testing/mocks/review-action.mock'; +import { ToastServiceMock } from '@testing/mocks/toast.service.mock'; import { TranslationServiceMock } from '@testing/mocks/translation.service.mock'; import { OSFTestingModule } from '@testing/osf.testing.module'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; +import { HelpScoutServiceMockFactory } from '@testing/providers/help-scout.service.mock'; +import { MetaTagsServiceMockFactory } from '@testing/providers/meta-tags.service.mock'; +import { PrerenderReadyServiceMockFactory } from '@testing/providers/prerender-ready.service.mock'; import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; @@ -98,6 +110,7 @@ describe('PreprintDetailsComponent', () => { ], providers: [ TranslationServiceMock, + ToastServiceMock, MockProvider(Router, routerMock), MockProvider(ActivatedRoute, activatedRouteMock), MockProvider(DataciteService, dataciteService), @@ -171,14 +184,6 @@ describe('PreprintDetailsComponent', () => { fixture.detectChanges(); }); - it('should create', () => { - expect(component).toBeTruthy(); - }); - - it('should initialize with correct default values', () => { - expect(component.classes).toBe('flex-1 flex flex-column w-full'); - }); - it('should return preprint from store', () => { const preprint = component.preprint(); expect(preprint).toBe(mockPreprint); @@ -297,14 +302,6 @@ describe('PreprintDetailsComponent', () => { expect(() => component.createNewVersionClicked()).not.toThrow(); }); - it('should have correct CSS classes', () => { - expect(component.classes).toBe('flex-1 flex flex-column w-full'); - }); - - it('should call dataciteService.logIdentifiableView on init', () => { - expect(dataciteService.logIdentifiableView).toHaveBeenCalledWith(component.preprint$); - }); - it('should handle preprint with different states', () => { const acceptedPreprint = { ...mockPreprint, reviewsState: ReviewsState.Accepted }; jest.spyOn(component, 'preprint').mockReturnValue(acceptedPreprint); @@ -337,11 +334,6 @@ describe('PreprintDetailsComponent', () => { expect(withdrawable).toBe(true); }); - it('should handle hasReadWriteAccess correctly', () => { - const hasAccess = component['hasWriteAccess'](); - expect(typeof hasAccess).toBe('boolean'); - }); - it('should handle preprint without write permissions', () => { const preprintWithoutWrite = { ...mockPreprint, @@ -353,3 +345,154 @@ describe('PreprintDetailsComponent', () => { expect(hasAccess).toBe(false); }); }); + +describe('PreprintDetailsComponent SSR Tests', () => { + let component: PreprintDetailsComponent; + let fixture: ComponentFixture; + let httpMock: HttpTestingController; + let mockActivatedRoute: ReturnType; + let mockRouter: ReturnType; + let store: Store; + + const mockPreprint = PREPRINT_MOCK; + const mockProvider = PREPRINT_PROVIDER_DETAILS_MOCK; + const mockContributors = [MOCK_CONTRIBUTOR]; + + beforeEach(async () => { + mockRouter = RouterMockBuilder.create().build(); + mockActivatedRoute = ActivatedRouteMockBuilder.create().withParams({ providerId: 'osf', id: 'preprint-1' }).build(); + + await TestBed.configureTestingModule({ + imports: [ + PreprintDetailsComponent, + OSFTestingModule, + ...MockComponents( + PreprintFileSectionComponent, + ShareAndDownloadComponent, + GeneralInformationComponent, + AdditionalInfoComponent, + StatusBannerComponent, + PreprintTombstoneComponent, + PreprintWarningBannerComponent, + ModerationStatusBannerComponent, + PreprintMakeDecisionComponent, + PreprintMetricsInfoComponent + ), + ], + providers: [ + provideServerRendering(), + { provide: PLATFORM_ID, useValue: 'server' }, + MockProvider(ActivatedRoute, mockActivatedRoute), + MockProvider(Router, mockRouter), + MockProvider(CustomDialogService, CustomDialogServiceMockBuilder.create().build()), + MockProvider(DataciteService, DataciteMockFactory()), + MockProvider(MetaTagsService, MetaTagsServiceMockFactory()), + MockProvider(PrerenderReadyService, PrerenderReadyServiceMockFactory()), + MockProvider(HelpScoutService, HelpScoutServiceMockFactory()), + TranslationServiceMock, + ToastServiceMock, + provideMockStore({ + signals: [ + { + selector: PreprintProvidersSelectors.getPreprintProviderDetails('osf'), + value: mockProvider, + }, + { + selector: PreprintProvidersSelectors.isPreprintProviderDetailsLoading, + value: false, + }, + { + selector: PreprintSelectors.getPreprint, + value: mockPreprint, + }, + { + selector: PreprintSelectors.isPreprintLoading, + value: false, + }, + { + selector: ContributorsSelectors.getBibliographicContributors, + value: mockContributors, + }, + { + selector: ContributorsSelectors.isBibliographicContributorsLoading, + value: false, + }, + { + selector: PreprintSelectors.getPreprintReviewActions, + value: [], + }, + { + selector: PreprintSelectors.arePreprintReviewActionsLoading, + value: false, + }, + { + selector: PreprintSelectors.getPreprintRequests, + value: [], + }, + { + selector: PreprintSelectors.arePreprintRequestsLoading, + value: false, + }, + { + selector: PreprintSelectors.getPreprintRequestActions, + value: [], + }, + { + selector: PreprintSelectors.arePreprintRequestActionsLoading, + value: false, + }, + { + selector: PreprintSelectors.hasAdminAccess, + value: false, + }, + { + selector: PreprintSelectors.hasWriteAccess, + value: false, + }, + { + selector: PreprintSelectors.getPreprintMetrics, + value: null, + }, + { + selector: PreprintSelectors.arePreprintMetricsLoading, + value: false, + }, + ], + }), + ], + }).compileComponents(); + + httpMock = TestBed.inject(HttpTestingController); + store = TestBed.inject(Store); + fixture = TestBed.createComponent(PreprintDetailsComponent); + component = fixture.componentInstance; + }); + + it('should render PreprintDetailsComponent server-side without errors', () => { + expect(() => { + fixture.detectChanges(); + }).not.toThrow(); + expect(component).toBeTruthy(); + }); + + it('should not access browser-only APIs during SSR', () => { + const platformId = TestBed.inject(PLATFORM_ID); + expect(platformId).toBe('server'); + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should not call browser-only actions in ngOnDestroy during SSR', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + + fixture.detectChanges(); + dispatchSpy.mockClear(); + component.ngOnDestroy(); + + expect(dispatchSpy).not.toHaveBeenCalled(); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/app/features/project/overview/project-overview.component.spec.ts b/src/app/features/project/overview/project-overview.component.spec.ts index 3cbbd1ee7..61373020d 100644 --- a/src/app/features/project/overview/project-overview.component.spec.ts +++ b/src/app/features/project/overview/project-overview.component.spec.ts @@ -4,7 +4,10 @@ import { MockComponents, MockProvider } from 'ng-mocks'; import { of } from 'rxjs'; +import { HttpTestingController } from '@angular/common/http/testing'; +import { PLATFORM_ID } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideServerRendering } from '@angular/platform-server'; import { ActivatedRoute, Router } from '@angular/router'; import { @@ -17,6 +20,7 @@ import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-l import { Mode } from '@osf/shared/enums/mode.enum'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; import { ToastService } from '@osf/shared/services/toast.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; import { AddonsSelectors, ClearConfiguredAddons } from '@osf/shared/stores/addons'; import { GetBookmarksCollectionId } from '@osf/shared/stores/bookmarks'; import { ClearCollections, CollectionsSelectors } from '@osf/shared/stores/collections'; @@ -171,3 +175,127 @@ describe('ProjectOverviewComponent', () => { expect(store.dispatch).toHaveBeenCalledWith(expect.any(ClearConfiguredAddons)); }); }); + +describe('ProjectOverviewComponent SSR Tests', () => { + let component: ProjectOverviewComponent; + let fixture: ComponentFixture; + let httpMock: HttpTestingController; + let mockActivatedRoute: ReturnType; + let mockRouter: ReturnType; + let store: Store; + + const mockProject: ProjectOverviewModel = { + ...MOCK_PROJECT_OVERVIEW, + id: 'project-123', + title: 'Test Project', + parentId: 'parent-123', + rootParentId: 'root-123', + isPublic: true, + }; + + beforeEach(async () => { + mockRouter = RouterMockBuilder.create().withUrl('/projects/project-123').build(); + const parentRoute = { + params: of({ id: 'project-123' }), + snapshot: { params: { id: 'project-123' }, queryParams: {} }, + } as any; + mockActivatedRoute = Object.assign( + ActivatedRouteMockBuilder.create().withParams({ id: 'project-123' }).withQueryParams({}).build(), + { parent: parentRoute } + ); + + await TestBed.configureTestingModule({ + imports: [ + ProjectOverviewComponent, + OSFTestingModule, + ...MockComponents( + SubHeaderComponent, + LoadingSpinnerComponent, + OverviewWikiComponent, + OverviewComponentsComponent, + LinkedResourcesComponent, + ProjectRecentActivityComponent, + ProjectOverviewToolbarComponent, + ProjectOverviewMetadataComponent, + FilesWidgetComponent, + ViewOnlyLinkMessageComponent, + OverviewParentProjectComponent, + CitationAddonCardComponent + ), + ], + providers: [ + provideServerRendering(), + { provide: PLATFORM_ID, useValue: 'server' }, + MockProvider(ActivatedRoute, mockActivatedRoute), + MockProvider(Router, mockRouter), + MockProvider(CustomDialogService, CustomDialogServiceMockBuilder.create().build()), + MockProvider(ToastService, { showSuccess: jest.fn() }), + MockProvider(ViewOnlyLinkHelperService, { hasViewOnlyParam: jest.fn().mockReturnValue(false) }), + provideMockStore({ + signals: [ + { selector: ProjectOverviewSelectors.getProject, value: mockProject }, + { selector: ProjectOverviewSelectors.getProjectLoading, value: false }, + { selector: ProjectOverviewSelectors.isProjectAnonymous, value: false }, + { selector: ProjectOverviewSelectors.hasWriteAccess, value: true }, + { selector: ProjectOverviewSelectors.hasAdminAccess, value: true }, + { selector: ProjectOverviewSelectors.isWikiEnabled, value: true }, + { selector: ProjectOverviewSelectors.getParentProject, value: null }, + { selector: ProjectOverviewSelectors.getParentProjectLoading, value: false }, + { selector: ProjectOverviewSelectors.getStorage, value: null }, + { selector: ProjectOverviewSelectors.isStorageLoading, value: false }, + { selector: CollectionsModerationSelectors.getCollectionSubmissions, value: [] }, + { selector: CollectionsModerationSelectors.getCurrentReviewAction, value: null }, + { selector: CollectionsModerationSelectors.getCurrentReviewActionLoading, value: false }, + { selector: CollectionsSelectors.getCollectionProvider, value: null }, + { selector: CollectionsSelectors.getCollectionProviderLoading, value: false }, + { selector: CurrentResourceSelectors.getResourceWithChildren, value: [] }, + { selector: CurrentResourceSelectors.isResourceWithChildrenLoading, value: false }, + { selector: AddonsSelectors.getAddonsResourceReference, value: [] }, + { selector: AddonsSelectors.getConfiguredCitationAddons, value: [] }, + { selector: AddonsSelectors.getOperationInvocation, value: null }, + ], + }), + ], + }).compileComponents(); + + httpMock = TestBed.inject(HttpTestingController); + store = TestBed.inject(Store); + fixture = TestBed.createComponent(ProjectOverviewComponent); + component = fixture.componentInstance; + }); + + it('should render ProjectOverviewComponent server-side without errors', () => { + expect(() => { + fixture.detectChanges(); + }).not.toThrow(); + expect(component).toBeTruthy(); + }); + + it('should not access browser-only APIs during SSR', () => { + const platformId = TestBed.inject(PLATFORM_ID); + expect(platformId).toBe('server'); + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should execute constructor effects without errors in SSR context', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + fixture.detectChanges(); + expect(dispatchSpy).toHaveBeenCalled(); + expect(component).toBeTruthy(); + }); + + it('should not call browser-only actions in ngOnDestroy during SSR', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + + fixture.detectChanges(); + dispatchSpy.mockClear(); + fixture.destroy(); + + expect(dispatchSpy).not.toHaveBeenCalled(); + }); + + afterEach(() => { + httpMock.verify(); + }); +}); diff --git a/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts b/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts index 506a9108d..ed58b1a97 100644 --- a/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts +++ b/src/app/features/registry/pages/registry-overview/registry-overview.component.spec.ts @@ -1,36 +1,136 @@ +import { Store } from '@ngxs/store'; + import { MockComponents, MockProvider } from 'ng-mocks'; +import { of } from 'rxjs'; + +import { HttpTestingController } from '@angular/common/http/testing'; +import { PLATFORM_ID, signal } from '@angular/core'; import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { provideServerRendering } from '@angular/platform-server'; +import { ActivatedRoute, Router } from '@angular/router'; import { DataResourcesComponent } from '@osf/shared/components/data-resources/data-resources.component'; import { LoadingSpinnerComponent } from '@osf/shared/components/loading-spinner/loading-spinner.component'; import { SubHeaderComponent } from '@osf/shared/components/sub-header/sub-header.component'; import { ViewOnlyLinkMessageComponent } from '@osf/shared/components/view-only-link-message/view-only-link-message.component'; +import { RegistrationReviewStates } from '@osf/shared/enums/registration-review-states.enum'; +import { ResourceType } from '@osf/shared/enums/resource-type.enum'; +import { RevisionReviewStates } from '@osf/shared/enums/revision-review-states.enum'; import { CustomDialogService } from '@osf/shared/services/custom-dialog.service'; +import { LoaderService } from '@osf/shared/services/loader.service'; +import { ToastService } from '@osf/shared/services/toast.service'; +import { ViewOnlyLinkHelperService } from '@osf/shared/services/view-only-link-helper.service'; +import { GetBookmarksCollectionId } from '@osf/shared/stores/bookmarks'; +import { ContributorsSelectors, GetBibliographicContributors } from '@osf/shared/stores/contributors'; import { ArchivingMessageComponent } from '../../components/archiving-message/archiving-message.component'; import { RegistrationOverviewToolbarComponent } from '../../components/registration-overview-toolbar/registration-overview-toolbar.component'; import { RegistryBlocksSectionComponent } from '../../components/registry-blocks-section/registry-blocks-section.component'; +import { RegistryMakeDecisionComponent } from '../../components/registry-make-decision/registry-make-decision.component'; import { RegistryOverviewMetadataComponent } from '../../components/registry-overview-metadata/registry-overview-metadata.component'; import { RegistryRevisionsComponent } from '../../components/registry-revisions/registry-revisions.component'; import { RegistryStatusesComponent } from '../../components/registry-statuses/registry-statuses.component'; import { WithdrawnMessageComponent } from '../../components/withdrawn-message/withdrawn-message.component'; -import { RegistrySelectors } from '../../store/registry'; +import { + CreateSchemaResponse, + GetRegistryById, + GetRegistryReviewActions, + GetRegistrySchemaResponses, + GetSchemaBlocks, + RegistrySelectors, +} from '../../store/registry'; import { RegistryOverviewComponent } from './registry-overview.component'; +import { MOCK_REGISTRATION_OVERVIEW_MODEL } from '@testing/mocks/registration-overview-model.mock'; +import { createMockSchemaResponse } from '@testing/mocks/schema-response.mock'; +import { ToastServiceMock } from '@testing/mocks/toast.service.mock'; import { OSFTestingModule } from '@testing/osf.testing.module'; import { CustomDialogServiceMockBuilder } from '@testing/providers/custom-dialog-provider.mock'; +import { LoaderServiceMock } from '@testing/providers/loader-service.mock'; +import { ActivatedRouteMockBuilder } from '@testing/providers/route-provider.mock'; +import { RouterMockBuilder } from '@testing/providers/router-provider.mock'; import { provideMockStore } from '@testing/providers/store-provider.mock'; describe('RegistryOverviewComponent', () => { let component: RegistryOverviewComponent; let fixture: ComponentFixture; - let mockCustomDialogService: ReturnType; + let store: Store; + let httpMock: HttpTestingController; - beforeEach(async () => { - mockCustomDialogService = CustomDialogServiceMockBuilder.create().withDefaultOpen().build(); - await TestBed.configureTestingModule({ + const createTestBed = (options?: { + isSSR?: boolean; + registryId?: string; + queryParams?: Record; + registry?: any; + schemaResponses?: any[]; + hasViewOnly?: boolean; + }) => { + const registryId = options?.registryId || 'registry-1'; + const mockRouter = RouterMockBuilder.create().withUrl('/registries/registry-1').build(); + const parentRoute = { + params: of({ id: registryId }), + snapshot: { params: { id: registryId }, queryParams: {} }, + } as any; + const mockActivatedRoute = Object.assign( + ActivatedRouteMockBuilder.create() + .withParams({ id: registryId }) + .withQueryParams(options?.queryParams || {}) + .build(), + { parent: parentRoute } + ); + + const mockCustomDialogService = CustomDialogServiceMockBuilder.create().withDefaultOpen().build(); + const mockLoaderService = new LoaderServiceMock(); + const mockViewOnlyHelper = { + hasViewOnlyParam: jest.fn().mockReturnValue(options?.hasViewOnly || false), + }; + + const defaultRegistry = options?.registry || null; + const defaultSchemaResponses = options?.schemaResponses || []; + + const providers: any[] = [ + MockProvider(ActivatedRoute, mockActivatedRoute), + MockProvider(Router, mockRouter), + MockProvider(CustomDialogService, mockCustomDialogService), + MockProvider(LoaderService, mockLoaderService), + MockProvider(ToastService, ToastServiceMock.useValue), + MockProvider(ViewOnlyLinkHelperService, mockViewOnlyHelper), + provideMockStore({ + actions: [ + { action: new GetRegistryById(registryId), value: of(void 0) }, + { action: new GetBookmarksCollectionId(), value: of(void 0) }, + { action: new GetBibliographicContributors(registryId, ResourceType.Registration), value: of(void 0) }, + { action: new GetSchemaBlocks(defaultRegistry?.registrationSchemaLink || ''), value: of(void 0) }, + { action: new GetRegistrySchemaResponses(registryId), value: of(void 0) }, + { action: new CreateSchemaResponse(registryId), value: of({ id: 'revision-1' }) }, + { action: new GetRegistryReviewActions(registryId), value: of(void 0) }, + ], + signals: [ + { selector: RegistrySelectors.getRegistry, value: defaultRegistry }, + { selector: RegistrySelectors.isRegistryLoading, value: false }, + { selector: RegistrySelectors.isRegistryAnonymous, value: false }, + { selector: RegistrySelectors.getSchemaResponses, value: defaultSchemaResponses }, + { selector: RegistrySelectors.isSchemaResponsesLoading, value: false }, + { selector: RegistrySelectors.getSchemaBlocks, value: [] }, + { selector: RegistrySelectors.isSchemaBlocksLoading, value: false }, + { selector: RegistrySelectors.areReviewActionsLoading, value: false }, + { selector: RegistrySelectors.getSchemaResponse, value: defaultSchemaResponses[0] || null }, + { selector: RegistrySelectors.hasWriteAccess, value: false }, + { selector: RegistrySelectors.hasAdminAccess, value: false }, + { selector: ContributorsSelectors.getBibliographicContributors, value: [] }, + { selector: ContributorsSelectors.isBibliographicContributorsLoading, value: false }, + { selector: ContributorsSelectors.hasMoreBibliographicContributors, value: false }, + ], + }), + ]; + + if (options?.isSSR) { + providers.push(provideServerRendering(), { provide: PLATFORM_ID, useValue: 'server' }); + } + + return TestBed.configureTestingModule({ imports: [ RegistryOverviewComponent, OSFTestingModule, @@ -48,58 +148,508 @@ describe('RegistryOverviewComponent', () => { ViewOnlyLinkMessageComponent ), ], - providers: [ - MockProvider(CustomDialogService, mockCustomDialogService), - provideMockStore({ - signals: [ - { selector: RegistrySelectors.getRegistry, value: null }, - { selector: RegistrySelectors.isRegistryLoading, value: false }, - { selector: RegistrySelectors.isRegistryAnonymous, value: false }, - { selector: RegistrySelectors.getInstitutions, value: [] }, - { selector: RegistrySelectors.isInstitutionsLoading, value: false }, - { selector: RegistrySelectors.getSchemaBlocks, value: [] }, - { selector: RegistrySelectors.isSchemaBlocksLoading, value: false }, - { selector: RegistrySelectors.areReviewActionsLoading, value: false }, - { selector: RegistrySelectors.getSchemaResponse, value: null }, - { selector: RegistrySelectors.getSchemaResponseLoading, value: false }, - { selector: RegistrySelectors.hasWriteAccess, value: false }, - { selector: RegistrySelectors.hasAdminAccess, value: false }, - { selector: RegistrySelectors.getReviewActions, value: [] }, - { selector: RegistrySelectors.isReviewActionSubmitting, value: false }, - ], - }), - ], - }).compileComponents(); + providers, + }); + }; + + beforeEach(async () => { + await createTestBed().compileComponents(); fixture = TestBed.createComponent(RegistryOverviewComponent); component = fixture.componentInstance; + store = TestBed.inject(Store); + httpMock = TestBed.inject(HttpTestingController); + }); + + afterEach(() => { + httpMock.verify(); + jest.clearAllMocks(); }); it('should create', () => { expect(component).toBeTruthy(); }); - it('should handle loading states', () => { - expect(component.isRegistryLoading()).toBe(false); - expect(component.isSchemaBlocksLoading()).toBe(false); - expect(component.areReviewActionsLoading()).toBe(false); + describe('Initialization', () => { + it('should initialize with default values', () => { + expect(component.isModeration).toBe(false); + expect(component.revisionId).toBeUndefined(); + expect(component.revisionInProgress).toBeUndefined(); + expect(component.selectedRevisionIndex()).toBe(0); + }); + + it('should handle loading states', () => { + expect(component.isRegistryLoading()).toBe(false); + expect(component.isSchemaBlocksLoading()).toBe(false); + expect(component.areReviewActionsLoading()).toBe(false); + }); + + it('should handle registry data', () => { + expect(component.registry()).toBeNull(); + expect(component.isAnonymous()).toBe(false); + expect(component.schemaBlocks()).toEqual([]); + expect(component.currentRevision()).toBeNull(); + }); + + it('should handle permissions', () => { + expect(component.hasWriteAccess()).toBe(false); + expect(component.hasAdminAccess()).toBe(false); + }); }); - it('should handle registry data', () => { - expect(component.registry()).toBeNull(); - expect(component.isAnonymous()).toBe(false); - expect(component.schemaBlocks()).toEqual([]); - expect(component.currentRevision()).toBeNull(); + describe('Computed Properties', () => { + beforeEach(async () => { + TestBed.resetTestingModule(); + await createTestBed({ + registry: MOCK_REGISTRATION_OVERVIEW_MODEL, + }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should compute hasViewOnly based on router query params', async () => { + TestBed.resetTestingModule(); + const mockViewOnlyHelper = { + hasViewOnlyParam: jest.fn().mockReturnValue(true), + }; + await createTestBed({ + registry: MOCK_REGISTRATION_OVERVIEW_MODEL, + hasViewOnly: true, + }).compileComponents(); + TestBed.overrideProvider(ViewOnlyLinkHelperService, { useValue: mockViewOnlyHelper }); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + expect(component.hasViewOnly()).toBe(true); + }); + + it('should compute hasViewOnly as false when view-only param is not present', async () => { + TestBed.resetTestingModule(); + const mockViewOnlyHelper = { + hasViewOnlyParam: jest.fn().mockReturnValue(false), + }; + await createTestBed({ + registry: MOCK_REGISTRATION_OVERVIEW_MODEL, + hasViewOnly: false, + }).compileComponents(); + TestBed.overrideProvider(ViewOnlyLinkHelperService, { useValue: mockViewOnlyHelper }); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + expect(component.hasViewOnly()).toBe(false); + }); + + it('should compute showToolbar as false when archiving', async () => { + const archivingRegistry = { ...MOCK_REGISTRATION_OVERVIEW_MODEL, archiving: true }; + TestBed.resetTestingModule(); + await createTestBed({ registry: archivingRegistry }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + expect(component.showToolbar()).toBe(false); + }); + + it('should compute showToolbar as false when withdrawn', async () => { + const withdrawnRegistry = { ...MOCK_REGISTRATION_OVERVIEW_MODEL, withdrawn: true }; + TestBed.resetTestingModule(); + await createTestBed({ registry: withdrawnRegistry }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + expect(component.showToolbar()).toBe(false); + }); + + it('should compute showToolbar as true when not archiving or withdrawn', () => { + expect(component.showToolbar()).toBe(true); + }); + + it('should compute isInitialState based on reviewsState', async () => { + const initialStateRegistry = { + ...MOCK_REGISTRATION_OVERVIEW_MODEL, + reviewsState: RegistrationReviewStates.Initial, + }; + TestBed.resetTestingModule(); + await createTestBed({ registry: initialStateRegistry }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + expect(component.isInitialState()).toBe(true); + }); + + it('should compute canMakeDecision when in moderation mode', async () => { + TestBed.resetTestingModule(); + await createTestBed({ + registry: MOCK_REGISTRATION_OVERVIEW_MODEL, + queryParams: { mode: 'moderator' }, + }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + expect(component.canMakeDecision()).toBe(true); + }); + + it('should compute canMakeDecision as false when archiving', async () => { + const archivingRegistry = { ...MOCK_REGISTRATION_OVERVIEW_MODEL, archiving: true }; + TestBed.resetTestingModule(); + await createTestBed({ + registry: archivingRegistry, + queryParams: { mode: 'moderator' }, + }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + expect(component.canMakeDecision()).toBe(false); + }); + + it('should compute isRootRegistration when rootParentId matches id', async () => { + const rootRegistry = { + ...MOCK_REGISTRATION_OVERVIEW_MODEL, + rootParentId: MOCK_REGISTRATION_OVERVIEW_MODEL.id, + }; + TestBed.resetTestingModule(); + await createTestBed({ registry: rootRegistry }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + expect(component.isRootRegistration()).toBe(true); + }); + + it('should compute isRootRegistration when rootParentId is null', async () => { + const noRootRegistry = { ...MOCK_REGISTRATION_OVERVIEW_MODEL, rootParentId: null }; + TestBed.resetTestingModule(); + await createTestBed({ registry: noRootRegistry }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + expect(component.isRootRegistration()).toBe(true); + }); + + it('should compute schemaResponse based on selectedRevisionIndex', async () => { + const schemaResponses = [ + createMockSchemaResponse('revision-1', RevisionReviewStates.Approved), + createMockSchemaResponse('revision-2', RevisionReviewStates.Approved), + ]; + TestBed.resetTestingModule(); + await createTestBed({ + registry: MOCK_REGISTRATION_OVERVIEW_MODEL, + schemaResponses, + }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + expect(component.schemaResponse()?.id).toBe('revision-1'); + component.openRevision(1); + expect(component.schemaResponse()?.id).toBe('revision-2'); + }); }); - it('should handle permissions', () => { - expect(component.hasWriteAccess()).toBe(false); - expect(component.hasAdminAccess()).toBe(false); + describe('Methods', () => { + beforeEach(async () => { + TestBed.resetTestingModule(); + await createTestBed({ + registry: MOCK_REGISTRATION_OVERVIEW_MODEL, + }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + }); + + it('should open revision and update selectedRevisionIndex', () => { + const revisionIndex = 2; + component.openRevision(revisionIndex); + expect(component.selectedRevisionIndex()).toBe(revisionIndex); + }); + + it('should call onUpdateRegistration and navigate to justification page', async () => { + TestBed.resetTestingModule(); + const mockLoaderService = new LoaderServiceMock(); + const showSpy = jest.spyOn(mockLoaderService, 'show'); + const schemaResponse = createMockSchemaResponse('revision-1', RevisionReviewStates.Approved); + const navigateSpy = jest.fn(); + const testRegistry = { ...MOCK_REGISTRATION_OVERVIEW_MODEL, id: 'registry-1' }; + await createTestBed({ + registry: testRegistry, + }).compileComponents(); + TestBed.overrideProvider(LoaderService, { useValue: mockLoaderService }); + TestBed.overrideProvider(Router, { + useValue: { + ...RouterMockBuilder.create().withUrl('/registries/registry-1').build(), + navigate: navigateSpy, + }, + }); + const mockStore = TestBed.inject(Store); + jest.spyOn(mockStore, 'selectSignal').mockImplementation((selector: any) => { + if (selector === RegistrySelectors.getSchemaResponse) { + return signal(schemaResponse); + } + if (selector === RegistrySelectors.getRegistry) { + return signal(testRegistry); + } + return signal(null); + }); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + component.onUpdateRegistration('registry-1'); + + expect(showSpy).toHaveBeenCalled(); + await new Promise((resolve) => setTimeout(resolve, 200)); + expect(navigateSpy).toHaveBeenCalledWith([`/registries/revisions/revision-1/justification`]); + }); + + it('should navigate to justification page when continuing update for approved revision', async () => { + TestBed.resetTestingModule(); + const navigateSpy = jest.fn(); + await createTestBed({ + registry: MOCK_REGISTRATION_OVERVIEW_MODEL, + }).compileComponents(); + TestBed.overrideProvider(Router, { + useValue: { + ...RouterMockBuilder.create().withUrl('/registries/registry-1').build(), + navigate: navigateSpy, + }, + }); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + component.revisionInProgress = createMockSchemaResponse('revision-1', RevisionReviewStates.Approved); + + component.onContinueUpdateRegistration(); + + expect(navigateSpy).toHaveBeenCalledWith([`/registries/revisions/revision-1/justification`]); + }); + + it('should navigate to review page when continuing update for unapproved revision', async () => { + TestBed.resetTestingModule(); + const navigateSpy = jest.fn(); + await createTestBed({ + registry: MOCK_REGISTRATION_OVERVIEW_MODEL, + }).compileComponents(); + TestBed.overrideProvider(Router, { + useValue: { + ...RouterMockBuilder.create().withUrl('/registries/registry-1').build(), + navigate: navigateSpy, + }, + }); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + component.revisionInProgress = createMockSchemaResponse('revision-1', RevisionReviewStates.Unapproved); + + component.onContinueUpdateRegistration(); + + expect(navigateSpy).toHaveBeenCalledWith([`/registries/revisions/revision-1/review`]); + }); + + it('should handle open make decision dialog', async () => { + const registryId = 'registry-1'; + TestBed.resetTestingModule(); + const mockCustomDialogService = CustomDialogServiceMockBuilder.create().build(); + const openSpy = jest.spyOn(mockCustomDialogService, 'open'); + const navigateSpy = jest.fn(); + const navigateByUrlSpy = jest.fn().mockResolvedValue(true); + const showSuccessSpy = jest.fn(); + + await createTestBed({ + registry: { ...MOCK_REGISTRATION_OVERVIEW_MODEL, id: registryId }, + queryParams: { revisionId: 'revision-1' }, + }).compileComponents(); + TestBed.overrideProvider(CustomDialogService, { useValue: mockCustomDialogService }); + TestBed.overrideProvider(Router, { + useValue: { + ...RouterMockBuilder.create().withUrl('/registries/registry-1').build(), + navigate: navigateSpy, + navigateByUrl: navigateByUrlSpy, + }, + }); + TestBed.overrideProvider(ToastService, { + useValue: { + showSuccess: showSuccessSpy, + }, + }); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + const mockDialogRef = { + onClose: { + pipe: jest.fn(() => + of({ + action: 'accept', + }) + ), + }, + }; + openSpy.mockReturnValue(mockDialogRef as any); + + component.handleOpenMakeDecisionDialog(); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + expect(openSpy).toHaveBeenCalledWith(RegistryMakeDecisionComponent, expect.any(Object)); + expect(showSuccessSpy).toHaveBeenCalledWith('moderation.makeDecision.acceptSuccess'); + expect(navigateByUrlSpy).toHaveBeenCalled(); + }); }); - it('should open revision', () => { - const revisionIndex = 1; - component.openRevision(revisionIndex); - expect(component.selectedRevisionIndex()).toBe(revisionIndex); + describe('Effects', () => { + it('should dispatch actions when registry ID is available', async () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + + TestBed.resetTestingModule(); + await createTestBed({ + registryId: 'registry-1', + registry: MOCK_REGISTRATION_OVERVIEW_MODEL, + }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(dispatchSpy).toHaveBeenCalled(); + }); + + it('should fetch schema blocks and responses when registry is available', async () => { + const registry = { + ...MOCK_REGISTRATION_OVERVIEW_MODEL, + id: 'registry-1', + registrationSchemaLink: 'https://example.com/schema', + }; + + TestBed.resetTestingModule(); + await createTestBed({ registry }).compileComponents(); + const mockStore = TestBed.inject(Store); + const dispatchSpy = jest.spyOn(mockStore, 'dispatch'); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + await new Promise((resolve) => setTimeout(resolve, 200)); + + const calls = dispatchSpy.mock.calls.map((call) => call[0]); + const hasSchemaBlocks = calls.some( + (action) => action instanceof GetSchemaBlocks && (action as any).schemaLink === registry.registrationSchemaLink + ); + const hasSchemaResponses = calls.some( + (action) => action instanceof GetRegistrySchemaResponses && (action as any).registryId === registry.id + ); + expect(hasSchemaBlocks || hasSchemaResponses).toBe(true); + }); + + it('should handle query params for revisionId and mode', async () => { + TestBed.resetTestingModule(); + await createTestBed({ + registry: MOCK_REGISTRATION_OVERVIEW_MODEL, + queryParams: { revisionId: 'revision-1', mode: 'moderator' }, + }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + await new Promise((resolve) => setTimeout(resolve, 100)); + + expect(component.revisionId).toBe('revision-1'); + expect(component.isModeration).toBe(true); + }); + }); + + describe('State-based Behavior', () => { + it('should handle archiving registry', async () => { + const archivingRegistry = { ...MOCK_REGISTRATION_OVERVIEW_MODEL, archiving: true }; + + TestBed.resetTestingModule(); + await createTestBed({ registry: archivingRegistry }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(component.showToolbar()).toBe(false); + expect(component.canMakeDecision()).toBe(false); + }); + + it('should handle withdrawn registry', async () => { + const withdrawnRegistry = { ...MOCK_REGISTRATION_OVERVIEW_MODEL, withdrawn: true }; + + TestBed.resetTestingModule(); + await createTestBed({ registry: withdrawnRegistry }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(component.showToolbar()).toBe(false); + expect(component.canMakeDecision()).toBe(false); + }); + + it('should handle different review states', async () => { + const pendingRegistry = { + ...MOCK_REGISTRATION_OVERVIEW_MODEL, + reviewsState: RegistrationReviewStates.Pending, + }; + + TestBed.resetTestingModule(); + await createTestBed({ registry: pendingRegistry }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + fixture.detectChanges(); + + expect(component.isInitialState()).toBe(false); + }); + + it('should find revision in progress from schema responses', async () => { + const schemaResponses = [ + createMockSchemaResponse('revision-1', RevisionReviewStates.Approved), + createMockSchemaResponse('revision-2', RevisionReviewStates.RevisionInProgress), + createMockSchemaResponse('revision-3', RevisionReviewStates.Approved), + ]; + + TestBed.resetTestingModule(); + await createTestBed({ + registry: MOCK_REGISTRATION_OVERVIEW_MODEL, + schemaResponses, + }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + component.openRevision(1); + fixture.detectChanges(); + + expect(component.revisionInProgress?.id).toBe('revision-2'); + }); + }); + + describe('SSR', () => { + beforeEach(async () => { + TestBed.resetTestingModule(); + await createTestBed({ + isSSR: true, + registryId: 'registry-1', + registry: MOCK_REGISTRATION_OVERVIEW_MODEL, + }).compileComponents(); + fixture = TestBed.createComponent(RegistryOverviewComponent); + component = fixture.componentInstance; + }); + + it('should render server-side without errors', () => { + expect(() => { + fixture.detectChanges(); + }).not.toThrow(); + expect(component).toBeTruthy(); + }); + + it('should not access browser-only APIs during SSR', () => { + const platformId = TestBed.inject(PLATFORM_ID); + expect(platformId).toBe('server'); + fixture.detectChanges(); + expect(component).toBeTruthy(); + }); + + it('should execute constructor effects without errors in SSR context', () => { + const dispatchSpy = jest.spyOn(store, 'dispatch'); + fixture.detectChanges(); + expect(dispatchSpy).toHaveBeenCalled(); + expect(component).toBeTruthy(); + }); }); }); From 3d91edfb3ec8fb7f3b48f6c6b83126951c107b08 Mon Sep 17 00:00:00 2001 From: nsemets Date: Mon, 8 Dec 2025 16:42:36 +0200 Subject: [PATCH 11/11] fix(ssr): updated ssr configs --- angular.json | 18 ++++++++---------- src/app/app.config.ts | 4 ++-- src/app/core/interceptors/auth.interceptor.ts | 5 ++++- src/app/core/interceptors/error.interceptor.ts | 4 +++- 4 files changed, 17 insertions(+), 14 deletions(-) diff --git a/angular.json b/angular.json index 73c15b127..b87b677be 100644 --- a/angular.json +++ b/angular.json @@ -151,29 +151,27 @@ }, "serve": { "builder": "@angular-devkit/build-angular:dev-server", + "options": { + "hmr": false + }, "configurations": { "production": { "buildTarget": "osf:build:production" }, "development": { - "buildTarget": "osf:build:development", - "hmr": true + "buildTarget": "osf:build:development" }, "docker": { - "buildTarget": "osf:build:docker", - "hmr": true + "buildTarget": "osf:build:docker" }, "staging": { - "buildTarget": "osf:build:staging", - "hmr": true + "buildTarget": "osf:build:staging" }, "test": { - "buildTarget": "osf:build:test", - "hmr": false + "buildTarget": "osf:build:test" }, "test-osf": { - "buildTarget": "osf:build:test-osf", - "hmr": false + "buildTarget": "osf:build:test-osf" } }, "defaultConfiguration": "development" diff --git a/src/app/app.config.ts b/src/app/app.config.ts index 2926885c4..8c68eae0a 100644 --- a/src/app/app.config.ts +++ b/src/app/app.config.ts @@ -6,7 +6,7 @@ import { ConfirmationService, MessageService } from 'primeng/api'; import { providePrimeNG } from 'primeng/config'; import { DialogService } from 'primeng/dynamicdialog'; -import { provideHttpClient, withFetch, withInterceptors } from '@angular/common/http'; +import { provideHttpClient, withInterceptors } from '@angular/common/http'; import { ApplicationConfig, ErrorHandler, importProvidersFrom, provideZoneChangeDetection } from '@angular/core'; import { provideClientHydration, withEventReplay } from '@angular/platform-browser'; import { provideAnimations } from '@angular/platform-browser/animations'; @@ -49,7 +49,7 @@ export const appConfig: ApplicationConfig = { }, }, }), - provideHttpClient(withFetch(), withInterceptors([authInterceptor, viewOnlyInterceptor, errorInterceptor])), + provideHttpClient(withInterceptors([authInterceptor, viewOnlyInterceptor, errorInterceptor])), provideRouter(routes, withInMemoryScrolling({ scrollPositionRestoration: 'top', anchorScrolling: 'enabled' })), provideStore(STATES), provideZoneChangeDetection({ eventCoalescing: true }), diff --git a/src/app/core/interceptors/auth.interceptor.ts b/src/app/core/interceptors/auth.interceptor.ts index d73511118..0ed51e98f 100644 --- a/src/app/core/interceptors/auth.interceptor.ts +++ b/src/app/core/interceptors/auth.interceptor.ts @@ -39,7 +39,10 @@ export const authInterceptor: HttpInterceptorFn = ( if (serverRequest) { const cookieHeader = serverRequest.headers.get('cookie') || ''; if (cookieHeader) { - headers['Cookie'] = cookieHeader; + if (isPlatformBrowser(platformId)) { + headers['Cookie'] = cookieHeader; + } + const csrfMatch = cookieHeader.match(/api-csrf=([^;]+)/); if (csrfMatch) { headers['X-CSRFToken'] = csrfMatch[1]; diff --git a/src/app/core/interceptors/error.interceptor.ts b/src/app/core/interceptors/error.interceptor.ts index b9717ad71..b87629369 100644 --- a/src/app/core/interceptors/error.interceptor.ts +++ b/src/app/core/interceptors/error.interceptor.ts @@ -54,7 +54,9 @@ export const errorInterceptor: HttpInterceptorFn = (req, next) => { if (error.status === 401) { if (!viewOnlyHelper.hasViewOnlyParam(router)) { - authService.logout(); + if (isPlatformBrowser(platformId)) { + authService.logout(); + } } return throwError(() => error); }