import { quizApi } from "@api/quiz"; import { devlog, getMessageFromFetchError } from "@frontend/kitui"; import { quizToEditQuizRequest } from "@model/quiz/edit"; import { Quiz, RawQuiz, rawQuizToQuiz } from "@model/quiz/quiz"; import { maxQuizSetupSteps, QuizConfig } from "@model/quizSettings"; import { createUntypedQuestion, updateQuestion } from "@root/questions/actions"; import { useQuestionsStore } from "@root/questions/store"; import { produce } from "immer"; import { enqueueSnackbar } from "notistack"; import { NavigateFunction } from "react-router-dom"; import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError"; import { RequestQueue } from "../../utils/requestQueue"; import { QuizStore, useQuizStore } from "./store"; export const setEditQuizId = (quizId: number | null) => setProducedState( (state) => { state.editQuizId = quizId; }, { type: "setEditQuizId", quizId, }, ); export const resetEditConfig = () => setProducedState((state) => { state.editQuizId = null; state.currentStep = 0; }, "resetEditConfig"); export const setQuizes = (quizes: RawQuiz[] | null) => setProducedState( (state) => { state.quizes = quizes?.map(rawQuizToQuiz) ?? []; }, { type: "setQuizes", quizes, }, ); const addQuiz = (quiz: Quiz) => setProducedState( (state) => { state.quizes.push(quiz); }, { type: "addQuiz", quiz, }, ); const removeQuiz = (quizId: string) => setProducedState( (state) => { const index = state.quizes.findIndex((q) => q.id === quizId); if (index === -1) return; state.quizes.splice(index, 1); }, { type: "removeQuiz", quizId, }, ); const setQuizBackendId = (quizId: string, backendId: number) => setProducedState( (state) => { const quiz = state.quizes.find((q) => q.id === quizId); if (!quiz) return; quiz.backendId = backendId; }, { type: "setQuizBackendId", quizId, backendId, }, ); export const incrementCurrentStep = () => setProducedState( (state) => { state.currentStep = Math.min( maxQuizSetupSteps - 1, state.currentStep + 1, ); }, { type: "incrementCurrentStep", }, ); export const decrementCurrentStep = () => setProducedState( (state) => { state.currentStep = Math.max(0, state.currentStep - 1); }, { type: "decrementCurrentStep", }, ); export const setCurrentStep = (step: number) => setProducedState( (state) => { state.currentStep = Math.max(0, Math.min(maxQuizSetupSteps - 1, step)); }, { type: "setCurrentStep", step, }, ); export const setQuizType = (quizId: string, quizType: QuizConfig["type"]) => { updateQuiz(quizId, (quiz) => { quiz.config.type = quizType; }); }; export const setQuizStartpageType = ( quizId: string, startpageType: QuizConfig["startpageType"], ) => { updateQuiz(quizId, (quiz) => { quiz.config.startpageType = startpageType; }); }; const REQUEST_DEBOUNCE = 200; const requestQueue = new RequestQueue(); let requestTimeoutId: ReturnType; export const updateQuiz = ( quizId: string | null | undefined, updateFn: (quiz: Quiz) => void, ) => { if (!quizId) return; setProducedState( (state) => { const quiz = state.quizes.find((q) => q.id === quizId); if (!quiz) return; updateFn(quiz); }, { type: "updateQuiz", quizId, updateFn: updateFn.toString(), }, ); clearTimeout(requestTimeoutId); requestTimeoutId = setTimeout(async () => { requestQueue.enqueue(`updateQuiz-${quizId}`, async () => { const quiz = useQuizStore.getState().quizes.find((q) => q.id === quizId); if (!quiz) return; const [editedQuiz, editedQuizError] = await quizApi.edit( quizToEditQuizRequest(quiz), ); if (editedQuizError || !editedQuiz) { devlog("Error editing quiz", editedQuizError, quizId); enqueueSnackbar(editedQuizError); return; } setQuizBackendId(quizId, editedQuiz.updated); setEditQuizId(editedQuiz.updated); }); }, REQUEST_DEBOUNCE); }; export const createQuiz = async (navigate: NavigateFunction) => requestQueue.enqueue("createQuiz", async () => { const [rawQuiz, createQuizError] = await quizApi.create(); if (createQuizError || !rawQuiz) { devlog("Error creating quiz", createQuizError); enqueueSnackbar(createQuizError); return; } const quiz = rawQuizToQuiz(rawQuiz); addQuiz(quiz); setEditQuizId(quiz.backendId); navigate("/edit"); createUntypedQuestion(rawQuiz.id); }); export const deleteQuiz = async (quizId: string) => requestQueue.enqueue(`deleteQuiz-${quizId}`, async () => { const quiz = useQuizStore.getState().quizes.find((q) => q.id === quizId); if (!quiz) return; const [_, deleteQuizError] = await quizApi.delete(quiz.backendId); if (deleteQuizError) { devlog("Error deleting quiz", deleteQuizError); enqueueSnackbar(deleteQuizError); return; } removeQuiz(quizId); }); export const updateRootContentId = (quizId: string, id: string) => { if (id.length === 0) { //дерева больше не существует, все результаты неактивны кроме результата линейности useQuestionsStore.getState().questions.forEach((q) => { if (q.type !== null && q.type === "result") { if (q.content.rule.parentId === "line") { if (q.content.usage === false) updateQuestion(q.id, (q) => { q.content.usage = true; }); } else { updateQuestion(q.id, (q) => { q.content.usage = false; }); } } }); } else { //было создано дерево, результат линейности неактивен useQuestionsStore.getState().questions.forEach((q) => { if (q.type !== null && q.content.rule.parentId === "line") { updateQuestion(q.id, (q) => { q.content.usage = false; }); } }); } updateQuiz(quizId, (quiz) => { quiz.config.haveRoot = id; }); }; export const copyQuiz = async (quizId: string) => requestQueue.enqueue(`copyQuiz`, async () => { const quiz = useQuizStore.getState().quizes.find((q) => q.id === quizId); if (!quiz) return; const [copiedQuiz, copyError] = await quizApi.copy(quiz.backendId); if (copyError || !copiedQuiz) { devlog("Error copying quiz", copyError); enqueueSnackbar(copyError); return; } let newQuiz: Quiz = { ...quiz, id: String(copiedQuiz.updated), session_count: 0, passed_count: 0, }; setProducedState( (state) => { state.quizes.unshift(newQuiz); }, { type: "addQuiz", quiz }, ); }); export const uploadQuizImage = async ( quizId: string, blob: Blob, updateFn: (quiz: Quiz, imageId: string) => void, ) => { const quiz = useQuizStore.getState().quizes.find((q) => q.id === quizId); if (!quiz) return; const [addedImages, addImagesError] = await quizApi.addImages( quiz.backendId, blob, ); if (addImagesError || !addedImages) { devlog("Error uploading quiz image", addImagesError); enqueueSnackbar(addImagesError); return; } const values = Object.values(addedImages); if (values.length !== 1) { console.warn("Error uploading image"); return; } const imageId = values[0]; updateQuiz(quizId, (quiz) => { updateFn( quiz, `https://storage.yandexcloud.net/squizimages/${quiz.qid}/${imageId}`, ); }); }; function setProducedState( recipe: (state: QuizStore) => void, action?: A, ) { useQuizStore.setState((state) => produce(state, recipe), false, action); }