Repository: hpi-schul-cloud/nuxt-client
Pull request #3602: BC-8009 - eslint deep imports
Author: login: hoeppner-dataport
Created: 2025-03-13T14:51:47Z | Merged: 2025-03-18T15:56:31Z

# Short Description
Fix ESlint rules:
- @typescript-eslint/no-restricted-imports
- incorrect undef lint-warnings for Clipboard and NodeJS
- fix deep-imports

## Links to Ticket and related Pull-Requests
BC-8009

## Changes

<!--
- What will the PR change?
- Why are the changes required?
- Links to documentation / tickets if exists, or provide more details here.
-->

## Data-security

<!--
Please note here about:
- any data model changes
- any changes about logging of user data
- any changes about permissions
- user input, authentication and other user data related things
If you are not sure if it is relevant, take a look at confluence or ask the data-security team.
-->

## Checklist before merging

- [x] QA: In addition to review, the code has been manually tested (if manual testing is possible)
- [x] PO: Any deviation from requirements was agreed with Product-Owner / ticket author / support-team
- [x] DEV: Every new component is implemented having accessibility in mind (e.g. aria-label, role property)

> Notice: Please keep this Pull-Request as a Draft (or add WIP label), until it is ready to be reviewed



Diff:
diff --git a/__mocks__/maska.ts b/__mocks__/maska.ts
index ad0d93c8795..6ce7ef773fb 100644
--- a/__mocks__/maska.ts
+++ b/__mocks__/maska.ts
@@ -1,8 +1,6 @@
 /**
  * This is a mock for the vMaska directive exported by maska package.
  */
-import { Jest as jest } from "@jest/environment";
-
 const vMaska = jest.fn();
 
 export { vMaska };
diff --git a/eslint.config.js b/eslint.config.js
index 133b5c3539c..cc9e5034034 100644
--- a/eslint.config.js
+++ b/eslint.config.js
@@ -44,6 +44,8 @@ module.exports = defineConfigWithVueTs([
 			ecmaVersion: "latest",
 			globals: {
 				...globals.node,
+				...globals.browser,
+				NodeJS: true,
 			},
 		},
 		plugins: {
@@ -58,9 +60,9 @@ module.exports = defineConfigWithVueTs([
 			],
 			"@typescript-eslint/no-explicit-any": "error",
 			"@typescript-eslint/no-inferrable-types": "error",
-			"@typescript-eslint/no-require-imports": "warn",
+			"@typescript-eslint/no-require-imports": "off",
 			"@typescript-eslint/no-restricted-imports": [
-				"warn",
+				"error",
 				{
 					patterns: [
 						{
@@ -135,7 +137,12 @@ module.exports = defineConfigWithVueTs([
 		},
 	},
 	{
-		files: ["**/*.unit.{j,t}s?(x)", "tests/**"],
+		files: [
+			"**/*.unit.{j,t}s?(x)",
+			"tests/**",
+			"**/test-utils/**",
+			"__mocks__/**",
+		],
 		languageOptions: {
 			globals: {
 				...globals.jest,
@@ -151,4 +158,10 @@ module.exports = defineConfigWithVueTs([
 			"schulcloud/material-icon-imports": "off",
 		},
 	},
+	{
+		files: ["**/*.{ts,vue}"],
+		rules: {
+			"@typescript-eslint/no-require-imports": "error",
+		},
+	},
 ]);
diff --git a/src/assets/img/index.d.ts b/src/assets/img/index.d.ts
index 235bb375be7..3d613ac182e 100644
--- a/src/assets/img/index.d.ts
+++ b/src/assets/img/index.d.ts
@@ -20,3 +20,16 @@ declare module "@/assets/img/logo/logo-image-mono.svg" {
 	const value: string;
 	export default value;
 }
+
+declare module "@/assets/img/bbb/no_permission.png" {
+	const value: string;
+	export default value;
+}
+declare module "@/assets/img/bbb/available.png" {
+	const value: string;
+	export default value;
+}
+declare module "@/assets/img/bbb/not_started.png" {
+	const value: string;
+	export default value;
+}
diff --git a/src/components/copy-result-modal/types/CopyResultItemElement.ts b/src/components/copy-result-modal/types/CopyResultItemElement.ts
index 4f8fd148785..a090b86214b 100644
--- a/src/components/copy-result-modal/types/CopyResultItemElement.ts
+++ b/src/components/copy-result-modal/types/CopyResultItemElement.ts
@@ -1,4 +1,4 @@
-import { CopyApiResponseTypeEnum } from "../../../serverApi/v3";
+import { CopyApiResponseTypeEnum } from "@/serverApi/v3/api";
 
 export interface CopyResultItemElement {
 	type: CopyApiResponseTypeEnum;
diff --git a/src/components/rooms/RoomVideoConferenceCard.vue b/src/components/rooms/RoomVideoConferenceCard.vue
index 85d8b621848..b547685070a 100644
--- a/src/components/rooms/RoomVideoConferenceCard.vue
+++ b/src/components/rooms/RoomVideoConferenceCard.vue
@@ -47,6 +47,9 @@
 </template>
 
 <script lang="ts">
+import noPermissionImg from "@/assets/img/bbb/no_permission.png";
+import availableImg from "@/assets/img/bbb/available.png";
+import notStartedImg from "@/assets/img/bbb/not_started.png";
 import { mdiReload } from "@icons/material";
 import { computed, ComputedRef, defineComponent } from "vue";
 import RoomBaseCard from "./RoomBaseCard.vue";
@@ -86,12 +89,12 @@ export default defineComponent({
 
 		const logoUrl: ComputedRef<string> = computed(() => {
 			if (!props.hasPermission) {
-				return require("@/assets/img/bbb/no_permission.png");
+				return noPermissionImg;
 			} else if (props.isRunning) {
-				return require("@/assets/img/bbb/available.png");
+				return availableImg;
 			}
 
-			return require("@/assets/img/bbb/not_started.png");
+			return notStartedImg;
 		});
 
 		return {
diff --git a/src/modules/data/board/Board.store.unit.ts b/src/modules/data/board/Board.store.unit.ts
index a7c9e817c7f..b3aff70069d 100644
--- a/src/modules/data/board/Board.store.unit.ts
+++ b/src/modules/data/board/Board.store.unit.ts
@@ -24,7 +24,7 @@ import {
 import { createPinia, setActivePinia } from "pinia";
 import { computed, ref } from "vue";
 import { Router, useRoute, useRouter } from "vue-router";
-import { BoardLayout } from "../../../serverApi/v3";
+import { BoardLayout } from "@/serverApi/v3/api";
 import { useBoardStore } from "./Board.store";
 import { UpdateBoardLayoutRequestPayload } from "./boardActions/boardActionPayload";
 import { useBoardRestApi } from "./boardActions/boardRestApi.composable";
diff --git a/src/modules/data/board/BoardPermissions.composable.unit.ts b/src/modules/data/board/BoardPermissions.composable.unit.ts
index 7181246fd63..1bc0aa6e40c 100644
--- a/src/modules/data/board/BoardPermissions.composable.unit.ts
+++ b/src/modules/data/board/BoardPermissions.composable.unit.ts
@@ -1,4 +1,4 @@
-import { useSharedBoardPageInformation } from "@/modules/data/board/BoardPageInformation.composable";
+import { useSharedBoardPageInformation } from "./BoardPageInformation.composable";
 import {
 	Permission,
 	ImportUserResponseRoleNamesEnum as Roles,
diff --git a/src/modules/data/board/Card.store.unit.ts b/src/modules/data/board/Card.store.unit.ts
index f5bf1ec1631..fa65bf58eb5 100644
--- a/src/modules/data/board/Card.store.unit.ts
+++ b/src/modules/data/board/Card.store.unit.ts
@@ -1,5 +1,4 @@
 import { useErrorHandler } from "@/components/error-handling/ErrorHandler.composable";
-import { CreateElementRequestPayload } from "@/modules/data/board/cardActions/cardActionPayload";
 import {
 	ContentElementType,
 	PreferredToolResponse,
@@ -18,7 +17,11 @@ import {
 import { cardResponseFactory } from "@@/tests/test-utils/factory/cardResponseFactory";
 import { drawingElementResponseFactory } from "@@/tests/test-utils/factory/drawingElementResponseFactory";
 import setupStores from "@@/tests/test-utils/setupStores";
-import { useCardStore, useSocketConnection } from "@data-board";
+import {
+	CreateElementRequestPayload,
+	useCardStore,
+	useSocketConnection,
+} from "@data-board";
 import { createMock, DeepMocked } from "@golevelup/ts-jest";
 import {
 	useBoardNotifier,
diff --git a/src/modules/data/board/boardActions/boardRestApi.composable.unit.ts b/src/modules/data/board/boardActions/boardRestApi.composable.unit.ts
index bea4fe58d21..c0dc46645ec 100644
--- a/src/modules/data/board/boardActions/boardRestApi.composable.unit.ts
+++ b/src/modules/data/board/boardActions/boardRestApi.composable.unit.ts
@@ -26,7 +26,7 @@ import { useSharedEditMode } from "@util-board";
 import { setActivePinia } from "pinia";
 import { computed, ref } from "vue";
 import { Router, useRouter } from "vue-router";
-import { BoardLayout } from "../../../../serverApi/v3";
+import { BoardLayout } from "@/serverApi/v3/api";
 import { useBoardApi } from "../BoardApi.composable";
 import { useBoardRestApi } from "./boardRestApi.composable";
 
diff --git a/src/modules/data/board/boardActions/boardSocketApi.composable.unit.ts b/src/modules/data/board/boardActions/boardSocketApi.composable.unit.ts
index 4083bb99926..8ec63b6bddb 100644
--- a/src/modules/data/board/boardActions/boardSocketApi.composable.unit.ts
+++ b/src/modules/data/board/boardActions/boardSocketApi.composable.unit.ts
@@ -23,7 +23,7 @@ import { useBoardNotifier, useSharedLastCreatedElement } from "@util-board";
 import { setActivePinia } from "pinia";
 import { useI18n } from "vue-i18n";
 import { Router, useRouter } from "vue-router";
-import { BoardLayout } from "../../../../serverApi/v3";
+import { BoardLayout } from "@/serverApi/v3/api";
 import { DeleteCardFailurePayload } from "../cardActions/cardActionPayload";
 import * as CardActions from "../cardActions/cardActions";
 import {
diff --git a/src/modules/data/board/index.ts b/src/modules/data/board/index.ts
index 245f9f2a877..84cbaa15f66 100644
--- a/src/modules/data/board/index.ts
+++ b/src/modules/data/board/index.ts
@@ -11,6 +11,7 @@ import { useContentElementState } from "./ContentElementState.composable";
 import { useForceRender } from "./fixSamePositionDnD.composable";
 import { useSocketConnection } from "./socket/socket";
 
+export * from "./cardActions/cardActionPayload";
 export {
 	boardActions,
 	cardActions,
diff --git a/src/modules/feature/board-collaborative-text-editor-element/test-utils/collaborativeTextEditorApiMock.ts b/src/modules/feature/board-collaborative-text-editor-element/test-utils/collaborativeTextEditorApiMock.ts
index e3060649c59..127c3492d4a 100644
--- a/src/modules/feature/board-collaborative-text-editor-element/test-utils/collaborativeTextEditorApiMock.ts
+++ b/src/modules/feature/board-collaborative-text-editor-element/test-utils/collaborativeTextEditorApiMock.ts
@@ -1,4 +1,3 @@
-import { Jest as jest } from "@jest/environment";
 import { useCollaborativeTextEditorApi } from "../composables/CollaborativeTextEditorApi.composable";
 
 interface Props {
diff --git a/src/modules/feature/board-file-element/shared/test-utils/fileStorageNotifier.ts b/src/modules/feature/board-file-element/shared/test-utils/fileStorageNotifier.ts
index b58bffc5819..3cc5fbf3b4a 100644
--- a/src/modules/feature/board-file-element/shared/test-utils/fileStorageNotifier.ts
+++ b/src/modules/feature/board-file-element/shared/test-utils/fileStorageNotifier.ts
@@ -1,4 +1,3 @@
-import { Jest as jest } from "@jest/environment";
 import { useFileStorageNotifier } from "../composables/FileStorageNotifications.composable";
 
 interface Props {
diff --git a/src/modules/feature/board/shared/AddElementDialog.composable.ts b/src/modules/feature/board/shared/AddElementDialog.composable.ts
index 7ab30dcfcd3..32f486ad85a 100644
--- a/src/modules/feature/board/shared/AddElementDialog.composable.ts
+++ b/src/modules/feature/board/shared/AddElementDialog.composable.ts
@@ -1,4 +1,4 @@
-import { CreateElementRequestPayload } from "@/modules/data/board/cardActions/cardActionPayload";
+import type { CreateElementRequestPayload } from "@data-board";
 import {
 	BoardFeature,
 	ContentElementType,
diff --git a/src/modules/feature/board/test-utils/AddElementDialogMock.ts b/src/modules/feature/board/test-utils/AddElementDialogMock.ts
index b0dd87387fb..c8660d09bb3 100644
--- a/src/modules/feature/board/test-utils/AddElementDialogMock.ts
+++ b/src/modules/feature/board/test-utils/AddElementDialogMock.ts
@@ -1,4 +1,3 @@
-import { Jest as jest } from "@jest/environment";
 import { Ref, ref } from "vue";
 import { useAddElementDialog } from "../shared/AddElementDialog.composable";
 
diff --git a/src/modules/feature/board/test-utils/sharedElementTypeSelectionMock.ts b/src/modules/feature/board/test-utils/sharedElementTypeSelectionMock.ts
index abcb4e04df7..8e1d7c0387b 100644
--- a/src/modules/feature/board/test-utils/sharedElementTypeSelectionMock.ts
+++ b/src/modules/feature/board/test-utils/sharedElementTypeSelectionMock.ts
@@ -1,4 +1,3 @@
-import { Jest as jest } from "@jest/environment";
 import { Ref, ref } from "vue";
 import { useSharedElementTypeSelection } from "../shared/SharedElementTypeSelection.composable";
 
diff --git a/src/modules/page/room/Rooms.page.unit.ts b/src/modules/page/room/Rooms.page.unit.ts
index 5aaa40b8deb..c6fff99b165 100644
--- a/src/modules/page/room/Rooms.page.unit.ts
+++ b/src/modules/page/room/Rooms.page.unit.ts
@@ -23,7 +23,7 @@ import { mdiPlus } from "@icons/material";
 import DefaultWireframe from "@/components/templates/DefaultWireframe.vue";
 import { RoomGrid } from "@feature-room";
 import ImportFlow from "@/components/share/ImportFlow.vue";
-import InfoAlert from "@/modules/ui/alert/InfoAlert.vue";
+import { InfoAlert } from "@ui-alert";
 
 jest.mock("vue-router");
 const useRouteMock = useRoute as jest.Mock;
diff --git a/src/router/guards/legacy-route-compatibility.guard.ts b/src/router/guards/legacy-route-compatibility.guard.ts
index 13864e480a4..0a7028cedb8 100644
--- a/src/router/guards/legacy-route-compatibility.guard.ts
+++ b/src/router/guards/legacy-route-compatibility.guard.ts
@@ -3,7 +3,7 @@ import {
 	NavigationGuardNext,
 	RouteLocationNormalized,
 } from "vue-router";
-const { isLegacyClient } = require("../legacy-client-route.js");
+import { isLegacyClient } from "../legacy-client-route.js";
 
 export const legacyCompatibilityGuard: NavigationGuard = (
 	to: RouteLocationNormalized,
diff --git a/tests/test-utils/api-mocks/fileStorageApiMock.ts b/tests/test-utils/api-mocks/fileStorageApiMock.ts
index 7d44e1c2e75..5ba188fbf38 100644
--- a/tests/test-utils/api-mocks/fileStorageApiMock.ts
+++ b/tests/test-utils/api-mocks/fileStorageApiMock.ts
@@ -1,6 +1,5 @@
 import { FileRecordResponse } from "@/fileStorageApi/v3";
 import { useFileStorageApi } from "@feature-board-file-element";
-import { Jest as jest } from "@jest/environment";
 import { ref } from "vue";
 
 interface Props {
diff --git a/tests/test-utils/composable-mocks/setupConfirmationComposableMock.ts b/tests/test-utils/composable-mocks/setupConfirmationComposableMock.ts
index c581ca5ede8..9b9f8dcc14d 100644
--- a/tests/test-utils/composable-mocks/setupConfirmationComposableMock.ts
+++ b/tests/test-utils/composable-mocks/setupConfirmationComposableMock.ts
@@ -1,4 +1,3 @@
-import { Jest as jest } from "@jest/environment";
 import { useConfirmationDialog } from "@ui-confirmation-dialog";
 import { ref } from "vue";
 
diff --git a/tests/test-utils/composable-mocks/setupDeleteConfirmationComposableMock.ts b/tests/test-utils/composable-mocks/setupDeleteConfirmationComposableMock.ts
index 2e3a29161a1..5b98b84b80f 100644
--- a/tests/test-utils/composable-mocks/setupDeleteConfirmationComposableMock.ts
+++ b/tests/test-utils/composable-mocks/setupDeleteConfirmationComposableMock.ts
@@ -1,4 +1,3 @@
-import { Jest as jest } from "@jest/environment";
 import { ref } from "vue";
 import { useDeleteConfirmationDialog } from "@ui-confirmation-dialog";
 


Reviews:
user: login: odalys-dataport
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-14T09:32:18Z
user: login: NFriedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T07:48:58Z
user: login: odalys-dataport
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-17T10:12:29Z
user: login: hoeppner-dataport
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T07:49:42Z
user: login: NFriedo
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T08:24:24Z
user: login: NFriedo
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:33:24Z
user: login: hoeppner-dataport
state: COMMENTED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T09:36:13Z
user: login: odalys-dataport
state: APPROVED
author_association: CONTRIBUTOR
submitted_at: 2025-03-18T10:45:58Z

Review comments:
diff_hunk: @@ -44,6 +44,8 @@ module.exports = defineConfigWithVueTs([
 			ecmaVersion: "latest",
 			globals: {
 				...globals.node,
+				NodeJS: true,
+				Clipboard: true,
path: eslint.config.js
user: login: odalys-dataport
body: Oohh nice! Can we do this for `Jest` as well? We're importing jest in some places even though it's not necessary (e.g.` __mocks__/maska.ts`)
created_at: 2025-03-14T09:28:06Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,4 +153,10 @@ module.exports = defineConfigWithVueTs([
 			"schulcloud/material-icon-imports": "off",
 		},
 	},
+	{
+		files: ["**/*.{ts,vue}"],
+		rules: {
+			"@typescript-eslint/no-require-imports": "error",
path: eslint.config.js
user: login: odalys-dataport
body: Could we set the rule to "error" as default and turn it off as an exception?
created_at: 2025-03-14T09:30:49Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,6 +44,8 @@ module.exports = defineConfigWithVueTs([
 			ecmaVersion: "latest",
 			globals: {
 				...globals.node,
+				NodeJS: true,
+				Clipboard: true,
path: eslint.config.js
user: login: NFriedo
body: We are doing this already but just for test files and files under test directory:

```ts
files: ["**/*.unit.{j,t}s?(x)", "tests/**"],
languageOptions: {
    globals: {
	...globals.jest,
```

Maybe this example file you mentioned should be moved to the tests directory?
created_at: 2025-03-17T07:48:58Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,6 +44,8 @@ module.exports = defineConfigWithVueTs([
 			ecmaVersion: "latest",
 			globals: {
 				...globals.node,
+				NodeJS: true,
+				Clipboard: true,
path: eslint.config.js
user: login: odalys-dataport
body: Like I said, there are more places where this is done. I looked them up by searching for `import { Jest`

Other examples:
- AddElementDialogMock.ts
- sharedElementTypeSelectionMock.ts
- ...

These are placed in our feature folder like `/modules/feature/<feature-name>/test-utils`. So I guess either we globally import Jest as well or move all these files to our test folder. We do not have to do this within this PR of course. We can take it into our FE meetings
created_at: 2025-03-17T10:12:29Z
author_association: CONTRIBUTOR
diff_hunk: @@ -151,4 +153,10 @@ module.exports = defineConfigWithVueTs([
 			"schulcloud/material-icon-imports": "off",
 		},
 	},
+	{
+		files: ["**/*.{ts,vue}"],
+		rules: {
+			"@typescript-eslint/no-require-imports": "error",
path: eslint.config.js
user: login: hoeppner-dataport
body: If I do it the way you suggested (and I tried earlier as my first approach), we get this error:
/Users/HoeppneM/sources/nuxt-client/src/router/legacy-client-route.js
  1:22  error  A `require()` style import is forbidden  @typescript-eslint/no-require-imports
  2:25  error  A `require()` style import is forbidden  @typescript-eslint/no-require-imports

As typescript-related rules should only apply for files containing typescript... I implemented it the way I did.
I think it is better this way :)
  
 
created_at: 2025-03-18T07:49:42Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,6 +44,8 @@ module.exports = defineConfigWithVueTs([
 			ecmaVersion: "latest",
 			globals: {
 				...globals.node,
+				NodeJS: true,
+				Clipboard: true,
path: eslint.config.js
user: login: NFriedo
body: Interesting, I always thought that we put everything test related in the `tests` directory. 
Maybe it's a good idea to talk about this in the FE meeting, as you suggested.

I actually liked it to have the jest globals just for the test files, but maybe thats just personal preference :)
created_at: 2025-03-18T08:24:23Z
author_association: CONTRIBUTOR
diff_hunk: @@ -44,6 +44,8 @@ module.exports = defineConfigWithVueTs([
 			ecmaVersion: "latest",
 			globals: {
 				...globals.node,
+				NodeJS: true,
+				Clipboard: true,
path: eslint.config.js
user: login: hoeppner-dataport
body: I added the **/test-utils/** and the __mocks__ as patterns for test-related files, so that the profit on the "...globals.jest" that Nicole mentioned => avoiding to import it also for all other files.

Removed the fake-imports `import { Jest as jest } from "@jest/environment";` from several files.
created_at: 2025-03-18T09:36:13Z
author_association: CONTRIBUTOR

PR comments:
user: login: sonarqubecloud[bot]
created_at: 2025-03-18T09:40:13Z
body: ## [![Quality Gate Passed](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/checks/QualityGateBadge/qg-passed-20px.png 'Quality Gate Passed')](https://sonarcloud.io/dashboard?id=hpi-schul-cloud_nuxt-client&pullRequest=3602) **Quality Gate passed**  
Issues  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 New issues](https://sonarcloud.io/project/issues?id=hpi-schul-cloud_nuxt-client&pullRequest=3602&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/accepted-16px.png '') [0 Accepted issues](https://sonarcloud.io/project/issues?id=hpi-schul-cloud_nuxt-client&pullRequest=3602&issueStatuses=ACCEPTED)

Measures  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0 Security Hotspots](https://sonarcloud.io/project/security_hotspots?id=hpi-schul-cloud_nuxt-client&pullRequest=3602&issueStatuses=OPEN,CONFIRMED&sinceLeakPeriod=true)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [100.0% Coverage on New Code](https://sonarcloud.io/component_measures?id=hpi-schul-cloud_nuxt-client&pullRequest=3602&metric=new_coverage&view=list)  
![](https://sonarsource.github.io/sonarcloud-github-static-resources/v2/common/passed-16px.png '') [0.0% Duplication on New Code](https://sonarcloud.io/component_measures?id=hpi-schul-cloud_nuxt-client&pullRequest=3602&metric=new_duplicated_lines_density&view=list)  
  
[See analysis details on SonarQube Cloud](https://sonarcloud.io/dashboard?id=hpi-schul-cloud_nuxt-client&pullRequest=3602)


author_association: NONE