Merge branch 'dev' into feat/assignment-page

# Conflicts:
#	backend/package.json
#	common/src/interfaces/assignment.ts
#	frontend/src/controllers/learning-paths.ts
#	frontend/src/i18n/locale/de.json
#	frontend/src/i18n/locale/en.json
#	frontend/src/i18n/locale/fr.json
#	frontend/src/i18n/locale/nl.json
#	frontend/src/views/assignments/CreateAssignment.vue
#	package-lock.json
This commit is contained in:
Joyelle Ndagijimana 2025-04-19 16:58:08 +02:00
commit a421b1996a
123 changed files with 2428 additions and 2658 deletions

View file

@ -1,51 +0,0 @@
<script setup lang="ts">
import { Language } from "@/data-objects/language.ts";
import type { UseQueryReturnType } from "@tanstack/vue-query";
import { useLearningObjectHTMLQuery } from "@/queries/learning-objects.ts";
import UsingQueryResult from "@/components/UsingQueryResult.vue";
const props = defineProps<{ hruid: string; language: Language; version: number }>();
const learningObjectHtmlQueryResult: UseQueryReturnType<Document, Error> = useLearningObjectHTMLQuery(
() => props.hruid,
() => props.language,
() => props.version,
);
</script>
<template>
<using-query-result
:query-result="learningObjectHtmlQueryResult as UseQueryReturnType<Document, Error>"
v-slot="learningPathHtml: { data: Document }"
>
<div
class="learning-object-container"
v-html="learningPathHtml.data.body.innerHTML"
></div>
</using-query-result>
</template>
<style scoped>
.learning-object-container {
padding: 20px;
}
:deep(hr) {
margin-top: 10px;
margin-bottom: 10px;
}
:deep(li) {
margin-left: 30px;
margin-top: 5px;
margin-bottom: 5px;
}
:deep(img) {
max-width: 80%;
}
:deep(h2),
:deep(h3),
:deep(h4),
:deep(h5),
:deep(h6) {
margin-top: 10px;
}
</style>

View file

@ -0,0 +1,55 @@
<script setup lang="ts">
import UsingQueryResult from "@/components/UsingQueryResult.vue";
import { useGroupsQuery } from "@/queries/groups.ts";
import type { GroupsResponse } from "@/controllers/groups.ts";
import { useI18n } from "vue-i18n";
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
const { t } = useI18n();
const props = defineProps<{
classId: string;
assignmentNumber: number;
}>();
const model = defineModel<number | undefined>({ default: undefined });
const groupsQuery = useGroupsQuery(props.classId, props.assignmentNumber, true);
interface GroupSelectorOption {
groupNumber: number | undefined;
label: string;
}
function groupOptions(groups: GroupDTO[]): GroupSelectorOption[] {
return [...groups]
.sort((a, b) => a.groupNumber - b.groupNumber)
.map((group, index) => ({
groupNumber: group.groupNumber,
label: `${index + 1}`,
}));
}
</script>
<template>
<using-query-result
:query-result="groupsQuery"
v-slot="{ data }: { data: GroupsResponse }"
>
<v-select
:label="t('viewAsGroup')"
:items="groupOptions(data.groups)"
v-model="model"
item-title="label"
class="group-selector-cb"
variant="outlined"
clearable
></v-select>
</using-query-result>
</template>
<style scoped>
.group-selector-cb {
margin-top: 10px;
}
</style>

View file

