Merge branch 'feat/endpoints-in-backend-om-eigen-leerpaden-en-leerobjecten-toe-te-voegen-aan-de-databank-#248' of https://github.com/SELab-2/Dwengo-1 into feat/endpoints-in-backend-om-eigen-leerpaden-en-leerobjecten-toe-te-voegen-aan-de-databank-#248
This commit is contained in:
		
						commit
						e0f6cc4150
					
				
					 72 changed files with 891 additions and 344 deletions
				
			
		|  | @ -17,6 +17,7 @@ | |||
|         "test:e2e": "playwright test" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@dwengo-1/common": "^0.2.0", | ||||
|         "@tanstack/react-query": "^5.69.0", | ||||
|         "@tanstack/vue-query": "^5.69.0", | ||||
|         "@vueuse/core": "^13.1.0", | ||||
|  |  | |||
|  | @ -31,4 +31,9 @@ | |||
|     ></v-text-field> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped></style> | ||||
| <style scoped> | ||||
|     .search-field { | ||||
|         width: 25%; | ||||
|         min-width: 300px; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
|  | @ -53,9 +53,9 @@ | |||
|         white-space: normal; | ||||
|     } | ||||
|     .results-grid { | ||||
|         margin: 20px; | ||||
|         margin: 20px auto; | ||||
|         display: flex; | ||||
|         align-items: stretch; | ||||
|         justify-content: center; | ||||
|         gap: 20px; | ||||
|         flex-wrap: wrap; | ||||
|     } | ||||
|  |  | |||
|  | @ -16,6 +16,7 @@ | |||
|     const _router = useRouter(); // Zonder '_' gaf dit een linter error voor unused variable | ||||
| 
 | ||||
|     const name: string = auth.authState.user!.profile.name!; | ||||
|     const username = auth.authState.user!.profile.preferred_username!; | ||||
|     const email = auth.authState.user!.profile.email; | ||||
|     const initials: string = name | ||||
|         .split(" ") | ||||
|  | @ -183,10 +184,15 @@ | |||
|             <v-card> | ||||
|                 <v-card-text> | ||||
|                     <div class="mx-auto text-center"> | ||||
|                         <v-avatar color="#0e6942"> | ||||
|                             <span class="text-h5">{{ initials }}</span> | ||||
|                         <v-avatar | ||||
|                             color="#0e6942" | ||||
|                             size="large" | ||||
|                             class="user-button mb-3" | ||||
|                         > | ||||
|                             <span>{{ initials }}</span> | ||||
|                         </v-avatar> | ||||
|                         <h3>{{ name }}</h3> | ||||
|                         <p class="text-caption mt-1">{{ username }}</p> | ||||
|                         <p class="text-caption mt-1">{{ email }}</p> | ||||
|                         <v-divider class="my-3"></v-divider> | ||||
|                         <v-btn | ||||
|  |  | |||
|  | @ -6,6 +6,7 @@ | |||
|     import type { AnswersResponse } from "@/controllers/answers"; | ||||
|     import type { AnswerData, AnswerDTO } from "@dwengo-1/common/interfaces/answer"; | ||||
|     import authService from "@/services/auth/auth-service"; | ||||
|     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||
| 
 | ||||
