Merge branch 'dev' into feat/discussions
This commit is contained in:
commit
edc52a559c
181 changed files with 7820 additions and 1515 deletions
|
@ -3,22 +3,28 @@ FROM node:22 AS build-stage
|
|||
# install simple http server for serving static content
|
||||
RUN npm install -g http-server
|
||||
|
||||
WORKDIR /app
|
||||
WORKDIR /app/dwengo
|
||||
|
||||
# Install dependencies
|
||||
|
||||
COPY package*.json ./
|
||||
COPY ./frontend/package.json ./frontend/
|
||||
# Frontend depends on common
|
||||
COPY common/package.json ./common/
|
||||
|
||||
RUN npm install --silent
|
||||
|
||||
# Build the frontend
|
||||
|
||||
# Root tsconfig.json
|
||||
COPY tsconfig.json ./
|
||||
COPY assets ./assets/
|
||||
COPY tsconfig.json tsconfig.build.json ./
|
||||
|
||||
WORKDIR /app/frontend
|
||||
COPY assets ./assets
|
||||
COPY common ./common
|
||||
|
||||
RUN npm run build --workspace=common
|
||||
|
||||
WORKDIR /app/dwengo/frontend
|
||||
|
||||
COPY frontend ./
|
||||
|
||||
|
@ -28,8 +34,8 @@ FROM nginx:stable AS production-stage
|
|||
|
||||
COPY config/nginx/nginx.conf /etc/nginx/nginx.conf
|
||||
|
||||
COPY --from=build-stage /app/assets /usr/share/nginx/html/assets
|
||||
COPY --from=build-stage /app/frontend/dist /usr/share/nginx/html
|
||||
COPY --from=build-stage /app/dwengo/assets /usr/share/nginx/html/assets
|
||||
COPY --from=build-stage /app/dwengo/frontend/dist /usr/share/nginx/html
|
||||
|
||||
EXPOSE 8080
|
||||
|
||||
|
|
81
frontend/e2e/assignments.spec.ts
Normal file
81
frontend/e2e/assignments.spec.ts
Normal file
|
@ -0,0 +1,81 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("Teacher can create new assignment", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Login
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "teacher" }).click();
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("testleerkracht1");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("password");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
// Go to assignments
|
||||
await expect(page.getByRole("banner").getByRole("link", { name: "Assignments" })).toBeVisible();
|
||||
await page.getByRole("banner").getByRole("link", { name: "Assignments" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Assignments" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "New Assignment" })).toBeVisible();
|
||||
|
||||
// Create new assignment
|
||||
await page.getByRole("button", { name: "New Assignment" }).click();
|
||||
await expect(page.getByRole("button", { name: "submit" })).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "cancel" })).toBeVisible();
|
||||
|
||||
await page.getByRole("textbox", { name: "Title Title" }).fill("Assignment test 1");
|
||||
await page.getByRole("textbox", { name: "Select a learning path Select" }).click();
|
||||
await page.getByText("Using notebooks").click();
|
||||
await page.getByRole("textbox", { name: "Pick a class Pick a class" }).click();
|
||||
await page.getByText("class01").click();
|
||||
await page.getByRole("textbox", { name: "Select Deadline Select" }).fill("2099-01-01T12:34");
|
||||
await page.getByRole("textbox", { name: "Description Description" }).fill("Assignment description");
|
||||
|
||||
await page.getByRole("button", { name: "submit" }).click();
|
||||
|
||||
await expect(page.getByText("Assignment test")).toBeVisible();
|
||||
await expect(page.getByRole("main").getByRole("button").first()).toBeVisible();
|
||||
await expect(page.getByRole("main")).toContainText("Assignment test 1");
|
||||
await expect(page.getByRole("link", { name: "Learning path" })).toBeVisible();
|
||||
await expect(page.getByRole("main")).toContainText("Assignment description");
|
||||
});
|
||||
|
||||
test("Student can see list of assignments", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Login
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "student" }).click();
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("testleerling1");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("password");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
// Go to assignments
|
||||
await expect(page.getByRole("banner").getByRole("link", { name: "Assignments" })).toBeVisible();
|
||||
await page.getByRole("banner").getByRole("link", { name: "Assignments" }).click();
|
||||
await expect(page.getByRole("heading", { name: "Assignments" })).toBeVisible();
|
||||
await expect(page.getByText("dire straits")).toBeVisible();
|
||||
await expect(page.locator(".button-row > .v-btn").first()).toBeVisible();
|
||||
await expect(page.getByText("Class: class01").first()).toBeVisible();
|
||||
});
|
||||
|
||||
test("Student can see assignment details", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Login
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "student" }).click();
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("testleerling1");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("password");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
// Go to assignments
|
||||
await expect(page.getByRole("banner").getByRole("link", { name: "Assignments" })).toBeVisible();
|
||||
await page.getByRole("banner").getByRole("link", { name: "Assignments" }).click();
|
||||
await expect(page.getByText("Assignment: Conditional")).toBeVisible();
|
||||
await expect(page.locator("div:nth-child(2) > .v-card > .button-row > .v-btn")).toBeVisible();
|
||||
|
||||
// View assignment details
|
||||
await page.locator("div:nth-child(2) > .v-card > .button-row > .v-btn").click();
|
||||
await expect(page.getByText("Assignment: Conditional")).toBeVisible();
|
||||
await expect(page.getByRole("link", { name: "Learning path" })).toBeVisible();
|
||||
await expect(page.getByRole("progressbar").locator("div").first()).toBeVisible();
|
||||
});
|
|
@ -1,8 +1,16 @@
|
|||
import { test, expect } from "./fixtures.js";
|
||||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("Users can filter", async ({ page }) => {
|
||||
await page.goto("/user");
|
||||
await page.goto("/");
|
||||
|
||||
// Login
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "teacher" }).click();
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("testleerkracht1");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("password");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
// Filter
|
||||
await page.getByRole("combobox").filter({ hasText: "Select a themeAll" }).locator("i").click();
|
||||
await page.getByText("Nature and climate").click();
|
||||
await page.getByRole("combobox").filter({ hasText: "Select ageAll agesSelect age" }).locator("i").click();
|
||||
|
|
|
@ -1,5 +0,0 @@
|
|||
import { test, expect } from "./fixtures.js";
|
||||
|
||||
test("myTest", async ({ page }) => {
|
||||
await expect(page).toHaveURL("/");
|
||||
});
|
107
frontend/e2e/class.spec.ts
Normal file
107
frontend/e2e/class.spec.ts
Normal file
|
@ -0,0 +1,107 @@
|
|||
import { test, expect } from "@playwright/test";
|
||||
|
||||
test("Teacher can create a class", async ({ page }) => {
|
||||
const className = "DeTijdLoze";
|
||||
|
||||
await page.goto("/");
|
||||
|
||||
// Login
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "teacher" }).click();
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("testleerkracht1");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("password");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
// Go to class
|
||||
await expect(page.getByRole("banner").getByRole("link", { name: "Classes" })).toBeVisible();
|
||||
await page.getByRole("banner").getByRole("link", { name: "Classes" }).click();
|
||||
|
||||
// Check if the class page is visible
|
||||
await expect(page.getByRole("heading", { name: "Classes" })).toBeVisible();
|
||||
await expect(page.getByRole("textbox", { name: "classname classname" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "create" })).toBeVisible();
|
||||
|
||||
// Create a class
|
||||
await page.getByRole("textbox", { name: "classname classname" }).click();
|
||||
await page.getByRole("textbox", { name: "classname classname" }).fill(className);
|
||||
await page.getByRole("button", { name: "create" }).click();
|
||||
|
||||
// Check if the class is created
|
||||
await expect(page.getByRole("dialog").getByText("code")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "close" })).toBeVisible();
|
||||
});
|
||||
|
||||
test("Teacher can share a class by code", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Login
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "teacher" }).click();
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("testleerkracht1");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("password");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
// Go to classes
|
||||
await expect(page.getByRole("banner").getByRole("link", { name: "Classes" })).toBeVisible();
|
||||
await page.getByRole("banner").getByRole("link", { name: "Classes" }).click();
|
||||
|
||||
await expect(page.getByRole("row", { name: "class01" }).locator("i").nth(1)).toBeVisible();
|
||||
await page.getByRole("row", { name: "class01" }).locator("i").nth(1).click();
|
||||
await expect(page.getByRole("button").filter({ hasText: /^$/ }).nth(2)).toBeVisible();
|
||||
await expect(page.getByRole("button").filter({ hasText: /^$/ }).nth(3)).toBeVisible();
|
||||
await page.getByRole("button").filter({ hasText: /^$/ }).nth(3).click();
|
||||
await expect(page.getByText("copied!")).toBeVisible();
|
||||
await page.getByRole("button", { name: "close" }).click();
|
||||
});
|
||||
|
||||
test("Student can join class by code", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Login
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "student" }).click();
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("testleerling1");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("password");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
// Go to class
|
||||
await expect(page.getByRole("banner").getByRole("link", { name: "Classes" })).toBeVisible();
|
||||
await page.getByRole("banner").getByRole("link", { name: "Classes" }).click();
|
||||
|
||||
// Check if the class page is visible
|
||||
await expect(page.getByRole("heading", { name: "Classes" })).toBeVisible();
|
||||
await expect(page.getByRole("heading", { name: "Join class" })).toBeVisible();
|
||||
await expect(page.getByRole("textbox", { name: "CODE CODE" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "submit" })).toBeVisible();
|
||||
|
||||
// Join a class
|
||||
await page.getByRole("textbox", { name: "CODE CODE" }).click();
|
||||
await page.getByRole("textbox", { name: "CODE CODE" }).fill("X2J9QT");
|
||||
await page.getByRole("button", { name: "submit" }).click();
|
||||
});
|
||||
|
||||
test("Teacher can remove student from class", async ({ page }) => {
|
||||
await page.goto("/");
|
||||
|
||||
// Login
|
||||
await page.getByRole("link", { name: "log in" }).click();
|
||||
await page.getByRole("button", { name: "teacher" }).click();
|
||||
await page.getByRole("textbox", { name: "Username or email" }).fill("testleerkracht1");
|
||||
await page.getByRole("textbox", { name: "Password" }).fill("password");
|
||||
await page.getByRole("button", { name: "Sign In" }).click();
|
||||
|
||||
await expect(page.getByRole("banner").getByRole("link", { name: "Classes" })).toBeVisible();
|
||||
await page.getByRole("banner").getByRole("link", { name: "Classes" }).click();
|
||||
await expect(page.getByRole("link", { name: "class01" })).toBeVisible();
|
||||
await expect(page.locator("#app")).toContainText("8");
|
||||
await page.getByRole("link", { name: "class01" }).click();
|
||||
await expect(page.getByRole("cell", { name: "Kurt Cobain" })).toBeVisible();
|
||||
await expect(page.getByRole("row", { name: "Kurt Cobain remove" }).getByRole("button")).toBeVisible();
|
||||
await page.getByRole("row", { name: "Kurt Cobain remove" }).getByRole("button").click();
|
||||
await expect(page.getByText("Are you sure?")).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "cancel" })).toBeVisible();
|
||||
await expect(page.getByRole("button", { name: "yes" })).toBeVisible();
|
||||
await page.getByRole("button", { name: "yes" }).click();
|
||||
await page.getByRole("banner").getByRole("link", { name: "Classes" }).click();
|
||||
await expect(page.locator("#app")).toContainText("7");
|
||||
});
|
|
@ -17,10 +17,12 @@
|
|||
"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",
|
||||
"axios": "^1.8.2",
|
||||
"json-editor-vue": "^0.18.1",
|
||||
"oidc-client-ts": "^3.1.0",
|
||||
"rollup": "^4.40.0",
|
||||
"uuid": "^11.1.0",
|
||||
|
|
54
frontend/src/assets/common.css
Normal file
54
frontend/src/assets/common.css
Normal file
|
@ -0,0 +1,54 @@
|
|||
.h1 {
|
||||
color: #0e6942;
|
||||
text-transform: uppercase;
|
||||
font-weight: bolder;
|
||||
font-size: 50px;
|
||||
padding-left: 1%;
|
||||
}
|
||||
|
||||
.empty-message {
|
||||
text-align: center;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.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;
|
||||
}
|
||||
|
||||
.table td,
|
||||
.table th {
|
||||
border-bottom: 1px solid #0e6942;
|
||||
border-top: 1px solid #0e6942;
|
||||
}
|
||||
|
||||
.table {
|
||||
width: 90%;
|
||||
padding-top: 10px;
|
||||
border-collapse: collapse;
|
||||
}
|
||||
|
||||
@media screen and (max-width: 850px) {
|
||||
.h1 {
|
||||
text-align: center;
|
||||
padding-left: 0;
|
||||
}
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
import { AGE_TO_THEMES, THEMESITEMS } from "@/utils/constants.ts";
|
||||
import { useThemeQuery } from "@/queries/themes.ts";
|
||||
import type { Theme } from "@/data-objects/theme.ts";
|
||||
import authService from "@/services/auth/auth-service";
|
||||
|
||||
const props = defineProps({
|
||||
selectedTheme: { type: String, required: true },
|
||||
|
@ -33,6 +34,8 @@
|
|||
cards.value = themes;
|
||||
}
|
||||
});
|
||||
|
||||
const isTeacher = computed(() => authService.authState.activeRole === "teacher");
|
||||
</script>
|
||||
|
||||
<template>
|
||||
|
@ -57,6 +60,39 @@
|
|||
</div>
|
||||
|
||||
<v-row v-else>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="4"
|
||||
class="d-flex"
|
||||
>
|
||||
<ThemeCard
|
||||
path="/learningPath/search"
|
||||
:is-absolute-path="true"
|
||||
:title="t('searchAllLearningPathsTitle')"
|
||||
:description="t('searchAllLearningPathsDescription')"
|
||||
icon="mdi-magnify"
|
||||
class="fill-height grey-bg-card"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-if="isTeacher"
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="4"
|
||||
class="d-flex"
|
||||
>
|
||||
<ThemeCard
|
||||
path="/my-content"
|
||||
:is-absolute-path="true"
|
||||
:title="t('ownLearningContentTitle')"
|
||||
:description="t('ownLearningContentDescription')"
|
||||
icon="mdi-pencil"
|
||||
class="fill-height grey-bg-card"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
v-for="card in cards"
|
||||
:key="card.key"
|
||||
|
@ -74,24 +110,13 @@
|
|||
class="fill-height"
|
||||
/>
|
||||
</v-col>
|
||||
<v-col
|
||||
cols="12"
|
||||
sm="6"
|
||||
md="4"
|
||||
lg="4"
|
||||
class="d-flex"
|
||||
>
|
||||
<ThemeCard
|
||||
path="/learningPath/search"
|
||||
:is-absolute-path="true"
|
||||
:title="t('searchAllLearningPathsTitle')"
|
||||
:description="t('searchAllLearningPathsDescription')"
|
||||
icon="mdi-magnify"
|
||||
class="fill-height"
|
||||
/>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
<style scoped>
|
||||
.grey-bg-card {
|
||||
background-color: #f6faf2;
|
||||
border: 2px solid #0e6942;
|
||||
}
|
||||
</style>
|
||||
|
|
63
frontend/src/components/ButtonWithConfirmation.vue
Normal file
63
frontend/src/components/ButtonWithConfirmation.vue
Normal file
|
@ -0,0 +1,63 @@
|
|||
<script setup lang="ts">
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
const props = defineProps<{
|
||||
text: string;
|
||||
prependIcon?: string;
|
||||
appendIcon?: string;
|
||||
confirmQueryText: string;
|
||||
variant?: "flat" | "text" | "elevated" | "tonal" | "outlined" | "plain" | undefined;
|
||||
color?: string;
|
||||
disabled?: boolean;
|
||||
}>();
|
||||
|
||||
const emit = defineEmits<{ (e: "confirm"): void }>();
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
function confirm(): void {
|
||||
emit("confirm");
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<v-dialog max-width="500">
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
<v-btn
|
||||
v-bind="activatorProps"
|
||||
:text="props.text"
|
||||
:prependIcon="props.prependIcon"
|
||||
:appendIcon="props.appendIcon"
|
||||
:variant="props.variant"
|
||||
:color="color"
|
||||
:disabled="props.disabled"
|
||||
></v-btn>
|
||||
</template>
|
||||
|
||||
<template v-slot:default="{ isActive }">
|
||||
<v-card :title="t('confirmDialogTitle')">
|
||||
<v-card-text>
|
||||
{{ props.confirmQueryText }}
|
||||
</v-card-text>
|
||||
|
||||
<v-card-actions>
|
||||
<v-spacer></v-spacer>
|
||||
|
||||
<v-btn
|
||||
:text="t('yes')"
|
||||
@click="
|
||||
confirm();
|
||||
isActive.value = false;
|
||||
"
|
||||
></v-btn>
|
||||
<v-btn
|
||||
:text="t('cancel')"
|
||||
@click="isActive.value = false"
|
||||
></v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
|
@ -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;
|
||||
}
|
||||
|
|
|
@ -7,13 +7,17 @@
|
|||
|
||||
// Import assets
|
||||
import dwengoLogo from "../../../assets/img/dwengo-groen-zwart.svg";
|
||||
import { useLocale } from "vuetify";
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const { current: vuetifyLocale } = useLocale();
|
||||
|
||||
const role = auth.authState.activeRole;
|
||||
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(" ")
|
||||
.map((n) => n[0])
|
||||
|
@ -30,6 +34,7 @@
|
|||
// Logic to change the language of the website to the selected language
|
||||
function changeLanguage(langCode: string): void {
|
||||
locale.value = langCode;
|
||||
vuetifyLocale.value = langCode;
|
||||
localStorage.setItem("user-lang", langCode);
|
||||
}
|
||||
|
||||
|
@ -89,31 +94,34 @@
|
|||
>
|
||||
{{ t("discussions") }}
|
||||
</v-btn>
|
||||
<v-menu open-on-hover>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon
|
||||
variant="text"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-translate"
|
||||
size="small"
|
||||
color="#0e6942"
|
||||
></v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(language, index) in languages"
|
||||
:key="index"
|
||||
@click="changeLanguage(language.code)"
|
||||
>
|
||||
<v-list-item-title>{{ language.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
</v-toolbar-items>
|
||||
<v-menu
|
||||
open-on-hover
|
||||
open-on-click
|
||||
>
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
v-bind="props"
|
||||
icon
|
||||
variant="text"
|
||||
>
|
||||
<v-icon
|
||||
icon="mdi-translate"
|
||||
size="small"
|
||||
color="#0e6942"
|
||||
></v-icon>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-list>
|
||||
<v-list-item
|
||||
v-for="(language, index) in languages"
|
||||
:key="index"
|
||||
@click="changeLanguage(language.code)"
|
||||
>
|
||||
<v-list-item-title>{{ language.name }}</v-list-item-title>
|
||||
</v-list-item>
|
||||
</v-list>
|
||||
</v-menu>
|
||||
<v-spacer></v-spacer>
|
||||
<v-dialog max-width="500">
|
||||
<template v-slot:activator="{ props: activatorProps }">
|
||||
|
@ -157,12 +165,48 @@
|
|||
</v-card>
|
||||
</template>
|
||||
</v-dialog>
|
||||
<v-avatar
|
||||
size="large"
|
||||
color="#0e6942"
|
||||
class="user-button"
|
||||
>{{ initials }}</v-avatar
|
||||
>
|
||||
<v-menu min-width="200px">
|
||||
<template v-slot:activator="{ props }">
|
||||
<v-btn
|
||||
icon
|
||||
v-bind="props"
|
||||
>
|
||||
<v-avatar
|
||||
color="#0e6942"
|
||||
size="large"
|
||||
class="user-button"
|
||||
>
|
||||
<span>{{ initials }}</span>
|
||||
</v-avatar>
|
||||
</v-btn>
|
||||
</template>
|
||||
<v-card>
|
||||
<v-card-text>
|
||||
<div class="mx-auto text-center">
|
||||
<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
|
||||
variant="text"
|
||||
rounded
|
||||
append-icon="mdi-logout"
|
||||
@click="performLogout"
|
||||
to="/login"
|
||||
>{{ t("logout") }}</v-btn
|
||||
>
|
||||
<v-divider class="my-3"></v-divider>
|
||||
</div>
|
||||
</v-card-text>
|
||||
</v-card>
|
||||
</v-menu>
|
||||
</v-app-bar>
|
||||
<v-navigation-drawer
|
||||
v-model="drawer"
|
||||
|
@ -247,6 +291,12 @@
|
|||
text-transform: none;
|
||||
}
|
||||
|
||||
.translate-button {
|
||||
z-index: 1;
|
||||
position: relative;
|
||||
margin-left: 10px;
|
||||
}
|
||||
|
||||
@media (max-width: 700px) {
|
||||
.menu {
|
||||
display: none;
|
||||
|
|
|
@ -13,6 +13,7 @@ import { LearningPathNode } from '@/data-objects/learning-paths/learning-path-no
|
|||
import { useGetLearningPathQuery } from '@/queries/learning-paths.ts';
|
||||
import { useLearningObjectListForPathQuery } from '@/queries/learning-objects.ts';
|
||||
import { useI18n } from 'vue-i18n';
|
||||
import { AccountType } from '@dwengo-1/common/src/util/account-types.ts';
|
||||
|
||||
const props = defineProps<{
|
||||
hruid: string;
|
||||
|
@ -95,7 +96,7 @@ function submitQuestion(): void {
|
|||
|
||||
<template>
|
||||
<div
|
||||
v-if="authService.authState.activeRole === 'student' && pathIsAssignment"
|
||||
v-if="authService.authState.activeRole === AccountType.Student && pathIsAssignment"
|
||||
class="question-box"
|
||||
>
|
||||
<div class="input-wrapper">
|
||||
|
|
|
@ -7,8 +7,8 @@
|
|||
import type { AnswerData, AnswerDTO } from "@dwengo-1/common/interfaces/answer";
|
||||
import authService from "@/services/auth/auth-service";
|
||||
import { useI18n } from "vue-i18n";
|
||||
|
||||
|
||||
|
||||
|
||||
const { t } = useI18n();
|
||||
|
||||
const props = defineProps<{
|
||||
|
@ -22,7 +22,7 @@
|
|||
expanded.value = !expanded.value;
|
||||
|
||||
// Scroll to the answers container if expanded
|
||||
if (expanded.value && answersContainer.value) {
|
||||
if (expanded.value && answersContainer.value) {
|
||||
setTimeout(() => {
|
||||
if (answersContainer.value) {
|
||||
answersContainer.value.scrollIntoView({
|
||||
|
@ -97,7 +97,7 @@
|
|||
{{ question.content }}
|
||||
</div>
|
||||
<div
|
||||
v-if="authService.authState.activeRole === 'teacher'"
|
||||
v-if="authService.authState.activeRole === AccountType.Teacher"
|
||||
class="answer-input-container"
|
||||
>
|
||||
<input
|
||||
|
|
|
@ -27,7 +27,7 @@
|
|||
<div v-if="isError">
|
||||
<v-empty-state
|
||||
icon="mdi-alert-circle-outline"
|
||||
:text="errorMessage"
|
||||
:text="t(errorMessage)"
|
||||
:title="t('error_title')"
|
||||
></v-empty-state>
|
||||
</div>
|
||||
|
|
|
@ -1,49 +1,30 @@
|
|||
<script setup lang="ts">
|
||||
import { ref, computed } from "vue";
|
||||
import { ref, watch } from "vue";
|
||||
import { deadlineRules } from "@/utils/assignment-rules.ts";
|
||||
|
||||
const date = ref("");
|
||||
const time = ref("23:59");
|
||||
const emit = defineEmits(["update:deadline"]);
|
||||
const emit = defineEmits<(e: "update:deadline", value: Date) => void>();
|
||||
|
||||
const formattedDeadline = computed(() => {
|
||||
if (!date.value || !time.value) return "";
|
||||
return `${date.value} ${time.value}`;
|
||||
});
|
||||
const datetime = ref("");
|
||||
|
||||
function updateDeadline(): void {
|
||||
if (date.value && time.value) {
|
||||
emit("update:deadline", formattedDeadline.value);
|
||||
// Watch the datetime value and emit the update
|
||||
watch(datetime, (val) => {
|
||||
const newDate = new Date(val);
|
||||
if (!isNaN(newDate.getTime())) {
|
||||
emit("update:deadline", newDate);
|
||||
}
|
||||
}
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="date"
|
||||
label="Select Deadline Date"
|
||||
type="date"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="deadlineRules"
|
||||
required
|
||||
@update:modelValue="updateDeadline"
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="time"
|
||||
label="Select Deadline Time"
|
||||
type="time"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
@update:modelValue="updateDeadline"
|
||||
></v-text-field>
|
||||
</v-card-text>
|
||||
</div>
|
||||
<v-card-text>
|
||||
<v-text-field
|
||||
v-model="datetime"
|
||||
type="datetime-local"
|
||||
label="Select Deadline"
|
||||
variant="outlined"
|
||||
density="compact"
|
||||
:rules="deadlineRules"
|
||||
required
|
||||
/>
|
||||
</v-card-text>
|
||||
</template>
|
||||
|
||||
<style scoped></style>
|
||||
|
|
|
@ -17,7 +17,7 @@ export class AnswerController extends BaseController {
|
|||
|
||||
constructor(questionId: QuestionId) {
|
||||
super(
|
||||
`learningObject/${questionId.learningObjectIdentifier.hruid}/:${questionId.learningObjectIdentifier.version}/questions/${questionId.sequenceNumber}/answers`,
|
||||
`learningObject/${questionId.learningObjectIdentifier.hruid}/${questionId.learningObjectIdentifier.version}/questions/${questionId.sequenceNumber}/answers`,
|
||||
);
|
||||
this.loId = questionId.learningObjectIdentifier;
|
||||
this.sequenceNumber = questionId.sequenceNumber;
|
||||
|
|
|
@ -37,6 +37,33 @@ export abstract class BaseController {
|
|||
return response.data;
|
||||
}
|
||||
|
||||
/**
|
||||
* Sends a POST-request with a form-data body with the given file.
|
||||
*
|
||||
* @param path Relative path in the api to send the request to.
|
||||
* @param formFieldName The name of the form field in which the file should be.
|
||||
* @param file The file to upload.
|
||||
* @param queryParams The query parameters.
|
||||
* @returns The response the POST request generated.
|
||||
*/
|
||||
protected async postFile<T>(
|
||||
path: string,
|
||||
formFieldName: string,
|
||||
file: File,
|
||||
queryParams?: QueryParams,
|
||||
): Promise<T> {
|
||||
const formData = new FormData();
|
||||
formData.append(formFieldName, file);
|
||||
const response = await apiClient.post<T>(this.absolutePathFor(path), formData, {
|
||||
params: queryParams,
|
||||
headers: {
|
||||
"Content-Type": "multipart/form-data",
|
||||
},
|
||||
});
|
||||
BaseController.assertSuccessResponse(response);
|
||||
return response.data;
|
||||
}
|
||||
|
||||
protected async delete<T>(path: string, queryParams?: QueryParams): Promise<T> {
|
||||
const response = await apiClient.delete<T>(this.absolutePathFor(path), { params: queryParams });
|
||||
BaseController.assertSuccessResponse(response);
|
||||
|
|
|
@ -14,4 +14,16 @@ export class LearningObjectController extends BaseController {
|
|||
async getHTML(hruid: string, language: Language, version: number): Promise<Document> {
|
||||
return this.get<Document>(`/${hruid}/html`, { language, version }, "document");
|
||||
}
|
||||
|
||||
async getAllAdministratedBy(admin: string): Promise<LearningObject[]> {
|
||||
return this.get<LearningObject[]>("/", { admin });
|
||||
}
|
||||
|
||||
async upload(learningObjectZip: File): Promise<LearningObject> {
|
||||
return this.postFile<LearningObject>("/", "learningObject", learningObjectZip);
|
||||
}
|
||||
|
||||
async deleteLearningObject(hruid: string, language: Language, version: number): Promise<LearningObject> {
|
||||
return this.delete<LearningObject>(`/${hruid}`, { language, version });
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,8 +1,8 @@
|
|||
import { BaseController } from "@/controllers/base-controller.ts";
|
||||
import { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import type { Language } from "@/data-objects/language.ts";
|
||||
import { single } from "@/utils/response-assertions.ts";
|
||||
import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto.ts";
|
||||
import { LearningPath } from "@/data-objects/learning-paths/learning-path";
|
||||
import { NotFoundException } from "@/exception/not-found-exception";
|
||||
import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
|
||||
export class LearningPathController extends BaseController {
|
||||
constructor() {
|
||||
|
@ -24,10 +24,13 @@ export class LearningPathController extends BaseController {
|
|||
assignmentNo: forGroup?.assignmentNo,
|
||||
classId: forGroup?.classId,
|
||||
});
|
||||
return LearningPath.fromDTO(single(dtos));
|
||||
if (dtos.length === 0) {
|
||||
throw new NotFoundException("learningPathNotFound");
|
||||
}
|
||||
return LearningPath.fromDTO(dtos[0]);
|
||||
}
|
||||
async getAllByTheme(theme: string): Promise<LearningPath[]> {
|
||||
const dtos = await this.get<LearningPathDTO[]>("/", { theme });
|
||||
async getAllByThemeAndLanguage(theme: string, language: Language): Promise<LearningPath[]> {
|
||||
const dtos = await this.get<LearningPathDTO[]>("/", { theme, language });
|
||||
return dtos.map((dto) => LearningPath.fromDTO(dto));
|
||||
}
|
||||
|
||||
|
@ -36,4 +39,20 @@ export class LearningPathController extends BaseController {
|
|||
const dtos = await this.get<LearningPathDTO[]>("/", query);
|
||||
return dtos.map((dto) => LearningPath.fromDTO(dto));
|
||||
}
|
||||
|
||||
async getAllByAdminRaw(admin: string): Promise<LearningPathDTO[]> {
|
||||
return await this.get<LearningPathDTO[]>("/", { admin });
|
||||
}
|
||||
|
||||
async postLearningPath(learningPath: Partial<LearningPathDTO>): Promise<LearningPathDTO> {
|
||||
return await this.post<LearningPathDTO>("/", learningPath);
|
||||
}
|
||||
|
||||
async putLearningPath(learningPath: Partial<LearningPathDTO>): Promise<LearningPathDTO> {
|
||||
return await this.put<LearningPathDTO>(`/${learningPath.hruid}/${learningPath.language}`, learningPath);
|
||||
}
|
||||
|
||||
async deleteLearningPath(hruid: string, language: string): Promise<LearningPathDTO> {
|
||||
return await this.delete<LearningPathDTO>(`/${hruid}/${language}`);
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,6 +1,5 @@
|
|||
import { BaseController } from "@/controllers/base-controller.ts";
|
||||
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
|
||||
import type { QuestionsResponse } from "@/controllers/questions.ts";
|
||||
import type { ClassesResponse } from "@/controllers/classes.ts";
|
||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||
|
||||
|
@ -40,10 +39,6 @@ export class TeacherController extends BaseController {
|
|||
return this.get<StudentsResponse>(`/${username}/students`, { full });
|
||||
}
|
||||
|
||||
async getQuestions(username: string, full = false): Promise<QuestionsResponse> {
|
||||
return this.get<QuestionsResponse>(`/${username}/questions`, { full });
|
||||
}
|
||||
|
||||
async getStudentJoinRequests(username: string, classId: string): Promise<JoinRequestsResponse> {
|
||||
return this.get<JoinRequestsResponse>(`/${username}/joinRequests/${classId}`);
|
||||
}
|
||||
|
|
|
@ -1,5 +1,5 @@
|
|||
import type { Language } from "@/data-objects/language.ts";
|
||||
import type { LearningPathNodeDTO } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import type { LearningObjectNode as LearningPathNodeDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
|
||||
export class LearningPathNode {
|
||||
public readonly learningobjectHruid: string;
|
||||
|
@ -14,7 +14,7 @@ export class LearningPathNode {
|
|||
learningobjectHruid: string;
|
||||
version: number;
|
||||
language: Language;
|
||||
transitions: { next: LearningPathNode; default: boolean }[];
|
||||
transitions: { next: LearningPathNode; default?: boolean }[];
|
||||
createdAt: Date;
|
||||
updatedAt: Date;
|
||||
done?: boolean;
|
||||
|
@ -22,7 +22,7 @@ export class LearningPathNode {
|
|||
this.learningobjectHruid = options.learningobjectHruid;
|
||||
this.version = options.version;
|
||||
this.language = options.language;
|
||||
this.transitions = options.transitions;
|
||||
this.transitions = options.transitions.map((it) => ({ next: it.next, default: it.default ?? false }));
|
||||
this.createdAt = options.createdAt;
|
||||
this.updatedAt = options.updatedAt;
|
||||
this.done = options.done || false;
|
||||
|
@ -50,8 +50,8 @@ export class LearningPathNode {
|
|||
return undefined;
|
||||
})
|
||||
.filter((it) => it !== undefined),
|
||||
createdAt: new Date(dto.created_at),
|
||||
updatedAt: new Date(dto.updatedAt),
|
||||
createdAt: dto.created_at ? new Date(dto.created_at) : new Date(),
|
||||
updatedAt: dto.updatedAt ? new Date(dto.updatedAt) : new Date(),
|
||||
done: dto.done,
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,6 +1,6 @@
|
|||
import type { Language } from "@/data-objects/language.ts";
|
||||
import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts";
|
||||
import type { LearningPathDTO } from "@/data-objects/learning-paths/learning-path-dto.ts";
|
||||
import type { LearningObjectNode, LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
|
||||
export interface LearningPathNodeDTO {
|
||||
_id: string;
|
||||
|
@ -77,20 +77,26 @@ export class LearningPath {
|
|||
hruid: dto.hruid,
|
||||
title: dto.title,
|
||||
description: dto.description,
|
||||
amountOfNodes: dto.num_nodes,
|
||||
amountOfNodesLeft: dto.num_nodes_left,
|
||||
amountOfNodes: dto.num_nodes ?? dto.nodes.length,
|
||||
amountOfNodesLeft: dto.num_nodes_left ?? dto.nodes.length,
|
||||
keywords: dto.keywords.split(" "),
|
||||
targetAges: { min: dto.min_age, max: dto.max_age },
|
||||
targetAges: {
|
||||
min: dto.min_age ?? NaN,
|
||||
max: dto.max_age ?? NaN,
|
||||
},
|
||||
startNode: LearningPathNode.fromDTOAndOtherNodes(LearningPath.getStartNode(dto), dto.nodes),
|
||||
image: dto.image,
|
||||
});
|
||||
}
|
||||
|
||||
static getStartNode(dto: LearningPathDTO): LearningPathNodeDTO {
|
||||
static getStartNode(dto: LearningPathDTO): LearningObjectNode {
|
||||
const startNodeDtos = dto.nodes.filter((it) => it.start_node === true);
|
||||
if (startNodeDtos.length < 1) {
|
||||
// The learning path has no starting node -> use the first node.
|
||||
return dto.nodes[0];
|
||||
if (dto.nodes.length > 0) {
|
||||
return dto.nodes[0];
|
||||
}
|
||||
throw new Error("emptyLearningPath");
|
||||
} // The learning path has 1 or more starting nodes -> use the first start node.
|
||||
return startNodeDtos[0];
|
||||
}
|
||||
|
|
|
@ -21,6 +21,7 @@
|
|||
"JoinClassExplanation": "Geben Sie den Code ein, den Ihnen die Lehrkraft mitgeteilt hat, um der Klasse beizutreten.",
|
||||
"invalidFormat": "Ungültiges Format",
|
||||
"submitCode": "senden",
|
||||
"submit": "senden",
|
||||
"members": "Mitglieder",
|
||||
"themes": "Themen",
|
||||
"choose-theme": "Wählen Sie ein Thema",
|
||||
|
@ -68,10 +69,10 @@
|
|||
"pick-class": "Wählen Sie eine klasse",
|
||||
"choose-students": "Studenten auswählen",
|
||||
"create-group": "Gruppe erstellen",
|
||||
"class": "klasse",
|
||||
"class": "Klasse",
|
||||
"delete": "löschen",
|
||||
"view-assignment": "Auftrag anzeigen",
|
||||
"code": "code",
|
||||
"code": "Code",
|
||||
"invitations": "Einladungen",
|
||||
"createClass": "Klasse erstellen",
|
||||
"createClassInstructions": "Geben Sie einen Namen für Ihre Klasse ein und klicken Sie auf „Erstellen“. Es erscheint ein Fenster mit einem Code, den Sie kopieren können. Geben Sie diesen Code an Ihre Schüler weiter und sie können Ihrer Klasse beitreten.",
|
||||
|
@ -83,7 +84,7 @@
|
|||
"onlyUse": "nur Buchstaben, Zahlen, Bindestriche (-) und Unterstriche (_) verwenden",
|
||||
"close": "schließen",
|
||||
"copied": "kopiert!",
|
||||
"accept": "akzeptieren",
|
||||
"accept": "Akzeptieren",
|
||||
"deny": "ablehnen",
|
||||
"sent": "sent",
|
||||
"failed": "fehlgeschlagen",
|
||||
|
@ -110,7 +111,7 @@
|
|||
"remove": "entfernen",
|
||||
"students": "Studenten",
|
||||
"classJoinRequests": "Beitrittsanfragen",
|
||||
"reject": "ablehnen",
|
||||
"reject": "Ablehnen",
|
||||
"areusure": "Sind Sie sicher?",
|
||||
"yes": "ja",
|
||||
"teachers": "Lehrer",
|
||||
|
@ -122,6 +123,50 @@
|
|||
"assignmentIndicator": "AUFGABE",
|
||||
"searchAllLearningPathsTitle": "Alle Lernpfade durchsuchen",
|
||||
"searchAllLearningPathsDescription": "Nicht gefunden, was Sie gesucht haben? Klicken Sie hier, um unsere gesamte Lernpfad-Datenbank zu durchsuchen.",
|
||||
"no-students-found": "Diese Klasse hat keine Schüler.",
|
||||
"no-invitations-found": "Sie haben keine ausstehenden Einladungen.",
|
||||
"no-join-requests-found": "Es gibt keine ausstehenden Beitrittsanfragen für diese Klasse.",
|
||||
"no-classes-found": "Sie sind noch keinem Kurs beigetreten.",
|
||||
"classCreated": "Klasse erstellt!",
|
||||
"success": "Erfolg",
|
||||
"submitted": "eingereicht",
|
||||
"see-submission": "Einsendung anzeigen",
|
||||
"view-submissions": "Einsendungen anzeigen",
|
||||
"valid-username": "Bitte geben Sie einen gültigen Benutzernamen ein",
|
||||
"creationFailed": "Erstellung fehlgeschlagen, bitte versuchen Sie es erneut",
|
||||
"no-assignments": "Derzeit gibt es keine Zuweisungen.",
|
||||
"deadline": "deadline",
|
||||
"learningObjects": "Lernobjekte",
|
||||
"learningPaths": "Lernpfade",
|
||||
"hruid": "HRUID",
|
||||
"language": "Sprache",
|
||||
"version": "Version",
|
||||
"previewFor": "Vorschau für ",
|
||||
"upload": "Hochladen",
|
||||
"learningObjectUploadTitle": "Lernobjekt hochladen",
|
||||
"uploadFailed": "Hochladen fehlgeschlagen",
|
||||
"invalidZip": "Dies ist keine gültige ZIP-Datei.",
|
||||
"emptyZip": "Diese ZIP-Datei ist leer.",
|
||||
"missingMetadata": "Dieses Lernobjekt enthält keine metadata.json-Datei.",
|
||||
"missingContent": "Dieses Lernobjekt enthält keine content.*-Datei.",
|
||||
"open": "öffnen",
|
||||
"editLearningPath": "Lernpfad bearbeiten",
|
||||
"newLearningPath": "Neuen Lernpfad erstellen",
|
||||
"saveChanges": "Änderungen speichern",
|
||||
"newLearningObject": "Lernobjekt hochladen",
|
||||
"confirmDialogTitle": "Bitte bestätigen",
|
||||
"learningPathDeleteQuery": "Möchten Sie diesen Lernpfad wirklich löschen?",
|
||||
"learningObjectDeleteQuery": "Möchten Sie dieses Lernobjekt wirklich löschen?",
|
||||
"learningPathCantModifyId": "Der HRUID oder die Sprache eines Lernpfads kann nicht geändert werden.",
|
||||
"error": "Fehler",
|
||||
"ownLearningContentTitle": "Eigene Lerninhalte",
|
||||
"ownLearningContentDescription": "Erstellen und verwalten Sie eigene Lernobjekte und Lernpfade. Nur für fortgeschrittene Nutzer.",
|
||||
"learningPathNotFound": "Dieser Lernpfad konnte nicht gefunden werden.",
|
||||
"emptyLearningPath": "Dieser Lernpfad enthält keine Lernobjekte.",
|
||||
"pathContainsNonExistingLearningObjects": "Mindestens eines der in diesem Pfad referenzierten Lernobjekte existiert nicht.",
|
||||
"targetAgesMandatory": "Zielalter müssen angegeben werden.",
|
||||
"hintRemoveIfUnconditionalTransition": "(entfernen, wenn dies ein bedingungsloser Übergang sein soll)",
|
||||
"hintKeywordsSeparatedBySpaces": "Schlüsselwörter durch Leerzeichen getrennt"
|
||||
"questions": "Fragen",
|
||||
"view-questions": "Fragen anzeigen auf ",
|
||||
"question-input-placeholder": "Frage...",
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"JoinClassExplanation": "Enter the code the teacher has given you to join the class.",
|
||||
"invalidFormat": "Invalid format.",
|
||||
"submitCode": "submit",
|
||||
"submit": "submit",
|
||||
"members": "Members",
|
||||
"themes": "Themes",
|
||||
"choose-theme": "Select a theme",
|
||||
|
@ -68,21 +69,21 @@
|
|||
"pick-class": "Pick a class",
|
||||
"choose-students": "Select students",
|
||||
"create-group": "Create group",
|
||||
"class": "class",
|
||||
"class": "Class",
|
||||
"delete": "delete",
|
||||
"view-assignment": "View assignment",
|
||||
"code": "code",
|
||||
"invitations": "invitations",
|
||||
"createClass": "create class",
|
||||
"code": "Code",
|
||||
"invitations": "Invitations",
|
||||
"createClass": "Create class",
|
||||
"classname": "classname",
|
||||
"EnterNameOfClass": "Enter a classname.",
|
||||
"create": "create",
|
||||
"sender": "sender",
|
||||
"sender": "Sender",
|
||||
"nameIsMandatory": "classname is mandatory",
|
||||
"onlyUse": "only use letters, numbers, dashes (-) and underscores (_)",
|
||||
"close": "close",
|
||||
"copied": "copied!",
|
||||
"accept": "accept",
|
||||
"accept": "Accept",
|
||||
"deny": "deny",
|
||||
"createClassInstructions": "Enter a name for your class and click on create. A window will appear with a code that you can copy. Give this code to your students and they will be able to join.",
|
||||
"sent": "sent",
|
||||
|
@ -108,12 +109,12 @@
|
|||
"progress": "Progress",
|
||||
"created": "created",
|
||||
"remove": "remove",
|
||||
"students": "students",
|
||||
"classJoinRequests": "join requests",
|
||||
"reject": "reject",
|
||||
"students": "Students",
|
||||
"classJoinRequests": "Join requests",
|
||||
"reject": "Reject",
|
||||
"areusure": "Are you sure?",
|
||||
"yes": "yes",
|
||||
"teachers": "teachers",
|
||||
"teachers": "Teachers",
|
||||
"accepted": "accepted",
|
||||
"rejected": "rejected",
|
||||
"enterUsername": "enter the username of the teacher you would like to invite",
|
||||
|
@ -122,6 +123,50 @@
|
|||
"assignmentIndicator": "ASSIGNMENT",
|
||||
"searchAllLearningPathsTitle": "Search all learning paths",
|
||||
"searchAllLearningPathsDescription": "You didn't find what you were looking for? Click here to search our whole database of available learning paths.",
|
||||
"no-students-found": "This class has no students.",
|
||||
"no-invitations-found": "You have no pending invitations.",
|
||||
"no-join-requests-found": "There are no pending join requests for this class.",
|
||||
"no-classes-found": "You are not yet part of a class.",
|
||||
"classCreated": "class created!",
|
||||
"success": "success",
|
||||
"submitted": "submitted",
|
||||
"see-submission": "view submission",
|
||||
"view-submissions": "view submissions",
|
||||
"valid-username": "please enter a valid username",
|
||||
"creationFailed": "creation failed, please try again",
|
||||
"no-assignments": "There are currently no assignments.",
|
||||
"deadline": "deadline",
|
||||
"learningObjects": "Learning objects",
|
||||
"learningPaths": "Learning paths",
|
||||
"hruid": "HRUID",
|
||||
"language": "Language",
|
||||
"version": "Version",
|
||||
"previewFor": "Preview for ",
|
||||
"upload": "Upload",
|
||||
"learningObjectUploadTitle": "Upload a learning object",
|
||||
"uploadFailed": "Upload failed",
|
||||
"invalidZip": "This is not a valid zip file.",
|
||||
"emptyZip": "This zip file is empty",
|
||||
"missingMetadata": "This learning object is missing a metadata.json file.",
|
||||
"missingContent": "This learning object is missing a content.* file.",
|
||||
"open": "open",
|
||||
"editLearningPath": "Edit learning path",
|
||||
"newLearningPath": "Create a new learning path",
|
||||
"saveChanges": "Save changes",
|
||||
"newLearningObject": "Upload learning object",
|
||||
"confirmDialogTitle": "Please confirm",
|
||||
"learningPathDeleteQuery": "Are you sure you want to delete this learning path?",
|
||||
"learningObjectDeleteQuery": "Are you sure you want to delete this learning object?",
|
||||
"learningPathCantModifyId": "The HRUID or language of a learning path cannot be modified.",
|
||||
"error": "Error",
|
||||
"ownLearningContentTitle": "Own learning content",
|
||||
"ownLearningContentDescription": "Create and administrate your own learning objects and learning paths. For advanced users only.",
|
||||
"learningPathNotFound": "This learning path could not be found.",
|
||||
"emptyLearningPath": "This learning path does not contain any learning objects.",
|
||||
"pathContainsNonExistingLearningObjects": "At least one of the learning objects referenced in this path does not exist.",
|
||||
"targetAgesMandatory": "Target ages must be specified.",
|
||||
"hintRemoveIfUnconditionalTransition": "(remove this if this should be an unconditional transition)",
|
||||
"hintKeywordsSeparatedBySpaces": "Keywords separated by spaces"
|
||||
"questions": "questions",
|
||||
"view-questions": "View questions in ",
|
||||
"question-input-placeholder": "question...",
|
||||
|
@ -130,5 +175,4 @@
|
|||
"answers-toggle-show": "Show answers",
|
||||
"no-questions": "No questions asked yet",
|
||||
"no-discussion-tip": "Choose a learning object to view its questions"
|
||||
|
||||
}
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"JoinClassExplanation": "Entrez le code que l'enseignant vous a donné pour rejoindre la classe.",
|
||||
"invalidFormat": "Format non valide.",
|
||||
"submitCode": "envoyer",
|
||||
"submit": "envoyer",
|
||||
"members": "Membres",
|
||||
"themes": "Thèmes",
|
||||
"choose-theme": "Choisis un thème",
|
||||
|
@ -68,22 +69,22 @@
|
|||
"pick-class": "Choisissez une classe",
|
||||
"choose-students": "Sélectionnez des élèves",
|
||||
"create-group": "Créer un groupe",
|
||||
"class": "classe",
|
||||
"class": "Classe",
|
||||
"delete": "supprimer",
|
||||
"view-assignment": "Voir le travail",
|
||||
"code": "code",
|
||||
"invitations": "invitations",
|
||||
"createClass": "créer une classe",
|
||||
"code": "Code",
|
||||
"invitations": "Invitations",
|
||||
"createClass": "Créer une classe",
|
||||
"createClassInstructions": "Entrez un nom pour votre classe et cliquez sur créer. Une fenêtre apparaît avec un code que vous pouvez copier. Donnez ce code à vos élèves et ils pourront rejoindre votre classe.",
|
||||
"classname": "nom de classe",
|
||||
"EnterNameOfClass": "saisir un nom de classe.",
|
||||
"create": "créer",
|
||||
"sender": "expéditeur",
|
||||
"sender": "Expéditeur",
|
||||
"nameIsMandatory": "le nom de classe est obligatoire",
|
||||
"onlyUse": "n'utiliser que des lettres, des chiffres, des tirets (-) et des traits de soulignement (_)",
|
||||
"close": "fermer",
|
||||
"copied": "copié!",
|
||||
"accept": "accepter",
|
||||
"accept": "Accepter",
|
||||
"deny": "refuser",
|
||||
"sent": "envoyé",
|
||||
"failed": "échoué",
|
||||
|
@ -108,12 +109,13 @@
|
|||
"submission": "Soumission",
|
||||
"progress": "Progrès",
|
||||
"remove": "supprimer",
|
||||
"students": "étudiants",
|
||||
"classJoinRequests": "demandes d'adhésion",
|
||||
"reject": "rejeter",
|
||||
"students": "Étudiants",
|
||||
|
||||
"classJoinRequests": "Demandes d'adhésion",
|
||||
"reject": "Rejeter",
|
||||
"areusure": "Êtes-vous sûr?",
|
||||
"yes": "oui",
|
||||
"teachers": "enseignants",
|
||||
"teachers": "Enseignants",
|
||||
"accepted": "acceptée",
|
||||
"rejected": "rejetée",
|
||||
"enterUsername": "entrez le nom d'utilisateur de l'enseignant que vous souhaitez inviter",
|
||||
|
@ -122,6 +124,50 @@
|
|||
"assignmentIndicator": "DEVOIR",
|
||||
"searchAllLearningPathsTitle": "Rechercher tous les parcours d'apprentissage",
|
||||
"searchAllLearningPathsDescription": "Vous n'avez pas trouvé ce que vous cherchiez ? Cliquez ici pour rechercher dans toute notre base de données de parcours d'apprentissage disponibles.",
|
||||
"no-students-found": "Cette classe n'a pas d'élèves.",
|
||||
"no-invitations-found": "Vous n'avez aucune invitation en attente.",
|
||||
"no-join-requests-found": "Il n'y a aucune demande d'adhésion en attente pour cette classe.",
|
||||
"no-classes-found": "Vous ne faites pas encore partie d'une classe.",
|
||||
"classCreated": "Classe créée !",
|
||||
"success": "succès",
|
||||
"submitted": "soumis",
|
||||
"see-submission": "voir la soumission",
|
||||
"view-submissions": "voir les soumissions",
|
||||
"valid-username": "veuillez entrer un nom d'utilisateur valide",
|
||||
"creationFailed": "échec de la création, veuillez réessayer",
|
||||
"no-assignments": "Il n'y a actuellement aucun travail.",
|
||||
"deadline": "délai",
|
||||
"learningObjects": "Objets d’apprentissage",
|
||||
"learningPaths": "Parcours d’apprentissage",
|
||||
"hruid": "HRUID",
|
||||
"language": "Langue",
|
||||
"version": "Version",
|
||||
"previewFor": "Aperçu de ",
|
||||
"upload": "Téléverser",
|
||||
"learningObjectUploadTitle": "Téléverser un objet d’apprentissage",
|
||||
"uploadFailed": "Échec du téléversement",
|
||||
"invalidZip": "Ce n’est pas un fichier ZIP valide.",
|
||||
"emptyZip": "Ce fichier ZIP est vide.",
|
||||
"missingMetadata": "Il manque un fichier metadata.json à cet objet d’apprentissage.",
|
||||
"missingContent": "Il manque un fichier content.* à cet objet d’apprentissage.",
|
||||
"open": "ouvrir",
|
||||
"editLearningPath": "Modifier le parcours",
|
||||
"newLearningPath": "Créer un nouveau parcours",
|
||||
"saveChanges": "Enregistrer les modifications",
|
||||
"newLearningObject": "Téléverser un objet d’apprentissage",
|
||||
"confirmDialogTitle": "Veuillez confirmer",
|
||||
"learningPathDeleteQuery": "Voulez-vous vraiment supprimer ce parcours d’apprentissage ?",
|
||||
"learningObjectDeleteQuery": "Voulez-vous vraiment supprimer cet objet d’apprentissage ?",
|
||||
"learningPathCantModifyId": "Le HRUID ou la langue d’un parcours ne peuvent pas être modifiés.",
|
||||
"error": "Erreur",
|
||||
"ownLearningContentTitle": "Contenu d’apprentissage personnel",
|
||||
"ownLearningContentDescription": "Créez et gérez vos propres objets et parcours d’apprentissage. Réservé aux utilisateurs avancés.",
|
||||
"learningPathNotFound": "Ce parcours d'apprentissage est introuvable.",
|
||||
"emptyLearningPath": "Ce parcours d'apprentissage ne contient aucun objet d'apprentissage.",
|
||||
"pathContainsNonExistingLearningObjects": "Au moins un des objets d’apprentissage référencés dans ce chemin n’existe pas.",
|
||||
"targetAgesMandatory": "Les âges cibles doivent être spécifiés.",
|
||||
"hintRemoveIfUnconditionalTransition": "(supprimer ceci s’il s’agit d’une transition inconditionnelle)",
|
||||
"hintKeywordsSeparatedBySpaces": "Mots-clés séparés par des espaces"
|
||||
"questions": "Questions",
|
||||
"view-questions": "Voir les questions dans ",
|
||||
"question-input-placeholder": "question...",
|
||||
|
|
|
@ -33,6 +33,7 @@
|
|||
"JoinClassExplanation": "Voer de code in die je van de docent hebt gekregen om lid te worden van de klas.",
|
||||
"invalidFormat": "Ongeldig formaat.",
|
||||
"submitCode": "verzenden",
|
||||
"submit": "verzenden",
|
||||
"members": "Leden",
|
||||
"themes": "Lesthema's",
|
||||
"choose-theme": "Kies een thema",
|
||||
|
@ -68,22 +69,22 @@
|
|||
"pick-class": "Kies een klas",
|
||||
"choose-students": "Studenten selecteren",
|
||||
"create-group": "Groep aanmaken",
|
||||
"class": "klas",
|
||||
"class": "Klas",
|
||||
"delete": "verwijderen",
|
||||
"view-assignment": "Opdracht bekijken",
|
||||
"code": "code",
|
||||
"invitations": "uitnodigingen",
|
||||
"createClass": "klas aanmaken",
|
||||
"code": "Code",
|
||||
"invitations": "Uitnodigingen",
|
||||
"createClass": "Klas aanmaken",
|
||||
"createClassInstructions": "Voer een naam in voor je klas en klik op create. Er verschijnt een venster met een code die je kunt kopiëren. Geef deze code aan je leerlingen en ze kunnen deelnemen aan je klas.",
|
||||
"classname": "klasnaam",
|
||||
"EnterNameOfClass": "Geef een klasnaam op.",
|
||||
"create": "aanmaken",
|
||||
"sender": "afzender",
|
||||
"sender": "Afzender",
|
||||
"nameIsMandatory": "klasnaam is verplicht",
|
||||
"onlyUse": "gebruik enkel letters, cijfers, dashes (-) en underscores (_)",
|
||||
"close": "sluiten",
|
||||
"copied": "gekopieerd!",
|
||||
"accept": "accepteren",
|
||||
"accept": "Accepteren",
|
||||
"deny": "weigeren",
|
||||
"sent": "verzonden",
|
||||
"failed": "mislukt",
|
||||
|
@ -108,12 +109,12 @@
|
|||
"submission": "Indiening",
|
||||
"progress": "Vooruitgang",
|
||||
"remove": "verwijder",
|
||||
"students": "studenten",
|
||||
"classJoinRequests": "deelname verzoeken",
|
||||
"reject": "weiger",
|
||||
"students": "Studenten",
|
||||
"classJoinRequests": "Deelname verzoeken",
|
||||
"reject": "Weiger",
|
||||
"areusure": "Bent u zeker?",
|
||||
"yes": "ja",
|
||||
"teachers": "leerkrachten",
|
||||
"teachers": "Leerkrachten",
|
||||
"accepted": "geaccepteerd",
|
||||
"rejected": "geweigerd",
|
||||
"enterUsername": "vul de gebruikersnaam van de leerkracht die je wilt uitnodigen in",
|
||||
|
@ -122,6 +123,50 @@
|
|||
"assignmentIndicator": "OPDRACHT",
|
||||
"searchAllLearningPathsTitle": "Alle leerpaden doorzoeken",
|
||||
"searchAllLearningPathsDescription": "Niet gevonden waar je naar op zoek was? Klik hier om onze volledige databank van beschikbare leerpaden te doorzoeken.",
|
||||
"no-students-found": "Deze klas heeft geen leerlingen.",
|
||||
"no-invitations-found": "U heeft geen openstaande uitnodigingen.",
|
||||
"no-join-requests-found": "Er zijn geen openstaande verzoeken om lid te worden van deze klas.",
|
||||
"no-classes-found": "U maakt nog geen deel uit van een klas.",
|
||||
"classCreated": "Klas aangemaakt!",
|
||||
"success": "succes",
|
||||
"submitted": "ingediend",
|
||||
"see-submission": "inzending bekijken",
|
||||
"view-submissions": "inzendingen bekijken",
|
||||
"valid-username": "voer een geldige gebruikersnaam in",
|
||||
"creationFailed": "aanmaak mislukt, probeer het opnieuw",
|
||||
"no-assignments": "Er zijn momenteel geen opdrachten.",
|
||||
"deadline": "deadline",
|
||||
"learningObjects": "Leerobjecten",
|
||||
"learningPaths": "Leerpaden",
|
||||
"hruid": "HRUID",
|
||||
"language": "Taal",
|
||||
"version": "Versie",
|
||||
"previewFor": "Voorbeeld van ",
|
||||
"upload": "Uploaden",
|
||||
"learningObjectUploadTitle": "Leerobject uploaden",
|
||||
"uploadFailed": "Upload mislukt",
|
||||
"invalidZip": "Dit is geen geldig zipbestand.",
|
||||
"emptyZip": "Dit zipbestand is leeg.",
|
||||
"missingMetadata": "Dit leerobject mist een metadata.json-bestand.",
|
||||
"missingContent": "Dit leerobject mist een content.*-bestand.",
|
||||
"open": "openen",
|
||||
"editLearningPath": "Leerpad bewerken",
|
||||
"newLearningPath": "Nieuw leerpad aanmaken",
|
||||
"saveChanges": "Wijzigingen opslaan",
|
||||
"newLearningObject": "Leerobject uploaden",
|
||||
"confirmDialogTitle": "Bevestig alstublieft",
|
||||
"learningPathDeleteQuery": "Weet u zeker dat u dit leerpad wilt verwijderen?",
|
||||
"learningObjectDeleteQuery": "Weet u zeker dat u dit leerobject wilt verwijderen?",
|
||||
"learningPathCantModifyId": "De HRUID of taal van een leerpad kan niet worden gewijzigd.",
|
||||
"error": "Fout",
|
||||
"ownLearningContentTitle": "Eigen leerinhoud",
|
||||
"ownLearningContentDescription": "Maak en beheer je eigen leerobjecten en leerpads. Alleen voor gevorderde gebruikers.",
|
||||
"learningPathNotFound": "Dit leerpad kon niet gevonden worden.",
|
||||
"emptyLearningPath": "Dit leerpad bevat geen leerobjecten.",
|
||||
"pathContainsNonExistingLearningObjects": "Ten minste één van de leerobjecten in dit pad bestaat niet.",
|
||||
"targetAgesMandatory": "Doelleeftijden moeten worden opgegeven.",
|
||||
"hintRemoveIfUnconditionalTransition": "(verwijder dit voor onvoorwaardelijke overgangen)",
|
||||
"hintKeywordsSeparatedBySpaces": "Trefwoorden gescheiden door spaties"
|
||||
"questions": "vragen",
|
||||
"view-questions": "Bekijk vragen in ",
|
||||
"question-input-placeholder": "vraag...",
|
||||
|
|
|
@ -7,15 +7,20 @@ import * as components from "vuetify/components";
|
|||
import * as directives from "vuetify/directives";
|
||||
import i18n from "./i18n/i18n.ts";
|
||||
|
||||
// JSON-editor
|
||||
import JsonEditorVue from "json-editor-vue";
|
||||
|
||||
// Components
|
||||
import App from "./App.vue";
|
||||
import router from "./router";
|
||||
import { aliases, mdi } from "vuetify/iconsets/mdi";
|
||||
import { VueQueryPlugin, QueryClient } from "@tanstack/vue-query";
|
||||
import { de, en, fr, nl } from "vuetify/locale";
|
||||
|
||||
const app = createApp(App);
|
||||
|
||||
app.use(router);
|
||||
app.use(JsonEditorVue, {});
|
||||
|
||||
const link = document.createElement("link");
|
||||
link.rel = "stylesheet";
|
||||
|
@ -32,6 +37,11 @@ const vuetify = createVuetify({
|
|||
mdi,
|
||||
},
|
||||
},
|
||||
locale: {
|
||||
locale: i18n.global.locale,
|
||||
fallback: "en",
|
||||
messages: { nl, en, de, fr },
|
||||
},
|
||||
});
|
||||
|
||||
const queryClient = new QueryClient({
|
||||
|
|
|
@ -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) });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,9 +1,16 @@
|
|||
import { type MaybeRefOrGetter, toValue } from "vue";
|
||||
import type { Language } from "@/data-objects/language.ts";
|
||||
import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query";
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
type UseMutationReturnType,
|
||||
type UseQueryReturnType,
|
||||
} from "@tanstack/vue-query";
|
||||
import { getLearningObjectController } from "@/controllers/controllers.ts";
|
||||
import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts";
|
||||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import type { AxiosError } from "axios";
|
||||
|
||||
export const LEARNING_OBJECT_KEY = "learningObject";
|
||||
const learningObjectController = getLearningObjectController();
|
||||
|
@ -24,15 +31,15 @@ export function useLearningObjectMetadataQuery(
|
|||
}
|
||||
|
||||
export function useLearningObjectHTMLQuery(
|
||||
hruid: MaybeRefOrGetter<string>,
|
||||
language: MaybeRefOrGetter<Language>,
|
||||
version: MaybeRefOrGetter<number>,
|
||||
hruid: MaybeRefOrGetter<string | undefined>,
|
||||
language: MaybeRefOrGetter<Language | undefined>,
|
||||
version: MaybeRefOrGetter<number | undefined>,
|
||||
): UseQueryReturnType<Document, Error> {
|
||||
return useQuery({
|
||||
queryKey: [LEARNING_OBJECT_KEY, "html", hruid, language, version],
|
||||
queryFn: async () => {
|
||||
const [hruidVal, languageVal, versionVal] = [toValue(hruid), toValue(language), toValue(version)];
|
||||
return learningObjectController.getHTML(hruidVal, languageVal, versionVal);
|
||||
return learningObjectController.getHTML(hruidVal!, languageVal!, versionVal!);
|
||||
},
|
||||
enabled: () => Boolean(toValue(hruid)) && Boolean(toValue(language)) && Boolean(toValue(version)),
|
||||
});
|
||||
|
@ -55,3 +62,49 @@ export function useLearningObjectListForPathQuery(
|
|||
enabled: () => Boolean(toValue(learningPath)),
|
||||
});
|
||||
}
|
||||
|
||||
export function useLearningObjectListForAdminQuery(
|
||||
admin: MaybeRefOrGetter<string | undefined>,
|
||||
): UseQueryReturnType<LearningObject[], Error> {
|
||||
return useQuery({
|
||||
queryKey: [LEARNING_OBJECT_KEY, "forAdmin", admin],
|
||||
queryFn: async () => {
|
||||
const adminVal = toValue(admin);
|
||||
return await learningObjectController.getAllAdministratedBy(adminVal!);
|
||||
},
|
||||
enabled: () => toValue(admin) !== undefined,
|
||||
});
|
||||
}
|
||||
|
||||
export function useUploadLearningObjectMutation(): UseMutationReturnType<
|
||||
LearningObject,
|
||||
AxiosError,
|
||||
{ learningObjectZip: File },
|
||||
unknown
|
||||
> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ learningObjectZip }) => await learningObjectController.upload(learningObjectZip),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: [LEARNING_OBJECT_KEY, "forAdmin"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteLearningObjectMutation(): UseMutationReturnType<
|
||||
LearningObject,
|
||||
AxiosError,
|
||||
{ hruid: string; language: Language; version: number },
|
||||
unknown
|
||||
> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ hruid, language, version }) =>
|
||||
await learningObjectController.deleteLearningObject(hruid, language, version),
|
||||
onSuccess: async () => {
|
||||
await queryClient.invalidateQueries({ queryKey: [LEARNING_OBJECT_KEY, "forAdmin"] });
|
||||
},
|
||||
});
|
||||
}
|
||||
|
|
|
@ -1,8 +1,16 @@
|
|||
import { type MaybeRefOrGetter, toValue } from "vue";
|
||||
import type { Language } from "@/data-objects/language.ts";
|
||||
import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query";
|
||||
import {
|
||||
useMutation,
|
||||
useQuery,
|
||||
useQueryClient,
|
||||
type UseMutationReturnType,
|
||||
type UseQueryReturnType,
|
||||
} from "@tanstack/vue-query";
|
||||
import { getLearningPathController } from "@/controllers/controllers";
|
||||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
|
||||
import type { AxiosError } from "axios";
|
||||
import type { LearningPath as LearningPathDTO } from "@dwengo-1/common/interfaces/learning-content";
|
||||
import type { LearningPath } from "@/data-objects/learning-paths/learning-path";
|
||||
|
||||
export const LEARNING_PATH_KEY = "learningPath";
|
||||
const learningPathController = getLearningPathController();
|
||||
|
@ -22,16 +30,69 @@ export function useGetLearningPathQuery(
|
|||
});
|
||||
}
|
||||
|
||||
export function useGetAllLearningPathsByThemeQuery(
|
||||
export function useGetAllLearningPathsByThemeAndLanguageQuery(
|
||||
theme: MaybeRefOrGetter<string>,
|
||||
language: MaybeRefOrGetter<Language>,
|
||||
): UseQueryReturnType<LearningPath[], Error> {
|
||||
return useQuery({
|
||||
queryKey: [LEARNING_PATH_KEY, "getAllByTheme", theme],
|
||||
queryFn: async () => learningPathController.getAllByTheme(toValue(theme)),
|
||||
queryKey: [LEARNING_PATH_KEY, "getAllByTheme", theme, language],
|
||||
queryFn: async () => learningPathController.getAllByThemeAndLanguage(toValue(theme), toValue(language)),
|
||||
enabled: () => Boolean(toValue(theme)),
|
||||
});
|
||||
}
|
||||
|
||||
export function useGetAllLearningPathsByAdminQuery(
|
||||
admin: MaybeRefOrGetter<string | undefined>,
|
||||
): UseQueryReturnType<LearningPathDTO[], AxiosError> {
|
||||
return useQuery({
|
||||
queryKey: [LEARNING_PATH_KEY, "getAllByAdmin", admin],
|
||||
queryFn: async () => learningPathController.getAllByAdminRaw(toValue(admin)!),
|
||||
enabled: () => Boolean(toValue(admin)),
|
||||
});
|
||||
}
|
||||
|
||||
export function usePostLearningPathMutation(): UseMutationReturnType<
|
||||
LearningPathDTO,
|
||||
AxiosError,
|
||||
{ learningPath: Partial<LearningPathDTO> },
|
||||
unknown
|
||||
> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ learningPath }) => learningPathController.postLearningPath(learningPath),
|
||||
onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function usePutLearningPathMutation(): UseMutationReturnType<
|
||||
LearningPathDTO,
|
||||
AxiosError,
|
||||
{ learningPath: Partial<LearningPathDTO> },
|
||||
unknown
|
||||
> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ learningPath }) => learningPathController.putLearningPath(learningPath),
|
||||
onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useDeleteLearningPathMutation(): UseMutationReturnType<
|
||||
LearningPathDTO,
|
||||
AxiosError,
|
||||
{ hruid: string; language: Language },
|
||||
unknown
|
||||
> {
|
||||
const queryClient = useQueryClient();
|
||||
|
||||
return useMutation({
|
||||
mutationFn: async ({ hruid, language }) => learningPathController.deleteLearningPath(hruid, language),
|
||||
onSuccess: async () => queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY] }),
|
||||
});
|
||||
}
|
||||
|
||||
export function useSearchLearningPathQuery(
|
||||
query: MaybeRefOrGetter<string | undefined>,
|
||||
language: MaybeRefOrGetter<string | undefined>,
|
||||
|
|
|
@ -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] {
|
||||
|
|
|
@ -10,7 +10,6 @@ import {
|
|||
import { TeacherController, type TeacherResponse, type TeachersResponse } from "@/controllers/teachers.ts";
|
||||
import type { ClassesResponse } from "@/controllers/classes.ts";
|
||||
import type { JoinRequestResponse, JoinRequestsResponse, StudentsResponse } from "@/controllers/students.ts";
|
||||
import type { QuestionsResponse } from "@/controllers/questions.ts";
|
||||
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
|
||||
import { studentJoinRequestQueryKey, studentJoinRequestsQueryKey } from "@/queries/students.ts";
|
||||
|
||||
|
@ -33,10 +32,6 @@ function teacherStudentsQueryKey(username: string, full: boolean): [string, stri
|
|||
return ["teacher-students", username, full];
|
||||
}
|
||||
|
||||
function teacherQuestionsQueryKey(username: string, full: boolean): [string, string, boolean] {
|
||||
return ["teacher-questions", username, full];
|
||||
}
|
||||
|
||||
export function teacherClassJoinRequests(classId: string): [string, string] {
|
||||
return ["teacher-class-join-requests", classId];
|
||||
}
|
||||
|
@ -80,17 +75,6 @@ export function useTeacherStudentsQuery(
|
|||
});
|
||||
}
|
||||
|
||||
export function useTeacherQuestionsQuery(
|
||||
username: MaybeRefOrGetter<string | undefined>,
|
||||
full: MaybeRefOrGetter<boolean> = false,
|
||||
): UseQueryReturnType<QuestionsResponse, Error> {
|
||||
return useQuery({
|
||||
queryKey: computed(() => teacherQuestionsQueryKey(toValue(username)!, toValue(full))),
|
||||
queryFn: async () => teacherController.getQuestions(toValue(username)!, toValue(full)),
|
||||
enabled: () => Boolean(toValue(username)),
|
||||
});
|
||||
}
|
||||
|
||||
export function useTeacherJoinRequestsQuery(
|
||||
username: MaybeRefOrGetter<string | undefined>,
|
||||
classId: MaybeRefOrGetter<string | undefined>,
|
||||
|
|
|
@ -1,21 +1,23 @@
|
|||
import { createRouter, createWebHistory } from "vue-router";
|
||||
import SingleAssignment from "@/views/assignments/SingleAssignment.vue";
|
||||
import SingleClass from "@/views/classes/SingleClass.vue";
|
||||
import SingleDiscussion from "@/views/discussions/SingleDiscussion.vue";
|
||||
import NotFound from "@/components/errors/NotFound.vue";
|
||||
import CreateAssignment from "@/views/assignments/CreateAssignment.vue";
|
||||
import CreateDiscussion from "@/views/discussions/CreateDiscussion.vue";
|
||||
import CallbackPage from "@/views/CallbackPage.vue";
|
||||
import UserClasses from "@/views/classes/UserClasses.vue";
|
||||
import UserAssignments from "@/views/assignments/UserAssignments.vue";
|
||||
import LearningPathPage from "@/views/learning-paths/LearningPathPage.vue";
|
||||
import LearningPathSearchPage from "@/views/learning-paths/LearningPathSearchPage.vue";
|
||||
import UserHomePage from "@/views/homepage/UserHomePage.vue";
|
||||
import SingleTheme from "@/views/SingleTheme.vue";
|
||||
import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue";
|
||||
import authService from "@/services/auth/auth-service";
|
||||
import DiscussionForward from "@/views/discussions/DiscussionForward.vue";
|
||||
import NoDiscussion from "@/views/discussions/NoDiscussion.vue";
|
||||
import { createRouter, createWebHistory } from 'vue-router';
|
||||
import SingleAssignment from '@/views/assignments/SingleAssignment.vue';
|
||||
import SingleClass from '@/views/classes/SingleClass.vue';
|
||||
import SingleDiscussion from '@/views/discussions/SingleDiscussion.vue';
|
||||
import NotFound from '@/components/errors/NotFound.vue';
|
||||
import CreateAssignment from '@/views/assignments/CreateAssignment.vue';
|
||||
import CreateDiscussion from '@/views/discussions/CreateDiscussion.vue';
|
||||
import CallbackPage from '@/views/CallbackPage.vue';
|
||||
import UserClasses from '@/views/classes/UserClasses.vue';
|
||||
import UserAssignments from '@/views/assignments/UserAssignments.vue';
|
||||
import LearningPathPage from '@/views/learning-paths/LearningPathPage.vue';
|
||||
import LearningPathSearchPage from '@/views/learning-paths/LearningPathSearchPage.vue';
|
||||
import UserHomePage from '@/views/homepage/UserHomePage.vue';
|
||||
import SingleTheme from '@/views/SingleTheme.vue';
|
||||
import LearningObjectView from '@/views/learning-paths/learning-object/LearningObjectView.vue';
|
||||
import authService from '@/services/auth/auth-service';
|
||||
import DiscussionForward from '@/views/discussions/DiscussionForward.vue';
|
||||
import NoDiscussion from '@/views/discussions/NoDiscussion.vue';
|
||||
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),
|
||||
|
@ -115,6 +117,12 @@ const router = createRouter({
|
|||
props: true,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/my-content",
|
||||
name: "OwnLearningContentPage",
|
||||
component: OwnLearningContentPage,
|
||||
meta: { requiresAuth: true },
|
||||
},
|
||||
{
|
||||
path: "/learningPath",
|
||||
children: [
|
||||
|
@ -153,7 +161,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}`;
|
||||
}
|
||||
|
|
|
@ -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>
|
||||
|
|
|
@ -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");
|
||||
}
|
||||
|
||||
|
@ -48,7 +49,7 @@
|
|||
|
||||
// Disable combobox when learningPath prop is passed
|
||||
const lpIsSelected = route.query.hruid !== undefined;
|
||||
const deadline = ref(null);
|
||||
const deadline = ref(new Date());
|
||||
const description = ref("");
|
||||
const groups = ref<string[][]>([]);
|
||||
|
||||
|
@ -86,6 +87,7 @@
|
|||
title: assignmentTitle.value,
|
||||
description: description.value,
|
||||
learningPath: lp || "",
|
||||
deadline: deadline.value,
|
||||
language: language.value,
|
||||
groups: groups.value,
|
||||
};
|
||||
|
@ -96,7 +98,7 @@
|
|||
|
||||
<template>
|
||||
<div class="main-container">
|
||||
<h1 class="title">{{ t("new-assignment") }}</h1>
|
||||
<h1 class="h1">{{ t("new-assignment") }}</h1>
|
||||
<v-card class="form-card">
|
||||
<v-form
|
||||
ref="form"
|
||||
|
|
|
@ -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);
|
||||
|
|
|
@ -22,8 +22,7 @@
|
|||
) => { groupProgressMap: Map<number, number> };
|
||||
}>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const language = ref<Language>(locale.value as Language);
|
||||
const { t } = useI18n();
|
||||
const learningPath = ref();
|
||||
// Get the user's username/id
|
||||
const username = asyncComputed(async () => {
|
||||
|
@ -38,7 +37,7 @@
|
|||
|
||||
const lpQueryResult = useGetLearningPathQuery(
|
||||
computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""),
|
||||
computed(() => language.value),
|
||||
computed(() => assignmentQueryResult.data.value?.assignment.language as Language),
|
||||
);
|
||||
|
||||
const groupsQueryResult = useGroupsQuery(props.classId, props.assignmentId, true);
|
||||
|
@ -100,7 +99,7 @@ language
|
|||
>
|
||||
<v-btn
|
||||
v-if="lpData"
|
||||
:to="`/learningPath/${lpData.hruid}/${language}/${lpData.startNode.learningobjectHruid}?forGroup=${group?.groupNumber}&assignmentNo=${assignmentId}&classId=${classId}`"
|
||||
:to="`/learningPath/${lpData.hruid}/${assignmentQueryResult.data.value?.assignment.language}/${lpData.startNode.learningobjectHruid}?forGroup=${group?.groupNumber}&assignmentNo=${assignmentId}&classId=${classId}`"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
>
|
||||
|
|
|
@ -19,8 +19,7 @@
|
|||
) => { groupProgressMap: Map<number, number> };
|
||||
}>();
|
||||
|
||||
const { t, locale } = useI18n();
|
||||
const language = computed(() => locale.value);
|
||||
const { t } = useI18n();
|
||||
const groups = ref();
|
||||
const learningPath = ref();
|
||||
|
||||
|
@ -29,7 +28,7 @@
|
|||
// Get learning path object
|
||||
const lpQueryResult = useGetLearningPathQuery(
|
||||
computed(() => assignmentQueryResult.data.value?.assignment?.learningPath ?? ""),
|
||||
computed(() => language.value as Language),
|
||||
computed(() => assignmentQueryResult.data.value?.assignment.language as Language),
|
||||
);
|
||||
|
||||
// Get all the groups withing the assignment
|
||||
|
@ -38,9 +37,9 @@
|
|||
|
||||
/* Crashes right now cause api data has inexistent hruid TODO: uncomment later and use it in progress bar
|
||||
Const {groupProgressMap} = props.useGroupsWithProgress(
|
||||
groups,
|
||||
learningPath,
|
||||
language
|
||||
groups,
|
||||
learningPath,
|
||||
language
|
||||
);
|
||||
*/
|
||||
|
||||
|
@ -121,7 +120,7 @@ Const {groupProgressMap} = props.useGroupsWithProgress(
|
|||
>
|
||||
<v-btn
|
||||
v-if="lpData"
|
||||
:to="`/learningPath/${lpData.hruid}/${language}/${lpData.startNode.learningobjectHruid}?assignmentNo=${assignmentId}&classId=${classId}`"
|
||||
:to="`/learningPath/${lpData.hruid}/${assignmentQueryResult.data.value?.assignment.language}/${lpData.startNode.learningobjectHruid}?assignmentNo=${assignmentId}&classId=${classId}`"
|
||||
variant="tonal"
|
||||
color="primary"
|
||||
>
|
||||
|
@ -203,8 +202,8 @@ Const {groupProgressMap} = props.useGroupsWithProgress(
|
|||
<v-btn
|
||||
color="primary"
|
||||
@click="dialog = false"
|
||||
>Close</v-btn
|
||||
>
|
||||
>Close
|
||||
</v-btn>
|
||||
</v-card-actions>
|
||||
</v-card>
|
||||
</v-dialog>
|
||||
|
|
|
@ -9,14 +9,16 @@
|
|||
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 } = useI18n();
|
||||
const { t, locale } = useI18n();
|
||||
const router = useRouter();
|
||||
|
||||
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;
|
||||
|
@ -27,30 +29,47 @@
|
|||
classesQueryResults = useStudentClassesQuery(username, true);
|
||||
}
|
||||
|
||||
//TODO: remove later
|
||||
const classController = new ClassController();
|
||||
|
||||
//TODO: replace by query that fetches all user's assignment
|
||||
const assignments = asyncComputed(async () => {
|
||||
const classes = classesQueryResults?.data?.value?.classes;
|
||||
if (!classes) return [];
|
||||
const result = await Promise.all(
|
||||
(classes as ClassDTO[]).map(async (cls) => {
|
||||
const { assignments } = await classController.getAssignments(cls.id);
|
||||
return assignments.map((a) => ({
|
||||
id: a.id,
|
||||
class: cls,
|
||||
title: a.title,
|
||||
description: a.description,
|
||||
learningPath: a.learningPath,
|
||||
language: a.language,
|
||||
groups: a.groups,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
const assignments = asyncComputed(
|
||||
async () => {
|
||||
const classes = classesQueryResults?.data?.value?.classes;
|
||||
if (!classes) return [];
|
||||
|
||||
return result.flat();
|
||||
}, []);
|
||||
const result = await Promise.all(
|
||||
(classes as ClassDTO[]).map(async (cls) => {
|
||||
const { assignments } = await classController.getAssignments(cls.id);
|
||||
return assignments.map((a) => ({
|
||||
id: a.id,
|
||||
class: cls,
|
||||
title: a.title,
|
||||
description: a.description,
|
||||
learningPath: a.learningPath,
|
||||
language: a.language,
|
||||
deadline: a.deadline,
|
||||
groups: a.groups,
|
||||
}));
|
||||
}),
|
||||
);
|
||||
|
||||
// Order the assignments by deadline
|
||||
return result.flat().sort((a, b) => {
|
||||
const now = Date.now();
|
||||
const aTime = new Date(a.deadline).getTime();
|
||||
const bTime = new Date(b.deadline).getTime();
|
||||
|
||||
const aIsPast = aTime < now;
|
||||
const bIsPast = bTime < now;
|
||||
|
||||
if (aIsPast && !bIsPast) return 1;
|
||||
if (!aIsPast && bIsPast) return -1;
|
||||
|
||||
return aTime - bTime;
|
||||
});
|
||||
},
|
||||
[],
|
||||
{ evaluating: true },
|
||||
);
|
||||
|
||||
async function goToCreateAssignment(): Promise<void> {
|
||||
await router.push("/assignment/create");
|
||||
|
@ -72,6 +91,35 @@
|
|||
mutate({ cid: clsId, an: num });
|
||||
}
|
||||
|
||||
function formatDate(date?: string | Date): string {
|
||||
if (!date) return "–";
|
||||
const d = new Date(date);
|
||||
|
||||
// Choose locale based on selected language
|
||||
const currentLocale = locale.value;
|
||||
|
||||
return d.toLocaleDateString(currentLocale, {
|
||||
weekday: "short",
|
||||
day: "2-digit",
|
||||
month: "long",
|
||||
year: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "2-digit",
|
||||
});
|
||||
}
|
||||
|
||||
function getDeadlineClass(deadline?: string | Date): string {
|
||||
if (!deadline) return "";
|
||||
|
||||
const date = new Date(deadline);
|
||||
const now = new Date();
|
||||
const in24Hours = new Date(now.getTime() + 24 * 60 * 60 * 1000);
|
||||
|
||||
if (date.getTime() < now.getTime()) return "deadline-passed";
|
||||
if (date.getTime() <= in24Hours.getTime()) return "deadline-in24hours";
|
||||
return "deadline-upcoming";
|
||||
}
|
||||
|
||||
onMounted(async () => {
|
||||
const user = await auth.loadUser();
|
||||
username.value = user?.profile?.preferred_username ?? "";
|
||||
|
@ -80,7 +128,7 @@
|
|||
|
||||
<template>
|
||||
<div class="assignments-container">
|
||||
<h1>{{ t("assignments") }}</h1>
|
||||
<h1 class="h1">{{ t("assignments") }}</h1>
|
||||
|
||||
<v-btn
|
||||
v-if="isTeacher"
|
||||
|
@ -107,6 +155,13 @@
|
|||
{{ assignment.class.displayName }}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
class="assignment-deadline"
|
||||
:class="getDeadlineClass(assignment.deadline)"
|
||||
>
|
||||
{{ t("deadline") }}:
|
||||
<span>{{ formatDate(assignment.deadline) }}</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="spacer"></div>
|
||||
|
@ -131,6 +186,13 @@
|
|||
</v-card>
|
||||
</v-col>
|
||||
</v-row>
|
||||
<v-row v-if="assignments.length === 0">
|
||||
<v-col cols="12">
|
||||
<div class="no-assignments">
|
||||
{{ t("no-assignments") }}
|
||||
</div>
|
||||
</v-col>
|
||||
</v-row>
|
||||
</v-container>
|
||||
</div>
|
||||
</template>
|
||||
|
@ -139,18 +201,32 @@
|
|||
.assignments-container {
|
||||
width: 100%;
|
||||
margin: 0 auto;
|
||||
padding: 2% 4%;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.center-btn {
|
||||
display: block;
|
||||
margin-left: auto;
|
||||
margin-right: auto;
|
||||
margin: 0 auto 2rem auto;
|
||||
font-weight: 600;
|
||||
background-color: #10ad61;
|
||||
color: white;
|
||||
transition: background-color 0.2s;
|
||||
}
|
||||
.center-btn:hover {
|
||||
background-color: #0e6942;
|
||||
}
|
||||
|
||||
.assignment-card {
|
||||
padding: 1rem;
|
||||
padding: 1.25rem;
|
||||
border-radius: 16px;
|
||||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.08);
|
||||
background-color: white;
|
||||
transition:
|
||||
transform 0.2s,
|
||||
box-shadow 0.2s;
|
||||
}
|
||||
.assignment-card:hover {
|
||||
box-shadow: 0 6px 16px rgba(0, 0, 0, 0.12);
|
||||
}
|
||||
|
||||
.top-content {
|
||||
|
@ -158,6 +234,35 @@
|
|||
word-break: break-word;
|
||||
}
|
||||
|
||||
.assignment-title {
|
||||
font-weight: 700;
|
||||
font-size: 1.4rem;
|
||||
color: #0e6942;
|
||||
margin-bottom: 0.3rem;
|
||||
}
|
||||
|
||||
.assignment-class,
|
||||
.assignment-deadline {
|
||||
font-size: 0.95rem;
|
||||
color: #444;
|
||||
margin-bottom: 0.2rem;
|
||||
}
|
||||
|
||||
.class-name {
|
||||
font-weight: 600;
|
||||
color: #097180;
|
||||
}
|
||||
|
||||
.assignment-deadline.deadline-passed {
|
||||
color: #d32f2f;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.assignment-deadline.deadline-in24hours {
|
||||
color: #f57c00;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.spacer {
|
||||
flex: 1;
|
||||
}
|
||||
|
@ -165,24 +270,14 @@
|
|||
.button-row {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 0.5rem;
|
||||
gap: 0.75rem;
|
||||
flex-wrap: wrap;
|
||||
}
|
||||
|
||||
.assignment-title {
|
||||
font-weight: bold;
|
||||
font-size: 1.5rem;
|
||||
margin-bottom: 0.1rem;
|
||||
word-break: break-word;
|
||||
}
|
||||
|
||||
.assignment-class {
|
||||
color: #666;
|
||||
font-size: 0.95rem;
|
||||
}
|
||||
|
||||
.class-name {
|
||||
font-weight: 500;
|
||||
color: #333;
|
||||
.no-assignments {
|
||||
text-align: center;
|
||||
font-size: 1.2rem;
|
||||
color: #777;
|
||||
padding: 3rem 0;
|
||||
}
|
||||
</style>
|
||||
|
|
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 { useStudentAssignmentsQuery } from '@/queries/students';
|
|||
import type { AssignmentDTO } from '@dwengo-1/common/interfaces/assignment';
|
||||
import QuestionNotification from '@/components/QuestionNotification.vue';
|
||||
import QuestionBox from '@/components/QuestionBox.vue';
|
||||
import { AccountType } from "@dwengo-1/common/util/account-types";
|
||||
|
||||
const router = useRouter();
|
||||
const route = useRoute();
|
||||
|
@ -207,8 +208,10 @@ const router = useRouter();
|
|||
</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
|
||||
|
@ -217,9 +220,9 @@ const router = useRouter();
|
|||
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[] }"
|
||||
|
@ -231,7 +234,9 @@ const router = useRouter();
|
|||
: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
|
||||
|
@ -255,10 +260,12 @@ const router = useRouter();
|
|||
</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
|
||||
>
|
||||
|
@ -266,7 +273,7 @@ const router = useRouter();
|
|||
</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") }}
|
||||
|
|
|
@ -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>
|
31
frontend/tests/controllers/answers-controller.test.ts
Normal file
31
frontend/tests/controllers/answers-controller.test.ts
Normal file
|
@ -0,0 +1,31 @@
|
|||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { AnswerController } from "../../src/controllers/answers";
|
||||
import { Language } from "@dwengo-1/common/util/language";
|
||||
|
||||
describe("AnswerController Tests", () => {
|
||||
let controller: AnswerController;
|
||||
|
||||
beforeEach(() => {
|
||||
const loiDTO = {
|
||||
hruid: "u_test_multiple_choice",
|
||||
language: Language.English,
|
||||
version: 1,
|
||||
};
|
||||
const questionId = { learningObjectIdentifier: loiDTO, sequenceNumber: 1 };
|
||||
controller = new AnswerController(questionId);
|
||||
});
|
||||
|
||||
it("should fetch all answers", async () => {
|
||||
const result = await controller.getAll(true);
|
||||
expect(result).toHaveProperty("answers");
|
||||
expect(Array.isArray(result.answers)).toBe(true);
|
||||
expect(result.answers.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should fetch an answer by sequencenumber", async () => {
|
||||
const answerNumber = 1; // Example sequence number
|
||||
const result = await controller.getBy(answerNumber);
|
||||
expect(result).toHaveProperty("answer");
|
||||
expect(result.answer).toHaveProperty("sequenceNumber", answerNumber);
|
||||
});
|
||||
});
|
|
@ -5,7 +5,7 @@ describe("AssignmentController Tests", () => {
|
|||
let controller: AssignmentController;
|
||||
|
||||
beforeEach(() => {
|
||||
controller = new AssignmentController("8764b861-90a6-42e5-9732-c0d9eb2f55f9"); // Example class ID
|
||||
controller = new AssignmentController("X2J9QT"); // Example class ID (class01)
|
||||
});
|
||||
|
||||
it("should fetch all assignments", async () => {
|
||||
|
|
|
@ -3,7 +3,7 @@ import { GroupController } from "../../src/controllers/groups";
|
|||
|
||||
describe("Test controller groups", () => {
|
||||
it("Get groups", async () => {
|
||||
const classId = "8764b861-90a6-42e5-9732-c0d9eb2f55f9";
|
||||
const classId = "X2J9QT"; // Class01
|
||||
const assignmentNumber = 21000;
|
||||
|
||||
const controller = new GroupController(classId, assignmentNumber);
|
||||
|
|
|
@ -0,0 +1,28 @@
|
|||
import { LearningObjectController } from "../../src/controllers/learning-objects";
|
||||
import { Language } from "@dwengo-1/common/util/language";
|
||||
import { beforeEach, describe, expect, it } from "vitest";
|
||||
|
||||
describe("Test controller learning object", () => {
|
||||
let controller: LearningObjectController;
|
||||
|
||||
beforeEach(async () => {
|
||||
controller = new LearningObjectController();
|
||||
});
|
||||
|
||||
it("can get the metadata of a learning object", async () => {
|
||||
const result = await controller.getMetadata("u_id01", Language.English, 1);
|
||||
expect(result).not.toBeNull();
|
||||
for (const property of ["key", "version", "language", "title"]) {
|
||||
expect(result).toHaveProperty(property);
|
||||
}
|
||||
expect(result.key).toEqual("u_id01");
|
||||
expect(result.version).toEqual(1);
|
||||
expect(result.language).toEqual(Language.English);
|
||||
});
|
||||
|
||||
it("can get the HTML of a learning object", async () => {
|
||||
const result = await controller.getHTML("u_id01", Language.English, 1);
|
||||
expect(result).toHaveProperty("body");
|
||||
expect(result.body).toHaveProperty("innerHTML");
|
||||
});
|
||||
});
|
|
@ -15,7 +15,12 @@ describe("Test controller learning paths", () => {
|
|||
});
|
||||
|
||||
it("Can get learning path by id", async () => {
|
||||
const data = await controller.getAllByTheme("kiks");
|
||||
const data = await controller.getAllByThemeAndLanguage("kiks", Language.Dutch);
|
||||
expect(data).to.have.length.greaterThan(0);
|
||||
});
|
||||
|
||||
it("Can get all learning paths administrated by a certain user.", async () => {
|
||||
const data = await controller.getAllByAdminRaw("user");
|
||||
expect(data.length).toBe(0); // This user does not administrate any learning paths in the test data.
|
||||
});
|
||||
});
|
||||
|
|
30
frontend/tests/controllers/questions-controller.test.ts
Normal file
30
frontend/tests/controllers/questions-controller.test.ts
Normal file
|
@ -0,0 +1,30 @@
|
|||
import { describe, it, expect, beforeEach } from "vitest";
|
||||
import { Language } from "@dwengo-1/common/util/language";
|
||||
import { QuestionController } from "../../src/controllers/questions";
|
||||
|
||||
describe("QuestionController Tests", () => {
|
||||
let controller: QuestionController;
|
||||
|
||||
beforeEach(() => {
|
||||
const loiDTO = {
|
||||
hruid: "u_test_multiple_choice",
|
||||
language: Language.English,
|
||||
version: 1,
|
||||
};
|
||||
controller = new QuestionController(loiDTO);
|
||||
});
|
||||
|
||||
it("should fetch all questions", async () => {
|
||||
const result = await controller.getAll(true);
|
||||
expect(result).toHaveProperty("questions");
|
||||
expect(Array.isArray(result.questions)).toBe(true);
|
||||
expect(result.questions.length).toBeGreaterThan(0);
|
||||
});
|
||||
|
||||
it("should fetch an question by sequencenumber", async () => {
|
||||
const questionNumber = 1; // Example sequence number
|
||||
const result = await controller.getBy(questionNumber);
|
||||
expect(result).toHaveProperty("question");
|
||||
expect(result.question).toHaveProperty("sequenceNumber", questionNumber);
|
||||
});
|
||||
});
|
|
@ -1,5 +1,6 @@
|
|||
import { spawn } from "child_process";
|
||||
import { ChildProcess, spawnSync } from "node:child_process";
|
||||
import { getLogger } from "../../backend/src/logging/initalize";
|
||||
|
||||
let backendProcess: ChildProcess;
|
||||
|
||||
|
@ -35,6 +36,8 @@ export async function setup(): Promise<void> {
|
|||
|
||||
export async function teardown(): Promise<void> {
|
||||
if (backendProcess) {
|
||||
backendProcess.kill();
|
||||
while (!backendProcess.kill()) {
|
||||
getLogger().error(`Failed to kill backend process! Retrying...`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
@ -9,6 +9,7 @@ export default defineConfig({
|
|||
resolve: {
|
||||
alias: {
|
||||
"@": fileURLToPath(new URL("./src", import.meta.url)),
|
||||
"@dwengo-1/common": fileURLToPath(new URL("../common/src", import.meta.url)),
|
||||
},
|
||||
},
|
||||
build: {
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue