derive graph state from store instead of updating it manually

This commit is contained in:
nflnkr 2024-01-09 19:41:35 +03:00
parent b8b5bc5d2e
commit 588e21ef1e
6 changed files with 47 additions and 114 deletions

@ -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)
}