diff --git a/package-lock.json b/package-lock.json index 904218170b..4365aee052 100644 --- a/package-lock.json +++ b/package-lock.json @@ -18,16 +18,15 @@ "@vueuse/core": "^13.3.0", "@vueuse/integrations": "^13.3.0", "axios": "^1.12.2", - "canvas": "^3.1.0", "dayjs": "^1.11.10", "focus-trap": "^7.6.5", "katex": "^0.16.21", - "kjua": "^0.10.0", "lodash-es": "^4.17.21", "maska": "^2.1.10", "mobile-drag-drop": "^3.0.0-rc.0", "object-hash": "^3.0.0", "pinia": "^3.0.2", + "qrcode.vue": "^3.6.0", "socket.io-client": "^4.8.1", "sortablejs": "^1.15.6", "sortablejs-vue3": "^1.2.11", @@ -3782,7 +3781,7 @@ "version": "5.0.1", "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -3792,7 +3791,7 @@ "version": "4.3.0", "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-convert": "^2.0.1" @@ -3907,6 +3906,7 @@ "version": "1.5.1", "resolved": "https://registry.npmjs.org/base64-js/-/base64-js-1.5.1.tgz", "integrity": "sha512-AKpaYlHn8t4SVbOHCy+b5+KKgvR4vrsD8vbvrbiQJps7fKDTkjkDry6ji0rUJjC0kzbNePLwzxq8iypo41qeWA==", + "dev": true, "funding": [ { "type": "github", @@ -3946,6 +3946,7 @@ "version": "4.1.0", "resolved": "https://registry.npmjs.org/bl/-/bl-4.1.0.tgz", "integrity": "sha512-1W07cM9gS6DcLperZfFSj+bWLtaPGSOHWhPiGzXmvVJbRLdG82sH/Kn8EtW1VqWVA54AKf2h5k5BbnIbwF3h6w==", + "dev": true, "license": "MIT", "dependencies": { "buffer": "^5.5.0", @@ -4020,6 +4021,7 @@ "version": "5.7.1", "resolved": "https://registry.npmjs.org/buffer/-/buffer-5.7.1.tgz", "integrity": "sha512-EHcyIPBQ4BSGlvjB16k5KgAJ27CIsHY/2JBmCRReo48y9rQ3MaUzWX3KVlBa4U7MyX02HdVj0K7C3WaB3ju7FQ==", + "dev": true, "funding": [ { "type": "github", @@ -4096,6 +4098,17 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "5.3.1", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-5.3.1.tgz", + "integrity": "sha512-L28STB170nwWS63UjtlEOE3dldQApaJXZkOI1uMFfzf3rRuPegHaHesyee+YxQ+W6SvRDQV6UrdOdRiR153wJg==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/caniuse-lite": { "version": "1.0.30001727", "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001727.tgz", @@ -4121,8 +4134,11 @@ "version": "3.1.2", "resolved": "https://registry.npmjs.org/canvas/-/canvas-3.1.2.tgz", "integrity": "sha512-Z/tzFAcBzoCvJlOSlCnoekh1Gu8YMn0J51+UAuXJAbW1Z6I9l2mZgdD7738MepoeeIcUdDtbMnOg6cC7GJxy/g==", + "dev": true, "hasInstallScript": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "node-addon-api": "^7.0.0", "prebuild-install": "^7.1.3" @@ -4202,7 +4218,10 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/chownr/-/chownr-1.1.4.tgz", "integrity": "sha512-jJ0bqzaylmJtVnNgzTeSOs8DPavpbYgEr/b0YL8/2GO3xJEhInFmhKMUnEJQjZumK7KXGFhUy89PrsJWlakBVg==", - "license": "ISC" + "dev": true, + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/cli-cursor": { "version": "3.1.0", @@ -4287,7 +4306,7 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "color-name": "~1.1.4" @@ -4300,7 +4319,7 @@ "version": "1.1.4", "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/colorjs.io": { @@ -4585,6 +4604,17 @@ } } }, + "node_modules/decamelize": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-1.2.0.tgz", + "integrity": "sha512-z2S+W9X73hAUUki+N+9Za2lBlun89zigOyGrsax+KUQ6wKW4ZoWpEYBkGhQjwAjjDCkWxhY0VKEhk8wzY7F5cA==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/decimal.js": { "version": "10.6.0", "resolved": "https://registry.npmjs.org/decimal.js/-/decimal.js-10.6.0.tgz", @@ -4596,7 +4626,10 @@ "version": "6.0.0", "resolved": "https://registry.npmjs.org/decompress-response/-/decompress-response-6.0.0.tgz", "integrity": "sha512-aW35yZM6Bb/4oJlZncMH2LCoZtJXTRxES17vE3hoRiowU2kWHaJKFkSBDnDR+cm9J+9QhXmREyIfv0pji9ejCQ==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "mimic-response": "^3.1.0" }, @@ -4621,7 +4654,10 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/deep-extend/-/deep-extend-0.6.0.tgz", "integrity": "sha512-LOHxIOaPYdHlJRtCQfDIVZtfw/ufM8+rVj649RIHzcm/vGwQRXFt6OPqIFWsm2XEMrNIEtWR64sY1LEKD2vAOA==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=4.0.0" } @@ -4726,11 +4762,22 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-2.0.4.tgz", "integrity": "sha512-3UDv+G9CsCKO1WKMGw9fwq/SWJYbI0c5Y7LU1AXYoDdbhE2AHQ6N6Nb34sG8Fj7T5APy8qXDCKuuIHd1BR0tVA==", + "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "engines": { "node": ">=8" } }, + "node_modules/dijkstrajs": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/dijkstrajs/-/dijkstrajs-1.0.3.tgz", + "integrity": "sha512-qiSlmBq9+BCdCA/L46dw8Uy93mloxsPSbwnm5yrKn2vMPiy8KyAskTF6zuV/j5BMsmOGZDPs7KjU+mjb670kfA==", + "license": "MIT", + "optional": true, + "peer": true + }, "node_modules/dompurify": { "version": "3.2.6", "resolved": "https://registry.npmjs.org/dompurify/-/dompurify-3.2.6.tgz", @@ -4834,7 +4881,7 @@ "version": "8.0.0", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", - "dev": true, + "devOptional": true, "license": "MIT" }, "node_modules/encodeurl": { @@ -4851,7 +4898,10 @@ "version": "1.4.5", "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.5.tgz", "integrity": "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "once": "^1.4.0" } @@ -5460,7 +5510,10 @@ "version": "2.0.3", "resolved": "https://registry.npmjs.org/expand-template/-/expand-template-2.0.3.tgz", "integrity": "sha512-XYfuKMvj4O35f/pOXLObndIRvyQ+/+6AhODh+OKWj9S9498pHHn/IMszH+gt0fBCRWMNfk1ZSp5x3AifmnI2vg==", + "dev": true, "license": "(MIT OR WTFPL)", + "optional": true, + "peer": true, "engines": { "node": ">=6" } @@ -5796,7 +5849,10 @@ "version": "1.0.0", "resolved": "https://registry.npmjs.org/fs-constants/-/fs-constants-1.0.0.tgz", "integrity": "sha512-y6OAwoSIf7FyjMIv94u+b5rdheZEjzR63GTyZJm5qh4Bi+2YgwLCcI/fPFZkL5PSixOt6ZNKm+w+Hfp/Bciwow==", - "license": "MIT" + "dev": true, + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/fs-extra": { "version": "11.3.1", @@ -5851,7 +5907,7 @@ "version": "2.0.5", "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", - "dev": true, + "devOptional": true, "license": "ISC", "engines": { "node": "6.* || 8.* || >= 10.*" @@ -5930,7 +5986,10 @@ "version": "0.0.0", "resolved": "https://registry.npmjs.org/github-from-package/-/github-from-package-0.0.0.tgz", "integrity": "sha512-SyHy3T1v2NUXn29OsWdxmK6RwHD+vkj3v8en8AOBZ1wBQ/hCAQ5bAQTD02kW4W9tUp/3Qh6J8r9EvntiyCmOOw==", - "license": "MIT" + "dev": true, + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/glob": { "version": "11.0.3", @@ -6204,6 +6263,7 @@ "version": "1.2.1", "resolved": "https://registry.npmjs.org/ieee754/-/ieee754-1.2.1.tgz", "integrity": "sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==", + "dev": true, "funding": [ { "type": "github", @@ -6268,12 +6328,14 @@ "version": "2.0.4", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==", + "dev": true, "license": "ISC" }, "node_modules/ini": { "version": "1.3.8", "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.8.tgz", "integrity": "sha512-JV/yugV2uzW5iMRSiZAyDtQd+nxtUnjeLt0acNdw98kKLrvuRVyB80tsREOE7yvGVgalhZ6RNXCmEHkUKBKxew==", + "dev": true, "license": "ISC" }, "node_modules/inquirer": { @@ -6347,7 +6409,7 @@ "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==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -6872,12 +6934,6 @@ "json-buffer": "3.0.1" } }, - "node_modules/kjua": { - "version": "0.10.0", - "resolved": "https://registry.npmjs.org/kjua/-/kjua-0.10.0.tgz", - "integrity": "sha512-OEV1EYPyBGfQN6iieNf0hhQ5RoX0UaV4n1PLita5QkaUpPB+LidX2J2afITnO76bQIDkUA+RF3wFfEFRdtg4cA==", - "license": "MIT" - }, "node_modules/kolorist": { "version": "1.8.0", "resolved": "https://registry.npmjs.org/kolorist/-/kolorist-1.8.0.tgz", @@ -7126,7 +7182,10 @@ "version": "3.1.0", "resolved": "https://registry.npmjs.org/mimic-response/-/mimic-response-3.1.0.tgz", "integrity": "sha512-z0yWI+4FDrrweS8Zmt4Ej5HdJmky15+L2e6Wgn3+iK5fWzb6T3fhNFq2+MeTRb064c6Wr4N/wv0DzQTjNzHNGQ==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=10" }, @@ -7154,7 +7213,10 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.8.tgz", "integrity": "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "funding": { "url": "https://github.com/sponsors/ljharb" } @@ -7179,7 +7241,10 @@ "version": "0.5.3", "resolved": "https://registry.npmjs.org/mkdirp-classic/-/mkdirp-classic-0.5.3.tgz", "integrity": "sha512-gKLcREMhtuZRwRAfqP3RFW+TK4JqApVBtOIftVgjuABpAtpxhPGaDcfvbhNvD0B8iD1oUr/txX35NjcaY6Ns/A==", - "license": "MIT" + "dev": true, + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/mlly": { "version": "1.7.4", @@ -7259,7 +7324,10 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/napi-build-utils/-/napi-build-utils-2.0.0.tgz", "integrity": "sha512-GEbrYkbfF7MoNaoh2iGG84Mnf/WZfB0GdGEsM8wz7Expx/LlWf5U8t9nvJKXSp3qr5IsEbK04cBGhol/KwOsWA==", - "license": "MIT" + "dev": true, + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/natural-compare": { "version": "1.4.0", @@ -7282,7 +7350,10 @@ "version": "3.75.0", "resolved": "https://registry.npmjs.org/node-abi/-/node-abi-3.75.0.tgz", "integrity": "sha512-OhYaY5sDsIka7H7AtijtI9jwGYLyl29eQn/W623DiN/MIv5sUqc4g7BIDThX+gb7di9f6xK02nkp8sdfFWZLTg==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "semver": "^7.3.5" }, @@ -7294,7 +7365,10 @@ "version": "7.1.1", "resolved": "https://registry.npmjs.org/node-addon-api/-/node-addon-api-7.1.1.tgz", "integrity": "sha512-5m3bsyrjFWE1xf7nz7YXdN4udnVtXK6/Yfgn5qnahL6bCkf2yKt4k3nuTKAtT4r3IG8JNR2ncsIMdZuAzJjHQQ==", - "license": "MIT" + "dev": true, + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/node-fetch": { "version": "2.7.0", @@ -7524,7 +7598,10 @@ "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, "license": "ISC", + "optional": true, + "peer": true, "dependencies": { "wrappy": "1" } @@ -7638,6 +7715,17 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=6" + } + }, "node_modules/pac-proxy-agent": { "version": "7.2.0", "resolved": "https://registry.npmjs.org/pac-proxy-agent/-/pac-proxy-agent-7.2.0.tgz", @@ -7752,7 +7840,7 @@ "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=8" @@ -7900,6 +7988,17 @@ "dev": true, "license": "MIT" }, + "node_modules/pngjs": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/pngjs/-/pngjs-5.0.0.tgz", + "integrity": "sha512-40QW5YalBNfQo5yRYmiw7Yz6TKKVr3h6970B2YE+3fQpsWcrbj1PzJgxeJ19DRQjhMbKPIuMY8rFaXc8moolVw==", + "license": "MIT", + "optional": true, + "peer": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/postcss": { "version": "8.5.6", "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.6.tgz", @@ -7946,7 +8045,10 @@ "version": "7.1.3", "resolved": "https://registry.npmjs.org/prebuild-install/-/prebuild-install-7.1.3.tgz", "integrity": "sha512-8Mf2cbV7x1cXPUILADGI3wuhfqWvtiLA1iclTDbFRZkgRQS0NqsPZphna9V+HyTEadheuPmjaJMsbzKQFOzLug==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "detect-libc": "^2.0.0", "expand-template": "^2.0.3", @@ -8070,7 +8172,10 @@ "version": "3.0.3", "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.3.tgz", "integrity": "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" @@ -8086,6 +8191,154 @@ "node": ">=6" } }, + "node_modules/qrcode": { + "version": "1.5.4", + "resolved": "https://registry.npmjs.org/qrcode/-/qrcode-1.5.4.tgz", + "integrity": "sha512-1ca71Zgiu6ORjHqFBDpnSMTR2ReToX4l1Au1VFLyVeBTFavzQnv5JxMFr3ukHVKpSrSA2MCk0lNJSykjUfz7Zg==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "dijkstrajs": "^1.0.1", + "pngjs": "^5.0.0", + "yargs": "^15.3.1" + }, + "bin": { + "qrcode": "bin/qrcode" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/qrcode.vue": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/qrcode.vue/-/qrcode.vue-3.6.0.tgz", + "integrity": "sha512-vQcl2fyHYHMjDO1GguCldJxepq2izQjBkDEEu9NENgfVKP6mv/e2SU62WbqYHGwTgWXLhxZ1NCD1dAZKHQq1fg==", + "license": "MIT", + "peerDependencies": { + "vue": "^3.0.0" + } + }, + "node_modules/qrcode/node_modules/cliui": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-6.0.0.tgz", + "integrity": "sha512-t6wbgtoCXvAzst7QgXxJYqPt0usEfbgQdftEPbLL/cvv6HPE5VgvqCuAIDR0NgU52ds6rFwqrgakNLrHEjCbrQ==", + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^6.2.0" + } + }, + "node_modules/qrcode/node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/qrcode/node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/y18n": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", + "integrity": "sha512-JKhqTOwSrqNA1NY5lSztJ1GrBiUodLMmIZuLiDaMRJ+itFd+ABVE8XBjOvIWL+rSqNDC74LCSFmlb/U4UZ4hJQ==", + "license": "ISC", + "optional": true, + "peer": true + }, + "node_modules/qrcode/node_modules/yargs": { + "version": "15.4.1", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-15.4.1.tgz", + "integrity": "sha512-aePbxDmcYW++PaqBsJ+HYUFwCdv4LVvdnhBy78E57PIor8/OVvhMrADFFEDh8DHDFRv/O9i3lPhsENjO7QX0+A==", + "license": "MIT", + "optional": true, + "peer": true, + "dependencies": { + "cliui": "^6.0.0", + "decamelize": "^1.2.0", + "find-up": "^4.1.0", + "get-caller-file": "^2.0.1", + "require-directory": "^2.1.1", + "require-main-filename": "^2.0.0", + "set-blocking": "^2.0.0", + "string-width": "^4.2.0", + "which-module": "^2.0.0", + "y18n": "^4.0.0", + "yargs-parser": "^18.1.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/qrcode/node_modules/yargs-parser": { + "version": "18.1.3", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-18.1.3.tgz", + "integrity": "sha512-o50j0JeToy/4K6OZcaQmW6lyXXKhq7csREXcDwk2omFPJEwUNOVtJKvmDr9EI1fAJZUyZcRF7kxGBWmRXudrCQ==", + "license": "ISC", + "optional": true, + "peer": true, + "dependencies": { + "camelcase": "^5.0.0", + "decamelize": "^1.2.0" + }, + "engines": { + "node": ">=6" + } + }, "node_modules/queue-microtask": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/queue-microtask/-/queue-microtask-1.2.3.tgz", @@ -8111,7 +8364,10 @@ "version": "1.2.8", "resolved": "https://registry.npmjs.org/rc/-/rc-1.2.8.tgz", "integrity": "sha512-y3bGgqKj3QBdxLbLkomlohkvsA8gdAiUQlSBJnBhfn+BPxg4bc62d8TcBW15wavDfgexCgccckhcZvywyQYPOw==", + "dev": true, "license": "(BSD-2-Clause OR MIT OR Apache-2.0)", + "optional": true, + "peer": true, "dependencies": { "deep-extend": "^0.6.0", "ini": "~1.3.0", @@ -8126,7 +8382,10 @@ "version": "2.0.1", "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-2.0.1.tgz", "integrity": "sha512-4gB8na07fecVVkOI6Rs4e7T6NOTki5EmL7TUduTs6bu3EdnSycntVJ4re8kgZA+wx9IueI2Y11bfbgwtzuE0KQ==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "engines": { "node": ">=0.10.0" } @@ -8149,6 +8408,7 @@ "version": "3.6.2", "resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-3.6.2.tgz", "integrity": "sha512-9u/sniCrY3D5WdsERHzHE4G2YCXqoG5FTHUiCC4SIbr6XcLZBY05ya9EKjYek9O5xOAwjGq+1JdGBAS7Q9ScoA==", + "dev": true, "license": "MIT", "dependencies": { "inherits": "^2.0.3", @@ -8184,12 +8444,20 @@ "version": "2.1.1", "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", - "dev": true, + "devOptional": true, "license": "MIT", "engines": { "node": ">=0.10.0" } }, + "node_modules/require-main-filename": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/require-main-filename/-/require-main-filename-2.0.0.tgz", + "integrity": "sha512-NKN5kMDylKuldxYLSUfrbo5Tuzh4hd+2E8NPPX02mZtn1VuREQToYe/ZdlJy+J3uCpfaiGF05e7B8W0iXbQHmg==", + "license": "ISC", + "optional": true, + "peer": true + }, "node_modules/requires-port": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/requires-port/-/requires-port-1.0.0.tgz", @@ -8360,6 +8628,7 @@ "version": "5.2.1", "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "dev": true, "funding": [ { "type": "github", @@ -8729,6 +8998,7 @@ "version": "7.7.2", "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.2.tgz", "integrity": "sha512-RF0Fw+rO5AMf9MAyaRXI4AV0Ulj5lMHqVxxdSgiVbixSCXoEmmX/jk0CuJw4+3SqroYO9VoUh+HcuJivvtJemA==", + "dev": true, "license": "ISC", "bin": { "semver": "bin/semver.js" @@ -8737,6 +9007,14 @@ "node": ">=10" } }, + "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, + "peer": true + }, "node_modules/shebang-command": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", @@ -8797,6 +9075,7 @@ "version": "1.0.1", "resolved": "https://registry.npmjs.org/simple-concat/-/simple-concat-1.0.1.tgz", "integrity": "sha512-cSFtAPtRhljv69IK0hTVZQ+OfE9nePi/rtJmw5UjHeVyVroEqJXP1sFztKUy1qU+xvz3u/sfYJLa947b7nAN2Q==", + "dev": true, "funding": [ { "type": "github", @@ -8811,12 +9090,15 @@ "url": "https://feross.org/support" } ], - "license": "MIT" + "license": "MIT", + "optional": true, + "peer": true }, "node_modules/simple-get": { "version": "4.0.1", "resolved": "https://registry.npmjs.org/simple-get/-/simple-get-4.0.1.tgz", "integrity": "sha512-brv7p5WgH0jmQJr1ZDDfKDOSeWWg+OVypG99A/5vYGPqJ6pxiaHLy8nxtFjBA7oMa01ebA9gfh1uMCFqOuXxvA==", + "dev": true, "funding": [ { "type": "github", @@ -8832,6 +9114,8 @@ } ], "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "decompress-response": "^6.0.0", "once": "^1.3.1", @@ -9044,6 +9328,7 @@ "version": "1.3.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-1.3.0.tgz", "integrity": "sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==", + "dev": true, "license": "MIT", "dependencies": { "safe-buffer": "~5.2.0" @@ -9053,7 +9338,7 @@ "version": "4.2.3", "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "emoji-regex": "^8.0.0", @@ -9084,7 +9369,7 @@ "version": "6.0.1", "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-regex": "^5.0.1" @@ -9244,7 +9529,10 @@ "version": "2.1.4", "resolved": "https://registry.npmjs.org/tar-fs/-/tar-fs-2.1.4.tgz", "integrity": "sha512-mDAjwmZdh7LTT6pNleZ05Yt65HC3E+NiQzl672vQG38jIrehtJk/J3mNwIg+vShQPcLF/LV7CMnDW6vjj6sfYQ==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "chownr": "^1.1.1", "mkdirp-classic": "^0.5.2", @@ -9256,7 +9544,10 @@ "version": "2.2.0", "resolved": "https://registry.npmjs.org/tar-stream/-/tar-stream-2.2.0.tgz", "integrity": "sha512-ujeqbceABgwMZxEJnk2HDY2DlnUZ+9oEcb1KzTVfYHio0UE6dG71n60d8D2I4qNvleWrrXpmjpt7vZeF1LnMZQ==", + "dev": true, "license": "MIT", + "optional": true, + "peer": true, "dependencies": { "bl": "^4.0.3", "end-of-stream": "^1.4.1", @@ -9554,7 +9845,10 @@ "version": "0.6.0", "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.6.0.tgz", "integrity": "sha512-McnNiV1l8RYeY8tBgEpuodCC1mLUdbSN+CYBL7kJsJNInOP8UjDDEwdk6Mw60vdLLrr5NHKZhMAOSrR2NZuQ+w==", + "dev": true, "license": "Apache-2.0", + "optional": true, + "peer": true, "dependencies": { "safe-buffer": "^5.0.1" }, @@ -9793,6 +10087,7 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, "license": "MIT" }, "node_modules/utils-merge": { @@ -10620,6 +10915,14 @@ "node": ">= 8" } }, + "node_modules/which-module": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/which-module/-/which-module-2.0.1.tgz", + "integrity": "sha512-iBdZ57RDvnOR9AGBhML2vFZf7h8vmBjhoaZqODJBFWHVtKkDmKuHai3cx5PgVMrX5YDNp27AofYbAwctSS+vhQ==", + "license": "ISC", + "optional": true, + "peer": true + }, "node_modules/why-is-node-running": { "version": "2.3.0", "resolved": "https://registry.npmjs.org/why-is-node-running/-/why-is-node-running-2.3.0.tgz", @@ -10651,7 +10954,7 @@ "version": "6.2.0", "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-6.2.0.tgz", "integrity": "sha512-r6lPcBGxZXlIcymEu7InxDMhdW0KDxpLgoFLcguasxCaJ/SOIZwINatK9KY/tf+ZrlywOKU0UDj3ATXUBfxJXA==", - "dev": true, + "devOptional": true, "license": "MIT", "dependencies": { "ansi-styles": "^4.0.0", @@ -10685,7 +10988,10 @@ "version": "1.0.2", "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", - "license": "ISC" + "dev": true, + "license": "ISC", + "optional": true, + "peer": true }, "node_modules/ws": { "version": "8.18.3", diff --git a/package.json b/package.json index 500e9156ff..ab6fce6aa5 100644 --- a/package.json +++ b/package.json @@ -33,16 +33,15 @@ "@vueuse/core": "^13.3.0", "@vueuse/integrations": "^13.3.0", "axios": "^1.12.2", - "canvas": "^3.1.0", "dayjs": "^1.11.10", "focus-trap": "^7.6.5", "katex": "^0.16.21", - "kjua": "^0.10.0", "lodash-es": "^4.17.21", "maska": "^2.1.10", "mobile-drag-drop": "^3.0.0-rc.0", "object-hash": "^3.0.0", "pinia": "^3.0.2", + "qrcode.vue": "^3.6.0", "socket.io-client": "^4.8.1", "sortablejs": "^1.15.6", "sortablejs-vue3": "^1.2.11", @@ -98,8 +97,7 @@ "overrides": { "@openapitools/openapi-generator-cli": { "axios": "$axios" - }, - "canvas": "^3.1.0" + } }, "engines": { "node": "22", diff --git a/src/locales/de.ts b/src/locales/de.ts index 152aee4e1e..158bb4a8df 100644 --- a/src/locales/de.ts +++ b/src/locales/de.ts @@ -1146,6 +1146,8 @@ export default { "Die Migration kann nicht gestartet werden, da das Ziel-Login-System und das derzeitige Login-System Ihrer Schule identisch sind!", "pages.administration.migration.moin_schule_system_not_found": "Das moin.schule-System kann nicht gefunden werden!", "pages.administration.or": "oder", + "pages.administration.printQr.printPageTabTitle": "QR-Codes teilen", + "pages.administration.printQr.printPageTitle": "Zum Registrieren bitte den QR Code scannen.", "pages.administration.printQr.emptyUser": "Dieser Nutzer wurde bereits registriert", "pages.administration.printQr.error": "Die Registrierungslinks konnten auf Grund eines Problems nicht generiert werden", diff --git a/src/locales/en.ts b/src/locales/en.ts index 29ac63af1f..7ce803b575 100644 --- a/src/locales/en.ts +++ b/src/locales/en.ts @@ -1136,6 +1136,8 @@ export default { "The migration cannot start because the target login system and your school's current login system are the same!", "pages.administration.migration.moin_schule_system_not_found": "Cannot find moin.schule system!", "pages.administration.or": "or", + "pages.administration.printQr.printPageTabTitle": "Share QR-Codes", + "pages.administration.printQr.printPageTitle": "Please scan the QR code to register.", "pages.administration.printQr.emptyUser": "The selected user(s) have already been registered", "pages.administration.printQr.error": "The registration links could not be generated", "pages.administration.remove.error": "Failed to delete users", diff --git a/src/locales/es.ts b/src/locales/es.ts index 557deab4cb..4cf196bdf5 100644 --- a/src/locales/es.ts +++ b/src/locales/es.ts @@ -1156,6 +1156,8 @@ export default { "¡La migración no puede comenzar porque el sistema de inicio de sesión de destino y el sistema de inicio de sesión actual de su escuela son el mismo!", "pages.administration.migration.moin_schule_system_not_found": "¡No puedo encontrar el sistema moin.schule!", "pages.administration.or": "o", + "pages.administration.printQr.printPageTabTitle": "Compartir códigos QR", + "pages.administration.printQr.printPageTitle": "Por favor, escanea el código QR para registrarte.", "pages.administration.printQr.emptyUser": "L{'@'}s usuari{'@'}s seleccionad{'@'}s ya han sido registrad{'@'}s", "pages.administration.printQr.error": "No se han podido generar los enlaces de registro", "pages.administration.remove.error": "Error al eliminar usuarios", diff --git a/src/locales/uk.ts b/src/locales/uk.ts index 939336276a..fc228994db 100644 --- a/src/locales/uk.ts +++ b/src/locales/uk.ts @@ -1144,6 +1144,8 @@ export default { "Неможливо розпочати міграцію, оскільки цільова система входу та поточна система входу у вашій школі збігаються!", "pages.administration.migration.moin_schule_system_not_found": "Не вдається знайти систему moin.schule!", "pages.administration.or": "або", + "pages.administration.printQr.printPageTabTitle": "Поділитися QR-кодами", + "pages.administration.printQr.printPageTitle": "Будь ласка, відскануйте QR-код для реєстрації.", "pages.administration.printQr.emptyUser": "Вибраний користувач(-і) вже зареєстрований(-і)", "pages.administration.printQr.error": "Не вдалося згенерувати посилання для реєстрації", "pages.administration.remove.error": "Не вдалося видалити користувачів", diff --git a/src/mixins/print.js b/src/mixins/print.js deleted file mode 100644 index 7f71595739..0000000000 --- a/src/mixins/print.js +++ /dev/null @@ -1,86 +0,0 @@ -import kjua from "kjua"; - -const printStyles = (optionalStyles) => ``; - -const getPageHtml = (head, body) => `${head}${body}`; - -const getQRCodeBase64Image = (text) => kjua({ text: text, render: "canvas" }).toDataURL(); - -const print = (content, styles = "") => { - const w = window.open(); - w.document.write(getPageHtml(printStyles(styles), content)); - w.document.close(); - /* eventListener is needed to give the browser some rendering time for the image */ - w.addEventListener("load", () => { - w.focus(); - w.print(); - // why is this timeout required? - setTimeout(() => { - w.close(); - }, 500); - }); -}; - -const printQRs = (items = []) => { - const styles = ` - .part{ - border: 1px solid #aaa; - width: 4cm; - float: left; - padding: 8px; - margin: 4px; - } - .qr-code{ width: 100% !important; height: auto !important; } - .qr-content{ - font-size: 0.7em; - opacity: 0.7; - width: 100%; - word-break: - break-all; - word-break: break-word; - margin: 2px 0 0; - } - .title{ margin: 4px 0; font-size: 1.25em; font-weight: bold; } - .description{ - font-size: 1em; - color: #555; - margin: 0; - word-break: break-all; - word-break: break-word; - }`; - let content = ""; - if (items.length === 0) { - content = "Keine Einträge zu drucken."; // TODO: develop concept to use language files here - } else { - content = items - .map((item) => { - const QRCodeBase64Image = getQRCodeBase64Image(item.qrContent); - return `
-
- ${item.qrContent} -
${item.qrContent}
-
- ${item.title ? `

${item.title}

` : ""} - ${item.description ? `

${item.description}

` : ""} -
`; - }) - .join("\n"); - } - return print(content, styles); -}; - -export default { - methods: { - $_print: print, - $_printQRs: printQRs, - }, -}; diff --git a/src/mixins/print.unit.js b/src/mixins/print.unit.js deleted file mode 100644 index 892178acaa..0000000000 --- a/src/mixins/print.unit.js +++ /dev/null @@ -1,102 +0,0 @@ -import print from "./print"; - -const getNewWindowMock = () => { - let content = ""; - return { - document: { - get innerHTML() { - return content; - }, - write: vi.fn().mockImplementation((...args) => { - args.forEach((text) => (content += text)); - }), - close: vi.fn(), - }, - addEventListener: vi.fn().mockImplementation((event, cb) => cb()), - focus: vi.fn(), - print: vi.fn(), - close: vi.fn(), - }; -}; - -let newWindowMock; - -describe("@/mixins/print", () => { - beforeEach(() => { - newWindowMock = getNewWindowMock(); - vi.spyOn(window, "open").mockImplementation().mockReturnValue(newWindowMock); - }); - - describe("$_print", () => { - const method = print.methods.$_print; - - it("can print plain content", () => { - const testContent = "some plain old content"; - method(testContent); - expect(newWindowMock.document.innerHTML).toContain(testContent); - }); - - it("can print content with custom styles", () => { - const testContent = "some plain old content"; - const testStyles = "body { margin: 2rem; border: 1px solid blue; }"; - method(testContent, testStyles); - expect(newWindowMock.document.innerHTML).toContain(testContent); - }); - - it("focuses new window before printing", () => { - const testContent = "some plain old content"; - method(testContent); - - expect(newWindowMock.document.write).toHaveBeenCalledWith(expect.stringContaining(testContent)); - expect(newWindowMock.focus).toHaveBeenCalled(); - expect(newWindowMock.print).toHaveBeenCalled(); - }); - - it("closes new window after print", () => { - const testContent = "some plain old content"; - vi.useFakeTimers(); - method(testContent); - vi.runAllTimers(); - expect(newWindowMock.print).toHaveBeenCalled(); - expect(newWindowMock.close).toHaveBeenCalled(); - }); - }); - - describe("$_printQRs", () => { - const method = print.methods.$_printQRs; - - it("can print items with all options", () => { - const testContent = [ - { - qrContent: "qrContent", - title: "title", - description: "description", - }, - ]; - method(testContent); - expect(newWindowMock.document.innerHTML).toContain(testContent[0].qrContent); - expect(newWindowMock.document.innerHTML).toContain(testContent[0].title); - expect(newWindowMock.document.innerHTML).toContain(testContent[0].description); - }); - - it("can print items with only QR content", () => { - const testContent = [ - { - qrContent: "qrContent", - }, - ]; - method(testContent); - expect(newWindowMock.document.innerHTML).toContain(testContent[0].qrContent); - }); - - it("prints an error if no items to print are given", () => { - method([]); - expect(newWindowMock.document.innerHTML).toContain("Keine Einträge zu drucken."); - }); - - it("can handle no parameters", () => { - method(); - expect(newWindowMock.document.innerHTML).toContain("Keine Einträge zu drucken."); - }); - }); -}); diff --git a/src/modules/ui/layout/topbar/PageShare.unit.ts b/src/modules/ui/layout/topbar/PageShare.unit.ts index 318fab319f..98711a0f06 100644 --- a/src/modules/ui/layout/topbar/PageShare.unit.ts +++ b/src/modules/ui/layout/topbar/PageShare.unit.ts @@ -1,54 +1,32 @@ import PageShare from "./PageShare.vue"; import { createTestingI18n, createTestingVuetify } from "@@/tests/test-utils/setup"; -import { createMock } from "@golevelup/ts-vitest"; import { mount } from "@vue/test-utils"; +import { beforeAll, vi } from "vitest"; describe("@ui-layout/PageShare", () => { - const setup = (attrs = {}) => { - const windowMock = createMock({ - document: { - write: vi.fn().mockImplementation(() => ""), - }, - print: vi.fn(), - close: vi.fn(), - }); - - Object.defineProperty(window, "open", { - configurable: true, - value: vi.fn().mockReturnValue(windowMock), + beforeAll(() => { + Object.defineProperty(window, "location", { + value: { href: "url" }, + writable: true, }); + }); + const setup = () => { const wrapper = mount(PageShare, { global: { plugins: [createTestingVuetify(), createTestingI18n()], }, - props: { - url: "url", - }, - ...attrs, }); - return { wrapper, windowMock }; + return { wrapper }; }; describe("with available languages", () => { it("should render qr code ", () => { const { wrapper } = setup(); - expect(wrapper.findComponent({ name: "QRCode" }).exists()).toBe(true); }); - it("should open print menu with QR-Code", async () => { - const { wrapper, windowMock } = setup(); - const printButton = wrapper.findComponent("[data-testid=qr-code-print]"); - await printButton.trigger("click"); - - expect(window.open).toHaveBeenCalled(); - expect(windowMock.document.write).toHaveBeenCalled(); - expect(windowMock.print).toHaveBeenCalled(); - expect(windowMock.close).toHaveBeenCalled(); - }); - it("should copy link to clipboard", async () => { Object.defineProperty(navigator, "clipboard", { value: { diff --git a/src/modules/ui/layout/topbar/PageShare.vue b/src/modules/ui/layout/topbar/PageShare.vue index f83e507626..cad120df8e 100644 --- a/src/modules/ui/layout/topbar/PageShare.vue +++ b/src/modules/ui/layout/topbar/PageShare.vue @@ -3,7 +3,8 @@
{{ $t("global.topbar.MenuQrCode.qrHintText") }}
- + +
import { mdiContentCopy, mdiPrinter } from "@icons/material"; import { QRCode } from "@ui-qr-code"; -import { ComponentPublicInstance, ref } from "vue"; - -const props = defineProps({ - url: { - type: String, - default: window.location.href, - }, -}); -const qrCode = ref>(); +import { printQrCodes } from "@util-browser"; +const url = window.location.href; const openPrintMenu = () => { - const win = window.open(); - - if (qrCode.value) { - win?.document.write(qrCode.value.$el.innerHTML); - win?.print(); - win?.close(); - } + printQrCodes([{ qrContent: url, title: document.title }]); }; -const onCopy = () => navigator.clipboard.writeText(props.url); +const onCopy = () => navigator.clipboard.writeText(url); + + + +`; + const qrCodeList = document.createElement("ul"); + qrCodeList.classList.add("qrcode-list"); + printWindow.document.body.appendChild(qrCodeList); + + qrCodeItems.forEach(({ qrContent, title }) => { + const qrCodeListItem = document.createElement("li"); + qrCodeListItem.className = "qr-item"; + + const qrCodeWrapper = document.createElement("div"); + qrCodeWrapper.className = "qr-code"; + const vnode = h(QrcodeVue, { + value: qrContent, + size: 200, + level: "H", + renderAs: "svg", + }); + render(vnode, qrCodeWrapper); + qrCodeListItem.appendChild(qrCodeWrapper); + + qrCodeListItem.innerHTML += ` + ${title ? `
${title}
` : ""} + `; + + qrCodeList.appendChild(qrCodeListItem); + }); + printWindow.document.body.appendChild(qrCodeList); + printWindow.print(); + printWindow.close(); + } else { + logger.warn("Could not open print window for QR codes."); + } +}; diff --git a/src/modules/util/browser/qr-code.utils.unit.ts b/src/modules/util/browser/qr-code.utils.unit.ts new file mode 100644 index 0000000000..37e8b70408 --- /dev/null +++ b/src/modules/util/browser/qr-code.utils.unit.ts @@ -0,0 +1,59 @@ +import { printQrCodes } from "./qr-code.utils"; +import { createMock } from "@golevelup/ts-vitest"; +import { createTestingPinia } from "@pinia/testing"; +import { setActivePinia } from "pinia"; +import { afterEach, beforeEach, describe, expect, it, vi } from "vitest"; + +describe("printQrCodes", () => { + const mockWindow = createMock({ + document: { + documentElement: { innerHTML: "" }, + body: { appendChild: vi.fn() }, + createElement: vi.fn(() => ({ + appendChild: vi.fn(), + })), + }, + print: vi.fn(), + close: vi.fn(), + }); + + beforeEach(() => { + setActivePinia(createTestingPinia()); + vi.spyOn(window, "open").mockReturnValue(mockWindow); + }); + + afterEach(() => { + vi.restoreAllMocks(); + }); + + it("should open a new print window", () => { + printQrCodes([{ qrContent: "https://example.com" }]); + + expect(window.open).toHaveBeenCalledWith("", "_blank"); + }); + + it("should add QR code items to the document", () => { + printQrCodes([{ qrContent: "https://example.com" }, { qrContent: "https://test.com" }]); + + expect(mockWindow.document.body.appendChild).toHaveBeenCalledTimes(2); + }); + + it("should trigger print and close the window", () => { + printQrCodes([{ qrContent: "https://example.com" }]); + + expect(mockWindow.print).toHaveBeenCalled(); + expect(mockWindow.close).toHaveBeenCalled(); + }); + + it("should handle empty array gracefully", () => { + printQrCodes([]); + + expect(mockWindow.print).toHaveBeenCalled(); + }); + + it("should not throw when window.open fails", () => { + vi.spyOn(window, "open").mockReturnValue(null); + + expect(() => printQrCodes([{ qrContent: "https://example.com" }])).not.toThrow(); + }); +}); diff --git a/src/pages/administration/StudentOverview.page.vue b/src/pages/administration/StudentOverview.page.vue index 6743ecbc7d..a322b38718 100644 --- a/src/pages/administration/StudentOverview.page.vue +++ b/src/pages/administration/StudentOverview.page.vue @@ -109,7 +109,6 @@ import ProgressModal from "@/components/molecules/ProgressModal"; import DataFilter from "@/components/organisms/DataFilter/DataFilter.vue"; import BackendDataTable from "@/components/organisms/DataTable/BackendDataTable"; import DefaultWireframe from "@/components/templates/DefaultWireframe.vue"; -import print from "@/mixins/print"; import UserHasPermission from "@/mixins/UserHasPermission"; import { printDate } from "@/plugins/datetime"; import { Permission } from "@/serverApi/v3"; @@ -131,6 +130,7 @@ import { mdiPlus, mdiQrcode, } from "@icons/material"; +import { printQrCodes } from "@util-browser"; import { reactive } from "vue"; import { mapGetters } from "vuex"; @@ -142,7 +142,7 @@ export default { ProgressModal, DataFilter, }, - mixins: [print, UserHasPermission], + mixins: [UserHasPermission], props: { showExternalSyncHint: { type: Boolean, @@ -496,7 +496,7 @@ export default { roleName: "student", }); if (this.qrLinks.length) { - this.$_printQRs(this.qrLinks); + printQrCodes(this.qrLinks, { printPageTitleKey: "pages.administration.printQr.printPageTitle" }); } else { notifyInfo(this.$t("pages.administration.printQr.emptyUser")); } diff --git a/src/pages/administration/TeacherOverview.page.vue b/src/pages/administration/TeacherOverview.page.vue index e39ab22e76..a0030c7acd 100644 --- a/src/pages/administration/TeacherOverview.page.vue +++ b/src/pages/administration/TeacherOverview.page.vue @@ -100,7 +100,6 @@ import ProgressModal from "@/components/molecules/ProgressModal"; import DataFilter from "@/components/organisms/DataFilter/DataFilter.vue"; import BackendDataTable from "@/components/organisms/DataTable/BackendDataTable"; import DefaultWireframe from "@/components/templates/DefaultWireframe.vue"; -import print from "@/mixins/print"; import UserHasPermission from "@/mixins/UserHasPermission"; import { printDate } from "@/plugins/datetime"; import { Permission, RoleName } from "@/serverApi/v3"; @@ -121,6 +120,7 @@ import { mdiPlus, mdiQrcode, } from "@icons/material"; +import { printQrCodes } from "@util-browser"; import { reactive } from "vue"; import { mapGetters } from "vuex"; @@ -132,7 +132,7 @@ export default { ProgressModal, DataFilter, }, - mixins: [print, UserHasPermission], + mixins: [UserHasPermission], props: { showExternalSyncHint: { type: Boolean, @@ -451,7 +451,7 @@ export default { roleName: "teacher", }); if (this.qrLinks.length) { - this.$_printQRs(this.qrLinks); + printQrCodes(this.qrLinks, { printPageTitleKey: "pages.administration.printQr.printPageTitle" }); } else { notifyInfo(this.$t("pages.administration.printQr.emptyUser")); } diff --git a/src/plugins/i18n.ts b/src/plugins/i18n.ts index 95813fe539..1ad73ff74e 100644 --- a/src/plugins/i18n.ts +++ b/src/plugins/i18n.ts @@ -57,4 +57,14 @@ const localCreateI18n = () => { return i18n; }; -export { localCreateI18n as createI18n }; +let i18nInstance: ReturnType; + +const createTypedI18nInstance = () => { + if (!i18nInstance) { + i18nInstance = localCreateI18n(); + } + return i18nInstance; +}; +export { createTypedI18nInstance as createI18n }; + +export const useI18nGlobal = () => createTypedI18nInstance()?.global; diff --git a/tsconfig.json b/tsconfig.json index 674179d47c..9cbadbe6fc 100644 --- a/tsconfig.json +++ b/tsconfig.json @@ -97,6 +97,7 @@ "@util-board": ["src/modules/util/board"], "@util-validators": ["src/modules/util/validators"], "@util-vue": ["src/modules/util/vue"], + "@util-browser": ["src/modules/util/browser"], "@util-input-masks": ["src/modules/util/input-masks"], "@util-device-detection": ["src/modules/util/device-detection"], "@util-error-notification": ["src/modules/util/error-notification"],