2024-03-18 02:46:04 +00:00
|
|
|
|
import { QuestionType } from "@model/question/question";
|
2024-01-05 16:48:35 +00:00
|
|
|
|
import { QuizQuestionResult } from "@model/questionTypes/result";
|
2024-02-23 14:07:44 +00:00
|
|
|
|
import {
|
|
|
|
|
AnyTypedQuizQuestion,
|
|
|
|
|
QuestionBranchingRule,
|
|
|
|
|
QuestionBranchingRuleMain,
|
|
|
|
|
UntypedQuizQuestion,
|
|
|
|
|
} from "@model/questionTypes/shared";
|
|
|
|
|
import {
|
|
|
|
|
createResult,
|
|
|
|
|
getQuestionByContentId,
|
|
|
|
|
updateQuestion,
|
|
|
|
|
} from "@root/questions/actions";
|
2024-01-05 16:48:35 +00:00
|
|
|
|
import { useQuestionsStore } from "@root/questions/store";
|
2024-02-23 14:23:26 +00:00
|
|
|
|
import { useQuizStore } from "@root/quizes/store";
|
2024-01-05 16:48:35 +00:00
|
|
|
|
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
|
|
|
|
|
import { useUiTools } from "@root/uiTools/store";
|
2024-01-17 15:42:25 +00:00
|
|
|
|
import { NodeSingular, PresetLayoutOptions } from "cytoscape";
|
2024-01-05 16:48:35 +00:00
|
|
|
|
import { enqueueSnackbar } from "notistack";
|
2023-11-29 15:45:15 +00:00
|
|
|
|
|
2024-01-17 15:42:25 +00:00
|
|
|
|
export interface Node {
|
2023-12-31 02:53:25 +00:00
|
|
|
|
data: {
|
2024-02-23 14:07:44 +00:00
|
|
|
|
isRoot: boolean;
|
2023-12-31 02:53:25 +00:00
|
|
|
|
id: string;
|
|
|
|
|
label: string;
|
2024-03-19 01:38:37 +00:00
|
|
|
|
qtype: string;
|
|
|
|
|
type: string;
|
2023-12-31 02:53:25 +00:00
|
|
|
|
parent?: string;
|
2024-03-19 01:38:37 +00:00
|
|
|
|
children: number;
|
2023-12-31 02:53:25 +00:00
|
|
|
|
};
|
2024-02-23 14:07:44 +00:00
|
|
|
|
classes: string;
|
2023-11-29 15:45:15 +00:00
|
|
|
|
}
|
2024-01-10 18:23:38 +00:00
|
|
|
|
|
2024-01-17 15:42:25 +00:00
|
|
|
|
export interface Edge {
|
2023-12-31 02:53:25 +00:00
|
|
|
|
data: {
|
|
|
|
|
source: string;
|
|
|
|
|
target: string;
|
|
|
|
|
};
|
2023-11-29 15:45:15 +00:00
|
|
|
|
}
|
|
|
|
|
|
2024-01-17 15:42:25 +00:00
|
|
|
|
export function isElementANode(element: Node | Edge): element is Node {
|
2024-02-23 14:07:44 +00:00
|
|
|
|
return !("source" in element.data && "target" in element.data);
|
2024-01-17 15:42:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export function isNodeInViewport(node: NodeSingular, padding: number = 0) {
|
2024-02-23 14:07:44 +00:00
|
|
|
|
const extent = node.cy().extent();
|
|
|
|
|
const bb = node.boundingBox();
|
2024-01-17 15:42:25 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
return (
|
|
|
|
|
bb.x2 > extent.x1 - padding &&
|
|
|
|
|
bb.x1 < extent.x2 + padding &&
|
|
|
|
|
bb.y2 > extent.y1 - padding &&
|
|
|
|
|
bb.y1 < extent.y2 + padding
|
|
|
|
|
);
|
2024-01-17 15:42:25 +00:00
|
|
|
|
}
|
|
|
|
|
|
2023-12-03 13:09:57 +00:00
|
|
|
|
export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => {
|
2024-02-23 14:07:44 +00:00
|
|
|
|
const nodes: Node[] = [];
|
|
|
|
|
const edges: Edge[] = [];
|
2023-12-31 02:53:25 +00:00
|
|
|
|
questions.forEach((question) => {
|
|
|
|
|
if (question.content.rule.parentId) {
|
2024-03-19 01:38:37 +00:00
|
|
|
|
const parentQuestion = {
|
|
|
|
|
...getQuestionByContentId(question.content.rule.parentId),
|
|
|
|
|
} as AnyTypedQuizQuestion;
|
2024-02-23 14:07:44 +00:00
|
|
|
|
let label =
|
|
|
|
|
question.title === "" || question.title === " "
|
|
|
|
|
? "noname"
|
|
|
|
|
: question.title;
|
2024-03-18 02:46:04 +00:00
|
|
|
|
if (label.length > 10) label = label.slice(0, 10).toLowerCase() + "…";
|
2024-01-17 15:42:25 +00:00
|
|
|
|
|
2023-12-31 02:53:25 +00:00
|
|
|
|
nodes.push({
|
|
|
|
|
data: {
|
2024-02-23 14:07:44 +00:00
|
|
|
|
isRoot: question.content.rule.parentId === "root",
|
2023-12-31 02:53:25 +00:00
|
|
|
|
id: question.content.id,
|
2024-02-23 14:07:44 +00:00
|
|
|
|
label,
|
2024-03-27 21:46:12 +00:00
|
|
|
|
qtype:
|
|
|
|
|
question.content.rule.parentId === "root"
|
|
|
|
|
? "root"
|
|
|
|
|
: parentQuestion.type,
|
2024-03-19 01:38:37 +00:00
|
|
|
|
type: question.type,
|
2024-03-27 21:46:12 +00:00
|
|
|
|
children: question.content.rule.children.length,
|
2023-12-31 02:53:25 +00:00
|
|
|
|
},
|
2024-02-23 14:07:44 +00:00
|
|
|
|
classes: "multiline-auto",
|
2023-12-31 02:53:25 +00:00
|
|
|
|
});
|
|
|
|
|
if (question.content.rule.parentId !== "root")
|
|
|
|
|
edges.push({
|
|
|
|
|
data: {
|
|
|
|
|
source: question.content.rule.parentId,
|
|
|
|
|
target: question.content.id,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
return [...nodes, ...edges];
|
|
|
|
|
};
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-01-10 18:23:38 +00:00
|
|
|
|
export const layoutOptions: PresetLayoutOptions = {
|
2024-02-23 14:07:44 +00:00
|
|
|
|
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,
|
2024-01-10 18:23:38 +00:00
|
|
|
|
};
|
|
|
|
|
|
2024-01-05 16:48:35 +00:00
|
|
|
|
export function clearDataAfterAddNode({
|
2024-02-23 14:07:44 +00:00
|
|
|
|
parentNodeContentId,
|
|
|
|
|
targetQuestion,
|
2024-01-05 16:48:35 +00:00
|
|
|
|
}: {
|
2024-02-23 14:07:44 +00:00
|
|
|
|
parentNodeContentId: string;
|
|
|
|
|
targetQuestion: AnyTypedQuizQuestion;
|
2024-01-05 16:48:35 +00:00
|
|
|
|
}) {
|
2024-02-23 14:07:44 +00:00
|
|
|
|
const parentQuestion = {
|
|
|
|
|
...getQuestionByContentId(parentNodeContentId),
|
|
|
|
|
} as AnyTypedQuizQuestion;
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
//смотрим не добавлен ли родителю 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),
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-01-05 16:48:35 +00:00
|
|
|
|
});
|
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
//предупреждаем добавленный вопрос о том, кто его родитель
|
|
|
|
|
updateQuestion(targetQuestion.content.id, (question) => {
|
|
|
|
|
question.content.rule.parentId = parentNodeContentId;
|
|
|
|
|
question.content.rule.main = [];
|
|
|
|
|
//Это листик. Сбросим ему на всякий случай не листиковые поля
|
|
|
|
|
question.content.rule.children = [];
|
|
|
|
|
question.content.rule.default = "";
|
|
|
|
|
});
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
const noChild = parentQuestion.content.rule.children.length === 0;
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
//предупреждаем родителя о новом потомке (если он ещё не знает о нём)
|
|
|
|
|
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;
|
|
|
|
|
});
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
if (!noChild) {
|
|
|
|
|
//детей больше 1
|
|
|
|
|
//- предупреждаем стор вопросов об открытии модалки ветвления
|
|
|
|
|
updateOpenedModalSettingsId(targetQuestion.content.id);
|
|
|
|
|
}
|
|
|
|
|
}
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
|
|
|
|
export function clearDataAfterRemoveNode({
|
2024-02-23 14:07:44 +00:00
|
|
|
|
trashQuestions,
|
|
|
|
|
targetQuestionContentId,
|
|
|
|
|
parentQuestionContentId,
|
2024-01-05 16:48:35 +00:00
|
|
|
|
}: {
|
2024-02-23 14:07:44 +00:00
|
|
|
|
trashQuestions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[];
|
|
|
|
|
targetQuestionContentId: string;
|
|
|
|
|
parentQuestionContentId: string;
|
2024-01-05 16:48:35 +00:00
|
|
|
|
}) {
|
2024-02-23 14:07:44 +00:00
|
|
|
|
updateQuestion(targetQuestionContentId, (question) => {
|
|
|
|
|
question.content.rule.parentId = "";
|
|
|
|
|
question.content.rule.children = [];
|
|
|
|
|
question.content.rule.main = [];
|
|
|
|
|
question.content.rule.default = "";
|
|
|
|
|
});
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
//Ищём родителя
|
|
|
|
|
const parentQuestion = getQuestionByContentId(parentQuestionContentId);
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
//Делаем результат родителя активным
|
|
|
|
|
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 {
|
2024-02-23 14:23:26 +00:00
|
|
|
|
createResult(useQuizStore.getState().editQuizId, parentQuestionContentId);
|
2024-02-23 14:07:44 +00:00
|
|
|
|
}
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
//чистим rule родителя
|
|
|
|
|
if (!parentQuestion?.type) {
|
|
|
|
|
return;
|
|
|
|
|
}
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
const newChildren = [...parentQuestion.content.rule.children];
|
|
|
|
|
newChildren.splice(
|
|
|
|
|
parentQuestion.content.rule.children.indexOf(targetQuestionContentId),
|
|
|
|
|
1,
|
|
|
|
|
);
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
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,
|
|
|
|
|
};
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
updateQuestion(parentQuestionContentId, (PQ) => {
|
|
|
|
|
PQ.content.rule = newRule;
|
|
|
|
|
});
|
|
|
|
|
}
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
|
|
|
|
export function calcNodePosition(node: any) {
|
2024-02-23 14:07:44 +00:00
|
|
|
|
const id = node.id();
|
|
|
|
|
const incomming = node.cy().edges(`[target="${id}"]`);
|
|
|
|
|
const layer = 0;
|
|
|
|
|
node.removeData("lastChild");
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
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(),
|
|
|
|
|
});
|
2024-01-05 16:48:35 +00:00
|
|
|
|
});
|
2024-02-23 14:07:44 +00:00
|
|
|
|
continue;
|
|
|
|
|
}
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
task?.parent.data(
|
|
|
|
|
"subtreeWidth",
|
|
|
|
|
task.children.reduce((p: any, n: any) => p + n.data("subtreeWidth"), 0),
|
|
|
|
|
);
|
|
|
|
|
}
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
const pos = { x: 0, y: 0 };
|
|
|
|
|
node.data("oldPos", pos);
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
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");
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
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,
|
|
|
|
|
});
|
|
|
|
|
});
|
2024-01-05 16:48:35 +00:00
|
|
|
|
}
|
2024-02-23 14:07:44 +00:00
|
|
|
|
return pos;
|
|
|
|
|
} else {
|
|
|
|
|
const opos = node.data("oldPos");
|
|
|
|
|
if (opos) {
|
|
|
|
|
return opos;
|
|
|
|
|
}
|
|
|
|
|
}
|
2023-12-02 09:59:31 +00:00
|
|
|
|
}
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
|
|
|
|
export const addNode = ({
|
2024-02-23 14:07:44 +00:00
|
|
|
|
parentNodeContentId,
|
|
|
|
|
targetNodeContentId,
|
2024-01-05 16:48:35 +00:00
|
|
|
|
}: {
|
2024-02-23 14:07:44 +00:00
|
|
|
|
parentNodeContentId: string;
|
|
|
|
|
targetNodeContentId?: string;
|
2024-01-05 16:48:35 +00:00
|
|
|
|
}) => {
|
2024-02-23 14:07:44 +00:00
|
|
|
|
//запрещаем работу родителя-ребенка если это один и тот же вопрос
|
|
|
|
|
if (parentNodeContentId === targetNodeContentId) return;
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-03-18 02:46:04 +00:00
|
|
|
|
//У 4 типов вопросов не может быть больше 1 потомка
|
|
|
|
|
const parentQuestion = {
|
|
|
|
|
...getQuestionByContentId(parentNodeContentId),
|
|
|
|
|
} as AnyTypedQuizQuestion;
|
2024-03-27 21:46:12 +00:00
|
|
|
|
if (
|
|
|
|
|
parentQuestion.type !== undefined &&
|
|
|
|
|
isQuestionProhibited(parentQuestion.type) &&
|
2024-03-18 02:46:04 +00:00
|
|
|
|
parentQuestion.content.rule.children.length > 0
|
|
|
|
|
) {
|
|
|
|
|
enqueueSnackbar("у вопроса этого типа может быть только 1 потомок");
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
|
|
|
|
|
const targetQuestion = {
|
|
|
|
|
...getQuestionByContentId(
|
|
|
|
|
targetNodeContentId || useUiTools.getState().dragQuestionContentId,
|
|
|
|
|
),
|
|
|
|
|
} as AnyTypedQuizQuestion;
|
2024-01-05 16:48:35 +00:00
|
|
|
|
|
2024-02-23 14:07:44 +00:00
|
|
|
|
if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId) {
|
|
|
|
|
clearDataAfterAddNode({ parentNodeContentId, targetQuestion });
|
2024-02-23 14:23:26 +00:00
|
|
|
|
createResult(useQuizStore.getState().editQuizId, targetQuestion.content.id);
|
2024-02-23 14:07:44 +00:00
|
|
|
|
} else {
|
2024-03-27 21:46:12 +00:00
|
|
|
|
enqueueSnackbar(
|
|
|
|
|
"Добавляемый вопрос не найден. Перетащите вопрос из списка",
|
|
|
|
|
);
|
2024-02-23 14:07:44 +00:00
|
|
|
|
}
|
2024-01-05 16:48:35 +00:00
|
|
|
|
};
|
2024-03-18 02:46:04 +00:00
|
|
|
|
|
2024-03-27 21:46:12 +00:00
|
|
|
|
export const isQuestionProhibited = (parentQType: string) =>
|
2024-03-18 02:46:04 +00:00
|
|
|
|
parentQType === "text" ||
|
|
|
|
|
parentQType === "date" ||
|
|
|
|
|
parentQType === "number" ||
|
2024-03-27 21:46:12 +00:00
|
|
|
|
parentQType === "page";
|