derive graph state from store instead of updating it manually
This commit is contained in:
parent
b8b5bc5d2e
commit
588e21ef1e
@ -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<getPopperInstance<SingularData>>;
|
||||
type PopperInstance = ReturnType<getPopperInstance<SingularData>>;
|
||||
|
||||
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<boolean>(false);
|
||||
|
||||
const cyRef = useRef<Core | null>(null);
|
||||
const popperContainerRef = useRef<HTMLDivElement | null>(null);
|
||||
const popperInstancesRef = useRef<PopperInstance[]>([]);
|
||||
|
||||
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() {
|
||||
|
||||
<CytoscapeComponent
|
||||
wheelSensitivity={0.1}
|
||||
elements={[]}
|
||||
// elements={createGraphElements(tree, quiz)}
|
||||
elements={cyElements}
|
||||
style={{
|
||||
height: "480px",
|
||||
background: "#F2F3F7",
|
||||
|
||||
@ -30,7 +30,7 @@ export const FirstNodeField = () => {
|
||||
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("Нет информации о взятом опроснике");
|
||||
|
||||
@ -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("Добавляемый вопрос не найден");
|
||||
}
|
||||
|
||||
@ -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<Core | null>;
|
||||
quizId: number | undefined,
|
||||
popperContainerRef: MutableRefObject<HTMLDivElement | null>;
|
||||
popperInstancesRef: MutableRefObject<PopperInstance[]>;
|
||||
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 };
|
||||
};
|
||||
|
||||
@ -10,24 +10,19 @@ import { clearDataAfterRemoveNode } from "../helper";
|
||||
|
||||
type UseRemoveNodeArgs = {
|
||||
cyRef: MutableRefObject<Core | null>;
|
||||
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) => {
|
||||
|
||||
@ -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)
|
||||
}
|
||||
|
||||
Loading…
Reference in New Issue
Block a user