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

310 lines
11 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,
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";
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,
setModalQuestionTargetContentId
}: CsComponentProps) {
2023-11-29 15:45:15 +00:00
const quiz = useCurrentQuiz();
const { dragQuestionContentId, desireToOpenABranchingModal, canCreatePublic, someWorkBackend } = useUiTools()
const trashQuestions = useQuestionsStore().questions
2023-12-25 15:38:40 +00:00
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,
});
useLayoutEffect(() => {
const cy = cyRef?.current
if (desireToOpenABranchingModal) {
setTimeout(() => {
cy?.getElementById(desireToOpenABranchingModal)?.data("eroticeyeblink", true)
}, 250)
} else {
cy?.elements().data("eroticeyeblink", false)
}
}, [desireToOpenABranchingModal])
//Техническая штучка. Гарантирует не отрисовку модалки по первому входу на страничку. И очистка данных по расскоменчиванию
//Быстро просто дешево и сердито :)
useLayoutEffect(() => {
updateOpenedModalSettingsId()
// updateRootContentId(quiz.id, "")
// clearRuleForAll()
}, [])
//Отлов mouseup для отрисовки ноды
useEffect(() => {
if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) {
addNode({ parentNodeContentId: modalQuestionParentContentId, targetNodeContentId: modalQuestionTargetContentId })
}
2023-12-04 15:40:15 +00:00
setModalQuestionParentContentId("")
setModalQuestionTargetContentId("")
}, [modalQuestionTargetContentId])
2023-12-01 08:12:59 +00:00
const addNode = ({ parentNodeContentId, targetNodeContentId }: { parentNodeContentId: string, targetNodeContentId?: string }) => {
2023-12-22 21:14:48 +00:00
if (quiz) {
//запрещаем работу родителя-ребенка если это один и тот же вопрос
if (parentNodeContentId === targetNodeContentId) return
const cy = cyRef?.current
const parentNodeChildren = cy?.$('edge[source = "' + parentNodeContentId + '"]')?.length
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
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)
const es = cy?.add([
{
data: {
id: targetQuestion.content.id,
label: targetQuestion.title === "" || targetQuestion.title === " " ? "noname" : targetQuestion.title
}
},
{
data: {
source: parentNodeContentId,
target: targetQuestion.content.id
}
2023-12-01 08:12:59 +00:00
}
2023-12-22 21:14:48 +00:00
])
cy?.layout(layoutOptions).run()
cy?.center(es)
} else {
enqueueSnackbar("Добавляемый вопрос не найден")
}
} else {
2023-12-22 21:14:48 +00:00
enqueueSnackbar("Квиз не найден")
}
}
2023-12-03 13:09:57 +00:00
const clearDataAfterAddNode = ({ parentNodeContentId, targetQuestion, parentNodeChildren }: { parentNodeContentId: string, targetQuestion: AnyTypedQuizQuestion, parentNodeChildren: number }) => {
const parentQuestion = { ...getQuestionByContentId(parentNodeContentId) } as AnyTypedQuizQuestion
//смотрим не добавлен ли родителю result. Если да - делаем его неактивным. Веточкам result не нужен
trashQuestions.forEach((targetQuestion) => {
if (targetQuestion.type === "result" && 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
console.log("детей ", noChild, " открываем модалку ветвления")
//- предупреждаем стор вопросов об открытии модалки ветвления
2023-12-04 13:33:43 +00:00
updateOpenedModalSettingsId(targetQuestion.content.id)
}
}
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]);
//Отработка первичного рендера странички графика
const firstRender = useRef(true)
useEffect(() => {
console.log("____________ПЕРВЧИНЫЙ РЕНДЕР____________")
console.log("______someWorkBackend______", someWorkBackend)
if (!someWorkBackend && firstRender.current) {
console.log("цс первично отрабатывает")
document
.querySelector("#root")
?.addEventListener("mouseup", cleardragQuestionContentId);
const cy = cyRef.current;
console.log("СПИСОК ЭЛЕМЕНТОВ ЦИТОСКЕЙПА В ПЕРВЧИНЫЙ РЕНДЕР")
console.log(cy?.elements())
const eles = cy?.add(
storeToNodes(
questions.filter(
(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));
cy?.fit();
//cy?.layout().run()
firstRender.current = false
}
2023-11-29 15:45:15 +00:00
return () => {
console.log("разрендер")
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-22 21:14:48 +00:00
mb="20px">
<Button
sx={{
height: "27px",
color: "#7E2AEA",
textDecoration: "underline",
fontSize: "16px",
}}
variant="text"
onClick={() => {
cyRef.current?.fit()
}}
>
Выровнять
</Button>
<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}
layout={(layoutOptions)}
cy={(cy) => {
cyRef.current = cy;
}}
autoungrabify={true}
/>
2023-12-21 14:56:29 +00:00
<DeleteNodeModal removeNode={removeNode} />
</>
2023-11-29 15:45:15 +00:00
);
};
function Clear() {
const quiz = useCurrentQuiz();
2023-12-21 14:56:29 +00:00
if (quiz) {
updateRootContentId(quiz?.id, "");
}
clearRuleForAll()
return <></>
}
export default withErrorBoundary(CsComponent, {
fallback: <Clear />,
onError: (error, info) => {
enqueueSnackbar("Дерево порвалось")
console.log(info)
console.log(error)
},
});