Merge branch 'dev' into feat/assignment-page

This commit is contained in:
Laure Jablonski 2025-04-20 10:23:01 +02:00 committed by GitHub
commit c29b4f8c29
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
21 changed files with 1004 additions and 490 deletions

View file

@ -1,16 +1,37 @@
import type { QuestionId } from "@dwengo-1/common/dist/interfaces/question.ts";
import { type MaybeRefOrGetter, toValue } from "vue";
import { useMutation, type UseMutationReturnType, useQuery, type UseQueryReturnType } from "@tanstack/vue-query";
import { computed, type MaybeRefOrGetter, toValue } from "vue";
import {
useMutation,
type UseMutationReturnType,
useQuery,
type UseQueryReturnType,
useQueryClient,
} from "@tanstack/vue-query";
import { AnswerController, type AnswerResponse, type AnswersResponse } from "@/controllers/answers.ts";
import type { AnswerData } from "@dwengo-1/common/dist/interfaces/answer.ts";
import type { AnswerData } from "@dwengo-1/common/interfaces/answer";
import type { QuestionId } from "@dwengo-1/common/interfaces/question";
// TODO caching
/** 🔑 Query keys */
export function answersQueryKey(
questionId: QuestionId,
full: boolean,
): [string, string, number, string, number, boolean] {
const loId = questionId.learningObjectIdentifier;
return ["answers", loId.hruid, loId.version!, loId.language, questionId.sequenceNumber, full];
}
export function answerQueryKey(
questionId: QuestionId,
sequenceNumber: number,
): [string, string, number, string, number, number] {
const loId = questionId.learningObjectIdentifier;
return ["answer", loId.hruid, loId.version!, loId.language, questionId.sequenceNumber, sequenceNumber];
}
export function useAnswersQuery(
questionId: MaybeRefOrGetter<QuestionId>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<AnswersResponse, Error> {
return useQuery({
queryKey: computed(() => answersQueryKey(toValue(questionId), toValue(full))),
queryFn: async () => new AnswerController(toValue(questionId)).getAll(toValue(full)),
enabled: () => Boolean(toValue(questionId)),
});
@ -21,31 +42,68 @@ export function useAnswerQuery(
sequenceNumber: MaybeRefOrGetter<number>,
): UseQueryReturnType<AnswerResponse, Error> {
return useQuery({
queryKey: computed(() => answerQueryKey(toValue(questionId), toValue(sequenceNumber))),
queryFn: async () => new AnswerController(toValue(questionId)).getBy(toValue(sequenceNumber)),
enabled: () => Boolean(toValue(questionId)),
enabled: () => Boolean(toValue(questionId)) && Boolean(toValue(sequenceNumber)),
});
}
export function useCreateAnswerMutation(
questionId: MaybeRefOrGetter<QuestionId>,
): UseMutationReturnType<AnswerResponse, Error, AnswerData, unknown> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data) => new AnswerController(toValue(questionId)).create(data),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: answersQueryKey(toValue(questionId), true),
});
await queryClient.invalidateQueries({
queryKey: answersQueryKey(toValue(questionId), false),
});
},
});
}
export function useDeleteAnswerMutation(
questionId: MaybeRefOrGetter<QuestionId>,
): UseMutationReturnType<AnswerResponse, Error, number, unknown> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (seq) => new AnswerController(toValue(questionId)).remove(seq),
onSuccess: async () => {
await queryClient.invalidateQueries({
queryKey: answersQueryKey(toValue(questionId), true),
});
await queryClient.invalidateQueries({
queryKey: answersQueryKey(toValue(questionId), false),
});
},
});
}
export function useUpdateAnswerMutation(
questionId: MaybeRefOrGetter<QuestionId>,
): UseMutationReturnType<AnswerResponse, Error, { answerData: AnswerData; seq: number }, unknown> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data, seq) => new AnswerController(toValue(questionId)).update(seq, data),
mutationFn: async ({ answerData, seq }) => new AnswerController(toValue(questionId)).update(seq, answerData),
onSuccess: async (_, { seq }) => {
await queryClient.invalidateQueries({
queryKey: answerQueryKey(toValue(questionId), seq),
});
await queryClient.invalidateQueries({
queryKey: answersQueryKey(toValue(questionId), true),
});
await queryClient.invalidateQueries({
queryKey: answersQueryKey(toValue(questionId), true),
});
await queryClient.invalidateQueries({
queryKey: answersQueryKey(toValue(questionId), false),
});
},
});
}