@ -3,8 +3,8 @@
import type { LearningPath } from "@/data-objects/learning-paths/learning-path.ts";
import { computed, type ComputedRef, ref } from "vue";
import type { LearningObject } from "@/data-objects/learning-objects/learning-object.ts";
import { useRoute } from "vue-router";
import LearningObjectView from "@/views/learning-paths/LearningObjectView.vue";
import { useRoute, useRouter } from "vue-router";
import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue";
import { useI18n } from "vue-i18n";
import LearningPathSearchField from "@/components/LearningPathSearchField.vue";
import { useGetLearningPathQuery } from "@/queries/learning-paths.ts";
@ -12,30 +12,38 @@
import UsingQueryResult from "@/components/UsingQueryResult.vue";
import authService from "@/services/auth/auth-service.ts";
import { LearningPathNode } from "@/data-objects/learning-paths/learning-path-node.ts";
import LearningPathGroupSelector from "@/views/learning-paths/LearningPathGroupSelector.vue";
const router = useRouter();
const route = useRoute();
const { t } = useI18n();
const props = defineProps<{ hruid: string; language: Language; learningObjectHruid?: string }>();
const props = defineProps<{
hruid: string;
language: Language;
learningObjectHruid?: string;
}>();
interface Personalization {
forStudent?: string;
interface LearningPathPageQuery {
forGroup?: string;
assignmentNo?: string;
classId?: string;
}
const personalization = computed(() => {
if (route.query.forStudent || route.query.forGroup) {
const query = computed(() => route.query as LearningPathPageQuery);
const forGroup = computed(() => {
if (query.value.forGroup && query.value.assignmentNo && query.value.classId) {
return {
forStudent: route.query.forStudent,
forGroup: route.query.forGroup,
} as Personalization;
forGroup: parseInt(query.value.forGroup),
assignmentNo: parseInt(query.value.assignmentNo),
classId: query.value.classId,
};
}
return {
forStudent: authService.authState.user?.profile?.preferred_username,
} as Personalization;
return undefined;
});
const learningPathQueryResult = useGetLearningPathQuery(props.hruid, props.language, personalization);
const learningPathQueryResult = useGetLearningPathQuery(props.hruid, props.language, forGroup);
const learningObjectListQueryResult = useLearningObjectListForPathQuery(learningPathQueryResult.data);
@ -98,6 +106,25 @@
}
return "notCompleted";
}
const forGroupQueryParam = computed<number | undefined>({
get: () => route.query.forGroup,
set: async (value: number | undefined) => {
const query = structuredClone(route.query);
query.forGroup = value;
await router.push({ query });
},
});
async function assign(): Promise<void> {
await router.push({
path: "/assignment/create",
query: {
hruid: props.hruid,
language: props.language,
},
});
}
</script>
<template>
@ -109,64 +136,87 @@
v-model="navigationDrawerShown"
:width="350"
>
<v-list-item>
<template v-slot:title>
<div class="learning-path-title">{{ learningPath.data.title }}</div>
</template>
<template v-slot:subtitle>
<div>{{ learningPath.data.description }}</div>
</template>
</v-list-item>
<v-list-item>
<template v-slot:subtitle>
<p>
<v-icon
:color="COLORS.notCompleted"
:icon="ICONS.notCompleted"
></v-icon>
{{ t("legendNotCompletedYet") }}
</p>
<p>
<v-icon
:color="COLORS.completed"
:icon="ICONS.completed"
></v-icon>
{{ t("legendCompleted") }}
</p>
<p>
<v-icon
:color="COLORS.teacherExclusive"
:icon="ICONS.teacherExclusive"
></v-icon>
{{ t("legendTeacherExclusive") }}
</p>
</template>
</v-list-item>
<v-divider></v-divider>
<div v-if="props.learningObjectHruid">
<using-query-result
:query-result="learningObjectListQueryResult"
v-slot="learningObjects: { data: LearningObject[] }"
>
<template v-for="node in learningObjects.data">
<v-list-item
link
:to="{ path: node.key, query: route.query }"
:title="node.title"
:active="node.key === props.learningObjectHruid"
:key="node.key"
v-if="!node.teacherExclusive || authService.authState.activeRole === 'teacher'"
>
<template v-slot:prepend>
<v-icon
:color="COLORS[getNavItemState(node)]"
:icon="ICONS[getNavItemState(node)]"
></v-icon>
</template>
<template v-slot:append> {{ node.estimatedTime }}' </template>
</v-list-item>
<div class="d-flex flex-column h-100">
<v-list-item>
<template v-slot:title>
<div class="learning-path-title">{{ learningPath.data.title }}</div>
</template>
</using-query-result>
<template v-slot:subtitle>
<div>{{ learningPath.data.description }}</div>
</template>
</v-list-item>
<v-list-item>
<template v-slot:subtitle>
<p>
<v-icon
:color="COLORS.notCompleted"
:icon="ICONS.notCompleted"
></v-icon>
{{ t("legendNotCompletedYet") }}
</p>
<p>
<v-icon
:color="COLORS.completed"
:icon="ICONS.completed"
></v-icon>
{{ t("legendCompleted") }}
</p>
<p>
<v-icon
:color="COLORS.teacherExclusive"
:icon="ICONS.teacherExclusive"
></v-icon>
{{ t("legendTeacherExclusive") }}
</p>
</template>
</v-list-item>
<v-list-item
v-if="query.classId && query.assignmentNo && authService.authState.activeRole === 'teacher'"
>
<template v-slot:default>
<learning-path-group-selector
:class-id="query.classId"
:assignment-number="parseInt(query.assignmentNo)"
v-model="forGroupQueryParam"
/>
</template>
</v-list-item>
<v-divider></v-divider>
<div v-if="props.learningObjectHruid">
<using-query-result
:query-result="learningObjectListQueryResult"
v-slot="learningObjects: { data: LearningObject[] }"
>
<template v-for="node in learningObjects.data">
<v-list-item
link
:to="{ path: node.key, query: route.query }"
:title="node.title"
:active="node.key === props.learningObjectHruid"
:key="node.key"
v-if="!node.teacherExclusive || authService.authState.activeRole === 'teacher'"
>
<template v-slot:prepend>
<v-icon
:color="COLORS[getNavItemState(node)]"
:icon="ICONS[getNavItemState(node)]"
></v-icon>
</template>
<template v-slot:append> {{ node.estimatedTime }}' </template>
</v-list-item>
</template>
</using-query-result>
</div>
<v-spacer></v-spacer>
<v-list-item v-if="authService.authState.activeRole === 'teacher'">
<template v-slot:default>
<v-btn
class="button-in-nav"
@click="assign()"
>{{ t("assignLearningPath") }}</v-btn
>
</template>
</v-list-item>
</div>
</v-navigation-drawer>
<div class="control-bar-above-content">
@ -180,12 +230,15 @@
<learning-path-search-field></learning-path-search-field>
</div>
</div>
<learning-object-view
:hruid="currentNode.learningobjectHruid"
:language="currentNode.language"
:version="currentNode.version"
v-if="currentNode"
></learning-object-view>
<div class="learning-object-view-container">
<learning-object-view
:hruid="currentNode.learningobjectHruid"
:language="currentNode.language"
:version="currentNode.version"
:group="forGroup"
v-if="currentNode"
></learning-object-view>
</div>
<div class="navigation-buttons-container">
<v-btn
prepend-icon="mdi-chevron-left"
@ -221,9 +274,18 @@
display: flex;
justify-content: space-between;
}
.learning-object-view-container {
padding-left: 20px;
padding-right: 20px;
padding-bottom: 20px;
}
.navigation-buttons-container {
padding: 20px;
display: flex;
justify-content: space-between;
}
.button-in-nav {
margin-top: 10px;
margin-bottom: 10px;
}
</style>

