frontPanel/src/pages/Questions/BranchingMap/CsComponent.tsx

393 lines
12 KiB
TypeScript
Raw Normal View History

2023-12-04 13:33:43 +00:00
import { useEffect, useLayoutEffect, useRef, useState } from "react";
2023-11-29 15:45:15 +00:00
import Cytoscape from "cytoscape";
import CytoscapeComponent from "react-cytoscapejs";
import popper from "cytoscape-popper";
import { Button, Box } from "@mui/material";
2023-12-20 12:34:07 +00:00
import { withErrorBoundary } from "react-error-boundary";
import { enqueueSnackbar } from "notistack";
2023-11-29 15:45:15 +00:00
import { useCurrentQuiz } from "@root/quizes/hooks";
2023-12-20 10:46:38 +00:00
import { updateRootContentId } from "@root/quizes/actions";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
2023-11-29 15:45:15 +00:00
import { useQuestionsStore } from "@root/questions/store";
2023-12-20 12:34:07 +00:00
import { useUiTools } from "@root/uiTools/store";
2023-12-20 10:46:38 +00:00
import {
deleteQuestion,
updateQuestion,
getQuestionByContentId,
clearRuleForAll,
createResult,
2023-12-20 10:46:38 +00:00
} from "@root/questions/actions";
import {
updateModalInfoWhyCantCreate,
2023-12-31 02:53:25 +00:00
updateOpenedModalSettingsId,
} from "@root/uiTools/actions";
import { cleardragQuestionContentId } from "@root/uiTools/actions";
2023-12-20 14:18:01 +00:00
import { updateDeleteId } from "@root/uiTools/actions";
import { DeleteNodeModal } from "../DeleteNodeModal";
import { ProblemIcon } from "@ui_kit/ProblemIcon";
2023-11-29 15:45:15 +00:00
import { useRemoveNode } from "./hooks/useRemoveNode";
import { usePopper } from "./hooks/usePopper";
2023-11-29 15:45:15 +00:00
import { storeToNodes } from "./helper";
import { stylesheet } from "./style/stylesheet";
import "./style/styles.css";
2023-11-29 15:45:15 +00:00
2023-12-20 12:34:07 +00:00
import type { Core } from "cytoscape";
2024-01-13 16:04:05 +00:00
import { nameCutter } from "./nameCutter";
2023-11-29 15:45:15 +00:00
Cytoscape.use(popper);
interface CsComponentProps {
modalQuestionParentContentId: string;
modalQuestionTargetContentId: string;
setOpenedModalQuestions: (open: boolean) => void;
setModalQuestionParentContentId: (id: string) => void;
setModalQuestionTargetContentId: (id: string) => void;
}
function CsComponent({
modalQuestionParentContentId,
modalQuestionTargetContentId,
setOpenedModalQuestions,
setModalQuestionParentContentId,
2023-12-31 02:53:25 +00:00
setModalQuestionTargetContentId,
}: CsComponentProps) {
2023-11-29 15:45:15 +00:00
const quiz = useCurrentQuiz();
2023-12-31 02:53:25 +00:00
const {
dragQuestionContentId,
desireToOpenABranchingModal,
canCreatePublic,
someWorkBackend,
} = useUiTools();
const trashQuestions = useQuestionsStore().questions;
const questions = trashQuestions.filter(
(question) =>
question.type !== "result" && question.type !== null && !question.deleted,
);
2023-11-29 15:45:15 +00:00
const [startCreate, setStartCreate] = useState("");
const [startRemove, setStartRemove] = useState("");
const cyRef = useRef<Core | null>(null);
2023-12-01 08:12:59 +00:00
const layoutsContainer = useRef<HTMLDivElement | null>(null);
2023-11-29 15:45:15 +00:00
const plusesContainer = useRef<HTMLDivElement | null>(null);
const crossesContainer = useRef<HTMLDivElement | null>(null);
const gearsContainer = useRef<HTMLDivElement | null>(null);
const { layoutOptions } = usePopper({
layoutsContainer,
plusesContainer,
crossesContainer,
gearsContainer,
setModalQuestionParentContentId,
setOpenedModalQuestions,
setStartCreate,
setStartRemove,
});
const { removeNode } = useRemoveNode({
cyRef,
layoutOptions,
layoutsContainer,
plusesContainer,
crossesContainer,
gearsContainer,
});
2023-12-31 10:36:30 +00:00
function fitGraphToRootNode() {
const cy = cyRef.current;
if (!cy) return;
const rootNode = cy.nodes().filter((n) => n.data("root"))[0];
if (!rootNode) throw new Error("Root node not found");
const height = cy.height();
const position = rootNode.position();
const shift = rootNode.width() / 2;
cy.pan({
x: position.x + shift,
y: position.y + height / 2,
});
}
useLayoutEffect(() => {
2023-12-31 02:53:25 +00:00
const cy = cyRef?.current;
if (desireToOpenABranchingModal) {
setTimeout(() => {
2023-12-31 02:53:25 +00:00
cy
?.getElementById(desireToOpenABranchingModal)
?.data("eroticeyeblink", true);
}, 250);
} else {
2023-12-31 02:53:25 +00:00
cy?.elements().data("eroticeyeblink", false);
}
2023-12-31 02:53:25 +00:00
}, [desireToOpenABranchingModal]);
//Техническая штучка. Гарантирует не отрисовку модалки по первому входу на страничку. И очистка данных по расскоменчиванию
//Быстро просто дешево и сердито :)
useLayoutEffect(() => {
2023-12-31 02:53:25 +00:00
updateOpenedModalSettingsId();
// updateRootContentId(quiz.id, "")
// clearRuleForAll()
2023-12-31 02:53:25 +00:00
}, []);
//Отлов mouseup для отрисовки ноды
useEffect(() => {
2023-12-31 02:53:25 +00:00
if (
modalQuestionTargetContentId.length !== 0 &&
modalQuestionParentContentId.length !== 0
) {
addNode({
parentNodeContentId: modalQuestionParentContentId,
targetNodeContentId: modalQuestionTargetContentId,
});
}
2023-12-31 02:53:25 +00:00
setModalQuestionParentContentId("");
setModalQuestionTargetContentId("");
}, [modalQuestionTargetContentId]);
const addNode = ({
parentNodeContentId,
targetNodeContentId,
}: {
parentNodeContentId: string;
targetNodeContentId?: string;
}) => {
2023-12-22 21:14:48 +00:00
if (quiz) {
//запрещаем работу родителя-ребенка если это один и тот же вопрос
2023-12-31 02:53:25 +00:00
if (parentNodeContentId === targetNodeContentId) return;
2023-12-22 21:14:48 +00:00
2023-12-31 02:53:25 +00:00
const cy = cyRef?.current;
const parentNodeChildren = cy?.$(
'edge[source = "' + parentNodeContentId + '"]',
)?.length;
2023-12-31 02:53:25 +00:00
const parentQuestion = getQuestionByContentId(parentNodeContentId);
//Нельзя добавлять больше 1 ребёнка вопросам типа страница, ползунок, своё поле для ввода и дата
2023-12-31 02:53:25 +00:00
if (
(parentQuestion?.type === "date" ||
parentQuestion?.type === "text" ||
parentQuestion?.type === "number" ||
parentQuestion?.type === "page") &&
parentQuestion.content.rule.children.length === 1
) {
2023-12-31 02:53:25 +00:00
enqueueSnackbar("у вопроса этого типа может быть только 1 потомок");
return;
}
2023-12-22 21:14:48 +00:00
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
2023-12-31 02:53:25 +00:00
const targetQuestion = {
...getQuestionByContentId(targetNodeContentId || dragQuestionContentId),
} as AnyTypedQuizQuestion;
if (
Object.keys(targetQuestion).length !== 0 &&
parentNodeContentId &&
parentNodeChildren !== undefined
) {
clearDataAfterAddNode({
parentNodeContentId,
targetQuestion,
parentNodeChildren,
});
cy?.data("changed", true);
createResult(quiz.backendId, targetQuestion.content.id);
2023-12-22 21:14:48 +00:00
const es = cy?.add([
{
data: {
id: targetQuestion.content.id,
2023-12-31 02:53:25 +00:00
label:
targetQuestion.title === "" || targetQuestion.title === " "
? "noname №" + targetQuestion.page
2024-01-13 16:04:05 +00:00
: nameCutter(targetQuestion.title),
2023-12-31 02:53:25 +00:00
parentType: parentNodeContentId,
},
2023-12-22 21:14:48 +00:00
},
{
data: {
source: parentNodeContentId,
2023-12-31 02:53:25 +00:00
target: targetQuestion.content.id,
},
},
]);
cy?.layout(layoutOptions).run();
cy?.center(es);
2023-12-22 21:14:48 +00:00
} else {
enqueueSnackbar("Перетащите на плюсик вопрос");
2023-12-22 21:14:48 +00:00
}
} else {
2023-12-31 02:53:25 +00:00
enqueueSnackbar("Quiz не найден");
}
2023-12-31 02:53:25 +00:00
};
const clearDataAfterAddNode = ({
parentNodeContentId,
targetQuestion,
parentNodeChildren,
}: {
parentNodeContentId: string;
targetQuestion: AnyTypedQuizQuestion;
parentNodeChildren: number;
}) => {
const parentQuestion = {
...getQuestionByContentId(parentNodeContentId),
} as AnyTypedQuizQuestion;
//смотрим не добавлен ли родителю result. Если да - делаем его неактивным. Веточкам result не нужен
trashQuestions.forEach((targetQuestion) => {
2023-12-31 02:53:25 +00:00
if (
targetQuestion.type === "result" &&
targetQuestion.content.rule.parentId === parentQuestion.content.id
) {
updateQuestion(targetQuestion.id, (q) => (q.content.usage = false));
}
2023-12-31 02:53:25 +00:00
});
//предупреждаем добавленный вопрос о том, кто его родитель
2023-12-31 02:53:25 +00:00
updateQuestion(targetQuestion.content.id, (question) => {
question.content.rule.parentId = parentNodeContentId;
question.content.rule.main = [];
//Это листик. Сбросим ему на всякий случай не листиковые поля
2023-12-31 02:53:25 +00:00
question.content.rule.children = [];
question.content.rule.default = "";
});
2023-12-31 02:53:25 +00:00
const noChild = parentQuestion.content.rule.children.length === 0;
//предупреждаем родителя о новом потомке (если он ещё не знает о нём)
2023-12-31 02:53:25 +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;
});
if (!noChild) {
//детей больше 1
//- предупреждаем стор вопросов об открытии модалки ветвления
2023-12-31 02:53:25 +00:00
updateOpenedModalSettingsId(targetQuestion.content.id);
}
2023-12-31 02:53:25 +00:00
};
2023-11-29 15:45:15 +00:00
useEffect(() => {
if (startCreate) {
addNode({ parentNodeContentId: startCreate });
2023-12-20 10:46:38 +00:00
cleardragQuestionContentId();
2023-11-29 15:45:15 +00:00
setStartCreate("");
}
}, [startCreate]);
useEffect(() => {
if (startRemove) {
updateDeleteId(startRemove);
2023-11-29 15:45:15 +00:00
setStartRemove("");
}
}, [startRemove]);
//Отработка первичного рендера странички графика
2023-12-31 02:53:25 +00:00
const firstRender = useRef(true);
useEffect(() => {
if (!someWorkBackend && firstRender.current) {
document
.querySelector("#root")
?.addEventListener("mouseup", cleardragQuestionContentId);
const cy = cyRef.current;
const eles = cy?.add(
storeToNodes(
questions.filter(
2023-12-31 02:53:25 +00:00
(question) => question.type && question.type !== "result",
) as AnyTypedQuizQuestion[],
),
);
cy?.data("changed", true);
// cy.data('changed', true)
const elecs = eles?.layout(layoutOptions).run();
cy?.on("add", () => cy.data("changed", true));
2023-12-31 10:36:30 +00:00
fitGraphToRootNode();
//cy?.layout().run()
2023-12-31 02:53:25 +00:00
firstRender.current = false;
}
2023-11-29 15:45:15 +00:00
return () => {
2023-12-20 10:46:38 +00:00
document
.querySelector("#root")
?.removeEventListener("mouseup", cleardragQuestionContentId);
2023-12-01 08:12:59 +00:00
layoutsContainer.current?.remove();
2023-11-29 15:45:15 +00:00
plusesContainer.current?.remove();
crossesContainer.current?.remove();
gearsContainer.current?.remove();
};
}, [someWorkBackend]);
2023-11-29 15:45:15 +00:00
return (
<>
<Box
2023-12-31 02:53:25 +00:00
sx={{
mb: "20px",
display: "flex",
justifyContent: "space-between",
}}
>
<Button
sx={{
height: "27px",
color: "#7E2AEA",
textDecoration: "underline",
fontSize: "16px",
}}
variant="text"
2023-12-31 10:36:30 +00:00
onClick={fitGraphToRootNode}
>
Выровнять
</Button>
2023-12-31 02:53:25 +00:00
<ProblemIcon
blink={!canCreatePublic}
onClick={() => updateModalInfoWhyCantCreate(true)}
/>
</Box>
2023-12-22 21:14:48 +00:00
<CytoscapeComponent
wheelSensitivity={0.1}
elements={[]}
// elements={createGraphElements(tree, quiz)}
style={{ height: "480px", background: "#F2F3F7" }}
stylesheet={stylesheet}
2023-12-31 02:53:25 +00:00
layout={layoutOptions}
cy={(cy) => {
cyRef.current = cy;
}}
autoungrabify={true}
2023-12-31 10:36:30 +00:00
zoom={0.6}
zoomingEnabled={false}
/>
2023-12-21 14:56:29 +00:00
<DeleteNodeModal removeNode={removeNode} />
</>
2023-11-29 15:45:15 +00:00
);
2023-12-31 02:53:25 +00:00
}
function Clear() {
const quiz = useCurrentQuiz();
2023-12-21 14:56:29 +00:00
if (quiz) {
updateRootContentId(quiz?.id, "");
}
2023-12-31 02:53:25 +00:00
clearRuleForAll();
return <></>;
}
export default withErrorBoundary(CsComponent, {
fallback: <Clear />,
onError: (error, info) => {
2023-12-31 02:53:25 +00:00
enqueueSnackbar("Дерево порвалось");
console.log(info);
console.log(error);
},
});