import { useEffect, useLayoutEffect, useRef, useState } from "react"; import Cytoscape from "cytoscape"; import CytoscapeComponent from "react-cytoscapejs"; import popper from "cytoscape-popper"; import { Button, Box } from "@mui/material"; import { withErrorBoundary } from "react-error-boundary"; import { enqueueSnackbar } from "notistack"; import { useCurrentQuiz } from "@root/quizes/hooks"; import { updateRootContentId } from "@root/quizes/actions"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import { useQuestionsStore } from "@root/questions/store"; import { useUiTools } from "@root/uiTools/store"; import { deleteQuestion, updateQuestion, getQuestionByContentId, clearRuleForAll, createResult, } from "@root/questions/actions"; import { updateModalInfoWhyCantCreate, updateOpenedModalSettingsId, } from "@root/uiTools/actions"; import { cleardragQuestionContentId } from "@root/uiTools/actions"; import { updateDeleteId } from "@root/uiTools/actions"; import { DeleteNodeModal } from "../DeleteNodeModal"; import { ProblemIcon } from "@ui_kit/ProblemIcon"; import { useRemoveNode } from "./hooks/useRemoveNode"; import { usePopper } from "./hooks/usePopper"; import { storeToNodes } from "./helper"; import { stylesheet } from "./style/stylesheet"; import "./style/styles.css"; import type { Core } from "cytoscape"; 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) { const quiz = useCurrentQuiz(); const { dragQuestionContentId, desireToOpenABranchingModal, canCreatePublic, someWorkBackend, } = useUiTools(); const trashQuestions = useQuestionsStore().questions; const questions = trashQuestions.filter( (question) => question.type !== "result" && question.type !== null && !question.deleted, ); const [startCreate, setStartCreate] = useState(""); const [startRemove, setStartRemove] = useState(""); const cyRef = useRef(null); const layoutsContainer = useRef(null); const plusesContainer = useRef(null); const crossesContainer = useRef(null); const gearsContainer = useRef(null); const { layoutOptions } = usePopper({ layoutsContainer, plusesContainer, crossesContainer, gearsContainer, setModalQuestionParentContentId, setOpenedModalQuestions, setStartCreate, setStartRemove, }); const { removeNode } = useRemoveNode({ cyRef, layoutOptions, layoutsContainer, plusesContainer, crossesContainer, gearsContainer, }); 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(() => { 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, }); } setModalQuestionParentContentId(""); setModalQuestionTargetContentId(""); }, [modalQuestionTargetContentId]); const addNode = ({ parentNodeContentId, targetNodeContentId, }: { parentNodeContentId: string; targetNodeContentId?: string; }) => { if (quiz) { //запрещаем работу родителя-ребенка если это один и тот же вопрос if (parentNodeContentId === targetNodeContentId) return; const cy = cyRef?.current; const parentNodeChildren = cy?.$( 'edge[source = "' + parentNodeContentId + '"]', )?.length; const parentQuestion = getQuestionByContentId(parentNodeContentId); //Нельзя добавлять больше 1 ребёнка вопросам типа страница, ползунок, своё поле для ввода и дата if ( (parentQuestion?.type === "date" || parentQuestion?.type === "text" || parentQuestion?.type === "number" || parentQuestion?.type === "page") && parentQuestion.content.rule.children.length === 1 ) { enqueueSnackbar("у вопроса этого типа может быть только 1 потомок"); return; } //если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа 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.page : targetQuestion.title, parentType: parentNodeContentId, }, }, { data: { source: parentNodeContentId, target: targetQuestion.content.id, }, }, ]); cy?.layout(layoutOptions).run(); cy?.center(es); } else { enqueueSnackbar("Перетащите на плюсик вопрос"); } } else { enqueueSnackbar("Quiz не найден"); } }; 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 //- предупреждаем стор вопросов об открытии модалки ветвления updateOpenedModalSettingsId(targetQuestion.content.id); } }; useEffect(() => { if (startCreate) { addNode({ parentNodeContentId: startCreate }); cleardragQuestionContentId(); setStartCreate(""); } }, [startCreate]); useEffect(() => { if (startRemove) { updateDeleteId(startRemove); setStartRemove(""); } }, [startRemove]); //Отработка первичного рендера странички графика 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( (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)); fitGraphToRootNode(); //cy?.layout().run() firstRender.current = false; } return () => { document .querySelector("#root") ?.removeEventListener("mouseup", cleardragQuestionContentId); layoutsContainer.current?.remove(); plusesContainer.current?.remove(); crossesContainer.current?.remove(); gearsContainer.current?.remove(); }; }, [someWorkBackend]); return ( <> updateModalInfoWhyCantCreate(true)} /> { cyRef.current = cy; }} autoungrabify={true} zoom={0.6} zoomingEnabled={false} /> ); } function Clear() { const quiz = useCurrentQuiz(); if (quiz) { updateRootContentId(quiz?.id, ""); } clearRuleForAll(); return <>; } export default withErrorBoundary(CsComponent, { fallback: , onError: (error, info) => { enqueueSnackbar("Дерево порвалось"); console.log(info); console.log(error); }, });