|     const props = defineProps<{ | ||||
|         question: QuestionDTO; | ||||
|  | @ -80,7 +81,7 @@ | |||
|             {{ question.content }} | ||||
|         </div> | ||||
|         <div | ||||
|             v-if="authService.authState.activeRole === 'teacher'" | ||||
|             v-if="authService.authState.activeRole === AccountType.Teacher" | ||||
|             class="answer-input-container" | ||||
|         > | ||||
|             <input | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ import { invalidateAllGroupKeys } from "./groups"; | |||
| import { invalidateAllSubmissionKeys } from "./submissions"; | ||||
| import type { TeachersResponse } from "@/controllers/teachers"; | ||||
| import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations"; | ||||
| import { studentClassesQueryKey } from "@/queries/students.ts"; | ||||
| 
 | ||||
| const classController = new ClassController(); | ||||
| 
 | ||||
|  | @ -171,6 +172,8 @@ export function useClassDeleteStudentMutation(): UseMutationReturnType< | |||
|             await queryClient.invalidateQueries({ queryKey: classQueryKey(data.class.id) }); | ||||
|             await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, true) }); | ||||
|             await queryClient.invalidateQueries({ queryKey: classStudentsKey(data.class.id, false) }); | ||||
|             await queryClient.invalidateQueries({ queryKey: studentClassesQueryKey(data.class.id, false) }); | ||||
|             await queryClient.invalidateQueries({ queryKey: studentClassesQueryKey(data.class.id, true) }); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
|  |  | |||
|  | @ -33,7 +33,7 @@ function studentsQueryKey(full: boolean): [string, boolean] { | |||
| function studentQueryKey(username: string): [string, string] { | ||||
|     return ["student", username]; | ||||
| } | ||||
| function studentClassesQueryKey(username: string, full: boolean): [string, string, boolean] { | ||||
| export function studentClassesQueryKey(username: string, full: boolean): [string, string, boolean] { | ||||
|     return ["student-classes", username, full]; | ||||
| } | ||||
| function studentAssignmentsQueryKey(username: string, full: boolean): [string, string, boolean] { | ||||
|  |  | |||
|  | @ -15,6 +15,7 @@ import SingleTheme from "@/views/SingleTheme.vue"; | |||
| import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue"; | ||||
| import authService from "@/services/auth/auth-service"; | ||||
| import OwnLearningContentPage from "@/views/own-learning-content/OwnLearningContentPage.vue"; | ||||
| import { allowRedirect, Redirect } from "@/utils/redirect.ts"; | ||||
| 
 | ||||
| const router = createRouter({ | ||||
|     history: createWebHistory(import.meta.env.BASE_URL), | ||||
|  | @ -150,7 +151,11 @@ router.beforeEach(async (to, _from, next) => { | |||
|     // Verify if user is logged in before accessing certain routes
 | ||||
|     if (to.meta.requiresAuth) { | ||||
|         if (!authService.isLoggedIn.value && !(await authService.loadUser())) { | ||||
|             next("/login"); | ||||
|             const path = to.fullPath; | ||||
|             if (allowRedirect(path)) { | ||||
|                 localStorage.setItem(Redirect.AFTER_LOGIN_KEY, path); | ||||
|             } | ||||
|             next(Redirect.LOGIN); | ||||
|         } else { | ||||
|             next(); | ||||
|         } | ||||
|  |  | |||
							
								
								
									
										12
									
								
								frontend/src/utils/redirect.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										12
									
								
								frontend/src/utils/redirect.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,12 @@ | |||
| export enum Redirect { | ||||
|     AFTER_LOGIN_KEY = "redirectAfterLogin", | ||||
|     HOME = "/user", | ||||
|     LOGIN = "/login", | ||||
|     ROOT = "/", | ||||
| } | ||||
| 
 | ||||
| const NOT_ALLOWED_REDIRECTS = new Set<Redirect>([Redirect.HOME, Redirect.ROOT, Redirect.LOGIN]); | ||||
| 
 | ||||
| export function allowRedirect(path: string): boolean { | ||||
|     return !NOT_ALLOWED_REDIRECTS.has(path as Redirect); | ||||
| } | ||||
|  | @ -3,6 +3,7 @@ | |||
|     import { useI18n } from "vue-i18n"; | ||||
|     import { onMounted, ref, type Ref } from "vue"; | ||||
|     import auth from "../services/auth/auth-service.ts"; | ||||
|     import { Redirect } from "@/utils/redirect.ts"; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
| 
 | ||||
|  | @ -10,10 +11,20 @@ | |||
| 
 | ||||
|     const errorMessage: Ref<string | null> = ref(null); | ||||
| 
 | ||||
|     async function redirectPage(): Promise<void> { | ||||
|         const redirectUrl = localStorage.getItem(Redirect.AFTER_LOGIN_KEY); | ||||
|         if (redirectUrl) { | ||||
|             localStorage.removeItem(Redirect.AFTER_LOGIN_KEY); | ||||
|             await router.replace(redirectUrl); | ||||
|         } else { | ||||
|             await router.replace(Redirect.HOME); | ||||
|         } | ||||
|     } | ||||
| 
 | ||||
|     onMounted(async () => { | ||||
|         try { | ||||
|             await auth.handleLoginCallback(); | ||||
|             await router.replace("/user"); // Redirect to theme page | ||||
|             await redirectPage(); | ||||
|         } catch (error) { | ||||
|             errorMessage.value = `${t("loginUnexpectedError")}: ${error}`; | ||||
|         } | ||||
|  |  | |||
|  | @ -3,6 +3,7 @@ | |||
|     import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg"; | ||||
|     import auth from "@/services/auth/auth-service.ts"; | ||||
|     import { watch } from "vue"; | ||||
|     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||
| 
 | ||||
|     const router = useRouter(); | ||||
| 
 | ||||
|  | @ -17,11 +18,11 @@ | |||
|     ); | ||||
| 
 | ||||
|     async function loginAsStudent(): Promise<void> { | ||||
|         await auth.loginAs("student"); | ||||
|         await auth.loginAs(AccountType.Student); | ||||
|     } | ||||
| 
 | ||||
|     async function loginAsTeacher(): Promise<void> { | ||||
|         await auth.loginAs("teacher"); | ||||
|         await auth.loginAs(AccountType.Teacher); | ||||
|     } | ||||
| </script> | ||||
| 
 | ||||
|  |  | |||
|  | @ -35,13 +35,14 @@ | |||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <div class="container"> | ||||
|     <div class="container d-flex flex-column align-items-center justify-center"> | ||||
|         <using-query-result :query-result="themeQueryResult"> | ||||
|             <h1>{{ currentThemeInfo!!.title }}</h1> | ||||
|             <p>{{ currentThemeInfo!!.description }}</p> | ||||
|             <div class="search-field-container"> | ||||
|             <br /> | ||||
|             <div class="search-field-container mt-sm-6"> | ||||
|                 <v-text-field | ||||
|                     class="search-field" | ||||
|                     class="search-field mx-auto" | ||||
|                     :label="t('search')" | ||||
|                     append-inner-icon="mdi-magnify" | ||||
|                     v-model="searchFilter" | ||||
|  | @ -60,13 +61,15 @@ | |||
| 
 | ||||
| <style scoped> | ||||
|     .search-field-container { | ||||
|         display: block; | ||||
|         margin: 20px; | ||||
|         justify-content: center !important; | ||||
|     } | ||||
|     .search-field { | ||||
|         max-width: 300px; | ||||
|         width: 25%; | ||||
|         min-width: 300px; | ||||
|     } | ||||
|     .container { | ||||
|         padding: 20px; | ||||
|         justify-content: center; | ||||
|         justify-items: center; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
|  | @ -14,6 +14,7 @@ | |||
|     import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | ||||
|     import { useCreateAssignmentMutation } from "@/queries/assignments.ts"; | ||||
|     import { useRoute } from "vue-router"; | ||||
|     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||
| 
 | ||||
|     const route = useRoute(); | ||||
|     const router = useRouter(); | ||||
|  | @ -23,7 +24,7 @@ | |||
| 
 | ||||
|     onMounted(async () => { | ||||
|         // Redirect student | ||||
|         if (role.value === "student") { | ||||
|         if (role.value === AccountType.Student) { | ||||
|             await router.push("/user"); | ||||
|         } | ||||
| 
 | ||||
|  |  | |||
|  | @ -8,9 +8,10 @@ | |||
|     import { useGetLearningPathQuery } from "@/queries/learning-paths.ts"; | ||||
|     import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts"; | ||||
|     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; | ||||
|     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||
| 
 | ||||
|     const role = auth.authState.activeRole; | ||||
|     const isTeacher = computed(() => role === "teacher"); | ||||
|     const isTeacher = computed(() => role === AccountType.Teacher); | ||||
| 
 | ||||
|     const route = useRoute(); | ||||
|     const classId = ref<string>(route.params.classId as string); | ||||
|  |  | |||
|  | @ -9,6 +9,7 @@ | |||
|     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; | ||||
|     import { asyncComputed } from "@vueuse/core"; | ||||
|     import { useDeleteAssignmentMutation } from "@/queries/assignments.ts"; | ||||
|     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||
|     import "../../assets/common.css"; | ||||
| 
 | ||||
|     const { t, locale } = useI18n(); | ||||
|  | @ -17,7 +18,7 @@ | |||
|     const role = ref(auth.authState.activeRole); | ||||
|     const username = ref<string>(""); | ||||
| 
 | ||||
|     const isTeacher = computed(() => role.value === "teacher"); | ||||
|     const isTeacher = computed(() => role.value === AccountType.Teacher); | ||||
| 
 | ||||
|     // Fetch and store all the teacher's classes | ||||
|     let classesQueryResults = undefined; | ||||
|  |  | |||
							
								
								
									
										20
									
								
								frontend/src/views/classes/ClassDisplay.vue
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										20
									
								
								frontend/src/views/classes/ClassDisplay.vue
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,20 @@ | |||
| <script setup lang="ts"> | ||||
|     import { useClassQuery } from "@/queries/classes"; | ||||
|     import { defineProps } from "vue"; | ||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
| 
 | ||||
|     const props = defineProps({ | ||||
|         classId: String, | ||||
|     }); | ||||
| 
 | ||||
|     const classQuery = useClassQuery(props.classId); | ||||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <using-query-result | ||||
|         :query-result="classQuery" | ||||
|         v-slot="{ data: classResponse }" | ||||
|     > | ||||
|         <span>{{ classResponse?.class.displayName }}</span> | ||||
|     </using-query-result> | ||||
| </template> | ||||
|  | @ -77,7 +77,7 @@ | |||
|                 }, | ||||
|                 onError: (e) => { | ||||
|                     dialog.value = false; | ||||
|                     showSnackbar(t("failed") + ": " + e.message, "error"); | ||||
|                     showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); | ||||
|                 }, | ||||
|             }, | ||||
|         ); | ||||
|  | @ -105,7 +105,7 @@ | |||
|                     } | ||||
|                 }, | ||||
|                 onError: (e) => { | ||||
|                     showSnackbar(t("failed") + ": " + e.message, "error"); | ||||
|                     showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); | ||||
|                 }, | ||||
|             }, | ||||
|         ); | ||||
|  | @ -126,7 +126,7 @@ | |||
|                 usernameTeacher.value = ""; | ||||
|             }, | ||||
|             onError: (e) => { | ||||
|                 showSnackbar(t("failed") + ": " + e.message, "error"); | ||||
|                 showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); | ||||
|             }, | ||||
|         }); | ||||
|     } | ||||
|  |  | |||
|  | @ -2,7 +2,7 @@ | |||
|     import { useI18n } from "vue-i18n"; | ||||
|     import authState from "@/services/auth/auth-service.ts"; | ||||
|     import { computed, onMounted, ref } from "vue"; | ||||
|     import { validate, version } from "uuid"; | ||||
|     import { useRoute } from "vue-router"; | ||||
|     import type { ClassDTO } from "@dwengo-1/common/interfaces/class"; | ||||
|     import { useCreateJoinRequestMutation, useStudentClassesQuery } from "@/queries/students"; | ||||
|     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; | ||||
|  | @ -15,6 +15,7 @@ | |||
|     import "../../assets/common.css"; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
|     const route = useRoute(); | ||||
| 
 | ||||
