diff --git a/src/pages/Questions/BranchingMap/CsComponent.tsx b/src/pages/Questions/BranchingMap/CsComponent.tsx index f86c3ace..faa660d5 100644 --- a/src/pages/Questions/BranchingMap/CsComponent.tsx +++ b/src/pages/Questions/BranchingMap/CsComponent.tsx @@ -14,7 +14,7 @@ import type { Core, PresetLayoutOptions, SingularData } from "cytoscape"; import Cytoscape from "cytoscape"; import popper, { getPopperInstance } from "cytoscape-popper"; import { enqueueSnackbar } from "notistack"; -import { useEffect, useLayoutEffect, useRef, useState } from "react"; +import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react"; import CytoscapeComponent from "react-cytoscapejs"; import { withErrorBoundary } from "react-error-boundary"; import { DeleteNodeModal } from "../DeleteNodeModal"; @@ -24,9 +24,9 @@ import { useRemoveNode } from "./hooks/useRemoveNode"; import "./style/styles.css"; import { stylesheet } from "./style/stylesheet"; -Cytoscape.use(popper); +Cytoscape.use(popper); -type PopperInstance = ReturnType>; +type PopperInstance = ReturnType>; function CsComponent() { const quiz = useCurrentQuiz(); @@ -35,30 +35,32 @@ function CsComponent() { const modalQuestionParentContentId = useUiTools(state => state.modalQuestionParentContentId); const modalQuestionTargetContentId = useUiTools(state => state.modalQuestionTargetContentId); const trashQuestions = useQuestionsStore(state => state.questions); - const questions = trashQuestions.filter((question) => question.type !== "result" && question.type !== null); const [isPanningCy, setIsPanningCy] = useState(false); const cyRef = useRef(null); const popperContainerRef = useRef(null); const popperInstancesRef = useRef([]); - const { createPoppers, removeAllPoppers, removePoppersById } = usePopper({ + const questions = useMemo(() => trashQuestions.filter( + (question) => question.type !== "result" && question.type !== null + ), [trashQuestions]); + + const cyElements = useMemo(() => { + const q = questions.filter( + (question): question is AnyTypedQuizQuestion => question.type !== null && question.type !== "result" + ); + + return storeToNodes(q); + }, [questions]); + + const { createPoppers, removeAllPoppers } = usePopper({ cyRef, - quizId: quiz?.backendId, - runCyLayout, popperContainerRef, popperInstancesRef, }); - function runCyLayout() { - cyRef.current?.layout(layoutOptions).run(); - createPoppers(); - }; - const { removeNode } = useRemoveNode({ cyRef, - runCyLayout, - removeButtons: removePoppersById, }); useLayoutEffect(() => { @@ -74,34 +76,20 @@ function CsComponent() { useEffect(() => { if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) { - if (!cyRef.current || !quiz) return; + if (!cyRef.current) return; - const es = addNode({ - cy: cyRef.current, - quizId: quiz.backendId, + addNode({ parentNodeContentId: modalQuestionParentContentId, targetNodeContentId: modalQuestionTargetContentId, }); - runCyLayout(); - if (es) cyRef.current.fit(es, 100); } setModalQuestionParentContentId(""); setModalQuestionTargetContentId(""); - }, [modalQuestionTargetContentId, quiz?.backendId]); + }, [modalQuestionTargetContentId]); useEffect(function onMount() { updateOpenedModalSettingsId(); document.querySelector("#root")?.addEventListener("mouseup", cleardragQuestionContentId); - const cy = cyRef.current; - if (!cy) return; - - cy.add( - storeToNodes( - questions.filter((question) => question.type && question.type !== "result") as AnyTypedQuizQuestion[], - ), - ); - runCyLayout(); - cy.fit(); return () => { document.querySelector("#root")?.removeEventListener("mouseup", cleardragQuestionContentId); @@ -109,7 +97,7 @@ function CsComponent() { }; }, []); - useEffect(function attachDragHandlers() { + useEffect(function removePoppersOnDrag() { const cy = cyRef.current; if (!cy) return; @@ -145,7 +133,13 @@ function CsComponent() { } else { createPoppers(); } - }, [isPanningCy]); + }, [isPanningCy, createPoppers]); + + useEffect(() => { + cyRef.current?.layout(layoutOptions).run(); + cyRef.current?.fit(undefined, 70); + createPoppers(); + }, [cyElements, createPoppers]); return ( <> @@ -172,8 +166,7 @@ function CsComponent() { { if (dragQuestionContentId) { updateRootContentId(quiz?.id, dragQuestionContentId); updateQuestion(dragQuestionContentId, (question) => question.content.rule.parentId = "root"); - createResult(quiz?.backendId, dragQuestionContentId); + createResult(dragQuestionContentId); } } else { enqueueSnackbar("Нет информации о взятом опроснике"); @@ -53,7 +53,7 @@ export const FirstNodeField = () => { if (modalQuestionTargetContentId) { updateRootContentId(quiz?.id, modalQuestionTargetContentId); updateQuestion(modalQuestionTargetContentId, (question) => question.content.rule.parentId = "root"); - createResult(quiz?.backendId, modalQuestionTargetContentId); + createResult(modalQuestionTargetContentId); } } else { enqueueSnackbar("Нет информации о взятом опроснике"); diff --git a/src/pages/Questions/BranchingMap/helper.ts b/src/pages/Questions/BranchingMap/helper.ts index 4dc5c2d6..b8f8874d 100644 --- a/src/pages/Questions/BranchingMap/helper.ts +++ b/src/pages/Questions/BranchingMap/helper.ts @@ -99,12 +99,10 @@ export function clearDataAfterAddNode({ }; export function clearDataAfterRemoveNode({ - quiz, trashQuestions, targetQuestionContentId, parentQuestionContentId, }: { - quiz: Quiz | undefined; trashQuestions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[], targetQuestionContentId: string; parentQuestionContentId: string; @@ -128,7 +126,7 @@ export function clearDataAfterRemoveNode({ q.content.usage = true; }); } else { - createResult(quiz?.backendId, parentQuestionContentId); + createResult(parentQuestionContentId); } //чистим rule родителя @@ -242,47 +240,23 @@ export function calcNodePosition(node: any) { } export const addNode = ({ - cy, - quizId, parentNodeContentId, targetNodeContentId, }: { - cy: Core; - quizId: number; parentNodeContentId: string; targetNodeContentId?: string; }) => { //запрещаем работу родителя-ребенка если это один и тот же вопрос if (parentNodeContentId === targetNodeContentId) return; - devlog("@addNode"); - const parentNodeChildren = cy.$('edge[source = "' + parentNodeContentId + '"]')?.length; //если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа const targetQuestion = { ...getQuestionByContentId(targetNodeContentId || useUiTools.getState().dragQuestionContentId), } as AnyTypedQuizQuestion; - if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) { + if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId) { clearDataAfterAddNode({ parentNodeContentId, targetQuestion }); - createResult(quizId, 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, - }, - }, - ]); - return es; + createResult(targetQuestion.content.id); } else { enqueueSnackbar("Добавляемый вопрос не найден"); } diff --git a/src/pages/Questions/BranchingMap/hooks/usePopper.ts b/src/pages/Questions/BranchingMap/hooks/usePopper.ts index a506cbcd..80715108 100644 --- a/src/pages/Questions/BranchingMap/hooks/usePopper.ts +++ b/src/pages/Questions/BranchingMap/hooks/usePopper.ts @@ -1,7 +1,7 @@ import { cleardragQuestionContentId, setModalQuestionParentContentId, setOpenedModalQuestions, updateDeleteId, updateOpenedModalSettingsId } from "@root/uiTools/actions"; import type { AbstractEventObject, Core, NodeSingular, SingularData } from "cytoscape"; import { getPopperInstance } from "cytoscape-popper"; -import { type MutableRefObject } from "react"; +import { useCallback, type MutableRefObject } from "react"; import { addNode } from "../helper"; type PopperItem = { @@ -29,31 +29,23 @@ type NodeSingularWithPopper = NodeSingular & { export const usePopper = ({ cyRef, - quizId, popperContainerRef, popperInstancesRef, - runCyLayout, }: { cyRef: MutableRefObject; - quizId: number | undefined, popperContainerRef: MutableRefObject; popperInstancesRef: MutableRefObject; - runCyLayout: () => void; }) => { - const removePoppersById = (id: string) => { - popperContainerRef.current?.querySelector(`.popper-layout[data-id='${id}']`)?.remove(); - }; - - const removeAllPoppers = () => { + const removeAllPoppers = useCallback(() => { cyRef.current?.removeListener("zoom render"); popperInstancesRef.current.forEach(p => p.destroy()); popperInstancesRef.current = []; popperContainerRef.current?.remove(); popperContainerRef.current = null; - }; + }, []); - const createPoppers = () => { + const createPoppers = useCallback(() => { removeAllPoppers(); const cy = cyRef.current; @@ -121,15 +113,7 @@ export const usePopper = ({ plusElement.setAttribute("data-id", item.id()); plusElement.style.zIndex = "1"; plusElement.addEventListener("mouseup", () => { - if (!cy || !quizId) return; - - const es = addNode({ - cy, - quizId, - parentNodeContentId: node.id(), - }); - runCyLayout(); - if (es) cy.fit(es, 100); + addNode({ parentNodeContentId: node.id() }); cleardragQuestionContentId(); }); @@ -257,8 +241,7 @@ export const usePopper = ({ cy.on("zoom render", onZoom); }); - }; + }, []); - - return { removeAllPoppers, removePoppersById, createPoppers }; + return { removeAllPoppers, createPoppers }; }; diff --git a/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts b/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts index 4288a922..2c31a007 100644 --- a/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts +++ b/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts @@ -10,24 +10,19 @@ import { clearDataAfterRemoveNode } from "../helper"; type UseRemoveNodeArgs = { cyRef: MutableRefObject; - runCyLayout: () => void; - removeButtons: (id: string) => void; }; export const useRemoveNode = ({ cyRef, - runCyLayout, - removeButtons, }: UseRemoveNodeArgs) => { const { questions: trashQuestions } = useQuestionsStore(); const quiz = useCurrentQuiz(); const removeNode = (targetNodeContentId: string) => { const deleteNodes: string[] = []; - const deleteEdges: SingularElementArgument[] = []; const cy = cyRef?.current; - const findChildrenToDelete = (node: CollectionReturnValue) => { + const deleteNodesRecursively = (node: CollectionReturnValue) => { //Узнаём грани, идущие от этой ноды cy ?.$('edge[source = "' + node.id() + '"]') @@ -35,20 +30,18 @@ export const useRemoveNode = ({ .forEach((edge) => { const edgeData = edge.data(); - //записываем id грани для дальнейшего удаления - deleteEdges.push(edge); //ищем ноду на конце грани, записываем её ID для дальнейшего удаления const targetNode = cy?.$("#" + edgeData.target); deleteNodes.push(targetNode.data().id); //вызываем функцию для анализа потомков уже у этой ноды - findChildrenToDelete(targetNode); + deleteNodesRecursively(targetNode); }); }; const elementToDelete = cy?.getElementById(targetNodeContentId); if (elementToDelete) { - findChildrenToDelete(elementToDelete); + deleteNodesRecursively(elementToDelete); } const targetQuestion = getQuestionByContentId(targetNodeContentId); @@ -73,12 +66,10 @@ export const useRemoveNode = ({ //createFrontResult(quiz.backendId, parentQuestionContentId); } clearDataAfterRemoveNode({ - quiz, trashQuestions, targetQuestionContentId: targetNodeContentId, parentQuestionContentId, }); - cy?.remove(cy?.$("#" + targetNodeContentId)); } } @@ -86,23 +77,13 @@ export const useRemoveNode = ({ deleteNodes.forEach((nodeId) => { //Ноды - cy?.remove(cy?.$("#" + nodeId)); - removeButtons(nodeId); updateQuestion(nodeId, (question) => { question.content.rule.parentId = ""; question.content.rule.main = []; question.content.rule.default = ""; question.content.rule.children = []; }); - }); - - deleteEdges.forEach((edge: any) => { - //Грани - cy?.remove(edge); - }); - - removeButtons(targetNodeContentId); - runCyLayout(); + }); //делаем result всех потомков неактивными trashQuestions.forEach((qr) => { diff --git a/src/stores/questions/actions.ts b/src/stores/questions/actions.ts index a90e2b54..773b270b 100644 --- a/src/stores/questions/actions.ts +++ b/src/stores/questions/actions.ts @@ -16,6 +16,8 @@ import { QuestionsStore, useQuestionsStore } from "./store"; import { useUiTools } from "../uiTools/store"; import { withErrorBoundary } from "react-error-boundary"; import { QuizQuestionResult } from "@model/questionTypes/result"; +import { useQuizPreviewStore } from "@root/quizPreview"; +import { useQuizStore } from "@root/quizes/store"; export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => { @@ -498,9 +500,9 @@ export const clearRuleForAll = () => { }; export const createResult = async ( - quizId: number | undefined, parentContentId?: string ) => requestQueue.enqueue(async () => { + const quizId = useQuizStore.getState().editQuizId; if (!quizId || !parentContentId) { console.error("Нет данных для создания результата. quizId: ", quizId, ", quizId: ", parentContentId) }