319 lines
7.9 KiB
TypeScript
319 lines
7.9 KiB
TypeScript
import { quizApi } from "@api/quiz";
|
|
import { devlog } 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<typeof setTimeout>;
|
|
|
|
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://s3.timeweb.cloud/3c580be9-cf31f296-d055-49cf-b39e-30c7959dc17b/squizimages/angesight/squizimages/${quiz.qid}/${imageId}`,
|
|
);
|
|
});
|
|
};
|
|
|
|
function setProducedState<A extends string | { type: unknown }>(
|
|
recipe: (state: QuizStore) => void,
|
|
action?: A,
|
|
) {
|
|
useQuizStore.setState((state) => produce(state, recipe), false, action);
|
|
}
|