frontPanel/src/stores/questions/actions.ts

393 lines
13 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 { defaultQuestionByType } from "../../constants/default";
import { produce } from "immer";
import { nanoid } from "nanoid";
import { enqueueSnackbar } from "notistack";
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,
});
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,
});
};
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,
) => {
setProducedState(state => {
const question = state.questions.find(q => q.id === questionId) || state.questions.find(q => 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);
// requestTimeoutId = setTimeout(() => {
requestQueue.enqueue(async () => {
const q = useQuestionsStore.getState().questions.find(q => q.id === questionId) || useQuestionsStore.getState().questions.find(q => q.content.id === questionId);
console.log("мы пытаемся найти вопрос ")
if (!q) return;
if (q.type === null) throw new Error("Cannot send update request for untyped question");
console.log(q.title)
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);
};
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 => {
question.type = type;
question.content = defaultQuestionByType[type].content;
});
};
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");
try {
const createdQuestion = await questionApi.create({
quiz_id: question.quizId,
type,
title: question.title,
description: question.description,
page: 0,
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) => 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);
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;
if (question.type === null) {
const copiedQuestion = structuredClone(question);
copiedQuestion.id = nanoid();
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 = nanoid();
setProducedState(state => {
state.questions.push(copiedQuestion);
}, {
type: "copyQuestion",
questionId,
quizId,
});
} 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) => {
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;
};
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});
}