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:
		
						commit
						a421b1996a
					
				
					 123 changed files with 2428 additions and 2658 deletions
				
			
		|  | @ -10,12 +10,14 @@ | |||
|         "preview": "vite preview", | ||||
|         "type-check": "vue-tsc --build", | ||||
|         "format": "prettier --write src/", | ||||
|         "test:e2e": "playwright test", | ||||
|         "format-check": "prettier --check src/", | ||||
|         "lint": "eslint . --fix", | ||||
|         "test:unit": "vitest --run", | ||||
|         "test:e2e": "playwright test" | ||||
|         "pretest:unit": "tsx ../docs/api/generate.ts && npm run build", | ||||
|         "test:unit": "vitest --run" | ||||
|     }, | ||||
|     "dependencies": { | ||||
|         "@dwengo-1/common": "^0.1.1", | ||||
|         "@tanstack/react-query": "^5.69.0", | ||||
|         "@tanstack/vue-query": "^5.69.0", | ||||
|         "@vueuse/core": "^13.1.0", | ||||
|  |  | |||
|  | @ -1,11 +1,11 @@ | |||
| import { BaseController } from "./base-controller"; | ||||
| import type { AssignmentDTO } from "@dwengo-1/common/interfaces/assignment"; | ||||
| import type { AssignmentDTO, AssignmentDTOId } from "@dwengo-1/common/interfaces/assignment"; | ||||
| import type { SubmissionsResponse } from "./submissions"; | ||||
| import type { QuestionsResponse } from "./questions"; | ||||
| import type { GroupsResponse } from "./groups"; | ||||
| 
 | ||||
| export interface AssignmentsResponse { | ||||
|     assignments: AssignmentDTO[] | string[]; | ||||
|     assignments: AssignmentDTO[] | AssignmentDTOId[]; | ||||
| } | ||||
| 
 | ||||
| export interface AssignmentResponse { | ||||
|  |  | |||
|  | @ -1,10 +1,10 @@ | |||
| import { BaseController } from "./base-controller"; | ||||
| import type { GroupDTO } from "@dwengo-1/common/interfaces/group"; | ||||
| import type { GroupDTO, GroupDTOId } from "@dwengo-1/common/interfaces/group"; | ||||
| import type { SubmissionsResponse } from "./submissions"; | ||||
| import type { QuestionsResponse } from "./questions"; | ||||
| 
 | ||||
| export interface GroupsResponse { | ||||
|     groups: GroupDTO[]; | ||||
|     groups: GroupDTO[] | GroupDTOId[]; | ||||
| } | ||||
| 
 | ||||
| export interface GroupResponse { | ||||
|  |  | |||
|  | @ -1,35 +1,33 @@ | |||
| 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 { 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"; | ||||
| 
 | ||||
| export class LearningPathController extends BaseController { | ||||
|     constructor() { | ||||
|         super("learningPath"); | ||||
|     } | ||||
| 
 | ||||
|     async search(query: string): Promise<LearningPath[]> { | ||||
|         const dtos = await this.get<LearningPathDTO[]>("/", {search: query}); | ||||
|     async search(query: string, language: string): Promise<LearningPath[]> { | ||||
|         const dtos = await this.get<LearningPathDTO[]>("/", { search: query, language }); | ||||
|         return dtos.map((dto) => LearningPath.fromDTO(dto)); | ||||
|     } | ||||
| 
 | ||||
|     async getBy( | ||||
|         hruid: string, | ||||
|         language: Language, | ||||
|         options?: { forGroup?: string; forStudent?: string }, | ||||
|         forGroup?: { forGroup: number; assignmentNo: number; classId: string }, | ||||
|     ): Promise<LearningPath> { | ||||
|         const dtos = await this.get<LearningPathDTO[]>("/", { | ||||
|             hruid, | ||||
|             language, | ||||
|             forGroup: options?.forGroup, | ||||
|             forStudent: options?.forStudent, | ||||
|             forGroup: forGroup?.forGroup, | ||||
|             assignmentNo: forGroup?.assignmentNo, | ||||
|             classId: forGroup?.classId, | ||||
|         }); | ||||
|         return LearningPath.fromDTO(single(dtos)); | ||||
|     } | ||||
| 
 | ||||
|     async getAllByTheme(theme: string): Promise<LearningPath[]> { | ||||
|         const dtos = await this.get<LearningPathDTO[]>("/", {theme}); | ||||
|         const dtos = await this.get<LearningPathDTO[]>("/", { theme }); | ||||
|         return dtos.map((dto) => LearningPath.fromDTO(dto)); | ||||
|     } | ||||
| 
 | ||||
|  |  | |||
|  | @ -1,5 +1,6 @@ | |||
| import { BaseController } from "./base-controller"; | ||||
| import type { SubmissionDTO, SubmissionDTOId } from "@dwengo-1/common/interfaces/submission"; | ||||
| import type { Language } from "@dwengo-1/common/util/language"; | ||||
| 
 | ||||
| export interface SubmissionsResponse { | ||||
|     submissions: SubmissionDTO[] | SubmissionDTOId[]; | ||||
|  | @ -10,16 +11,36 @@ export interface SubmissionResponse { | |||
| } | ||||
| 
 | ||||
| export class SubmissionController extends BaseController { | ||||
|     constructor(classid: string, assignmentNumber: number, groupNumber: number) { | ||||
|         super(`class/${classid}/assignments/${assignmentNumber}/groups/${groupNumber}/submissions`); | ||||
|     constructor(hruid: string) { | ||||
|         super(`learningObject/${hruid}/submissions`); | ||||
|     } | ||||
| 
 | ||||
|     async getAll(full = true): Promise<SubmissionsResponse> { | ||||
|         return this.get<SubmissionsResponse>(`/`, { full }); | ||||
|     async getAll( | ||||
|         language: Language, | ||||
|         version: number, | ||||
|         classId: string, | ||||
|         assignmentId: number, | ||||
|         groupId?: number, | ||||
|         full = true, | ||||
|     ): Promise<SubmissionsResponse> { | ||||
|         return this.get<SubmissionsResponse>(`/`, { language, version, classId, assignmentId, groupId, full }); | ||||
|     } | ||||
| 
 | ||||
|     async getByNumber(submissionNumber: number): Promise<SubmissionResponse> { | ||||
|         return this.get<SubmissionResponse>(`/${submissionNumber}`); | ||||
|     async getByNumber( | ||||
|         language: Language, | ||||
|         version: number, | ||||
|         classId: string, | ||||
|         assignmentId: number, | ||||
|         groupId: number, | ||||
|         submissionNumber: number, | ||||
|     ): Promise<SubmissionResponse> { | ||||
|         return this.get<SubmissionResponse>(`/${submissionNumber}`, { | ||||
|             language, | ||||
|             version, | ||||
|             classId, | ||||
|             assignmentId, | ||||
|             groupId, | ||||
|         }); | ||||
|     } | ||||
| 
 | ||||
|     async createSubmission(data: SubmissionDTO): Promise<SubmissionResponse> { | ||||
|  |  | |||
|  | @ -1,13 +1,13 @@ | |||
| { | ||||
|     "welcome": "Willkommen", | ||||
|     "student": "schüler", | ||||
|     "teacher": "lehrer", | ||||
|     "student": "Schüler", | ||||
|     "teacher": "Lehrer", | ||||
|     "assignments": "Aufgaben", | ||||
|     "classes": "Klasses", | ||||
|     "classes": "Klassen", | ||||
|     "discussions": "Diskussionen", | ||||
|     "login": "einloggen", | ||||
|     "logout": "ausloggen", | ||||
|     "cancel": "kündigen", | ||||
|     "cancel": "abbrechen", | ||||
|     "logoutVerification": "Sind Sie sicher, dass Sie sich abmelden wollen?", | ||||
|     "homeTitle": "Unsere Stärken", | ||||
|     "homeIntroduction1": "Wir entwickeln innovative Workshops und Bildungsressourcen, die wir in Zusammenarbeit mit Lehrern und Freiwilligen Schülern auf der ganzen Welt zur Verfügung stellen. Unsere Train-the-Trainer-Sitzungen ermöglichen es ihnen, unsere praktischen Workshops an die Schüler weiterzugeben.", | ||||
|  | @ -23,10 +23,10 @@ | |||
|     "submitCode": "senden", | ||||
|     "members": "Mitglieder", | ||||
|     "themes": "Themen", | ||||
|     "choose-theme": "Wähle ein thema", | ||||
|     "choose-theme": "Wählen Sie ein Thema", | ||||
|     "choose-age": "Alter auswählen", | ||||
|     "theme-options": { | ||||
|         "all": "Alle themen", | ||||
|         "all": "Alle Themen", | ||||
|         "culture": "Kultur", | ||||
|         "electricity-and-mechanics": "Elektrizität und Mechanik", | ||||
|         "nature-and-climate": "Natur und Klima", | ||||
|  | @ -37,11 +37,11 @@ | |||
|         "algorithms": "Algorithmisches Denken" | ||||
|     }, | ||||
|     "age-options": { | ||||
|         "all": "Alle altersgruppen", | ||||
|         "all": "Alle Altersgruppen", | ||||
|         "primary-school": "Grundschule", | ||||
|         "lower-secondary": "12-14 jahre alt", | ||||
|         "upper-secondary": "14-16 jahre alt", | ||||
|         "high-school": "16-18 jahre alt", | ||||
|         "lower-secondary": "12-14 Jahre alt", | ||||
|         "upper-secondary": "14-16 Jahre alt", | ||||
|         "high-school": "16-18 Jahre alt", | ||||
|         "older": "18 und älter" | ||||
|     }, | ||||
|     "read-more": "Mehr lesen", | ||||
|  | @ -86,9 +86,20 @@ | |||
|     "accept": "akzeptieren", | ||||
|     "deny": "ablehnen", | ||||
|     "sent": "sent", | ||||
|     "failed": "gescheitert", | ||||
|     "failed": "fehlgeschlagen", | ||||
|     "wrong": "etwas ist schief gelaufen", | ||||
|     "created": "erstellt", | ||||
|     "submitSolution": "Lösung einreichen", | ||||
|     "submitNewSolution": "Neue Lösung einreichen", | ||||
|     "markAsDone": "Als fertig markieren", | ||||
|     "groupSubmissions": "Einreichungen dieser Gruppe", | ||||
|     "taskCompleted": "Aufgabe erledigt.", | ||||
|     "submittedBy": "Eingereicht von", | ||||
|     "timestamp": "Zeitpunkt", | ||||
|     "loadSubmission": "Einladen", | ||||
|     "noSubmissionsYet": "Noch keine Lösungen eingereicht.", | ||||
|     "viewAsGroup": "Fortschritt ansehen von Gruppe...", | ||||
|     "assignLearningPath": "Als Aufgabe geben" | ||||
|     "group": "Gruppe", | ||||
|     "description": "Beschreibung", | ||||
|     "no-submission": "keine vorlage", | ||||
|  |  | |||
|  | @ -89,6 +89,17 @@ | |||
|     "failed": "failed", | ||||
|     "wrong": "something went wrong", | ||||
|     "created": "created", | ||||
|     "submitSolution": "Submit solution", | ||||
|     "submitNewSolution": "Submit new solution", | ||||
|     "markAsDone": "Mark as completed", | ||||
|     "groupSubmissions": "This group's submissions", | ||||
|     "taskCompleted": "Task completed.", | ||||
|     "submittedBy": "Submitted by", | ||||
|     "timestamp": "Timestamp", | ||||
|     "loadSubmission": "Load", | ||||
|     "noSubmissionsYet": "No submissions yet.", | ||||
|     "viewAsGroup": "View progress of group...", | ||||
|     "assignLearningPath": "assign", | ||||
|     "group": "Group", | ||||
|     "description": "Description", | ||||
|     "no-submission": "no submission", | ||||
|  |  | |||
|  | @ -89,6 +89,17 @@ | |||
|     "failed": "échoué", | ||||
|     "wrong": "quelque chose n'a pas fonctionné", | ||||
|     "created": "créé", | ||||
|     "submitSolution": "Soumettre la solution", | ||||
|     "submitNewSolution": "Soumettre une nouvelle solution", | ||||
|     "markAsDone": "Marquer comme terminé", | ||||
|     "groupSubmissions": "Soumissions de ce groupe", | ||||
|     "taskCompleted": "Tâche terminée.", | ||||
|     "submittedBy": "Soumis par", | ||||
|     "timestamp": "Horodatage", | ||||
|     "loadSubmission": "Charger", | ||||
|     "noSubmissionsYet": "Pas encore de soumissions.", | ||||
|     "viewAsGroup": "Voir la progression du groupe...", | ||||
|     "assignLearningPath": "donner comme tâche" | ||||
|     "group": "Groupe", | ||||
|     "description": "Description", | ||||
|     "no-submission": "aucune soumission", | ||||
|  |  | |||
|  | @ -89,6 +89,17 @@ | |||
|     "failed": "mislukt", | ||||
|     "wrong": "er ging iets verkeerd", | ||||
|     "created": "gecreëerd", | ||||
|     "submitSolution": "Oplossing indienen", | ||||
|     "submitNewSolution": "Nieuwe oplossing indienen", | ||||
|     "markAsDone": "Markeren als afgewerkt", | ||||
|     "groupSubmissions": "Indieningen van deze groep", | ||||
|     "taskCompleted": "Taak afgewerkt.", | ||||
|     "submittedBy": "Ingediend door", | ||||
|     "timestamp": "Tijdstip", | ||||
|     "loadSubmission": "Inladen", | ||||
|     "noSubmissionsYet": "Nog geen indieningen.", | ||||
|     "viewAsGroup": "Vooruitgang bekijken van groep...", | ||||
|     "assignLearningPath": "Als opdracht geven" | ||||
|     "group": "Groep", | ||||
|     "description": "Beschrijving", | ||||
|     "no-submission": "geen indiening", | ||||
|  |  | |||
|  | @ -10,7 +10,7 @@ import { | |||
|     useQueryClient, | ||||
|     type UseQueryReturnType, | ||||
| } from "@tanstack/vue-query"; | ||||
| import { computed, type MaybeRefOrGetter, toValue } from "vue"; | ||||
| import { computed, toValue, type MaybeRefOrGetter } from "vue"; | ||||
| import { invalidateAllSubmissionKeys } from "./submissions"; | ||||
| 
 | ||||
| type GroupsQueryKey = ["groups", string, number, boolean]; | ||||
|  | @ -160,7 +160,7 @@ export function useDeleteGroupMutation(): UseMutationReturnType< | |||
|             const gn = response.group.groupNumber; | ||||
| 
 | ||||
|             await invalidateAllGroupKeys(queryClient, cid, an, gn); | ||||
|             await invalidateAllSubmissionKeys(queryClient, cid, an, gn); | ||||
|             await invalidateAllSubmissionKeys(queryClient, undefined, undefined, undefined, cid, an, gn); | ||||
|         }, | ||||
|     }); | ||||
| } | ||||
|  |  | |||
|  | @ -5,7 +5,7 @@ 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"; | ||||
| 
 | ||||
| const LEARNING_OBJECT_KEY = "learningObject"; | ||||
| export const LEARNING_OBJECT_KEY = "learningObject"; | ||||
| const learningObjectController = getLearningObjectController(); | ||||
| 
 | ||||
| export function useLearningObjectMetadataQuery( | ||||
|  |  | |||
|  | @ -4,19 +4,19 @@ import {useQuery, type UseQueryReturnType} from "@tanstack/vue-query"; | |||
| import {getLearningPathController} from "@/controllers/controllers"; | ||||
| import type {LearningPath} from "@/data-objects/learning-paths/learning-path.ts"; | ||||
| 
 | ||||
| const LEARNING_PATH_KEY = "learningPath"; | ||||
| export const LEARNING_PATH_KEY = "learningPath"; | ||||
| const learningPathController = getLearningPathController(); | ||||
| 
 | ||||
| export function useGetLearningPathQuery( | ||||
|     hruid: MaybeRefOrGetter<string>, | ||||
|     language: MaybeRefOrGetter<Language>, | ||||
|     options?: MaybeRefOrGetter<{ forGroup?: string; forStudent?: string }>, | ||||
|     forGroup?: MaybeRefOrGetter<{ forGroup: number; assignmentNo: number; classId: string } | undefined>, | ||||
| ): UseQueryReturnType<LearningPath, Error> { | ||||
|     return useQuery({ | ||||
|         queryKey: [LEARNING_PATH_KEY, "get", hruid, language, options], | ||||
|         queryKey: [LEARNING_PATH_KEY, "get", hruid, language, forGroup], | ||||
|         queryFn: async () => { | ||||
|             const [hruidVal, languageVal, optionsVal] = [toValue(hruid), toValue(language), toValue(options)]; | ||||
|             return learningPathController.getBy(hruidVal, languageVal, optionsVal); | ||||
|             const [hruidVal, languageVal, forGroupVal] = [toValue(hruid), toValue(language), toValue(forGroup)]; | ||||
|             return learningPathController.getBy(hruidVal, languageVal, forGroupVal); | ||||
|         }, | ||||
|         enabled: () => Boolean(toValue(hruid)) && Boolean(toValue(language)), | ||||
|     }); | ||||
|  | @ -34,12 +34,14 @@ export function useGetAllLearningPathsByThemeQuery( | |||
| 
 | ||||
| export function useSearchLearningPathQuery( | ||||
|     query: MaybeRefOrGetter<string | undefined>, | ||||
|     language: MaybeRefOrGetter<string | undefined>, | ||||
| ): UseQueryReturnType<LearningPath[], Error> { | ||||
|     return useQuery({ | ||||
|         queryKey: [LEARNING_PATH_KEY, "search", query], | ||||
|         queryKey: [LEARNING_PATH_KEY, "search", query, language], | ||||
|         queryFn: async () => { | ||||
|             const queryVal = toValue(query)!; | ||||
|             return learningPathController.search(queryVal); | ||||
|             const languageVal = toValue(language)!; | ||||
|             return learningPathController.search(queryVal, languageVal); | ||||
|         }, | ||||
|         enabled: () => Boolean(toValue(query)), | ||||
|     }); | ||||
|  |  | |||
|  | @ -1,39 +1,39 @@ | |||
| import { SubmissionController, type SubmissionResponse, type SubmissionsResponse } from "@/controllers/submissions"; | ||||
| import { SubmissionController, type SubmissionResponse } from "@/controllers/submissions"; | ||||
| import type { SubmissionDTO } from "@dwengo-1/common/interfaces/submission"; | ||||
| import { | ||||
|     QueryClient, | ||||
|     useMutation, | ||||
|     type UseMutationReturnType, | ||||
|     useQuery, | ||||
|     useQueryClient, | ||||
|     type UseMutationReturnType, | ||||
|     type UseQueryReturnType, | ||||
| } from "@tanstack/vue-query"; | ||||
| import { computed, toValue, type MaybeRefOrGetter } from "vue"; | ||||
| import { computed, type MaybeRefOrGetter, toValue } from "vue"; | ||||
| import { LEARNING_PATH_KEY } from "@/queries/learning-paths.ts"; | ||||
| import { LEARNING_OBJECT_KEY } from "@/queries/learning-objects.ts"; | ||||
| import type { Language } from "@dwengo-1/common/util/language"; | ||||
| 
 | ||||
| type SubmissionsQueryKey = ["submissions", string, number, number, boolean]; | ||||
| export const SUBMISSION_KEY = "submissions"; | ||||
| 
 | ||||
| function submissionsQueryKey( | ||||
|     classid: string, | ||||
|     assignmentNumber: number, | ||||
|     groupNumber: number, | ||||
|     full: boolean, | ||||
| ): SubmissionsQueryKey { | ||||
|     return ["submissions", classid, assignmentNumber, groupNumber, full]; | ||||
| } | ||||
| 
 | ||||
| type SubmissionQueryKey = ["submission", string, number, number, number]; | ||||
| type SubmissionQueryKey = ["submission", string, Language | undefined, number, string, number, number, number]; | ||||
| 
 | ||||
| function submissionQueryKey( | ||||
|     hruid: string, | ||||
|     language: Language, | ||||
|     version: number, | ||||
|     classid: string, | ||||
|     assignmentNumber: number, | ||||
|     groupNumber: number, | ||||
|     submissionNumber: number, | ||||
| ): SubmissionQueryKey { | ||||
|     return ["submission", classid, assignmentNumber, groupNumber, submissionNumber]; | ||||
|     return ["submission", hruid, language, version, classid, assignmentNumber, groupNumber, submissionNumber]; | ||||
| } | ||||
| 
 | ||||
| export async function invalidateAllSubmissionKeys( | ||||
|     queryClient: QueryClient, | ||||
|     hruid?: string, | ||||
|     language?: Language, | ||||
|     version?: number, | ||||
|     classid?: string, | ||||
|     assignmentNumber?: number, | ||||
|     groupNumber?: number, | ||||
|  | @ -43,101 +43,134 @@ export async function invalidateAllSubmissionKeys( | |||
| 
 | ||||
|     await Promise.all( | ||||
|         keys.map(async (key) => { | ||||
|             const queryKey = [key, classid, assignmentNumber, groupNumber, submissionNumber].filter( | ||||
|                 (arg) => arg !== undefined, | ||||
|             ); | ||||
|             const queryKey = [ | ||||
|                 key, | ||||
|                 hruid, | ||||
|                 language, | ||||
|                 version, | ||||
|                 classid, | ||||
|                 assignmentNumber, | ||||
|                 groupNumber, | ||||
|                 submissionNumber, | ||||
|             ].filter((arg) => arg !== undefined); | ||||
|             return queryClient.invalidateQueries({ queryKey: queryKey }); | ||||
|         }), | ||||
|     ); | ||||
| 
 | ||||
|     await queryClient.invalidateQueries({ | ||||
|         queryKey: ["submissions", classid, assignmentNumber, groupNumber].filter((arg) => arg !== undefined), | ||||
|         queryKey: ["submissions", hruid, language, version, classid, assignmentNumber, groupNumber].filter( | ||||
|             (arg) => arg !== undefined, | ||||
|         ), | ||||
|     }); | ||||
|     await queryClient.invalidateQueries({ | ||||
|         queryKey: ["group-submissions", classid, assignmentNumber, groupNumber].filter((arg) => arg !== undefined), | ||||
|         queryKey: ["group-submissions", hruid, language, version, classid, assignmentNumber, groupNumber].filter( | ||||
|             (arg) => arg !== undefined, | ||||
|         ), | ||||
|     }); | ||||
|     await queryClient.invalidateQueries({ | ||||
|         queryKey: ["assignment-submissions", classid, assignmentNumber].filter((arg) => arg !== undefined), | ||||
|         queryKey: ["assignment-submissions", hruid, language, version, classid, assignmentNumber].filter( | ||||
|             (arg) => arg !== undefined, | ||||
|         ), | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| function checkEnabled( | ||||
|     classid: string | undefined, | ||||
|     assignmentNumber: number | undefined, | ||||
|     groupNumber: number | undefined, | ||||
|     submissionNumber: number | undefined, | ||||
| ): boolean { | ||||
|     return ( | ||||
|         Boolean(classid) && | ||||
|         !isNaN(Number(groupNumber)) && | ||||
|         !isNaN(Number(assignmentNumber)) && | ||||
|         !isNaN(Number(submissionNumber)) | ||||
|     ); | ||||
| } | ||||
| 
 | ||||
| interface Values { | ||||
|     cid: string | undefined; | ||||
|     an: number | undefined; | ||||
|     gn: number | undefined; | ||||
|     sn: number | undefined; | ||||
|     f: boolean; | ||||
| } | ||||
| 
 | ||||
| function toValues( | ||||
|     classid: MaybeRefOrGetter<string | undefined>, | ||||
|     assignmentNumber: MaybeRefOrGetter<number | undefined>, | ||||
|     groupNumber: MaybeRefOrGetter<number | undefined>, | ||||
|     submissionNumber: MaybeRefOrGetter<number | undefined>, | ||||
|     full: MaybeRefOrGetter<boolean>, | ||||
| ): Values { | ||||
|     return { | ||||
|         cid: toValue(classid), | ||||
|         an: toValue(assignmentNumber), | ||||
|         gn: toValue(groupNumber), | ||||
|         sn: toValue(submissionNumber), | ||||
|         f: toValue(full), | ||||
|     }; | ||||
| function checkEnabled(properties: MaybeRefOrGetter<unknown>[]): boolean { | ||||
|     return properties.every((prop) => Boolean(toValue(prop))); | ||||
| } | ||||
| 
 | ||||
| export function useSubmissionsQuery( | ||||
|     hruid: MaybeRefOrGetter<string | undefined>, | ||||
|     language: MaybeRefOrGetter<Language | undefined>, | ||||
|     version: MaybeRefOrGetter<number | undefined>, | ||||
|     classid: MaybeRefOrGetter<string | undefined>, | ||||
|     assignmentNumber: MaybeRefOrGetter<number | undefined>, | ||||
|     groupNumber: MaybeRefOrGetter<number | undefined>, | ||||
|     full: MaybeRefOrGetter<boolean> = true, | ||||
| ): UseQueryReturnType<SubmissionsResponse, Error> { | ||||
|     const { cid, an, gn, sn, f } = toValues(classid, assignmentNumber, groupNumber, 1, full); | ||||
| 
 | ||||
| ): UseQueryReturnType<SubmissionDTO[], Error> { | ||||
|     return useQuery({ | ||||
|         queryKey: computed(() => submissionsQueryKey(cid!, an!, gn!, f)), | ||||
|         queryFn: async () => new SubmissionController(cid!, an!, gn!).getAll(f), | ||||
|         enabled: () => checkEnabled(cid, an, gn, sn), | ||||
|         queryKey: ["submissions", hruid, language, version, classid, assignmentNumber, groupNumber, full], | ||||
|         queryFn: async () => { | ||||
|             const hruidVal = toValue(hruid); | ||||
|             const languageVal = toValue(language); | ||||
|             const versionVal = toValue(version); | ||||
|             const classIdVal = toValue(classid); | ||||
|             const assignmentNumberVal = toValue(assignmentNumber); | ||||
|             const groupNumberVal = toValue(groupNumber); | ||||
|             const fullVal = toValue(full); | ||||
| 
 | ||||
|             const response = await new SubmissionController(hruidVal!).getAll( | ||||
|                 languageVal, | ||||
|                 versionVal!, | ||||
|                 classIdVal!, | ||||
|                 assignmentNumberVal!, | ||||
|                 groupNumberVal, | ||||
|                 fullVal, | ||||
|             ); | ||||
|             return response ? (response.submissions as SubmissionDTO[]) : undefined; | ||||
|         }, | ||||
|         enabled: () => checkEnabled([hruid, language, version, classid, assignmentNumber]), | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| export function useSubmissionQuery( | ||||
|     hruid: MaybeRefOrGetter<string | undefined>, | ||||
|     language: MaybeRefOrGetter<Language | undefined>, | ||||
|     version: MaybeRefOrGetter<number | undefined>, | ||||
|     classid: MaybeRefOrGetter<string | undefined>, | ||||
|     assignmentNumber: MaybeRefOrGetter<number | undefined>, | ||||
|     groupNumber: MaybeRefOrGetter<number | undefined>, | ||||
|     submissionNumber: MaybeRefOrGetter<number | undefined>, | ||||
| ): UseQueryReturnType<SubmissionResponse, Error> { | ||||
|     const { cid, an, gn, sn } = toValues(classid, assignmentNumber, groupNumber, 1, true); | ||||
|     const hruidVal = toValue(hruid); | ||||
|     const languageVal = toValue(language); | ||||
|     const versionVal = toValue(version); | ||||
|     const classIdVal = toValue(classid); | ||||
|     const assignmentNumberVal = toValue(assignmentNumber); | ||||
|     const groupNumberVal = toValue(groupNumber); | ||||
|     const submissionNumberVal = toValue(submissionNumber); | ||||
| 
 | ||||
|     return useQuery({ | ||||
|         queryKey: computed(() => submissionQueryKey(cid!, an!, gn!, sn!)), | ||||
|         queryFn: async () => new SubmissionController(cid!, an!, gn!).getByNumber(sn!), | ||||
|         enabled: () => checkEnabled(cid, an, gn, sn), | ||||
|         queryKey: computed(() => | ||||
|             submissionQueryKey( | ||||
|                 hruidVal!, | ||||
|                 languageVal, | ||||
|                 versionVal!, | ||||
|                 classIdVal!, | ||||
|                 assignmentNumberVal!, | ||||
|                 groupNumberVal!, | ||||
|                 submissionNumberVal!, | ||||
|             ), | ||||
|         ), | ||||
|         queryFn: async () => | ||||
|             new SubmissionController(hruidVal!).getByNumber( | ||||
|                 languageVal, | ||||
|                 versionVal!, | ||||
|                 classIdVal!, | ||||
|                 assignmentNumberVal!, | ||||
|                 groupNumberVal!, | ||||
|                 submissionNumberVal!, | ||||
|             ), | ||||
|         enabled: () => | ||||
|             Boolean(hruidVal) && | ||||
|             Boolean(languageVal) && | ||||
|             Boolean(versionVal) && | ||||
|             Boolean(classIdVal) && | ||||
|             Boolean(assignmentNumberVal) && | ||||
|             Boolean(submissionNumber), | ||||
|     }); | ||||
| } | ||||
| 
 | ||||
| export function useCreateSubmissionMutation(): UseMutationReturnType< | ||||
|     SubmissionResponse, | ||||
|     Error, | ||||
|     { cid: string; an: number; gn: number; data: SubmissionDTO }, | ||||
|     { data: SubmissionDTO }, | ||||
|     unknown | ||||
| > { | ||||
|     const queryClient = useQueryClient(); | ||||
| 
 | ||||
|     return useMutation({ | ||||
|         mutationFn: async ({ cid, an, gn, data }) => new SubmissionController(cid, an, gn).createSubmission(data), | ||||
|         mutationFn: async ({ data }) => | ||||
|             new SubmissionController(data.learningObjectIdentifier.hruid).createSubmission(data), | ||||
|         onSuccess: async (response) => { | ||||
|             if (!response.submission.group) { | ||||
|                 await invalidateAllSubmissionKeys(queryClient); | ||||
|  | @ -149,7 +182,14 @@ export function useCreateSubmissionMutation(): UseMutationReturnType< | |||
|                 const an = typeof assignment === "number" ? assignment : assignment.id; | ||||
|                 const gn = response.submission.group.groupNumber; | ||||
| 
 | ||||
|                 await invalidateAllSubmissionKeys(queryClient, cid, an, gn); | ||||
|                 const { hruid, language, version } = response.submission.learningObjectIdentifier; | ||||
|                 await invalidateAllSubmissionKeys(queryClient, hruid, language, version, cid, an, gn); | ||||
| 
 | ||||
|                 await queryClient.invalidateQueries({ queryKey: [LEARNING_PATH_KEY, "get"] }); | ||||
| 
 | ||||
|                 await queryClient.invalidateQueries({ | ||||
|                     queryKey: [LEARNING_OBJECT_KEY, "metadata", hruid, language, version], | ||||
|                 }); | ||||
|             } | ||||
|         }, | ||||
|     }); | ||||
|  | @ -164,7 +204,7 @@ export function useDeleteSubmissionMutation(): UseMutationReturnType< | |||
|     const queryClient = useQueryClient(); | ||||
| 
 | ||||
|     return useMutation({ | ||||
|         mutationFn: async ({ cid, an, gn, sn }) => new SubmissionController(cid, an, gn).deleteSubmission(sn), | ||||
|         mutationFn: async ({ cid, sn }) => new SubmissionController(cid).deleteSubmission(sn), | ||||
|         onSuccess: async (response) => { | ||||
|             if (!response.submission.group) { | ||||
|                 await invalidateAllSubmissionKeys(queryClient); | ||||
|  | @ -176,7 +216,9 @@ export function useDeleteSubmissionMutation(): UseMutationReturnType< | |||
|                 const an = typeof assignment === "number" ? assignment : assignment.id; | ||||
|                 const gn = response.submission.group.groupNumber; | ||||
| 
 | ||||
|                 await invalidateAllSubmissionKeys(queryClient, cid, an, gn); | ||||
|                 const { hruid, language, version } = response.submission.learningObjectIdentifier; | ||||
| 
 | ||||
|                 await invalidateAllSubmissionKeys(queryClient, hruid, language, version, cid, an, gn); | ||||
|             } | ||||
|         }, | ||||
|     }); | ||||
|  |  | |||
|  | @ -14,7 +14,7 @@ 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/LearningObjectView.vue"; | ||||
| import LearningObjectView from "@/views/learning-paths/learning-object/LearningObjectView.vue"; | ||||
| 
 | ||||
| const router = createRouter({ | ||||
|     history: createWebHistory(import.meta.env.BASE_URL), | ||||
|  |  | |||
							
								
								
									
										5
									
								
								frontend/src/utils/array-utils.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										5
									
								
								frontend/src/utils/array-utils.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,5 @@ | |||
| export function copyArrayWith<T>(index: number, newValue: T, array: T[]): T[] { | ||||
|     const copy = [...array]; | ||||
|     copy[index] = newValue; | ||||
|     return copy; | ||||
| } | ||||
							
								
								
									
										29
									
								
								frontend/src/utils/deep-equals.ts
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										29
									
								
								frontend/src/utils/deep-equals.ts
									
										
									
									
									
										Normal file
									
								
							|  | @ -0,0 +1,29 @@ | |||
| export function deepEquals<T>(a: T, b: T): boolean { | ||||
|     if (a === b) { | ||||
|         return true; | ||||
|     } | ||||
| 
 | ||||
|     if (typeof a !== "object" || typeof b !== "object" || a === null || b === null) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     if (Array.isArray(a) !== Array.isArray(b)) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     if (Array.isArray(a) && Array.isArray(b)) { | ||||
|         if (a.length !== b.length) { | ||||
|             return false; | ||||
|         } | ||||
|         return a.every((val, i) => deepEquals(val, b[i])); | ||||
|     } | ||||
| 
 | ||||
|     const keysA = Object.keys(a) as (keyof T)[]; | ||||
|     const keysB = Object.keys(b) as (keyof T)[]; | ||||
| 
 | ||||
|     if (keysA.length !== keysB.length) { | ||||
|         return false; | ||||
|     } | ||||
| 
 | ||||
|     return keysA.every((key) => deepEquals(a[key], b[key])); | ||||
| } | ||||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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> | ||||
|  |  | |||
|  | @ -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); | ||||
|     }, | ||||
| }; | ||||
							
								
								
									
										8
									
								
								frontend/src/views/learning-paths/gift-adapters/gift-adapter.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										8
									
								
								frontend/src/views/learning-paths/gift-adapters/gift-adapter.d.ts
									
										
									
									
										vendored
									
									
										Normal 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; | ||||
| } | ||||
|  | @ -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); | ||||
| } | ||||
|  | @ -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); | ||||
|         }); | ||||
|     }, | ||||
| }; | ||||
|  | @ -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> | ||||
|  | @ -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> | ||||
							
								
								
									
										1
									
								
								frontend/src/views/learning-paths/learning-object/submission-data.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								frontend/src/views/learning-paths/learning-object/submission-data.d.ts
									
										
									
									
										vendored
									
									
										Normal file
									
								
							|  | @ -0,0 +1 @@ | |||
| export type SubmissionData = (string | number | object | null)[]; | ||||
|  | @ -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> | ||||
|  | @ -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> | ||||
|  | @ -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> | ||||
		Reference in a new issue
	
	 Joyelle Ndagijimana
						Joyelle Ndagijimana