frontPanel/src/stores/questions/actions.ts
2023-12-08 16:36:00 +03:00

447 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import { questionApi } from "@api/question";
import { quizApi } from "@api/quiz";
import { devlog } from "@frontend/kitui";
import { questionToEditQuestionRequest } from "@model/question/edit";
import { QuestionType, RawQuestion, rawQuestionToQuestion } from "@model/question/question";
import { AnyTypedQuizQuestion, QuestionVariant, UntypedQuizQuestion, createQuestionVariant } from "@model/questionTypes/shared";
import { updateRootContentId } from "@root/quizes/actions";
import { produce } from "immer";
import { nanoid } from "nanoid";
import { enqueueSnackbar } from "notistack";
import { defaultQuestionByType } from "../../constants/default";
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
import { RequestQueue } from "../../utils/requestQueue";
import { QuestionsStore, useQuestionsStore } from "./store";
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => {
const untypedQuestions = state.questions.filter(q => q.type === null);
state.questions = questions?.map(rawQuestionToQuestion) ?? [];
state.questions.push(...untypedQuestions);
}, {
type: "setQuestions",
questions,
});
export const createUntypedQuestion = (quizId: number) => setProducedState(state => {
state.questions.push({
id: nanoid(),
quizId,
type: null,
title: "",
description: "",
deleted: false,
expanded: true,
});
}, {
type: "createUntypedQuestion",
quizId,
});
const removeQuestion = (questionId: string) => setProducedState(state => {
const index = state.questions.findIndex(q => q.id === questionId);
if (index === -1) return;
state.questions.splice(index, 1);
}, {
type: "removeQuestion",
questionId,
});
export const updateUntypedQuestion = (
questionId: string,
updateFn: (question: UntypedQuizQuestion) => void,
) => {
setProducedState(state => {
const question = state.questions.find(q => q.id === questionId);
if (!question) return;
if (question.type !== null) throw new Error("Cannot update typed question, use 'updateQuestion' instead");
updateFn(question);
}, {
type: "updateUntypedQuestion",
questionId,
updateFn: updateFn.toString(),
});
};
export const cleanQuestions = () => setProducedState(state => {
state.questions = [];
}, {
type: "cleanQuestions",
});
const setQuestionBackendId = (questionId: string, backendId: number) => setProducedState(state => {
const question = state.questions.find(q => q.id === questionId);
if (!question) return;
if (question.type === null) throw new Error("Cannot set backend id for untyped question");
question.backendId = backendId;
}, {
type: "setQuestionBackendId",
questionId: questionId,
backendId,
});
const updateQuestionOrders = () => {
const questions = useQuestionsStore.getState().questions.filter(
(question): question is AnyTypedQuizQuestion => question.type !== null
);
questions.forEach((question, index) => {
updateQuestion(question.id, question => {
question.page = index;
}, true);
});
};
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);
}, {
type: "reorderQuestions",
sourceIndex,
destinationIndex,
});
updateQuestionOrders();
};
export const toggleExpandQuestion = (questionId: string) => setProducedState(state => {
const question = state.questions.find(q => q.id === questionId);
if (!question) return;
question.expanded = !question.expanded;
}, {
type: "toggleExpandQuestion",
questionId,
});
export const collapseAllQuestions = () => setProducedState(state => {
state.questions.forEach(question => question.expanded = false);
}, "collapseAllQuestions");
const REQUEST_DEBOUNCE = 200;
const requestQueue = new RequestQueue();
let requestTimeoutId: ReturnType<typeof setTimeout>;
export const updateQuestion = (
questionId: string,
updateFn: (question: AnyTypedQuizQuestion) => void,
skipQueue = false,
) => {
setProducedState(state => {
const question = state.questions.find(q => q.id === questionId) || state.questions.find(q => q.type !== null && q.content.id === questionId);
if (!question) return;
if (question.type === null) throw new Error("Cannot update untyped question, use 'updateUntypedQuestion' instead");
updateFn(question);
}, {
type: "updateQuestion",
questionId,
updateFn: updateFn.toString(),
});
// clearTimeout(requestTimeoutId);
const request = async () => {
const q = useQuestionsStore.getState().questions.find(q => q.id === questionId) || useQuestionsStore.getState().questions.find(q => q.type !== null && q.content.id === questionId);
if (!q) return;
if (q.type === null) throw new Error("Cannot send update request for untyped question");
try {
const response = await questionApi.edit(questionToEditQuestionRequest(q));
setQuestionBackendId(questionId, response.updated);
} catch (error) {
if (isAxiosCanceledError(error)) return;
devlog("Error editing question", { error, questionId });
enqueueSnackbar("Не удалось сохранить вопрос");
}
};
if (skipQueue) {
request();
return;
}
// requestTimeoutId = setTimeout(() => {
requestQueue.enqueue(request);
// }, REQUEST_DEBOUNCE);
};
export const addQuestionVariant = (questionId: string) => {
updateQuestion(questionId, question => {
switch (question.type) {
case "variant":
case "emoji":
case "select":
case "images":
case "varimg":
question.content.variants.push(createQuestionVariant());
break;
default: throw new Error(`Cannot add variant to question of type "${question.type}"`);
}
});
};
export const deleteQuestionVariant = (questionId: string, variantId: string) => {
updateQuestion(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: string,
variantId: string,
field: keyof QuestionVariant,
value: QuestionVariant[keyof QuestionVariant],
) => {
updateQuestion(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 reorderQuestionVariants = (
questionId: string,
sourceIndex: number,
destinationIndex: number,
) => {
if (sourceIndex === destinationIndex) return;
updateQuestion(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 uploadQuestionImage = async (
questionId: string,
quizQid: string | undefined,
blob: Blob,
updateFn: (question: AnyTypedQuizQuestion, imageId: string) => void,
) => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question || !quizQid) return;
try {
const response = await quizApi.addImages(question.quizId, blob);
const values = Object.values(response);
if (values.length !== 1) {
console.warn("Error uploading image");
return;
}
const imageId = values[0];
const imageUrl = `https://squiz.pena.digital/squizimages/${quizQid}/${imageId}`;
updateQuestion(questionId, question => {
updateFn(question, imageUrl);
});
return imageUrl;
} catch (error) {
devlog("Error uploading question image", error);
enqueueSnackbar("Не удалось загрузить изображение");
}
};
export const setQuestionInnerName = (
questionId: string,
name: string,
) => {
updateQuestion(questionId, question => {
question.content.innerName = name;
});
};
export const changeQuestionType = (
questionId: string,
type: QuestionType,
) => {
updateQuestion(questionId, question => {
const oldId = question.content.id;
const oldRule = question.content.rule;
oldRule.main = [];
question.type = type;
question.content = defaultQuestionByType[type].content;
question.content.id = oldId;
question.content.rule = oldRule;
});
};
export const createTypedQuestion = async (
questionId: string,
type: QuestionType,
) => requestQueue.enqueue(async () => {
const questions = useQuestionsStore.getState().questions;
const question = questions.find(q => q.id === questionId);
if (!question) return;
if (question.type !== null) throw new Error("Cannot upgrade already typed question");
try {
const createdQuestion = await questionApi.create({
quiz_id: question.quizId,
type,
title: question.title,
description: question.description,
page: questions.length,
required: true,
content: JSON.stringify(defaultQuestionByType[type].content),
});
setProducedState(state => {
const questionIndex = state.questions.findIndex(q => q.id === questionId);
if (questionIndex !== -1) state.questions.splice(
questionIndex,
1,
rawQuestionToQuestion(createdQuestion)
);
}, {
type: "createTypedQuestion",
question,
});
} catch (error) {
devlog("Error creating question", error);
enqueueSnackbar("Не удалось создать вопрос");
}
});
export const deleteQuestion = async (questionId: string, quizId: string) => requestQueue.enqueue(async () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
if (question.type === null) {
removeQuestion(questionId);
return;
}
try {
await questionApi.delete(question.backendId);
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quizId, "");
clearRoleForAll();
}
removeQuestion(questionId);
} catch (error) {
devlog("Error deleting question", error);
enqueueSnackbar("Не удалось удалить вопрос");
}
});
export const copyQuestion = async (questionId: string, quizId: number) => requestQueue.enqueue(async () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
const frontId = nanoid();
if (question.type === null) {
const copiedQuestion = structuredClone(question);
copiedQuestion.id = frontId;
setProducedState(state => {
state.questions.push(copiedQuestion);
}, {
type: "copyQuestion",
questionId,
quizId,
});
return;
}
try {
const { updated: newQuestionId } = await questionApi.copy(question.backendId, quizId);
const copiedQuestion = structuredClone(question);
copiedQuestion.backendId = newQuestionId;
copiedQuestion.id = frontId;
copiedQuestion.content.id = frontId;
copiedQuestion.content.rule = { main: [], parentId: "", default: "" };
setProducedState(state => {
state.questions.push(copiedQuestion);
}, {
type: "copyQuestion",
questionId,
quizId,
});
updateQuestionOrders();
} catch (error) {
devlog("Error copying question", error);
enqueueSnackbar("Не удалось скопировать вопрос");
}
});
function setProducedState<A extends string | { type: unknown; }>(
recipe: (state: QuestionsStore) => void,
action?: A,
) {
useQuestionsStore.setState(state => produce(state, recipe), false, action);
};
export const cleardragQuestionContentId = () => {
useQuestionsStore.setState({ dragQuestionContentId: null });
};
export const getQuestionById = (questionId: string | null) => {
if (questionId === null) return null;
return useQuestionsStore.getState().questions.find(q => q.id === questionId) || null;
};
export const getQuestionByContentId = (questionContentId: string | null) => {
if (questionContentId === null) return null;
return useQuestionsStore.getState().questions.find(q => {
if (q.type === null) return false;
return (q.content.id === questionContentId);
}) || null;
};
export const updateOpenedModalSettingsId = (id?: string) => useQuestionsStore.setState({ openedModalSettingsId: id ? id : null });
export const updateDragQuestionContentId = (contentId?: string) => {
useQuestionsStore.setState({ dragQuestionContentId: contentId ? contentId : null });
};
export const clearRoleForAll = () => {
const { questions } = useQuestionsStore.getState();
questions.forEach(question => {
if (question.type !== null && (question.content.rule.main.length > 0 || question.content.rule.default.length > 0 || question.content.rule.parentId.length > 0)) {
updateQuestion(question.content.id, question => {
question.content.rule.parentId = "";
question.content.rule.main = [];
question.content.rule.default = "";
});
}
});
};
export const updateOpenBranchingPanel = (value: boolean) => useQuestionsStore.setState({ openBranchingPanel: !value });