diff --git a/docker/artemis/config/playwright.env b/docker/artemis/config/playwright.env index c57d79d581c7..a3ee64a57442 100644 --- a/docker/artemis/config/playwright.env +++ b/docker/artemis/config/playwright.env @@ -27,6 +27,8 @@ ARTEMIS_CONTINUOUSINTEGRATION_EMPTYCOMMITNECESSARY="true" ARTEMIS_APOLLON_CONVERSIONSERVICEURL="https://apollon.ase.in.tum.de/api/converter" +ARTEMIS_TELEMETRY_ENABLED="false" + # Token is valid 3 days JHIPSTER_SECURITY_AUTHENTICATION_JWT_TOKENVALIDITYINSECONDS="259200" # Token is valid 30 days @@ -38,6 +40,7 @@ INFO_IMPRINT="https://ase.in.tum.de/lehrstuhl_1/component/content/article/179-im INFO_TESTSERVER="true" INFO_TEXTASSESSMENTANALYTICSENABLED="true" INFO_STUDENTEXAMSTORESESSIONDATA="true" +INFO_OPERATORNAME="TUM" LOGGING_FILE_NAME="/opt/artemis/data/artemis.log" diff --git a/docs/admin/setup/customization.rst b/docs/admin/setup/customization.rst index e86adbeca56c..2094cc8b5069 100644 --- a/docs/admin/setup/customization.rst +++ b/docs/admin/setup/customization.rst @@ -7,4 +7,5 @@ instead of the TUM defaults: * The logo next to the “Artemis” heading on the navbar → ``${artemisRunDirectory}/public/images/logo.png`` * The favicon → ``${artemisRunDirectory}/logo/favicon.svg`` * The contact email address in the ``application-{dev,prod}.yml`` configuration file under the key ``info.contact`` +* The operator's name (e.g. university) and the operator's contact information (administrator email address and name) can be specified in the ``application-{dev,prod}.yml`` configuration file under the keys ``info.operatorName``, ``info.contact`` and ``info.operatorAdminName``. These values are also displayed on the ``/about`` page. The operator's name is required, while the administrator's name is optional. Artemis also uses this information for the :ref:`telemetry` service. * The maximal number of plagiarism results stored per plagiarism checks in the ``application-{dev,prod}.yml`` configuration file under the key ``artemis.plagiarism-checks.plagiarism-results-limit`` diff --git a/docs/admin/telemetry.rst b/docs/admin/telemetry.rst new file mode 100644 index 000000000000..95c3b8168f3a --- /dev/null +++ b/docs/admin/telemetry.rst @@ -0,0 +1,37 @@ +.. _telemetry: + +Telemetry +========= + +To help to improve Artemis, we collect some data when the application starts. +This feature can be disabled by setting `telemetry.enabled` in the `application-prod.yml` to `false`. +When this is set to false, no data is sent to the Artemis maintainer team. +By setting `telemetry.sendAdminDetails` to false, personal information of the instance's admin (i.e. contact email and name) is excluded from the telemetry data. +This includes the contact email and the administrator's name. + +Artemis collects the following data at the startup of an instance: + +* The used Artemis version +* The contact email address of the admin, which is set in `info.contact` +* The name of the admin, set in `info.operatorAdminName` (optional) +* The server's URL +* The operator's name +* The used profiles (e.g. Gitlab, Jenkins, LocalVC, Aeolus, ...) + +Example configuration in `application-prod.yml`: + +.. code-block:: yaml + + artemis: + telemetry: + enabled: true + sendAdminDetails: false + destination: telemetry.artemis.cit.tum.de + + info: + contact: contactMailAddress@cit.tum.de + operatorName: Technical University of Munich + operatorAdminName: Stephan Krusche + +We collect this data to enhance Artemis by understanding how it is used, ensuring compatibility with different configurations, and providing better support to our users. +Collecting admin contact information allows us to communicate important updates or address critical issues directly. diff --git a/docs/index.rst b/docs/index.rst index 64cd387ed5e1..32c130434e4f 100644 --- a/docs/index.rst +++ b/docs/index.rst @@ -77,6 +77,7 @@ All these exercises are supposed to be run either live in the lecture with insta admin/database admin/knownIssues admin/benchmarking-tool + admin/telemetry .. toctree:: diff --git a/package-lock.json b/package-lock.json index 0867dc1f015b..f05b86d4cc8f 100644 --- a/package-lock.json +++ b/package-lock.json @@ -10,18 +10,18 @@ "hasInstallScript": true, "license": "MIT", "dependencies": { - "@angular/animations": "18.2.2", - "@angular/cdk": "18.2.2", - "@angular/common": "18.2.2", - "@angular/compiler": "18.2.2", - "@angular/core": "18.2.2", - "@angular/forms": "18.2.2", - "@angular/localize": "18.2.2", - "@angular/material": "18.2.2", - "@angular/platform-browser": "18.2.2", - "@angular/platform-browser-dynamic": "18.2.2", - "@angular/router": "18.2.2", - "@angular/service-worker": "18.2.2", + "@angular/animations": "18.2.3", + "@angular/cdk": "18.2.3", + "@angular/common": "18.2.3", + "@angular/compiler": "18.2.3", + "@angular/core": "18.2.3", + "@angular/forms": "18.2.3", + "@angular/localize": "18.2.3", + "@angular/material": "18.2.3", + "@angular/platform-browser": "18.2.3", + "@angular/platform-browser-dynamic": "18.2.3", + "@angular/router": "18.2.3", + "@angular/service-worker": "18.2.3", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "18.1.0", "@fingerprintjs/fingerprintjs": "4.4.3", @@ -34,7 +34,7 @@ "@ng-bootstrap/ng-bootstrap": "17.0.1", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@sentry/angular": "8.27.0", + "@sentry/angular": "8.28.0", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", "@vscode/codicons": "0.0.36", @@ -43,9 +43,9 @@ "core-js": "3.38.1", "crypto-js": "4.2.0", "dayjs": "1.11.13", - "diff-match-patch-typescript": "1.0.8", + "diff-match-patch-typescript": "1.1.0", "dompurify": "3.1.6", - "export-to-csv": "1.3.0", + "export-to-csv": "1.4.0", "fast-json-patch": "3.1.1", "franc-min": "6.2.0", "html-diff-ts": "1.4.2", @@ -59,7 +59,8 @@ "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", - "posthog-js": "1.160.0", + "pdfjs-dist": "4.6.82", + "posthog-js": "1.160.3", "rxjs": "7.8.1", "showdown": "2.1.0", "showdown-highlight": "3.1.0", @@ -77,33 +78,33 @@ }, "devDependencies": { "@angular-builders/jest": "18.0.0", - "@angular-devkit/build-angular": "18.2.2", + "@angular-devkit/build-angular": "18.2.3", "@angular-eslint/builder": "18.3.0", "@angular-eslint/eslint-plugin": "18.3.0", "@angular-eslint/eslint-plugin-template": "18.3.0", "@angular-eslint/schematics": "18.3.0", "@angular-eslint/template-parser": "18.3.0", - "@angular/cli": "18.2.2", - "@angular/compiler-cli": "18.2.2", - "@angular/language-service": "18.2.2", - "@sentry/types": "8.27.0", + "@angular/cli": "18.2.3", + "@angular/compiler-cli": "18.2.3", + "@angular/language-service": "18.2.3", + "@sentry/types": "8.28.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", "@types/jest": "29.5.12", "@types/lodash-es": "4.17.12", - "@types/node": "22.5.1", + "@types/node": "22.5.4", "@types/papaparse": "5.3.14", "@types/showdown": "2.0.6", "@types/smoothscroll-polyfill": "0.3.4", "@types/sockjs-client": "1.5.4", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.3.0", - "@typescript-eslint/parser": "8.3.0", + "@typescript-eslint/eslint-plugin": "8.4.0", + "@typescript-eslint/parser": "8.4.0", "eslint": "9.9.1", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", - "eslint-plugin-jest": "28.8.1", + "eslint-plugin-jest": "28.8.3", "eslint-plugin-jest-extended": "2.4.0", "eslint-plugin-prettier": "5.2.1", "folder-hash": "4.0.4", @@ -115,10 +116,11 @@ "jest-fail-on-console": "3.3.0", "jest-junit": "16.0.0", "jest-preset-angular": "14.2.2", - "lint-staged": "15.2.9", + "lint-staged": "15.2.10", "ng-mocks": "14.13.1", "prettier": "3.3.3", - "sass": "1.77.8", + "rimraf": "6.0.1", + "sass": "1.78.0", "ts-jest": "29.2.5", "typescript": "5.5.4", "weak-napi": "2.0.2" @@ -209,13 +211,13 @@ } }, "node_modules/@angular-devkit/architect": { - "version": "0.1802.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.2.tgz", - "integrity": "sha512-LPRl9jhcf0NgshaL6RoUy1uL/cAyNt7oxctoZ9EHUu8eh5E9W/jZGhVowjOLpirwqYhmEzKJJIeS49Ssqs3RQg==", + "version": "0.1802.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/architect/-/architect-0.1802.3.tgz", + "integrity": "sha512-WQ2AmkUKy1bqrDlNfozW8+VT2Tv/Fdmu4GIXps3ytZANyAKiIvTzmmql2cRCXXraa9FNMjLWNvz+qolDxWVdYQ==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.2", + "@angular-devkit/core": "18.2.3", "rxjs": "7.8.1" }, "engines": { @@ -225,17 +227,17 @@ } }, "node_modules/@angular-devkit/build-angular": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.2.tgz", - "integrity": "sha512-7HEnTN2T1jnjuItXKcApOsoYGgfou4+POju3ZbwIQukDZ3B2COskvQkVTxqPNrQ0ZjT2mxZYoVlmGW9M+7N25g==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-angular/-/build-angular-18.2.3.tgz", + "integrity": "sha512-uUQba0SIskKORHcPayt7LpqPRKD//48EW92SgGHEArn2KklM+FSYBOA9OtrJeZ/UAcoJpdLDtvyY4+S7oFzomg==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.2", - "@angular-devkit/build-webpack": "0.1802.2", - "@angular-devkit/core": "18.2.2", - "@angular/build": "18.2.2", + "@angular-devkit/architect": "0.1802.3", + "@angular-devkit/build-webpack": "0.1802.3", + "@angular-devkit/core": "18.2.3", + "@angular/build": "18.2.3", "@babel/core": "7.25.2", "@babel/generator": "7.25.0", "@babel/helper-annotate-as-pure": "7.24.7", @@ -246,7 +248,7 @@ "@babel/preset-env": "7.25.3", "@babel/runtime": "7.25.0", "@discoveryjs/json-ext": "0.6.1", - "@ngtools/webpack": "18.2.2", + "@ngtools/webpack": "18.2.3", "@vitejs/plugin-basic-ssl": "1.1.0", "ansi-colors": "4.1.3", "autoprefixer": "10.4.20", @@ -289,7 +291,7 @@ "vite": "5.4.0", "watchpack": "2.4.1", "webpack": "5.94.0", - "webpack-dev-middleware": "7.3.0", + "webpack-dev-middleware": "7.4.2", "webpack-dev-server": "5.0.4", "webpack-merge": "6.0.1", "webpack-subresource-integrity": "5.1.0" @@ -379,13 +381,13 @@ "license": "0BSD" }, "node_modules/@angular-devkit/build-webpack": { - "version": "0.1802.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.2.tgz", - "integrity": "sha512-Pj+YmKh0nJOKl6QAsqYh3SqfuVJrFqjyp5WrG9BgfsMD9GCMD+5teMHNYJlp+vG/C8e7VdZp4rqOon8K9Xn4Mw==", + "version": "0.1802.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/build-webpack/-/build-webpack-0.1802.3.tgz", + "integrity": "sha512-/Nixv9uAg6v/OPoZa0PB0zi+iezzBkgLrnrJnestny5B536l9WRtsw97RjeQDu+x2BClQsxNe8NL2A7EvjVD6w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.2", + "@angular-devkit/architect": "0.1802.3", "rxjs": "7.8.1" }, "engines": { @@ -399,9 +401,9 @@ } }, "node_modules/@angular-devkit/core": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.2.tgz", - "integrity": "sha512-Zz0tGptI/QQnUBDdp+1G5wGwQWMjpfe2oO+UohkrDVgFS71yVj4VDnOy51kMTxBvzw+36evTgthPpmzqPIfxBw==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/core/-/core-18.2.3.tgz", + "integrity": "sha512-vbFs+ofNK9OWeMIcFarFjegXVklhtSdLTEFKZ9trDVr8alTJdjI9AiYa6OOUTDAyq0hqYxV26xlCisWAPe7s5w==", "dev": true, "license": "MIT", "dependencies": { @@ -427,13 +429,13 @@ } }, "node_modules/@angular-devkit/schematics": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.2.tgz", - "integrity": "sha512-PU6+3nX+gQ3gofR7BGwXuvNUNeeV2raURaZjlPfGpBqjyTBxukMV71QsTTWptAZT4WibCWkTFp6X1gvsOGbjMg==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular-devkit/schematics/-/schematics-18.2.3.tgz", + "integrity": "sha512-N3tRAzBW2yWQhebvc1Ha18XTMSXOQTfr8HNjx7Fasx0Fg1tNyGR612MJNZw6je/PqyItKeUHOhztvFMfCQjRyg==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.2", + "@angular-devkit/core": "18.2.3", "jsonc-parser": "3.3.1", "magic-string": "0.30.11", "ora": "5.4.1", @@ -546,9 +548,9 @@ } }, "node_modules/@angular/animations": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.2.tgz", - "integrity": "sha512-jh/dGrY77HGm54HdTiQsxmvoRfFeJgHeWAK2+nWCPoc4b7OHcWxy/04cYffs0/27ThmABmppP7ERAyZ0f60uow==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/animations/-/animations-18.2.3.tgz", + "integrity": "sha512-rIATopHr83lYR0X05buHeHssq9CGw0I0YPIQcpUTGnlqIpJcQVCf7jCFn4KGZrE9V55hFY3MD4S28njlwCToQQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -557,18 +559,18 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.2" + "@angular/core": "18.2.3" } }, "node_modules/@angular/build": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.2.tgz", - "integrity": "sha512-okaDdTMXnDhvnnnih6rPQnexL6htfEAPr19bB1Ci9d31gEjVuKZCjlcw2sPZ6BUyilwC9nZlCI5vbH1Ljf6mzA==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/build/-/build-18.2.3.tgz", + "integrity": "sha512-USrD2Zvcb1te2dnqhH7JZ5XeJDg/t7fjUHR4f93vvMrnrncwCjLoHbHpz01HCHfcIVRgsYUdAmAi1iG7vpak7w==", "dev": true, "license": "MIT", "dependencies": { "@ampproject/remapping": "2.3.0", - "@angular-devkit/architect": "0.1802.2", + "@angular-devkit/architect": "0.1802.3", "@babel/core": "7.25.2", "@babel/helper-annotate-as-pure": "7.24.7", "@babel/helper-split-export-declaration": "7.24.7", @@ -648,9 +650,9 @@ } }, "node_modules/@angular/cdk": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.2.tgz", - "integrity": "sha512-+u7ZcMA24WO03vDzlBJJWq+okZLFDeW9JrtHzrdiT09FDt4sdUp+7PddXaZcRHIXjJL+CaCLQ6slaqPNEufqgg==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/cdk/-/cdk-18.2.3.tgz", + "integrity": "sha512-lUcpYTxPZuntJ1FK7V2ugapCGMIhT6TUDjIGgXfS9AxGSSKgwr8HNs6Ze9pcjYC44UhP40sYAZuiaFwmE60A2A==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -665,18 +667,18 @@ } }, "node_modules/@angular/cli": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.2.tgz", - "integrity": "sha512-HVVaMxnbID0q+V3KE+JqzGbPHcBUFo1RKhBZ/jxY7USZNzgtyYbRc0IYqPWNdr99UT5QefTJrjVazJo1nqQZvQ==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/cli/-/cli-18.2.3.tgz", + "integrity": "sha512-40258vuliH6+p8QSByZe5EcIXSj0iR3PNF6yuusClR/ByToHOnmuPw7WC+AYr0ooozmqlim/EjQe4/037OUB3w==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/architect": "0.1802.2", - "@angular-devkit/core": "18.2.2", - "@angular-devkit/schematics": "18.2.2", + "@angular-devkit/architect": "0.1802.3", + "@angular-devkit/core": "18.2.3", + "@angular-devkit/schematics": "18.2.3", "@inquirer/prompts": "5.3.8", "@listr2/prompt-adapter-inquirer": "2.0.15", - "@schematics/angular": "18.2.2", + "@schematics/angular": "18.2.3", "@yarnpkg/lockfile": "1.1.0", "ini": "4.1.3", "jsonc-parser": "3.3.1", @@ -699,9 +701,9 @@ } }, "node_modules/@angular/common": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.2.tgz", - "integrity": "sha512-AQe4xnnNNch/sXRnV82C8FmhijxPATKfPGojC2qbAG2o6VkWKgt5Lbj0O8WxvSIOS5Syedv+O2kLY/JMGWHNtw==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/common/-/common-18.2.3.tgz", + "integrity": "sha512-NFL4yXXImSCH7i1xnHykUjHa9vl9827fGiwSV2mnf7LjSUsyDzFD8/54dNuYN9OY8AUD+PnK0YdNro6cczVyIA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -710,14 +712,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.2", + "@angular/core": "18.2.3", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/compiler": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.2.tgz", - "integrity": "sha512-gmVNCXZiv/CIk2eKRLnH19N9VsPuE2s3Oxm0MNi003zk1cLy7D4YEm4fSrjKXtPY8MMpRXiu5f63W94hLwWEVw==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/compiler/-/compiler-18.2.3.tgz", + "integrity": "sha512-Il3ljs0j1GaYoqYFdShjUP1ryck5xTOaA8uQuRgqwU0FOwEDfugSAM3Qf7nJx/sgxTM0Lm/Nrdv2u6i1gZWeuQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -726,7 +728,7 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/core": "18.2.2" + "@angular/core": "18.2.3" }, "peerDependenciesMeta": { "@angular/core": { @@ -735,9 +737,9 @@ } }, "node_modules/@angular/compiler-cli": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.2.tgz", - "integrity": "sha512-fF7lDrTA12YGqVjF4LyMi4hm58cv9G6CWmzSlvun0nMYCwrbRNnakZsj19dOfiIqqu4MwHaF4w3PTmUSxkMuiw==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/compiler-cli/-/compiler-cli-18.2.3.tgz", + "integrity": "sha512-BcmqYKnkcJTkGjuPztClZNQve7tdI290J5F3iZBx6c7/vaw8EU8EGZtpWYZpgiVn5S6jhcKyc1dLF9ggO9vftg==", "license": "MIT", "dependencies": { "@babel/core": "7.25.2", @@ -758,14 +760,14 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.2", + "@angular/compiler": "18.2.3", "typescript": ">=5.4 <5.6" } }, "node_modules/@angular/core": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.2.tgz", - "integrity": "sha512-Rx6XajL0Ydj9hXUSPDvL2Q/kMzWtbiE3VxZFJnkE+fLQiWvr0GncB+NTb/nQ6QlPQ0ly60DvuI3KLcGDuFtGVA==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/core/-/core-18.2.3.tgz", + "integrity": "sha512-VGhMJxj7d0rYpqVfQrcGRB7EE/BCziotft/I/YPl6bOMPSAvMukG7DXQuJdYpNrr62ks78mlzHlZX/cdmB9Prw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -779,9 +781,9 @@ } }, "node_modules/@angular/forms": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.2.tgz", - "integrity": "sha512-K8cv0w6o7+ocQfUrdSA3XaKrYfa1+2TlmtyxPHjEd2mCu2R+Yqo5RqJ3P8keFewJ1+bSLhz6xnn6mumwl0RnUQ==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/forms/-/forms-18.2.3.tgz", + "integrity": "sha512-+OBaAH0e8hue9eyLnbgpxg1/X9fps6bwXECfJ0nL5BDPU5itZ428YJbEnj5bTx0hEbqfTRiV4LgexdI+D9eOpw==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -790,16 +792,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.2", - "@angular/core": "18.2.2", - "@angular/platform-browser": "18.2.2", + "@angular/common": "18.2.3", + "@angular/core": "18.2.3", + "@angular/platform-browser": "18.2.3", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/language-service": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.2.tgz", - "integrity": "sha512-aROQNQeLf+o+F5OVvE/9BUe/Tpv8pjzmrZlogBbic5cb4IqSNhR4RjxbgIyXBO/6bhLCZwqfmMqRbW2J2xqMkg==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/language-service/-/language-service-18.2.3.tgz", + "integrity": "sha512-bTZ1O7s0uJqKdd9ImCleRS9Wg6yVy2ZXchnS5ap2gYJx51MJgwOM/fL6is0OsovtZG/UJaKK5FeEqUUxNqZJVA==", "dev": true, "license": "MIT", "engines": { @@ -807,9 +809,9 @@ } }, "node_modules/@angular/localize": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.2.tgz", - "integrity": "sha512-grWQ3CVbizOWCthGpyIlNNnZCpF/xpWYa6tIsPzKOXLCyqFQ7vOEtSludNN1nsUmMlZQt76+wA17Fx0qcNx0EA==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/localize/-/localize-18.2.3.tgz", + "integrity": "sha512-ZTliuRfH/hGwQTmFb1FwKOyMUks2ATuFVFzKnxbsxoo+XgTg+e12FcUfPEfdtPAteZ9gSuc/9hP8sM0RzW0LPg==", "license": "MIT", "dependencies": { "@babel/core": "7.25.2", @@ -826,21 +828,21 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/compiler": "18.2.2", - "@angular/compiler-cli": "18.2.2" + "@angular/compiler": "18.2.3", + "@angular/compiler-cli": "18.2.3" } }, "node_modules/@angular/material": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.2.tgz", - "integrity": "sha512-c+EQo1GEvM2w3qasgV/BGxB0bpJeSGs2WcMVTXCYVMcqEk8nwpALwfZiCAYl8JoKoiC5k993zz19xP2Eu14qkQ==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/material/-/material-18.2.3.tgz", + "integrity": "sha512-JFfvXaMHMhskncaxxus4sDvie9VYdMkfYgfinkLXpZlPFyn1IzjDw0c1BcrcsuD7UxQVZ/v5tucCgq1FQfGRpA==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" }, "peerDependencies": { "@angular/animations": "^18.0.0 || ^19.0.0", - "@angular/cdk": "18.2.2", + "@angular/cdk": "18.2.3", "@angular/common": "^18.0.0 || ^19.0.0", "@angular/core": "^18.0.0 || ^19.0.0", "@angular/forms": "^18.0.0 || ^19.0.0", @@ -849,9 +851,9 @@ } }, "node_modules/@angular/platform-browser": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.2.tgz", - "integrity": "sha512-Bfvl8elCFxyJ9vlwamr4X5sVMcp/tSwBal2coyl0WR+/PH2PAAtf+/WMYxIN90yZmPiJx6RZWUSJRlHOFiFp3A==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser/-/platform-browser-18.2.3.tgz", + "integrity": "sha512-M2ob4zN7tAcL2mx7U6KnZNqNFPFl9MlPBE0FrjQjIzAjU0wSYPIJXmaPu9aMUp9niyo+He5iX98I+URi2Yc99g==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -860,9 +862,9 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/animations": "18.2.2", - "@angular/common": "18.2.2", - "@angular/core": "18.2.2" + "@angular/animations": "18.2.3", + "@angular/common": "18.2.3", + "@angular/core": "18.2.3" }, "peerDependenciesMeta": { "@angular/animations": { @@ -871,9 +873,9 @@ } }, "node_modules/@angular/platform-browser-dynamic": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.2.tgz", - "integrity": "sha512-UM/+1nY4iIj1v4lxAmV3XRHPAh/4qfNKScCLq8tJGot64rPCbtCl0Rl8rFFGqxAFvTErVDaJycUgWNZSfVl/hw==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/platform-browser-dynamic/-/platform-browser-dynamic-18.2.3.tgz", + "integrity": "sha512-nWi9ZxN4KpbJkttIckFO1PCoW0+gb/18xFO+JWyLBAtcbsudj/Mv0P/fdOaSfQdLkPhZfORr3ZcfiTkhmuGyEg==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -882,16 +884,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.2", - "@angular/compiler": "18.2.2", - "@angular/core": "18.2.2", - "@angular/platform-browser": "18.2.2" + "@angular/common": "18.2.3", + "@angular/compiler": "18.2.3", + "@angular/core": "18.2.3", + "@angular/platform-browser": "18.2.3" } }, "node_modules/@angular/router": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.2.tgz", - "integrity": "sha512-tBHwuNtZNjzYAoVdveTI1ke/ZnQjKhc7gqDk9HCH2JUpdQhGbTvCKwDM51ktJpPMPcZlA263lQyy7VIyvdtK0A==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/router/-/router-18.2.3.tgz", + "integrity": "sha512-fvD9eSDIiIbeYoUokoWkXzu7/ZaxlzKPUHFqX1JuKuH5ciQDeT/d7lp4mj31Bxammhohzi3+z12THJYsCkj/iQ==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -900,16 +902,16 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.2", - "@angular/core": "18.2.2", - "@angular/platform-browser": "18.2.2", + "@angular/common": "18.2.3", + "@angular/core": "18.2.3", + "@angular/platform-browser": "18.2.3", "rxjs": "^6.5.3 || ^7.4.0" } }, "node_modules/@angular/service-worker": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.2.tgz", - "integrity": "sha512-az0v0gNkAjOQ4DThDWfNJv2DkH63B4Vj/WnXd8pbY/C7Be6w3S1mN2y9vJClWAzUH/GSLQHnOrZJfnZtTc8M0w==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@angular/service-worker/-/service-worker-18.2.3.tgz", + "integrity": "sha512-KplaBYhhwsM3gPeOImfDGhAknN+BIcZJkHl8YRnhoUEFHsTZ8LTV02C4LWQL3YTu3pK+uj/lPMKi1CA37cXQ8g==", "license": "MIT", "dependencies": { "tslib": "^2.3.0" @@ -921,8 +923,8 @@ "node": "^18.19.1 || ^20.11.1 || >=22.0.0" }, "peerDependencies": { - "@angular/common": "18.2.2", - "@angular/core": "18.2.2" + "@angular/common": "18.2.3", + "@angular/core": "18.2.3" } }, "node_modules/@babel/code-frame": { @@ -3559,15 +3561,15 @@ } }, "node_modules/@inquirer/checkbox": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.4.7.tgz", - "integrity": "sha512-5YwCySyV1UEgqzz34gNsC38eKxRBtlRDpJLlKcRtTjlYA/yDKuc1rfw+hjw+2WJxbAZtaDPsRl5Zk7J14SBoBw==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/checkbox/-/checkbox-2.5.0.tgz", + "integrity": "sha512-sMgdETOfi2dUHT8r7TT1BTKOwNvdDGFDXYWtQ2J69SvlYNntk9I/gJe7r5yvMwwsuKnYbuRs3pNhx4tgNck5aA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", + "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -3590,16 +3592,16 @@ } }, "node_modules/@inquirer/core": { - "version": "9.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.0.10.tgz", - "integrity": "sha512-TdESOKSVwf6+YWDz8GhS6nKscwzkIyakEzCLJ5Vh6O3Co2ClhCJ0A4MG909MUWfaWdpJm7DE45ii51/2Kat9tA==", + "version": "9.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/core/-/core-9.1.0.tgz", + "integrity": "sha512-RZVfH//2ytTjmaBIzeKT1zefcQZzuruwkpTwwbe/i2jTl4o9M+iML5ChULzz6iw1Ok8iUBBsRCjY2IEbD8Ft4w==", "dev": true, "license": "MIT", "dependencies": { "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/type": "^1.5.3", "@types/mute-stream": "^0.0.4", - "@types/node": "^22.1.0", + "@types/node": "^22.5.2", "@types/wrap-ansi": "^3.0.0", "ansi-escapes": "^4.3.2", "cli-spinners": "^2.9.2", @@ -3615,14 +3617,14 @@ } }, "node_modules/@inquirer/editor": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.1.22.tgz", - "integrity": "sha512-K1QwTu7GCK+nKOVRBp5HY9jt3DXOfPGPr6WRDrPImkcJRelG9UTx2cAtK1liXmibRrzJlTWOwqgWT3k2XnS62w==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/editor/-/editor-2.2.0.tgz", + "integrity": "sha512-9KHOpJ+dIL5SZli8lJ6xdaYLPPzB8xB9GZItg39MBybzhxA16vxmszmQFrRwbOA918WA2rvu8xhDEg/p6LXKbw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "external-editor": "^3.1.0" }, "engines": { @@ -3630,14 +3632,14 @@ } }, "node_modules/@inquirer/expand": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.1.22.tgz", - "integrity": "sha512-wTZOBkzH+ItPuZ3ZPa9lynBsdMp6kQ9zbjVPYEtSBG7UulGjg2kQiAnUjgyG4SlntpTce5bOmXAPvE4sguXjpA==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/expand/-/expand-2.2.0.tgz", + "integrity": "sha512-PD0z1dTRTIlpcnXRMRvdVPfBe10jBf4i7YLBU8tNWDkf3HxqmdymVvqnT8XG+hxQSvqfpJCe13Jv2Iv1eB3bIg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -3655,42 +3657,42 @@ } }, "node_modules/@inquirer/input": { - "version": "2.2.9", - "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.2.9.tgz", - "integrity": "sha512-7Z6N+uzkWM7+xsE+3rJdhdG/+mQgejOVqspoW+w0AbSZnL6nq5tGMEVASaYVWbkoSzecABWwmludO2evU3d31g==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/input/-/input-2.3.0.tgz", + "integrity": "sha512-XfnpCStx2xgh1LIRqPXrTNEEByqQWoxsWYzNRSEUxJ5c6EQlhMogJ3vHKu8aXuTacebtaZzMAHwEL0kAflKOBw==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2" + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" }, "engines": { "node": ">=18" } }, "node_modules/@inquirer/number": { - "version": "1.0.10", - "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.0.10.tgz", - "integrity": "sha512-kWTxRF8zHjQOn2TJs+XttLioBih6bdc5CcosXIzZsrTY383PXI35DuhIllZKu7CdXFi2rz2BWPN9l0dPsvrQOA==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/number/-/number-1.1.0.tgz", + "integrity": "sha512-ilUnia/GZUtfSZy3YEErXLJ2Sljo/mf9fiKc08n18DdwdmDbOzRcTv65H1jjDvlsAuvdFXf4Sa/aL7iw/NanVA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2" + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3" }, "engines": { "node": ">=18" } }, "node_modules/@inquirer/password": { - "version": "2.1.22", - "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.1.22.tgz", - "integrity": "sha512-5Fxt1L9vh3rAKqjYwqsjU4DZsEvY/2Gll+QkqR4yEpy6wvzLxdSgFhUcxfDAOtO4BEoTreWoznC0phagwLU5Kw==", + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@inquirer/password/-/password-2.2.0.tgz", + "integrity": "sha512-5otqIpgsPYIshqhgtEwSspBQE40etouR8VIxzpJkv9i0dVHIpyhiivbkH9/dGiMLdyamT54YRdGJLfl8TFnLHg==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2" }, "engines": { @@ -3720,14 +3722,14 @@ } }, "node_modules/@inquirer/rawlist": { - "version": "2.2.4", - "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.2.4.tgz", - "integrity": "sha512-pb6w9pWrm7EfnYDgQObOurh2d2YH07+eDo3xQBsNAM2GRhliz6wFXGi1thKQ4bN6B0xDd6C3tBsjdr3obsCl3Q==", + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@inquirer/rawlist/-/rawlist-2.3.0.tgz", + "integrity": "sha512-zzfNuINhFF7OLAtGHfhwOW2TlYJyli7lOUoJUXw/uyklcwalV6WRXBXtFIicN8rTRK1XTiPWB4UY+YuW8dsnLQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", - "@inquirer/type": "^1.5.2", + "@inquirer/core": "^9.1.0", + "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -3735,15 +3737,15 @@ } }, "node_modules/@inquirer/search": { - "version": "1.0.7", - "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.0.7.tgz", - "integrity": "sha512-p1wpV+3gd1eST/o5N3yQpYEdFNCzSP0Klrl+5bfD3cTTz8BGG6nf4Z07aBW0xjlKIj1Rp0y3x/X4cZYi6TfcLw==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/@inquirer/search/-/search-1.1.0.tgz", + "integrity": "sha512-h+/5LSj51dx7hp5xOn4QFnUaKeARwUCLs6mIhtkJ0JYPBLmEYjdHSYh7I6GrLg9LwpJ3xeX0FZgAG1q0QdCpVQ==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", + "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/type": "^1.5.3", "yoctocolors-cjs": "^2.1.2" }, "engines": { @@ -3751,15 +3753,15 @@ } }, "node_modules/@inquirer/select": { - "version": "2.4.7", - "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.4.7.tgz", - "integrity": "sha512-JH7XqPEkBpNWp3gPCqWqY8ECbyMoFcCZANlL6pV9hf59qK6dGmkOlx1ydyhY+KZ0c5X74+W6Mtp+nm2QX0/MAQ==", + "version": "2.5.0", + "resolved": "https://registry.npmjs.org/@inquirer/select/-/select-2.5.0.tgz", + "integrity": "sha512-YmDobTItPP3WcEI86GvPo+T2sRHkxxOq/kXmsBjHS5BVXUgvgZ5AfJjkvQvZr03T81NnI3KrrRuMzeuYUQRFOA==", "dev": true, "license": "MIT", "dependencies": { - "@inquirer/core": "^9.0.10", + "@inquirer/core": "^9.1.0", "@inquirer/figures": "^1.0.5", - "@inquirer/type": "^1.5.2", + "@inquirer/type": "^1.5.3", "ansi-escapes": "^4.3.2", "yoctocolors-cjs": "^2.1.2" }, @@ -3768,9 +3770,9 @@ } }, "node_modules/@inquirer/type": { - "version": "1.5.2", - "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.2.tgz", - "integrity": "sha512-w9qFkumYDCNyDZmNQjf/n6qQuvQ4dMC3BJesY4oF+yr0CxR5vxujflAVeIcS6U336uzi9GM0kAfZlLrZ9UTkpA==", + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@inquirer/type/-/type-1.5.3.tgz", + "integrity": "sha512-xUQ14WQGR/HK5ei+2CvgcwoH9fQ4PgPGmVFSN0pc1+fVyDL3MREhyAY7nxEErSu6CkllBM3D7e3e+kOvtu+eIg==", "dev": true, "license": "MIT", "dependencies": { @@ -5010,6 +5012,93 @@ "uuid": "dist/bin/uuid" } }, + "node_modules/@mapbox/node-pre-gyp": { + "version": "1.0.11", + "resolved": "https://registry.npmjs.org/@mapbox/node-pre-gyp/-/node-pre-gyp-1.0.11.tgz", + "integrity": "sha512-Yhlar6v9WQgUp/He7BdgzOz8lqMQ8sU+jkCq7Wx8Myc5YFJLbEe7lgui/V7G1qB1DJykHSGwreceSaD60Y0PUQ==", + "license": "BSD-3-Clause", + "optional": true, + "dependencies": { + "detect-libc": "^2.0.0", + "https-proxy-agent": "^5.0.0", + "make-dir": "^3.1.0", + "node-fetch": "^2.6.7", + "nopt": "^5.0.0", + "npmlog": "^5.0.1", + "rimraf": "^3.0.2", + "semver": "^7.3.5", + "tar": "^6.1.11" + }, + "bin": { + "node-pre-gyp": "bin/node-pre-gyp" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/abbrev": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/abbrev/-/abbrev-1.1.1.tgz", + "integrity": "sha512-nne9/IiQ/hzIhY6pdDnbBtz7DjPTKrY00P/zvPSm5pOFkl6xuGrGnXn/VtTNNfNtAfZ9/1RtehkszU9qcTii0Q==", + "license": "ISC", + "optional": true + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/agent-base": { + "version": "6.0.2", + "resolved": "https://registry.npmjs.org/agent-base/-/agent-base-6.0.2.tgz", + "integrity": "sha512-RZNwNclF7+MS/8bDg70amg32dyeZGZxiDuQmZxKLAlQjr3jGyLx+4Kkk58UO7D2QdgFIQCovuSuZESne6RG6XQ==", + "license": "MIT", + "optional": true, + "dependencies": { + "debug": "4" + }, + "engines": { + "node": ">= 6.0.0" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/https-proxy-agent": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/https-proxy-agent/-/https-proxy-agent-5.0.1.tgz", + "integrity": "sha512-dFcAjpTQFgoLMzC2VwU+C/CbS7uRL0lWmxDITmqm7C+7F0Odmj6s9l6alZc6AELXhrnggM2CeWSXHGOdX2YtwA==", + "license": "MIT", + "optional": true, + "dependencies": { + "agent-base": "6", + "debug": "4" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/make-dir": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/make-dir/-/make-dir-3.1.0.tgz", + "integrity": "sha512-g3FeP20LNwhALb/6Cz6Dd4F2ngze0jz7tbzrD2wAV+o9FeNHe4rL+yK2md0J/fiSf1sa1ADhXqi5+oVwOM/eGw==", + "license": "MIT", + "optional": true, + "dependencies": { + "semver": "^6.0.0" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/@mapbox/node-pre-gyp/node_modules/nopt": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/nopt/-/nopt-5.0.0.tgz", + "integrity": "sha512-Tbj67rffqceeLpcRXrT7vKAN8CwfPeIBgM7E6iBkmKLV7bEMwpGgYLGv0jACUsECaa/vuxP0IjEont6umdMgtQ==", + "license": "ISC", + "optional": true, + "dependencies": { + "abbrev": "1" + }, + "bin": { + "nopt": "bin/nopt.js" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/@msgpackr-extract/msgpackr-extract-darwin-arm64": { "version": "3.0.3", "resolved": "https://registry.npmjs.org/@msgpackr-extract/msgpackr-extract-darwin-arm64/-/msgpackr-extract-darwin-arm64-3.0.3.tgz", @@ -5112,9 +5201,9 @@ } }, "node_modules/@ngtools/webpack": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.2.tgz", - "integrity": "sha512-YhADmc+lVjLt3kze07A+yLry2yzcghdclu+7D3EDfa6fG2Pk33HK3MY2I0Z0BO+Ivoq7cV7yxm+naR+Od0Y5ng==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@ngtools/webpack/-/webpack-18.2.3.tgz", + "integrity": "sha512-DDuBHcu23qckt43SexBJaPEIeMc/HKaFOidILZM9D4gU4C9VroMActdR218dvQ802QfL0S46t5Ykz8ENprIfjA==", "dev": true, "license": "MIT", "engines": { @@ -5759,14 +5848,14 @@ ] }, "node_modules/@schematics/angular": { - "version": "18.2.2", - "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.2.tgz", - "integrity": "sha512-0uPA1kQ38RnbNrzMlveX/QAqQIDu2INl5IYd3EUbJZRfYSp1VVyOSyuIBJ+1iUl5Y5VUa2uylaVZXhFdKWprXw==", + "version": "18.2.3", + "resolved": "https://registry.npmjs.org/@schematics/angular/-/angular-18.2.3.tgz", + "integrity": "sha512-whSON70z9HYb4WboVXmPFE/RLKJJQLWNzNcUyi8OSDZkQbJnYgPp0///n738m26Y/XeJDv11q1gESy+Zl2AdUw==", "dev": true, "license": "MIT", "dependencies": { - "@angular-devkit/core": "18.2.2", - "@angular-devkit/schematics": "18.2.2", + "@angular-devkit/core": "18.2.3", + "@angular-devkit/schematics": "18.2.3", "jsonc-parser": "3.3.1" }, "engines": { @@ -5776,73 +5865,73 @@ } }, "node_modules/@sentry-internal/browser-utils": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.27.0.tgz", - "integrity": "sha512-YTIwQ1GM1NTRXgN4DvpFSQ2x4pjlqQ0FQAyHW5x2ZYv4z7VmqG4Xkid1P/srQUipECk6nxkebfD4WR19nLsvnQ==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/browser-utils/-/browser-utils-8.28.0.tgz", + "integrity": "sha512-tE9++KEy8SlqibTmYymuxFVAnutsXBqrwQ936WJbjaMfkqXiro7C1El0ybkprskd0rKS7kln20Q6nQlNlMEoTA==", "license": "MIT", "dependencies": { - "@sentry/core": "8.27.0", - "@sentry/types": "8.27.0", - "@sentry/utils": "8.27.0" + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/feedback": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.27.0.tgz", - "integrity": "sha512-b71PQc9aK1X9b/SO1DiJlrnAEx4n0MzPZQ/tKd9oRWDyGit6pJWZfQns9r2rvc96kJPMOTxFAa/upXRCkA723A==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/feedback/-/feedback-8.28.0.tgz", + "integrity": "sha512-5vYunPCDBLCJ8QNnhepacdYheiN+UtYxpGAIaC/zjBC1nDuBgWs+TfKPo1UlO/1sesfgs9ibpxtShOweucL61g==", "license": "MIT", "dependencies": { - "@sentry/core": "8.27.0", - "@sentry/types": "8.27.0", - "@sentry/utils": "8.27.0" + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.27.0.tgz", - "integrity": "sha512-Ofucncaon98dvlxte2L//hwuG9yILSxNrTz/PmO0k+HzB9q+oBic4667QF+azWR2qv4oKSWpc+vEovP3hVqveA==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay/-/replay-8.28.0.tgz", + "integrity": "sha512-70jvzzOL5O74gahgXKyRkZgiYN93yly5gq+bbj4/6NRQ+EtPd285+ccy0laExdfyK0ugvvwD4v+1MQit52OAsg==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.27.0", - "@sentry/core": "8.27.0", - "@sentry/types": "8.27.0", - "@sentry/utils": "8.27.0" + "@sentry-internal/browser-utils": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry-internal/replay-canvas": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.27.0.tgz", - "integrity": "sha512-uuEfiWbjwugB9M4KxXxovHYiKRqg/R6U4EF8xM/Ub4laUuEcWsfRp7lQ3MxL3qYojbca8ncIFic2bIoKMPeejA==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry-internal/replay-canvas/-/replay-canvas-8.28.0.tgz", + "integrity": "sha512-RfpYHDHMUKGeEdx41QtHITjEn6P3tGaDPHvatqdrD3yv4j+wbJ6laX1PrIxCpGFUtjdzkqi/KUcvUd2kzbH/FA==", "license": "MIT", "dependencies": { - "@sentry-internal/replay": "8.27.0", - "@sentry/core": "8.27.0", - "@sentry/types": "8.27.0", - "@sentry/utils": "8.27.0" + "@sentry-internal/replay": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/angular": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.27.0.tgz", - "integrity": "sha512-0BjjrqnVMofVbQGEwfZgYAZWFl4ewkWRjcUj+NIX4iJpRZZniKZxo6XOlo/pTkt4oVHsbNHJO0C1tS+gRZFErg==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry/angular/-/angular-8.28.0.tgz", + "integrity": "sha512-zHl0OSgBsHnQCINepRxYDsosvKnwJPc9tdRJyIgQ6JCG1kWZf0lHncXRnJBkBSrJk2wJQ0acondhwHRyAptRGg==", "license": "MIT", "dependencies": { - "@sentry/browser": "8.27.0", - "@sentry/core": "8.27.0", - "@sentry/types": "8.27.0", - "@sentry/utils": "8.27.0", + "@sentry/browser": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0", "tslib": "^2.4.1" }, "engines": { @@ -5856,52 +5945,52 @@ } }, "node_modules/@sentry/browser": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.27.0.tgz", - "integrity": "sha512-eL1eaHwoYUGkp4mpeYesH6WtCrm+0u9jYCW5Lm0MAeTmpx22BZKEmj0OljuUJXGnJwFbvPDlRjyz6QG11m8kZA==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry/browser/-/browser-8.28.0.tgz", + "integrity": "sha512-i/gjMYzIGQiPFH1pCbdnTwH9xs9mTAqzN+goP3GWX5a58frc7h8vxyA/5z0yMd0aCW6U8mVxnoAT72vGbKbx0g==", "license": "MIT", "dependencies": { - "@sentry-internal/browser-utils": "8.27.0", - "@sentry-internal/feedback": "8.27.0", - "@sentry-internal/replay": "8.27.0", - "@sentry-internal/replay-canvas": "8.27.0", - "@sentry/core": "8.27.0", - "@sentry/types": "8.27.0", - "@sentry/utils": "8.27.0" + "@sentry-internal/browser-utils": "8.28.0", + "@sentry-internal/feedback": "8.28.0", + "@sentry-internal/replay": "8.28.0", + "@sentry-internal/replay-canvas": "8.28.0", + "@sentry/core": "8.28.0", + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/core": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.27.0.tgz", - "integrity": "sha512-4frlXluHT3Du+Omw91K04jpvbfMtydvg4Bxj2+gt/DT19Swhm/fbEpzdUjgbAd3Jinj/n0qk/jFRXjr9JZKFjg==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry/core/-/core-8.28.0.tgz", + "integrity": "sha512-+If9uubvpZpvaQQw4HLiKPhrSS9/KcoA/AcdQkNm+5CVwAoOmDPtyYfkPBgfo2hLZnZQqR1bwkz/PrNoOm+gqA==", "license": "MIT", "dependencies": { - "@sentry/types": "8.27.0", - "@sentry/utils": "8.27.0" + "@sentry/types": "8.28.0", + "@sentry/utils": "8.28.0" }, "engines": { "node": ">=14.18" } }, "node_modules/@sentry/types": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.27.0.tgz", - "integrity": "sha512-B6lrP46+m2x0lfqWc9F4VcUbN893mVGnPEd7KIMRk95mPzkFJ3sNxggTQF5/ZfNO7lDQYQb22uysB5sj/BqFiw==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry/types/-/types-8.28.0.tgz", + "integrity": "sha512-hOfqfd92/AzBrEdMgmmV1VfOXJbIfleFTnerRl0mg/+CcNgP/6+Fdonp354TD56ouWNF2WkOM6sEKSXMWp6SEQ==", "license": "MIT", "engines": { "node": ">=14.18" } }, "node_modules/@sentry/utils": { - "version": "8.27.0", - "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.27.0.tgz", - "integrity": "sha512-gyJM3SyLQe0A3mkQVVNdKYvk3ZoikkYgyA/D+5StFNLKdyUgEbJgXOGXrQSSYPF7BSX6Sc5b0KHCglPII0KuKw==", + "version": "8.28.0", + "resolved": "https://registry.npmjs.org/@sentry/utils/-/utils-8.28.0.tgz", + "integrity": "sha512-smhk7PJpvDMQ2DB5p2qn9UeoUHdU41IgjMmS2xklZpa8tjzBTxDeWpGvrX2fuH67D9bAJuLC/XyZjJCHLoEW5g==", "license": "MIT", "dependencies": { - "@sentry/types": "8.27.0" + "@sentry/types": "8.28.0" }, "engines": { "node": ">=14.18" @@ -6320,19 +6409,6 @@ "@types/trusted-types": "*" } }, - "node_modules/@types/eslint": { - "version": "9.6.1", - "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", - "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", - "dev": true, - "license": "MIT", - "optional": true, - "peer": true, - "dependencies": { - "@types/estree": "*", - "@types/json-schema": "*" - } - }, "node_modules/@types/estree": { "version": "1.0.5", "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.5.tgz", @@ -6495,9 +6571,9 @@ } }, "node_modules/@types/node": { - "version": "22.5.1", - "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.1.tgz", - "integrity": "sha512-KkHsxej0j9IW1KKOOAA/XBA0z08UFSrRQHErzEfA3Vgq57eXIMYboIlHJuYIfd+lwCQjtKqUu3UnmKbtUc9yRw==", + "version": "22.5.4", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.5.4.tgz", + "integrity": "sha512-FDuKUJQm/ju9fT/SeX/6+gBzoPzlVCzfzmGkwKvRHQVxi4BntVbyIwf6a4Xn62mrvndLiml6z/UBXIdEVjQLXg==", "dev": true, "license": "MIT", "dependencies": { @@ -6545,9 +6621,9 @@ "license": "MIT" }, "node_modules/@types/react": { - "version": "18.3.4", - "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.4.tgz", - "integrity": "sha512-J7W30FTdfCxDDjmfRM+/JqLHBIyl7xUIp9kwK637FGmY7+mkSFSe6L4jpZzhj5QMfLssSDP4/i75AKkrdC7/Jw==", + "version": "18.3.5", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.5.tgz", + "integrity": "sha512-WeqMfGJLGuLCqHGYRGHxnKrXcTitc6L/nBUWfWPcTarG3t9PsquqUMuVeXZeca+mglY4Vo5GZjCi0A3Or2lnxA==", "license": "MIT", "dependencies": { "@types/prop-types": "*", @@ -6701,17 +6777,17 @@ "license": "MIT" }, "node_modules/@typescript-eslint/eslint-plugin": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.3.0.tgz", - "integrity": "sha512-FLAIn63G5KH+adZosDYiutqkOkYEx0nvcwNNfJAf+c7Ae/H35qWwTYvPZUKFj5AS+WfHG/WJJfWnDnyNUlp8UA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.4.0.tgz", + "integrity": "sha512-rg8LGdv7ri3oAlenMACk9e+AR4wUV0yrrG+XKsGKOK0EVgeEDqurkXMPILG2836fW4ibokTB5v4b6Z9+GYQDEw==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/regexpp": "^4.10.0", - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/type-utils": "8.3.0", - "@typescript-eslint/utils": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/scope-manager": "8.4.0", + "@typescript-eslint/type-utils": "8.4.0", + "@typescript-eslint/utils": "8.4.0", + "@typescript-eslint/visitor-keys": "8.4.0", "graphemer": "^1.4.0", "ignore": "^5.3.1", "natural-compare": "^1.4.0", @@ -6735,16 +6811,16 @@ } }, "node_modules/@typescript-eslint/parser": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.3.0.tgz", - "integrity": "sha512-h53RhVyLu6AtpUzVCYLPhZGL5jzTD9fZL+SYf/+hYOx2bDkyQXztXSc4tbvKYHzfMXExMLiL9CWqJmVz6+78IQ==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.4.0.tgz", + "integrity": "sha512-NHgWmKSgJk5K9N16GIhQ4jSobBoJwrmURaLErad0qlLjrpP5bECYg+wxVTGlGZmJbU03jj/dfnb6V9bw+5icsA==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/typescript-estree": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/scope-manager": "8.4.0", + "@typescript-eslint/types": "8.4.0", + "@typescript-eslint/typescript-estree": "8.4.0", + "@typescript-eslint/visitor-keys": "8.4.0", "debug": "^4.3.4" }, "engines": { @@ -6764,14 +6840,14 @@ } }, "node_modules/@typescript-eslint/scope-manager": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.3.0.tgz", - "integrity": "sha512-mz2X8WcN2nVu5Hodku+IR8GgCOl4C0G/Z1ruaWN4dgec64kDBabuXyPAr+/RgJtumv8EEkqIzf3X2U5DUKB2eg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.4.0.tgz", + "integrity": "sha512-n2jFxLeY0JmKfUqy3P70rs6vdoPjHK8P/w+zJcV3fk0b0BwRXC/zxRTEnAsgYT7MwdQDt/ZEbtdzdVC+hcpF0A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0" + "@typescript-eslint/types": "8.4.0", + "@typescript-eslint/visitor-keys": "8.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6782,14 +6858,14 @@ } }, "node_modules/@typescript-eslint/type-utils": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.3.0.tgz", - "integrity": "sha512-wrV6qh//nLbfXZQoj32EXKmwHf4b7L+xXLrP3FZ0GOUU72gSvLjeWUl5J5Ue5IwRxIV1TfF73j/eaBapxx99Lg==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.4.0.tgz", + "integrity": "sha512-pu2PAmNrl9KX6TtirVOrbLPLwDmASpZhK/XU7WvoKoCUkdtq9zF7qQ7gna0GBZFN0hci0vHaSusiL2WpsQk37A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/typescript-estree": "8.3.0", - "@typescript-eslint/utils": "8.3.0", + "@typescript-eslint/typescript-estree": "8.4.0", + "@typescript-eslint/utils": "8.4.0", "debug": "^4.3.4", "ts-api-utils": "^1.3.0" }, @@ -6807,9 +6883,9 @@ } }, "node_modules/@typescript-eslint/types": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.3.0.tgz", - "integrity": "sha512-y6sSEeK+facMaAyixM36dQ5NVXTnKWunfD1Ft4xraYqxP0lC0POJmIaL/mw72CUMqjY9qfyVfXafMeaUj0noWw==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.4.0.tgz", + "integrity": "sha512-T1RB3KQdskh9t3v/qv7niK6P8yvn7ja1mS7QK7XfRVL6wtZ8/mFs/FHf4fKvTA0rKnqnYxl/uHFNbnEt0phgbw==", "dev": true, "license": "MIT", "engines": { @@ -6821,14 +6897,14 @@ } }, "node_modules/@typescript-eslint/typescript-estree": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.3.0.tgz", - "integrity": "sha512-Mq7FTHl0R36EmWlCJWojIC1qn/ZWo2YiWYc1XVtasJ7FIgjo0MVv9rZWXEE7IK2CGrtwe1dVOxWwqXUdNgfRCA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.4.0.tgz", + "integrity": "sha512-kJ2OIP4dQw5gdI4uXsaxUZHRwWAGpREJ9Zq6D5L0BweyOrWsL6Sz0YcAZGWhvKnH7fm1J5YFE1JrQL0c9dd53A==", "dev": true, "license": "BSD-2-Clause", "dependencies": { - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/visitor-keys": "8.3.0", + "@typescript-eslint/types": "8.4.0", + "@typescript-eslint/visitor-keys": "8.4.0", "debug": "^4.3.4", "fast-glob": "^3.3.2", "is-glob": "^4.0.3", @@ -6850,16 +6926,16 @@ } }, "node_modules/@typescript-eslint/utils": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.3.0.tgz", - "integrity": "sha512-F77WwqxIi/qGkIGOGXNBLV7nykwfjLsdauRB/DOFPdv6LTF3BHHkBpq81/b5iMPSF055oO2BiivDJV4ChvNtXA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.4.0.tgz", + "integrity": "sha512-swULW8n1IKLjRAgciCkTCafyTHHfwVQFt8DovmaF69sKbOxTSFMmIZaSHjqO9i/RV0wIblaawhzvtva8Nmm7lQ==", "dev": true, "license": "MIT", "dependencies": { "@eslint-community/eslint-utils": "^4.4.0", - "@typescript-eslint/scope-manager": "8.3.0", - "@typescript-eslint/types": "8.3.0", - "@typescript-eslint/typescript-estree": "8.3.0" + "@typescript-eslint/scope-manager": "8.4.0", + "@typescript-eslint/types": "8.4.0", + "@typescript-eslint/typescript-estree": "8.4.0" }, "engines": { "node": "^18.18.0 || ^20.9.0 || >=21.1.0" @@ -6873,13 +6949,13 @@ } }, "node_modules/@typescript-eslint/visitor-keys": { - "version": "8.3.0", - "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.3.0.tgz", - "integrity": "sha512-RmZwrTbQ9QveF15m/Cl28n0LXD6ea2CjkhH5rQ55ewz3H24w+AMCJHPVYaZ8/0HoG8Z3cLLFFycRXxeO2tz9FA==", + "version": "8.4.0", + "resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.4.0.tgz", + "integrity": "sha512-zTQD6WLNTre1hj5wp09nBIDiOc2U5r/qmzo7wxPn4ZgAjHql09EofqhF9WF+fZHzL5aCyaIpPcT2hyxl73kr9A==", "dev": true, "license": "MIT", "dependencies": { - "@typescript-eslint/types": "8.3.0", + "@typescript-eslint/types": "8.4.0", "eslint-visitor-keys": "^3.4.3" }, "engines": { @@ -7350,6 +7426,43 @@ "url": "https://github.com/sponsors/jonschlinkert" } }, + "node_modules/aproba": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/aproba/-/aproba-2.0.0.tgz", + "integrity": "sha512-lYe4Gx7QT+MKGbDsA+Z+he/Wtef0BiwDOlK/XkBrdfsh9J/jPPXbX0tE9x9cl27Tmu5gg3QUbUrQYa/y+KOHPQ==", + "license": "ISC", + "optional": true + }, + "node_modules/are-we-there-yet": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/are-we-there-yet/-/are-we-there-yet-2.0.0.tgz", + "integrity": "sha512-Ci/qENmwHnsYo9xKIcUJN5LeDKdJ6R1Z1j9V/J5wyq8nh/mYPEpIKJbBZXtZjG04HiK7zV/p6Vs9952MrMeUIw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "delegates": "^1.0.0", + "readable-stream": "^3.6.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/are-we-there-yet/node_modules/readable-stream": { + "version": "3.6.2", + "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", + "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "license": "MIT", + "optional": true, + "dependencies": { + "inherits": "^2.0.3", + "string_decoder": "^1.1.1", + "util-deprecate": "^1.0.1" + }, + "engines": { + "node": ">= 6" + } + }, "node_modules/arg": { "version": "4.1.3", "resolved": "https://registry.npmjs.org/arg/-/arg-4.1.3.tgz", @@ -7747,7 +7860,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/base64-js": { @@ -8136,9 +8249,9 @@ } }, "node_modules/caniuse-lite": { - "version": "1.0.30001654", - "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001654.tgz", - "integrity": "sha512-wLJc602fW0OdrUR+PqsBUH3dgrjDcT+mWs/Kw86zPvgjiqOiI2TXMkBFK4KihYzZclmJxrFwgYhZDSEogFai/g==", + "version": "1.0.30001657", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001657.tgz", + "integrity": "sha512-DPbJAlP8/BAXy3IgiWmZKItubb3TYGP0WscQQlVGIfT4s/YlFYVuJgyOsQNP7rJRChx/qdMeLJQJP0Sgg2yjNA==", "funding": [ { "type": "opencollective", @@ -8155,6 +8268,22 @@ ], "license": "CC-BY-4.0" }, + "node_modules/canvas": { + "version": "2.11.2", + "resolved": "https://registry.npmjs.org/canvas/-/canvas-2.11.2.tgz", + "integrity": "sha512-ItanGBMrmRV7Py2Z+Xhs7cT+FNt5K0vPL4p9EZ/UX/Mu7hFbkxSjKF2KVtPwX7UYWp7dRKnrTvReflgrItJbdw==", + "hasInstallScript": true, + "license": "MIT", + "optional": true, + "dependencies": { + "@mapbox/node-pre-gyp": "^1.0.0", + "nan": "^2.17.0", + "simple-get": "^3.0.3" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/chalk": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", @@ -8214,7 +8343,7 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/chownr/-/chownr-2.0.0.tgz", "integrity": "sha512-bIomtDF5KGpdogkLd9VspvFzk9KfpyyGlS8YFVZl7TGPBHL5snIOnxeshwVgPteQ9b4Eydl+pVbIyE1DcvCWgQ==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=10" @@ -8480,6 +8609,16 @@ "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", "license": "MIT" }, + "node_modules/color-support": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-support/-/color-support-1.1.3.tgz", + "integrity": "sha512-qiBjkpbMLO/HL68y+lh4q0/O1MZFj2RX6X/KmMa3+gJD3z+WwI1ZzDHysvqHGS3mP6mznPckpXmw1nI9cJjyRg==", + "license": "ISC", + "optional": true, + "bin": { + "color-support": "bin.js" + } + }, "node_modules/colorette": { "version": "2.0.20", "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", @@ -8559,7 +8698,7 @@ "version": "0.0.1", "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/connect-history-api-fallback": { @@ -8572,6 +8711,13 @@ "node": ">=0.8" } }, + "node_modules/console-control-strings": { + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/console-control-strings/-/console-control-strings-1.1.0.tgz", + "integrity": "sha512-ty/fTekppD2fIwRvnZAVdeOiGd1c7YXEixbgJTNzqcxJWKQnjJ/V1bNEEE6hygpM3WjwHFUVK6HTjWSzV4a8sQ==", + "license": "ISC", + "optional": true + }, "node_modules/content-disposition": { "version": "0.5.4", "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", @@ -9427,6 +9573,19 @@ "dev": true, "license": "MIT" }, + "node_modules/decompress-response": { + "version": "4.2.1", + "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-4.2.1.tgz", + "integrity": "sha512-jOSne2qbyE+/r8G1VU+G/82LBs2Fs4LAsTiLSHOCOMZQl2OKZ6i8i4IyHemTe+/yIXOtTcRQMzPcgyhoFlqPkw==", + "license": "MIT", + "optional": true, + "dependencies": { + "mimic-response": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/dedent": { "version": "1.5.3", "resolved": "https://registry.npmjs.org/dedent/-/dedent-1.5.3.tgz", @@ -9556,6 +9715,13 @@ "node": ">=0.4.0" } }, + "node_modules/delegates": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/delegates/-/delegates-1.0.0.tgz", + "integrity": "sha512-bd2L678uiWATM6m5Z1VzNCErI3jiGzt6HGY8OVICs40JQq/HALfbyNJmp0UDakEY4pMMaN0Ly5om/B1VI/+xfQ==", + "license": "MIT", + "optional": true + }, "node_modules/depd": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", @@ -9591,7 +9757,7 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.3.tgz", "integrity": "sha512-bwy0MGW55bG41VqxxypOsdSdGqLwXPI/focwgTYCFMbdUiBAxLg9CFzG08sz2aqzknwiX7Hkl0bQENjg8iLByw==", - "dev": true, + "devOptional": true, "license": "Apache-2.0", "engines": { "node": ">=8" @@ -9625,9 +9791,9 @@ } }, "node_modules/diff-match-patch-typescript": { - "version": "1.0.8", - "resolved": "https://registry.npmjs.org/diff-match-patch-typescript/-/diff-match-patch-typescript-1.0.8.tgz", - "integrity": "sha512-UPvsAUDje0DUOhx5V5jrXPe/5GHyBwZzS4myPFDM3Tbd/xJQyXbMkklc6aFqKBYzyhtdSMPD1CHHTDye/7cgow==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/diff-match-patch-typescript/-/diff-match-patch-typescript-1.1.0.tgz", + "integrity": "sha512-7WFVb3bRj5o+xRJtd1mLpbB9o19GE1FpY/v7z4GgMurmyaxZnuYdsEwn/K93ugn3nB+ce7KMn9hYjfAtXmUkVQ==", "license": "Apache-2.0" }, "node_modules/diff-sequences": { @@ -9772,9 +9938,9 @@ } }, "node_modules/electron-to-chromium": { - "version": "1.5.13", - "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.13.tgz", - "integrity": "sha512-lbBcvtIJ4J6sS4tb5TLp1b4LyfCdMkwStzXPyAgVgTRAsep4bvrAGaBOP7ZJtQMNJpSQ9SqG4brWOroNaQtm7Q==", + "version": "1.5.15", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.15.tgz", + "integrity": "sha512-Z4rIDoImwEJW+YYKnPul4DzqsWVqYetYVN3XqDmRpgV0mjz0hYTaeeh+8/9CL1bk3AHYmF4freW/NTiVoXA2gA==", "license": "ISC" }, "node_modules/emittery": { @@ -10254,9 +10420,9 @@ } }, "node_modules/eslint-plugin-jest": { - "version": "28.8.1", - "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.8.1.tgz", - "integrity": "sha512-G46XMyYu6PtSNJUkQ0hsPjzXYpzq/O4vpCciMizTKRJG8kNsRreGoMRDG6H9FIB/xVgfFuclVnuX4XRvFUzrZQ==", + "version": "28.8.3", + "resolved": "https://registry.npmjs.org/eslint-plugin-jest/-/eslint-plugin-jest-28.8.3.tgz", + "integrity": "sha512-HIQ3t9hASLKm2IhIOqnu+ifw7uLZkIlR7RYNv7fMcEi/p0CIiJmfriStQS2LDkgtY4nyLbIZAD+JL347Yc2ETQ==", "dev": true, "license": "MIT", "dependencies": { @@ -10871,9 +11037,9 @@ "license": "Apache-2.0" }, "node_modules/export-to-csv": { - "version": "1.3.0", - "resolved": "https://registry.npmjs.org/export-to-csv/-/export-to-csv-1.3.0.tgz", - "integrity": "sha512-msPjbfozZdYzDghAEKmCVH5veMeKHNacplE6noXvGiA8AeV1qa/SOxp6JXDjF9R8Kf6v3ypI6jskiY19dkhZeA==", + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/export-to-csv/-/export-to-csv-1.4.0.tgz", + "integrity": "sha512-6CX17Cu+rC2Fi2CyZ4CkgVG3hLl6BFsdAxfXiZkmDFIDY4mRx2y2spdeH6dqPHI9rP+AsHEfGeKz84Uuw7+Pmg==", "license": "MIT", "engines": { "node": "^v12.20.0 || >=v14.13.0" @@ -11215,9 +11381,9 @@ } }, "node_modules/follow-redirects": { - "version": "1.15.6", - "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.6.tgz", - "integrity": "sha512-wWN62YITEaOpSK584EZXJafH1AGpO8RVgElfkuXbTOrPX4fIfOyEpW/CsiNd8JdYrAoOvafRTOEnvsO++qCqFA==", + "version": "1.15.8", + "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.8.tgz", + "integrity": "sha512-xgrmBhBToVKay1q2Tao5LI26B83UhrB/vM1avwVSDzt8rx3rO6AizBAaF46EgksTVr+rFTQaqZZ9MVBfUe4nig==", "dev": true, "funding": [ { @@ -11331,7 +11497,7 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/fsevents": { @@ -11358,6 +11524,67 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gauge": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/gauge/-/gauge-3.0.2.tgz", + "integrity": "sha512-+5J6MS/5XksCuXq++uFRsnUd7Ovu1XenbeuIuNRJxYWjgQbPuFhT14lAvsWfqfAmnwluf1OwMjz39HjfLPci0Q==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "aproba": "^1.0.3 || ^2.0.0", + "color-support": "^1.1.2", + "console-control-strings": "^1.0.0", + "has-unicode": "^2.0.1", + "object-assign": "^4.1.1", + "signal-exit": "^3.0.0", + "string-width": "^4.2.3", + "strip-ansi": "^6.0.1", + "wide-align": "^1.1.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/gauge/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/gauge/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/gauge/node_modules/signal-exit": { + "version": "3.0.7", + "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.7.tgz", + "integrity": "sha512-wnD2ZE+l+SPC/uoS0vXeE9L1+0wuaMqKlfz9AMUo38JsyLSBWSFcHR1Rri62LZc12vLr1gb3jl7iwQhgwpAbGQ==", + "license": "ISC", + "optional": true + }, + "node_modules/gauge/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/gensync": { "version": "1.0.0-beta.2", "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", @@ -11454,7 +11681,7 @@ "resolved": "https://registry.npmjs.org/glob/-/glob-7.2.3.tgz", "integrity": "sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==", "deprecated": "Glob versions prior to v9 are no longer supported", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "fs.realpath": "^1.0.0", @@ -11494,7 +11721,7 @@ "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", "integrity": "sha512-iCuPHDFgrHX7H2vEI/5xpz07zSHB00TpugqhmYtVmMO6518mCuRMoOYFldEBl0g187ufozdaHgWKcYFb61qGiA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "balanced-match": "^1.0.0", @@ -11505,7 +11732,7 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", "integrity": "sha512-J7p63hRiAjw1NDEww1W7i37+ByIrOWO5XQQAzZ3VOcL0PNybwpfmV/N05zFAzwQ9USyEcX6t3UO+K5aqBQOIHw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "brace-expansion": "^1.1.7" @@ -11635,6 +11862,13 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/has-unicode": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/has-unicode/-/has-unicode-2.0.1.tgz", + "integrity": "sha512-8Rf9Y83NBReMnx0gFzA8JImQACstCYWUplepDa9xprwwtmgEZUF0h/i5xSA625zB/I37EtrswSST6OXxwaaIJQ==", + "license": "ISC", + "optional": true + }, "node_modules/hasown": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", @@ -12150,7 +12384,7 @@ "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", "deprecated": "This module is not supported, and leaks memory. Do not use it. Check out lru-cache if you want a good and tested way to coalesce async requests by a key value, which is much more comprehensive and powerful.", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "once": "^1.3.0", @@ -14844,9 +15078,9 @@ } }, "node_modules/launch-editor": { - "version": "2.8.1", - "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.8.1.tgz", - "integrity": "sha512-elBx2l/tp9z99X5H/qev8uyDywVh0VXAwEbjk8kJhnc5grOFkGh7aW6q55me9xnYbss261XtnUrysZ+XvGbhQA==", + "version": "2.9.1", + "resolved": "https://registry.npmjs.org/launch-editor/-/launch-editor-2.9.1.tgz", + "integrity": "sha512-Gcnl4Bd+hRO9P9icCP/RVVT2o8SFlPXofuCxvA2SaZuH45whSvf5p8x5oih5ftLiVhEI4sp5xDY+R+b3zJBh5w==", "dev": true, "license": "MIT", "dependencies": { @@ -15006,9 +15240,9 @@ "license": "MIT" }, "node_modules/lint-staged": { - "version": "15.2.9", - "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.9.tgz", - "integrity": "sha512-BZAt8Lk3sEnxw7tfxM7jeZlPRuT4M68O0/CwZhhaw6eeWu0Lz5eERE3m386InivXB64fp/mDID452h48tvKlRQ==", + "version": "15.2.10", + "resolved": "https://registry.npmjs.org/lint-staged/-/lint-staged-15.2.10.tgz", + "integrity": "sha512-5dY5t743e1byO19P9I4b3x8HJwalIznL5E1FWYnU6OWw33KxNBSLAc6Cy7F2PsFEO8FKnLwjwm5hx7aMF0jzZg==", "dev": true, "license": "MIT", "dependencies": { @@ -15018,7 +15252,7 @@ "execa": "~8.0.1", "lilconfig": "~3.1.2", "listr2": "~8.2.4", - "micromatch": "~4.0.7", + "micromatch": "~4.0.8", "pidtree": "~0.6.0", "string-argv": "~0.3.2", "yaml": "~2.5.0" @@ -15820,6 +16054,19 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/mimic-response": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-2.1.0.tgz", + "integrity": "sha512-wXqjST+SLt7R009ySCglWBCFpjUygmCIfD790/kVbiGmUgfYGuB14PiTd5DwVxSV4NcYHjzMkoj5LjQZwTQLEA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/mini-css-extract-plugin": { "version": "2.9.0", "resolved": "https://registry.npmjs.org/mini-css-extract-plugin/-/mini-css-extract-plugin-2.9.0.tgz", @@ -16018,7 +16265,7 @@ "version": "2.1.2", "resolved": "https://registry.npmjs.org/minizlib/-/minizlib-2.1.2.tgz", "integrity": "sha512-bAxsR8BVfj60DWXHE3u30oHzfl4G7khkSuPW+qvpd7jFRHm7dLxOjUk1EHACJ/hxLY8phGJ0YhYHZo7jil7Qdg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "minipass": "^3.0.0", @@ -16032,7 +16279,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -16045,14 +16292,14 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/mkdirp": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-1.0.4.tgz", "integrity": "sha512-vVqVZQyf3WLx2Shd0qJ9xuvqgAyKPLAiqITEtqW0oIUjzo3PePDd6fW9iFz30ef7Ysp/oiWqbhszeGWW2T6Gzw==", - "dev": true, + "devOptional": true, "license": "MIT", "bin": { "mkdirp": "bin/cmd.js" @@ -16173,6 +16420,13 @@ "url": "https://github.com/sponsors/wooorm" } }, + "node_modules/nan": { + "version": "2.20.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.20.0.tgz", + "integrity": "sha512-bk3gXBZDGILuuo/6sKtr0DQmSThYHLtNCdSdXk9YkxD/jK6X2vmCyyXBBxyqZ4XcnzTyYEAThfX3DCEnLf6igw==", + "license": "MIT", + "optional": true + }, "node_modules/nanoid": { "version": "3.3.7", "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.7.tgz", @@ -16321,6 +16575,52 @@ "dev": true, "license": "MIT" }, + "node_modules/node-fetch": { + "version": "2.7.0", + "resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz", + "integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==", + "license": "MIT", + "optional": true, + "dependencies": { + "whatwg-url": "^5.0.0" + }, + "engines": { + "node": "4.x || >=6.0.0" + }, + "peerDependencies": { + "encoding": "^0.1.0" + }, + "peerDependenciesMeta": { + "encoding": { + "optional": true + } + } + }, + "node_modules/node-fetch/node_modules/tr46": { + "version": "0.0.3", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-0.0.3.tgz", + "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", + "license": "MIT", + "optional": true + }, + "node_modules/node-fetch/node_modules/webidl-conversions": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-3.0.1.tgz", + "integrity": "sha512-2JAn3z8AR6rjK8Sm8orRC0h/bcl/DqL7tRPdGZ4I1CjdF+EaMLmYxBHyXuKL849eucPFhvBoxMsflfOb8kxaeQ==", + "license": "BSD-2-Clause", + "optional": true + }, + "node_modules/node-fetch/node_modules/whatwg-url": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-5.0.0.tgz", + "integrity": "sha512-saE57nupxk6v3HY35+jzBwYa0rKSy0XR8JSxZPwgLr7ys0IBzhGviA1/TUGJLmSVqs8pb9AnvICXEuOHLprYTw==", + "license": "MIT", + "optional": true, + "dependencies": { + "tr46": "~0.0.3", + "webidl-conversions": "^3.0.0" + } + }, "node_modules/node-forge": { "version": "1.3.1", "resolved": "https://registry.npmjs.org/node-forge/-/node-forge-1.3.1.tgz", @@ -16607,6 +16907,20 @@ "node": ">=8" } }, + "node_modules/npmlog": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/npmlog/-/npmlog-5.0.1.tgz", + "integrity": "sha512-AqZtDUWOMKs1G/8lwylVjrdYgqA4d9nu8hc+0gzRxlDb1I10+FHBGMXs6aiQHFdCUUlqH99MUMuLfzWDNDtfxw==", + "deprecated": "This package is no longer supported.", + "license": "ISC", + "optional": true, + "dependencies": { + "are-we-there-yet": "^2.0.0", + "console-control-strings": "^1.1.0", + "gauge": "^3.0.0", + "set-blocking": "^2.0.0" + } + }, "node_modules/nth-check": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", @@ -16683,7 +16997,7 @@ "version": "1.4.0", "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "wrappy": "1" @@ -17144,7 +17458,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.1.tgz", "integrity": "sha512-AVbw3UJ2e9bq64vSaS9Am0fje1Pa8pbGqTTsmXfaIiMpnr5DlDhfJOuLj9Sf95ZPVDAUerDfEk88MPmPe7UCQg==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" @@ -17211,6 +17525,29 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/path2d": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/path2d/-/path2d-0.2.1.tgz", + "integrity": "sha512-Fl2z/BHvkTNvkuBzYTpTuirHZg6wW9z8+4SND/3mDTEcYbbNKWAy21dz9D3ePNNwrrK8pqZO5vLPZ1hLF6T7XA==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/pdfjs-dist": { + "version": "4.6.82", + "resolved": "https://registry.npmjs.org/pdfjs-dist/-/pdfjs-dist-4.6.82.tgz", + "integrity": "sha512-BUOryeRFwvbLe0lOU6NhkJNuVQUp06WxlJVVCsxdmJ4y5cU3O3s3/0DunVdK1PMm7v2MUw52qKYaidhDH1Z9+w==", + "license": "Apache-2.0", + "engines": { + "node": ">=18" + }, + "optionalDependencies": { + "canvas": "^2.11.2", + "path2d": "^0.2.1" + } + }, "node_modules/pepjs": { "version": "0.5.3", "resolved": "https://registry.npmjs.org/pepjs/-/pepjs-0.5.3.tgz", @@ -17218,9 +17555,9 @@ "license": "MIT" }, "node_modules/picocolors": { - "version": "1.0.1", - "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.0.1.tgz", - "integrity": "sha512-anP1Z8qwhkbmu7MFP5iTt+wQKXgwzf7zTyGlcdzabySa9vd0Xt392U0rVmz9poOaBj0uHJKyyo9/upk0HrEQew==", + "version": "1.1.0", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz", + "integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==", "license": "ISC" }, "node_modules/picomatch": { @@ -17536,9 +17873,9 @@ "license": "MIT" }, "node_modules/posthog-js": { - "version": "1.160.0", - "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.160.0.tgz", - "integrity": "sha512-K/RRgmPYIpP69nnveCJfkclb8VU+R+jsgqlrKaLGsM5CtQM9g01WOzAiT3u36WLswi58JiFMXgJtECKQuoqTgQ==", + "version": "1.160.3", + "resolved": "https://registry.npmjs.org/posthog-js/-/posthog-js-1.160.3.tgz", + "integrity": "sha512-mGvxOIlWPtdPx8EI0MQ81wNKlnH2K0n4RqwQOl044b34BCKiFVzZ7Hc7geMuZNaRAvCi5/5zyGeWHcAYZQxiMQ==", "license": "MIT", "dependencies": { "fflate": "^0.4.8", @@ -18317,38 +18654,107 @@ "license": "MIT" }, "node_modules/rimraf": { - "version": "5.0.10", - "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-5.0.10.tgz", - "integrity": "sha512-l0OE8wL34P4nJH/H2ffoaniAokM2qSmrtXHmlpvYr5AVVX8msAyW0l8NVJFDxlSK4u3Uh/f41cQheDVdnYijwQ==", + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-6.0.1.tgz", + "integrity": "sha512-9dkvaxAsk/xNXSJzMgFqqMCuFgt2+KsOFek3TMLfo8NCPfWpBmqwyNn5Y+NX56QUYfCtsyhF3ayiboEoUmJk/A==", "dev": true, "license": "ISC", "dependencies": { - "glob": "^10.3.7" + "glob": "^11.0.0", + "package-json-from-dist": "^1.0.0" }, "bin": { "rimraf": "dist/esm/bin.mjs" }, + "engines": { + "node": "20 || >=22" + }, "funding": { "url": "https://github.com/sponsors/isaacs" } }, "node_modules/rimraf/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": "11.0.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-11.0.0.tgz", + "integrity": "sha512-9UiX/Bl6J2yaBbxKoEBRm4Cipxgok8kQYcOPEhScPwebu2I0HoQOuYdIO6S3hLuWoZgpDpwQZMzTFxgpkyT76g==", "dev": true, "license": "ISC", "dependencies": { "foreground-child": "^3.1.0", - "jackspeak": "^3.1.2", - "minimatch": "^9.0.4", + "jackspeak": "^4.0.1", + "minimatch": "^10.0.0", "minipass": "^7.1.2", "package-json-from-dist": "^1.0.0", - "path-scurry": "^1.11.1" + "path-scurry": "^2.0.0" }, "bin": { "glob": "dist/esm/bin.mjs" }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/jackspeak": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/jackspeak/-/jackspeak-4.0.1.tgz", + "integrity": "sha512-cub8rahkh0Q/bw1+GxP7aeSe29hHHn2V4m29nnDlvCdlgU+3UGxkZp7Z53jLUdpX3jdTO0nJZUDl3xvbWc2Xog==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "@isaacs/cliui": "^8.0.2" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + }, + "optionalDependencies": { + "@pkgjs/parseargs": "^0.11.0" + } + }, + "node_modules/rimraf/node_modules/lru-cache": { + "version": "11.0.0", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-11.0.0.tgz", + "integrity": "sha512-Qv32eSV1RSCfhY3fpPE2GNZ8jgM9X7rdAfemLWqTUxwiyIC4jJ6Sy0fZ8H+oLWevO6i4/bizg7c8d8i6bxrzbA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "20 || >=22" + } + }, + "node_modules/rimraf/node_modules/minimatch": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-10.0.1.tgz", + "integrity": "sha512-ethXTt3SGGR+95gudmqJ1eNhRO7eGEGIgYA9vnPatK4/etz2MEVDno5GMCibdMTuBMyElzIlgxMna3K94XDIDQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": "20 || >=22" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/rimraf/node_modules/path-scurry": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/path-scurry/-/path-scurry-2.0.0.tgz", + "integrity": "sha512-ypGJsmGtdXUOeM5u93TyeIEfEhM6s+ljAhrk5vAvSx8uyY/02OvrZnA0YNGUrPXfpJMgI1ODd3nwz8Npx4O4cg==", + "dev": true, + "license": "BlueOak-1.0.0", + "dependencies": { + "lru-cache": "^11.0.0", + "minipass": "^7.1.2" + }, + "engines": { + "node": "20 || >=22" + }, "funding": { "url": "https://github.com/sponsors/isaacs" } @@ -18455,9 +18861,9 @@ "license": "MIT" }, "node_modules/sass": { - "version": "1.77.8", - "resolved": "https://registry.npmjs.org/sass/-/sass-1.77.8.tgz", - "integrity": "sha512-4UHg6prsrycW20fqLGPShtEvo/WyHRVRHwOP4DzkUrObWoWI05QBSfzU71TVB7PFaL104TwNaHpjlWXAZbQiNQ==", + "version": "1.78.0", + "resolved": "https://registry.npmjs.org/sass/-/sass-1.78.0.tgz", + "integrity": "sha512-AaIqGSrjo5lA2Yg7RvFZrlXDBCp3nV4XP73GrLGvdRWWwk+8H3l0SDvq/5bA4eF+0RFPLuWUk3E+P1U/YqnpsQ==", "dev": true, "license": "MIT", "dependencies": { @@ -18741,6 +19147,13 @@ "node": ">= 0.8.0" } }, + "node_modules/set-blocking": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/set-blocking/-/set-blocking-2.0.0.tgz", + "integrity": "sha512-KiKBS8AnWGEyLzofFfmvKwpdPzqiy16LvQfK3yv/fVH7Bj13/wl3JSR1J+rfgRE9q7xUJK4qvgS8raSOeLUehw==", + "license": "ISC", + "optional": true + }, "node_modules/set-function-length": { "version": "1.2.2", "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.2.2.tgz", @@ -18936,6 +19349,39 @@ "node": "^16.14.0 || >=18.0.0" } }, + "node_modules/simple-concat": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", + "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ], + "license": "MIT", + "optional": true + }, + "node_modules/simple-get": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-3.1.1.tgz", + "integrity": "sha512-CQ5LTKGfCpvE1K0n2us+kuMPbk/q0EKl82s4aheV9oXjFEz6W/Y7oQFVJuU6QG77hRT4Ghb5RURteF5vnWjupA==", + "license": "MIT", + "optional": true, + "dependencies": { + "decompress-response": "^4.2.0", + "once": "^1.3.1", + "simple-concat": "^1.0.0" + } + }, "node_modules/simple-statistics": { "version": "7.8.5", "resolved": "https://registry.npmjs.org/simple-statistics/-/simple-statistics-7.8.5.tgz", @@ -19575,7 +20021,7 @@ "version": "6.2.1", "resolved": "https://registry.npmjs.org/tar/-/tar-6.2.1.tgz", "integrity": "sha512-DZ4yORTwrbTj/7MZYq2w+/ZFdI6OZ/f9SFHR+71gIVUZhOQPHzVCLpvRnPgyaMpfWxxk/4ONva3GQSyNIKRv6A==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "chownr": "^2.0.0", @@ -19593,7 +20039,7 @@ "version": "2.1.0", "resolved": "https://registry.npmjs.org/fs-minipass/-/fs-minipass-2.1.0.tgz", "integrity": "sha512-V/JgOLFCS+R6Vcq0slCuaeWEdNC3ouDlJMNIsacH2VtALiu9mV4LPrHc5cDl8k5aw6J8jwgWWpiTo5RYhmIzvg==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "minipass": "^3.0.0" @@ -19606,7 +20052,7 @@ "version": "3.3.6", "resolved": "https://registry.npmjs.org/minipass/-/minipass-3.3.6.tgz", "integrity": "sha512-DxiNidxSEK+tHG6zOIklvNOwm3hvCrbUrdtzY74U6HKTJxvIDfOUL5W5P2Ghd3DTkhhKPYGqeNUIh5qcM4YBfw==", - "dev": true, + "devOptional": true, "license": "ISC", "dependencies": { "yallist": "^4.0.0" @@ -19619,7 +20065,7 @@ "version": "5.0.0", "resolved": "https://registry.npmjs.org/minipass/-/minipass-5.0.0.tgz", "integrity": "sha512-3FnjYuehv9k6ovOEbyOswadCDPX1piCfhV8ncmYtHOjuPwylVWsghTLo7rabjC3Rx5xD4HDx8Wm1xnMF7S5qFQ==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": ">=8" @@ -19629,7 +20075,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/yallist/-/yallist-4.0.0.tgz", "integrity": "sha512-3wdGidZyq5PB084XLES5TpOSRA3wjXAlIWMhum2kRcv/41Sn2emQ0dycQW4uZXLejwKvg6EsvbdlVL+FYEct7A==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/terser": { @@ -21537,6 +21983,48 @@ "node": ">= 8" } }, + "node_modules/wide-align": { + "version": "1.1.5", + "resolved": "https://registry.npmjs.org/wide-align/-/wide-align-1.1.5.tgz", + "integrity": "sha512-eDMORYaPNZ4sQIuuYPDHdQvf4gyCF9rEEV/yPxGfwPkRodwEgiMUUXTx/dex+Me0wxx53S+NgUHaP7y3MGlDmg==", + "license": "ISC", + "optional": true, + "dependencies": { + "string-width": "^1.0.2 || 2 || 3 || 4" + } + }, + "node_modules/wide-align/node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "license": "MIT", + "optional": true + }, + "node_modules/wide-align/node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "license": "MIT", + "optional": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/wide-align/node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "license": "MIT", + "optional": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, "node_modules/wildcard": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", @@ -21728,7 +22216,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "dev": true, + "devOptional": true, "license": "ISC" }, "node_modules/write-file-atomic": { @@ -21826,9 +22314,9 @@ "license": "ISC" }, "node_modules/yaml": { - "version": "2.5.0", - "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.0.tgz", - "integrity": "sha512-2wWLbGbYDiSqqIKoPjar3MPgB94ErzCtrNE1FdqGuaO0pi2JGjmE8aW8TDZwzU7vuxcGRdL/4gPQwQ7hD5AMSw==", + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.5.1.tgz", + "integrity": "sha512-bLQOjaX/ADgQ20isPJRvF0iRUHIxVhYvr53Of7wGcWlO2jvtUlH5m87DsmulFVxRpNLOnI4tB6p/oh8D7kpn9Q==", "dev": true, "license": "ISC", "bin": { diff --git a/package.json b/package.json index 6a576592bbf0..ab806fb582e0 100644 --- a/package.json +++ b/package.json @@ -13,18 +13,18 @@ "node_modules" ], "dependencies": { - "@angular/animations": "18.2.2", - "@angular/cdk": "18.2.2", - "@angular/common": "18.2.2", - "@angular/compiler": "18.2.2", - "@angular/core": "18.2.2", - "@angular/forms": "18.2.2", - "@angular/localize": "18.2.2", - "@angular/material": "18.2.2", - "@angular/platform-browser": "18.2.2", - "@angular/platform-browser-dynamic": "18.2.2", - "@angular/router": "18.2.2", - "@angular/service-worker": "18.2.2", + "@angular/animations": "18.2.3", + "@angular/cdk": "18.2.3", + "@angular/common": "18.2.3", + "@angular/compiler": "18.2.3", + "@angular/core": "18.2.3", + "@angular/forms": "18.2.3", + "@angular/localize": "18.2.3", + "@angular/material": "18.2.3", + "@angular/platform-browser": "18.2.3", + "@angular/platform-browser-dynamic": "18.2.3", + "@angular/router": "18.2.3", + "@angular/service-worker": "18.2.3", "@ctrl/ngx-emoji-mart": "9.2.0", "@danielmoncada/angular-datetime-picker": "18.1.0", "@fingerprintjs/fingerprintjs": "4.4.3", @@ -37,7 +37,7 @@ "@ng-bootstrap/ng-bootstrap": "17.0.1", "@ngx-translate/core": "15.0.0", "@ngx-translate/http-loader": "8.0.0", - "@sentry/angular": "8.27.0", + "@sentry/angular": "8.28.0", "@swimlane/ngx-charts": "20.5.0", "@swimlane/ngx-graph": "8.4.0", "@vscode/codicons": "0.0.36", @@ -46,9 +46,9 @@ "core-js": "3.38.1", "crypto-js": "4.2.0", "dayjs": "1.11.13", - "diff-match-patch-typescript": "1.0.8", + "diff-match-patch-typescript": "1.1.0", "dompurify": "3.1.6", - "export-to-csv": "1.3.0", + "export-to-csv": "1.4.0", "fast-json-patch": "3.1.1", "franc-min": "6.2.0", "html-diff-ts": "1.4.2", @@ -62,7 +62,8 @@ "ngx-infinite-scroll": "18.0.0", "ngx-webstorage": "18.0.0", "papaparse": "5.4.1", - "posthog-js": "1.160.0", + "pdfjs-dist": "4.6.82", + "posthog-js": "1.160.3", "rxjs": "7.8.1", "showdown": "2.1.0", "showdown-highlight": "3.1.0", @@ -96,11 +97,12 @@ "eslint": "^9.9.0" }, "eslint-plugin-jest": { - "@typescript-eslint/eslint-plugin": "^8.1.0" + "@typescript-eslint/eslint-plugin": "^8.4.0" }, "jsdom": "24.1.1", "katex": "0.16.11", "postcss": "8.4.41", + "rimraf": "6.0.1", "semver": "7.6.3", "showdown-katex": { "showdown": "2.1.0" @@ -113,33 +115,33 @@ }, "devDependencies": { "@angular-builders/jest": "18.0.0", - "@angular-devkit/build-angular": "18.2.2", + "@angular-devkit/build-angular": "18.2.3", "@angular-eslint/builder": "18.3.0", "@angular-eslint/eslint-plugin": "18.3.0", "@angular-eslint/eslint-plugin-template": "18.3.0", "@angular-eslint/schematics": "18.3.0", "@angular-eslint/template-parser": "18.3.0", - "@angular/cli": "18.2.2", - "@angular/compiler-cli": "18.2.2", - "@angular/language-service": "18.2.2", - "@sentry/types": "8.27.0", + "@angular/cli": "18.2.3", + "@angular/compiler-cli": "18.2.3", + "@angular/language-service": "18.2.3", + "@sentry/types": "8.28.0", "@types/crypto-js": "4.2.2", "@types/d3-shape": "3.1.6", "@types/dompurify": "3.0.5", "@types/jest": "29.5.12", "@types/lodash-es": "4.17.12", - "@types/node": "22.5.1", + "@types/node": "22.5.4", "@types/papaparse": "5.3.14", "@types/showdown": "2.0.6", "@types/smoothscroll-polyfill": "0.3.4", "@types/sockjs-client": "1.5.4", "@types/uuid": "10.0.0", - "@typescript-eslint/eslint-plugin": "8.3.0", - "@typescript-eslint/parser": "8.3.0", + "@typescript-eslint/eslint-plugin": "8.4.0", + "@typescript-eslint/parser": "8.4.0", "eslint": "9.9.1", "eslint-config-prettier": "9.1.0", "eslint-plugin-deprecation": "3.0.0", - "eslint-plugin-jest": "28.8.1", + "eslint-plugin-jest": "28.8.3", "eslint-plugin-jest-extended": "2.4.0", "eslint-plugin-prettier": "5.2.1", "folder-hash": "4.0.4", @@ -151,10 +153,11 @@ "jest-fail-on-console": "3.3.0", "jest-junit": "16.0.0", "jest-preset-angular": "14.2.2", - "lint-staged": "15.2.9", + "lint-staged": "15.2.10", "ng-mocks": "14.13.1", "prettier": "3.3.3", - "sass": "1.77.8", + "rimraf": "6.0.1", + "sass": "1.78.0", "ts-jest": "29.2.5", "typescript": "5.5.4", "weak-napi": "2.0.2" diff --git a/src/main/java/de/tum/in/www1/artemis/config/PropertiesConfigurationGuard.java b/src/main/java/de/tum/in/www1/artemis/config/PropertiesConfigurationGuard.java new file mode 100644 index 000000000000..898bf2d78c38 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/config/PropertiesConfigurationGuard.java @@ -0,0 +1,31 @@ +package de.tum.in.www1.artemis.config; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_SCHEDULING; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.InitializingBean; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.context.annotation.Profile; +import org.springframework.stereotype.Component; + +@Component +@Profile(PROFILE_SCHEDULING) +public class PropertiesConfigurationGuard implements InitializingBean { + + private static final Logger log = LoggerFactory.getLogger(PropertiesConfigurationGuard.class); + + @Value("${info.operatorName:#{null}}") + private String operatorName; + + /** + * Checks if the info.operatorName value is set in the configuration ymls, and exits the application if not. + */ + public void afterPropertiesSet() { + if (this.operatorName == null || this.operatorName.isEmpty()) { + log.error( + "The name of the operator (University) is not configured in the application-prod.yml! It is needed to be displayed in the /about page, and for the telemetry service."); + throw new IllegalArgumentException("The name of the operator (university) must be configured, but is not!"); + } + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java b/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java index 73d81028276b..41662e7c3be6 100644 --- a/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java +++ b/src/main/java/de/tum/in/www1/artemis/config/lti/CustomLti13Configurer.java @@ -5,9 +5,9 @@ import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.oauth2.client.registration.ClientRegistrationRepository; import org.springframework.security.web.authentication.logout.LogoutFilter; -import org.springframework.security.web.authentication.preauth.AbstractPreAuthenticatedProcessingFilter; import org.springframework.stereotype.Component; +import de.tum.in.www1.artemis.security.jwt.JWTFilter; import de.tum.in.www1.artemis.service.OnlineCourseConfigurationService; import de.tum.in.www1.artemis.service.connectors.lti.Lti13Service; import de.tum.in.www1.artemis.web.filter.Lti13LaunchFilter; @@ -74,7 +74,7 @@ public void configure(HttpSecurity http) { // https://www.imsglobal.org/spec/security/v1p0/#step-3-authentication-response OAuth2LoginAuthenticationFilter defaultLoginFilter = configureLoginFilter(clientRegistrationRepository(http), oidcLaunchFlowAuthenticationProvider, authorizationRequestRepository); - http.addFilterAfter(new Lti13LaunchFilter(defaultLoginFilter, "/" + LTI13_LOGIN_PATH, lti13Service(http)), AbstractPreAuthenticatedProcessingFilter.class); + http.addFilterAfter(new Lti13LaunchFilter(defaultLoginFilter, "/" + LTI13_LOGIN_PATH, lti13Service(http)), JWTFilter.class); } protected Lti13Service lti13Service(HttpSecurity http) { diff --git a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java index 8cd15a15b573..f339eb61f51d 100644 --- a/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java +++ b/src/main/java/de/tum/in/www1/artemis/repository/StudentParticipationRepository.java @@ -703,18 +703,38 @@ default Page findAllWithEagerSubmissionsAndResultsByExerci """) List findAllWithEagerLegalSubmissionsAndEagerResultsByExerciseId(@Param("exerciseId") long exerciseId); + /** + * Retrieves all distinct `StudentParticipation` entities for a specific exercise, + * including their latest non-illegal submission and the latest rated result for each submission. + * The method fetches related submissions, results, student, and team data to avoid the N+1 select problem. + * + *