View file

@ -9,11 +9,11 @@
import LearningPathsGrid from "@/components/LearningPathsGrid.vue";
const route = useRoute();
const { t } = useI18n();
const { t, locale } = useI18n();
const query = computed(() => route.query.query as string | undefined);
const searchQueryResults = useSearchLearningPathQuery(query);
const searchQueryResults = useSearchLearningPathQuery(query, locale);
</script>
<template>

View file

@ -0,0 +1,18 @@
export const essayQuestionAdapter: GiftAdapter = {
questionType: "Essay",
installListener(
questionElement: Element,
answerUpdateCallback: (newAnswer: string | number | object) => void,
): void {
const textArea = questionElement.querySelector("textarea")!;
textArea.addEventListener("input", () => {
answerUpdateCallback(textArea.value);
});
},
setAnswer(questionElement: Element, answer: string | number | object): void {
const textArea = questionElement.querySelector("textarea")!;
textArea.value = String(answer);
},
};

View file

@ -0,0 +1,8 @@
interface GiftAdapter {
questionType: string;
installListener(
questionElement: Element,
answerUpdateCallback: (newAnswer: string | number | object) => void,
): void;
setAnswer(questionElement: Element, answer: string | number | object): void;
}

View file

@ -0,0 +1,8 @@
import { multipleChoiceQuestionAdapter } from "@/views/learning-paths/gift-adapters/multiple-choice-question-adapter.ts";
import { essayQuestionAdapter } from "@/views/learning-paths/gift-adapters/essay-question-adapter.ts";
export const giftAdapters = [multipleChoiceQuestionAdapter, essayQuestionAdapter];
export function getGiftAdapterForType(questionType: string): GiftAdapter | undefined {
return giftAdapters.find((it) => it.questionType === questionType);
}