|     // Username of logged in student | ||||
|     const username = ref<string | undefined>(undefined); | ||||
|  | @ -38,6 +39,11 @@ | |||
|         } finally { | ||||
|             isLoading.value = false; | ||||
|         } | ||||
| 
 | ||||
|         const queryCode = route.query.code as string | undefined; | ||||
|         if (queryCode) { | ||||
|             code.value = queryCode; | ||||
|         } | ||||
|     }); | ||||
| 
 | ||||
|     // Fetch all classes of the logged in student | ||||
|  | @ -75,11 +81,15 @@ | |||
| 
 | ||||
|     // The code a student sends in to join a class needs to be formatted as v4 to be valid | ||||
|     // These rules are used to display a message to the user if they use a code that has an invalid format | ||||
|     function codeRegex(value: string): boolean { | ||||
|         return /^[a-zA-Z0-9]{6}$/.test(value); | ||||
|     } | ||||
| 
 | ||||
|     const codeRules = [ | ||||
|         (value: string | undefined): string | boolean => { | ||||
|             if (value === undefined || value === "") { | ||||
|                 return true; | ||||
|             } else if (value !== undefined && validate(value) && version(value) === 4) { | ||||
|             } else if (codeRegex(value)) { | ||||
|                 return true; | ||||
|             } | ||||
|             return t("invalidFormat"); | ||||
|  | @ -92,7 +102,7 @@ | |||
|     // Function called when a student submits a code to join a class | ||||
|     function submitCode(): void { | ||||
|         // Check if the code is valid | ||||
|         if (code.value !== undefined && validate(code.value) && version(code.value) === 4) { | ||||
|         if (code.value !== undefined && codeRegex(code.value)) { | ||||
|             mutate( | ||||
|                 { username: username.value!, classId: code.value }, | ||||
|                 { | ||||
|  | @ -100,7 +110,7 @@ | |||
|                         showSnackbar(t("sent"), "success"); | ||||
|                     }, | ||||
|                     onError: (e) => { | ||||
|                         showSnackbar(t("failed") + ": " + e.message, "error"); | ||||
|                         showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); | ||||
|                     }, | ||||
|                 }, | ||||
|             ); | ||||
|  | @ -260,7 +270,7 @@ | |||
|                             <v-text-field | ||||
|                                 label="CODE" | ||||
|                                 v-model="code" | ||||
|                                 placeholder="XXXXXXXX-XXXX-4XXX-XXXX-XXXXXXXXXXXX" | ||||
|                                 placeholder="XXXXXX" | ||||
|                                 :rules="codeRules" | ||||
|                                 variant="outlined" | ||||
|                             ></v-text-field> | ||||
|  |  | |||
|  | @ -8,7 +8,7 @@ | |||
|     import { useTeacherClassesQuery } from "@/queries/teachers"; | ||||
|     import type { ClassesResponse } from "@/controllers/classes"; | ||||
|     import UsingQueryResult from "@/components/UsingQueryResult.vue"; | ||||
|     import { useClassesQuery, useCreateClassMutation } from "@/queries/classes"; | ||||
|     import { useCreateClassMutation } from "@/queries/classes"; | ||||
|     import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations"; | ||||
|     import { | ||||
|         useRespondTeacherInvitationMutation, | ||||
|  | @ -16,6 +16,7 @@ | |||
|     } from "@/queries/teacher-invitations"; | ||||
|     import { useDisplay } from "vuetify"; | ||||
|     import "../../assets/common.css"; | ||||
|     import ClassDisplay from "@/views/classes/ClassDisplay.vue"; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
| 
 | ||||
|  | @ -41,7 +42,6 @@ | |||
| 
 | ||||
|     // Fetch all classes of the logged in teacher | ||||
|     const classesQuery = useTeacherClassesQuery(username, true); | ||||
|     const allClassesQuery = useClassesQuery(); | ||||
|     const { mutate } = useCreateClassMutation(); | ||||
|     const getInvitationsQuery = useTeacherInvitationsReceivedQuery(username); | ||||
|     const { mutate: respondToInvitation } = useRespondTeacherInvitationMutation(); | ||||
|  | @ -70,7 +70,7 @@ | |||
|                 await getInvitationsQuery.refetch(); | ||||
|             }, | ||||
|             onError: (e) => { | ||||
|                 showSnackbar(t("failed") + ": " + e.message, "error"); | ||||
|                 showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error"); | ||||
|             }, | ||||
|         }); | ||||
|     } | ||||
|  | @ -132,17 +132,12 @@ | |||
|     // Show the teacher, copying of the code was a successs | ||||
|     const copied = ref(false); | ||||
| 
 | ||||
