diff --git a/src/pages/Questions/BranchingMap/CsComponent.tsx b/src/pages/Questions/BranchingMap/CsComponent.tsx index f5ab0a57..f86c3ace 100644 --- a/src/pages/Questions/BranchingMap/CsComponent.tsx +++ b/src/pages/Questions/BranchingMap/CsComponent.tsx @@ -1,301 +1,232 @@ -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 { devlog } from "@frontend/kitui"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; -import { useQuestionsStore } from "@root/questions/store"; -import { useUiTools } from "@root/uiTools/store"; +import { Box, Button } from "@mui/material"; import { - deleteQuestion, - updateQuestion, - getQuestionByContentId, - clearRuleForAll, - createResult, + clearRuleForAll } 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 { useQuestionsStore } from "@root/questions/store"; +import { updateRootContentId } from "@root/quizes/actions"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import { cleardragQuestionContentId, setModalQuestionParentContentId, setModalQuestionTargetContentId, updateModalInfoWhyCantCreate, updateOpenedModalSettingsId } from "@root/uiTools/actions"; +import { useUiTools } from "@root/uiTools/store"; import { ProblemIcon } from "@ui_kit/ProblemIcon"; - -import { useRemoveNode } from "./hooks/useRemoveNode"; +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 CytoscapeComponent from "react-cytoscapejs"; +import { withErrorBoundary } from "react-error-boundary"; +import { DeleteNodeModal } from "../DeleteNodeModal"; +import { addNode, calcNodePosition, storeToNodes } from "./helper"; import { usePopper } from "./hooks/usePopper"; - -import { storeToNodes } from "./helper"; -import { stylesheet } from "./style/stylesheet"; +import { useRemoveNode } from "./hooks/useRemoveNode"; import "./style/styles.css"; +import { stylesheet } from "./style/stylesheet"; -import type { Core } from "cytoscape"; +Cytoscape.use(popper); -Cytoscape.use(popper); +type PopperInstance = ReturnType>; -interface CsComponentProps { - modalQuestionParentContentId: string; - modalQuestionTargetContentId: string; - setOpenedModalQuestions: (open: boolean) => void; - setModalQuestionParentContentId: (id: string) => void; - setModalQuestionTargetContentId: (id: string) => void; +function CsComponent() { + const quiz = useCurrentQuiz(); + const desireToOpenABranchingModal = useUiTools(state => state.desireToOpenABranchingModal); + const canCreatePublic = useUiTools(state => state.canCreatePublic); + 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({ + cyRef, + quizId: quiz?.backendId, + runCyLayout, + popperContainerRef, + popperInstancesRef, + }); + + function runCyLayout() { + cyRef.current?.layout(layoutOptions).run(); + createPoppers(); + }; + + const { removeNode } = useRemoveNode({ + cyRef, + runCyLayout, + removeButtons: removePoppersById, + }); + + useLayoutEffect(() => { + const cy = cyRef?.current; + if (desireToOpenABranchingModal) { + setTimeout(() => { + cy?.getElementById(desireToOpenABranchingModal)?.data("eroticeyeblink", true); + }, 250); + } else { + cy?.elements().data("eroticeyeblink", false); + } + }, [desireToOpenABranchingModal]); + + useEffect(() => { + if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) { + if (!cyRef.current || !quiz) return; + + const es = addNode({ + cy: cyRef.current, + quizId: quiz.backendId, + parentNodeContentId: modalQuestionParentContentId, + targetNodeContentId: modalQuestionTargetContentId, + }); + runCyLayout(); + if (es) cyRef.current.fit(es, 100); + } + setModalQuestionParentContentId(""); + setModalQuestionTargetContentId(""); + }, [modalQuestionTargetContentId, quiz?.backendId]); + + 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); + removeAllPoppers(); + }; + }, []); + + useEffect(function attachDragHandlers() { + const cy = cyRef.current; + if (!cy) return; + + let isPointerDown = false; + + const onPointerDown = () => { + isPointerDown = true; + cy.data("dragging", true); + }; + const onPointerUp = () => { + isPointerDown = false; + cy.data("dragging", false); + setIsPanningCy(false); + }; + const handleMove = () => { + setIsPanningCy(isPointerDown); + }; + + cy.on("vmousedown", onPointerDown); + cy.on("vmousemove", handleMove); + document.addEventListener("pointerup", onPointerUp); + + return () => { + cy.off("vmousedown", onPointerDown); + cy.off("vmousemove", handleMove); + document.removeEventListener("pointerup", onPointerUp); + }; + }, []); + + useEffect(function poppersLifecycle() { + if (isPanningCy) { + removeAllPoppers(); + } else { + createPoppers(); + } + }, [isPanningCy]); + + return ( + <> + + + updateModalInfoWhyCantCreate(true)} + /> + + + { + cyRef.current = cy; + }} + autoungrabify={true} + /> + + + ); } -function CsComponent({ - modalQuestionParentContentId, - modalQuestionTargetContentId, - setOpenedModalQuestions, - setModalQuestionParentContentId, - setModalQuestionTargetContentId -}: CsComponentProps) { - const quiz = useCurrentQuiz(); - - const { dragQuestionContentId, desireToOpenABranchingModal, canCreatePublic } = useUiTools() - const trashQuestions = useQuestionsStore().questions - const questions = trashQuestions.filter((question) => question.type !== "result" && question.type !== null) - 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, - }); - - - useEffect(() => { - return () => { - // if (!canCreatePublic) updateModalInfoWhyCantCreate(true) - } - }, []); - - 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() - }, []) - 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 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 - } - } - ]) - cy?.layout(layoutOptions).run() - cy?.center(es) - } else { - enqueueSnackbar("Добавляемый вопрос не найден") - } - - } else { - enqueueSnackbar("Квиз не найден") - } - } - - 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]); - - useEffect(() => { - 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)); - cy?.fit(); - //cy?.layout().run() - - return () => { - document - .querySelector("#root") - ?.removeEventListener("mouseup", cleardragQuestionContentId); - layoutsContainer.current?.remove(); - plusesContainer.current?.remove(); - crossesContainer.current?.remove(); - gearsContainer.current?.remove(); - }; - }, []); - - - return ( - <> - - - updateModalInfoWhyCantCreate(true)} /> - - - { - cyRef.current = cy; - }} - autoungrabify={true} - /> - - - ); -}; - function Clear() { - const quiz = useCurrentQuiz(); - if (quiz) { - updateRootContentId(quiz?.id, ""); - } - clearRuleForAll() - return <> + 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) - }, + fallback: , + onError: (error, info) => { + enqueueSnackbar("Дерево порвалось"); + devlog(info); + devlog(error); + }, }); + +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, +}; diff --git a/src/pages/Questions/BranchingMap/FirstNodeField.tsx b/src/pages/Questions/BranchingMap/FirstNodeField.tsx index ba14fc18..45e63b6f 100644 --- a/src/pages/Questions/BranchingMap/FirstNodeField.tsx +++ b/src/pages/Questions/BranchingMap/FirstNodeField.tsx @@ -1,69 +1,65 @@ -import { Box } from "@mui/material" -import { useEffect, useRef, useLayoutEffect } from "react"; -import { deleteQuestion, clearRuleForAll, updateQuestion, createResult } from "@root/questions/actions" -import { updateOpenedModalSettingsId } from "@root/uiTools/actions" -import { updateRootContentId } from "@root/quizes/actions" -import { useCurrentQuiz } from "@root/quizes/hooks" -import { useQuestionsStore } from "@root/questions/store" -import { enqueueSnackbar } from "notistack"; +import { Box } from "@mui/material"; +import { clearRuleForAll, createResult, updateQuestion } from "@root/questions/actions"; +import { updateRootContentId } from "@root/quizes/actions"; +import { useCurrentQuiz } from "@root/quizes/hooks"; +import { setOpenedModalQuestions, updateOpenedModalSettingsId } from "@root/uiTools/actions"; import { useUiTools } from "@root/uiTools/store"; +import { enqueueSnackbar } from "notistack"; +import { useEffect, useLayoutEffect, useRef } from "react"; -interface Props { - setOpenedModalQuestions: (open: boolean) => void; - modalQuestionTargetContentId: string; -} -export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetContentId }: Props) => { +export const FirstNodeField = () => { const quiz = useCurrentQuiz(); - + const modalQuestionTargetContentId = useUiTools(state => state.modalQuestionTargetContentId); useLayoutEffect(() => { - updateOpenedModalSettingsId() - updateRootContentId(quiz.id, "") - clearRuleForAll() - }, []) + if (!quiz) return; + + updateOpenedModalSettingsId(); + updateRootContentId(quiz.id, ""); + clearRuleForAll(); + }, []); - const { questions } = useQuestionsStore() - const { dragQuestionContentId } = useUiTools() + const { dragQuestionContentId } = useUiTools(); const Container = useRef(null); - const modalOpen = () => setOpenedModalQuestions(true) + const modalOpen = () => setOpenedModalQuestions(true); const newRootNode = () => { if (quiz) { if (dragQuestionContentId) { - updateRootContentId(quiz?.id, dragQuestionContentId) - updateQuestion(dragQuestionContentId, (question) => question.content.rule.parentId = "root") - createResult(quiz?.backendId, dragQuestionContentId) + updateRootContentId(quiz?.id, dragQuestionContentId); + updateQuestion(dragQuestionContentId, (question) => question.content.rule.parentId = "root"); + createResult(quiz?.backendId, dragQuestionContentId); } } else { - enqueueSnackbar("Нет информации о взятом опроснике") + enqueueSnackbar("Нет информации о взятом опроснике"); } - } + }; useEffect(() => { - Container.current?.addEventListener("mouseup", newRootNode) - Container.current?.addEventListener("click", modalOpen) - return () => { - Container.current?.removeEventListener("mouseup", newRootNode) - Container.current?.removeEventListener("click", modalOpen) - } - }, [dragQuestionContentId]) + Container.current?.addEventListener("mouseup", newRootNode); + Container.current?.addEventListener("click", modalOpen); + return () => { + Container.current?.removeEventListener("mouseup", newRootNode); + Container.current?.removeEventListener("click", modalOpen); + }; + }, [dragQuestionContentId]); useEffect(() => { if (quiz) { if (modalQuestionTargetContentId) { - updateRootContentId(quiz?.id, modalQuestionTargetContentId) - updateQuestion(modalQuestionTargetContentId, (question) => question.content.rule.parentId = "root") - createResult(quiz?.backendId, modalQuestionTargetContentId) + updateRootContentId(quiz?.id, modalQuestionTargetContentId); + updateQuestion(modalQuestionTargetContentId, (question) => question.content.rule.parentId = "root"); + createResult(quiz?.backendId, modalQuestionTargetContentId); } } else { - enqueueSnackbar("Нет информации о взятом опроснике") + enqueueSnackbar("Нет информации о взятом опроснике"); } - }, [modalQuestionTargetContentId]) + }, [modalQuestionTargetContentId]); return ( @@ -82,5 +78,5 @@ export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetCon > + - ) -} + ); +}; diff --git a/src/pages/Questions/BranchingMap/helper.ts b/src/pages/Questions/BranchingMap/helper.ts index 0423d219..4dc5c2d6 100644 --- a/src/pages/Questions/BranchingMap/helper.ts +++ b/src/pages/Questions/BranchingMap/helper.ts @@ -1,28 +1,39 @@ -import { AnyTypedQuizQuestion } from "@model/questionTypes/shared" +import { devlog } from "@frontend/kitui"; +import { QuizQuestionResult } from "@model/questionTypes/result"; +import { AnyTypedQuizQuestion, QuestionBranchingRule, QuestionBranchingRuleMain, UntypedQuizQuestion } from "@model/questionTypes/shared"; +import { Quiz } from "@model/quiz/quiz"; +import { createResult, getQuestionByContentId, updateQuestion } from "@root/questions/actions"; +import { useQuestionsStore } from "@root/questions/store"; +import { updateOpenedModalSettingsId } from "@root/uiTools/actions"; +import { useUiTools } from "@root/uiTools/store"; +import { Core } from "cytoscape"; +import { enqueueSnackbar } from "notistack"; interface Nodes { data: { id: string; label: string; parent?: string; - } + }; } interface Edges { data: { source: string; target: string; - } + }; } export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => { - const nodes: Nodes[] = [] - const edges: Edges[] = [] + const nodes: Nodes[] = []; + const edges: Edges[] = []; questions.forEach((question) => { if (question.content.rule.parentId) { - nodes.push({data: { - id: question.content.id, - label: question.title === "" || question.title === " " ? "noname" : question.title - }}) + nodes.push({ + data: { + id: question.content.id, + label: question.title === "" || question.title === " " ? "noname" : question.title + } + }); // nodes.push({ // data: { // id: "delete" + question.content.id, @@ -30,11 +41,249 @@ export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => { // parent: question.content.id, // } // },) - if (question.content.rule.parentId !== "root") edges.push({data: { - source: question.content.rule.parentId, - target: question.content.id - }}) + if (question.content.rule.parentId !== "root") edges.push({ + data: { + source: question.content.rule.parentId, + target: question.content.id + } + }); } - }) + }); return [...nodes, ...edges]; +}; + +export function clearDataAfterAddNode({ + parentNodeContentId, + targetQuestion, +}: { + parentNodeContentId: string; + targetQuestion: AnyTypedQuizQuestion; +}) { + const parentQuestion = { ...getQuestionByContentId(parentNodeContentId) } as AnyTypedQuizQuestion; + + //смотрим не добавлен ли родителю result. Если да - делаем его неактивным. Веточкам result не нужен + useQuestionsStore.getState().questions.filter( + (question): question is QuizQuestionResult => question.type === "result" + ).forEach((targetQuestion) => { + if ( + 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); + } +}; + +export function clearDataAfterRemoveNode({ + quiz, + trashQuestions, + targetQuestionContentId, + parentQuestionContentId, +}: { + quiz: Quiz | undefined; + trashQuestions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[], + targetQuestionContentId: string; + parentQuestionContentId: string; +}) { + updateQuestion(targetQuestionContentId, (question) => { + question.content.rule.parentId = ""; + question.content.rule.children = []; + question.content.rule.main = []; + question.content.rule.default = ""; + }); + + //Ищём родителя + const parentQuestion = getQuestionByContentId(parentQuestionContentId); + + //Делаем результат родителя активным + const parentResult = trashQuestions.find( + (q): q is QuizQuestionResult => q.type === "result" && q.content.rule.parentId === parentQuestionContentId, + ); + if (parentResult) { + updateQuestion(parentResult.content.id, (q) => { + q.content.usage = true; + }); + } else { + createResult(quiz?.backendId, parentQuestionContentId); + } + + //чистим rule родителя + if (!parentQuestion?.type) { + return; + } + + const newChildren = [...parentQuestion.content.rule.children]; + newChildren.splice(parentQuestion.content.rule.children.indexOf(targetQuestionContentId), 1); + + 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, + }; + + updateQuestion(parentQuestionContentId, (PQ) => { + PQ.content.rule = newRule; + }); +}; + +export function calcNodePosition(node: any) { + const id = node.id(); + const incomming = node.cy().edges(`[target="${id}"]`); + const layer = 0; + node.removeData("lastChild"); + + 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(), + }); + }); + continue; + } + + task?.parent.data( + "subtreeWidth", + task.children.reduce((p: any, n: any) => p + n.data("subtreeWidth"), 0), + ); + } + + const pos = { x: 0, y: 0 }; + node.data("oldPos", pos); + + 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"); + + 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, + }); + }); + } + return pos; + } else { + const opos = node.data("oldPos"); + if (opos) { + return opos; + } + } } + +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) { + 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; + } else { + enqueueSnackbar("Добавляемый вопрос не найден"); + } +}; diff --git a/src/pages/Questions/BranchingMap/hooks/usePopper.ts b/src/pages/Questions/BranchingMap/hooks/usePopper.ts index 615532a1..a506cbcd 100644 --- a/src/pages/Questions/BranchingMap/hooks/usePopper.ts +++ b/src/pages/Questions/BranchingMap/hooks/usePopper.ts @@ -1,479 +1,264 @@ -import { updateOpenedModalSettingsId } from "@root/uiTools/actions"; - -import type { MutableRefObject } from "react"; -import type { - PresetLayoutOptions, - LayoutEventObject, - NodeSingular, - AbstractEventObject, -} from "cytoscape"; - -type usePopperArgs = { - layoutsContainer: MutableRefObject; - plusesContainer: MutableRefObject; - crossesContainer: MutableRefObject; - gearsContainer: MutableRefObject; - setModalQuestionParentContentId: (id: string) => void; - setOpenedModalQuestions: (open: boolean) => void; - setStartCreate: (id: string) => void; - setStartRemove: (id: string) => void; -}; +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 { addNode } from "../helper"; type PopperItem = { - id: () => string; + id: () => string; }; type Modifier = { - name: string; - options: unknown; + name: string; + options: unknown; }; type PopperConfig = { - popper: { - placement: string; - modifiers?: Modifier[]; - }; - content: (items: PopperItem[]) => void; + popper: { + placement: string; + modifiers?: Modifier[]; + }; + content: (items: PopperItem[]) => void; }; -type Popper = { - update: () => Promise; - setOptions: (modifiers: { modifiers?: Modifier[] }) => void; -}; +type PopperInstance = ReturnType>; type NodeSingularWithPopper = NodeSingular & { - popper: (config: PopperConfig) => Popper; + popper: (config: PopperConfig) => PopperInstance; }; export const usePopper = ({ - layoutsContainer, - plusesContainer, - crossesContainer, - gearsContainer, - setModalQuestionParentContentId, - setOpenedModalQuestions, - setStartCreate, - setStartRemove, -}: usePopperArgs) => { - const removeButtons = (id: string) => { - layoutsContainer.current - ?.querySelector(`.popper-layout[data-id='${id}']`) - ?.remove(); - plusesContainer.current - ?.querySelector(`.popper-plus[data-id='${id}']`) - ?.remove(); - crossesContainer.current - ?.querySelector(`.popper-cross[data-id='${id}']`) - ?.remove(); - gearsContainer.current - ?.querySelector(`.popper-gear[data-id='${id}']`) - ?.remove(); - }; + 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 initialPopperIcons = ({ cy }: LayoutEventObject) => { - const container = - (document.body.querySelector( - ".__________cytoscape_container" - ) as HTMLDivElement) || null; + const removeAllPoppers = () => { + cyRef.current?.removeListener("zoom render"); - if (!container) { - return; - } + popperInstancesRef.current.forEach(p => p.destroy()); + popperInstancesRef.current = []; + popperContainerRef.current?.remove(); + popperContainerRef.current = null; + }; - container.style.overflow = "hidden"; + const createPoppers = () => { + removeAllPoppers(); - if (!plusesContainer.current) { - plusesContainer.current = document.createElement("div"); - plusesContainer.current.setAttribute("id", "popper-pluses"); - container.append(plusesContainer.current); - } - if (!crossesContainer.current) { - crossesContainer.current = document.createElement("div"); - crossesContainer.current.setAttribute("id", "popper-crosses"); - container.append(crossesContainer.current); - } - if (!gearsContainer.current) { - gearsContainer.current = document.createElement("div"); - gearsContainer.current.setAttribute("id", "popper-gears"); - container.append(gearsContainer.current); - } - if (!layoutsContainer.current) { - layoutsContainer.current = document.createElement("div"); - layoutsContainer.current.setAttribute("id", "popper-layouts"); - container.append(layoutsContainer.current); - } + const cy = cyRef.current; + if (!cy) return; - const ext = cy.extent(); - const nodesInView = cy.nodes().filter((n) => { - const bb = n.boundingBox(); - return ( - bb.x2 > ext.x1 && bb.x1 < ext.x2 && bb.y2 > ext.y1 && bb.y1 < ext.y2 - ); - }); + const container = cy.container(); - nodesInView.toArray()?.forEach((item) => { - const node = item as NodeSingularWithPopper; + if (!container) { + console.warn("Cannot create popper container"); + return; + } - const layoutsPopper = node.popper({ - popper: { - placement: "left", - modifiers: [{ name: "flip", options: { boundary: node } }], - }, - content: ([item]) => { - const itemId = item.id(); - const itemElement = layoutsContainer.current?.querySelector( - `.popper-layout[data-id='${itemId}']` - ); - if (itemElement) { - return itemElement; - } + if (!popperContainerRef.current) { + popperContainerRef.current = document.createElement("div"); + popperContainerRef.current.setAttribute("id", "poppers-container"); + container.append(popperContainerRef.current); + } - const layoutElement = document.createElement("div"); - layoutElement.style.zIndex = "0"; - layoutElement.classList.add("popper-layout"); - layoutElement.setAttribute("data-id", item.id()); - layoutElement.addEventListener("mouseup", () => { - //Узнаём грани, идущие от этой ноды - setModalQuestionParentContentId(item.id()); - setOpenedModalQuestions(true); - }); - layoutsContainer.current?.appendChild(layoutElement); + cy.nodes().forEach((item) => { + const node = item as NodeSingularWithPopper; - return layoutElement; - }, - }); + const layoutsPopper = node.popper({ + popper: { + placement: "left", + modifiers: [{ name: "flip", options: { boundary: node } }], + }, + content: (items) => { + const item = items[0]; + const itemId = item.id(); + const itemElement = popperContainerRef.current?.querySelector(`.popper-layout[data-id='${itemId}']`); + if (itemElement) { + return itemElement; + } - const plusesPopper = node.popper({ - popper: { - placement: "right", - modifiers: [{ name: "flip", options: { boundary: node } }], - }, - content: ([item]) => { - const itemId = item.id(); - const itemElement = plusesContainer.current?.querySelector( - `.popper-plus[data-id='${itemId}']` - ); - if (itemElement) { - return itemElement; - } + const layoutElement = document.createElement("div"); + layoutElement.style.zIndex = "0"; + layoutElement.classList.add("popper-layout"); + layoutElement.setAttribute("data-id", item.id()); + layoutElement.addEventListener("mouseup", () => { + //Узнаём грани, идущие от этой ноды + setModalQuestionParentContentId(item.id()); + setOpenedModalQuestions(true); + }); + popperContainerRef.current?.appendChild(layoutElement); - const plusElement = document.createElement("div"); - plusElement.classList.add("popper-plus"); - plusElement.setAttribute("data-id", item.id()); - plusElement.style.zIndex = "1"; - plusElement.addEventListener("mouseup", () => { - setStartCreate(node.id()); - }); + return layoutElement; + }, + }); + popperInstancesRef.current.push(layoutsPopper); - plusesContainer.current?.appendChild(plusElement); + const plusesPopper = node.popper({ + popper: { + placement: "right", + modifiers: [{ name: "flip", options: { boundary: node } }], + }, + content: ([item]) => { + const itemId = item.id(); + const itemElement = popperContainerRef.current?.querySelector(`.popper-plus[data-id='${itemId}']`); + if (itemElement) { + return itemElement; + } - return plusElement; - }, - }); + const plusElement = document.createElement("div"); + plusElement.classList.add("popper-plus"); + plusElement.setAttribute("data-id", item.id()); + plusElement.style.zIndex = "1"; + plusElement.addEventListener("mouseup", () => { + if (!cy || !quizId) return; - const crossesPopper = node.popper({ - popper: { - placement: "top-end", - modifiers: [{ name: "flip", options: { boundary: node } }], - }, - content: ([item]) => { - const itemId = item.id(); - const itemElement = crossesContainer.current?.querySelector( - `.popper-cross[data-id='${itemId}']` - ); - if (itemElement) { - return itemElement; - } + const es = addNode({ + cy, + quizId, + parentNodeContentId: node.id(), + }); + runCyLayout(); + if (es) cy.fit(es, 100); + cleardragQuestionContentId(); + }); - const crossElement = document.createElement("div"); - crossElement.classList.add("popper-cross"); - crossElement.setAttribute("data-id", item.id()); - crossElement.style.zIndex = "2"; - crossesContainer.current?.appendChild(crossElement); - crossElement.addEventListener("mouseup", () => { - setStartRemove(node.id()); - }); + popperContainerRef.current?.appendChild(plusElement); - return crossElement; - }, - }); - let gearsPopper: Popper | null = null; - if (node.data().root !== true) { - gearsPopper = node.popper({ - popper: { - placement: "left", - modifiers: [{ name: "flip", options: { boundary: node } }], - }, - content: ([item]) => { - const itemId = item.id(); + return plusElement; + }, + }); + popperInstancesRef.current.push(plusesPopper); - const itemElement = gearsContainer.current?.querySelector( - `.popper-gear[data-id='${itemId}']` - ); - if (itemElement) { - return itemElement; + const crossesPopper = node.popper({ + popper: { + placement: "top-end", + modifiers: [{ name: "flip", options: { boundary: node } }], + }, + content: ([item]) => { + const itemId = item.id(); + const itemElement = popperContainerRef.current?.querySelector(`.popper-cross[data-id='${itemId}']`); + if (itemElement) { + return itemElement; + } + + const crossElement = document.createElement("div"); + crossElement.classList.add("popper-cross"); + crossElement.setAttribute("data-id", item.id()); + crossElement.style.zIndex = "2"; + popperContainerRef.current?.appendChild(crossElement); + crossElement.addEventListener("mouseup", () => { + updateDeleteId(node.id()); + }); + + return crossElement; + }, + }); + popperInstancesRef.current.push(crossesPopper); + + let gearsPopper: PopperInstance | null = null; + if (node.data().root !== true) { + gearsPopper = node.popper({ + popper: { + placement: "left", + modifiers: [{ name: "flip", options: { boundary: node } }], + }, + content: ([item]) => { + const itemId = item.id(); + + const itemElement = popperContainerRef.current?.querySelector(`.popper-gear[data-id='${itemId}']`); + if (itemElement) { + return itemElement; + } + + const gearElement = document.createElement("div"); + gearElement.classList.add("popper-gear"); + gearElement.setAttribute("data-id", item.id()); + gearElement.style.zIndex = "1"; + popperContainerRef.current?.appendChild(gearElement); + gearElement.addEventListener("mouseup", () => { + updateOpenedModalSettingsId(item.id()); + }); + + return gearElement; + }, + }); + popperInstancesRef.current.push(gearsPopper); } - const gearElement = document.createElement("div"); - gearElement.classList.add("popper-gear"); - gearElement.setAttribute("data-id", item.id()); - gearElement.style.zIndex = "1"; - gearsContainer.current?.appendChild(gearElement); - gearElement.addEventListener("mouseup", () => { - updateOpenedModalSettingsId(item.id()); - }); + const onZoom = (event: AbstractEventObject) => { + if (event.cy.data("dragging")) return; + const zoom = event.cy.zoom(); - return gearElement; - }, + crossesPopper.setOptions({ + modifiers: [ + { name: "flip", options: { boundary: node } }, + { name: "offset", options: { offset: [-5 * zoom, -30 * zoom] } }, + ], + }); + + layoutsPopper.setOptions({ + modifiers: [ + { name: "flip", options: { boundary: node } }, + { name: "offset", options: { offset: [0, -130 * zoom] } }, + ], + }); + plusesPopper.setOptions({ + modifiers: [ + { name: "flip", options: { boundary: node } }, + { name: "offset", options: { offset: [0, 0] } }, + ], + }); + gearsPopper?.setOptions({ + modifiers: [ + { name: "flip", options: { boundary: node } }, + { name: "offset", options: { offset: [0, 0] } }, + ], + }); + + popperContainerRef.current?.querySelectorAll(".popper-layout").forEach((item) => { + const element = item as HTMLDivElement; + element.style.width = `${130 * zoom}px`; + element.style.height = `${130 * zoom}px`; + }); + + popperContainerRef.current?.querySelectorAll(".popper-plus").forEach((item) => { + const element = item as HTMLDivElement; + element.style.width = `${40 * zoom}px`; + element.style.height = `${40 * zoom}px`; + element.style.fontSize = `${40 * zoom}px`; + element.style.borderRadius = `${6 * zoom}px`; + }); + + popperContainerRef.current?.querySelectorAll(".popper-cross").forEach((item) => { + const element = item as HTMLDivElement; + element.style.width = `${24 * zoom}px`; + element.style.height = `${24 * zoom}px`; + element.style.fontSize = `${24 * zoom}px`; + element.style.borderRadius = `${6 * zoom}px`; + }); + + popperContainerRef?.current?.querySelectorAll(".popper-gear").forEach((item) => { + const element = item as HTMLDivElement; + element.style.width = `${60 * zoom}px`; + element.style.height = `${40 * zoom}px`; + }); + }; + + cy.on("zoom render", onZoom); }); - } - const update = async () => { - await plusesPopper.update(); - await crossesPopper.update(); - await gearsPopper?.update(); - await layoutsPopper.update(); - }; + }; - const onZoom = (event: AbstractEventObject) => { - const zoom = event.cy.zoom(); - //update(); - - crossesPopper.setOptions({ - modifiers: [ - { name: "flip", options: { boundary: node } }, - { name: "offset", options: { offset: [-5 * zoom, -30 * zoom] } }, - ], - }); - - layoutsPopper.setOptions({ - modifiers: [ - { name: "flip", options: { boundary: node } }, - { name: "offset", options: { offset: [0, -130 * zoom] } }, - ], - }); - plusesPopper.setOptions({ - modifiers: [ - { name: "flip", options: { boundary: node } }, - { name: "offset", options: { offset: [0, 0 * zoom] } }, - ], - }); - gearsPopper?.setOptions({ - modifiers: [ - { name: "flip", options: { boundary: node } }, - { name: "offset", options: { offset: [0, 0] } }, - ], - }); - - layoutsContainer.current - ?.querySelectorAll("#popper-layouts > .popper-layout") - .forEach((item) => { - const element = item as HTMLDivElement; - element.style.width = `${130 * zoom}px`; - element.style.height = `${130 * zoom}px`; - }); - - plusesContainer.current - ?.querySelectorAll("#popper-pluses > .popper-plus") - .forEach((item) => { - const element = item as HTMLDivElement; - element.style.width = `${40 * zoom}px`; - element.style.height = `${40 * zoom}px`; - element.style.fontSize = `${40 * zoom}px`; - element.style.borderRadius = `${6 * zoom}px`; - }); - - crossesContainer.current - ?.querySelectorAll("#popper-crosses > .popper-cross") - .forEach((item) => { - const element = item as HTMLDivElement; - element.style.width = `${24 * zoom}px`; - element.style.height = `${24 * zoom}px`; - element.style.fontSize = `${24 * zoom}px`; - element.style.borderRadius = `${6 * zoom}px`; - }); - - gearsContainer?.current - ?.querySelectorAll("#popper-gears > .popper-gear") - .forEach((item) => { - const element = item as HTMLDivElement; - element.style.width = `${60 * zoom}px`; - element.style.height = `${40 * zoom}px`; - }); - }; - - //node?.on("position", update); - let pressed = false; - let hide = false; - cy?.on("mousedown", () => { - pressed = true; - }); - cy?.on("mouseup", () => { - pressed = false; - hide = false; - - const gc = gearsContainer.current; - if (gc) gc.style.display = "block"; - const pc = plusesContainer.current; - const xc = crossesContainer.current; - const lc = layoutsContainer.current; - if (pc) pc.style.display = "block"; - if (xc) xc.style.display = "block"; - if (lc) lc.style.display = "block"; - update(); - }); - - cy?.on("mousemove", () => { - if (pressed && !hide) { - hide = true; - const gc = gearsContainer.current; - if (gc) gc.style.display = "none"; - const pc = plusesContainer.current; - const xc = crossesContainer.current; - const lc = layoutsContainer.current; - if (pc) pc.style.display = "none"; - if (xc) xc.style.display = "none"; - if (lc) lc.style.display = "block"; - } - }); - - cy?.on("zoom render", onZoom); - }); - }; - - const readyLO = (event: LayoutEventObject) => { - if (event.cy.data("firstNode") === "nonroot") { - event.cy.data("firstNode", "root"); - event.cy - .nodes() - .sort((a, b) => (a.data("root") ? 1 : -1)) - .layout(layoutOptions) - .run(); - } else { - event.cy.data("changed", false); - event.cy.removeData("firstNode"); - } - - //удаляем иконки - event.cy.nodes().forEach((ele: any) => { - const data = ele.data(); - data.id && removeButtons(data.id); - }); - - initialPopperIcons(event); - }; - - const layoutOptions: PresetLayoutOptions = { - name: "preset", - - positions: (node) => { - if (!node.cy().data("changed")) { - return node.data("oldPos"); - } - const id = node.id(); - const incomming = node.cy().edges(`[target="${id}"]`); - const layer = 0; - node.removeData("lastChild"); - - 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 = []; - children.forEach((n) => { - 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) => - 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) => { - return node.data("subtreeWidth") === undefined; - }); - if (unprocessed.length !== 0) { - queue.push(task); - unprocessed.forEach((t) => { - queue.push({ - parent: t, - children: t.cy().edges(`[source="${t.id()}"]`).targets(), - }); - }); - continue; - } - - task?.parent.data( - "subtreeWidth", - task.children.reduce((p, n) => p + n.data("subtreeWidth"), 0) - ); - } - - const pos = { x: 0, y: 0 }; - node.data("oldPos", pos); - - 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) => { - const width = n.data("subtreeWidth"); - - 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, - }); - }); - } - node.cy().data("changed", false); - return pos; - } else { - const opos = node.data("oldPos"); - if (opos) { - return opos; - } - } - }, // map of (node id) => (position obj); or function(node){ return somPos; } - zoom: undefined, // the zoom level to set (prob want fit = false if set) - pan: 1, // the pan level to set (prob want fit = false if set) - fit: false, // whether to fit to viewport - padding: 30, // padding on fit - animate: false, // whether to transition the node positions - animationDuration: 500, // duration of animation in ms if enabled - animationEasing: undefined, // easing of animation if enabled - animateFilter: function (node, i) { - return false; - }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts - ready: readyLO, // callback on layoutready - transform: function (node, position) { - return position; - }, // transform a given node position. Useful for changing flow direction in discrete layouts - }; - - return { layoutOptions }; + return { removeAllPoppers, removePoppersById, createPoppers }; }; diff --git a/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts b/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts index 60997905..4288a922 100644 --- a/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts +++ b/src/pages/Questions/BranchingMap/hooks/useRemoveNode.ts @@ -1,221 +1,122 @@ -import { - deleteQuestion, - updateQuestion, - getQuestionByContentId, - clearRuleForAll, - createResult, -} from "@root/questions/actions"; +import { devlog } from "@frontend/kitui"; +import { QuizQuestionResult } from "@model/questionTypes/result"; +import { clearRuleForAll, getQuestionByContentId, updateQuestion } from "@root/questions/actions"; import { useQuestionsStore } from "@root/questions/store"; -import { useCurrentQuiz } from "@root/quizes/hooks"; import { updateRootContentId } from "@root/quizes/actions"; - +import { useCurrentQuiz } from "@root/quizes/hooks"; +import type { CollectionReturnValue, Core, SingularElementArgument } from "cytoscape"; import type { MutableRefObject } from "react"; -import type { - Core, - CollectionReturnValue, - PresetLayoutOptions, -} from "cytoscape"; -import type { - AnyTypedQuizQuestion, - QuestionBranchingRule, - QuestionBranchingRuleMain, -} from "../../../../model/questionTypes/shared"; +import { clearDataAfterRemoveNode } from "../helper"; type UseRemoveNodeArgs = { - cyRef: MutableRefObject; - layoutOptions: PresetLayoutOptions; - layoutsContainer: MutableRefObject; - plusesContainer: MutableRefObject; - crossesContainer: MutableRefObject; - gearsContainer: MutableRefObject; + cyRef: MutableRefObject; + runCyLayout: () => void; + removeButtons: (id: string) => void; }; export const useRemoveNode = ({ - cyRef, - layoutOptions, - layoutsContainer, - plusesContainer, - crossesContainer, - gearsContainer, + cyRef, + runCyLayout, + removeButtons, }: UseRemoveNodeArgs) => { - const { questions: trashQuestions } = useQuestionsStore(); - const quiz = useCurrentQuiz(); + const { questions: trashQuestions } = useQuestionsStore(); + const quiz = useCurrentQuiz(); - const removeButtons = (id: string) => { - layoutsContainer.current - ?.querySelector(`.popper-layout[data-id='${id}']`) - ?.remove(); - plusesContainer.current - ?.querySelector(`.popper-plus[data-id='${id}']`) - ?.remove(); - crossesContainer.current - ?.querySelector(`.popper-cross[data-id='${id}']`) - ?.remove(); - gearsContainer.current - ?.querySelector(`.popper-gear[data-id='${id}']`) - ?.remove(); - }; + const removeNode = (targetNodeContentId: string) => { + const deleteNodes: string[] = []; + const deleteEdges: SingularElementArgument[] = []; + const cy = cyRef?.current; - const clearDataAfterRemoveNode = ({ - targetQuestionContentId, - parentQuestionContentId, - }: { - targetQuestionContentId: string; - parentQuestionContentId: string; - }) => { - updateQuestion(targetQuestionContentId, (question) => { - question.content.rule.parentId = ""; - question.content.rule.children = []; - question.content.rule.main = []; - question.content.rule.default = ""; - }); + const findChildrenToDelete = (node: CollectionReturnValue) => { + //Узнаём грани, идущие от этой ноды + cy + ?.$('edge[source = "' + node.id() + '"]') + ?.toArray() + .forEach((edge) => { + const edgeData = edge.data(); - //Ищём родителя - const parentQuestion = getQuestionByContentId(parentQuestionContentId); + //записываем id грани для дальнейшего удаления + deleteEdges.push(edge); + //ищем ноду на конце грани, записываем её ID для дальнейшего удаления + const targetNode = cy?.$("#" + edgeData.target); + deleteNodes.push(targetNode.data().id); + //вызываем функцию для анализа потомков уже у этой ноды + findChildrenToDelete(targetNode); + }); + }; - //Делаем результат родителя активным - const parentResult = trashQuestions.find(q => q.type === "result" && q.content.rule.parentId === parentQuestionContentId) - if (parentResult) { - updateQuestion(parentResult.content.id, q => { - q.content.usage = true - }) - } else { - createResult(quiz?.backendId, parentQuestionContentId) - } + const elementToDelete = cy?.getElementById(targetNodeContentId); - //чистим rule родителя - if (!parentQuestion?.type) { - return; - } - - const newChildren = [...parentQuestion.content.rule.children]; - newChildren.splice( - parentQuestion.content.rule.children.indexOf(targetQuestionContentId), - 1 - ); - - 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, - }; - - updateQuestion(parentQuestionContentId, (PQ) => { - PQ.content.rule = newRule; - }); - }; - - const removeNode = (targetNodeContentId: string) => { - const deleteNodes: string[] = []; - const deleteEdges: any = []; - const cy = cyRef?.current; - - const findChildrenToDelete = (node: CollectionReturnValue) => { - //Узнаём грани, идущие от этой ноды - cy?.$('edge[source = "' + node.id() + '"]') - ?.toArray() - .forEach((edge) => { - const edgeData = edge.data(); - - //записываем id грани для дальнейшего удаления - deleteEdges.push(edge); - //ищем ноду на конце грани, записываем её ID для дальнейшего удаления - const targetNode = cy?.$("#" + edgeData.target); - deleteNodes.push(targetNode.data().id); - //вызываем функцию для анализа потомков уже у этой ноды - findChildrenToDelete(targetNode); - }); - }; - - const elementToDelete = cy?.getElementById(targetNodeContentId); - - if (elementToDelete) { - findChildrenToDelete(elementToDelete); - } - - const targetQuestion = getQuestionByContentId(targetNodeContentId); - - if ( - targetQuestion?.type && - targetQuestion.content.rule.parentId === "root" && - quiz - ) { - updateRootContentId(quiz?.id, ""); - updateQuestion(targetNodeContentId, (question) => { - question.content.rule.parentId = ""; - question.content.rule.main = []; - question.content.rule.children = []; - question.content.rule.default = ""; - }); - clearRuleForAll(); - } else { - const parentQuestionContentId = cy - ?.$('edge[target = "' + targetNodeContentId + '"]') - ?.toArray()?.[0] - ?.data()?.source; - if (targetNodeContentId && parentQuestionContentId) { - if ( - quiz && - cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0 - ) { - console.log(parentQuestionContentId) - //createFrontResult(quiz.backendId, parentQuestionContentId); + if (elementToDelete) { + findChildrenToDelete(elementToDelete); } - clearDataAfterRemoveNode({ - targetQuestionContentId: targetNodeContentId, - parentQuestionContentId, + + const targetQuestion = getQuestionByContentId(targetNodeContentId); + + if (targetQuestion?.type && targetQuestion.content.rule.parentId === "root" && quiz) { + updateRootContentId(quiz.id, ""); + updateQuestion(targetNodeContentId, (question) => { + question.content.rule.parentId = ""; + question.content.rule.main = []; + question.content.rule.children = []; + question.content.rule.default = ""; + }); + clearRuleForAll(); + } else { + const parentQuestionContentId = cy + ?.$('edge[target = "' + targetNodeContentId + '"]') + ?.toArray()?.[0] + ?.data()?.source; + if (targetNodeContentId && parentQuestionContentId) { + if (quiz && cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0) { + devlog(parentQuestionContentId); + //createFrontResult(quiz.backendId, parentQuestionContentId); + } + clearDataAfterRemoveNode({ + quiz, + trashQuestions, + targetQuestionContentId: targetNodeContentId, + parentQuestionContentId, + }); + cy?.remove(cy?.$("#" + targetNodeContentId)); + } + } + + //После всех манипуляций удаляем грани и ноды из CS Чистим rule потомков на беке + + 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 = []; + }); }); - cy?.remove(cy?.$("#" + targetNodeContentId)) - .layout(layoutOptions) - .run(); - } - } - //После всех манипуляций удаляем грани и ноды из CS Чистим rule потомков на беке + deleteEdges.forEach((edge: any) => { + //Грани + cy?.remove(edge); + }); - 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 = []; - }); - }); + removeButtons(targetNodeContentId); + runCyLayout(); - deleteEdges.forEach((edge: any) => { - //Грани - cy?.remove(edge); - }); + //делаем result всех потомков неактивными + trashQuestions.forEach((qr) => { + if ( + qr.type === "result" && + (deleteNodes.includes(qr.content.rule.parentId || "") || + (targetQuestion?.type && qr.content.rule.parentId === targetQuestion.content.id)) + ) { + updateQuestion(qr.content.id, (q) => { + q.content.usage = false; + }); + } + }); + }; - removeButtons(targetNodeContentId); - cy?.data("changed", true); - cy?.layout(layoutOptions).run(); - - //делаем result всех потомков неактивными - trashQuestions.forEach((qr) => { - if ( - qr.type === "result" && - (deleteNodes.includes(qr.content.rule.parentId || "") || - (targetQuestion?.type && - qr.content.rule.parentId === targetQuestion.content.id)) - ) { - updateQuestion(qr.content.id, q => { - q.content.usage = false - }) - } - }); - }; - - return { removeNode }; + return { removeNode }; }; diff --git a/src/pages/Questions/BranchingMap/index.tsx b/src/pages/Questions/BranchingMap/index.tsx index 6e6c137b..fffb8c18 100644 --- a/src/pages/Questions/BranchingMap/index.tsx +++ b/src/pages/Questions/BranchingMap/index.tsx @@ -1,54 +1,34 @@ import { Box } from "@mui/material"; -import { FirstNodeField } from "./FirstNodeField"; -import CsComponent from "./CsComponent"; import { useCurrentQuiz } from "@root/quizes/hooks"; -import { useEffect, useState } from "react"; -import { BranchingQuestionsModal } from "../BranchingQuestionsModal"; import { useUiTools } from "@root/uiTools/store"; +import { BranchingQuestionsModal } from "../BranchingQuestionsModal"; +import CsComponent from "./CsComponent"; +import { FirstNodeField } from "./FirstNodeField"; export const BranchingMap = () => { - const quiz = useCurrentQuiz(); - const { dragQuestionContentId } = useUiTools(); - const [modalQuestionParentContentId, setModalQuestionParentContentId] = - useState(""); - const [modalQuestionTargetContentId, setModalQuestionTargetContentId] = - useState(""); - const [openedModalQuestions, setOpenedModalQuestions] = - useState(false); + const quiz = useCurrentQuiz(); + const dragQuestionContentId = useUiTools(state => state.dragQuestionContentId); - return ( - - {quiz?.config.haveRoot ? ( - - ) : ( - - )} - - - ); + return ( + + {quiz?.config.haveRoot ? ( + + ) : ( + + )} + + + ); }; diff --git a/src/pages/Questions/BranchingMap/style/styles.css b/src/pages/Questions/BranchingMap/style/styles.css index 98e76b8d..5e9f735a 100644 --- a/src/pages/Questions/BranchingMap/style/styles.css +++ b/src/pages/Questions/BranchingMap/style/styles.css @@ -1,4 +1,4 @@ -#popper-pluses > .popper-plus { +.popper-plus { cursor: pointer; display: flex; align-items: center; @@ -9,13 +9,13 @@ font-size: 0px; } -#popper-pluses > .popper-plus::before { +.popper-plus::before { content: "+"; color: rgba(154, 154, 175, 0.5); font-size: inherit; } -#popper-crosses > .popper-cross { +.popper-cross { cursor: pointer; display: flex; align-items: center; @@ -25,14 +25,14 @@ font-size: 0px; } -#popper-crosses > .popper-cross::before { +.popper-cross::before { content: "+"; transform: rotate(45deg); color: #fff; font-size: inherit; } -#popper-gears > .popper-gear { +.popper-gear { cursor: pointer; display: flex; align-items: center; diff --git a/src/pages/Questions/BranchingQuestionsModal/index.tsx b/src/pages/Questions/BranchingQuestionsModal/index.tsx index ff140d9a..63e6e47f 100644 --- a/src/pages/Questions/BranchingQuestionsModal/index.tsx +++ b/src/pages/Questions/BranchingQuestionsModal/index.tsx @@ -1,90 +1,84 @@ import { Box, Modal, Button, Typography } from "@mui/material"; import { useQuestionsStore } from "@root/questions/store"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; +import { useUiTools } from "@root/uiTools/store"; +import { setModalQuestionTargetContentId, setOpenedModalQuestions } from "@root/uiTools/actions"; -interface Props { - openedModalQuestions: boolean; - setModalQuestionTargetContentId: (contentId: string) => void; - setOpenedModalQuestions: (open: boolean) => void; -} -export const BranchingQuestionsModal = ({ - openedModalQuestions, - setOpenedModalQuestions, - setModalQuestionTargetContentId, -}: Props) => { - const trashQuestions = useQuestionsStore().questions; - const questions = trashQuestions.filter( - (question) => question.type !== "result" - ); +export const BranchingQuestionsModal = () => { + const trashQuestions = useQuestionsStore().questions; + const questions = trashQuestions.filter( + (question) => question.type !== "result" + ); + const openedModalQuestions = useUiTools(state => state.openedModalQuestions); - const handleClose = () => { - setOpenedModalQuestions(false); - }; + const handleClose = () => { + setOpenedModalQuestions(false); + }; - const typedQuestions: AnyTypedQuizQuestion[] = questions.filter( - (question) => - question.type && - !question.content.rule.parentId && - question.type !== "result" - ) as AnyTypedQuizQuestion[]; + const typedQuestions: AnyTypedQuizQuestion[] = questions.filter( + (question) => + question.type && + !question.content.rule.parentId && + question.type !== "result" + ) as AnyTypedQuizQuestion[]; - if (typedQuestions.length === 0) return <>; + if (typedQuestions.length === 0) return <>; - return ( - - - - {typedQuestions.map((question) => ( - - ))} - - - - ); + > + + {typedQuestions.map((question) => ( + + ))} + + + + ); }; diff --git a/src/stores/questions/actions.ts b/src/stores/questions/actions.ts index 97e4b7fb..a90e2b54 100644 --- a/src/stores/questions/actions.ts +++ b/src/stores/questions/actions.ts @@ -498,7 +498,7 @@ export const clearRuleForAll = () => { }; export const createResult = async ( - quizId: number, + quizId: number | undefined, parentContentId?: string ) => requestQueue.enqueue(async () => { if (!quizId || !parentContentId) { @@ -543,4 +543,3 @@ export const createResult = async ( } } }); - diff --git a/src/stores/uiTools/actions.ts b/src/stores/uiTools/actions.ts index cd6da193..a386b455 100644 --- a/src/stores/uiTools/actions.ts +++ b/src/stores/uiTools/actions.ts @@ -38,3 +38,7 @@ export const updateCanCreatePublic = (can: boolean) => useUiTools.setState({ can export const updateModalInfoWhyCantCreate = (can: boolean) => useUiTools.setState({ openModalInfoWhyCantCreate: can }); export const updateDeleteId = (deleteNodeId: string | null = null) => useUiTools.setState({ deleteNodeId }); + +export const setModalQuestionParentContentId = (modalQuestionParentContentId: string) => useUiTools.setState({ modalQuestionParentContentId }); +export const setModalQuestionTargetContentId = (modalQuestionTargetContentId: string) => useUiTools.setState({ modalQuestionTargetContentId }); +export const setOpenedModalQuestions = (open: boolean) => useUiTools.setState({ openedModalQuestions: open }); diff --git a/src/stores/uiTools/store.ts b/src/stores/uiTools/store.ts index 52803348..1499aa58 100644 --- a/src/stores/uiTools/store.ts +++ b/src/stores/uiTools/store.ts @@ -10,7 +10,10 @@ export type UiTools = { canCreatePublic: boolean; whyCantCreatePublic: Record//ид вопроса и список претензий к нему openModalInfoWhyCantCreate: boolean; -deleteNodeId: string | null; + deleteNodeId: string | null; + modalQuestionParentContentId: string; + modalQuestionTargetContentId: string; + openedModalQuestions: boolean; }; export type WhyCantCreatePublic = { name: string; @@ -27,13 +30,16 @@ const initialState: UiTools = { canCreatePublic: false, whyCantCreatePublic: {}, openModalInfoWhyCantCreate: false, -deleteNodeId: null, + deleteNodeId: null, + modalQuestionParentContentId: "", + modalQuestionTargetContentId: "", + openedModalQuestions: false, }; export const useUiTools = create()( - devtools(() => initialState, { - name: "UiTools", - enabled: process.env.NODE_ENV === "development", - trace: process.env.NODE_ENV === "development", - }) + devtools(() => initialState, { + name: "UiTools", + enabled: process.env.NODE_ENV === "development", + trace: process.env.NODE_ENV === "development", + }) );