frontPanel/src/stores/quizes/actions.ts

208 lines
5.6 KiB
TypeScript
Raw Normal View History

2023-11-13 18:04:51 +00:00
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 { QuizConfig, QuizSetupStep, maxQuizSetupSteps } from "@model/quizSettings";
import { produce } from "immer";
import { enqueueSnackbar } from "notistack";
import { NavigateFunction } from "react-router-dom";
import { QuizStore, useQuizStore } from "./store";
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
export const setQuizes = (quizes: RawQuiz[] | null) => setProducedState(state => {
state.quizById = {};
if (quizes === null) return;
quizes.forEach(quiz => state.quizById[quiz.id] = rawQuizToQuiz(quiz));
}, {
type: "setQuizes",
quizes,
});
export const setQuiz = (quiz: Quiz) => setProducedState(state => {
state.quizById[quiz.id] = quiz;
}, {
type: "setQuiz",
quiz,
});
export const removeQuiz = (quizId: number) => setProducedState(state => {
delete state.quizById[quizId];
}, {
type: "removeQuiz",
quizId,
});
export const setQuizField = <T extends keyof Quiz>(
quizId: number,
field: T,
value: Quiz[T],
) => setProducedState(state => {
const quiz = state.quizById[quizId];
if (!quiz) return;
2023-11-14 13:10:41 +00:00
const oldId = quiz.id;
2023-11-13 18:04:51 +00:00
quiz[field] = value;
2023-11-14 13:10:41 +00:00
if (field === "id") {
delete state.quizById[oldId];
state.quizById[value as number] = quiz;
}
2023-11-13 18:04:51 +00:00
}, {
type: "setQuizField",
quizId,
field,
value,
});
export const updateQuiz = (
quizId: number,
updateFn: (quiz: Quiz) => void,
) => setProducedState(state => {
const quiz = state.quizById[quizId];
if (!quiz) return;
updateFn(quiz);
}, {
type: "updateQuiz",
quizId,
updateFn: updateFn.toString(),
});
export const incrementCurrentStep = () => setProducedState(state => {
state.currentStep = Math.min(
maxQuizSetupSteps, state.currentStep + 1
) as QuizSetupStep;
2023-11-14 13:10:41 +00:00
}, {
type: "incrementCurrentStep",
2023-11-13 18:04:51 +00:00
});
export const decrementCurrentStep = () => setProducedState(state => {
state.currentStep = Math.max(
1, state.currentStep - 1
) as QuizSetupStep;
2023-11-14 13:10:41 +00:00
}, {
type: "decrementCurrentStep",
2023-11-13 18:04:51 +00:00
});
export const setCurrentStep = (step: number) => setProducedState(state => {
state.currentStep = Math.max(0, Math.min(maxQuizSetupSteps, step)) as QuizSetupStep;
});
export const createQuiz = async (navigate: NavigateFunction) => {
try {
const quiz = await quizApi.create({
name: "Quiz name",
description: "Quiz description",
});
setQuiz(rawQuizToQuiz(quiz));
navigate(`/setting/${quiz.id}`);
} catch (error) {
devlog("Error creating quiz", error);
const message = getMessageFromFetchError(error) ?? "";
enqueueSnackbar(`Не удалось создать квиз. ${message}`);
}
};
export const deleteQuiz = async (quizId: number) => {
try {
await quizApi.delete(quizId);
removeQuiz(quizId);
} catch (error) {
devlog("Error deleting quiz", error);
const message = getMessageFromFetchError(error) ?? "";
enqueueSnackbar(`Не удалось удалить квиз. ${message}`);
}
};
export const setQuizType = (
quizId: number,
quizType: QuizConfig["type"],
navigate: NavigateFunction,
) => {
updateQuizWithFnOptimistic(
quizId,
quiz => {
quiz.config.type = quizType;
},
navigate,
);
incrementCurrentStep();
};
export const setQuizStartpageType = (
2023-11-14 13:10:41 +00:00
quizId: number | undefined,
2023-11-13 18:04:51 +00:00
startpageType: QuizConfig["startpageType"],
navigate: NavigateFunction,
) => {
updateQuizWithFnOptimistic(
quizId,
quiz => {
quiz.config.startpageType = startpageType;
},
navigate,
);
incrementCurrentStep();
};
let savedOriginalQuiz: Quiz | null = null;
let controller: AbortController | null = null;
export const updateQuizWithFnOptimistic = async (
2023-11-14 13:10:41 +00:00
quizId: number | undefined,
2023-11-13 18:04:51 +00:00
updateFn: (quiz: Quiz) => void,
navigate: NavigateFunction,
rollbackOnError = true,
) => {
2023-11-14 13:10:41 +00:00
if (!quizId) return;
2023-11-13 18:04:51 +00:00
const quiz = useQuizStore.getState().quizById[quizId] ?? null;
if (!quiz) return;
const currentUpdatedQuiz = produce(quiz, updateFn);
controller?.abort();
controller = new AbortController();
savedOriginalQuiz ??= quiz;
setQuiz(currentUpdatedQuiz);
try {
2023-11-14 13:10:41 +00:00
const { updated: newId } = await quizApi.edit(quizToEditQuizRequest(currentUpdatedQuiz), controller.signal);
2023-11-13 18:04:51 +00:00
// await new Promise((resolve, reject) => setTimeout(reject, 2000, new Error("Api rejected")));
2023-11-14 13:10:41 +00:00
setQuizField(quiz.id, "id", newId);
navigate(`/setting/${newId}`, { replace: true });
2023-11-13 18:04:51 +00:00
controller = null;
savedOriginalQuiz = null;
} catch (error) {
if (isAxiosCanceledError(error)) return;
devlog("Error editing quiz", { error, quiz, currentUpdatedQuiz });
enqueueSnackbar("Не удалось сохранить настройки квиза");
if (rollbackOnError) {
if (!savedOriginalQuiz) {
devlog("Cannot rollback quiz");
throw new Error("Cannot rollback quiz");
}
setQuiz(savedOriginalQuiz);
}
controller = null;
savedOriginalQuiz = null;
}
};
function setProducedState<A extends string | { type: unknown; }>(
recipe: (state: QuizStore) => void,
action?: A,
) {
useQuizStore.setState(state => produce(state, recipe), false, action);
}