View file

@ -13,6 +13,8 @@ import { computed, toValue, type MaybeRefOrGetter } from "vue";
import { invalidateAllAssignmentKeys } from "./assignments";
import { invalidateAllGroupKeys } from "./groups";
import { invalidateAllSubmissionKeys } from "./submissions";
import type { TeachersResponse } from "@/controllers/teachers";
import type { TeacherInvitationsResponse } from "@/controllers/teacher-invitations";
const classController = new ClassController();
@ -176,7 +178,7 @@ export function useClassDeleteStudentMutation(): UseMutationReturnType<
export function useClassTeachersQuery(
id: MaybeRefOrGetter<string | undefined>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<StudentsResponse, Error> {
): UseQueryReturnType<TeachersResponse, Error> {
return useQuery({
queryKey: computed(() => classTeachersKey(toValue(id)!, toValue(full))),
queryFn: async () => classController.getTeachers(toValue(id)!, toValue(full)),
@ -223,7 +225,7 @@ export function useClassDeleteTeacherMutation(): UseMutationReturnType<
export function useClassTeacherInvitationsQuery(
id: MaybeRefOrGetter<string | undefined>,
full: MaybeRefOrGetter<boolean> = true,
): UseQueryReturnType<StudentsResponse, Error> {
): UseQueryReturnType<TeacherInvitationsResponse, Error> {
return useQuery({
queryKey: computed(() => classTeacherInvitationsKey(toValue(id)!, toValue(full))),
queryFn: async () => classController.getTeacherInvitations(toValue(id)!, toValue(full)),

View file

@ -14,12 +14,12 @@ export function questionsQueryKey(
loId: LearningObjectIdentifierDTO,
full: boolean,
): [string, string, number, string, boolean] {
return ["questions", loId.hruid, loId.version, loId.language, full];
return ["questions", loId.hruid, loId.version!, loId.language, full];
}
export function questionQueryKey(questionId: QuestionId): [string, string, number, string, number] {
const loId = questionId.learningObjectIdentifier;
return ["question", loId.hruid, loId.version, loId.language, questionId.sequenceNumber];
return ["question", loId.hruid, loId.version!, loId.language, questionId.sequenceNumber];
}
export function useQuestionsQuery(
@ -39,7 +39,7 @@ export function useQuestionQuery(
const loId = toValue(questionId).learningObjectIdentifier;
const sequenceNumber = toValue(questionId).sequenceNumber;
return useQuery({
queryKey: computed(() => questionQueryKey(loId, sequenceNumber)),
queryKey: computed(() => questionQueryKey(toValue(questionId))),
queryFn: async () => new QuestionController(loId).getBy(sequenceNumber),
enabled: () => Boolean(toValue(questionId)),
});
@ -55,6 +55,7 @@ export function useCreateQuestionMutation(
onSuccess: async () => {
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) });
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) });
await queryClient.invalidateQueries({ queryKey: ["answers"] });
},
});
}
@ -88,6 +89,8 @@ export function useDeleteQuestionMutation(
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), true) });
await queryClient.invalidateQueries({ queryKey: questionsQueryKey(toValue(loId), false) });
await queryClient.invalidateQueries({ queryKey: questionQueryKey(toValue(questionId)) });
await queryClient.invalidateQueries({ queryKey: ["answers"] });
await queryClient.invalidateQueries({ queryKey: ["answer"] });
},
});
}

View file

@ -21,6 +21,7 @@ import type { GroupsResponse } from "@/controllers/groups.ts";
import type { SubmissionsResponse } from "@/controllers/submissions.ts";
import type { QuestionsResponse } from "@/controllers/questions.ts";
import type { StudentDTO } from "@dwengo-1/common/interfaces/student";
import { teacherClassJoinRequests } from "@/queries/teachers.ts";
const studentController = new StudentController();
@ -189,13 +190,13 @@ export function useCreateJoinRequestMutation(): UseMutationReturnType<
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async ({ username, classId }) => studentController.createJoinRequest(username, classId),
onSuccess: async (newJoinRequest) => {
await queryClient.invalidateQueries({
queryKey: studentJoinRequestsQueryKey(newJoinRequest.request.requester.username),
});
await queryClient.invalidateQueries({ queryKey: teacherClassJoinRequests(newJoinRequest.request.class) });
},
});
}
@ -215,6 +216,7 @@ export function useDeleteJoinRequestMutation(): UseMutationReturnType<
const classId = deletedJoinRequest.request.class;
await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) });
await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) });
await queryClient.invalidateQueries({ queryKey: teacherClassJoinRequests(classId) });
},
});
}

View file

@ -1,36 +1,56 @@
import { useMutation, useQuery, type UseMutationReturnType, type UseQueryReturnType } from "@tanstack/vue-query";
import { computed, toValue } from "vue";
import {
useMutation,
useQuery,
useQueryClient,
type UseMutationReturnType,
type UseQueryReturnType,
} from "@tanstack/vue-query";
import { toValue } from "vue";
import type { MaybeRefOrGetter } from "vue";
import {
TeacherInvitationController,
type TeacherInvitationResponse,
type TeacherInvitationsResponse,
} from "@/controllers/teacher-invitations.ts";
} from "@/controllers/teacher-invitations";
import type { TeacherInvitationData } from "@dwengo-1/common/interfaces/teacher-invitation";
import type { TeacherDTO } from "@dwengo-1/common/interfaces/teacher";
const controller = new TeacherInvitationController();
/** 🔑 Query keys */
export function teacherInvitationsSentQueryKey(username: string): [string, string, string] {
return ["teacher-invitations", "sent", username];
}
export function teacherInvitationsReceivedQueryKey(username: string): [string, string, string] {
return ["teacher-invitations", "received", username];
}
export function teacherInvitationQueryKey(data: TeacherInvitationData): [string, string, string, string] {
return ["teacher-invitation", data.sender, data.receiver, data.class];
}
/**
All the invitations the teacher sent
**/
* All the invitations the teacher sent
*/
export function useTeacherInvitationsSentQuery(
username: MaybeRefOrGetter<string | undefined>,
): UseQueryReturnType<TeacherInvitationsResponse, Error> {
return useQuery({
queryFn: computed(async () => controller.getAll(toValue(username), true)),
queryKey: teacherInvitationsSentQueryKey(toValue(username)!),
queryFn: async () => controller.getAll(toValue(username)!, true),
enabled: () => Boolean(toValue(username)),
});
}
/**
All the pending invitations sent to this teacher
* All the pending invitations sent to this teacher
*/
export function useTeacherInvitationsReceivedQuery(
username: MaybeRefOrGetter<string | undefined>,
): UseQueryReturnType<TeacherInvitationsResponse, Error> {
return useQuery({
queryFn: computed(async () => controller.getAll(toValue(username), false)),
queryKey: teacherInvitationsReceivedQueryKey(toValue(username)!),
queryFn: async () => controller.getAll(toValue(username)!, false),
enabled: () => Boolean(toValue(username)),
});
}
@ -39,7 +59,8 @@ export function useTeacherInvitationQuery(
data: MaybeRefOrGetter<TeacherInvitationData | undefined>,
): UseQueryReturnType<TeacherInvitationResponse, Error> {
return useQuery({
queryFn: computed(async () => controller.getBy(toValue(data))),
queryKey: teacherInvitationQueryKey(toValue(data)),
queryFn: async () => controller.getBy(toValue(data)),
enabled: () => Boolean(toValue(data)),
});
}
@ -47,32 +68,68 @@ export function useTeacherInvitationQuery(
export function useCreateTeacherInvitationMutation(): UseMutationReturnType<
TeacherInvitationResponse,
Error,
TeacherDTO,
TeacherInvitationData,
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: TeacherInvitationData) => controller.create(data),
mutationFn: async (data) => controller.create(data),
onSuccess: async (_, data) => {
await queryClient.invalidateQueries({
queryKey: teacherInvitationsSentQueryKey(data.sender),
});
await queryClient.invalidateQueries({
queryKey: teacherInvitationsReceivedQueryKey(data.receiver),
});
},
});
}
export function useRespondTeacherInvitationMutation(): UseMutationReturnType<
TeacherInvitationResponse,
Error,
TeacherDTO,
TeacherInvitationData,
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: TeacherInvitationData) => controller.respond(data),
mutationFn: async (data) => controller.respond(data),
onSuccess: async (_, data) => {
await queryClient.invalidateQueries({
queryKey: teacherInvitationsSentQueryKey(data.sender),
});
await queryClient.invalidateQueries({
queryKey: teacherInvitationsReceivedQueryKey(data.receiver),
});
await queryClient.invalidateQueries({
queryKey: teacherInvitationQueryKey(data),
});
},
});
}
export function useDeleteTeacherInvitationMutation(): UseMutationReturnType<
TeacherInvitationResponse,
Error,
TeacherDTO,
TeacherInvitationData,
unknown
> {
const queryClient = useQueryClient();
return useMutation({
mutationFn: async (data: TeacherInvitationData) => controller.remove(data),
mutationFn: async (data) => controller.remove(data),
onSuccess: async (_, data) => {
await queryClient.invalidateQueries({
queryKey: teacherInvitationsSentQueryKey(data.sender),
});
await queryClient.invalidateQueries({
queryKey: teacherInvitationsReceivedQueryKey(data.receiver),
});
await queryClient.invalidateQueries({
queryKey: teacherInvitationQueryKey(data),
});
},
});
}

