import { QuizQuestionResult } from "@model/questionTypes/result"; import { AnyTypedQuizQuestion, QuestionBranchingRule, QuestionBranchingRuleMain, UntypedQuizQuestion } from "@model/questionTypes/shared"; 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 { PresetLayoutOptions } 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 const layoutOptions: PresetLayoutOptions = { name: "preset", positions: calcNodePosition, zoom: undefined, pan: 1, fit: false, padding: 30, animate: false, animationDuration: 500, animationEasing: undefined, animateFilter: () => false, ready: event => { if (event.cy.data("firstNode") === "nonroot") { event.cy.data("firstNode", "root"); event.cy.nodes().sort((a, b) => (a.data("root") ? 1 : -1)); } else { event.cy.removeData("firstNode"); } }, transform: (_, p) => p, }; 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(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(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("Добавляемый вопрос не найден"); } };