Merge fix/progress-bar into feat/232-assignments-pagina-ui-ux
This commit is contained in:
commit
368130c431
149 changed files with 4429 additions and 1120 deletions
|
@ -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}`;
|
||||
}
|
||||
|
|
|
@ -28,7 +28,7 @@
|
|||
alt="Dwengo logo"
|
||||
style="align-self: center"
|
||||
/>
|
||||
<h1>{{ t("homeTitle") }}</h1>
|
||||
<h1 class="h1">{{ t("homeTitle") }}</h1>
|
||||
<p class="info">
|
||||
{{ t("homeIntroduction1") }}
|
||||
</p>
|
||||
|
@ -84,7 +84,10 @@
|
|||
</div>
|
||||
</div>
|
||||
<div class="container_right">
|
||||
<v-menu open-on-hover>
|
||||
<v-menu
|
||||
open-on-hover
|
||||
open-on-click
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
|
|
|
@ -1,17 +1,28 @@
|
|||
<script setup lang="ts">
|
||||
import { useRouter } from "vue-router";
|
||||
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();
|
||||
|
||||
watch(
|
||||
() => auth.isLoggedIn.value,
|
||||
async (newVal) => {
|
||||
if (newVal) {
|
||||
await router.push("/user");
|
||||
}
|
||||
},
|
||||
{ immediate: true },
|
||||
);
|
||||
|
||||
async function loginAsStudent(): Promise<void> {
|
||||
await auth.loginAs("student");
|
||||
await auth.loginAs(AccountType.Student);
|
||||
}
|
||||
|
||||
async function loginAsTeacher(): Promise<void> {
|
||||
await auth.loginAs("teacher");
|
||||
}
|
||||
|
||||
async function performLogout(): Promise<void> {
|
||||
await auth.logout();
|
||||
await auth.loginAs(AccountType.Teacher);
|
||||
}
|
||||
</script>
|
||||
|
||||
|
@ -65,13 +76,6 @@
|
|||
</div>
|
||||
</ul>
|
||||
</div>
|
||||
<div v-if="auth.isLoggedIn.value">
|
||||
<p>
|
||||
You are currently logged in as {{ auth.authState.user!.profile.name }} ({{ auth.authState.activeRole }})
|
||||
</p>
|
||||
<v-btn @click="performLogout">Logout</v-btn>
|
||||
<v-btn to="/user">home</v-btn>
|
||||
</div>
|
||||
</main>
|
||||
</template>
|
||||
|
||||
|
|
|
@ -2,10 +2,11 @@
|
|||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import LearningPathsGrid from "@/components/LearningPathsGrid.vue";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import { useGetAllLearningPathsByThemeQuery } from "@/queries/learning-paths.ts";
|
||||
import { useGetAllLearningPathsByThemeAndLanguageQuery } from "@/queries/learning-paths.ts";
|
||||
import { computed, ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useThemeQuery } from "@/queries/themes.ts";
|
||||
import type { Language } from "@/data-objects/language";
|
||||
|
||||
const props = defineProps<{ theme: string }>();
|
||||
|
||||
|
@ -16,7 +17,10 @@
|
|||
|
||||
const currentThemeInfo = computed(() => themeQueryResult.data.value?.find((it) => it.key === props.theme));
|
||||
|
||||
const learningPathsForThemeQueryResult = useGetAllLearningPathsByThemeQuery(() => props.theme);
|
||||
const learningPathsForThemeQueryResult = useGetAllLearningPathsByThemeAndLanguageQuery(
|
||||
() => props.theme,
|
||||
() => locale.value as Language,
|
||||
);
|
||||
|
||||
const { t } = useI18n();
|
||||
const searchFilter = ref("");
|
||||
|
@ -31,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"
|
||||
|
@ -56,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>
|
||||
|
|
|
@ -1,72 +1,29 @@
|
|||
<script setup lang="ts">
|
||||
import auth from "@/services/auth/auth-service.ts";
|
||||
import { computed, type Ref, ref, watchEffect } from "vue";
|
||||
import { computed, ref } from "vue";
|
||||
import StudentAssignment from "@/views/assignments/StudentAssignment.vue";
|
||||
import TeacherAssignment from "@/views/assignments/TeacherAssignment.vue";
|
||||
import { useRoute } from "vue-router";
|
||||
import type { Language } from "@/data-objects/language.ts";
|
||||
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);
|
||||
const assignmentId = ref(Number(route.params.id));
|
||||
|
||||
function useGroupsWithProgress(
|
||||
groups: Ref<GroupDTO[]>,
|
||||
hruid: Ref<string>,
|
||||
language: Ref<string>,
|
||||
): { groupProgressMap: Map<number, number> } {
|
||||
const groupProgressMap: Map<number, number> = new Map<number, number>();
|
||||
|
||||
watchEffect(() => {
|
||||
// Clear existing entries to avoid stale data
|
||||
groupProgressMap.clear();
|
||||
|
||||
const lang = ref(language.value as Language);
|
||||
|
||||
groups.value.forEach((group) => {
|
||||
const groupKey = group.groupNumber;
|
||||
const forGroup = ref({
|
||||
forGroup: groupKey,
|
||||
assignmentNo: assignmentId,
|
||||
classId: classId,
|
||||
});
|
||||
|
||||
const query = useGetLearningPathQuery(hruid.value, lang, forGroup);
|
||||
|
||||
const data = query.data.value;
|
||||
|
||||
groupProgressMap.set(groupKey, data ? calculateProgress(data) : 0);
|
||||
});
|
||||
});
|
||||
|
||||
return {
|
||||
groupProgressMap,
|
||||
};
|
||||
}
|
||||
|
||||
function calculateProgress(lp: LearningPath): number {
|
||||
return ((lp.amountOfNodes - lp.amountOfNodesLeft) / lp.amountOfNodes) * 100;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<TeacherAssignment
|
||||
:class-id="classId"
|
||||
:assignment-id="assignmentId"
|
||||
:use-groups-with-progress="useGroupsWithProgress"
|
||||
v-if="isTeacher"
|
||||
>
|
||||
</TeacherAssignment>
|
||||
<StudentAssignment
|
||||
:class-id="classId"
|
||||
:assignment-id="assignmentId"
|
||||
:use-groups-with-progress="useGroupsWithProgress"
|
||||
v-else
|
||||
>
|
||||
</StudentAssignment>
|
||||
|
|
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>
|
|
@ -13,6 +13,7 @@
|
|||
import { useCreateTeacherInvitationMutation } from "@/queries/teacher-invitations";
|
||||
import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation";
|
||||
import { useDisplay } from "vuetify";
|
||||
import "../../assets/common.css";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -76,7 +77,7 @@
|
|||
},
|
||||
onError: (e) => {
|
||||
dialog.value = false;
|
||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
||||
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -104,7 +105,7 @@
|
|||
}
|
||||
},
|
||||
onError: (e) => {
|
||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
||||
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -112,7 +113,7 @@
|
|||
|
||||
function sentInvite(): void {
|
||||
if (!usernameTeacher.value) {
|
||||
showSnackbar(t("please enter a valid username"), "error");
|
||||
showSnackbar(t("valid-username"), "error");
|
||||
return;
|
||||
}
|
||||
const data: TeacherInvitationData = {
|
||||
|
@ -125,7 +126,7 @@
|
|||
usernameTeacher.value = "";
|
||||
},
|
||||
onError: (e) => {
|
||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
||||
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -186,7 +187,7 @@
|
|||
v-slot="classResponse: { data: ClassResponse }"
|
||||
>
|
||||
<div>
|
||||
<h1 class="title">{{ classResponse.data.class.displayName }}</h1>
|
||||
<h1 class="h1">{{ classResponse.data.class.displayName }}</h1>
|
||||
<using-query-result
|
||||
:query-result="getStudents"
|
||||
v-slot="studentsResponse: { data: StudentsResponse }"
|
||||
|
@ -211,16 +212,31 @@
|
|||
<th class="header"></th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
|
||||
<tbody v-if="studentsResponse.data.students.length">
|
||||
<tr
|
||||
v-for="s in studentsResponse.data.students as StudentDTO[]"
|
||||
:key="s.id"
|
||||
>
|
||||
<td>{{ s.firstName + " " + s.lastName }}</td>
|
||||
<td>
|
||||
{{ s.firstName + " " + s.lastName }}
|
||||
<v-btn @click="showPopup(s)">{{ t("remove") }}</v-btn>
|
||||
</td>
|
||||
<td>
|
||||
<v-btn @click="showPopup(s)"> {{ t("remove") }} </v-btn>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
||||
<tbody v-else>
|
||||
<tr>
|
||||
<td
|
||||
colspan="2"
|
||||
class="empty-message"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-information-outline"
|
||||
size="small"
|
||||
>
|
||||
</v-icon>
|
||||
{{ t("no-students-found") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
|
@ -242,7 +258,7 @@
|
|||
<th class="header">{{ t("accept") + "/" + t("reject") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody v-if="joinRequests.data.joinRequests.length">
|
||||
<tr
|
||||
v-for="jr in joinRequests.data.joinRequests as ClassJoinRequestDTO[]"
|
||||
:key="(jr.class, jr.requester, jr.status)"
|
||||
|
@ -287,6 +303,21 @@
|
|||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr>
|
||||
<td
|
||||
colspan="2"
|
||||
class="empty-message"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-information-outline"
|
||||
size="small"
|
||||
>
|
||||
</v-icon>
|
||||
{{ t("no-join-requests-found") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-col>
|
||||
</using-query-result>
|
||||
|
@ -356,49 +387,6 @@
|
|||
</main>
|
||||
</template>
|
||||
<style scoped>
|
||||
.header {
|
||||
font-weight: bold !important;
|
||||
background-color: #0e6942;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table thead th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
.table thead th:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(odd) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(even) {
|
||||
background-color: #f6faf2;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-bottom: 1px solid #0e6942;
|
||||
border-top: 1px solid #0e6942;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 90%;
|
||||
padding-top: 10px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
padding-top: 2%;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #0e6942;
|
||||
font-size: 30px;
|
||||
|
@ -407,6 +395,7 @@
|
|||
.join {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
margin-left: 1%;
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
}
|
||||
|
@ -416,16 +405,7 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.join {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
|
|
|
@ -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";
|
||||
|
@ -12,8 +12,10 @@
|
|||
import { useClassStudentsQuery, useClassTeachersQuery } from "@/queries/classes";
|
||||
import type { StudentsResponse } from "@/controllers/students";
|
||||
import type { TeachersResponse } from "@/controllers/teachers";
|
||||
import "../../assets/common.css";
|
||||
|
||||
const { t } = useI18n();
|
||||
const route = useRoute();
|
||||
|
||||
// Username of logged in student
|
||||
const username = ref<string | undefined>(undefined);
|
||||
|
@ -37,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
|
||||
|
@ -74,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");
|
||||
|
@ -91,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 },
|
||||
{
|
||||
|
@ -99,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");
|
||||
},
|
||||
},
|
||||
);
|
||||
|
@ -135,7 +146,7 @@
|
|||
></v-empty-state>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 class="title">{{ t("classes") }}</h1>
|
||||
<h1 class="h1">{{ t("classes") }}</h1>
|
||||
<using-query-result
|
||||
:query-result="classesQuery"
|
||||
v-slot="classResponse: { data: ClassesResponse }"
|
||||
|
@ -161,7 +172,7 @@
|
|||
<th class="header">{{ t("members") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody v-if="classResponse.data.classes.length">
|
||||
<tr
|
||||
v-for="c in classResponse.data.classes as ClassDTO[]"
|
||||
:key="c.id"
|
||||
|
@ -181,6 +192,21 @@
|
|||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr>
|
||||
<td
|
||||
colspan="3"
|
||||
class="empty-message"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-information-outline"
|
||||
size="small"
|
||||
>
|
||||
</v-icon>
|
||||
{{ t("no-classes-found") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-col>
|
||||
</v-row>
|
||||
|
@ -244,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>
|
||||
|
@ -271,49 +297,6 @@
|
|||
</main>
|
||||
</template>
|
||||
<style scoped>
|
||||
.header {
|
||||
font-weight: bold !important;
|
||||
background-color: #0e6942;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table thead th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
.table thead th:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(odd) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(even) {
|
||||
background-color: #f6faf2;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-bottom: 1px solid #0e6942;
|
||||
border-top: 1px solid #0e6942;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 90%;
|
||||
padding-top: 10px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
padding-top: 2%;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #0e6942;
|
||||
font-size: 30px;
|
||||
|
@ -321,6 +304,7 @@
|
|||
|
||||
.join {
|
||||
display: flex;
|
||||
margin-left: 1%;
|
||||
flex-direction: column;
|
||||
gap: 20px;
|
||||
margin-top: 50px;
|
||||
|
@ -331,16 +315,7 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 800px) {
|
||||
h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.join {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
|
|
|
@ -8,13 +8,15 @@
|
|||
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,
|
||||
useTeacherInvitationsReceivedQuery,
|
||||
} from "@/queries/teacher-invitations";
|
||||
import { useDisplay } from "vuetify";
|
||||
import "../../assets/common.css";
|
||||
import ClassDisplay from "@/views/classes/ClassDisplay.vue";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
|
@ -40,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();
|
||||
|
@ -69,7 +70,7 @@
|
|||
await getInvitationsQuery.refetch();
|
||||
},
|
||||
onError: (e) => {
|
||||
showSnackbar(t("failed") + ": " + e.message, "error");
|
||||
showSnackbar(t("failed") + ": " + e.response.data.error || e.message, "error");
|
||||
},
|
||||
});
|
||||
}
|
||||
|
@ -112,7 +113,7 @@
|
|||
dialog.value = true;
|
||||
}
|
||||
if (!className.value || className.value === "") {
|
||||
showSnackbar(t("name is mandatory"), "error");
|
||||
showSnackbar(t("nameIsMandatory"), "error");
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -131,10 +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;
|
||||
|
||||
if (!isDialog) showSnackbar(t("copied"), "white");
|
||||
}
|
||||
|
||||
// Custom breakpoints
|
||||
|
@ -162,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;
|
||||
|
@ -183,7 +187,7 @@
|
|||
></v-empty-state>
|
||||
</div>
|
||||
<div v-else>
|
||||
<h1 class="title">{{ t("classes") }}</h1>
|
||||
<h1 class="h1">{{ t("classes") }}</h1>
|
||||
<using-query-result
|
||||
:query-result="classesQuery"
|
||||
v-slot="classesResponse: { data: ClassesResponse }"
|
||||
|
@ -212,7 +216,7 @@
|
|||
<th class="header">{{ t("members") }}</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tbody v-if="classesResponse.data.classes.length">
|
||||
<tr
|
||||
v-for="c in classesResponse.data.classes as ClassDTO[]"
|
||||
:key="c.id"
|
||||
|
@ -223,22 +227,58 @@
|
|||
variant="text"
|
||||
>
|
||||
{{ c.displayName }}
|
||||
<v-icon end> mdi-menu-right </v-icon>
|
||||
<v-icon end> mdi-menu-right</v-icon>
|
||||
</v-btn>
|
||||
</td>
|
||||
<td>
|
||||
<span v-if="!isMdAndDown">{{ c.id }}</span>
|
||||
<v-row
|
||||
v-if="!isMdAndDown"
|
||||
dense
|
||||
align="center"
|
||||
no-gutters
|
||||
>
|
||||
<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>
|
||||
</tr>
|
||||
</tbody>
|
||||
<tbody v-else>
|
||||
<tr>
|
||||
<td
|
||||
colspan="3"
|
||||
class="empty-message"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-information-outline"
|
||||
size="small"
|
||||
>
|
||||
</v-icon>
|
||||
{{ t("no-classes-found") }}
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</v-table>
|
||||
</v-col>
|
||||
<v-col
|
||||
|
@ -270,8 +310,8 @@
|
|||
type="submit"
|
||||
@click="createClass"
|
||||
block
|
||||
>{{ t("create") }}</v-btn
|
||||
>
|
||||
>{{ t("create") }}
|
||||
</v-btn>
|
||||
</v-form>
|
||||
</v-sheet>
|
||||
<v-container>
|
||||
|
@ -280,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"
|
||||
|
@ -318,7 +373,7 @@
|
|||
</v-container>
|
||||
</using-query-result>
|
||||
|
||||
<h1 class="title">
|
||||
<h1 class="h1">
|
||||
{{ t("invitations") }}
|
||||
</h1>
|
||||
<v-container
|
||||
|
@ -338,20 +393,13 @@
|
|||
: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"
|
||||
>
|
||||
<td>
|
||||
{{
|
||||
(classesResponse.data.classes as ClassDTO[]).filter(
|
||||
(c) => c.id == i.classId,
|
||||
)[0].displayName
|
||||
}}
|
||||
<ClassDisplay :classId="i.classId" />
|
||||
</td>
|
||||
<td>
|
||||
{{
|
||||
|
@ -393,11 +441,27 @@
|
|||
color="red"
|
||||
variant="text"
|
||||
>
|
||||
</v-btn></div
|
||||
></span>
|
||||
</v-btn>
|
||||
</div>
|
||||
</span>
|
||||
</td>
|
||||
</tr>
|
||||
</using-query-result>
|
||||
</template>
|
||||
<template v-else>
|
||||
<tr>
|
||||
<td
|
||||
colspan="3"
|
||||
class="empty-message"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-information-outline"
|
||||
size="small"
|
||||
>
|
||||
</v-icon>
|
||||
{{ t("no-invitations-found") }}
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
</using-query-result>
|
||||
</tbody>
|
||||
</v-table>
|
||||
|
@ -420,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"
|
||||
|
@ -449,49 +528,6 @@
|
|||
</main>
|
||||
</template>
|
||||
<style scoped>
|
||||
.header {
|
||||
font-weight: bold !important;
|
||||
background-color: #0e6942;
|
||||
color: white;
|
||||
padding: 10px;
|
||||
}
|
||||
|
||||
table thead th:first-child {
|
||||
border-top-left-radius: 10px;
|
||||
}
|
||||
|
||||
.table thead th:last-child {
|
||||
border-top-right-radius: 10px;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(odd) {
|
||||
background-color: white;
|
||||
}
|
||||
|
||||
.table tbody tr:nth-child(even) {
|
||||
background-color: #f6faf2;
|
||||
}
|
||||
|
||||
td,
|
||||
th {
|
||||
border-bottom: 1px solid #0e6942;
|
||||
border-top: 1px solid #0e6942;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 90%;
|
||||
padding-top: 10px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
padding-top: 2%;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #0e6942;
|
||||
font-size: 30px;
|
||||
|
@ -509,16 +545,7 @@
|
|||
text-decoration: underline;
|
||||
}
|
||||
|
||||
main {
|
||||
margin-left: 30px;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 850px) {
|
||||
h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
|
||||
.join {
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
|
@ -541,10 +568,6 @@
|
|||
flex-direction: column !important;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.responsive-col {
|
||||
max-width: 100% !important;
|
||||
flex-basis: 100% !important;
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -3,6 +3,7 @@
|
|||
import { useI18n } from "vue-i18n";
|
||||
import { THEMESITEMS, AGE_TO_THEMES } from "@/utils/constants.ts";
|
||||
import BrowseThemes from "@/components/BrowseThemes.vue";
|
||||
import "../../assets/common.css";
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
|
||||
|
@ -46,7 +47,7 @@
|
|||
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<h1 class="title">{{ t("themes") }}</h1>
|
||||
<h1 class="h1">{{ t("themes") }}</h1>
|
||||
<v-container class="dropdowns">
|
||||
<v-select
|
||||
class="v-select"
|
||||
|
@ -77,24 +78,6 @@
|
|||
</template>
|
||||
|
||||
<style scoped>
|
||||
.main-container {
|
||||
min-height: 100vh;
|
||||
min-width: 100vw;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: flex-start;
|
||||
justify-content: flex-start;
|
||||
}
|
||||
|
||||
.title {
|
||||
max-width: 50rem;
|
||||
margin-left: 1rem;
|
||||
margin-top: 1rem;
|
||||
text-align: center;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.dropdowns {
|
||||
display: flex;
|
||||
justify-content: space-between;
|
||||
|
@ -107,12 +90,6 @@
|
|||
min-width: 100px;
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.main-container {
|
||||
padding: 1rem;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.dropdowns {
|
||||
flex-direction: column;
|
||||
|
|
|
@ -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,9 +248,9 @@
|
|||
v-model="forGroupQueryParam"
|
||||
/>
|
||||
</template>
|
||||
</v-list-item>
|
||||
</v-list-itemF>
|
||||
<v-divider></v-divider>
|
||||
<div v-if="props.learningObjectHruid">
|
||||
<div>
|
||||
<using-query-result
|
||||
:query-result="learningObjectListQueryResult"
|
||||
v-slot="learningObjects: { data: LearningObject[] }"
|
||||
|
@ -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,10 +288,12 @@
|
|||
</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"
|
||||
width="100%"
|
||||
:color="COLORS.teacherExclusive"
|
||||
@click="assign()"
|
||||
>{{ t("assignLearningPath") }}</v-btn
|
||||
>
|
||||
|
@ -294,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") }}
|
||||
|
@ -323,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,32 +17,37 @@
|
|||
</script>
|
||||
|
||||
<template>
|
||||
<div class="search-field-container">
|
||||
<learning-path-search-field class="search-field"></learning-path-search-field>
|
||||
</div>
|
||||
<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[] }"
|
||||
>
|
||||
<learning-paths-grid :learning-paths="data"></learning-paths-grid>
|
||||
</using-query-result>
|
||||
<div content="empty-state-container">
|
||||
<v-empty-state
|
||||
<using-query-result
|
||||
:query-result="searchQueryResults"
|
||||
v-slot="{ data }: { data: LearningPath[] }"
|
||||
>
|
||||
<learning-paths-grid :learning-paths="data" />
|
||||
</using-query-result>
|
||||
|
||||
<div
|
||||
v-if="!query"
|
||||
icon="mdi-magnify"
|
||||
:title="t('enterSearchTerm')"
|
||||
:text="t('enterSearchTermDescription')"
|
||||
></v-empty-state>
|
||||
class="empty-state-container"
|
||||
>
|
||||
<v-empty-state
|
||||
icon="mdi-magnify"
|
||||
:title="t('enterSearchTerm')"
|
||||
:text="t('enterSearchTermDescription')"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.search-field-container {
|
||||
display: block;
|
||||
margin: 20px;
|
||||
.search-page-container {
|
||||
padding-top: 40px;
|
||||
padding-bottom: 40px;
|
||||
}
|
||||
.search-field {
|
||||
max-width: 300px;
|
||||
.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) {
|
||||
|
|
|
@ -0,0 +1,81 @@
|
|||
<script setup lang="ts">
|
||||
import { useLearningObjectListForAdminQuery } from "@/queries/learning-objects.ts";
|
||||
import OwnLearningObjectsView from "@/views/own-learning-content/learning-objects/OwnLearningObjectsView.vue";
|
||||
import OwnLearningPathsView from "@/views/own-learning-content/learning-paths/OwnLearningPathsView.vue";
|
||||
import authService from "@/services/auth/auth-service.ts";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import type { LearningObject } from "@/data-objects/learning-objects/learning-object";
|
||||
import { ref, type Ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { useGetAllLearningPathsByAdminQuery } from "@/queries/learning-paths";
|
||||
import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const learningObjectsQuery = useLearningObjectListForAdminQuery(
|
||||
authService.authState.user?.profile.preferred_username,
|
||||
);
|
||||
|
||||
const learningPathsQuery = useGetAllLearningPathsByAdminQuery(
|
||||
authService.authState.user?.profile.preferred_username,
|
||||
);
|
||||
|
||||
type Tab = "learningObjects" | "learningPaths";
|
||||
const tab: Ref<Tab> = ref("learningObjects");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="tab-pane-container">
|
||||
<h1 class="title">{{ t("ownLearningContentTitle") }}</h1>
|
||||
|
||||
<v-tabs v-model="tab">
|
||||
<v-tab value="learningObjects">{{ t("learningObjects") }}</v-tab>
|
||||
<v-tab value="learningPaths">{{ t("learningPaths") }}</v-tab>
|
||||
</v-tabs>
|
||||
|
||||
<v-tabs-window
|
||||
v-model="tab"
|
||||
class="main-content"
|
||||
>
|
||||
<v-tabs-window-item
|
||||
value="learningObjects"
|
||||
class="main-content"
|
||||
>
|
||||
<using-query-result
|
||||
:query-result="learningObjectsQuery"
|
||||
v-slot="response: { data: LearningObject[] }"
|
||||
>
|
||||
<own-learning-objects-view :learningObjects="response.data"></own-learning-objects-view>
|
||||
</using-query-result>
|
||||
</v-tabs-window-item>
|
||||
<v-tabs-window-item value="learningPaths">
|
||||
<using-query-result
|
||||
:query-result="learningPathsQuery"
|
||||
v-slot="response: { data: LearningPathDTO[] }"
|
||||
>
|
||||
<own-learning-paths-view :learningPaths="response.data" />
|
||||
</using-query-result>
|
||||
</v-tabs-window-item>
|
||||
</v-tabs-window>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
font-size: 50px;
|
||||
}
|
||||
|
||||
.tab-pane-container {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
padding: 20px 30px;
|
||||
}
|
||||
.main-content {
|
||||
flex: 1 1;
|
||||
height: 100%;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,59 @@
|
|||
<script setup lang="ts">
|
||||
import type { LearningObject } from "@/data-objects/learning-objects/learning-object";
|
||||
import UsingQueryResult from "@/components/UsingQueryResult.vue";
|
||||
import LearningObjectContentView from "../../learning-paths/learning-object/content/LearningObjectContentView.vue";
|
||||
import ButtonWithConfirmation from "@/components/ButtonWithConfirmation.vue";
|
||||
import { useDeleteLearningObjectMutation, useLearningObjectHTMLQuery } from "@/queries/learning-objects";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
selectedLearningObject?: LearningObject;
|
||||
}>();
|
||||
|
||||
const learningObjectQueryResult = useLearningObjectHTMLQuery(
|
||||
() => props.selectedLearningObject?.key,
|
||||
() => props.selectedLearningObject?.language,
|
||||
() => props.selectedLearningObject?.version,
|
||||
);
|
||||
|
||||
const { isPending, mutate } = useDeleteLearningObjectMutation();
|
||||
|
||||
function deleteLearningObject(): void {
|
||||
if (props.selectedLearningObject) {
|
||||
mutate({
|
||||
hruid: props.selectedLearningObject.key,
|
||||
language: props.selectedLearningObject.language,
|
||||
version: props.selectedLearningObject.version,
|
||||
});
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card
|
||||
v-if="selectedLearningObject"
|
||||
:title="t('previewFor') + selectedLearningObject.title"
|
||||
>
|
||||
<template v-slot:text>
|
||||
<using-query-result
|
||||
:query-result="learningObjectQueryResult"
|
||||
v-slot="response: { data: Document }"
|
||||
>
|
||||
<learning-object-content-view :learning-object-content="response.data"></learning-object-content-view>
|
||||
</using-query-result>
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<button-with-confirmation
|
||||
@confirm="deleteLearningObject"
|
||||
prepend-icon="mdi mdi-delete"
|
||||
color="red"
|
||||
:text="t('delete')"
|
||||
:confirmQueryText="t('learningObjectDeleteQuery')"
|
||||
/>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,82 @@
|
|||
<script setup lang="ts">
|
||||
import { useUploadLearningObjectMutation } from "@/queries/learning-objects";
|
||||
import { ref, watch, type Ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { VFileUpload } from "vuetify/labs/VFileUpload";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const dialogOpen = ref(false);
|
||||
|
||||
interface ContainsErrorString {
|
||||
error: string;
|
||||
}
|
||||
|
||||
const fileToUpload: Ref<File | undefined> = ref(undefined);
|
||||
|
||||
const { isPending, error, isError, isSuccess, mutate } = useUploadLearningObjectMutation();
|
||||
|
||||
watch(isSuccess, (newIsSuccess) => {
|
||||
if (newIsSuccess) {
|
||||
dialogOpen.value = false;
|
||||
fileToUpload.value = undefined;
|
||||
}
|
||||
});
|
||||
|
||||
function uploadFile() {
|
||||
if (fileToUpload.value) {
|
||||
mutate({ learningObjectZip: fileToUpload.value });
|
||||
}
|
||||
}
|
||||
</script>
|
||||
<template>
|
||||
<v-dialog
|
||||
max-width="500"
|
||||
v-model="dialogOpen"
|
||||
>
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
prepend-icon="mdi mdi-plus"
|
||||
:text="t('newLearningObject')"
|
||||
v-bind="activatorProps"
|
||||
color="rgb(14, 105, 66)"
|
||||
size="large"
|
||||
>
|
||||
</v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:default="{ isActive }">
|
||||
<v-card :title="t('learningObjectUploadTitle')">
|
||||
<v-card-text>
|
||||
<v-file-upload
|
||||
icon="mdi-upload"
|
||||
v-model="fileToUpload"
|
||||
:disabled="isPending"
|
||||
></v-file-upload>
|
||||
<v-alert
|
||||
v-if="error"
|
||||
icon="mdi mdi-alert-circle"
|
||||
type="error"
|
||||
:title="t('uploadFailed')"
|
||||
:text="t((error.response?.data as ContainsErrorString).error ?? error.message)"
|
||||
></v-alert>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
<v-btn
|
||||
:text="t('cancel')"
|
||||
@click="isActive.value = false"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
:text="t('upload')"
|
||||
@click="uploadFile()"
|
||||
:loading="isPending"
|
||||
:disabled="!fileToUpload"
|
||||
></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
</template>
|
||||
<style scoped></style>
|
|
@ -0,0 +1,79 @@
|
|||
<script setup lang="ts">
|
||||
import type { LearningObject } from "@/data-objects/learning-objects/learning-object";
|
||||
import LearningObjectUploadButton from "@/views/own-learning-content/learning-objects/LearningObjectUploadButton.vue";
|
||||
import LearningObjectPreviewCard from "./LearningObjectPreviewCard.vue";
|
||||
import { computed, ref, watch, type Ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
learningObjects: LearningObject[];
|
||||
}>();
|
||||
|
||||
const tableHeaders = [
|
||||
{ title: t("hruid"), width: "250px", key: "key" },
|
||||
{ title: t("language"), width: "50px", key: "language" },
|
||||
{ title: t("version"), width: "50px", key: "version" },
|
||||
{ title: t("title"), key: "title" },
|
||||
];
|
||||
|
||||
const selectedLearningObjects: Ref<LearningObject[]> = ref([]);
|
||||
|
||||
watch(
|
||||
() => props.learningObjects,
|
||||
() => (selectedLearningObjects.value = []),
|
||||
);
|
||||
|
||||
const selectedLearningObject = computed(() =>
|
||||
selectedLearningObjects.value ? selectedLearningObjects.value[0] : undefined,
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="root">
|
||||
<div class="table-container">
|
||||
<learning-object-upload-button />
|
||||
<v-data-table
|
||||
class="table"
|
||||
v-model="selectedLearningObjects"
|
||||
:items="props.learningObjects"
|
||||
:headers="tableHeaders"
|
||||
select-strategy="single"
|
||||
show-select
|
||||
return-object
|
||||
/>
|
||||
</div>
|
||||
<div
|
||||
class="preview-container"
|
||||
v-if="selectedLearningObject"
|
||||
>
|
||||
<learning-object-preview-card
|
||||
class="preview"
|
||||
:selectedLearningObject="selectedLearningObject"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.root {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.preview-container {
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
}
|
||||
.table-container {
|
||||
flex: 1;
|
||||
}
|
||||
.preview {
|
||||
width: 100%;
|
||||
}
|
||||
.table {
|
||||
width: 100%;
|
||||
margin-top: 20px;
|
||||
}
|
||||
</style>
|
|
@ -0,0 +1,147 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
import { computed, ref, watch, type Ref } from "vue";
|
||||
import JsonEditorVue from "json-editor-vue";
|
||||
import ButtonWithConfirmation from "@/components/ButtonWithConfirmation.vue";
|
||||
import {
|
||||
useDeleteLearningPathMutation,
|
||||
usePostLearningPathMutation,
|
||||
usePutLearningPathMutation,
|
||||
} from "@/queries/learning-paths";
|
||||
import { Language } from "@/data-objects/language";
|
||||
import type { LearningPath } from "@dwengo-1/common/interfaces/learning-content";
|
||||
import type { AxiosError } from "axios";
|
||||
import { parse } from "uuid";
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
selectedLearningPath?: LearningPath;
|
||||
}>();
|
||||
|
||||
const { isPending, mutate, error: deleteError, isSuccess: deleteSuccess } = useDeleteLearningPathMutation();
|
||||
|
||||
const DEFAULT_LEARNING_PATH: Partial<LearningPath> = {
|
||||
language: "en",
|
||||
hruid: "...",
|
||||
title: "...",
|
||||
description: "...",
|
||||
nodes: [
|
||||
{
|
||||
learningobject_hruid: "...",
|
||||
language: Language.English,
|
||||
version: 1,
|
||||
start_node: true,
|
||||
transitions: [
|
||||
{
|
||||
default: true,
|
||||
condition: t("hintRemoveIfUnconditionalTransition"),
|
||||
next: {
|
||||
hruid: "...",
|
||||
version: 1,
|
||||
language: "...",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const { isPending: isPostPending, error: postError, mutate: doPost } = usePostLearningPathMutation();
|
||||
const { isPending: isPutPending, error: putError, mutate: doPut } = usePutLearningPathMutation();
|
||||
|
||||
const learningPath: Ref<Partial<LearningPath> | string> = ref(DEFAULT_LEARNING_PATH);
|
||||
|
||||
const parsedLearningPath = computed(() =>
|
||||
typeof learningPath.value === "string" ? (JSON.parse(learningPath.value) as LearningPath) : learningPath.value,
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.selectedLearningPath,
|
||||
() => (learningPath.value = props.selectedLearningPath ?? DEFAULT_LEARNING_PATH),
|
||||
);
|
||||
|
||||
function uploadLearningPath(): void {
|
||||
if (props.selectedLearningPath) {
|
||||
doPut({ learningPath: parsedLearningPath.value });
|
||||
} else {
|
||||
doPost({ learningPath: parsedLearningPath.value });
|
||||
}
|
||||
}
|
||||
|
||||
function deleteLearningPath(): void {
|
||||
if (props.selectedLearningPath) {
|
||||
mutate({
|
||||
hruid: props.selectedLearningPath.hruid,
|
||||
language: props.selectedLearningPath.language as Language,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
function extractErrorMessage(error: AxiosError): string {
|
||||
return (error.response?.data as { error: string }).error ?? error.message;
|
||||
}
|
||||
|
||||
const isIdModified = computed(
|
||||
() =>
|
||||
props.selectedLearningPath !== undefined &&
|
||||
(props.selectedLearningPath.hruid !== parsedLearningPath.value.hruid ||
|
||||
props.selectedLearningPath.language !== parsedLearningPath.value.language),
|
||||
);
|
||||
|
||||
function getErrorMessage(): string | null {
|
||||
if (postError.value) {
|
||||
return t(extractErrorMessage(postError.value));
|
||||
} else if (putError.value) {
|
||||
return t(extractErrorMessage(putError.value));
|
||||
} else if (deleteError.value) {
|
||||
return t(extractErrorMessage(deleteError.value));
|
||||
} else if (isIdModified.value) {
|
||||
return t("learningPathCantModifyId");
|
||||
}
|
||||
return null;
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-card :title="props.selectedLearningPath ? t('editLearningPath') : t('newLearningPath')">
|
||||
<template v-slot:text>
|
||||
<json-editor-vue v-model="learningPath"></json-editor-vue>
|
||||
<v-alert
|
||||
v-if="postError || putError || deleteError || isIdModified"
|
||||
icon="mdi mdi-alert-circle"
|
||||
type="error"
|
||||
:title="t('error')"
|
||||
:text="getErrorMessage()!"
|
||||
></v-alert>
|
||||
</template>
|
||||
<template v-slot:actions>
|
||||
<v-btn
|
||||
@click="uploadLearningPath"
|
||||
prependIcon="mdi mdi-check"
|
||||
:loading="isPostPending || isPutPending"
|
||||
:disabled="parsedLearningPath.hruid === DEFAULT_LEARNING_PATH.hruid || isIdModified"
|
||||
>
|
||||
{{ props.selectedLearningPath ? t("saveChanges") : t("create") }}
|
||||
</v-btn>
|
||||
<button-with-confirmation
|
||||
@confirm="deleteLearningPath"
|
||||
:disabled="!props.selectedLearningPath"
|
||||
:text="t('delete')"
|
||||
color="red"
|
||||
prependIcon="mdi mdi-delete"
|
||||
:confirmQueryText="t('learningPathDeleteQuery')"
|
||||
/>
|
||||
<v-btn
|
||||
:href="`/learningPath/${props.selectedLearningPath?.hruid}/${props.selectedLearningPath?.language}/start`"
|
||||
target="_blank"
|
||||
prepend-icon="mdi mdi-open-in-new"
|
||||
:disabled="!props.selectedLearningPath"
|
||||
>
|
||||
{{ t("open") }}
|
||||
</v-btn>
|
||||
</template>
|
||||
</v-card>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
|
@ -0,0 +1,77 @@
|
|||
<script setup lang="ts">
|
||||
import LearningPathPreviewCard from "./LearningPathPreviewCard.vue";
|
||||
import { computed, ref, watch, type Ref } from "vue";
|
||||
import { useI18n } from "vue-i18n";
|
||||
import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
|
||||
const { t } = useI18n();
|
||||
const props = defineProps<{
|
||||
learningPaths: LearningPathDTO[];
|
||||
}>();
|
||||
|
||||
const tableHeaders = [
|
||||
{ title: t("hruid"), width: "250px", key: "hruid" },
|
||||
{ title: t("language"), width: "50px", key: "language" },
|
||||
{ title: t("title"), key: "title" },
|
||||
];
|
||||
|
||||
const selectedLearningPaths: Ref<LearningPathDTO[]> = ref([]);
|
||||
|
||||
const selectedLearningPath = computed(() =>
|
||||
selectedLearningPaths.value ? selectedLearningPaths.value[0] : undefined,
|
||||
);
|
||||
|
||||
watch(
|
||||
() => props.learningPaths,
|
||||
() => (selectedLearningPaths.value = []),
|
||||
);
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="root">
|
||||
<div class="table-container">
|
||||
<v-data-table
|
||||
class="table"
|
||||
v-model="selectedLearningPaths"
|
||||
:items="props.learningPaths"
|
||||
:headers="tableHeaders"
|
||||
select-strategy="single"
|
||||
show-select
|
||||
return-object
|
||||
/>
|
||||
</div>
|
||||
<div class="preview-container">
|
||||
<learning-path-preview-card
|
||||
class="preview"
|
||||
:selectedLearningPath="selectedLearningPath"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
.fab {
|
||||
position: absolute;
|
||||
right: 20px;
|
||||
bottom: 20px;
|
||||
}
|
||||
.root {
|
||||
display: flex;
|
||||
gap: 20px;
|
||||
padding: 20px;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
.preview-container {
|
||||
flex: 1;
|
||||
min-width: 400px;
|
||||
}
|
||||
.table-container {
|
||||
flex: 1;
|
||||
}
|
||||
.preview {
|
||||
width: 100%;
|
||||
}
|
||||
.table {
|
||||
width: 100%;
|
||||
}
|
||||
</style>
|
Loading…
Add table
Add a link
Reference in a new issue