|     // Copy the generated code to the clipboard | ||||
|     async function copyToClipboard(): Promise<void> { | ||||
|         await navigator.clipboard.writeText(code.value); | ||||
|         copied.value = true; | ||||
|     } | ||||
|     async function copyToClipboard(code: string, isDialog = false, isLink = false): Promise<void> { | ||||
|         const content = isLink ? `${window.location.origin}/user/class?code=${code}` : code; | ||||
|         await navigator.clipboard.writeText(content); | ||||
|         copied.value = isDialog; | ||||
| 
 | ||||
|     async function copyCode(selectedCode: string): Promise<void> { | ||||
|         code.value = selectedCode; | ||||
|         await copyToClipboard(); | ||||
|         showSnackbar(t("copied"), "white"); | ||||
|         copied.value = false; | ||||
|         if (!isDialog) showSnackbar(t("copied"), "white"); | ||||
|     } | ||||
| 
 | ||||
|     // Custom breakpoints | ||||
|  | @ -170,6 +165,7 @@ | |||
|     // Code display dialog logic | ||||
|     const viewCodeDialog = ref(false); | ||||
|     const selectedCode = ref(""); | ||||
| 
 | ||||
|     function openCodeDialog(codeToView: string): void { | ||||
|         selectedCode.value = codeToView; | ||||
|         viewCodeDialog.value = true; | ||||
|  | @ -231,24 +227,38 @@ | |||
|                                                 variant="text" | ||||
|                                             > | ||||
|                                                 {{ c.displayName }} | ||||
|                                                 <v-icon end> mdi-menu-right </v-icon> | ||||
|                                                 <v-icon end> mdi-menu-right</v-icon> | ||||
|                                             </v-btn> | ||||
|                                         </td> | ||||
|                                         <td> | ||||
|                                             <v-btn | ||||
|                                             <v-row | ||||
|                                                 v-if="!isMdAndDown" | ||||
|                                                 variant="text" | ||||
|                                                 append-icon="mdi-content-copy" | ||||
|                                                 @click="copyCode(c.id)" | ||||
|                                                 dense | ||||
|                                                 align="center" | ||||
|                                                 no-gutters | ||||
|                                             > | ||||
|                                                 {{ c.id }} | ||||
|                                             </v-btn> | ||||
|                                                 <v-btn | ||||
|                                                     variant="text" | ||||
|                                                     append-icon="mdi-content-copy" | ||||
|                                                     @click="copyToClipboard(c.id)" | ||||
|                                                 > | ||||
|                                                     {{ c.id }} | ||||
|                                                 </v-btn> | ||||
|                                                 <v-btn | ||||
|                                                     icon | ||||
|                                                     variant="text" | ||||
|                                                     @click="copyToClipboard(c.id, false, true)" | ||||
|                                                 > | ||||
|                                                     <v-icon>mdi-link-variant</v-icon> | ||||
|                                                 </v-btn> | ||||
|                                             </v-row> | ||||
|                                             <span | ||||
|                                                 v-else | ||||
|                                                 style="cursor: pointer" | ||||
|                                                 @click="openCodeDialog(c.id)" | ||||
|                                                 ><v-icon icon="mdi-eye"></v-icon | ||||
|                                             ></span> | ||||
|                                             > | ||||
|                                                 <v-icon icon="mdi-eye"></v-icon> | ||||
|                                             </span> | ||||
|                                         </td> | ||||
| 
 | ||||
|                                         <td>{{ c.students.length }}</td> | ||||
|  | @ -300,8 +310,8 @@ | |||
|                                             type="submit" | ||||
|                                             @click="createClass" | ||||
|                                             block | ||||
|                                             >{{ t("create") }}</v-btn | ||||
|                                         > | ||||
|                                             >{{ t("create") }} | ||||
|                                         </v-btn> | ||||
|                                     </v-form> | ||||
|                                 </v-sheet> | ||||
|                                 <v-container> | ||||
|  | @ -310,14 +320,29 @@ | |||
|                                         max-width="400px" | ||||
|                                     > | ||||
|                                         <v-card> | ||||
|                                             <v-card-title class="headline">code</v-card-title> | ||||
|                                             <v-card-title class="headline">{{ t("code") }}</v-card-title> | ||||
|                                             <v-card-text> | ||||
|                                                 <v-text-field | ||||
|                                                     v-model="code" | ||||
|                                                     readonly | ||||
|                                                     append-inner-icon="mdi-content-copy" | ||||
|                                                     @click:append-inner="copyToClipboard" | ||||
|                                                 ></v-text-field> | ||||
|                                                 > | ||||
|                                                     <template #append> | ||||
|                                                         <v-btn | ||||
|                                                             icon | ||||
|                                                             variant="text" | ||||
|                                                             @click="copyToClipboard(code, true)" | ||||
|                                                         > | ||||
|                                                             <v-icon>mdi-content-copy</v-icon> | ||||
|                                                         </v-btn> | ||||
|                                                         <v-btn | ||||
|                                                             icon | ||||
|                                                             variant="text" | ||||
|                                                             @click="copyToClipboard(code, true, true)" | ||||
|                                                         > | ||||
|                                                             <v-icon>mdi-link-variant</v-icon> | ||||
|                                                         </v-btn> | ||||
|                                                     </template> | ||||
|                                                 </v-text-field> | ||||
|                                                 <v-slide-y-transition> | ||||
|                                                     <div | ||||
|                                                         v-if="copied" | ||||
|  | @ -368,85 +393,75 @@ | |||
|                             :query-result="getInvitationsQuery" | ||||
|                             v-slot="invitationsResponse: { data: TeacherInvitationsResponse }" | ||||
|                         > | ||||
|                             <using-query-result | ||||
|                                 :query-result="allClassesQuery" | ||||
|                                 v-slot="classesResponse: { data: ClassesResponse }" | ||||
|                             > | ||||
|                                 <template v-if="invitationsResponse.data.invitations.length"> | ||||
|                                     <tr | ||||
|                                         v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]" | ||||
|                                         :key="i.classId" | ||||
|                             <template v-if="invitationsResponse.data.invitations.length"> | ||||
|                                 <tr | ||||
|                                     v-for="i in invitationsResponse.data.invitations as TeacherInvitationDTO[]" | ||||
|                                     :key="i.classId" | ||||
|                                 > | ||||
|                                     <td> | ||||
|                                         <ClassDisplay :classId="i.classId" /> | ||||
|                                     </td> | ||||
|                                     <td> | ||||
|                                         {{ | ||||
|                                             (i.sender as TeacherDTO).firstName + " " + (i.sender as TeacherDTO).lastName | ||||
|                                         }} | ||||
|                                     </td> | ||||
|                                     <td class="text-right"> | ||||
|                                         <span v-if="!isSmAndDown"> | ||||
|                                             <div> | ||||
|                                                 <v-btn | ||||
|                                                     color="green" | ||||
|                                                     @click="handleInvitation(i, true)" | ||||
|                                                     class="mr-2" | ||||
|                                                 > | ||||
|                                                     {{ t("accept") }} | ||||
|                                                 </v-btn> | ||||
|                                                 <v-btn | ||||
|                                                     color="red" | ||||
|                                                     @click="handleInvitation(i, false)" | ||||
|                                                 > | ||||
|                                                     {{ t("deny") }} | ||||
|                                                 </v-btn> | ||||
|                                             </div> | ||||
|                                         </span> | ||||
|                                         <span v-else> | ||||
|                                             <div> | ||||
|                                                 <v-btn | ||||
|                                                     @click="handleInvitation(i, true)" | ||||
|                                                     class="mr-2" | ||||
|                                                     icon="mdi-check-circle" | ||||
|                                                     color="green" | ||||
|                                                     variant="text" | ||||
|                                                 > | ||||
|                                                 </v-btn> | ||||
|                                                 <v-btn | ||||
|                                                     @click="handleInvitation(i, false)" | ||||
|                                                     class="mr-2" | ||||
|                                                     icon="mdi-close-circle" | ||||
|                                                     color="red" | ||||
|                                                     variant="text" | ||||
|                                                 > | ||||
|                                                 </v-btn> | ||||
|                                             </div> | ||||
|                                         </span> | ||||
|                                     </td> | ||||
|                                 </tr> | ||||
|                             </template> | ||||
|                             <template v-else> | ||||
|                                 <tr> | ||||
|                                     <td | ||||
|                                         colspan="3" | ||||
|                                         class="empty-message" | ||||
|                                     > | ||||
|                                         <td> | ||||
|                                             {{ | ||||
|                                                 (classesResponse.data.classes as ClassDTO[]).filter( | ||||
|                                                     (c) => c.id == i.classId, | ||||
|                                                 )[0].displayName | ||||
|                                             }} | ||||
|                                         </td> | ||||
|                                         <td> | ||||
|                                             {{ | ||||
|                                                 (i.sender as TeacherDTO).firstName + | ||||
|                                                 " " + | ||||
|                                                 (i.sender as TeacherDTO).lastName | ||||
|                                             }} | ||||
|                                         </td> | ||||
|                                         <td class="text-right"> | ||||
|                                             <span v-if="!isSmAndDown"> | ||||
|                                                 <div> | ||||
|                                                     <v-btn | ||||
|                                                         color="green" | ||||
|                                                         @click="handleInvitation(i, true)" | ||||
|                                                         class="mr-2" | ||||
|                                                     > | ||||
|                                                         {{ t("accept") }} | ||||
|                                                     </v-btn> | ||||
|                                                     <v-btn | ||||
|                                                         color="red" | ||||
|                                                         @click="handleInvitation(i, false)" | ||||
|                                                     > | ||||
|                                                         {{ t("deny") }} | ||||
|                                                     </v-btn> | ||||
|                                                 </div> | ||||
|                                             </span> | ||||
|                                             <span v-else> | ||||
|                                                 <div> | ||||
|                                                     <v-btn | ||||
|                                                         @click="handleInvitation(i, true)" | ||||
|                                                         class="mr-2" | ||||
|                                                         icon="mdi-check-circle" | ||||
|                                                         color="green" | ||||
|                                                         variant="text" | ||||
|                                                     > | ||||
|                                                     </v-btn> | ||||
|                                                     <v-btn | ||||
|                                                         @click="handleInvitation(i, false)" | ||||
|                                                         class="mr-2" | ||||
|                                                         icon="mdi-close-circle" | ||||
|                                                         color="red" | ||||
|                                                         variant="text" | ||||
|                                                     > | ||||
|                                                     </v-btn></div | ||||
|                                             ></span> | ||||
|                                         </td> | ||||
|                                     </tr> | ||||
|                                 </template> | ||||
|                                 <template v-else> | ||||
|                                     <tr> | ||||
|                                         <td | ||||
|                                             colspan="3" | ||||
|                                             class="empty-message" | ||||
|                                         <v-icon | ||||
|                                             icon="mdi-information-outline" | ||||
|                                             size="small" | ||||
|                                         > | ||||
|                                             <v-icon | ||||
|                                                 icon="mdi-information-outline" | ||||
|                                                 size="small" | ||||
|                                             > | ||||
|                                             </v-icon> | ||||
|                                             {{ t("no-invitations-found") }} | ||||
|                                         </td> | ||||
|                                     </tr> | ||||
|                                 </template> | ||||
|                             </using-query-result> | ||||
|                                         </v-icon> | ||||
|                                         {{ t("no-invitations-found") }} | ||||
|                                     </td> | ||||
|                                 </tr> | ||||
|                             </template> | ||||
|                         </using-query-result> | ||||
|                     </tbody> | ||||
|                 </v-table> | ||||
|  | @ -469,9 +484,24 @@ | |||
|                     <v-text-field | ||||
|                         v-model="selectedCode" | ||||
|                         readonly | ||||
|                         append-inner-icon="mdi-content-copy" | ||||
|                         @click:append-inner="copyToClipboard" | ||||
|                     ></v-text-field> | ||||
|                     > | ||||
|                         <template #append> | ||||
|                             <v-btn | ||||
|                                 icon | ||||
|                                 variant="text" | ||||
|                                 @click="copyToClipboard(selectedCode, true)" | ||||
|                             > | ||||
|                                 <v-icon>mdi-content-copy</v-icon> | ||||
|                             </v-btn> | ||||
|                             <v-btn | ||||
|                                 icon | ||||
|                                 variant="text" | ||||
|                                 @click="copyToClipboard(selectedCode, true, true)" | ||||
|                             > | ||||
|                                 <v-icon>mdi-link-variant</v-icon> | ||||
|                             </v-btn> | ||||
|                         </template> | ||||
|                     </v-text-field> | ||||
|                     <v-slide-y-transition> | ||||
|                         <div | ||||
|                             v-if="copied" | ||||
|  |  | |||
|  | @ -2,6 +2,7 @@ | |||
|     import authState from "@/services/auth/auth-service.ts"; | ||||
|     import TeacherClasses from "./TeacherClasses.vue"; | ||||
|     import StudentClasses from "./StudentClasses.vue"; | ||||
|     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||
| 
 | ||||
|     // Determine if role is student or teacher to render correct view | ||||
|     const role: string = authState.authState.activeRole!; | ||||
|  | @ -9,7 +10,7 @@ | |||
| 
 | ||||
| <template> | ||||
|     <main> | ||||
|         <TeacherClasses v-if="role === 'teacher'"></TeacherClasses> | ||||
|         <TeacherClasses v-if="role === AccountType.Teacher"></TeacherClasses> | ||||
|         <StudentClasses v-else></StudentClasses> | ||||
|     </main> | ||||
| </template> | ||||
|  |  | |||
|  | @ -22,6 +22,7 @@ | |||
|     import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | ||||
|     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; | ||||
|     import QuestionNotification from "@/components/QuestionNotification.vue"; | ||||
|     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||
| 
 | ||||
|     const router = useRouter(); | ||||
|     const route = useRoute(); | ||||
|  | @ -235,8 +236,10 @@ | |||
|                         </p> | ||||
|                     </template> | ||||
|                 </v-list-item> | ||||
|                 <v-list-item | ||||
|                     v-if="query.classId && query.assignmentNo && authService.authState.activeRole === 'teacher'" | ||||
|                 <v-list-itemF | ||||
|                     v-if=" | ||||
|                         query.classId && query.assignmentNo && authService.authState.activeRole === AccountType.Teacher | ||||
|                     " | ||||
|                 > | ||||
|                     <template v-slot:default> | ||||
|                         <learning-path-group-selector | ||||
|  | @ -245,7 +248,7 @@ | |||
|                             v-model="forGroupQueryParam" | ||||
|                         /> | ||||
|                     </template> | ||||
|                 </v-list-item> | ||||
|                 </v-list-itemF> | ||||
|                 <v-divider></v-divider> | ||||
|                 <div> | ||||
|                     <using-query-result | ||||
|  | @ -259,7 +262,9 @@ | |||
|                                 :title="node.title" | ||||
|                                 :active="node.key === props.learningObjectHruid" | ||||
|                                 :key="node.key" | ||||
|                                 v-if="!node.teacherExclusive || authService.authState.activeRole === 'teacher'" | ||||
|                                 v-if=" | ||||
|                                     !node.teacherExclusive || authService.authState.activeRole === AccountType.Teacher | ||||
|                                 " | ||||
|                             > | ||||
|                                 <template v-slot:prepend> | ||||
|                                     <v-icon | ||||
|  | @ -283,7 +288,7 @@ | |||
|                     </using-query-result> | ||||
|                 </div> | ||||
|                 <v-spacer></v-spacer> | ||||
|                 <v-list-item v-if="authService.authState.activeRole === 'teacher'"> | ||||
|                 <v-list-item v-if="authService.authState.activeRole === AccountType.Teacher"> | ||||
|                     <template v-slot:default> | ||||
|                         <v-btn | ||||
|                             class="button-in-nav" | ||||
|  | @ -296,7 +301,7 @@ | |||
|                 </v-list-item> | ||||
|                 <v-list-item> | ||||
|                     <div | ||||
|                         v-if="authService.authState.activeRole === 'student' && pathIsAssignment" | ||||
|                         v-if="authService.authState.activeRole === AccountType.Student && pathIsAssignment" | ||||
|                         class="assignment-indicator" | ||||
|                     > | ||||
|                         {{ t("assignmentIndicator") }} | ||||
|  | @ -325,7 +330,7 @@ | |||
|             ></learning-object-view> | ||||
|         </div> | ||||
|         <div | ||||
|             v-if="authService.authState.activeRole === 'student' && pathIsAssignment" | ||||
|             v-if="authService.authState.activeRole === AccountType.Student && pathIsAssignment" | ||||
|             class="question-box" | ||||
|         > | ||||
|             <div class="input-wrapper"> | ||||
|  |  | |||
|  | @ -17,43 +17,29 @@ | |||
| </script> | ||||
| 
 | ||||
| <template> | ||||
|     <v-container class="search-page-container"> | ||||
|         <v-row | ||||
|             justify="center" | ||||
|             class="mb-6" | ||||
|     <div class="search-page-container d-flex flex-column align-items-center justify-center"> | ||||
|         <div class="search-field-container"> | ||||
|             <learning-path-search-field class="mx-auto" /> | ||||
|         </div> | ||||
| 
 | ||||
|         <using-query-result | ||||
|             :query-result="searchQueryResults" | ||||
|             v-slot="{ data }: { data: LearningPath[] }" | ||||
|         > | ||||
|             <v-col | ||||
|                 cols="12" | ||||
|                 sm="8" | ||||
|                 md="6" | ||||
|                 lg="4" | ||||
|             > | ||||
|                 <learning-path-search-field class="search-field" /> | ||||
|             </v-col> | ||||
|         </v-row> | ||||
|             <learning-paths-grid :learning-paths="data" /> | ||||
|         </using-query-result> | ||||
| 
 | ||||
|         <v-row justify="center"> | ||||
|             <v-col cols="12"> | ||||
|                 <using-query-result | ||||
|                     :query-result="searchQueryResults" | ||||
|                     v-slot="{ data }: { data: LearningPath[] }" | ||||
|                 > | ||||
|                     <learning-paths-grid :learning-paths="data" /> | ||||
|                 </using-query-result> | ||||
| 
 | ||||
|                 <div | ||||
|                     v-if="!query" | ||||
|                     class="empty-state-container" | ||||
|                 > | ||||
|                     <v-empty-state | ||||
|                         icon="mdi-magnify" | ||||
|                         :title="t('enterSearchTerm')" | ||||
|                         :text="t('enterSearchTermDescription')" | ||||
|                     /> | ||||
|                 </div> | ||||
|             </v-col> | ||||
|         </v-row> | ||||
|     </v-container> | ||||
|         <div | ||||
|             v-if="!query" | ||||
|             class="empty-state-container" | ||||
|         > | ||||
|             <v-empty-state | ||||
|                 icon="mdi-magnify" | ||||
|                 :title="t('enterSearchTerm')" | ||||
|                 :text="t('enterSearchTermDescription')" | ||||
|             /> | ||||
|         </div> | ||||
|     </div> | ||||
| </template> | ||||
| 
 | ||||
| <style scoped> | ||||
|  | @ -61,8 +47,7 @@ | |||
|         padding-top: 40px; | ||||
|         padding-bottom: 40px; | ||||
|     } | ||||
| 
 | ||||
