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) {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue