frontPanel/src/pages/Questions/BranchingMap/helper.ts

347 lines
10 KiB
TypeScript
Raw Normal View History

2024-01-05 16:48:35 +00:00
import { QuizQuestionResult } from "@model/questionTypes/result";
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: {
isRoot: boolean;
2023-12-31 02:53:25 +00:00
id: string;
label: string;
parent?: string;
};
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 {
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) {
const extent = node.cy().extent();
const bb = node.boundingBox();
2024-01-17 15:42:25 +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[]) => {
const nodes: Node[] = [];
const edges: Edge[] = [];
2023-12-31 02:53:25 +00:00
questions.forEach((question) => {
if (question.content.rule.parentId) {
let label =
question.title === "" || question.title === " "
? "noname"
: question.title;
if (label.length > 25) label = label.slice(0, 25) + "…";
2024-01-17 15:42:25 +00:00
2023-12-31 02:53:25 +00:00
nodes.push({
data: {
isRoot: question.content.rule.parentId === "root",
2023-12-31 02:53:25 +00:00
id: question.content.id,
label,
2023-12-31 02:53:25 +00:00
},
classes: "multiline-auto",
2023-12-31 02:53:25 +00:00
});
// 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];
};
2024-01-05 16:48:35 +00:00
2024-01-10 18:23:38 +00:00
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,
2024-01-10 18:23:38 +00:00
};
2024-01-05 16:48:35 +00:00
export function clearDataAfterAddNode({
parentNodeContentId,
targetQuestion,
2024-01-05 16:48:35 +00:00
}: {
parentNodeContentId: string;
targetQuestion: AnyTypedQuizQuestion;
2024-01-05 16:48:35 +00:00
}) {
const parentQuestion = {
...getQuestionByContentId(parentNodeContentId),
} as AnyTypedQuizQuestion;
2024-01-05 16:48:35 +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
});
//предупреждаем добавленный вопрос о том, кто его родитель
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
const noChild = parentQuestion.content.rule.children.length === 0;
2024-01-05 16:48:35 +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
if (!noChild) {
//детей больше 1
//- предупреждаем стор вопросов об открытии модалки ветвления
updateOpenedModalSettingsId(targetQuestion.content.id);
}
}
2024-01-05 16:48:35 +00:00
export function clearDataAfterRemoveNode({
trashQuestions,
targetQuestionContentId,
parentQuestionContentId,
2024-01-05 16:48:35 +00:00
}: {
trashQuestions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[];
targetQuestionContentId: string;
parentQuestionContentId: string;
2024-01-05 16:48:35 +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
//Ищём родителя
const parentQuestion = getQuestionByContentId(parentQuestionContentId);
2024-01-05 16:48:35 +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-01-05 16:48:35 +00:00
//чистим rule родителя
if (!parentQuestion?.type) {
return;
}
2024-01-05 16:48:35 +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
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
updateQuestion(parentQuestionContentId, (PQ) => {
PQ.content.rule = newRule;
});
}
2024-01-05 16:48:35 +00:00
export function calcNodePosition(node: any) {
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
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
});
continue;
}
2024-01-05 16:48:35 +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
const pos = { x: 0, y: 0 };
node.data("oldPos", pos);
2024-01-05 16:48:35 +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
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
}
return pos;
} else {
const opos = node.data("oldPos");
if (opos) {
return opos;
}
}
}
2024-01-05 16:48:35 +00:00
export const addNode = ({
parentNodeContentId,
targetNodeContentId,
2024-01-05 16:48:35 +00:00
}: {
parentNodeContentId: string;
targetNodeContentId?: string;
2024-01-05 16:48:35 +00:00
}) => {
//запрещаем работу родителя-ребенка если это один и тот же вопрос
if (parentNodeContentId === targetNodeContentId) return;
2024-01-05 16:48:35 +00:00
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
const targetQuestion = {
...getQuestionByContentId(
targetNodeContentId || useUiTools.getState().dragQuestionContentId,
),
} as AnyTypedQuizQuestion;
2024-01-05 16:48:35 +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);
} else {
enqueueSnackbar("Добавляемый вопрос не найден");
}
2024-01-05 16:48:35 +00:00
};