|     .search-field { | ||||
|         max-width: 100%; | ||||
|     .search-field-container { | ||||
|         justify-content: center !important; | ||||
|     } | ||||
| </style> | ||||
|  |  | |||
|  | @ -8,8 +8,9 @@ | |||
|     import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data"; | ||||
|     import LearningObjectContentView from "@/views/learning-paths/learning-object/content/LearningObjectContentView.vue"; | ||||
|     import LearningObjectSubmissionsView from "@/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsView.vue"; | ||||
|     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||
| 
 | ||||
|     const _isStudent = computed(() => authService.authState.activeRole === "student"); | ||||
|     const _isStudent = computed(() => authService.authState.activeRole === AccountType.Student); | ||||
| 
 | ||||
|     const props = defineProps<{ | ||||
|         hruid: string; | ||||
|  |  | |||
|  | @ -11,6 +11,7 @@ | |||
|     import type { StudentDTO } from "@dwengo-1/common/interfaces/student"; | ||||
|     import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; | ||||
|     import { useI18n } from "vue-i18n"; | ||||
|     import { AccountType } from "@dwengo-1/common/util/account-types"; | ||||
| 
 | ||||
|     const { t } = useI18n(); | ||||
| 
 | ||||
|  | @ -31,7 +32,7 @@ | |||
|         mutate: submitSolution, | ||||
|     } = useCreateSubmissionMutation(); | ||||
| 
 | ||||
|     const isStudent = computed(() => authService.authState.activeRole === "student"); | ||||
|     const isStudent = computed(() => authService.authState.activeRole === AccountType.Student); | ||||
| 
 | ||||
|     const isSubmitDisabled = computed(() => { | ||||
|         if (!props.submissionData || props.submissions === undefined) { | ||||
|  |  | |||
		Reference in a new issue
	
	 Gerald Schmittinger
						Gerald Schmittinger