frontPanel/src/stores/questions/actions.ts

366 lines
11 KiB
TypeScript
Raw Normal View History

2023-11-02 16:45:28 +00:00
import { questionApi } from "@api/question";
import { devlog } from "@frontend/kitui";
import { EditQuestionResponse, questionToEditQuestionRequest } from "@model/question/edit";
import { QuestionType, RawQuestion, rawQuestionToQuestion } from "@model/question/question";
2023-11-15 18:38:02 +00:00
import { AnyQuizQuestion, ImageQuestionVariant, QuestionVariant, createQuestionImageVariant, createQuestionVariant } from "@model/questionTypes/shared";
2023-11-02 16:45:28 +00:00
import { produce } from "immer";
import { enqueueSnackbar } from "notistack";
2023-11-17 15:42:49 +00:00
import { notReachable } from "../../utils/notReachable";
2023-11-14 16:44:27 +00:00
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
import { QuestionsStore, useQuestionsStore } from "./store";
import { RequestQueue } from "../../utils/requestQueue";
2023-11-02 16:45:28 +00:00
2023-11-14 20:15:52 +00:00
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => {
state.questions = questions?.map(rawQuestionToQuestion) ?? [];
2023-11-14 16:44:27 +00:00
}, {
2023-11-14 20:15:52 +00:00
type: "setQuestions",
questions,
2023-11-14 16:44:27 +00:00
});
2023-11-02 16:45:28 +00:00
const setQuestion = (question: AnyQuizQuestion) => setProducedState(state => {
const index = state.questions.findIndex(q => q.id === question.id);
state.questions.splice(index, 1, question);
2023-11-02 16:45:28 +00:00
}, {
type: "setQuestion",
question,
});
2023-11-17 15:42:49 +00:00
const addQuestion = (question: AnyQuizQuestion) => setProducedState(state => {
state.questions.push(question);
}, {
type: "addQuestion",
question,
});
const removeQuestion = (questionId: number) => setProducedState(state => {
const index = state.questions.findIndex(q => q.id === questionId);
state.questions.splice(index, 1);
2023-11-15 18:38:02 +00:00
}, {
type: "removeQuestion",
questionId,
});
const setQuestionField = <T extends keyof AnyQuizQuestion>(
2023-11-02 16:45:28 +00:00
questionId: number,
field: T,
2023-11-14 20:15:52 +00:00
value: AnyQuizQuestion[T],
2023-11-02 16:45:28 +00:00
) => setProducedState(state => {
const question = state.questions.find(q => q.id === questionId);
2023-11-02 16:45:28 +00:00
if (!question) return;
question[field] = value;
}, {
type: "setQuestionField",
questionId,
field,
value,
});
export const reorderQuestions = (
sourceIndex: number,
destinationIndex: number,
) => {
if (sourceIndex === destinationIndex) return;
setProducedState(state => {
const [removed] = state.questions.splice(sourceIndex, 1);
state.questions.splice(destinationIndex, 0, removed);
});
};
2023-11-15 18:38:02 +00:00
export const toggleExpandQuestion = (questionId: number) => setProducedState(state => {
const question = state.questions.find(q => q.id === questionId);
2023-11-15 18:38:02 +00:00
if (!question) return;
question.expanded = !question.expanded;
});
export const collapseAllQuestions = () => setProducedState(state => {
state.questions.forEach(question => question.expanded = false);
});
2023-11-15 18:38:02 +00:00
export const toggleOpenQuestionModal = (questionId: number) => setProducedState(state => {
const question = state.questions.find(q => q.id === questionId);
2023-11-15 18:38:02 +00:00
if (!question) return;
question.openedModalSettings = !question.openedModalSettings;
});
export const addQuestionVariant = (questionId: number) => {
updateQuestionWithFnOptimistic(questionId, question => {
switch (question.type) {
case "variant":
case "emoji":
case "select":
question.content.variants.push(createQuestionVariant());
break;
case "images":
case "varimg":
question.content.variants.push(createQuestionImageVariant());
break;
case "text":
case "date":
case "number":
case "file":
case "page":
case "rating":
throw new Error(`Cannot add variant to question of type "${question.type}"`);
default: notReachable(question);
}
});
};
export const deleteQuestionVariant = (questionId: number, variantId: string) => {
updateQuestionWithFnOptimistic(questionId, question => {
if (!("variants" in question.content)) return;
const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId);
if (variantIndex === -1) return;
question.content.variants.splice(variantIndex, 1);
});
};
export const setQuestionVariantField = (
questionId: number,
variantId: string,
field: keyof QuestionVariant,
value: QuestionVariant[keyof QuestionVariant],
) => {
updateQuestionWithFnOptimistic(questionId, question => {
if (!("variants" in question.content)) return;
const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId);
if (variantIndex === -1) return;
const variant = question.content.variants[variantIndex];
variant[field] = value;
});
};
export const setQuestionImageVariantField = (
questionId: number,
variantId: string,
field: keyof ImageQuestionVariant,
value: ImageQuestionVariant[keyof ImageQuestionVariant],
) => {
updateQuestionWithFnOptimistic(questionId, question => {
if (!("variants" in question.content)) return;
const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId);
if (variantIndex === -1) return;
const variant = question.content.variants[variantIndex];
if (!("originalImageUrl" in variant)) return;
variant[field] = value;
});
};
export const reorderQuestionVariants = (
questionId: number,
sourceIndex: number,
destinationIndex: number,
) => {
if (sourceIndex === destinationIndex) return;
updateQuestionWithFnOptimistic(questionId, question => {
if (!("variants" in question.content)) return;
const [removed] = question.content.variants.splice(sourceIndex, 1);
question.content.variants.splice(destinationIndex, 0, removed);
});
};
export const setQuestionBackgroundImage = (
questionId: number,
url: string,
) => {
updateQuestionWithFnOptimistic(questionId, question => {
if (question.content.back === url) return;
if (
question.content.back !== question.content.originalBack
) URL.revokeObjectURL(question.content.back);
question.content.back = url;
});
};
export const setQuestionOriginalBackgroundImage = (
questionId: number,
url: string,
) => {
updateQuestionWithFnOptimistic(questionId, question => {
if (question.content.originalBack === url) return;
URL.revokeObjectURL(question.content.originalBack);
question.content.originalBack = url;
});
};
export const setVariantImageUrl = (
questionId: number,
variantId: string,
url: string,
) => {
updateQuestionWithFnOptimistic(questionId, question => {
if (!("variants" in question.content)) return;
const variant = question.content.variants.find(variant => variant.id === variantId);
if (!variant || !("originalImageUrl" in variant)) return;
if (variant.extendedText === url) return;
if (variant.extendedText !== variant.originalImageUrl) URL.revokeObjectURL(variant.extendedText);
variant.extendedText = url;
});
};
export const setVariantOriginalImageUrl = (
questionId: number,
variantId: string,
url: string,
) => {
updateQuestionWithFnOptimistic(questionId, question => {
if (!("variants" in question.content)) return;
const variant = question.content.variants.find(
variant => variant.id === variantId
) as ImageQuestionVariant | undefined;
if (!variant || !("originalImageUrl" in variant)) return;
if (variant.originalImageUrl === url) return;
URL.revokeObjectURL(variant.originalImageUrl);
variant.originalImageUrl = url;
});
};
export const setPageQuestionPicture = (
questionId: number,
url: string,
) => {
updateQuestionWithFnOptimistic(questionId, question => {
if (question.type !== "page") return;
if (question.content.picture === url) return;
if (
question.content.picture !== question.content.originalPicture
) URL.revokeObjectURL(question.content.picture);
question.content.picture = url;
});
};
export const setPageQuestionOriginalPicture = (
questionId: number,
url: string,
) => {
updateQuestionWithFnOptimistic(questionId, question => {
if (question.type !== "page") return;
if (question.content.originalPicture === url) return;
URL.revokeObjectURL(question.content.originalPicture);
question.content.originalPicture = url;
});
};
export const setQuestionInnerName = (
questionId: number,
name: string,
) => {
updateQuestionWithFnOptimistic(questionId, question => {
question.content.innerName = name;
});
};
const REQUEST_DEBOUNCE = 1000;
const requestQueue = new RequestQueue<EditQuestionResponse>();
let requestTimeoutId: ReturnType<typeof setTimeout>;
2023-11-02 16:45:28 +00:00
2023-11-15 18:38:02 +00:00
export const updateQuestionWithFnOptimistic = async (
2023-11-02 16:45:28 +00:00
questionId: number,
2023-11-15 18:38:02 +00:00
updateFn: (question: AnyQuizQuestion) => void,
2023-11-02 16:45:28 +00:00
) => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
2023-11-02 16:45:28 +00:00
if (!question) return;
const updatedQuestion = produce(question, updateFn);
setQuestion(updatedQuestion);
2023-11-15 18:38:02 +00:00
clearTimeout(requestTimeoutId);
requestTimeoutId = setTimeout(async () => {
requestQueue.enqueue(async (prevResponse) => {
const questionId = prevResponse?.updated ?? updatedQuestion.id;
const response = await questionApi.edit(questionToEditQuestionRequest(updatedQuestion, questionId));
2023-11-02 16:45:28 +00:00
setQuestionField(questionId, "id", response.updated);
2023-11-15 18:38:02 +00:00
return response;
}).catch(error => {
if (isAxiosCanceledError(error)) return;
2023-11-02 16:45:28 +00:00
devlog("Error editing question", { error, question, updatedQuestion });
enqueueSnackbar("Не удалось сохранить вопрос");
});
}, REQUEST_DEBOUNCE);
2023-11-02 16:45:28 +00:00
};
export const createQuestion = async (quizId: number, type: QuestionType = "variant") => {
2023-11-02 16:45:28 +00:00
try {
2023-11-14 16:44:27 +00:00
const question = await questionApi.create({
quiz_id: quizId,
type,
2023-11-14 16:44:27 +00:00
});
2023-11-02 16:45:28 +00:00
2023-11-17 15:42:49 +00:00
addQuestion(rawQuestionToQuestion(question));
2023-11-02 16:45:28 +00:00
} 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("Не удалось удалить вопрос");
}
};
2023-11-15 18:38:02 +00:00
export const copyQuestion = async (questionId: number, quizId: number) => {
try {
const { updated: newQuestionId } = await questionApi.copy(questionId, quizId);
setProducedState(state => {
const question = state.questions.find(q => q.id === questionId);
2023-11-15 18:38:02 +00:00
if (!question) return;
console.log(question);
const copiedQuestion = structuredClone(question);
copiedQuestion.id = newQuestionId;
state.questions.push(copiedQuestion);
2023-11-15 18:38:02 +00:00
}, {
type: "copyQuestion",
questionId,
quizId,
});
} catch (error) {
devlog("Error copying question", error);
enqueueSnackbar("Не удалось скопировать вопрос");
}
};
2023-11-02 16:45:28 +00:00
function setProducedState<A extends string | { type: unknown; }>(
recipe: (state: QuestionsStore) => void,
action?: A,
) {
useQuestionsStore.setState(state => produce(state, recipe), false, action);
}