View file

@ -0,0 +1,27 @@
export const multipleChoiceQuestionAdapter: GiftAdapter = {
questionType: "MC",
installListener(
questionElement: Element,
answerUpdateCallback: (newAnswer: string | number | object) => void,
): void {
questionElement.querySelectorAll("input[type=radio]").forEach((element) => {
const input = element as HTMLInputElement;
input.addEventListener("change", () => {
answerUpdateCallback(parseInt(input.value));
});
// Optional: initialize value if already selected
if (input.checked) {
answerUpdateCallback(parseInt(input.value));
}
});
},
setAnswer(questionElement: Element, answer: string | number | object): void {
questionElement.querySelectorAll("input[type=radio]").forEach((element) => {
const input = element as HTMLInputElement;
input.checked = String(answer) === String(input.value);
});
},
};

View file

@ -0,0 +1,73 @@
<script setup lang="ts">
import { Language } from "@/data-objects/language.ts";
import type { UseQueryReturnType } from "@tanstack/vue-query";
import { useLearningObjectHTMLQuery } from "@/queries/learning-objects.ts";
import UsingQueryResult from "@/components/UsingQueryResult.vue";
import { computed, ref } from "vue";
import authService from "@/services/auth/auth-service.ts";
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";
const _isStudent = computed(() => authService.authState.activeRole === "student");
const props = defineProps<{
hruid: string;
language: Language;
version: number;
group?: { forGroup: number; assignmentNo: number; classId: string };
}>();
const learningObjectHtmlQueryResult: UseQueryReturnType<Document, Error> = useLearningObjectHTMLQuery(
() => props.hruid,
() => props.language,
() => props.version,
);
const currentSubmission = ref<SubmissionData>([]);
</script>
<template>
<using-query-result
:query-result="learningObjectHtmlQueryResult as UseQueryReturnType<Document, Error>"
v-slot="learningPathHtml: { data: Document }"
>
<learning-object-content-view
:learning-object-content="learningPathHtml.data"
v-model:submission-data="currentSubmission"
/>
<div class="content-submissions-spacer" />
<learning-object-submissions-view
v-if="props.group"
:group="props.group"
:hruid="props.hruid"
:language="props.language"
:version="props.version"
v-model:submission-data="currentSubmission"
/>
</using-query-result>
</template>
<style scoped>
:deep(hr) {
margin-top: 10px;
margin-bottom: 10px;
}
:deep(li) {
margin-left: 30px;
margin-top: 5px;
margin-bottom: 5px;
}
:deep(img) {
max-width: 80%;
}
:deep(h2),
:deep(h3),
:deep(h4),
:deep(h5),
:deep(h6) {
margin-top: 10px;
}
.content-submissions-spacer {
height: 20px;
}
</style>

View file

@ -0,0 +1,90 @@
<script setup lang="ts">
import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data";
import { getGiftAdapterForType } from "@/views/learning-paths/gift-adapters/gift-adapters.ts";
import { computed, nextTick, onMounted, watch } from "vue";
import { copyArrayWith } from "@/utils/array-utils.ts";
const props = defineProps<{
learningObjectContent: Document;
submissionData?: SubmissionData;
}>();
const emit = defineEmits<(e: "update:submissionData", value: SubmissionData) => void>();
const submissionData = computed<SubmissionData | undefined>({
get: () => props.submissionData,
set: (v?: SubmissionData): void => {
if (v) emit("update:submissionData", v);
},
});
function forEachQuestion(
doAction: (questionIndex: number, questionName: string, questionType: string, questionElement: Element) => void,
): void {
const questions = document.querySelectorAll(".gift-question");
questions.forEach((question) => {
const name = question.id.match(/gift-q(\d+)/)?.[1];
const questionType = question.className
.split(" ")
.find((it) => it.startsWith("gift-question-type"))
?.match(/gift-question-type-([^ ]*)/)?.[1];
if (!name || isNaN(parseInt(name)) || !questionType) return;
const index = parseInt(name) - 1;
doAction(index, name, questionType, question);
});
}
function attachQuestionListeners(): void {
forEachQuestion((index, _name, type, element) => {
getGiftAdapterForType(type)?.installListener(element, (newAnswer) => {
submissionData.value = copyArrayWith(index, newAnswer, submissionData.value ?? []);
});
});
}
function setAnswers(answers: SubmissionData): void {
forEachQuestion((index, _name, type, element) => {
const answer = answers[index];
if (answer !== null && answer !== undefined) {
getGiftAdapterForType(type)?.setAnswer(element, answer);
} else if (answer === undefined) {
answers[index] = null;
}
});
submissionData.value = answers;
}
onMounted(async () =>
nextTick(() => {
attachQuestionListeners();
setAnswers(props.submissionData ?? []);
}),
);
watch(
() => props.learningObjectContent,
async () => {
await nextTick();
attachQuestionListeners();
},
);
watch(
() => props.submissionData,
async () => {
await nextTick();
setAnswers(props.submissionData ?? []);
},
);
</script>
<template>
<div
class="learning-object-container"
v-html="learningObjectContent.body.innerHTML"
></div>
</template>
<style scoped></style>