+ * The method ensures that: + *

+ * + * @param exerciseId the ID of the exercise for which to retrieve participations. + * @return a list of distinct `StudentParticipation` entities matching the criteria. + */ @Query(""" SELECT DISTINCT p FROM StudentParticipation p - LEFT JOIN FETCH p.results r - LEFT JOIN FETCH r.submission rs LEFT JOIN FETCH p.submissions s - LEFT JOIN FETCH s.results sr + LEFT JOIN FETCH s.results r + LEFT JOIN FETCH p.student + LEFT JOIN FETCH p.team WHERE p.exercise.id = :exerciseId AND p.testRun = FALSE - AND p.submissions IS NOT EMPTY - AND (s.type <> de.tum.in.www1.artemis.domain.enumeration.SubmissionType.ILLEGAL OR s.type IS NULL) - AND (rs.type <> de.tum.in.www1.artemis.domain.enumeration.SubmissionType.ILLEGAL OR rs.type IS NULL) + AND s.id = (SELECT MAX(s2.id) + FROM p.submissions s2 + WHERE s2.type <> de.tum.in.www1.artemis.domain.enumeration.SubmissionType.ILLEGAL OR s2.type IS NULL) + AND r.id = (SELECT MAX(r2.id) + FROM s.results r2 + WHERE r2.rated = TRUE) """) List findAllForPlagiarism(@Param("exerciseId") long exerciseId); diff --git a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiService.java b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiService.java index bd7b778439a9..0dd67f46a739 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/connectors/lti/LtiService.java @@ -11,6 +11,7 @@ import org.slf4j.Logger; import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Profile; import org.springframework.http.HttpHeaders; import org.springframework.http.ResponseCookie; @@ -42,6 +43,9 @@ @Profile("lti") public class LtiService { + @Value("${artemis.lti.trustExternalLTISystems:false}") + private boolean trustExternalLTISystems; + public static final String LTI_GROUP_NAME = "lti"; protected static final List SIMPLE_USER_LIST_AUTHORITY = Collections.singletonList(new SimpleGrantedAuthority(Role.STUDENT.getAuthority())); @@ -105,6 +109,14 @@ public void authenticateLtiUser(String email, String username, String firstName, // 2. Case: Lookup user with the LTI email address and make sure it's not in use if (artemisAuthenticationProvider.getUsernameForEmail(email).isPresent() || userRepository.findOneByEmailIgnoreCase(email).isPresent()) { log.info("User with email {} already exists. Email is already in use.", email); + + if (trustExternalLTISystems) { + log.info("Trusting external LTI system. Authenticating user with email: {}", email); + User user = userRepository.findUserWithGroupsAndAuthoritiesByEmail(email).orElseThrow(); + SecurityContextHolder.getContext().setAuthentication(new UsernamePasswordAuthenticationToken(user.getLogin(), user.getPassword(), user.getGrantedAuthorities())); + return; + } + throw new LtiEmailAlreadyInUseException(); } @@ -179,23 +191,16 @@ private void addUserToExerciseGroup(User user, Course course) { * @param response the response to add the JWT cookie to */ public void buildLtiResponse(UriComponentsBuilder uriComponentsBuilder, HttpServletResponse response) { - // TODO SK: why do we logout the user here if it was already activated? - User user = userRepository.getUser(); if (!user.getActivated()) { - log.info("User is not activated. Adding JWT cookie for activation."); - log.info("Add JWT cookie so the user will be logged in"); - ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(true); - response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); - + log.info("User is not activated. Adding initialize parameter to query."); uriComponentsBuilder.queryParam("initialize", ""); } - else { - log.info("User is activated. Adding JWT cookie for logout."); - prepareLogoutCookie(response); - uriComponentsBuilder.queryParam("ltiSuccessLoginRequired", user.getLogin()); - } + + log.info("Add/Update JWT cookie so the user will be logged in."); + ResponseCookie responseCookie = jwtCookieService.buildLoginCookie(true); + response.addHeader(HttpHeaders.SET_COOKIE, responseCookie.toString()); } /** diff --git a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java index 16b674bb748d..68d63b1f49e7 100644 --- a/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java +++ b/src/main/java/de/tum/in/www1/artemis/service/plagiarism/ProgrammingPlagiarismDetectionService.java @@ -185,7 +185,7 @@ public File checkPlagiarismWithJPlagReport(long programmingExerciseId, float sim private JPlagResult computeJPlagResult(ProgrammingExercise programmingExercise, float similarityThreshold, int minimumScore, int minimumSize) { long programmingExerciseId = programmingExercise.getId(); final var targetPath = fileService.getTemporaryUniqueSubfolderPath(repoDownloadClonePath, 60); - List participations = filterStudentParticipationsForComparison(programmingExercise, minimumScore); + List participations = findStudentParticipationsForComparison(programmingExercise, minimumScore); log.info("Download repositories for JPlag for programming exercise {} to compare {} participations", programmingExerciseId, participations.size()); if (participations.size() < 2) { @@ -326,8 +326,10 @@ private Language getJPlagProgrammingLanguage(ProgrammingExercise programmingExer * @param minimumScore consider only submissions whose score is greater or equal to this value * @return an unmodifiable list containing the latest text submission for every participation */ - public List filterStudentParticipationsForComparison(ProgrammingExercise programmingExercise, int minimumScore) { + public List findStudentParticipationsForComparison(ProgrammingExercise programmingExercise, int minimumScore) { + long start = System.nanoTime(); var studentParticipations = studentParticipationRepository.findAllForPlagiarism(programmingExercise.getId()); + log.info("findAllForPlagiarism took {}", TimeLogUtil.formatDurationFrom(start)); return studentParticipations.parallelStream().filter(participation -> !participation.isPracticeMode()) .filter(participation -> participation instanceof ProgrammingExerciseStudentParticipation).filter(plagiarismService.filterForStudents()) diff --git a/src/main/java/de/tum/in/www1/artemis/service/telemetry/TelemetryService.java b/src/main/java/de/tum/in/www1/artemis/service/telemetry/TelemetryService.java new file mode 100644 index 000000000000..7e49653d99d9 --- /dev/null +++ b/src/main/java/de/tum/in/www1/artemis/service/telemetry/TelemetryService.java @@ -0,0 +1,122 @@ +package de.tum.in.www1.artemis.service.telemetry; + +import static de.tum.in.www1.artemis.config.Constants.PROFILE_SCHEDULING; + +import java.util.Arrays; +import java.util.List; + +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.boot.context.event.ApplicationReadyEvent; +import org.springframework.context.annotation.Profile; +import org.springframework.context.event.EventListener; +import org.springframework.core.env.Environment; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.MediaType; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectWriter; + +import de.tum.in.www1.artemis.service.ProfileService; + +@Service +@Profile(PROFILE_SCHEDULING) +public class TelemetryService { + + @JsonInclude(JsonInclude.Include.NON_EMPTY) + public record TelemetryData(String version, String serverUrl, String operator, String contact, List profiles, String adminName) { + } + + private static final Logger log = LoggerFactory.getLogger(TelemetryService.class); + + private final Environment env; + + private final RestTemplate restTemplate; + + private final ProfileService profileService; + + @Value("${artemis.telemetry.enabled}") + public boolean useTelemetry; + + @Value("${artemis.telemetry.sendAdminDetails}") + private boolean sendAdminDetails; + + @Value("${artemis.telemetry.destination}") + private String destination; + + @Value("${artemis.version}") + private String version; + + @Value("${server.url}") + private String serverUrl; + + @Value("${info.operatorName}") + private String operator; + + @Value("${info.operatorAdminName}") + private String operatorAdminName; + + @Value("${info.contact}") + private String contact; + + public TelemetryService(Environment env, RestTemplate restTemplate, ProfileService profileService) { + this.env = env; + this.restTemplate = restTemplate; + this.profileService = profileService; + } + + /** + * Sends telemetry to the server specified in artemis.telemetry.destination. + * This function runs once, at the startup of the application. + * If telemetry is disabled in artemis.telemetry.enabled, no data is sent. + */ + @EventListener(ApplicationReadyEvent.class) + public void sendTelemetry() { + if (!useTelemetry || profileService.isDevActive()) { + return; + } + + log.info("Sending telemetry information"); + try { + sendTelemetryByPostRequest(); + } + catch (JsonProcessingException e) { + log.warn("JsonProcessingException in sendTelemetry.", e); + } + catch (Exception e) { + log.warn("Exception in sendTelemetry, with dst URI: {}", destination, e); + } + + } + + /** + * Assembles the telemetry data, and sends it to the external telemetry server. + * + * @throws Exception if the writing the telemetry data to a json format fails, or the connection to the telemetry server fails + */ + public void sendTelemetryByPostRequest() throws Exception { + List activeProfiles = Arrays.asList(env.getActiveProfiles()); + TelemetryData telemetryData; + if (sendAdminDetails) { + telemetryData = new TelemetryData(version, serverUrl, operator, contact, activeProfiles, operatorAdminName); + } + else { + telemetryData = new TelemetryData(version, serverUrl, operator, null, activeProfiles, null); + } + + HttpHeaders headers = new HttpHeaders(); + headers.setContentType(MediaType.APPLICATION_JSON); + ObjectWriter objectWriter = new ObjectMapper().writer().withDefaultPrettyPrinter(); + + var telemetryJson = objectWriter.writeValueAsString(telemetryData); + HttpEntity requestEntity = new HttpEntity<>(telemetryJson, headers); + var response = restTemplate.postForEntity(destination + "/api/telemetry", requestEntity, String.class); + log.info("Successfully sent telemetry data. {}", response.getBody()); + } +} diff --git a/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java b/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java index ad959b26096f..8627d8f701ed 100644 --- a/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java +++ b/src/main/java/de/tum/in/www1/artemis/web/rest/FileResource.java @@ -69,6 +69,7 @@ import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastInstructor; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastStudent; import de.tum.in.www1.artemis.security.annotations.EnforceAtLeastTutor; +import de.tum.in.www1.artemis.security.annotations.enforceRoleInCourse.EnforceAtLeastEditorInCourse; import de.tum.in.www1.artemis.service.AuthorizationCheckService; import de.tum.in.www1.artemis.service.FilePathService; import de.tum.in.www1.artemis.service.FileService; @@ -372,6 +373,24 @@ public ResponseEntity getLectureAttachment(@PathVariable Long lectureId, return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), false); } + /** + * GET /files/courses/{courseId}/attachments/{attachmentId} : Returns the file associated with the + * given attachment ID as a downloadable resource + * + * @param courseId The ID of the course that the Attachment belongs to + * @param attachmentId the ID of the attachment to retrieve + * @return ResponseEntity containing the file as a resource + */ + @GetMapping("files/courses/{courseId}/attachments/{attachmentId}") + @EnforceAtLeastEditorInCourse + public ResponseEntity getAttachmentFile(@PathVariable Long courseId, @PathVariable Long attachmentId) { + Attachment attachment = attachmentRepository.findByIdElseThrow(attachmentId); + Course course = courseRepository.findByIdElseThrow(courseId); + checkAttachmentExistsInCourseOrThrow(course, attachment); + + return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), false); + } + /** * GET /files/attachments/lecture/{lectureId}/merge-pdf : Get the lecture units * PDF attachments merged @@ -428,6 +447,26 @@ public ResponseEntity getAttachmentUnitAttachment(@PathVariable Long att return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), false); } + /** + * GET files/courses/{courseId}/attachment-units/{attachmenUnitId} : Returns the file associated with the + * given attachmentUnit ID as a downloadable resource + * + * @param courseId The ID of the course that the Attachment belongs to + * @param attachmentUnitId the ID of the attachment to retrieve + * @return ResponseEntity containing the file as a resource + */ + @GetMapping("files/courses/{courseId}/attachment-units/{attachmentUnitId}") + @EnforceAtLeastEditorInCourse + public ResponseEntity getAttachmentUnitFile(@PathVariable Long courseId, @PathVariable Long attachmentUnitId) { + log.debug("REST request to get file for attachment unit : {}", attachmentUnitId); + AttachmentUnit attachmentUnit = attachmentUnitRepository.findByIdElseThrow(attachmentUnitId); + Course course = courseRepository.findByIdElseThrow(courseId); + Attachment attachment = attachmentUnit.getAttachment(); + checkAttachmentUnitExistsInCourseOrThrow(course, attachmentUnit); + + return buildFileResponse(getActualPathFromPublicPathString(attachment.getLink()), false); + } + /** * GET files/attachments/slides/attachment-unit/:attachmentUnitId/slide/:slideNumber : Get the lecture unit attachment slide by slide number * @@ -557,6 +596,30 @@ private void checkAttachmentAuthorizationOrThrow(Course course, Attachment attac } } + /** + * Checks if the attachment exists in the mentioned course + * + * @param course the course to check if the attachment is part of it + * @param attachment the attachment for which the existence should be checked + */ + private void checkAttachmentExistsInCourseOrThrow(Course course, Attachment attachment) { + if (!attachment.getLecture().getCourse().equals(course)) { + throw new EntityNotFoundException("This attachment does not exist in this course."); + } + } + + /** + * Checks if the attachment exists in the mentioned course + * + * @param course the course to check if the attachment is part of it + * @param attachmentUnit the attachment unit for which the existence should be checked + */ + private void checkAttachmentUnitExistsInCourseOrThrow(Course course, AttachmentUnit attachmentUnit) { + if (!attachmentUnit.getLecture().getCourse().equals(course)) { + throw new EntityNotFoundException("This attachment unit does not exist in this course."); + } + } + /** * Reads the file and turns it into a ResponseEntity * diff --git a/src/main/resources/config/application-dev.yml b/src/main/resources/config/application-dev.yml index ec007df2f264..56e3edc14e78 100644 --- a/src/main/resources/config/application-dev.yml +++ b/src/main/resources/config/application-dev.yml @@ -113,10 +113,12 @@ info: - programming_exercise_fail_tour: 'test' - programming_exercise_success_tour: 'test' - tutor_assessment_tour: 'Patterns in Software Engineering' - contact: artemis@xcit.tum.de #default value, can be overridden if needed # Specifies whether text assessment analytics service (TextAssessmentEventResource) is enabled/disabled # default value set to true for development environment text-assessment-analytics-enabled: true + operatorName: Some Artemis Operator # Must be set before starting the application in production. Shown in the about us page and sent to the telemetry service (e.g. the name of the university "Technische Universität München") + operatorAdminName: Admin # Can be set to be shown in the about us page, and to be sent to the telemetry service + contact: admin@uni.de # The admins contact email address, shown in the about us page, and sent to the telemetry service # Eureka configuration eureka: @@ -133,9 +135,15 @@ theia: images: java: - Java-17: "ghcr.io/ls1intum/theia/java-17:latest" - Java-Test: "ghcr.io/ls1intum/theia/java-test:latest" - Java-Test2: "ghcr.io/ls1intum/theia/java-test:2" + Java-17: "ghcr.io/ls1intum/theia/java-17:latest" + Java-Test: "ghcr.io/ls1intum/theia/java-test:latest" + Java-Test2: "ghcr.io/ls1intum/theia/java-test:2" c: C: "ghcr.io/ls1intum/theia/c:latest" +# Telemetry service: disabled for development +artemis: + telemetry: + enabled: false # Disable sending any telemetry information to the telemetry service by setting this to false + sendAdminDetails: false # Include the admins email and name in the telemetry data. Set to false to disable + destination: telemetry.artemis.cit.tum.de diff --git a/src/main/resources/config/application-prod.yml b/src/main/resources/config/application-prod.yml index 5fb8190cc112..7d86700066e4 100644 --- a/src/main/resources/config/application-prod.yml +++ b/src/main/resources/config/application-prod.yml @@ -15,6 +15,12 @@ artemis: push-notification-relay: https://hermes.artemis.cit.tum.de + # Artemis sends the artemis version, the university name, the universities main admin contact, (email + name), the server url, + # and used profiles (gitlab, localVC, ...) to a telemetry collection service. + telemetry: + enabled: true # Disable sending any telemetry information to the telemetry service by setting this to false + sendAdminDetails: true # Include personal identifiable information of the admin, including email and name in the telemetry data + destination: telemetry.artemis.cit.tum.de spring: devtools: @@ -119,8 +125,10 @@ info: - programming_exercise_fail_tour: 'tutorial' - programming_exercise_success_tour: 'tutorial' - tutor_assessment_tour: 'Patterns in Software Engineering' - contact: artemis@xcit.tum.de #default value, can be overridden on the server test-server: false # false --> production, true --> test server, --> empty == local # Specifies whether text assessment analytics service (TextAssessmentEventResource) is enabled/disabled # default value set to false in production text-assessment-analytics-enabled: false + operatorName: # Must be set before starting the application in production. Shown in the about us page and sent to the telemetry service (e.g. university or school) + operatorAdminName: # Can be set before starting the application in production. Shown in the about us page and used by the telemetry service + contact: # The admins contact email address, shown in the about us page, and sent to the telemetry service diff --git a/src/main/webapp/app/core/about-us/about-us.component.html b/src/main/webapp/app/core/about-us/about-us.component.html index aef3b249c38a..29eda0afe1a7 100644 --- a/src/main/webapp/app/core/about-us/about-us.component.html +++ b/src/main/webapp/app/core/about-us/about-us.component.html @@ -68,8 +68,35 @@

- {{ 'artemisApp.git.branch' | artemisTranslate }}: {{ gitBranchName }}
- {{ 'artemisApp.git.commit' | artemisTranslate }}: {{ gitCommitId }} +
+
{{ gitBranchName }}
+
+
+
{{ gitCommitId }}
+
+ +
+

+
+
+
{{ operatorName }}
+
+ @if (operatorAdminName) { +
+
+
{{ operatorAdminName }}
+
+ } + @if (operatorContactEmail) { + + }
diff --git a/src/main/webapp/app/core/about-us/about-us.component.scss b/src/main/webapp/app/core/about-us/about-us.component.scss index 18f8bfcd0a33..6a8714e970e6 100644 --- a/src/main/webapp/app/core/about-us/about-us.component.scss +++ b/src/main/webapp/app/core/about-us/about-us.component.scss @@ -37,3 +37,13 @@ flex: 0 0 50%; max-width: 50%; } + +.inline-block { + display: inline-block; + vertical-align: middle; + margin-right: 20px; +} + +.fixed-width { + width: 200px; +} diff --git a/src/main/webapp/app/core/about-us/about-us.component.ts b/src/main/webapp/app/core/about-us/about-us.component.ts index aa8f35742728..42ba47b5093a 100644 --- a/src/main/webapp/app/core/about-us/about-us.component.ts +++ b/src/main/webapp/app/core/about-us/about-us.component.ts @@ -19,8 +19,11 @@ export class AboutUsComponent implements OnInit { email: string; data: AboutUsModel; - gitCommitId: string; - gitBranchName: string; + gitCommitId?: string; + gitBranchName?: string; + operatorName?: string; + operatorAdminName?: string; + operatorContactEmail?: string; // Array of tuple containing translation keys and translation values readonly SECTIONS: [string, { [key: string]: string }][] = [ @@ -61,7 +64,10 @@ export class AboutUsComponent implements OnInit { ngOnInit(): void { this.staticContentService.getStaticJsonFromArtemisServer('about-us.json').subscribe((data) => { // Map contributors into the model, as the returned data are just plain objects - this.data = { ...data, contributors: data.contributors.map((con: any) => new ContributorModel(con.fullName, con.photoDirectory, con.sortBy, con.role, con.website)) }; + this.data = { + ...data, + contributors: data.contributors.map((con: any) => new ContributorModel(con.fullName, con.photoDirectory, con.sortBy, con.role, con.website)), + }; // Sort by last name // Either the last "word" in the name, or the dedicated sortBy field, if present @@ -74,9 +80,11 @@ export class AboutUsComponent implements OnInit { this.gitCommitId = profileInfo.git.commit.id.abbrev; this.gitBranchName = profileInfo.git.branch; } + this.operatorName = profileInfo.operatorName; + this.operatorAdminName = profileInfo.operatorAdminName; + this.operatorContactEmail = profileInfo.contact; }); } - /** * Create the mail reference for the contact */ diff --git a/src/main/webapp/app/core/language/language.helper.ts b/src/main/webapp/app/core/language/language.helper.ts index 036c2f63699c..60cfe80b69c2 100644 --- a/src/main/webapp/app/core/language/language.helper.ts +++ b/src/main/webapp/app/core/language/language.helper.ts @@ -98,7 +98,7 @@ export class JhiLanguageHelper { return 'en'; } - public getNavigatorReference(): any { + public getNavigatorReference(): Navigator { return navigator; } } diff --git a/src/main/webapp/app/home/home.component.html b/src/main/webapp/app/home/home.component.html index 09685f917014..2ac14640fc07 100644 --- a/src/main/webapp/app/home/home.component.html +++ b/src/main/webapp/app/home/home.component.html @@ -13,8 +13,8 @@

Please sign in with your account.
} @if (accountName) { -
- Please sign in with your account. +
+ Sign in with your account.
} +
+ + + diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss new file mode 100644 index 000000000000..1ce2019305cb --- /dev/null +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.scss @@ -0,0 +1,89 @@ +.pdf-container { + position: relative; + display: grid; + grid-template-columns: repeat(auto-fill, minmax(250px, 1fr)); + gap: 30px; + max-height: 60vh; + height: 60vh; + overflow-y: auto; + border: 1px solid var(--border-color); + padding: 10px; + margin: 10px; + width: 95%; + box-shadow: 0 2px 5px var(--pdf-preview-pdf-container-shadow); + align-items: start; + z-index: 0; + + @media (max-width: 800px) { + grid-template-columns: repeat(2, 1fr); + } + + @media (max-width: 500px) { + grid-template-columns: 1fr; + } +} + +.enlarged-container { + position: absolute; + top: 0; + left: 0; + width: 100%; + height: 100%; + display: flex; + justify-content: center; + align-items: center; + background-color: var(--pdf-preview-enlarged-container-overlay); + z-index: 2; + + .btn-close { + position: absolute; + top: 10px; + right: 10px; + cursor: pointer; + color: var(--bs-body-color); + } +} + +.nav-button { + position: absolute; + transform: translateY(-50%); + cursor: pointer; + border-radius: 50%; + width: 30px; + height: 30px; + display: flex; + justify-content: center; + align-items: center; + font-size: 20px; + z-index: 3; +} + +.nav-button.left { + left: calc(5% + 10px); + + @media (max-width: 1200px) { + left: 10px; + } +} + +.nav-button.right { + right: calc(5% + 10px); + + @media (max-width: 1200px) { + right: 10px; + } +} + +.page-number-display { + position: absolute; + bottom: 10px; + right: calc(5% + 10px); + font-size: 18px; + color: var(--bs-body-color); + z-index: 2; + text-shadow: 0 1px 3px rgba(0, 0, 0, 0.3); + + @media (max-width: 1200px) { + right: 10px; + } +} diff --git a/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts new file mode 100644 index 000000000000..f2075dc57fbb --- /dev/null +++ b/src/main/webapp/app/lecture/pdf-preview/pdf-preview.component.ts @@ -0,0 +1,341 @@ +import { Component, ElementRef, HostListener, OnDestroy, OnInit, ViewChild } from '@angular/core'; +import { ActivatedRoute } from '@angular/router'; +import { AttachmentService } from 'app/lecture/attachment.service'; +import * as PDFJS from 'pdfjs-dist'; +import 'pdfjs-dist/build/pdf.worker'; +import { Attachment } from 'app/entities/attachment.model'; +import { AttachmentUnit } from 'app/entities/lecture-unit/attachmentUnit.model'; +import { AttachmentUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service'; +import { onError } from 'app/shared/util/global.utils'; +import { AlertService } from 'app/core/util/alert.service'; +import { Subscription } from 'rxjs'; +import { Course } from 'app/entities/course.model'; +import { HttpErrorResponse } from '@angular/common/http'; +import { ArtemisSharedModule } from 'app/shared/shared.module'; + +type NavigationDirection = 'next' | 'prev'; + +@Component({ + selector: 'jhi-pdf-preview-component', + templateUrl: './pdf-preview.component.html', + styleUrls: ['./pdf-preview.component.scss'], + standalone: true, + imports: [ArtemisSharedModule], +}) +export class PdfPreviewComponent implements OnInit, OnDestroy { + @ViewChild('pdfContainer', { static: true }) pdfContainer: ElementRef; + @ViewChild('enlargedCanvas') enlargedCanvas: ElementRef; + + readonly DEFAULT_SLIDE_WIDTH = 250; + course?: Course; + attachment?: Attachment; + attachmentUnit?: AttachmentUnit; + isEnlargedView = false; + currentPage = 1; + totalPages = 0; + attachmentSub: Subscription; + attachmentUnitSub: Subscription; + + constructor( + public route: ActivatedRoute, + private attachmentService: AttachmentService, + private attachmentUnitService: AttachmentUnitService, + private alertService: AlertService, + ) {} + + ngOnInit() { + this.route.data.subscribe((data) => { + this.course = data.course; + if ('attachment' in data) { + this.attachment = data.attachment; + this.attachmentSub = this.attachmentService.getAttachmentFile(this.course!.id!, this.attachment!.id!).subscribe({ + next: (blob: Blob) => this.loadPdf(URL.createObjectURL(blob)), + error: (error: HttpErrorResponse) => onError(this.alertService, error), + }); + } else if ('attachmentUnit' in data) { + this.attachmentUnit = data.attachmentUnit; + this.attachmentUnitSub = this.attachmentUnitService.getAttachmentFile(this.course!.id!, this.attachmentUnit!.id!).subscribe({ + next: (blob: Blob) => this.loadPdf(URL.createObjectURL(blob)), + error: (error: HttpErrorResponse) => onError(this.alertService, error), + }); + } + }); + } + + ngOnDestroy() { + this.attachmentSub?.unsubscribe(); + this.attachmentUnitSub?.unsubscribe(); + } + + /** + * Handles keyboard events for navigation within the PDF viewer. + * @param event The keyboard event captured. + */ + @HostListener('document:keydown', ['$event']) + handleKeyboardEvents(event: KeyboardEvent) { + if (this.isEnlargedView) { + if (event.key === 'ArrowRight' && this.currentPage < this.totalPages) { + this.navigatePages('next'); + } else if (event.key === 'ArrowLeft' && this.currentPage > 1) { + this.navigatePages('prev'); + } + } + } + + /** + * Adjusts the canvas size based on the window resize event to ensure proper display. + */ + @HostListener('window:resize') + resizeCanvasBasedOnContainer() { + this.adjustCanvasSize(); + } + + /** + * Loads a PDF from a provided URL and initializes viewer setup. + * @param fileUrl The URL of the file to load. + */ + async loadPdf(fileUrl: string) { + try { + const loadingTask = PDFJS.getDocument(fileUrl); + const pdf = await loadingTask.promise; + this.totalPages = pdf.numPages; + + for (let i = 1; i <= pdf.numPages; i++) { + const page = await pdf.getPage(i); + const viewport = page.getViewport({ scale: 2 }); + const canvas = this.createCanvas(viewport); + const context = canvas.getContext('2d'); + if (context) { + await page.render({ canvasContext: context, viewport }).promise; + } + + const container = this.createContainer(canvas, i); + this.pdfContainer.nativeElement.appendChild(container); + } + + URL.revokeObjectURL(fileUrl); + } catch (error) { + onError(this.alertService, error); + } + } + + /** + * Creates a canvas for each page of the PDF to allow for individual page rendering. + * @param viewport The viewport settings used for rendering the page. + * @returns A new HTMLCanvasElement configured for the PDF page. + */ + private createCanvas(viewport: PDFJS.PageViewport): HTMLCanvasElement { + const canvas = document.createElement('canvas'); + /* Canvas styling is predefined because Canvas tags do not support CSS classes + * as they are not HTML elements but rather a bitmap drawing surface. + * See: https://stackoverflow.com/a/29675448 + * */ + canvas.height = viewport.height; + canvas.width = viewport.width; + const fixedWidth = this.DEFAULT_SLIDE_WIDTH; + const scaleFactor = fixedWidth / viewport.width; + canvas.style.width = `${fixedWidth}px`; + canvas.style.height = `${viewport.height * scaleFactor}px`; + return canvas; + } + + /** + * Creates a container div for each canvas, facilitating layering and interaction. + * @param canvas The canvas element that displays a PDF page. + * @param pageIndex The index of the page within the PDF document. + * @returns A configured div element that includes the canvas and interactive overlays. + */ + createContainer(canvas: HTMLCanvasElement, pageIndex: number): HTMLDivElement { + const container = document.createElement('div'); + /* Dynamically created elements are not detected by DOM, that is why we need to set the styles manually. + * See: https://stackoverflow.com/a/70911189 + */ + container.classList.add('pdf-page-container'); + container.style.cssText = `position: relative; display: inline-block; width: ${canvas.style.width}; height: ${canvas.style.height}; margin: 20px; box-shadow: 0 2px 6px var(--pdf-preview-canvas-shadow);`; + + const overlay = this.createOverlay(pageIndex); + container.appendChild(canvas); + container.appendChild(overlay); + + container.addEventListener('mouseenter', () => { + overlay.style.opacity = '1'; + }); + container.addEventListener('mouseleave', () => { + overlay.style.opacity = '0'; + }); + overlay.addEventListener('click', () => this.displayEnlargedCanvas(canvas, pageIndex)); + + return container; + } + + /** + * Generates an interactive overlay for each PDF page to allow for user interactions. + * @param pageIndex The index of the page. + * @returns A div element styled as an overlay. + */ + private createOverlay(pageIndex: number): HTMLDivElement { + const overlay = document.createElement('div'); + overlay.innerHTML = `${pageIndex}`; + /* Dynamically created elements are not detected by DOM, that is why we need to set the styles manually. + * See: https://stackoverflow.com/a/70911189 + */ + overlay.style.cssText = `position: absolute; top: 0; left: 0; width: 100%; height: 100%; display: flex; justify-content: center; align-items: center; font-size: 24px; color: white; z-index: 1; transition: opacity 0.3s ease; opacity: 0; cursor: pointer; background-color: var(--pdf-preview-container-overlay)`; + return overlay; + } + + /** + * Dynamically updates the canvas size within an enlarged view based on the viewport. + */ + adjustCanvasSize = () => { + if (this.isEnlargedView) { + const canvasElements = this.pdfContainer.nativeElement.querySelectorAll('.pdf-page-container canvas'); + if (this.currentPage - 1 < canvasElements.length) { + const canvas = canvasElements[this.currentPage - 1] as HTMLCanvasElement; + this.updateEnlargedCanvas(canvas); + } + } + }; + + /** + * Displays a canvas in an enlarged view for detailed examination. + * @param originalCanvas The original canvas element displaying the page. + * @param pageIndex The index of the page being displayed. + */ + displayEnlargedCanvas(originalCanvas: HTMLCanvasElement, pageIndex: number) { + this.isEnlargedView = true; + this.currentPage = pageIndex; + this.updateEnlargedCanvas(originalCanvas); + this.toggleBodyScroll(true); + } + /** + * Updates the enlarged canvas dimensions to optimize PDF page display within the current viewport. + * This method dynamically adjusts the size, position, and scale of the canvas to maintain the aspect ratio, + * ensuring the content is centered and displayed appropriately within the available space. + * It is called within an animation frame to synchronize updates with the browser's render cycle for smooth visuals. + * + * @param {HTMLCanvasElement} originalCanvas - The source canvas element used to extract image data for resizing and redrawing. + */ + updateEnlargedCanvas(originalCanvas: HTMLCanvasElement) { + requestAnimationFrame(() => { + if (!this.isEnlargedView) return; + + const scaleFactor = this.calculateScaleFactor(originalCanvas); + this.resizeCanvas(originalCanvas, scaleFactor); + this.redrawCanvas(originalCanvas); + this.positionCanvas(); + }); + } + + /** + * Calculates the scaling factor to adjust the canvas size based on the dimensions of the container. + * This method ensures that the canvas is scaled to fit within the container without altering the aspect ratio. + * + * @param {HTMLCanvasElement} originalCanvas - The original canvas element representing the PDF page. + * @returns {number} The scaling factor used to resize the original canvas to fit within the container dimensions. + */ + calculateScaleFactor(originalCanvas: HTMLCanvasElement): number { + const containerWidth = this.pdfContainer.nativeElement.clientWidth; + const containerHeight = this.pdfContainer.nativeElement.clientHeight; + const scaleX = containerWidth / originalCanvas.width; + const scaleY = containerHeight / originalCanvas.height; + return Math.min(scaleX, scaleY); + } + + /** + * Resizes the canvas according to the computed scale factor. + * This method updates the dimensions of the enlarged canvas element to ensure that the entire PDF page + * is visible and properly scaled within the viewer. + * + * @param {HTMLCanvasElement} originalCanvas - The canvas element from which the image is scaled. + * @param {number} scaleFactor - The factor by which the canvas is resized. + */ + resizeCanvas(originalCanvas: HTMLCanvasElement, scaleFactor: number): void { + const enlargedCanvas = this.enlargedCanvas.nativeElement; + enlargedCanvas.width = originalCanvas.width * scaleFactor; + enlargedCanvas.height = originalCanvas.height * scaleFactor; + } + + /** + * Redraws the original canvas content onto the enlarged canvas at the updated scale. + * This method ensures that the image is rendered clearly and correctly positioned on the enlarged canvas. + * + * @param {HTMLCanvasElement} originalCanvas - The original canvas containing the image to be redrawn. + */ + redrawCanvas(originalCanvas: HTMLCanvasElement): void { + const enlargedCanvas = this.enlargedCanvas.nativeElement; + const context = enlargedCanvas.getContext('2d'); + if (context) { + context.clearRect(0, 0, enlargedCanvas.width, enlargedCanvas.height); + context.drawImage(originalCanvas, 0, 0, enlargedCanvas.width, enlargedCanvas.height); + } + } + + /** + * Adjusts the position of the enlarged canvas to center it within the viewport of the PDF container. + * This method ensures that the canvas is both vertically and horizontally centered, providing a consistent + * and visually appealing layout. + */ + positionCanvas(): void { + const enlargedCanvas = this.enlargedCanvas.nativeElement; + const containerWidth = this.pdfContainer.nativeElement.clientWidth; + const containerHeight = this.pdfContainer.nativeElement.clientHeight; + + enlargedCanvas.style.position = 'absolute'; + enlargedCanvas.style.left = `${(containerWidth - enlargedCanvas.width) / 2}px`; + enlargedCanvas.style.top = `${(containerHeight - enlargedCanvas.height) / 2}px`; + enlargedCanvas.parentElement!.style.top = `${this.pdfContainer.nativeElement.scrollTop}px`; + } + + /** + * Closes the enlarged view of the PDF and re-enables scrolling in the PDF container. + */ + closeEnlargedView(event: MouseEvent) { + this.isEnlargedView = false; + this.toggleBodyScroll(false); + event.stopPropagation(); + } + + /** + * Toggles the ability to scroll through the PDF container. + * @param disable A boolean flag indicating whether scrolling should be disabled (`true`) or enabled (`false`). + */ + toggleBodyScroll(disable: boolean): void { + this.pdfContainer.nativeElement.style.overflow = disable ? 'hidden' : 'auto'; + } + + /** + * Closes the enlarged view if a click event occurs outside the actual canvas area but within the enlarged container. + * @param event The mouse event captured, used to determine the location of the click. + */ + closeIfOutside(event: MouseEvent): void { + const target = event.target as HTMLElement; + const enlargedCanvas = this.enlargedCanvas.nativeElement; + + if (target.classList.contains('enlarged-container') && target !== enlargedCanvas) { + this.closeEnlargedView(event); + } + } + + /** + * Handles navigation between PDF pages and stops event propagation to prevent unwanted side effects. + * @param direction The direction to navigate. + * @param event The MouseEvent to be stopped. + */ + handleNavigation(direction: NavigationDirection, event: MouseEvent): void { + event.stopPropagation(); + this.navigatePages(direction); + } + + /** + * Navigates to a specific page in the PDF based on the direction relative to the current page. + * @param direction The navigation direction (next or previous). + */ + navigatePages(direction: NavigationDirection) { + const nextPageIndex = direction === 'next' ? this.currentPage + 1 : this.currentPage - 1; + if (nextPageIndex > 0 && nextPageIndex <= this.totalPages) { + this.currentPage = nextPageIndex; + const canvas = this.pdfContainer.nativeElement.querySelectorAll('.pdf-page-container canvas')[this.currentPage - 1] as HTMLCanvasElement; + this.updateEnlargedCanvas(canvas); + } + } +} diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard-step.component.ts b/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard-step.component.ts index 3827abac634f..9c8d6973a233 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard-step.component.ts +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard-step.component.ts @@ -19,6 +19,6 @@ export class LectureUpdateWizardStepComponent { @Input() descriptionTranslationKey: string; - faCheck = faCheck; - faDotCircle = faDotCircle; + protected readonly faCheck = faCheck; + protected readonly faDotCircle = faDotCircle; } diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard.component.html b/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard.component.html index 71a77421d145..cb704901206e 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard.component.html +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard.component.html @@ -1,19 +1,16 @@
- @if (currentStep >= 1) { + @if (currentStep >= LECTURE_UPDATE_WIZARD_TITLE_STEP) { } - @if (currentStep >= 2) { + @if (currentStep >= LECTURE_UPDATE_WIZARD_PERIOD_STEP) { } - @if (currentStep >= 3) { + @if (currentStep >= LECTURE_UPDATE_WIZARD_ATTACHMENT_STEP) { } - @if (currentStep >= 4) { + @if (currentStep >= LECTURE_UPDATE_WIZARD_UNIT_STEP) { } - @if (currentStep >= 5) { - - }
@@ -24,38 +21,32 @@
-
+
-
+
-
+
-
-
-
diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard.component.ts b/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard.component.ts index ea23823ea0dd..d360ffc962ca 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard.component.ts +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-update-wizard.component.ts @@ -5,7 +5,6 @@ import { Lecture } from 'app/entities/lecture.model'; import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { faArrowRight, faCheck, faHandshakeAngle } from '@fortawesome/free-solid-svg-icons'; import { LectureUpdateWizardUnitsComponent } from 'app/lecture/wizard-mode/lecture-wizard-units.component'; -import { LectureUpdateWizardCompetenciesComponent } from 'app/lecture/wizard-mode/lecture-wizard-competencies.component'; import { take } from 'rxjs/operators'; @Component({ @@ -21,7 +20,11 @@ export class LectureUpdateWizardComponent implements OnInit { @Input() isSaving: boolean; @ViewChild(LectureUpdateWizardUnitsComponent, { static: false }) unitsComponent: LectureUpdateWizardUnitsComponent; - @ViewChild(LectureUpdateWizardCompetenciesComponent, { static: false }) competenciesComponent: LectureUpdateWizardCompetenciesComponent; + + readonly LECTURE_UPDATE_WIZARD_TITLE_STEP = 1; + readonly LECTURE_UPDATE_WIZARD_PERIOD_STEP = 2; + readonly LECTURE_UPDATE_WIZARD_ATTACHMENT_STEP = 3; + readonly LECTURE_UPDATE_WIZARD_UNIT_STEP = 4; currentStep: number; @@ -47,7 +50,13 @@ export class LectureUpdateWizardComponent implements OnInit { if (params.step && !isNaN(+params.step)) { this.currentStep = +params.step; } else { - this.currentStep = this.lecture.id ? 5 : this.lecture.startDate !== undefined || this.lecture.endDate !== undefined ? 2 : 1; + if (this.lecture.id) { + this.currentStep = this.LECTURE_UPDATE_WIZARD_UNIT_STEP; + } else if (this.lecture.startDate === undefined && this.lecture.endDate === undefined) { + this.currentStep = this.LECTURE_UPDATE_WIZARD_TITLE_STEP; + } else if (!this.lecture.id) { + this.currentStep = this.LECTURE_UPDATE_WIZARD_PERIOD_STEP; + } } this.router.navigate([], { @@ -62,7 +71,7 @@ export class LectureUpdateWizardComponent implements OnInit { * Progress to the next step of the wizard mode */ next() { - if (this.currentStep === 2 || this.currentStep === 5) { + if (this.currentStep === this.LECTURE_UPDATE_WIZARD_PERIOD_STEP || this.currentStep === this.LECTURE_UPDATE_WIZARD_UNIT_STEP) { this.saveLectureFunction(); return; } @@ -77,30 +86,16 @@ export class LectureUpdateWizardComponent implements OnInit { this.currentStep++; } - /** - * Checks if the given step has already been completed - */ - isCompleted(step: number) { - return this.currentStep > step; - } - - /** - * Checks if the given step is the current one - */ - isCurrent(step: number) { - return this.currentStep === step; - } - getNextIcon() { - return this.currentStep < 5 ? faArrowRight : faCheck; + return this.currentStep < this.LECTURE_UPDATE_WIZARD_UNIT_STEP ? faArrowRight : faCheck; } getNextText() { - return this.currentStep < 5 ? 'artemisApp.lecture.home.nextStepLabel' : 'entity.action.finish'; + return this.currentStep < this.LECTURE_UPDATE_WIZARD_UNIT_STEP ? 'artemisApp.lecture.home.nextStepLabel' : 'entity.action.finish'; } toggleWizardMode() { - if (this.currentStep <= 2) { + if (this.currentStep <= this.LECTURE_UPDATE_WIZARD_PERIOD_STEP) { this.toggleModeFunction(); } else { this.router.navigate(['course-management', this.lecture.course!.id, 'lectures', this.lecture.id]); diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.html b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.html deleted file mode 100644 index e7e90982591c..000000000000 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.html +++ /dev/null @@ -1,97 +0,0 @@ -
-

-

- -

-
-
- @if (competencies && competencies.length > 0) { - - - - - - - - - - - - @for (competency of competencies; track trackCompetencyId($index, competency)) { - - - - - - - } - -
ID - - - -
- {{ competency.id }} - @if (currentlyProcessedCompetency?.id === competency?.id) { -
- } -
{{ competency.title }}{{ getConnectedUnitsForCompetency(competency) }} -
- - - @if (lecture.isAtLeastInstructor) { - - } -
-
- - } -
-
- @if (isLoadingCompetencies) { -
-
- -
-
- } - @if (!isEditingCompetency) { -
- -
- } - @if (isLoadingCompetencyForm) { -
-
- -
-
- } - @if ((isAddingCompetency || isEditingCompetency || isConnectingCompetency) && !isLoadingCompetencyForm) { - - } -
diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.scss b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.scss deleted file mode 100644 index b23d804a9fd8..000000000000 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.scss +++ /dev/null @@ -1,14 +0,0 @@ -.edit-overlay { - position: absolute; - left: 0; - right: 0; - top: -1px; - display: flex; - justify-content: center; - align-items: center; - background-color: var(--lecture-attachment-edit-overlay-color); - z-index: 9; - font-size: 18px; - height: calc(100% + 1px); - border: none; -} diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.ts b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.ts deleted file mode 100644 index 8b6253399a31..000000000000 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-competencies.component.ts +++ /dev/null @@ -1,273 +0,0 @@ -import { Component, Input, OnInit } from '@angular/core'; -import { Observable, Subject } from 'rxjs'; -import { Lecture } from 'app/entities/lecture.model'; -import { Competency } from 'app/entities/competency.model'; -import { onError } from 'app/shared/util/global.utils'; -import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; -import { faLink, faPencilAlt } from '@fortawesome/free-solid-svg-icons'; -import { HttpErrorResponse, HttpResponse } from '@angular/common/http'; -import { AlertService } from 'app/core/util/alert.service'; -import { LectureService } from 'app/lecture/lecture.service'; -import { CompetencyService } from 'app/course/competencies/competency.service'; -import { finalize } from 'rxjs/operators'; -import { TranslateService } from '@ngx-translate/core'; -import { ExerciseUnit } from 'app/entities/lecture-unit/exerciseUnit.model'; -import { CourseCompetencyFormData } from 'app/course/competencies/forms/course-competency-form.component'; - -@Component({ - selector: 'jhi-lecture-update-wizard-competencies', - templateUrl: './lecture-wizard-competencies.component.html', - styleUrls: ['./lecture-wizard-competencies.component.scss'], -}) -export class LectureUpdateWizardCompetenciesComponent implements OnInit { - @Input() currentStep: number; - @Input() lecture: Lecture; - @Input() isSaving: boolean; - - isAddingCompetency: boolean; - isLoadingCompetencyForm: boolean; - isLoadingCompetencies: boolean; - isEditingCompetency: boolean; - isConnectingCompetency: boolean; - - currentlyProcessedCompetency: Competency; - competencies: Competency[] = []; - competencyFormData: CourseCompetencyFormData; - - private dialogErrorSource = new Subject(); - dialogError$ = this.dialogErrorSource.asObservable(); - - faPencilAlt = faPencilAlt; - faLink = faLink; - - constructor( - protected alertService: AlertService, - protected lectureService: LectureService, - protected competencyService: CompetencyService, - protected courseCompetencyService: CompetencyService, - protected translateService: TranslateService, - ) {} - - ngOnInit() { - this.loadCompetencies(); - } - - showCreateCompetency() { - this.isLoadingCompetencyForm = true; - this.isConnectingCompetency = false; - this.isAddingCompetency = !this.isAddingCompetency; - this.competencyFormData = { - id: undefined, - title: undefined, - description: undefined, - taxonomy: undefined, - connectedLectureUnits: undefined, - }; - - this.subscribeToLoadUnitResponse(this.lectureService.findWithDetails(this.lecture.id!)); - } - - protected subscribeToLoadUnitResponse(result: Observable>) { - result.subscribe({ - next: (response: HttpResponse) => this.onLoadUnitSuccess(response.body!), - error: (error: HttpErrorResponse) => this.onLoadError(error), - }); - } - - protected subscribeToLoadCompetenciesResponse(result: Observable>) { - result.subscribe({ - next: (response: HttpResponse) => this.onLoadCompetenciesSuccess(response.body!), - error: (error: HttpErrorResponse) => this.onLoadError(error), - }); - } - - /** - * Action on successful lecture unit fetch - */ - protected onLoadUnitSuccess(lecture: Lecture) { - this.lecture = lecture; - - this.isLoadingCompetencyForm = false; - } - - /** - * Action on successful competencies fetch - */ - protected onLoadCompetenciesSuccess(competencies: Competency[]) { - this.isLoadingCompetencies = false; - - this.competencies = competencies; - } - - /** - * Action on unsuccessful fetch - * @param error the error handed to the alert service - */ - protected onLoadError(error: HttpErrorResponse) { - this.isSaving = false; - this.isLoadingCompetencyForm = false; - this.isLoadingCompetencies = false; - - onError(this.alertService, error); - } - - onCompetencyFormSubmitted(formData: CourseCompetencyFormData) { - if (this.isEditingCompetency) { - this.editCompetency(formData); - } else { - this.createCompetency(formData); - } - } - - createCompetency(formData: CourseCompetencyFormData) { - if (!formData?.title) { - return; - } - - const { title, description, taxonomy, connectedLectureUnits } = formData; - this.currentlyProcessedCompetency = {}; - - this.currentlyProcessedCompetency.title = title; - this.currentlyProcessedCompetency.description = description; - this.currentlyProcessedCompetency.taxonomy = taxonomy; - this.currentlyProcessedCompetency.lectureUnits = connectedLectureUnits; - - this.isLoadingCompetencyForm = true; - - this.competencyService - .create(this.currentlyProcessedCompetency!, this.lecture.course!.id!) - .pipe( - finalize(() => { - this.isLoadingCompetencyForm = false; - }), - ) - .subscribe({ - next: (response: HttpResponse) => { - this.isAddingCompetency = false; - - // The rest api is returning lecture units and exercises separately after creating/editing but we - // need the unit to show it in the table as connected. Since it's only for showing it, as a - // workaround we take the unit from the lecture which is the same one. - const newCompetency = response.body!; - const exerciseUnits = this.lecture.lectureUnits?.filter((unit: ExerciseUnit) => newCompetency.exercises?.find((exercise) => exercise.id === unit.exercise?.id)); - newCompetency.lectureUnits = newCompetency.lectureUnits?.concat(exerciseUnits ?? []); - - this.competencies = this.competencies.concat(newCompetency); - - this.alertService.success(`Competency ${this.currentlyProcessedCompetency.title} was successfully created.`); - }, - error: (res: HttpErrorResponse) => onError(this.alertService, res), - }); - } - - editCompetency(formData: CourseCompetencyFormData) { - const { title, description, taxonomy, connectedLectureUnits } = formData; - - this.currentlyProcessedCompetency.title = title; - this.currentlyProcessedCompetency.description = description; - this.currentlyProcessedCompetency.taxonomy = taxonomy; - this.currentlyProcessedCompetency.lectureUnits = connectedLectureUnits; - - this.isLoadingCompetencyForm = true; - - this.competencyService - .update(this.currentlyProcessedCompetency, this.lecture.course!.id!) - .pipe( - finalize(() => { - this.isLoadingCompetencyForm = false; - }), - ) - .subscribe({ - next: (response: HttpResponse) => { - this.isEditingCompetency = false; - this.isConnectingCompetency = false; - - // The rest api is returning lecture units and exercises separately after creating/editing but we - // need the unit to show it in the table as connected. Since it's only for showing it, as a - // workaround we take the unit from the lecture which is the same one. - const editedCompetency = response.body!; - const exerciseUnits = this.lecture.lectureUnits?.filter((unit: ExerciseUnit) => - editedCompetency.exercises?.find((exercise) => exercise.id === unit.exercise?.id), - ); - editedCompetency.lectureUnits = editedCompetency.lectureUnits?.concat(exerciseUnits ?? []); - - const index = this.competencies.findIndex((competency) => competency.id === this.currentlyProcessedCompetency.id); - if (index === -1) { - this.competencies = this.competencies.concat(editedCompetency); - } else { - this.competencies[index] = editedCompetency; - } - - this.alertService.success(`Competency ${this.currentlyProcessedCompetency.title} was successfully edited.`); - this.currentlyProcessedCompetency = {}; - }, - error: (res: HttpErrorResponse) => onError(this.alertService, res), - }); - } - - trackCompetencyId(index: number, item: Competency) { - return item.id; - } - - loadCompetencies() { - this.isLoadingCompetencies = true; - this.isLoadingCompetencyForm = true; - - this.subscribeToLoadCompetenciesResponse(this.courseCompetencyService.getAllForCourse(this.lecture.course!.id!)); - this.subscribeToLoadUnitResponse(this.lectureService.findWithDetails(this.lecture.id!)); - } - - getConnectedUnitsForCompetency(competency: Competency) { - const units = competency.lectureUnits?.filter((unit) => this.lecture.lectureUnits?.find((u) => u.id === unit.id)); - - if (units === undefined || units.length === 0) { - return this.translateService.instant('artemisApp.lecture.wizardMode.competencyNoConnectedUnits'); - } - - return units.map((unit) => unit.name).join(', '); - } - - startEditCompetency(competency: Competency) { - const connectedUnits: LectureUnit[] = []; - competency.lectureUnits?.forEach((unit) => connectedUnits.push(Object.assign({}, unit))); - - this.isLoadingCompetencyForm = true; - this.isEditingCompetency = true; - this.currentlyProcessedCompetency = competency; - - this.competencyFormData = { - id: competency.id, - title: competency.title, - description: competency.description, - taxonomy: competency.taxonomy, - connectedLectureUnits: connectedUnits, - }; - - this.subscribeToLoadUnitResponse(this.lectureService.findWithDetails(this.lecture.id!)); - } - - startConnectingCompetency(competency: Competency) { - this.isConnectingCompetency = true; - - this.startEditCompetency(competency); - } - - deleteCompetency(competency: Competency) { - this.competencyService.delete(competency.id!, this.lecture.course!.id!).subscribe({ - next: () => { - this.competencies = this.competencies.filter((existingCompetency) => existingCompetency.id !== competency.id); - this.dialogErrorSource.next(''); - }, - error: (error: HttpErrorResponse) => this.dialogErrorSource.next(error.message), - }); - } - - onCompetencyFormCanceled() { - this.isAddingCompetency = false; - this.isEditingCompetency = false; - this.isConnectingCompetency = false; - this.isLoadingCompetencyForm = false; - - this.currentlyProcessedCompetency = {}; - } -} diff --git a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-units.component.ts b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-units.component.ts index 71d2484fbba4..9d08f4063e84 100644 --- a/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-units.component.ts +++ b/src/main/webapp/app/lecture/wizard-mode/lecture-wizard-units.component.ts @@ -105,12 +105,13 @@ export class LectureUpdateWizardUnitsComponent implements OnInit { return; } - const { name, releaseDate, content } = formData; + const { name, releaseDate, content, competencies } = formData; this.currentlyProcessedTextUnit = this.isEditingLectureUnit ? this.currentlyProcessedTextUnit : new TextUnit(); this.currentlyProcessedTextUnit.name = name; this.currentlyProcessedTextUnit.releaseDate = releaseDate; this.currentlyProcessedTextUnit.content = content; + this.currentlyProcessedTextUnit.competencies = competencies; (this.isEditingLectureUnit ? this.textUnitService.update(this.currentlyProcessedTextUnit, this.lecture.id!) @@ -129,13 +130,14 @@ export class LectureUpdateWizardUnitsComponent implements OnInit { return; } - const { name, description, releaseDate, source } = formData; + const { name, description, releaseDate, source, competencies } = formData; this.currentlyProcessedVideoUnit = this.isEditingLectureUnit ? this.currentlyProcessedVideoUnit : new VideoUnit(); this.currentlyProcessedVideoUnit.name = name || undefined; this.currentlyProcessedVideoUnit.releaseDate = releaseDate || undefined; this.currentlyProcessedVideoUnit.description = description || undefined; this.currentlyProcessedVideoUnit.source = source || undefined; + this.currentlyProcessedVideoUnit.competencies = competencies; (this.isEditingLectureUnit ? this.videoUnitService.update(this.currentlyProcessedVideoUnit, this.lecture.id!) @@ -154,13 +156,14 @@ export class LectureUpdateWizardUnitsComponent implements OnInit { return; } - const { name, description, releaseDate, source } = formData; + const { name, description, releaseDate, source, competencies } = formData; this.currentlyProcessedOnlineUnit = this.isEditingLectureUnit ? this.currentlyProcessedOnlineUnit : new OnlineUnit(); this.currentlyProcessedOnlineUnit.name = name || undefined; this.currentlyProcessedOnlineUnit.releaseDate = releaseDate || undefined; this.currentlyProcessedOnlineUnit.description = description || undefined; this.currentlyProcessedOnlineUnit.source = source || undefined; + this.currentlyProcessedOnlineUnit.competencies = competencies || undefined; (this.isEditingLectureUnit ? this.onlineUnitService.update(this.currentlyProcessedOnlineUnit, this.lecture.id!) @@ -179,7 +182,7 @@ export class LectureUpdateWizardUnitsComponent implements OnInit { return; } - const { description, name, releaseDate, updateNotificationText } = attachmentUnitFormData.formProperties; + const { description, name, releaseDate, updateNotificationText, competencies } = attachmentUnitFormData.formProperties; const { file, fileName } = attachmentUnitFormData.fileProperties; this.currentlyProcessedAttachmentUnit = this.isEditingLectureUnit ? this.currentlyProcessedAttachmentUnit : new AttachmentUnit(); @@ -210,6 +213,9 @@ export class LectureUpdateWizardUnitsComponent implements OnInit { if (description) { this.currentlyProcessedAttachmentUnit.description = description; } + if (competencies) { + this.currentlyProcessedAttachmentUnit.competencies = competencies; + } const formData = new FormData(); formData.append('file', file, fileName); diff --git a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts index b24b25a4b64d..14cc8732f244 100644 --- a/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts +++ b/src/main/webapp/app/shared/layouts/navbar/navbar.component.ts @@ -532,6 +532,9 @@ export class NavbarComponent implements OnInit, OnDestroy { // Special case: Don't display the ID here but the name directly (clicking the ID wouldn't work) this.addTranslationAsCrumb(currentPath, 'example-submission-editor'); break; + case 'attachments': + this.addBreadcrumb(currentPath, segment, false); + break; // No breadcrumbs for those segments case 'competency-management': case 'prerequisite-management': diff --git a/src/main/webapp/app/shared/layouts/profiles/profile-info.model.ts b/src/main/webapp/app/shared/layouts/profiles/profile-info.model.ts index 18c335e0079f..9d7bafb69cde 100644 --- a/src/main/webapp/app/shared/layouts/profiles/profile-info.model.ts +++ b/src/main/webapp/app/shared/layouts/profiles/profile-info.model.ts @@ -56,6 +56,8 @@ export class ProfileInfo { }; }; public theiaPortalURL: string; + public operatorName: string; + public operatorAdminName?: string; } export const hasEditableBuildPlan = (profileInfo: ProfileInfo): boolean => { diff --git a/src/main/webapp/app/shared/layouts/profiles/profile.service.ts b/src/main/webapp/app/shared/layouts/profiles/profile.service.ts index ce197b034a47..ac828dc310ea 100644 --- a/src/main/webapp/app/shared/layouts/profiles/profile.service.ts +++ b/src/main/webapp/app/shared/layouts/profiles/profile.service.ts @@ -61,6 +61,8 @@ export class ProfileService { profileInfo.externalUserManagementURL = data.externalUserManagementURL ?? ''; profileInfo.contact = data.contact; + profileInfo.operatorName = data.operatorName; + profileInfo.operatorAdminName = data.operatorAdminName; profileInfo.registrationEnabled = data.registrationEnabled; profileInfo.needsToAcceptTerms = data.needsToAcceptTerms; profileInfo.allowedEmailPattern = data.allowedEmailPattern; diff --git a/src/main/webapp/content/scss/themes/_dark-variables.scss b/src/main/webapp/content/scss/themes/_dark-variables.scss index fe7182454138..b972c760771a 100644 --- a/src/main/webapp/content/scss/themes/_dark-variables.scss +++ b/src/main/webapp/content/scss/themes/_dark-variables.scss @@ -317,6 +317,12 @@ $stat-av-sc-legend-critical: rgba(204, 0, 0, 1); $stat-av-sc-legend-median: rgba(127, 127, 127, 255); $stat-av-sc-legend-best: rgba(40, 164, 40, 1); +// Component: pdf-preview.component.scss +$pdf-preview-pdf-container-shadow: scale-color($black, $alpha: -90%); +$pdf-preview-canvas-shadow: scale-color($white, $alpha: -40%); +$pdf-preview-container-overlay: scale-color($black, $alpha: -60%); +$pdf-preview-enlarged-container-overlay: scale-color($black, $alpha: -20%); + // Programming Exercise Update $update-programming-exercise-plan-preview-box-background: transparentize(#fff, 0.95); diff --git a/src/main/webapp/content/scss/themes/_default-variables.scss b/src/main/webapp/content/scss/themes/_default-variables.scss index a0c05c0c757f..4490f3ad7ac4 100644 --- a/src/main/webapp/content/scss/themes/_default-variables.scss +++ b/src/main/webapp/content/scss/themes/_default-variables.scss @@ -242,6 +242,12 @@ $stat-av-sc-legend-critical: rgba(204, 0, 0, 1); $stat-av-sc-legend-median: rgba(127, 127, 127, 255); $stat-av-sc-legend-best: rgba(40, 164, 40, 1); +// Component: pdf-preview.component.scss +$pdf-preview-pdf-container-shadow: scale-color($black, $alpha: -90%); +$pdf-preview-canvas-shadow: scale-color($black, $alpha: -90%); +$pdf-preview-container-overlay: scale-color($black, $alpha: -60%); +$pdf-preview-enlarged-container-overlay: scale-color($black, $alpha: -80%); + // Programming Exercise Update $update-programming-exercise-plan-preview-box-background: transparentize(black, 0.95); diff --git a/src/main/webapp/i18n/de/dataExport.json b/src/main/webapp/i18n/de/dataExport.json index 3e9b2ed36c56..bb82eb2604da 100644 --- a/src/main/webapp/i18n/de/dataExport.json +++ b/src/main/webapp/i18n/de/dataExport.json @@ -3,7 +3,7 @@ "dataExport": { "title": "Datenexport", "titleDownload": "Datenexport herunterladen", - "description": "Hier kannst du einen Datenexport gemäß Art.15 DSGVO Auskunftsrecht der betroffenen Person anfordern. Sobald der Export erstellt wurde, erhältst du eine Email mit einem Link um diesen herunterzuladen. Du kannst den Export sieben Tage lang herunterladen, danach wird er gelöscht. ", + "description": "Hier kannst du einen Datenexport gemäß Art.15 DSGVO Auskunftsrecht der betroffenen Person anfordern. Sobald der Export erstellt wurde, erhältst du eine E-Mail mit einem Link um diesen herunterzuladen. Du kannst den Export sieben Tage lang herunterladen, danach wird er gelöscht. ", "descriptionDownload": "Hier kannst du deinen angeforderten Datenexport herunterladen.", "request": "Datenexport anfordern", "download": "Datenexport herunterladen", diff --git a/src/main/webapp/i18n/de/global.json b/src/main/webapp/i18n/de/global.json index e687aaaf72fb..9db7bb972f26 100644 --- a/src/main/webapp/i18n/de/global.json +++ b/src/main/webapp/i18n/de/global.json @@ -122,12 +122,13 @@ "continueTutorial": "Tutorial fortsetzen" }, "form": { - "username": "Login", + "username": "Login oder E-Mail", "firstname": "Vorname", "firstname.placeholder": "Vorname", "lastname": "Nachname", "lastname.placeholder": "Nachname", - "username.placeholder": "Login", + "username.placeholder": "Login oder E-Mail", + "username.tumPlaceholder": "z.B. go42tum / example@tum.de", "currentpassword": "Aktuelles Passwort", "currentpassword.placeholder": "Aktuelles Passwort", "newpassword": "Neues Passwort", diff --git a/src/main/webapp/i18n/de/home.json b/src/main/webapp/i18n/de/home.json index 0a1246af1b82..0dab67a168fa 100644 --- a/src/main/webapp/i18n/de/home.json +++ b/src/main/webapp/i18n/de/home.json @@ -4,8 +4,8 @@ "subtitle": "Interaktives Lernen mit Individuellem Feedback", "login": { "traditional": { - "pleaseSignInAccount": "Bitte melde dich mit deinen {{account}} Zugangsdaten an.", - "pleaseSignIn": "Bitte melde dich mit deinen Zugangsdaten an." + "pleaseSignInAccount": "Melde dich mit deinen {{account}} Zugangsdaten an", + "pleaseSignIn": "Melde dich mit deinen Zugangsdaten an" }, "saml2": { "pleaseSignInProvider": "Bitte melde dich via {{provider}} an.", @@ -13,12 +13,13 @@ } }, "logged": { - "message": "Du bist als Nutzer:in \"{{username}}\" angemeldet." + "message": "Du bist als \"{{username}}\" angemeldet." }, "errors": { "failedToLogin": "Fehler bei der Anmeldung! Bitte überprüfe deinen Login und dein Passwort und versuche es erneut.", "usernameIncorrect": "

Ungültiger Login

", - "tumWarning": "

Wenn du Studierende:r an der TUM bist, sollte deine Kennung das Format ab12xyz haben.
Füge weder '@mytum.de' noch '@tum.de' hinzu.

", + "passwordIncorrect": "Das Passwort muss zwischen 8 und 50 Zeichen lang sein.", + "tumWarning": "An der TUM nutze z.B. go42tum oder example@tum.de", "loginWarning": "Scheint so, als hättest du Probleme bei der Anmeldung :-(
Bitte besuche {{ name }} und versuche dich dort anzumelden.
Danach versuche es hier noch einmal.", "sessionExpired": "Du wurdest ausgeloggt, da die Sitzung abgelaufen ist. Bitte logge dich erneut ein, um zur vorherigen Seite zurückzukehren." } diff --git a/src/main/webapp/i18n/de/lecture.json b/src/main/webapp/i18n/de/lecture.json index 61e68defd8e9..c5d76e7c292d 100644 --- a/src/main/webapp/i18n/de/lecture.json +++ b/src/main/webapp/i18n/de/lecture.json @@ -96,7 +96,12 @@ "deleteQuestion": "Soll der Anhang wirklich gelöscht werden?", "created": "Anhang erstellt mit ID {{ param }}", "updated": "Anhang aktualisiert mit ID {{ param }}", - "deleted": "Anhang gelöscht mit ID {{ param }}" + "deleted": "Anhang gelöscht mit ID {{ param }}", + "pdfPreview": { + "title": "Anhang", + "attachmentIDError": "Ungültiger Anhang oder ungültige Anhangs-ID.", + "attachmentUnitIDError": "Ungültige Dateieinheit oder ungültige Dateieinheits-ID." + } } } } diff --git a/src/main/webapp/i18n/de/login.json b/src/main/webapp/i18n/de/login.json index 60210204b6f8..ce66283a2cdf 100644 --- a/src/main/webapp/i18n/de/login.json +++ b/src/main/webapp/i18n/de/login.json @@ -15,7 +15,7 @@ } }, "password": { - "forgot": "Du hast dein Passwort vergessen?" + "forgot": "Passwort vergessen?" }, "ide": { "confirmation": "Wills du deinen Login und dein Passwort in deiner IDE speichern?\nDas wird das Importieren deiner Programmieraufgaben sehr vereinfachen!", diff --git a/src/main/webapp/i18n/de/metis.json b/src/main/webapp/i18n/de/metis.json index 5f6c9b228098..5eabc98efd9e 100644 --- a/src/main/webapp/i18n/de/metis.json +++ b/src/main/webapp/i18n/de/metis.json @@ -99,7 +99,7 @@ "courseWideContext": "Kursweite Themen", "exercises": "Übungen", "lectures": "Vorlesungen", - "announcementHint": "Alle Teilnehmenden des Kurses, die dies nicht explizit abgestellt haben, erhalten automatisch eine Benachrichtigungs-Email mit dem Inhalt der Ankündigung. Ankündigungen können nur durch die Übungsleitung erstellt werden. Mögliche Antworten können nicht als klärend markiert werden.", + "announcementHint": "Alle Teilnehmenden des Kurses, die dies nicht explizit abgestellt haben, erhalten automatisch eine Benachrichtigungs-E-Mail mit dem Inhalt der Ankündigung. Ankündigungen können nur durch die Übungsleitung erstellt werden. Mögliche Antworten können nicht als klärend markiert werden.", "title": "Titel", "postMarkedAsResolvedTooltip": "Nachricht wurde als geklärt markiert", "postMarkedAsAnnouncementTooltip": "Ankündigung", diff --git a/src/main/webapp/i18n/de/operatorInfo.json b/src/main/webapp/i18n/de/operatorInfo.json new file mode 100644 index 000000000000..769631e8a7b4 --- /dev/null +++ b/src/main/webapp/i18n/de/operatorInfo.json @@ -0,0 +1,10 @@ +{ + "artemisApp": { + "operatorInfo": { + "title": "Betreiber", + "name": "Name", + "admin": "Administrator:in", + "adminEmail": "Kontakt-E-Mail" + } + } +} diff --git a/src/main/webapp/i18n/de/organizationManagement.json b/src/main/webapp/i18n/de/organizationManagement.json index faf2f5079640..a4ea52a18802 100644 --- a/src/main/webapp/i18n/de/organizationManagement.json +++ b/src/main/webapp/i18n/de/organizationManagement.json @@ -9,7 +9,7 @@ "logoUrl": "Logo URL", "Nutzer:innens": "Nutzer:innen", "courses": "Kurse", - "emailPattern": "Email-Muster", + "emailPattern": "E-Mail-Muster", "delete": { "question": "Soll die Organisation {{ title }} wirklich gelöscht werden?" }, @@ -31,7 +31,7 @@ "select": "Auswählen", "title": "Organisationen" }, - "patternChangeWarning": "Beim Ändern des Email-Musters wird eine Neuzuweisung der Nutzer:innen zu dieser Organisation ausgeführt" + "patternChangeWarning": "Beim Ändern des E-Mail-Musters wird eine Neuzuweisung der Nutzer:innen zu dieser Organisation ausgeführt" }, "organization": { "updated": "Organization updated!", diff --git a/src/main/webapp/i18n/de/reset.json b/src/main/webapp/i18n/de/reset.json index 3529d750a4b1..572a8f89811c 100644 --- a/src/main/webapp/i18n/de/reset.json +++ b/src/main/webapp/i18n/de/reset.json @@ -4,10 +4,10 @@ "title": "Passwort zurücksetzen", "form": { "button": "Passwort zurücksetzen", - "emailUsername": "E-Mail/Benutzername" + "emailUsername": "E-Mail / Login" }, "messages": { - "info": "Gib die E-Mail-Adresse ein, die du bei der Registrierung verwendest hast oder deinen Benutzernamen.", + "info": "Gib die E-Mail-Adresse ein, die du bei der Registrierung verwendest hast oder deinen Login.", "success": "Eine E-Mail mit weiteren Instruktionen für das Zurücksetzen des Passworts wurde gesendet. Aus Sicherheitsgründen erscheint dieser Hinweis auch wenn der angegebene Account nicht existiert. Überprüfe deine Eingabe, wenn du keine E-Mail erhältst." }, "external": { diff --git a/src/main/webapp/i18n/de/userSettings.json b/src/main/webapp/i18n/de/userSettings.json index d7e1c3de5180..ac23773e91f3 100644 --- a/src/main/webapp/i18n/de/userSettings.json +++ b/src/main/webapp/i18n/de/userSettings.json @@ -47,7 +47,7 @@ "registrationNumber": "Matrikelnummer", "fullName": "Vollständiger Name", "login": "Login", - "email": "Email", + "email": "E-Mail", "joinedArtemis": "Artemis beigetreten am", "sshKey": "Öffentlicher SSH Schlüssel" }, diff --git a/src/main/webapp/i18n/en/global.json b/src/main/webapp/i18n/en/global.json index 7e59d4c46f22..8760c0192b62 100644 --- a/src/main/webapp/i18n/en/global.json +++ b/src/main/webapp/i18n/en/global.json @@ -123,12 +123,13 @@ "continueTutorial": "Continue tutorial" }, "form": { - "username": "Username", + "username": "Login or email", "firstname": "First name", - "firstname.placeholder": "Your first name", + "firstname.placeholder": "First name", "lastname": "Last name", - "lastname.placeholder": "Your last name", - "username.placeholder": "Your Username", + "lastname.placeholder": "Last name", + "username.placeholder": "Login or email", + "username.tumPlaceholder": "e.g. go42tum / example@tum.de", "currentpassword": "Current password", "currentpassword.placeholder": "Current password", "newpassword": "New password", diff --git a/src/main/webapp/i18n/en/home.json b/src/main/webapp/i18n/en/home.json index 3a5cdd0a0596..81e5cf7c6da4 100644 --- a/src/main/webapp/i18n/en/home.json +++ b/src/main/webapp/i18n/en/home.json @@ -4,8 +4,8 @@ "subtitle": "Interactive Learning with Individual Feedback", "login": { "traditional": { - "pleaseSignInAccount": "Please sign in with your {{account}} account.", - "pleaseSignIn": "Please sign in with your account." + "pleaseSignInAccount": "Sign in with your {{account}} account", + "pleaseSignIn": "Sign in with your account" }, "saml2": { "pleaseSignInProvider": "Please sign in with {{provider}}.", @@ -13,12 +13,13 @@ } }, "logged": { - "message": "You are logged in as user \"{{username}}\"." + "message": "You are logged in as \"{{username}}\"." }, "errors": { - "failedToLogin": "Failed to sign in! Please check your username and password and try again.", - "usernameIncorrect": "

Invalid username

", - "tumWarning": "

If you are a TUM student, your username
should have the format ab12xyz.
Do not include '@mytum.de' or '@tum.de'.

", + "failedToLogin": "Failed to sign in! Please check your login and password and try again.", + "usernameIncorrect": "

Invalid login

", + "passwordIncorrect": "The password must be between 8 and 50 characters long.", + "tumWarning": "If you are at TUM, use e.g. go42tum or example@tum.de", "loginWarning": "Seems like you are having issues signing in :-(
Please go to {{ name }} and try to sign in there.
After that, try again here.", "sessionExpired": "You have been logged out, because your session has expired. Please log in again to return to the previous page." } diff --git a/src/main/webapp/i18n/en/lecture.json b/src/main/webapp/i18n/en/lecture.json index bb8ddb7ad737..b03b840b400f 100644 --- a/src/main/webapp/i18n/en/lecture.json +++ b/src/main/webapp/i18n/en/lecture.json @@ -96,7 +96,12 @@ "deleteQuestion": "Do you really want to delete the attachment?", "created": "Created new attachment with identifier {{ param }}", "updated": "Updated attachment with identifier {{ param }}", - "deleted": "Deleted attachment with identifier {{ param }}" + "deleted": "Deleted attachment with identifier {{ param }}", + "pdfPreview": { + "title": "Attachment", + "attachmentIDError": "Invalid Attachment or Attachment ID.", + "attachmentUnitIDError": "Invalid Attachment Unit or Attachment Unit ID." + } } } } diff --git a/src/main/webapp/i18n/en/login.json b/src/main/webapp/i18n/en/login.json index 46ad1c16400d..7f49b5270a27 100644 --- a/src/main/webapp/i18n/en/login.json +++ b/src/main/webapp/i18n/en/login.json @@ -4,7 +4,7 @@ "divider": "or", "form": { "password": "Password", - "password.placeholder": "Your password", + "password.placeholder": "Password", "rememberme": "Remember me", "acceptTerms": "Accept terms", "button": "Sign in" @@ -15,7 +15,7 @@ } }, "password": { - "forgot": "Did you forget your password?" + "forgot": "Forgot password?" }, "ide": { "confirmation": "Do you want to store your credentials in your IDE?\nThis will make importing your exercises a lot easier!", diff --git a/src/main/webapp/i18n/en/operatorInfo.json b/src/main/webapp/i18n/en/operatorInfo.json new file mode 100644 index 000000000000..a6e174a9b9fd --- /dev/null +++ b/src/main/webapp/i18n/en/operatorInfo.json @@ -0,0 +1,10 @@ +{ + "artemisApp": { + "operatorInfo": { + "title": "Operator", + "name": "Name", + "admin": "Administrator", + "adminEmail": "Contact Email" + } + } +} diff --git a/src/main/webapp/i18n/en/reset.json b/src/main/webapp/i18n/en/reset.json index 3b1401352991..22b919a79791 100644 --- a/src/main/webapp/i18n/en/reset.json +++ b/src/main/webapp/i18n/en/reset.json @@ -4,10 +4,10 @@ "title": "Reset your password", "form": { "button": "Reset password", - "emailUsername": "Email/Username" + "emailUsername": "Email / Login" }, "messages": { - "info": "Enter the email address you used to register or your username.", + "info": "Enter the email address you used to register or your login.", "success": "Check your email for details on how to reset your password. For security reasons, this hint is visible even if the account doesn't exist. Recheck your input if you don't receive an email." }, "external": { diff --git a/src/test/java/de/tum/in/www1/artemis/FileIntegrationTest.java b/src/test/java/de/tum/in/www1/artemis/FileIntegrationTest.java index 13dee755daa6..fb2ad2dec51d 100644 --- a/src/test/java/de/tum/in/www1/artemis/FileIntegrationTest.java +++ b/src/test/java/de/tum/in/www1/artemis/FileIntegrationTest.java @@ -68,7 +68,7 @@ class FileIntegrationTest extends AbstractSpringIntegrationIndependentTest { @BeforeEach void initTestCase() { - userUtilService.addUsers(TEST_PREFIX, 1, 1, 0, 1); + userUtilService.addUsers(TEST_PREFIX, 1, 1, 1, 1); } @Test @@ -307,4 +307,41 @@ private AttachmentUnit uploadAttachmentUnit(Lecture lecture, MockMultipartFile f AttachmentUnit.class, expectedStatus); } + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void testGetAttachmentFileAsEditor() throws Exception { + Lecture lecture = lectureUtilService.createCourseWithLecture(true); + + Attachment attachment = LectureFactory.generateAttachmentWithFile(ZonedDateTime.now(), lecture.getId(), false); + attachment.setId(1L); + attachment.setLecture(lecture); + + Long courseId = lecture.getCourse().getId(); + Long attachmentId = attachment.getId(); + + lectureRepo.save(lecture); + attachmentRepo.save(attachment); + + request.get("/api/files/courses/" + courseId + "/attachments/" + attachmentId, HttpStatus.OK, byte[].class); + } + + @Test + @WithMockUser(username = TEST_PREFIX + "editor1", roles = "EDITOR") + void testGetAttachmentUnitFileAsEditor() throws Exception { + Lecture lecture = lectureUtilService.createCourseWithLecture(true); + + AttachmentUnit attachmentUnit = lectureUtilService.createAttachmentUnit(true); + attachmentUnit.setLecture(lecture); + Attachment attachment = attachmentUnit.getAttachment(); + + lectureRepo.save(lecture); + attachmentRepo.save(attachment); + attachmentUnitRepo.save(attachmentUnit); + + Long courseId = lecture.getCourse().getId(); + Long attachmentUnitId = attachmentUnit.getId(); + + request.get("/api/files/courses/" + courseId + "/attachment-units/" + attachmentUnitId, HttpStatus.OK, byte[].class); + } + } diff --git a/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java b/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java index 1097afd3a46b..effc5d69160a 100644 --- a/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/connectors/LtiServiceTest.java @@ -118,16 +118,14 @@ void addLtiQueryParamsNewUser() { verify(response).addHeader(any(), any()); String initialize = uriComponents.getQueryParams().getFirst("initialize"); - String ltiSuccessLoginRequired = uriComponents.getQueryParams().getFirst("ltiSuccessLoginRequired"); assertThat(initialize).isEmpty(); - assertThat(ltiSuccessLoginRequired).isNull(); } @Test void addLtiQueryParamsExistingUser() { when(userRepository.getUser()).thenReturn(user); user.setActivated(true); - when(jwtCookieService.buildLogoutCookie()).thenReturn(mock(ResponseCookie.class)); + when(jwtCookieService.buildLoginCookie(true)).thenReturn(mock(ResponseCookie.class)); UriComponentsBuilder uriComponentsBuilder = UriComponentsBuilder.newInstance(); HttpServletResponse response = mock(HttpServletResponse.class); @@ -136,12 +134,10 @@ void addLtiQueryParamsExistingUser() { UriComponents uriComponents = uriComponentsBuilder.build(); - verify(jwtCookieService).buildLogoutCookie(); + verify(jwtCookieService).buildLoginCookie(true); verify(response).addHeader(any(), any()); String initialize = uriComponents.getQueryParams().getFirst("initialize"); - String ltiSuccessLoginRequired = uriComponents.getQueryParams().getFirst("ltiSuccessLoginRequired"); - assertThat(ltiSuccessLoginRequired).isEqualTo(user.getLogin()); assertThat(initialize).isNull(); } diff --git a/src/test/java/de/tum/in/www1/artemis/service/plagiarism/ContinuousPlagiarismControlServiceTest.java b/src/test/java/de/tum/in/www1/artemis/plagiarism/ContinuousPlagiarismControlServiceTest.java similarity index 96% rename from src/test/java/de/tum/in/www1/artemis/service/plagiarism/ContinuousPlagiarismControlServiceTest.java rename to src/test/java/de/tum/in/www1/artemis/plagiarism/ContinuousPlagiarismControlServiceTest.java index 45374611d0ba..957238bac53e 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/plagiarism/ContinuousPlagiarismControlServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/plagiarism/ContinuousPlagiarismControlServiceTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.service.plagiarism; +package de.tum.in.www1.artemis.plagiarism; import static java.util.Collections.emptySet; import static java.util.Collections.singleton; @@ -43,6 +43,11 @@ import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismCaseRepository; import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismComparisonRepository; import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismResultRepository; +import de.tum.in.www1.artemis.service.plagiarism.ContinuousPlagiarismControlService; +import de.tum.in.www1.artemis.service.plagiarism.PlagiarismCaseService; +import de.tum.in.www1.artemis.service.plagiarism.PlagiarismDetectionService; +import de.tum.in.www1.artemis.service.plagiarism.PlagiarismPostService; +import de.tum.in.www1.artemis.service.plagiarism.ProgrammingLanguageNotSupportedForPlagiarismDetectionException; class ContinuousPlagiarismControlServiceTest { diff --git a/src/test/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionConfigHelperTest.java b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismDetectionConfigHelperTest.java similarity index 96% rename from src/test/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionConfigHelperTest.java rename to src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismDetectionConfigHelperTest.java index 83b6938a5040..1672a44291af 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionConfigHelperTest.java +++ b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismDetectionConfigHelperTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.service.plagiarism; +package de.tum.in.www1.artemis.plagiarism; import static org.assertj.core.api.Assertions.assertThat; import static org.mockito.Mockito.mock; @@ -12,6 +12,7 @@ import de.tum.in.www1.artemis.domain.modeling.ModelingExercise; import de.tum.in.www1.artemis.domain.plagiarism.PlagiarismDetectionConfig; import de.tum.in.www1.artemis.repository.ModelingExerciseRepository; +import de.tum.in.www1.artemis.service.plagiarism.PlagiarismDetectionConfigHelper; class PlagiarismDetectionConfigHelperTest { diff --git a/src/test/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionServiceTest.java b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismDetectionServiceTest.java similarity index 93% rename from src/test/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionServiceTest.java rename to src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismDetectionServiceTest.java index d80897af7d4f..39ab8d09aad3 100644 --- a/src/test/java/de/tum/in/www1/artemis/service/plagiarism/PlagiarismDetectionServiceTest.java +++ b/src/test/java/de/tum/in/www1/artemis/plagiarism/PlagiarismDetectionServiceTest.java @@ -1,4 +1,4 @@ -package de.tum.in.www1.artemis.service.plagiarism; +package de.tum.in.www1.artemis.plagiarism; import static java.util.Collections.emptyList; import static java.util.Collections.emptySet; @@ -22,6 +22,11 @@ import de.tum.in.www1.artemis.domain.plagiarism.modeling.ModelingPlagiarismResult; import de.tum.in.www1.artemis.domain.plagiarism.text.TextPlagiarismResult; import de.tum.in.www1.artemis.repository.plagiarism.PlagiarismResultRepository; +import de.tum.in.www1.artemis.service.plagiarism.ModelingPlagiarismDetectionService; +import de.tum.in.www1.artemis.service.plagiarism.PlagiarismDetectionService; +import de.tum.in.www1.artemis.service.plagiarism.ProgrammingLanguageNotSupportedForPlagiarismDetectionException; +import de.tum.in.www1.artemis.service.plagiarism.ProgrammingPlagiarismDetectionService; +import de.tum.in.www1.artemis.service.plagiarism.TextPlagiarismDetectionService; import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeature; import de.tum.in.www1.artemis.service.programming.ProgrammingLanguageFeatureService; diff --git a/src/test/java/de/tum/in/www1/artemis/telemetry/TelemetryServiceTest.java b/src/test/java/de/tum/in/www1/artemis/telemetry/TelemetryServiceTest.java new file mode 100644 index 000000000000..9ff3a4db9874 --- /dev/null +++ b/src/test/java/de/tum/in/www1/artemis/telemetry/TelemetryServiceTest.java @@ -0,0 +1,78 @@ +package de.tum.in.www1.artemis.telemetry; + +import static org.mockito.Mockito.spy; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.method; +import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError; +import static org.springframework.test.web.client.response.MockRestResponseCreators.withStatus; + +import java.net.URI; + +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.extension.ExtendWith; +import org.mockito.junit.jupiter.MockitoExtension; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.http.HttpMethod; +import org.springframework.http.HttpStatus; +import org.springframework.http.MediaType; +import org.springframework.test.web.client.ExpectedCount; +import org.springframework.test.web.client.MockRestServiceServer; +import org.springframework.web.client.RestTemplate; + +import com.fasterxml.jackson.databind.ObjectMapper; + +import de.tum.in.www1.artemis.AbstractSpringIntegrationIndependentTest; +import de.tum.in.www1.artemis.service.telemetry.TelemetryService; + +@ExtendWith(MockitoExtension.class) +class TelemetryServiceTest extends AbstractSpringIntegrationIndependentTest { + + @Value("${artemis.telemetry.destination}") + private String destination; + + @Autowired + private RestTemplate restTemplate; + + private MockRestServiceServer mockServer; + + private final ObjectMapper mapper = new ObjectMapper(); + + @Autowired + private TelemetryService telemetryService; + + private TelemetryService telemetryServiceSpy; + + @BeforeEach + void init() { + telemetryServiceSpy = spy(telemetryService); + mockServer = MockRestServiceServer.createServer(restTemplate); + telemetryServiceSpy.useTelemetry = true; + } + + @Test + void testSendTelemetry_TelemetryEnabled() throws Exception { + mockServer.expect(ExpectedCount.once(), requestTo(new URI(destination + "/api/telemetry"))).andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(mapper.writeValueAsString("Success!"))); + telemetryServiceSpy.sendTelemetry(); + mockServer.verify(); + } + + @Test + void testSendTelemetry_TelemetryDisabled() throws Exception { + mockServer.expect(ExpectedCount.never(), requestTo(new URI(destination + "/api/telemetry"))).andExpect(method(HttpMethod.POST)) + .andRespond(withStatus(HttpStatus.OK).contentType(MediaType.APPLICATION_JSON).body(mapper.writeValueAsString("Success!"))); + telemetryServiceSpy.useTelemetry = false; + telemetryServiceSpy.sendTelemetry(); + mockServer.verify(); + } + + @Test + void testSendTelemetry_ExceptionHandling() throws Exception { + mockServer.expect(ExpectedCount.once(), requestTo(new URI(destination + "/api/telemetry"))).andExpect(method(HttpMethod.POST)) + .andRespond(withServerError().body(mapper.writeValueAsString("Failure!"))); + telemetryServiceSpy.sendTelemetry(); + mockServer.verify(); + } +} diff --git a/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit.service.spec.ts b/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit.service.spec.ts index 765d4e646a7e..504f8ffd3903 100644 --- a/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit.service.spec.ts +++ b/src/test/javascript/spec/component/lecture-unit/attachment-unit/attachment-unit.service.spec.ts @@ -199,4 +199,23 @@ describe('AttachmentUnitService', () => { expect(response.body).toEqual(expected); })); + + describe('getAttachmentFile', () => { + it('should retrieve a file as Blob for a given course and attachment unit ID', () => { + const courseId = 5; + const attachmentUnitId = 10; + const expectedBlob = new Blob(['example data'], { type: 'application/pdf' }); + + service.getAttachmentFile(courseId, attachmentUnitId).subscribe((response) => { + expect(response).toEqual(expectedBlob); + }); + + const req = httpMock.expectOne({ + url: `api/files/courses/${courseId}/attachment-units/${attachmentUnitId}`, + method: 'GET', + }); + expect(req.request.responseType).toBe('blob'); + req.flush(expectedBlob); + }); + }); }); diff --git a/src/test/javascript/spec/component/lecture-unit/lecture-unit-management.component.spec.ts b/src/test/javascript/spec/component/lecture-unit/lecture-unit-management.component.spec.ts index 07fdcc72f02e..bad4cf68f981 100644 --- a/src/test/javascript/spec/component/lecture-unit/lecture-unit-management.component.spec.ts +++ b/src/test/javascript/spec/component/lecture-unit/lecture-unit-management.component.spec.ts @@ -28,7 +28,7 @@ import { Competency } from 'app/entities/competency.model'; import { UnitCreationCardComponent } from 'app/lecture/lecture-unit/lecture-unit-management/unit-creation-card/unit-creation-card.component'; import { ArtemisDatePipe } from 'app/shared/pipes/artemis-date.pipe'; import { MockRouterLinkDirective } from '../../helpers/mocks/directive/mock-router-link.directive'; -import { LectureUnit } from 'app/entities/lecture-unit/lectureUnit.model'; +import { LectureUnit, LectureUnitType } from 'app/entities/lecture-unit/lectureUnit.model'; import { CdkDragDrop } from '@angular/cdk/drag-drop'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { OnlineUnit } from 'app/entities/lecture-unit/onlineUnit.model'; @@ -169,6 +169,14 @@ describe('LectureUnitManagementComponent', () => { expect(lectureUnitManagementComponent.getDeleteQuestionKey(new OnlineUnit())).toBe('artemisApp.onlineUnit.delete.question'); }); + it('should return default question translation key for unhandled types', () => { + const mockUnit = { + type: null, + }; + + expect(lectureUnitManagementComponent.getDeleteQuestionKey(mockUnit as unknown as LectureUnit)).toBe(''); + }); + it('should give the correct confirmation text translation key', () => { expect(lectureUnitManagementComponent.getDeleteConfirmationTextKey(new AttachmentUnit())).toBe('artemisApp.attachmentUnit.delete.typeNameToConfirm'); expect(lectureUnitManagementComponent.getDeleteConfirmationTextKey(new ExerciseUnit())).toBe('artemisApp.exerciseUnit.delete.typeNameToConfirm'); @@ -177,6 +185,14 @@ describe('LectureUnitManagementComponent', () => { expect(lectureUnitManagementComponent.getDeleteConfirmationTextKey(new OnlineUnit())).toBe('artemisApp.onlineUnit.delete.typeNameToConfirm'); }); + it('should return default confirmation text translation key for unhandled types', () => { + const mockUnit = { + type: null, + }; + + expect(lectureUnitManagementComponent.getDeleteConfirmationTextKey(mockUnit as unknown as LectureUnit)).toBe(''); + }); + it('should give the correct action type', () => { expect(lectureUnitManagementComponent.getActionType(new AttachmentUnit())).toEqual(ActionType.Delete); expect(lectureUnitManagementComponent.getActionType(new ExerciseUnit())).toEqual(ActionType.Unlink); @@ -184,4 +200,29 @@ describe('LectureUnitManagementComponent', () => { expect(lectureUnitManagementComponent.getActionType(new VideoUnit())).toEqual(ActionType.Delete); expect(lectureUnitManagementComponent.getActionType(new OnlineUnit())).toEqual(ActionType.Delete); }); + + describe('isViewButtonAvailable', () => { + it('should return true for an attachment unit with a PDF link', () => { + const lectureUnit = { + type: LectureUnitType.ATTACHMENT, + attachment: { link: 'file.pdf' }, + } as LectureUnit; + expect(lectureUnitManagementComponent.isViewButtonAvailable(lectureUnit)).toBeTrue(); + }); + + it('should return false for file extension different than .pdf', () => { + const lectureUnit = { + type: LectureUnitType.ATTACHMENT, + attachment: { link: 'file.txt' }, + }; + expect(lectureUnitManagementComponent.isViewButtonAvailable(lectureUnit)).toBeFalse(); + }); + + it('should return false for a text unit', () => { + const lectureUnit = { + type: LectureUnitType.TEXT, + }; + expect(lectureUnitManagementComponent.isViewButtonAvailable(lectureUnit)).toBeFalse(); + }); + }); }); diff --git a/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts b/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts index 773142efa909..72bab398d1f1 100644 --- a/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/lecture-attachments.component.spec.ts @@ -20,6 +20,7 @@ import { of, take, throwError } from 'rxjs'; import { HttpResponse } from '@angular/common/http'; import { NgbTooltip } from '@ng-bootstrap/ng-bootstrap'; import { LectureService } from 'app/lecture/lecture.service'; +import { RouterModule } from '@angular/router'; describe('LectureAttachmentsComponent', () => { let comp: LectureAttachmentsComponent; @@ -84,7 +85,7 @@ describe('LectureAttachmentsComponent', () => { beforeEach(() => { return TestBed.configureTestingModule({ - imports: [ArtemisTestModule, MockDirective(NgbTooltip)], + imports: [ArtemisTestModule, MockDirective(NgbTooltip), RouterModule], declarations: [ LectureAttachmentsComponent, MockComponent(FormDateTimePickerComponent), @@ -154,7 +155,7 @@ describe('LectureAttachmentsComponent', () => { fixture.detectChanges(); tick(); expect(comp.attachments).toHaveLength(3); - expect(attachmentServiceFindAllByLectureIdStub).toHaveBeenCalledOnce(); + expect(attachmentServiceFindAllByLectureIdStub).toHaveBeenCalledTimes(2); })); it('should not accept too large file', fakeAsync(() => { @@ -379,4 +380,21 @@ describe('LectureAttachmentsComponent', () => { expect(comp.attachmentToBeCreated.link).toBe(myBlob1.name); expect(attachmentServiceFindAllByLectureIdStub).toHaveBeenCalledOnce(); })); + + describe('isViewButtonAvailable', () => { + it('should return true if the attachment link ends with .pdf', () => { + const attachment = { id: 1, link: 'example.pdf', attachmentType: 'FILE' } as Attachment; + expect(comp.isViewButtonAvailable(attachment.link!)).toBeTrue(); + }); + + it('should return false if the attachment link does not end with .pdf', () => { + const attachment = { id: 2, link: 'example.txt', attachmentType: 'FILE' } as Attachment; + expect(comp.isViewButtonAvailable(attachment.link!)).toBeFalse(); + }); + + it.each([['document.docx'], ['spreadsheet.xlsx'], ['presentation.pptx'], ['image.jpeg']])('should return false for common file extension %s', (link) => { + const attachment = { link }; + expect(comp.isViewButtonAvailable(attachment.link)).toBeFalse(); + }); + }); }); diff --git a/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts new file mode 100644 index 000000000000..03e494ccbd41 --- /dev/null +++ b/src/test/javascript/spec/component/lecture/pdf-preview.component.spec.ts @@ -0,0 +1,437 @@ +import { MockTranslateService } from '../../helpers/mocks/service/mock-translate.service'; + +jest.mock('pdfjs-dist', () => { + return { + getDocument: jest.fn(() => ({ + promise: Promise.resolve({ + numPages: 1, + getPage: jest.fn(() => + Promise.resolve({ + getViewport: jest.fn(() => ({ width: 600, height: 800, scale: 1 })), + render: jest.fn(() => ({ + promise: Promise.resolve(), + })), + }), + ), + }), + })), + }; +}); + +jest.mock('pdfjs-dist/build/pdf.worker', () => { + return {}; +}); + +function createMockEvent(target: Element, eventType = 'click'): MouseEvent { + const event = new MouseEvent(eventType, { + view: window, + bubbles: true, + cancelable: true, + }); + Object.defineProperty(event, 'target', { value: target, writable: false }); + return event; +} + +import { ComponentFixture, TestBed } from '@angular/core/testing'; +import { ActivatedRoute } from '@angular/router'; +import { of, throwError } from 'rxjs'; +import { AttachmentService } from 'app/lecture/attachment.service'; +import { AttachmentUnitService } from 'app/lecture/lecture-unit/lecture-unit-management/attachmentUnit.service'; +import { PdfPreviewComponent } from 'app/lecture/pdf-preview/pdf-preview.component'; +import { ElementRef } from '@angular/core'; +import { AlertService } from 'app/core/util/alert.service'; +import { HttpErrorResponse } from '@angular/common/http'; +import { TranslateService } from '@ngx-translate/core'; + +describe('PdfPreviewComponent', () => { + let component: PdfPreviewComponent; + let fixture: ComponentFixture; + let attachmentServiceMock: any; + let attachmentUnitServiceMock: any; + let alertServiceMock: any; + let routeMock: any; + let mockCanvasElement: HTMLCanvasElement; + let mockEnlargedCanvas: HTMLCanvasElement; + let mockContext: any; + let mockOverlay: HTMLDivElement; + + beforeEach(async () => { + global.URL.createObjectURL = jest.fn().mockReturnValue('mocked_blob_url'); + attachmentServiceMock = { + getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), + }; + attachmentUnitServiceMock = { + getAttachmentFile: jest.fn().mockReturnValue(of(new Blob([''], { type: 'application/pdf' }))), + }; + routeMock = { + data: of({ + course: { id: 1, name: 'Example Course' }, + attachment: { id: 1, name: 'Example PDF' }, + attachmentUnit: { id: 1, name: 'Chapter 1' }, + }), + }; + alertServiceMock = { + addAlert: jest.fn(), + error: jest.fn(), + }; + + await TestBed.configureTestingModule({ + imports: [PdfPreviewComponent], + providers: [ + { provide: ActivatedRoute, useValue: routeMock }, + { provide: AttachmentService, useValue: attachmentServiceMock }, + { provide: AttachmentUnitService, useValue: attachmentUnitServiceMock }, + { provide: AlertService, useValue: alertServiceMock }, + { provide: TranslateService, useClass: MockTranslateService }, + ], + }).compileComponents(); + + const pdfContainerElement = document.createElement('div'); + Object.defineProperty(pdfContainerElement, 'clientWidth', { value: 800 }); + Object.defineProperty(pdfContainerElement, 'clientHeight', { value: 600 }); + + fixture = TestBed.createComponent(PdfPreviewComponent); + component = fixture.componentInstance; + + mockCanvasElement = document.createElement('canvas'); + mockCanvasElement.width = 800; + mockCanvasElement.height = 600; + + jest.spyOn(component, 'updateEnlargedCanvas').mockImplementation(() => { + component.enlargedCanvas.nativeElement = mockCanvasElement; + }); + + mockEnlargedCanvas = document.createElement('canvas'); + mockEnlargedCanvas.classList.add('enlarged-canvas'); + component.enlargedCanvas = new ElementRef(mockEnlargedCanvas); + + mockContext = { + clearRect: jest.fn(), + drawImage: jest.fn(), + } as unknown as CanvasRenderingContext2D; + jest.spyOn(mockCanvasElement, 'getContext').mockReturnValue(mockContext); + + jest.spyOn(window, 'requestAnimationFrame').mockImplementation((cb: FrameRequestCallback) => { + cb(0); + return 0; + }); + mockOverlay = document.createElement('div'); + mockOverlay.style.opacity = '0'; + mockCanvasElement.appendChild(mockOverlay); + + fixture.detectChanges(); + + component.pdfContainer = new ElementRef(document.createElement('div')); + component.enlargedCanvas = new ElementRef(document.createElement('canvas')); + fixture.detectChanges(); + }); + + afterEach(() => { + jest.clearAllMocks(); + }); + + it('should load attachment file and verify service calls when attachment data is available', () => { + component.ngOnInit(); + expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); + expect(attachmentUnitServiceMock.getAttachmentFile).not.toHaveBeenCalled(); + }); + + it('should load attachment unit file and verify service calls when attachment unit data is available', () => { + routeMock.data = of({ + course: { id: 1, name: 'Example Course' }, + attachmentUnit: { id: 1, name: 'Chapter 1' }, + }); + component.ngOnInit(); + expect(attachmentUnitServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); + expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalled(); + }); + + it('should handle errors and trigger alert when loading an attachment file fails', () => { + const errorResponse = new HttpErrorResponse({ + status: 404, + statusText: 'Not Found', + error: 'File not found', + }); + + const attachmentService = TestBed.inject(AttachmentService); + jest.spyOn(attachmentService, 'getAttachmentFile').mockReturnValue(throwError(() => errorResponse)); + const alertServiceSpy = jest.spyOn(alertServiceMock, 'error'); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(alertServiceSpy).toHaveBeenCalledOnce(); + }); + + it('should handle errors and trigger alert when loading an attachment unit file fails', () => { + routeMock.data = of({ + course: { id: 1, name: 'Example Course' }, + attachmentUnit: { id: 1, name: 'Chapter 1' }, + }); + const errorResponse = new HttpErrorResponse({ + status: 404, + statusText: 'Not Found', + error: 'File not found', + }); + + const attachmentUnitService = TestBed.inject(AttachmentUnitService); + jest.spyOn(attachmentUnitService, 'getAttachmentFile').mockReturnValue(throwError(() => errorResponse)); + const alertServiceSpy = jest.spyOn(alertServiceMock, 'error'); + + component.ngOnInit(); + fixture.detectChanges(); + + expect(alertServiceSpy).toHaveBeenCalledOnce(); + }); + + it('should load PDF and verify rendering of pages', () => { + const mockBlob = new Blob(['PDF content'], { type: 'application/pdf' }); + + attachmentServiceMock.getAttachmentFile.mockReturnValue(of(mockBlob)); + component.ngOnInit(); + + expect(URL.createObjectURL).toHaveBeenCalledWith(mockBlob); + expect(attachmentServiceMock.getAttachmentFile).toHaveBeenCalledWith(1, 1); + expect(component.totalPages).toBeGreaterThan(0); + }); + + it('should navigate through pages using keyboard in enlarged view', () => { + component.isEnlargedView = true; + component.totalPages = 5; + component.currentPage = 3; + + const eventRight = new KeyboardEvent('keydown', { key: 'ArrowRight' }); + const eventLeft = new KeyboardEvent('keydown', { key: 'ArrowLeft' }); + + component.handleKeyboardEvents(eventRight); + expect(component.currentPage).toBe(4); + + component.handleKeyboardEvents(eventLeft); + expect(component.currentPage).toBe(3); + }); + + it('should toggle enlarged view state', () => { + const mockCanvas = document.createElement('canvas'); + component.displayEnlargedCanvas(mockCanvas, 1); + expect(component.isEnlargedView).toBeTrue(); + + const clickEvent = new MouseEvent('click', { + button: 0, + }); + + component.closeEnlargedView(clickEvent); + expect(component.isEnlargedView).toBeFalse(); + }); + + it('should prevent scrolling when enlarged view is active', () => { + component.toggleBodyScroll(true); + expect(component.pdfContainer.nativeElement.style.overflow).toBe('hidden'); + + component.toggleBodyScroll(false); + expect(component.pdfContainer.nativeElement.style.overflow).toBe('auto'); + }); + + it('should not update canvas size if not in enlarged view', () => { + component.isEnlargedView = false; + component.currentPage = 3; + + const spy = jest.spyOn(component, 'updateEnlargedCanvas'); + component.adjustCanvasSize(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('should not update canvas size if the current page canvas does not exist', () => { + component.isEnlargedView = true; + component.currentPage = 10; + + const spy = jest.spyOn(component, 'updateEnlargedCanvas'); + component.adjustCanvasSize(); + + expect(spy).not.toHaveBeenCalled(); + }); + + it('should prevent navigation beyond last page', () => { + component.currentPage = component.totalPages = 5; + component.handleKeyboardEvents(new KeyboardEvent('keydown', { key: 'ArrowRight' })); + + expect(component.currentPage).toBe(5); + }); + + it('should prevent navigation before first page', () => { + component.currentPage = 1; + component.handleKeyboardEvents(new KeyboardEvent('keydown', { key: 'ArrowLeft' })); + + expect(component.currentPage).toBe(1); + }); + + it('should unsubscribe attachment subscription during component destruction', () => { + const spySub = jest.spyOn(component.attachmentSub, 'unsubscribe'); + component.ngOnDestroy(); + expect(spySub).toHaveBeenCalled(); + }); + + it('should unsubscribe attachmentUnit subscription during component destruction', () => { + routeMock.data = of({ + course: { id: 1, name: 'Example Course' }, + attachmentUnit: { id: 1, name: 'Chapter 1' }, + }); + component.ngOnInit(); + fixture.detectChanges(); + expect(component.attachmentUnitSub).toBeDefined(); + const spySub = jest.spyOn(component.attachmentUnitSub, 'unsubscribe'); + component.ngOnDestroy(); + expect(spySub).toHaveBeenCalled(); + }); + + it('should stop event propagation and navigate pages', () => { + const navigateSpy = jest.spyOn(component, 'navigatePages'); + const eventMock = { stopPropagation: jest.fn() } as unknown as MouseEvent; + + component.handleNavigation('next', eventMock); + + expect(eventMock.stopPropagation).toHaveBeenCalled(); + expect(navigateSpy).toHaveBeenCalledWith('next'); + }); + + it('should call updateEnlargedCanvas when window is resized and conditions are met', () => { + component.isEnlargedView = true; + component.currentPage = 1; + + const canvas = document.createElement('canvas'); + const pdfContainer = document.createElement('div'); + pdfContainer.className = 'pdf-page-container'; + pdfContainer.appendChild(canvas); + component.pdfContainer = { + nativeElement: pdfContainer, + } as ElementRef; + + const updateEnlargedCanvasSpy = jest.spyOn(component, 'updateEnlargedCanvas'); + const adjustCanvasSizeSpy = jest.spyOn(component, 'adjustCanvasSize'); + + window.dispatchEvent(new Event('resize')); + expect(adjustCanvasSizeSpy).toHaveBeenCalled(); + expect(updateEnlargedCanvasSpy).toHaveBeenCalledWith(canvas); + }); + + it('should close the enlarged view if click is outside the canvas within the enlarged container', () => { + const target = document.createElement('div'); + target.classList.add('enlarged-container'); + const mockEvent = createMockEvent(target); + + component.isEnlargedView = true; + const closeSpy = jest.spyOn(component, 'closeEnlargedView'); + + component.closeIfOutside(mockEvent); + + expect(closeSpy).toHaveBeenCalled(); + expect(component.isEnlargedView).toBeFalse(); + }); + + it('should not close the enlarged view if the click is on the canvas itself', () => { + const mockEvent = createMockEvent(mockEnlargedCanvas); + Object.defineProperty(mockEvent, 'target', { value: mockEnlargedCanvas, writable: false }); + + component.isEnlargedView = true; + + const closeSpy = jest.spyOn(component, 'closeEnlargedView'); + + component.closeIfOutside(mockEvent as unknown as MouseEvent); + + expect(closeSpy).not.toHaveBeenCalled(); + }); + + it('should calculate the correct scale factor based on container and canvas dimensions', () => { + Object.defineProperty(component.pdfContainer.nativeElement, 'clientWidth', { value: 1000, configurable: true }); + Object.defineProperty(component.pdfContainer.nativeElement, 'clientHeight', { value: 800, configurable: true }); + + mockCanvasElement.width = 500; + mockCanvasElement.height = 400; + + const scaleFactor = component.calculateScaleFactor(mockCanvasElement); + expect(scaleFactor).toBe(2); + }); + + it('should resize the canvas based on the given scale factor', () => { + mockCanvasElement.width = 500; + mockCanvasElement.height = 400; + component.resizeCanvas(mockCanvasElement, 2); + + expect(component.enlargedCanvas.nativeElement.width).toBe(1000); + expect(component.enlargedCanvas.nativeElement.height).toBe(800); + }); + + it('should clear and redraw the canvas with the new dimensions', () => { + mockCanvasElement.width = 500; + mockCanvasElement.height = 400; + + jest.spyOn(mockContext, 'clearRect'); + jest.spyOn(mockContext, 'drawImage'); + + component.resizeCanvas(mockCanvasElement, 2); + component.redrawCanvas(mockCanvasElement); + + expect(component.enlargedCanvas.nativeElement.width).toBe(1000); // 500 * 2 + expect(component.enlargedCanvas.nativeElement.height).toBe(800); // 400 * 2 + + expect(mockContext.clearRect).toHaveBeenCalledWith(0, 0, 1000, 800); + expect(mockContext.drawImage).toHaveBeenCalledWith(mockCanvasElement, 0, 0, 1000, 800); + }); + + it('should correctly position the canvas', () => { + const parent = document.createElement('div'); + component.pdfContainer = { nativeElement: { clientWidth: 1000, clientHeight: 800, scrollTop: 500 } } as ElementRef; + const canvasElem = component.enlargedCanvas.nativeElement; + parent.appendChild(canvasElem); + canvasElem.width = 500; + canvasElem.height = 400; + component.positionCanvas(); + expect(canvasElem.style.left).toBe('250px'); + expect(canvasElem.style.top).toBe('200px'); + expect(parent.style.top).toBe('500px'); + }); + + it('should create a container with correct styles and children', () => { + const mockCanvas = document.createElement('canvas'); + mockCanvas.style.width = '600px'; + mockCanvas.style.height = '400px'; + + const container = component.createContainer(mockCanvas, 1); + expect(container.tagName).toBe('DIV'); + expect(container.classList.contains('pdf-page-container')).toBeTruthy(); + expect(container.style.position).toBe('relative'); + expect(container.style.display).toBe('inline-block'); + expect(container.style.width).toBe('600px'); + expect(container.style.height).toBe('400px'); + expect(container.style.margin).toBe('20px'); + expect(container.children).toHaveLength(2); + + expect(container.firstChild).toBe(mockCanvas); + }); + + it('should handle mouseenter and mouseleave events correctly', () => { + const mockCanvas = document.createElement('canvas'); + const container = component.createContainer(mockCanvas, 1); + const overlay = container.children[1] as HTMLElement; + + // Trigger mouseenter + const mouseEnterEvent = new Event('mouseenter'); + container.dispatchEvent(mouseEnterEvent); + expect(overlay.style.opacity).toBe('1'); + + // Trigger mouseleave + const mouseLeaveEvent = new Event('mouseleave'); + container.dispatchEvent(mouseLeaveEvent); + expect(overlay.style.opacity).toBe('0'); + }); + + it('should handle click event on overlay to trigger displayEnlargedCanvas', () => { + jest.spyOn(component, 'displayEnlargedCanvas'); + const mockCanvas = document.createElement('canvas'); + const container = component.createContainer(mockCanvas, 1); + const overlay = container.children[1]; + + overlay.dispatchEvent(new Event('click')); + expect(component.displayEnlargedCanvas).toHaveBeenCalledWith(mockCanvas, 1); + }); +}); diff --git a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-competencies.component.spec.ts b/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-competencies.component.spec.ts deleted file mode 100644 index 914fe55c18ab..000000000000 --- a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-competencies.component.spec.ts +++ /dev/null @@ -1,518 +0,0 @@ -import { ComponentFixture, TestBed, fakeAsync } from '@angular/core/testing'; -import { MockComponent, MockPipe, MockProvider } from 'ng-mocks'; -import { AlertService } from 'app/core/util/alert.service'; -import { ActivatedRoute, Router } from '@angular/router'; -import { MockRouter } from '../../../helpers/mocks/mock-router'; -import { of, throwError } from 'rxjs'; -import { HttpResponse } from '@angular/common/http'; -import { Lecture } from 'app/entities/lecture.model'; -import { LectureUpdateWizardCompetenciesComponent } from 'app/lecture/wizard-mode/lecture-wizard-competencies.component'; -import { LectureService } from 'app/lecture/lecture.service'; -import { MockTranslateService } from '../../../helpers/mocks/service/mock-translate.service'; -import { TranslateService } from '@ngx-translate/core'; -import { CompetencyService } from 'app/course/competencies/competency.service'; -import { Competency } from 'app/entities/competency.model'; -import { Course } from 'app/entities/course.model'; -import { ArtemisTranslatePipe } from 'app/shared/pipes/artemis-translate.pipe'; -import { TextUnit } from 'app/entities/lecture-unit/textUnit.model'; -import { ExerciseUnit } from 'app/entities/lecture-unit/exerciseUnit.model'; -import { TextExercise } from 'app/entities/text/text-exercise.model'; -import { FaIconComponent } from '@fortawesome/angular-fontawesome'; -import { CourseCompetencyFormData } from 'app/course/competencies/forms/course-competency-form.component'; - -describe('LectureWizardCompetenciesComponent', () => { - let wizardCompetenciesComponentFixture: ComponentFixture; - let wizardCompetenciesComponent: LectureUpdateWizardCompetenciesComponent; - - beforeEach(() => { - TestBed.configureTestingModule({ - imports: [], - declarations: [LectureUpdateWizardCompetenciesComponent, MockPipe(ArtemisTranslatePipe), MockComponent(FaIconComponent)], - providers: [ - MockProvider(AlertService), - MockProvider(LectureService), - MockProvider(CompetencyService), - { provide: TranslateService, useClass: MockTranslateService }, - { provide: Router, useClass: MockRouter }, - { - provide: ActivatedRoute, - useValue: { queryParams: of({}) }, - }, - ], - schemas: [], - }) - .compileComponents() - .then(() => { - wizardCompetenciesComponentFixture = TestBed.createComponent(LectureUpdateWizardCompetenciesComponent); - wizardCompetenciesComponent = wizardCompetenciesComponentFixture.componentInstance; - - const course = new Course(); - course.id = 2; - - wizardCompetenciesComponent.lecture = new Lecture(); - wizardCompetenciesComponent.lecture.id = 1; - wizardCompetenciesComponent.lecture.course = course; - }); - }); - - afterEach(() => { - jest.restoreAllMocks(); - }); - - it('should initialize and load data', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - - const lecture = new Lecture(); - lecture.id = 1; - const lectureResponse: HttpResponse = new HttpResponse({ - body: lecture, - status: 201, - }); - const lectureStub = jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(of(lectureResponse)); - - const competencies: Competency[] = [{}]; - const competenciesResponse: HttpResponse = new HttpResponse({ - body: competencies, - status: 201, - }); - const competenciesStub = jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(of(competenciesResponse)); - - wizardCompetenciesComponentFixture.detectChanges(); - expect(wizardCompetenciesComponent).not.toBeNull(); - - wizardCompetenciesComponentFixture.whenStable().then(() => { - expect(lectureStub).toHaveBeenCalledOnce(); - expect(competenciesStub).toHaveBeenCalledOnce(); - - expect(wizardCompetenciesComponent.lecture).toBe(lecture); - expect(wizardCompetenciesComponent.competencies).toBe(competencies); - }); - })); - - it('should show create form and load lecture when clicked', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - - const lecture = new Lecture(); - lecture.id = 1; - const lectureResponse: HttpResponse = new HttpResponse({ - body: lecture, - status: 201, - }); - const lectureStub = jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(of(lectureResponse)); - - const competencies: Competency[] = [{}]; - const competenciesResponse: HttpResponse = new HttpResponse({ - body: competencies, - status: 201, - }); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(of(competenciesResponse)); - - wizardCompetenciesComponentFixture.detectChanges(); - - wizardCompetenciesComponent.showCreateCompetency(); - - wizardCompetenciesComponentFixture.whenStable().then(() => { - expect(lectureStub).toHaveBeenCalledTimes(2); - - expect(wizardCompetenciesComponent.lecture).toBe(lecture); - expect(wizardCompetenciesComponent.isAddingCompetency).toBeTrue(); - }); - })); - - it('should show an alert when loading fails', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - const alertService = TestBed.inject(AlertService); - - const lectureStub = jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - - const competencies: Competency[] = [{}]; - const competenciesResponse: HttpResponse = new HttpResponse({ - body: competencies, - status: 201, - }); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(of(competenciesResponse)); - - const alertStub = jest.spyOn(alertService, 'error'); - - wizardCompetenciesComponentFixture.detectChanges(); - - wizardCompetenciesComponentFixture.whenStable().then(() => { - expect(lectureStub).toHaveBeenCalledOnce(); - expect(alertStub).toHaveBeenCalledOnce(); - }); - })); - - it('should show an alert when creating fails', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - const alertService = TestBed.inject(AlertService); - - jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - const createStub = jest.spyOn(competencyService, 'create').mockReturnValue(throwError(() => ({ status: 404 }))); - - const competencies: Competency[] = [{}]; - const competenciesResponse: HttpResponse = new HttpResponse({ - body: competencies, - status: 201, - }); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(of(competenciesResponse)); - - const alertStub = jest.spyOn(alertService, 'error'); - - wizardCompetenciesComponentFixture.detectChanges(); - - wizardCompetenciesComponent.createCompetency({ - id: 1, - title: 'Competency', - }); - - wizardCompetenciesComponentFixture.whenStable().then(() => { - expect(createStub).toHaveBeenCalledOnce(); - expect(alertStub).toHaveBeenCalledTimes(2); - }); - })); - - it('should show an alert when deleting fails', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - const alertService = TestBed.inject(AlertService); - - jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - - const competencies: Competency[] = [{}]; - const competenciesResponse: HttpResponse = new HttpResponse({ - body: competencies, - status: 201, - }); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(of(competenciesResponse)); - - const alertStub = jest.spyOn(alertService, 'error'); - const deleteStub = jest.spyOn(competencyService, 'delete').mockReturnValue(throwError(() => ({ status: 404 }))); - - wizardCompetenciesComponentFixture.detectChanges(); - - wizardCompetenciesComponent.deleteCompetency(competencies[0]); - - wizardCompetenciesComponentFixture.whenStable().then(() => { - expect(deleteStub).toHaveBeenCalledOnce(); - expect(alertStub).toHaveBeenCalledOnce(); - }); - })); - - it('should show an alert when editing fails', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - const alertService = TestBed.inject(AlertService); - - jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - const editStub = jest.spyOn(competencyService, 'update').mockReturnValue(throwError(() => ({ status: 404 }))); - - const competencies: Competency[] = [{}]; - const competenciesResponse: HttpResponse = new HttpResponse({ - body: competencies, - status: 201, - }); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(of(competenciesResponse)); - - const alertStub = jest.spyOn(alertService, 'error'); - - wizardCompetenciesComponentFixture.detectChanges(); - - wizardCompetenciesComponentFixture.whenStable().then(() => { - wizardCompetenciesComponent.currentlyProcessedCompetency = {}; - wizardCompetenciesComponent.editCompetency({ - id: 1, - title: 'Competency', - }); - wizardCompetenciesComponentFixture.whenStable().then(() => { - expect(editStub).toHaveBeenCalledOnce(); - expect(alertStub).toHaveBeenCalledTimes(2); - }); - }); - })); - - it('should close all forms when canceling', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - - jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - - const competencies: Competency[] = [{}]; - const competenciesResponse: HttpResponse = new HttpResponse({ - body: competencies, - status: 201, - }); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(of(competenciesResponse)); - - wizardCompetenciesComponentFixture.detectChanges(); - - wizardCompetenciesComponent.onCompetencyFormCanceled(); - - wizardCompetenciesComponentFixture.whenStable().then(() => { - expect(wizardCompetenciesComponent.isAddingCompetency).toBeFalse(); - expect(wizardCompetenciesComponent.isEditingCompetency).toBeFalse(); - expect(wizardCompetenciesComponent.isLoadingCompetencyForm).toBeFalse(); - }); - })); - - it('should delete the competency when clicked', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - - jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - - const competencies: Competency[] = [{}]; - const competenciesResponse: HttpResponse = new HttpResponse({ - body: competencies, - status: 201, - }); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(of(competenciesResponse)); - const deleteStub = jest.spyOn(competencyService, 'delete').mockReturnValue(of(new HttpResponse({ status: 201 }))); - - wizardCompetenciesComponentFixture.detectChanges(); - - wizardCompetenciesComponent.deleteCompetency(competencies[0]); - - wizardCompetenciesComponentFixture.whenStable().then(() => { - expect(deleteStub).toHaveBeenCalledOnce(); - }); - })); - - it('should open the form when editing', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - - jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(throwError(() => ({ status: 404 }))); - - wizardCompetenciesComponentFixture.detectChanges(); - - const competency: Competency = {}; - competency.id = 12; - wizardCompetenciesComponent.startEditCompetency(competency); - - wizardCompetenciesComponentFixture.whenStable().then(() => { - expect(wizardCompetenciesComponent.isEditingCompetency).toBeTrue(); - expect(wizardCompetenciesComponent.currentlyProcessedCompetency.id).toBe(12); - }); - })); - - it('should return the connected units for a competency and lecture', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - - jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(throwError(() => ({ status: 404 }))); - - wizardCompetenciesComponentFixture.detectChanges(); - - const lectureUnit = new TextUnit(); - lectureUnit.name = 'Test'; - lectureUnit.id = 5; - - wizardCompetenciesComponent.lecture.lectureUnits = [lectureUnit]; - - const competency: Competency = {}; - competency.id = 12; - competency.lectureUnits = [lectureUnit]; - const result = wizardCompetenciesComponent.getConnectedUnitsForCompetency(competency); - - expect(result).toBe('Test'); - })); - - it('should return no connected units for empty competency', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - - jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(throwError(() => ({ status: 404 }))); - - wizardCompetenciesComponentFixture.detectChanges(); - - const competency: Competency = {}; - competency.id = 12; - const result = wizardCompetenciesComponent.getConnectedUnitsForCompetency(competency); - - expect(result).toBe('artemisApp.lecture.wizardMode.competencyNoConnectedUnits'); - })); - - it('should call the service and show an alert when creating a competency', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - const alertService = TestBed.inject(AlertService); - - jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(throwError(() => ({ status: 404 }))); - - const createStub = jest.spyOn(competencyService, 'create').mockReturnValue(of(new HttpResponse({ status: 201, body: {} }))); - const alertStub = jest.spyOn(alertService, 'success'); - - wizardCompetenciesComponentFixture.detectChanges(); - - const formData: CourseCompetencyFormData = { - id: 1, - title: 'Competency', - }; - - wizardCompetenciesComponentFixture.whenStable().then(() => { - wizardCompetenciesComponent.isEditingCompetency = false; - wizardCompetenciesComponent.onCompetencyFormSubmitted(formData); - - wizardCompetenciesComponentFixture.whenStable().then(() => { - expect(wizardCompetenciesComponent.isAddingCompetency).toBeFalse(); - expect(createStub).toHaveBeenCalledOnce(); - expect(alertStub).toHaveBeenCalledOnce(); - }); - }); - })); - - it('should append exercises as units when creating a competency', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - const alertService = TestBed.inject(AlertService); - - jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(throwError(() => ({ status: 404 }))); - - const competency: Competency = {}; - const exercise = new TextExercise(undefined, undefined); - exercise.id = 2; - competency.exercises = [exercise]; - competency.lectureUnits = []; - const createStub = jest.spyOn(competencyService, 'create').mockReturnValue(of(new HttpResponse({ status: 201, body: competency }))); - const alertStub = jest.spyOn(alertService, 'success'); - - const unit = new ExerciseUnit(); - unit.id = 2; - unit.exercise = exercise; - - wizardCompetenciesComponent.lecture.lectureUnits = [unit]; - - wizardCompetenciesComponentFixture.detectChanges(); - - const formData: CourseCompetencyFormData = { - id: 1, - title: 'Competency', - }; - - wizardCompetenciesComponentFixture.whenStable().then(() => { - wizardCompetenciesComponent.lecture.lectureUnits = [unit]; - wizardCompetenciesComponent.isEditingCompetency = false; - wizardCompetenciesComponent.onCompetencyFormSubmitted(formData); - - wizardCompetenciesComponentFixture.whenStable().then(() => { - expect(wizardCompetenciesComponent.isAddingCompetency).toBeFalse(); - expect(createStub).toHaveBeenCalledOnce(); - expect(alertStub).toHaveBeenCalledOnce(); - - expect(wizardCompetenciesComponent.competencies).toHaveLength(1); - expect(wizardCompetenciesComponent.competencies[0]!.lectureUnits![0]!.id).toBe(2); - }); - }); - })); - - it('should not call the service when creating a competency with an empty form', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - const alertService = TestBed.inject(AlertService); - - jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(throwError(() => ({ status: 404 }))); - - const createStub = jest.spyOn(competencyService, 'create').mockReturnValue(of(new HttpResponse({ status: 201, body: {} }))); - const alertStub = jest.spyOn(alertService, 'success'); - - wizardCompetenciesComponentFixture.detectChanges(); - - const formData: CourseCompetencyFormData = {}; - - wizardCompetenciesComponentFixture.whenStable().then(() => { - wizardCompetenciesComponent.createCompetency(formData); - - wizardCompetenciesComponentFixture.whenStable().then(() => { - expect(createStub).not.toHaveBeenCalled(); - expect(alertStub).not.toHaveBeenCalled(); - }); - }); - })); - - it('should call the service and show an alert when editing a competency', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - const alertService = TestBed.inject(AlertService); - - jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(throwError(() => ({ status: 404 }))); - - const editStub = jest.spyOn(competencyService, 'update').mockReturnValue(of(new HttpResponse({ status: 201, body: {} }))); - const alertStub = jest.spyOn(alertService, 'success'); - - wizardCompetenciesComponentFixture.detectChanges(); - - const formData: CourseCompetencyFormData = { - id: 1, - title: 'Competency', - }; - - wizardCompetenciesComponentFixture.whenStable().then(() => { - wizardCompetenciesComponent.currentlyProcessedCompetency = {}; - wizardCompetenciesComponent.isEditingCompetency = true; - wizardCompetenciesComponent.onCompetencyFormSubmitted(formData); - - wizardCompetenciesComponentFixture.whenStable().then(() => { - expect(wizardCompetenciesComponent.isEditingCompetency).toBeFalse(); - expect(editStub).toHaveBeenCalledOnce(); - expect(alertStub).toHaveBeenCalledOnce(); - }); - }); - })); - - it('should append exercises as units when editing a competency', fakeAsync(() => { - const lectureService = TestBed.inject(LectureService); - const competencyService = TestBed.inject(CompetencyService); - const alertService = TestBed.inject(AlertService); - - jest.spyOn(lectureService, 'findWithDetails').mockReturnValue(throwError(() => ({ status: 404 }))); - jest.spyOn(competencyService, 'getAllForCourse').mockReturnValue(throwError(() => ({ status: 404 }))); - - const competency: Competency = {}; - const exercise = new TextExercise(undefined, undefined); - exercise.id = 2; - competency.exercises = [exercise]; - competency.lectureUnits = []; - const editStub = jest.spyOn(competencyService, 'update').mockReturnValue(of(new HttpResponse({ status: 201, body: competency }))); - const alertStub = jest.spyOn(alertService, 'success'); - - const unit = new ExerciseUnit(); - unit.id = 2; - unit.exercise = exercise; - - wizardCompetenciesComponent.lecture.lectureUnits = [unit]; - - wizardCompetenciesComponentFixture.detectChanges(); - - const formData: CourseCompetencyFormData = { - id: 1, - title: 'Competency', - }; - - wizardCompetenciesComponentFixture.whenStable().then(() => { - wizardCompetenciesComponent.currentlyProcessedCompetency = {}; - wizardCompetenciesComponent.lecture.lectureUnits = [unit]; - wizardCompetenciesComponent.isEditingCompetency = true; - wizardCompetenciesComponent.onCompetencyFormSubmitted(formData); - - wizardCompetenciesComponentFixture.whenStable().then(() => { - expect(wizardCompetenciesComponent.isEditingCompetency).toBeFalse(); - expect(editStub).toHaveBeenCalledOnce(); - expect(alertStub).toHaveBeenCalledOnce(); - - expect(wizardCompetenciesComponent.competencies).toHaveLength(1); - expect(wizardCompetenciesComponent.competencies[0]!.lectureUnits![0]!.id).toBe(2); - }); - }); - })); -}); diff --git a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-units.component.spec.ts b/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-units.component.spec.ts index cb597fb41f64..58113a1c5b16 100644 --- a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-units.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard-units.component.spec.ts @@ -163,7 +163,16 @@ describe('LectureWizardUnitComponent', () => { name: 'Test', releaseDate: dayjs().year(2010).month(3).date(5), description: 'Lorem Ipsum', - source: 'https://www.youtube.com/embed/8iU8LPEa4o0', + source: 'https://youtu.be/dQw4w9WgXcQ', + competencies: [ + { + id: 1, + masteryThreshold: 0, + optional: false, + taxonomy: undefined, + title: 'Test', + }, + ], }; const response: HttpResponse = new HttpResponse({ @@ -192,6 +201,7 @@ describe('LectureWizardUnitComponent', () => { expect(videoUnitCallArgument.description).toEqual(formData.description); expect(videoUnitCallArgument.releaseDate).toEqual(formData.releaseDate); expect(videoUnitCallArgument.source).toEqual(formData.source); + expect(videoUnitCallArgument.competencies).toEqual(formData.competencies); expect(lectureIdCallArgument).toBe(1); expect(createStub).toHaveBeenCalledOnce(); @@ -227,6 +237,15 @@ describe('LectureWizardUnitComponent', () => { name: 'Test', releaseDate: dayjs().year(2010).month(3).date(5), content: 'Lorem Ipsum', + competencies: [ + { + id: 1, + masteryThreshold: 0, + optional: false, + taxonomy: undefined, + title: 'Test', + }, + ], }; const persistedTextUnit: TextUnit = new TextUnit(); @@ -254,6 +273,15 @@ describe('LectureWizardUnitComponent', () => { wizardUnitComponent.createEditTextUnit(formData); wizardUnitComponentFixture.whenStable().then(() => { + const textUnitCallArgument: TextUnit = createStub.mock.calls[0][0]; + const lectureIdCallArgument: number = createStub.mock.calls[0][1]; + + expect(textUnitCallArgument.name).toEqual(formData.name); + expect(textUnitCallArgument.content).toEqual(formData.content); + expect(textUnitCallArgument.releaseDate).toEqual(formData.releaseDate); + expect(textUnitCallArgument.competencies).toEqual(formData.competencies); + expect(lectureIdCallArgument).toBe(1); + expect(createStub).toHaveBeenCalledOnce(); expect(updateSpy).toHaveBeenCalledOnce(); @@ -368,6 +396,15 @@ describe('LectureWizardUnitComponent', () => { releaseDate: dayjs().year(2010).month(3).date(5), description: 'Lorem Ipsum', source: 'https://www.example.com', + competencies: [ + { + id: 1, + masteryThreshold: 0, + optional: false, + taxonomy: undefined, + title: 'Test', + }, + ], }; const response: HttpResponse = new HttpResponse({ @@ -396,6 +433,7 @@ describe('LectureWizardUnitComponent', () => { expect(onlineUnitCallArgument.description).toEqual(formDate.description); expect(onlineUnitCallArgument.releaseDate).toEqual(formDate.releaseDate); expect(onlineUnitCallArgument.source).toEqual(formDate.source); + expect(onlineUnitCallArgument.competencies).toEqual(formDate.competencies); expect(lectureIdCallArgument).toBe(1); expect(createStub).toHaveBeenCalledOnce(); @@ -436,6 +474,15 @@ describe('LectureWizardUnitComponent', () => { releaseDate: dayjs().year(2010).month(3).date(5), version: 2, updateNotificationText: 'lorem ipsum', + competencies: [ + { + id: 1, + masteryThreshold: 0, + optional: false, + taxonomy: undefined, + title: 'Test', + }, + ], }, fileProperties: { file: fakeFile, @@ -479,7 +526,11 @@ describe('LectureWizardUnitComponent', () => { wizardUnitComponent.createEditAttachmentUnit(attachmentUnitFormData); wizardUnitComponentFixture.whenStable().then(() => { + const lectureIdCallArgument: number = createAttachmentUnitStub.mock.calls[0][1]; + + expect(lectureIdCallArgument).toBe(1); expect(createAttachmentUnitStub).toHaveBeenCalledWith(formData, 1); + expect(updateSpy).toHaveBeenCalledOnce(); updateSpy.mockRestore(); @@ -605,6 +656,62 @@ describe('LectureWizardUnitComponent', () => { }); })); + it('should show alert upon unsuccessful attachment form submission with error information', fakeAsync(() => { + const attachmentUnitService = TestBed.inject(AttachmentUnitService); + const alertService = TestBed.inject(AlertService); + + const fakeFile = new File([''], 'Test-File.pdf', { type: 'application/pdf' }); + + const attachmentUnitFormData: AttachmentUnitFormData = { + formProperties: { + name: 'test', + description: 'lorem ipsum', + releaseDate: dayjs().year(2010).month(3).date(5), + version: 2, + updateNotificationText: 'lorem ipsum', + }, + fileProperties: { + file: fakeFile, + fileName: 'lorem ipsum', + }, + }; + + const examplePath = '/path/to/file'; + + const attachment = new Attachment(); + attachment.version = 1; + attachment.attachmentType = AttachmentType.FILE; + attachment.releaseDate = attachmentUnitFormData.formProperties.releaseDate; + attachment.name = attachmentUnitFormData.formProperties.name; + attachment.link = examplePath; + + const attachmentUnit = new AttachmentUnit(); + attachmentUnit.description = attachmentUnitFormData.formProperties.description; + attachmentUnit.attachment = attachment; + + const formData = new FormData(); + formData.append('file', fakeFile, attachmentUnitFormData.fileProperties.fileName); + formData.append('attachment', objectToJsonBlob(attachment)); + formData.append('attachmentUnit', objectToJsonBlob(attachmentUnit)); + + const createAttachmentUnitStub = jest + .spyOn(attachmentUnitService, 'create') + .mockReturnValue(throwError(() => ({ status: 404, error: { params: 'file', title: 'Test Title' } }))); + const alertStub = jest.spyOn(alertService, 'error'); + + wizardUnitComponentFixture.detectChanges(); + tick(); + + wizardUnitComponent.isAttachmentUnitFormOpen = true; + + wizardUnitComponent.createEditAttachmentUnit(attachmentUnitFormData); + + wizardUnitComponentFixture.whenStable().then(() => { + expect(createAttachmentUnitStub).toHaveBeenCalledOnce(); + expect(alertStub).toHaveBeenCalledOnce(); + }); + })); + it('should not send POST request upon empty attachment form submission', fakeAsync(() => { const attachmentUnitService = TestBed.inject(AttachmentUnitService); diff --git a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard.component.spec.ts b/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard.component.spec.ts index 39f6b3b80998..716de13900a0 100644 --- a/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard.component.spec.ts +++ b/src/test/javascript/spec/component/lecture/wizard-mode/lecture-wizard.component.spec.ts @@ -12,7 +12,6 @@ import { LectureUpdateWizardComponent } from 'app/lecture/wizard-mode/lecture-up import { ArtemisNavigationUtilService } from 'app/utils/navigation.utils'; import { CourseManagementService } from 'app/course/manage/course-management.service'; import { LectureUpdateWizardStepComponent } from 'app/lecture/wizard-mode/lecture-update-wizard-step.component'; -import { LectureUpdateWizardCompetenciesComponent } from 'app/lecture/wizard-mode/lecture-wizard-competencies.component'; import { LectureUpdateWizardUnitsComponent } from 'app/lecture/wizard-mode/lecture-wizard-units.component'; import { LectureUpdateWizardAttachmentsComponent } from 'app/lecture/wizard-mode/lecture-wizard-attachments.component'; import { LectureUpdateWizardPeriodComponent } from 'app/lecture/wizard-mode/lecture-wizard-period.component'; @@ -32,7 +31,6 @@ describe('LectureWizardComponent', () => { LectureUpdateWizardComponent, MockPipe(ArtemisTranslatePipe), MockComponent(LectureUpdateWizardStepComponent), - MockComponent(LectureUpdateWizardCompetenciesComponent), MockComponent(LectureUpdateWizardUnitsComponent), MockComponent(LectureUpdateWizardAttachmentsComponent), MockComponent(LectureUpdateWizardPeriodComponent), @@ -75,7 +73,7 @@ describe('LectureWizardComponent', () => { expect(wizardComponent).not.toBeNull(); wizardComponentFixture.whenStable().then(() => { - expect(wizardComponent.currentStep).toBe(5); + expect(wizardComponent.currentStep).toBe(4); }); })); @@ -134,7 +132,7 @@ describe('LectureWizardComponent', () => { wizardComponentFixture.detectChanges(); wizardComponentFixture.whenStable().then(() => { - expect(wizardComponent.currentStep).toBe(5); + expect(wizardComponent.currentStep).toBe(4); wizardComponent.next(); expect(saveStub).toHaveBeenCalledOnce(); }); @@ -150,46 +148,6 @@ describe('LectureWizardComponent', () => { }); })); - it('should return is completed for smaller step', fakeAsync(() => { - wizardComponentFixture.detectChanges(); - - wizardComponentFixture.whenStable().then(() => { - wizardComponent.currentStep = 2; - const result = wizardComponent.isCompleted(1); - expect(result).toBeTrue(); - }); - })); - - it('should not return is completed for bigger step', fakeAsync(() => { - wizardComponentFixture.detectChanges(); - - wizardComponentFixture.whenStable().then(() => { - wizardComponent.currentStep = 2; - const result = wizardComponent.isCompleted(3); - expect(result).toBeFalse(); - }); - })); - - it('should return is current for same step', fakeAsync(() => { - wizardComponentFixture.detectChanges(); - - wizardComponentFixture.whenStable().then(() => { - wizardComponent.currentStep = 2; - const result = wizardComponent.isCurrent(2); - expect(result).toBeTrue(); - }); - })); - - it('should not return is current for different step', fakeAsync(() => { - wizardComponentFixture.detectChanges(); - - wizardComponentFixture.whenStable().then(() => { - wizardComponent.currentStep = 2; - const result = wizardComponent.isCurrent(1); - expect(result).toBeFalse(); - }); - })); - it('should return correct icon for last step', fakeAsync(() => { wizardComponentFixture.detectChanges(); diff --git a/src/test/javascript/spec/component/shared/code-button.component.spec.ts b/src/test/javascript/spec/component/shared/code-button.component.spec.ts index 967f51a4e333..6423338cddbf 100644 --- a/src/test/javascript/spec/component/shared/code-button.component.spec.ts +++ b/src/test/javascript/spec/component/shared/code-button.component.spec.ts @@ -83,6 +83,7 @@ describe('CodeButtonComponent', () => { }, }, theiaPortalURL: 'https://theia-test.k8s.ase.cit.tum.de', + operatorName: 'TUM', }; let participation: ProgrammingExerciseStudentParticipation = new ProgrammingExerciseStudentParticipation(); diff --git a/src/test/javascript/spec/core/language/language.helper.spec.ts b/src/test/javascript/spec/core/language/language.helper.spec.ts index 04998034ff39..82b7c7b0fdcf 100644 --- a/src/test/javascript/spec/core/language/language.helper.spec.ts +++ b/src/test/javascript/spec/core/language/language.helper.spec.ts @@ -35,7 +35,7 @@ describe('Language Helper', () => { it('determinePreferredLanguages should respect user preference', inject([JhiLanguageHelper], (service: JhiLanguageHelper) => { const navigator = { languages: ['de', 'en'], - }; + } as unknown as Navigator; const languageChangeSpy = jest.spyOn(service, 'getNavigatorReference').mockReturnValue(navigator); expect(service.determinePreferredLanguage()).toBe('de'); @@ -45,7 +45,7 @@ describe('Language Helper', () => { it('determinePreferredLanguage should return english if no other language matches', inject([JhiLanguageHelper], (service: JhiLanguageHelper) => { const navigator = { languages: ['elvish', 'orcish'], - }; + } as unknown as Navigator; const languageChangeSpy = jest.spyOn(service, 'getNavigatorReference').mockReturnValue(navigator); expect(service.determinePreferredLanguage()).toBe('en'); diff --git a/src/test/javascript/spec/service/attachment.service.spec.ts b/src/test/javascript/spec/service/attachment.service.spec.ts index b29db559c4e9..77356f0aa445 100644 --- a/src/test/javascript/spec/service/attachment.service.spec.ts +++ b/src/test/javascript/spec/service/attachment.service.spec.ts @@ -145,4 +145,23 @@ describe('Attachment Service', () => { expect(results).toEqual(elemDefault); }); }); + + describe('getAttachmentFile', () => { + it('should retrieve a file as Blob for a given course and attachment ID', () => { + const courseId = 1; + const attachmentId = 100; + const expectedBlob = new Blob(['dummy content'], { type: 'application/pdf' }); + + service.getAttachmentFile(courseId, attachmentId).subscribe((resp) => { + expect(resp).toEqual(expectedBlob); + }); + + const req = httpMock.expectOne({ + url: `api/files/courses/${courseId}/attachments/${attachmentId}`, + method: 'GET', + }); + expect(req.request.responseType).toBe('blob'); + req.flush(expectedBlob); + }); + }); }); diff --git a/src/test/javascript/spec/service/profile.service.spec.ts b/src/test/javascript/spec/service/profile.service.spec.ts index cc833e61a7f6..45d63d7f9482 100644 --- a/src/test/javascript/spec/service/profile.service.spec.ts +++ b/src/test/javascript/spec/service/profile.service.spec.ts @@ -145,6 +145,7 @@ describe('ProfileService', () => { }, }, }, + operatorName: 'TUM', theiaPortalURL: 'http://theia-test.k8s.ase.cit.tum.de', }; @@ -261,6 +262,7 @@ describe('ProfileService', () => { }, }, theiaPortalURL: 'http://theia-test.k8s.ase.cit.tum.de', + operatorName: 'TUM', }; beforeEach(() => { diff --git a/src/test/playwright/e2e/Login.spec.ts b/src/test/playwright/e2e/Login.spec.ts index 375fc05a0311..b084440df47c 100644 --- a/src/test/playwright/e2e/Login.spec.ts +++ b/src/test/playwright/e2e/Login.spec.ts @@ -38,7 +38,7 @@ test.describe('Login page tests', () => { const alertElement = await page.waitForSelector('.alert'); expect(await alertElement.isVisible()).toBeTruthy(); const alertText = await alertElement.textContent(); - expect(alertText).toContain('Failed to sign in! Please check your username and password and try again.'); + expect(alertText).toContain('Failed to sign in! Please check your login and password and try again.'); await page.click('#login-button'); await page.click('#login-button'); diff --git a/src/test/resources/config/application-artemis.yml b/src/test/resources/config/application-artemis.yml index 1bc086c96a92..9cac2ec6dc93 100644 --- a/src/test/resources/config/application-artemis.yml +++ b/src/test/resources/config/application-artemis.yml @@ -62,5 +62,15 @@ artemis: restricted-modules: module_text_test_restricted,module_programming_test_restricted apollon: conversion-service-url: http://localhost:8080 + telemetry: + enabled: true + sendAdminDetails: true + destination: http://localhost:8081 plagiarism-checks: plagiarism-results-limit: 100 + +info: + operatorName: Some Artemis Operator # Must be set before starting the application in production. Shown in the about us page, and sent to the telemetry service + operatorAdminName: Some Universities Admin # Can be set to be shown in the about us page, and to be sent to the telemetry service + contact: admin@uni.de # The admins contact email address, shown in the about us page, and sent to the telemetry service + diff --git a/src/test/resources/config/application.yml b/src/test/resources/config/application.yml index ba0acb609f26..f699af193497 100644 --- a/src/test/resources/config/application.yml +++ b/src/test/resources/config/application.yml @@ -251,6 +251,7 @@ info: # default value set to true for tests text-assessment-analytics-enabled: true student-exam-store-session-data: true + contact: contactEmail@contact.de jhipster: clientApp: