frontPanel/src/stores/questions/actions.ts

394 lines
13 KiB
TypeScript
Raw Normal View History

2023-11-02 16:45:28 +00:00
import { questionApi } from "@api/question";
2023-12-01 18:05:59 +00:00
import { quizApi } from "@api/quiz";
2023-11-02 16:45:28 +00:00
import { devlog } from "@frontend/kitui";
2023-11-27 23:07:24 +00:00
import { questionToEditQuestionRequest } from "@model/question/edit";
import { QuestionType, RawQuestion, rawQuestionToQuestion } from "@model/question/question";
2023-11-29 13:49:52 +00:00
import { AnyTypedQuizQuestion, QuestionVariant, UntypedQuizQuestion, createQuestionVariant } from "@model/questionTypes/shared";
import { defaultQuestionByType } from "../../constants/default";
2023-11-02 16:45:28 +00:00
import { produce } from "immer";
2023-11-28 19:16:00 +00:00
import { nanoid } from "nanoid";
2023-11-02 16:45:28 +00:00
import { enqueueSnackbar } from "notistack";
2023-11-14 16:44:27 +00:00
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
import { RequestQueue } from "../../utils/requestQueue";
2023-11-27 23:07:24 +00:00
import { QuestionsStore, useQuestionsStore } from "./store";
2023-11-02 16:45:28 +00:00
2023-11-14 20:15:52 +00:00
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => {
2023-11-29 13:49:52 +00:00
const untypedQuestions = state.questions.filter(q => q.type === null);
state.questions = questions?.map(rawQuestionToQuestion) ?? [];
2023-11-29 13:49:52 +00:00
state.questions.push(...untypedQuestions);
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
2023-11-29 13:49:52 +00:00
export const createUntypedQuestion = (quizId: number) => setProducedState(state => {
state.questions.push({
id: nanoid(),
quizId,
type: null,
title: "",
description: "",
deleted: false,
expanded: true,
});
2023-11-17 15:42:49 +00:00
}, {
2023-11-29 13:49:52 +00:00
type: "createUntypedQuestion",
quizId,
2023-11-17 15:42:49 +00:00
});
2023-11-27 23:07:24 +00:00
const removeQuestion = (questionId: string) => setProducedState(state => {
const index = state.questions.findIndex(q => q.id === questionId);
2023-11-27 23:07:24 +00:00
if (index === -1) return;
state.questions.splice(index, 1);
2023-11-15 18:38:02 +00:00
}, {
type: "removeQuestion",
questionId,
});
2023-11-29 13:49:52 +00:00
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",
});
2023-11-27 23:07:24 +00:00
const setQuestionBackendId = (questionId: string, backendId: number) => setProducedState(state => {
const question = state.questions.find(q => q.id === questionId);
2023-11-02 16:45:28 +00:00
if (!question) return;
2023-11-29 13:49:52 +00:00
if (question.type === null) throw new Error("Cannot set backend id for untyped question");
2023-11-02 16:45:28 +00:00
2023-11-27 23:07:24 +00:00
question.backendId = backendId;
2023-11-02 16:45:28 +00:00
}, {
2023-11-27 23:07:24 +00:00
type: "setQuestionBackendId",
questionId: questionId,
backendId,
2023-11-02 16:45:28 +00:00
});
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-29 13:49:52 +00:00
}, {
type: "reorderQuestions",
sourceIndex,
destinationIndex,
});
};
2023-11-27 23:07:24 +00:00
export const toggleExpandQuestion = (questionId: string) => 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;
2023-11-29 13:49:52 +00:00
}, {
type: "toggleExpandQuestion",
questionId,
2023-11-15 18:38:02 +00:00
});
export const collapseAllQuestions = () => setProducedState(state => {
state.questions.forEach(question => question.expanded = false);
2023-11-29 13:49:52 +00:00
}, "collapseAllQuestions");
const REQUEST_DEBOUNCE = 200;
const requestQueue = new RequestQueue();
let requestTimeoutId: ReturnType<typeof setTimeout>;
export const updateQuestion = (
questionId: string,
updateFn: (question: AnyTypedQuizQuestion) => void,
) => {
setProducedState(state => {
const question = state.questions.find(q => q.id === questionId) || state.questions.find(q => q.content.id === questionId);
2023-11-29 13:49:52 +00:00
if (!question) return;
if (question.type === null) throw new Error("Cannot update untyped question, use 'updateUntypedQuestion' instead");
2023-11-29 13:49:52 +00:00
updateFn(question);
}, {
type: "updateQuestion",
questionId,
updateFn: updateFn.toString(),
});
// clearTimeout(requestTimeoutId);
// requestTimeoutId = setTimeout(() => {
2023-11-29 13:49:52 +00:00
requestQueue.enqueue(async () => {
const q = useQuestionsStore.getState().questions.find(q => q.id === questionId) || useQuestionsStore.getState().questions.find(q => q.content.id === questionId);
console.log("мы пытаемся найти вопрос ")
2023-11-29 13:49:52 +00:00
if (!q) return;
if (q.type === null) throw new Error("Cannot send update request for untyped question");
console.log(q.title)
2023-11-29 13:49:52 +00:00
const response = await questionApi.edit(questionToEditQuestionRequest(q));
setQuestionBackendId(questionId, response.updated);
}).catch(error => {
if (isAxiosCanceledError(error)) return;
devlog("Error editing question", { error, questionId });
enqueueSnackbar("Не удалось сохранить вопрос");
});
// }, REQUEST_DEBOUNCE);
2023-11-29 13:49:52 +00:00
};
2023-11-27 23:07:24 +00:00
export const addQuestionVariant = (questionId: string) => {
updateQuestion(questionId, question => {
2023-11-15 18:38:02 +00:00
switch (question.type) {
case "variant":
case "emoji":
case "select":
case "images":
case "varimg":
2023-11-28 19:16:00 +00:00
question.content.variants.push(createQuestionVariant());
2023-11-15 18:38:02 +00:00
break;
2023-11-28 19:16:00 +00:00
default: throw new Error(`Cannot add variant to question of type "${question.type}"`);
2023-11-15 18:38:02 +00:00
}
});
};
2023-11-27 23:07:24 +00:00
export const deleteQuestionVariant = (questionId: string, variantId: string) => {
updateQuestion(questionId, question => {
2023-11-15 18:38:02 +00:00
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 = (
2023-11-27 23:07:24 +00:00
questionId: string,
2023-11-15 18:38:02 +00:00
variantId: string,
field: keyof QuestionVariant,
value: QuestionVariant[keyof QuestionVariant],
) => {
2023-11-27 23:07:24 +00:00
updateQuestion(questionId, question => {
2023-11-15 18:38:02 +00:00
if (!("variants" in question.content)) return;
2023-12-01 14:33:55 +00:00
2023-11-15 18:38:02 +00:00
const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId);
if (variantIndex === -1) return;
2023-12-01 14:33:55 +00:00
2023-11-15 18:38:02 +00:00
const variant = question.content.variants[variantIndex];
variant[field] = value;
});
};
export const reorderQuestionVariants = (
2023-11-27 23:07:24 +00:00
questionId: string,
2023-11-15 18:38:02 +00:00
sourceIndex: number,
destinationIndex: number,
) => {
if (sourceIndex === destinationIndex) return;
2023-11-27 23:07:24 +00:00
updateQuestion(questionId, question => {
2023-11-15 18:38:02 +00:00
if (!("variants" in question.content)) return;
2023-12-01 14:33:55 +00:00
2023-11-15 18:38:02 +00:00
const [removed] = question.content.variants.splice(sourceIndex, 1);
question.content.variants.splice(destinationIndex, 0, removed);
});
};
2023-12-01 18:05:59 +00:00
export const uploadQuestionImage = async (
2023-11-27 23:07:24 +00:00
questionId: string,
2023-12-01 18:05:59 +00:00
quizQid: string | undefined,
blob: Blob,
updateFn: (question: AnyTypedQuizQuestion, imageId: string) => void,
) => {
2023-12-01 18:05:59 +00:00
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question || !quizQid) return;
2023-12-01 18:05:59 +00:00
try {
const response = await quizApi.addImages(question.quizId, blob);
2023-12-01 18:05:59 +00:00
const values = Object.values(response);
if (values.length !== 1) {
console.warn("Error uploading image");
return;
}
2023-12-01 18:05:59 +00:00
const imageId = values[0];
const imageUrl = `https://squiz.pena.digital/squizimages/${quizQid}/${imageId}`;
2023-12-01 18:05:59 +00:00
updateQuestion(questionId, question => {
updateFn(question, imageUrl);
});
2023-12-01 18:05:59 +00:00
return imageUrl;
} catch (error) {
devlog("Error uploading question image", error);
2023-12-01 18:05:59 +00:00
enqueueSnackbar("Не удалось загрузить изображение");
2023-12-01 14:33:55 +00:00
}
};
export const setQuestionInnerName = (
2023-11-27 23:07:24 +00:00
questionId: string,
name: string,
) => {
2023-11-27 23:07:24 +00:00
updateQuestion(questionId, question => {
question.content.innerName = name;
});
};
2023-11-29 13:49:52 +00:00
export const changeQuestionType = (
2023-11-27 23:07:24 +00:00
questionId: string,
2023-11-29 13:49:52 +00:00
type: QuestionType,
2023-11-02 16:45:28 +00:00
) => {
2023-11-29 13:49:52 +00:00
updateQuestion(questionId, question => {
question.type = type;
question.content = defaultQuestionByType[type].content;
2023-11-27 23:07:24 +00:00
});
2023-11-02 16:45:28 +00:00
};
2023-11-29 13:49:52 +00:00
export const createTypedQuestion = async (
questionId: string,
type: QuestionType,
) => requestQueue.enqueue(async () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
if (question.type !== null) throw new Error("Cannot upgrade already typed question");
2023-11-02 16:45:28 +00:00
try {
2023-11-29 13:49:52 +00:00
const createdQuestion = await questionApi.create({
quiz_id: question.quizId,
type,
2023-11-29 13:49:52 +00:00
title: question.title,
description: question.description,
page: 0,
required: true,
content: JSON.stringify(defaultQuestionByType[type].content),
2023-11-14 16:44:27 +00:00
});
2023-11-02 16:45:28 +00:00
2023-11-29 13:49:52 +00:00
setProducedState(state => {
const questionIndex = state.questions.findIndex(q => q.id === questionId);
if (questionIndex !== -1) state.questions.splice(
questionIndex,
1,
rawQuestionToQuestion(createdQuestion)
);
}, {
type: "createTypedQuestion",
question,
});
2023-11-02 16:45:28 +00:00
} catch (error) {
devlog("Error creating question", error);
enqueueSnackbar("Не удалось создать вопрос");
}
2023-11-27 23:07:24 +00:00
});
export const deleteQuestion = async (questionId: string) => requestQueue.enqueue(async () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
2023-11-02 16:45:28 +00:00
2023-11-29 13:49:52 +00:00
if (question.type === null) {
removeQuestion(questionId);
return;
}
2023-11-02 16:45:28 +00:00
try {
2023-11-27 23:07:24 +00:00
await questionApi.delete(question.backendId);
2023-11-02 16:45:28 +00:00
removeQuestion(questionId);
} catch (error) {
devlog("Error deleting question", error);
enqueueSnackbar("Не удалось удалить вопрос");
}
2023-11-27 23:07:24 +00:00
});
export const copyQuestion = async (questionId: string, quizId: number) => requestQueue.enqueue(async () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
2023-11-02 16:45:28 +00:00
2023-11-29 13:49:52 +00:00
if (question.type === null) {
const copiedQuestion = structuredClone(question);
copiedQuestion.id = nanoid();
setProducedState(state => {
state.questions.push(copiedQuestion);
}, {
type: "copyQuestion",
questionId,
quizId,
});
return;
}
2023-11-15 18:38:02 +00:00
try {
2023-11-27 23:07:24 +00:00
const { updated: newQuestionId } = await questionApi.copy(question.backendId, quizId);
2023-11-15 18:38:02 +00:00
2023-11-27 23:07:24 +00:00
const copiedQuestion = structuredClone(question);
copiedQuestion.backendId = newQuestionId;
copiedQuestion.id = nanoid();
2023-11-15 18:38:02 +00:00
2023-11-27 23:07:24 +00:00
setProducedState(state => {
state.questions.push(copiedQuestion);
2023-11-15 18:38:02 +00:00
}, {
type: "copyQuestion",
2023-11-29 13:49:52 +00:00
questionId,
2023-11-15 18:38:02 +00:00
quizId,
});
} catch (error) {
devlog("Error copying question", error);
enqueueSnackbar("Не удалось скопировать вопрос");
}
2023-11-27 23:07:24 +00:00
});
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);
2023-11-29 15:45:15 +00:00
};
export const cleardragQuestionContentId = () => {
useQuestionsStore.setState({dragQuestionContentId: null});
2023-11-29 15:45:15 +00:00
};
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) => {
console.log("questionContentId " + questionContentId)
if (questionContentId === null) return null;
return useQuestionsStore.getState().questions.find(q => {
console.log(q.content.id)
console.log(q.content.id === questionContentId)
return ( q.content.id === questionContentId)}) || null;
};
2023-11-29 15:45:15 +00:00
export const updateOpenedModalSettingsId = (id?: string) => useQuestionsStore.setState({openedModalSettingsId: id ? id : null});
export const updateDragQuestionContentId = (contentId?: string) => {
console.log("contentId " + contentId)
useQuestionsStore.setState({dragQuestionContentId: contentId ? contentId : null});
};
export const updateOpenBranchingPanel = (value: boolean) => useQuestionsStore.setState({openBranchingPanel: !value});