View file

@ -0,0 +1 @@
export type SubmissionData = (string | number | object | null)[];

View file

@ -0,0 +1,61 @@
<script setup lang="ts">
import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission";
import { computed } from "vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const props = defineProps<{
allSubmissions: SubmissionDTO[];
}>();
const emit = defineEmits<(e: "submission-selected", submission: SubmissionDTO) => void>();
const headers = computed(() => [
{ title: "#", value: "submissionNo", width: "50px" },
{ title: t("submittedBy"), value: "submittedBy" },
{ title: t("timestamp"), value: "timestamp" },
{ title: "", key: "action", width: "70px", sortable: false },
]);
const data = computed(() =>
[...props.allSubmissions]
.sort((a, b) => (a.submissionNumber ?? 0) - (b.submissionNumber ?? 0))
.map((submission, index) => ({
submissionNo: index + 1,
submittedBy: `${submission.submitter.firstName} ${submission.submitter.lastName}`,
timestamp: submission.time ? new Date(submission.time).toLocaleString() : "-",
dto: submission,
})),
);
function selectSubmission(submission: SubmissionDTO): void {
emit("submission-selected", submission);
}
</script>
<template>
<v-card>
<v-card-title>{{ t("groupSubmissions") }}</v-card-title>
<v-card-text>
<v-data-table
:headers="headers"
:items="data"
density="compact"
hide-default-footer
:no-data-text="t('noSubmissionsYet')"
>
<template v-slot:[`item.action`]="{ item }">
<v-btn
density="compact"
variant="plain"
@click="selectSubmission(item.dto)"
>
{{ t("loadSubmission") }}
</v-btn>
</template>
</v-data-table>
</v-card-text>
</v-card>
</template>
<style scoped></style>

View file

@ -0,0 +1,97 @@
<script setup lang="ts">
import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data";
import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission";
import { Language } from "@/data-objects/language.ts";
import { useSubmissionsQuery } from "@/queries/submissions.ts";
import UsingQueryResult from "@/components/UsingQueryResult.vue";
import SubmitButton from "@/views/learning-paths/learning-object/submissions/SubmitButton.vue";
import { computed, watch } from "vue";
import LearningObjectSubmissionsTable from "@/views/learning-paths/learning-object/submissions/LearningObjectSubmissionsTable.vue";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const props = defineProps<{
submissionData?: SubmissionData;
hruid: string;
language: Language;
version: number;
group: { forGroup: number; assignmentNo: number; classId: string };
}>();
const emit = defineEmits<(e: "update:submissionData", value: SubmissionData) => void>();
const submissionQuery = useSubmissionsQuery(
() => props.hruid,
() => props.language,
() => props.version,
() => props.group.classId,
() => props.group.assignmentNo,
() => props.group.forGroup,
() => true,
);
function emitSubmissionData(submissionData: SubmissionData): void {
emit("update:submissionData", submissionData);
}
function emitSubmission(submission: SubmissionDTO): void {
emitSubmissionData(JSON.parse(submission.content));
}
watch(submissionQuery.data, () => {
const submissions = submissionQuery.data.value;
if (submissions && submissions.length > 0) {
emitSubmission(submissions[submissions.length - 1]);
} else {
emitSubmissionData([]);
}
});
const lastSubmission = computed<SubmissionData>(() => {
const submissions = submissionQuery.data.value;
if (!submissions || submissions.length === 0) {
return undefined;
}
return JSON.parse(submissions[submissions.length - 1].content);
});
const showSubmissionTable = computed(() => props.submissionData !== undefined && props.submissionData.length > 0);
const showIsDoneMessage = computed(() => lastSubmission.value !== undefined && lastSubmission.value.length === 0);
</script>
<template>
<using-query-result
:query-result="submissionQuery"
v-slot="submissions: { data: SubmissionDTO[] }"
>
<submit-button
:hruid="props.hruid"
:language="props.language"
:version="props.version"
:group="props.group"
:submission-data="props.submissionData"
:submissions="submissions.data"
/>
<div class="submit-submissions-spacer"></div>
<v-alert
icon="mdi-check"
:text="t('taskCompleted')"
type="success"
variant="tonal"
density="compact"
v-if="showIsDoneMessage"
></v-alert>
<learning-object-submissions-table
v-if="submissionQuery.data && showSubmissionTable"
:all-submissions="submissions.data"
@submission-selected="emitSubmission"
/>
</using-query-result>
</template>
<style scoped>
.submit-submissions-spacer {
height: 20px;
}
</style>