View file

@ -37,6 +37,10 @@ function teacherQuestionsQueryKey(username: string, full: boolean): [string, str
return ["teacher-questions", username, full];
}
export function teacherClassJoinRequests(classId: string): [string, string] {
return ["teacher-class-join-requests", classId];
}
export function useTeachersQuery(full: MaybeRefOrGetter<boolean> = false): UseQueryReturnType<TeachersResponse, Error> {
return useQuery({
queryKey: computed(() => teachersQueryKey(toValue(full))),
@ -92,7 +96,7 @@ export function useTeacherJoinRequestsQuery(
classId: MaybeRefOrGetter<string | undefined>,
): UseQueryReturnType<JoinRequestsResponse, Error> {
return useQuery({
queryKey: computed(() => JOIN_REQUESTS_QUERY_KEY(toValue(username)!, toValue(classId)!)),
queryKey: computed(() => teacherClassJoinRequests(toValue(classId)!)),
queryFn: async () => teacherController.getStudentJoinRequests(toValue(username)!, toValue(classId)!),
enabled: () => Boolean(toValue(username)) && Boolean(toValue(classId)),
});
@ -133,10 +137,11 @@ export function useUpdateJoinRequestMutation(): UseMutationReturnType<
mutationFn: async ({ teacherUsername, classId, studentUsername, accepted }) =>
teacherController.updateStudentJoinRequest(teacherUsername, classId, studentUsername, accepted),
onSuccess: async (deletedJoinRequest) => {
const username = deletedJoinRequest.request.requester;
const username = deletedJoinRequest.request.requester.username;
const classId = deletedJoinRequest.request.class;
await queryClient.invalidateQueries({ queryKey: studentJoinRequestsQueryKey(username) });
await queryClient.invalidateQueries({ queryKey: studentJoinRequestQueryKey(username, classId) });
await queryClient.invalidateQueries({ queryKey: teacherClassJoinRequests(classId) });
},
});
}

View file

@ -1,7 +1,7 @@
import { useQuery, type UseQueryReturnType } from "@tanstack/vue-query";
import { type MaybeRefOrGetter, toValue } from "vue";
import type { Theme } from "@dwengo-1/interfaces/theme";
import { getThemeController } from "@/controllers/controllers.ts";
import type { Theme } from "@dwengo-1/common/interfaces/theme";
const themeController = getThemeController();