140 lines
4.1 KiB
TypeScript
140 lines
4.1 KiB
TypeScript
import { questionApi } from "@api/question";
|
||
import { devlog } from "@frontend/kitui";
|
||
import { RawQuestion, rawQuestionToQuestion } from "@model/question/question";
|
||
import { produce } from "immer";
|
||
import { enqueueSnackbar } from "notistack";
|
||
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
|
||
import { QuestionsStore, useQuestionsStore } from "./store";
|
||
import { questionToEditQuestionRequest } from "@model/question/edit";
|
||
import { AnyQuizQuestion } from "@model/questionTypes/shared";
|
||
|
||
|
||
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => {
|
||
state.questionsById = {};
|
||
if (questions === null) return;
|
||
|
||
questions.forEach(question => state.questionsById[question.id] = rawQuestionToQuestion(question));
|
||
}, {
|
||
type: "setQuestions",
|
||
questions,
|
||
});
|
||
|
||
export const setQuestion = (question: AnyQuizQuestion) => setProducedState(state => {
|
||
state.questionsById[question.id] = question;
|
||
}, {
|
||
type: "setQuestion",
|
||
question,
|
||
});
|
||
|
||
export const setQuestionField = <T extends keyof AnyQuizQuestion>(
|
||
questionId: number,
|
||
field: T,
|
||
value: AnyQuizQuestion[T],
|
||
) => setProducedState(state => {
|
||
const question = state.questionsById[questionId];
|
||
if (!question) return;
|
||
|
||
question[field] = value;
|
||
}, {
|
||
type: "setQuestionField",
|
||
questionId,
|
||
field,
|
||
value,
|
||
});
|
||
|
||
let savedOriginalQuestion: AnyQuizQuestion | null = null;
|
||
let controller: AbortController | null = null;
|
||
|
||
export const setQuestionFieldOptimistic = async <T extends keyof AnyQuizQuestion>(
|
||
questionId: number,
|
||
field: T,
|
||
value: AnyQuizQuestion[T],
|
||
) => {
|
||
const question = useQuestionsStore.getState().questionsById[questionId] ?? null;
|
||
if (!question) return;
|
||
|
||
const currentUpdatedQuestion = produce(question, draft => {
|
||
draft[field] = value;
|
||
});
|
||
controller?.abort();
|
||
controller = new AbortController();
|
||
savedOriginalQuestion ??= question;
|
||
|
||
setQuestion(currentUpdatedQuestion);
|
||
try {
|
||
const { updated } = await questionApi.edit(
|
||
questionToEditQuestionRequest(currentUpdatedQuestion),
|
||
controller.signal,
|
||
);
|
||
|
||
setQuestionField(question.id, "id", updated);
|
||
controller = null;
|
||
savedOriginalQuestion = null;
|
||
} catch (error) {
|
||
if (isAxiosCanceledError(error)) return;
|
||
|
||
devlog("Error editing question", { error, question, currentUpdatedQuestion });
|
||
enqueueSnackbar("Не удалось сохранить вопрос");
|
||
if (!savedOriginalQuestion) {
|
||
devlog("Cannot rollback question");
|
||
throw new Error("Cannot rollback question");
|
||
}
|
||
|
||
setQuestion(savedOriginalQuestion);
|
||
controller = null;
|
||
savedOriginalQuestion = null;
|
||
}
|
||
};
|
||
|
||
export const updateQuestionWithFn = (
|
||
questionId: number,
|
||
updateFn: (question: AnyQuizQuestion) => void,
|
||
) => setProducedState(state => {
|
||
const question = state.questionsById[questionId];
|
||
if (!question) return;
|
||
|
||
updateFn(question);
|
||
}, {
|
||
type: "updateQuestion",
|
||
questionId,
|
||
updateFn: updateFn.toString(),
|
||
});
|
||
|
||
export const createQuestion = async (quizId: number) => {
|
||
try {
|
||
const question = await questionApi.create({
|
||
quiz_id: quizId,
|
||
});
|
||
|
||
setQuestion(rawQuestionToQuestion(question));
|
||
} catch (error) {
|
||
devlog("Error creating question", error);
|
||
enqueueSnackbar("Не удалось создать вопрос");
|
||
}
|
||
};
|
||
|
||
export const deleteQuestion = async (questionId: number) => {
|
||
try {
|
||
await questionApi.delete(questionId);
|
||
|
||
removeQuestion(questionId);
|
||
} catch (error) {
|
||
devlog("Error deleting question", error);
|
||
enqueueSnackbar("Не удалось удалить вопрос");
|
||
}
|
||
};
|
||
|
||
export const removeQuestion = (questionId: number) => setProducedState(state => {
|
||
delete state.questionsById[questionId];
|
||
}, {
|
||
type: "removeQuestion",
|
||
questionId,
|
||
});
|
||
|
||
function setProducedState<A extends string | { type: unknown; }>(
|
||
recipe: (state: QuestionsStore) => void,
|
||
action?: A,
|
||
) {
|
||
useQuestionsStore.setState(state => produce(state, recipe), false, action);
|
||
}
|