View file

@ -0,0 +1,98 @@
<script setup lang="ts">
import { computed } from "vue";
import authService from "@/services/auth/auth-service.ts";
import type { SubmissionData } from "@/views/learning-paths/learning-object/submission-data";
import { Language } from "@/data-objects/language.ts";
import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission";
import { useCreateSubmissionMutation } from "@/queries/submissions.ts";
import { deepEquals } from "@/utils/deep-equals.ts";
import type { UserProfile } from "oidc-client-ts";
import type { LearningObjectIdentifierDTO } from "@dwengo-1/common/interfaces/learning-content";
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
import type { GroupDTO } from "@dwengo-1/common/interfaces/group";
import { useI18n } from "vue-i18n";
const { t } = useI18n();
const props = defineProps<{
submissionData?: SubmissionData;
submissions: SubmissionDTO[];
hruid: string;
language: Language;
version: number;
group: { forGroup: number; assignmentNo: number; classId: string };
}>();
const {
isPending: submissionIsPending,
// - isError: submissionFailed,
// - error: submissionError,
// - isSuccess: submissionSuccess,
mutate: submitSolution,
} = useCreateSubmissionMutation();
const isStudent = computed(() => authService.authState.activeRole === "student");
const isSubmitDisabled = computed(() => {
if (!props.submissionData || props.submissions === undefined) {
return true;
}
if (props.submissionData.some((answer) => answer === null)) {
return false;
}
if (props.submissions.length === 0) {
return false;
}
return deepEquals(JSON.parse(props.submissions[props.submissions.length - 1].content), props.submissionData);
});
function submitCurrentAnswer(): void {
const { forGroup, assignmentNo, classId } = props.group;
const currentUser: UserProfile = authService.authState.user!.profile;
const learningObjectIdentifier: LearningObjectIdentifierDTO = {
hruid: props.hruid,
language: props.language,
version: props.version,
};
const submitter: StudentDTO = {
id: currentUser.preferred_username!,
username: currentUser.preferred_username!,
firstName: currentUser.given_name!,
lastName: currentUser.family_name!,
};
const group: GroupDTO = {
class: classId,
assignment: assignmentNo,
groupNumber: forGroup,
};
const submission: SubmissionDTO = {
learningObjectIdentifier,
submitter,
group,
content: JSON.stringify(props.submissionData),
};
submitSolution({ data: submission });
}
const buttonText = computed(() => {
if (props.submissionData && props.submissionData.length === 0) {
return t("markAsDone");
}
return t(props.submissions.length > 0 ? "submitNewSolution" : "submitSolution");
});
</script>
<template>
<v-btn
v-if="isStudent && !isSubmitDisabled"
prepend-icon="mdi-check"
variant="elevated"
:loading="submissionIsPending"
:disabled="isSubmitDisabled"
@click="submitCurrentAnswer()"
>
{{ buttonText }}
</v-btn>
</template>
<style scoped></style>