264 lines
9.9 KiB
TypeScript
264 lines
9.9 KiB
TypeScript
import { devlog } from "@frontend/kitui";
|
||
import { QuizQuestionResult } from "@model/questionTypes/result";
|
||
import { AnyTypedQuizQuestion, QuestionBranchingRule, QuestionBranchingRuleMain, UntypedQuizQuestion } from "@model/questionTypes/shared";
|
||
import { Quiz } from "@model/quiz/quiz";
|
||
import { createResult, getQuestionByContentId, updateQuestion } from "@root/questions/actions";
|
||
import { useQuestionsStore } from "@root/questions/store";
|
||
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
|
||
import { useUiTools } from "@root/uiTools/store";
|
||
import { Core } from "cytoscape";
|
||
import { enqueueSnackbar } from "notistack";
|
||
|
||
interface Nodes {
|
||
data: {
|
||
id: string;
|
||
label: string;
|
||
parent?: string;
|
||
};
|
||
}
|
||
interface Edges {
|
||
data: {
|
||
source: string;
|
||
target: string;
|
||
};
|
||
}
|
||
|
||
export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => {
|
||
const nodes: Nodes[] = [];
|
||
const edges: Edges[] = [];
|
||
questions.forEach((question) => {
|
||
if (question.content.rule.parentId) {
|
||
nodes.push({
|
||
data: {
|
||
id: question.content.id,
|
||
label: question.title === "" || question.title === " " ? "noname" : question.title
|
||
}
|
||
});
|
||
// nodes.push({
|
||
// data: {
|
||
// id: "delete" + question.content.id,
|
||
// label: "X",
|
||
// parent: question.content.id,
|
||
// }
|
||
// },)
|
||
if (question.content.rule.parentId !== "root") edges.push({
|
||
data: {
|
||
source: question.content.rule.parentId,
|
||
target: question.content.id
|
||
}
|
||
});
|
||
}
|
||
});
|
||
return [...nodes, ...edges];
|
||
};
|
||
|
||
export function clearDataAfterAddNode({
|
||
parentNodeContentId,
|
||
targetQuestion,
|
||
}: {
|
||
parentNodeContentId: string;
|
||
targetQuestion: AnyTypedQuizQuestion;
|
||
}) {
|
||
const parentQuestion = { ...getQuestionByContentId(parentNodeContentId) } as AnyTypedQuizQuestion;
|
||
|
||
//смотрим не добавлен ли родителю result. Если да - делаем его неактивным. Веточкам result не нужен
|
||
useQuestionsStore.getState().questions.filter(
|
||
(question): question is QuizQuestionResult => question.type === "result"
|
||
).forEach((targetQuestion) => {
|
||
if (
|
||
targetQuestion.content.rule.parentId === parentQuestion.content.id
|
||
) {
|
||
updateQuestion<QuizQuestionResult>(targetQuestion.id, (q) => (q.content.usage = false));
|
||
}
|
||
});
|
||
|
||
//предупреждаем добавленный вопрос о том, кто его родитель
|
||
updateQuestion(targetQuestion.content.id, (question) => {
|
||
question.content.rule.parentId = parentNodeContentId;
|
||
question.content.rule.main = [];
|
||
//Это листик. Сбросим ему на всякий случай не листиковые поля
|
||
question.content.rule.children = [];
|
||
question.content.rule.default = "";
|
||
});
|
||
|
||
const noChild = parentQuestion.content.rule.children.length === 0;
|
||
|
||
//предупреждаем родителя о новом потомке (если он ещё не знает о нём)
|
||
if (!parentQuestion.content.rule.children.includes(targetQuestion.content.id))
|
||
updateQuestion(parentNodeContentId, (question) => {
|
||
question.content.rule.children = [...question.content.rule.children, targetQuestion.content.id];
|
||
//единственному ребёнку даём дефолт по-умолчанию
|
||
question.content.rule.default = noChild ? targetQuestion.content.id : question.content.rule.default;
|
||
});
|
||
|
||
if (!noChild) {
|
||
//детей больше 1
|
||
//- предупреждаем стор вопросов об открытии модалки ветвления
|
||
updateOpenedModalSettingsId(targetQuestion.content.id);
|
||
}
|
||
};
|
||
|
||
export function clearDataAfterRemoveNode({
|
||
trashQuestions,
|
||
targetQuestionContentId,
|
||
parentQuestionContentId,
|
||
}: {
|
||
trashQuestions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[],
|
||
targetQuestionContentId: string;
|
||
parentQuestionContentId: string;
|
||
}) {
|
||
updateQuestion(targetQuestionContentId, (question) => {
|
||
question.content.rule.parentId = "";
|
||
question.content.rule.children = [];
|
||
question.content.rule.main = [];
|
||
question.content.rule.default = "";
|
||
});
|
||
|
||
//Ищём родителя
|
||
const parentQuestion = getQuestionByContentId(parentQuestionContentId);
|
||
|
||
//Делаем результат родителя активным
|
||
const parentResult = trashQuestions.find(
|
||
(q): q is QuizQuestionResult => q.type === "result" && q.content.rule.parentId === parentQuestionContentId,
|
||
);
|
||
if (parentResult) {
|
||
updateQuestion<QuizQuestionResult>(parentResult.content.id, (q) => {
|
||
q.content.usage = true;
|
||
});
|
||
} else {
|
||
createResult(parentQuestionContentId);
|
||
}
|
||
|
||
//чистим rule родителя
|
||
if (!parentQuestion?.type) {
|
||
return;
|
||
}
|
||
|
||
const newChildren = [...parentQuestion.content.rule.children];
|
||
newChildren.splice(parentQuestion.content.rule.children.indexOf(targetQuestionContentId), 1);
|
||
|
||
const newRule: QuestionBranchingRule = {
|
||
children: newChildren,
|
||
default:
|
||
parentQuestion.content.rule.default === targetQuestionContentId
|
||
? ""
|
||
: parentQuestion.content.rule.default,
|
||
//удаляем условия перехода от родителя к этому вопросу,
|
||
main: parentQuestion.content.rule.main.filter(
|
||
(data: QuestionBranchingRuleMain) => data.next !== targetQuestionContentId,
|
||
),
|
||
parentId: parentQuestion.content.rule.parentId,
|
||
};
|
||
|
||
updateQuestion(parentQuestionContentId, (PQ) => {
|
||
PQ.content.rule = newRule;
|
||
});
|
||
};
|
||
|
||
export function calcNodePosition(node: any) {
|
||
const id = node.id();
|
||
const incomming = node.cy().edges(`[target="${id}"]`);
|
||
const layer = 0;
|
||
node.removeData("lastChild");
|
||
|
||
if (incomming.length === 0) {
|
||
if (node.cy().data("firstNode") === undefined) node.cy().data("firstNode", "root");
|
||
node.data("root", true);
|
||
const children = node.cy().edges(`[source="${id}"]`).targets();
|
||
node.data("layer", layer);
|
||
node.data("children", children.length);
|
||
const queue: any[] = [];
|
||
children.forEach((n: any) => {
|
||
queue.push({ task: n, layer: layer + 1 });
|
||
});
|
||
while (queue.length) {
|
||
const task = queue.pop();
|
||
task.task.data("layer", task.layer);
|
||
task.task.removeData("subtreeWidth");
|
||
const children = node.cy().edges(`[source="${task.task.id()}"]`).targets();
|
||
task.task.data("children", children.length);
|
||
if (children.length !== 0) {
|
||
children.forEach((n: any) => queue.push({ task: n, layer: task.layer + 1 }));
|
||
}
|
||
}
|
||
queue.push({ parent: node, children: children });
|
||
while (queue.length) {
|
||
const task = queue.pop();
|
||
if (task.children.length === 0) {
|
||
task.parent.data("subtreeWidth", task.parent.height() + 50);
|
||
continue;
|
||
}
|
||
const unprocessed = task?.children.filter((node: any) => {
|
||
return node.data("subtreeWidth") === undefined;
|
||
});
|
||
if (unprocessed.length !== 0) {
|
||
queue.push(task);
|
||
unprocessed.forEach((t: any) => {
|
||
queue.push({
|
||
parent: t,
|
||
children: t.cy().edges(`[source="${t.id()}"]`).targets(),
|
||
});
|
||
});
|
||
continue;
|
||
}
|
||
|
||
task?.parent.data(
|
||
"subtreeWidth",
|
||
task.children.reduce((p: any, n: any) => p + n.data("subtreeWidth"), 0),
|
||
);
|
||
}
|
||
|
||
const pos = { x: 0, y: 0 };
|
||
node.data("oldPos", pos);
|
||
|
||
queue.push({ task: children, parent: node });
|
||
while (queue.length) {
|
||
const task = queue.pop();
|
||
const oldPos = task.parent.data("oldPos");
|
||
let yoffset = oldPos.y - task.parent.data("subtreeWidth") / 2;
|
||
task.task.forEach((n: any) => {
|
||
const width = n.data("subtreeWidth");
|
||
|
||
n.data("oldPos", {
|
||
x: 250 * n.data("layer"),
|
||
y: yoffset + width / 2,
|
||
});
|
||
yoffset += width;
|
||
queue.push({
|
||
task: n.cy().edges(`[source="${n.id()}"]`).targets(),
|
||
parent: n,
|
||
});
|
||
});
|
||
}
|
||
return pos;
|
||
} else {
|
||
const opos = node.data("oldPos");
|
||
if (opos) {
|
||
return opos;
|
||
}
|
||
}
|
||
}
|
||
|
||
export const addNode = ({
|
||
parentNodeContentId,
|
||
targetNodeContentId,
|
||
}: {
|
||
parentNodeContentId: string;
|
||
targetNodeContentId?: string;
|
||
}) => {
|
||
//запрещаем работу родителя-ребенка если это один и тот же вопрос
|
||
if (parentNodeContentId === targetNodeContentId) return;
|
||
|
||
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
|
||
const targetQuestion = {
|
||
...getQuestionByContentId(targetNodeContentId || useUiTools.getState().dragQuestionContentId),
|
||
} as AnyTypedQuizQuestion;
|
||
|
||
if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId) {
|
||
clearDataAfterAddNode({ parentNodeContentId, targetQuestion });
|
||
createResult(targetQuestion.content.id);
|
||
} else {
|
||
enqueueSnackbar("Добавляемый вопрос не найден");
|
||
}
|
||
};
|