при удалении корня все использованные вопросы сбрасывают rule

This commit is contained in:
Nastya 2023-12-04 19:33:50 +03:00
commit c3d1353a9b
21 changed files with 1039 additions and 789 deletions

93
package-lock.json generated

@ -40,6 +40,7 @@
"react": "^18.2.0",
"react-beautiful-dnd": "^13.1.1",
"react-cytoscapejs": "^2.0.0",
"react-datepicker": "^4.24.0",
"react-dnd": "^16.0.1",
"react-dnd-html5-backend": "^16.0.1",
"react-dom": "^18.2.0",
@ -61,7 +62,8 @@
"@emoji-mart/react": "^1.1.1",
"@types/cytoscape-popper": "^2.0.4",
"@types/react-beautiful-dnd": "^13.1.4",
"@types/react-cytoscapejs": "^1.2.5",
"@types/react-cytoscapejs": "^1.2.4",
"@types/react-datepicker": "^4.19.3",
"craco-alias": "^3.0.1",
"cypress": "^13.4.0"
}
@ -4524,6 +4526,18 @@
"@types/react": "*"
}
},
"node_modules/@types/react-datepicker": {
"version": "4.19.3",
"resolved": "https://registry.npmjs.org/@types/react-datepicker/-/react-datepicker-4.19.3.tgz",
"integrity": "sha512-85F9eKWu9fGiD9r4KVVMPYAdkJJswR3Wci9PvqplmB6T+D+VbUqPeKtifg96NZ4nEhufjehW+SX4JLrEWVplWw==",
"dev": true,
"dependencies": {
"@popperjs/core": "^2.9.2",
"@types/react": "*",
"date-fns": "^2.0.1",
"react-popper": "^2.2.5"
}
},
"node_modules/@types/react-dnd": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/@types/react-dnd/-/react-dnd-3.0.2.tgz",
@ -6374,6 +6388,11 @@
"integrity": "sha512-cOU9usZw8/dXIXKtwa8pM0OTJQuJkxMN6w30csNRUerHfeQ5R6U3kkU/FtJeIf3M202OHfY2U8ccInBG7/xogA==",
"license": "MIT"
},
"node_modules/classnames": {
"version": "2.3.2",
"resolved": "https://registry.npmjs.org/classnames/-/classnames-2.3.2.tgz",
"integrity": "sha512-CSbhY4cFEJRe6/GQzIk5qXZ4Jeg5pcsP7b5peFSDpffpe1cqjASH/n9UTjBwOp6XpMSTwQ8Za2K5V02ueA7Tmw=="
},
"node_modules/clean-css": {
"version": "5.3.1",
"resolved": "https://registry.npmjs.org/clean-css/-/clean-css-5.3.1.tgz",
@ -7438,6 +7457,21 @@
"node": ">=10"
}
},
"node_modules/date-fns": {
"version": "2.30.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-2.30.0.tgz",
"integrity": "sha512-fnULvOpxnC5/Vg3NCiWelDsLiUc9bRwAPs/+LfTLNvetFCtCTN+yQz15C/fs4AwX1R9K5GLtLfn8QW+dWisaAw==",
"dependencies": {
"@babel/runtime": "^7.21.0"
},
"engines": {
"node": ">=0.11"
},
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/date-fns"
}
},
"node_modules/dayjs": {
"version": "1.11.10",
"resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz",
@ -15126,6 +15160,23 @@
"react": ">=15.0.0"
}
},
"node_modules/react-datepicker": {
"version": "4.24.0",
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-4.24.0.tgz",
"integrity": "sha512-2QUC2pP+x4v3Jp06gnFllxKsJR0yoT/K6y86ItxEsveTXUpsx+NBkChWXjU0JsGx/PL8EQnsxN0wHl4zdA1m/g==",
"dependencies": {
"@popperjs/core": "^2.11.8",
"classnames": "^2.2.6",
"date-fns": "^2.30.0",
"prop-types": "^15.7.2",
"react-onclickoutside": "^6.13.0",
"react-popper": "^2.3.0"
},
"peerDependencies": {
"react": "^16.9.0 || ^17 || ^18",
"react-dom": "^16.9.0 || ^17 || ^18"
}
},
"node_modules/react-dev-utils": {
"version": "12.0.1",
"resolved": "https://registry.npmjs.org/react-dev-utils/-/react-dev-utils-12.0.1.tgz",
@ -15308,6 +15359,38 @@
"integrity": "sha512-xWGDIW6x921xtzPkhiULtthJHoJvBbF3q26fzloPCK0hsvxtPVelvftw3zjbHWSkR2km9Z+4uxbDDK/6Zw9B8w==",
"license": "MIT"
},
"node_modules/react-onclickoutside": {
"version": "6.13.0",
"resolved": "https://registry.npmjs.org/react-onclickoutside/-/react-onclickoutside-6.13.0.tgz",
"integrity": "sha512-ty8So6tcUpIb+ZE+1HAhbLROvAIJYyJe/1vRrrcmW+jLsaM+/powDRqxzo6hSh9CuRZGSL1Q8mvcF5WRD93a0A==",
"funding": {
"type": "individual",
"url": "https://github.com/Pomax/react-onclickoutside/blob/master/FUNDING.md"
},
"peerDependencies": {
"react": "^15.5.x || ^16.x || ^17.x || ^18.x",
"react-dom": "^15.5.x || ^16.x || ^17.x || ^18.x"
}
},
"node_modules/react-popper": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/react-popper/-/react-popper-2.3.0.tgz",
"integrity": "sha512-e1hj8lL3uM+sgSR4Lxzn5h1GxBlpa4CQz0XLF8kx4MDrDRWY0Ena4c97PUeSX9i5W3UAfDP0z0FXCTQkoXUl3Q==",
"dependencies": {
"react-fast-compare": "^3.0.1",
"warning": "^4.0.2"
},
"peerDependencies": {
"@popperjs/core": "^2.0.0",
"react": "^16.8.0 || ^17 || ^18",
"react-dom": "^16.8.0 || ^17 || ^18"
}
},
"node_modules/react-popper/node_modules/react-fast-compare": {
"version": "3.2.2",
"resolved": "https://registry.npmjs.org/react-fast-compare/-/react-fast-compare-3.2.2.tgz",
"integrity": "sha512-nsO+KSNgo1SbJqJEYRE9ERzo7YtYbou/OqjSQKxV7jcKox7+usiUVZOAC+XnDOABXggQTno0Y1CpVnuWEc1boQ=="
},
"node_modules/react-redux": {
"version": "7.2.9",
"resolved": "https://registry.npmjs.org/react-redux/-/react-redux-7.2.9.tgz",
@ -18037,6 +18120,14 @@
"makeerror": "1.0.12"
}
},
"node_modules/warning": {
"version": "4.0.3",
"resolved": "https://registry.npmjs.org/warning/-/warning-4.0.3.tgz",
"integrity": "sha512-rpJyN222KWIvHJ/F53XSZv0Zl/accqHR8et1kpaMTD/fLCRxtV8iX8czMzY7sVZupTI3zcUTg8eycS2kNF9l6w==",
"dependencies": {
"loose-envify": "^1.0.0"
}
},
"node_modules/watchpack": {
"version": "2.4.0",
"resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.4.0.tgz",

@ -30,7 +30,6 @@ async function getQuestionList(body?: Partial<GetQuestionListRequest>) {
}
function editQuestion(body: EditQuestionRequest, signal?: AbortSignal) {
console.log("`${baseUrl}/question/edit` start")
return makeRequest<EditQuestionRequest, EditQuestionResponse>({
url: `${baseUrl}/question/edit`,
body,

@ -1,7 +1,7 @@
import { MessageIcon } from "@icons/messagIcon";
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import TextareaAutosize from "@mui/base/TextareaAutosize";
import {TextareaAutosize} from "@mui/base/TextareaAutosize";
import {
Box,
FormControl,

@ -1,4 +1,4 @@
import { useEffect, useRef, useState } from "react";
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import Cytoscape from "cytoscape";
import CytoscapeComponent from "react-cytoscapejs";
import popper from "cytoscape-popper";
@ -122,16 +122,21 @@ export const CsComponent = ({
const crossesContainer = useRef<HTMLDivElement | null>(null);
const gearsContainer = useRef<HTMLDivElement | null>(null);
useLayoutEffect(() => {
updateOpenedModalSettingsId()
}, [])
useEffect(() => {
console.log("modalQuestionTargetContentId " + modalQuestionTargetContentId)
if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) {
console.log("был выбран вопрос " + modalQuestionTargetContentId)
addNode({ parentNodeContentId:modalQuestionParentContentId, targetNodeContentId:modalQuestionTargetContentId })
addNode({ parentNodeContentId: modalQuestionParentContentId, targetNodeContentId: modalQuestionTargetContentId })
}
setModalQuestionParentContentId("")
setModalQuestionTargetContentId("")
console.log("modalQuestionTargetContentId " + modalQuestionTargetContentId)
}, [modalQuestionTargetContentId])
const addNode = ({ parentNodeContentId, targetNodeContentId }: { parentNodeContentId: string, targetNodeContentId?: string }) => {
console.log("dragQuestionContentId " + dragQuestionContentId)
const cy = cyRef?.current
const parentNodeChildren = cy?.$('edge[source = "' + parentNodeContentId + '"]')?.length
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
@ -139,7 +144,7 @@ export const CsComponent = ({
if (Object.keys(targetQuestion).length !== 0 && Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) {
clearDataAfterAddNode({ parentNodeContentId, targetQuestion, parentNodeChildren })
cy?.data('changed',true)
cy?.data('changed', true)
cy?.add([
{
data: {
@ -161,8 +166,6 @@ export const CsComponent = ({
}
const clearDataAfterAddNode = ({ parentNodeContentId, targetQuestion, parentNodeChildren }: { parentNodeContentId: string, targetQuestion: AnyQuizQuestion, parentNodeChildren: number }) => {
console.log("записываю на бек ид родителя")
console.log({ parentNodeContentId, targetQuestion, parentNodeChildren })
//предупреждаем добавленный вопрос о том, кто его родитель
updateQuestion(targetQuestion.content.id, question => {
question.content.rule.parentId = parentNodeContentId
@ -170,8 +173,8 @@ export const CsComponent = ({
})
//Если детей больше 1 - предупреждаем стор вопросов об открытии модалки ветвления
if (parentNodeChildren > 1) {
updateOpenedModalSettingsId(parentNodeContentId)
if (parentNodeChildren >= 1) {
updateOpenedModalSettingsId(targetQuestion.content.id)
} else {
//Если ребёнок первый - добавляем его родителю как дефолтный
updateQuestion(parentNodeContentId, question => question.content.rule.default = targetQuestion.content.id)
@ -179,9 +182,8 @@ export const CsComponent = ({
}
const removeNode = ({ targetNodeContentId }: { targetNodeContentId: string }) => {
const deleteNodes = [] as string[]
const deleteEdges:any = []
console.log("remove")
const deleteNodes = [] as string[]
const deleteEdges: any = []
const cy = cyRef?.current
const findChildrenToDelete = (node) => {
@ -205,60 +207,54 @@ export const CsComponent = ({
const targetQuestion = getQuestionByContentId(targetNodeContentId)
if (targetQuestion.content.rule.parentId === "root" && quiz) {
console.log("click ROOT")
console.log("ROOT")
console.log(quiz)
updateRootContentId(quiz?.id, "")
updateQuestion(targetNodeContentId, question => {
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.default = ""
})
updateRootContentId(quiz?.id, false)
console.log(quiz, getQuestionByContentId(targetNodeContentId))
} else {
console.log("click not ROOT")
const parentQuestionContentId = cy?.$('edge[target = "' + targetNodeContentId + '"]')?.toArray()?.[0]?.data()?.source
if (targetNodeContentId && parentQuestionContentId) {
clearDataAfterRemoveNode({ targetQuestionContentId: targetNodeContentId, parentQuestionContentId })
cy?.remove(cy?.$('#' + targetNodeContentId)).layout(lyopts).run()
clearDataAfterRemoveNode({ targetQuestionContentId: targetNodeContentId, parentQuestionContentId })
cy?.remove(cy?.$('#' + targetNodeContentId)).layout(lyopts).run()
}
console.log(deleteNodes, deleteEdges)
//После всех манипуляций удаляем грани из CS и ноды из бекенда
deleteNodes.forEach((nodeId) => {//Ноды
cy?.remove(cy?.$("#" + nodeId))
removeButtons(nodeId)
updateQuestion(nodeId, question => {
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.default = ""
})
})
deleteEdges.forEach((edge:any) => {//Грани
cy?.remove(edge)
})
removeButtons(targetNodeContentId)
cy?.data('changed',true)
cy?.layout(lyopts).run()
}
//После всех манипуляций удаляем грани из CS и ноды из бекенда
deleteNodes.forEach((nodeId) => {//Ноды
cy?.remove(cy?.$("#" + nodeId))
removeButtons(nodeId)
updateQuestion(nodeId, question => {
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.default = ""
})
})
deleteEdges.forEach((edge: any) => {//Грани
cy?.remove(edge)
})
removeButtons(targetNodeContentId)
cy?.data('changed', true)
cy?.layout(lyopts).run()
}
const clearDataAfterRemoveNode = ({ targetQuestionContentId, parentQuestionContentId }: { targetQuestionContentId: string, parentQuestionContentId: string }) => {
console.log({ targetQuestionContentId, parentQuestionContentId })
updateQuestion(targetQuestionContentId, question => {
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.default = ""
})
updateQuestion(parentQuestionContentId, question => {
console.log("parent default " + question.content.rule.default)
console.log("target ID " + targetQuestionContentId)
console.log(question.content.rule.default === targetQuestionContentId)
//Заменяем id удаляемого вопроса либо на id одного из оставшихся, либо на пустую строку
if (question.content.rule.default === targetQuestionContentId) question.content.rule.default = ""
})
@ -282,22 +278,20 @@ export const CsComponent = ({
const readyLO = (e) => {
console.log(e.cy.data('firstNode'),"SKEEER",e.cy.data('changed'))
if (e.cy.data('firstNode') === 'nonroot') {
e.cy.data('firstNode','root')
e.cy.nodes().sort((a,b) => (a.data('root')?1:-1)).layout(lyopts).run()
} else {
e.cy.data('firstNode', 'root')
e.cy.nodes().sort((a, b) => (a.data('root') ? 1 : -1)).layout(lyopts).run()
e.cy.data('changed', false)
e.cy.removeData('firstNode')
}
} else {
e.cy.data('changed', false)
e.cy.removeData('firstNode')
}
//удаляем иконки
e.cy.nodes().forEach((ele: any) => {
const data = ele.data()
console.log(data)
data.id && removeButtons(data.id);
})
initialPopperIcons(e)
@ -307,11 +301,9 @@ console.log(e.cy.data('firstNode'),"SKEEER",e.cy.data('changed'))
name: 'preset',
positions: (e) => {
console.log('BBBBBBBBBBBBBBB', e.cy().data('changed'))
if (!e.cy().data('changed')) {
return e.data('oldPos')
} else {e.removeData('oldPos')}
console.log('POSITIIIIIIIONS')
} else { e.removeData('oldPos') }
const id = e.id()
const incomming = e.cy().edges(`[target="${id}"]`)
const layer = 0
@ -319,7 +311,7 @@ console.log(e.cy.data('firstNode'),"SKEEER",e.cy.data('changed'))
if (incomming.length === 0) {
if (e.cy().data('firstNode') === undefined)
e.cy().data('firstNode','root')
e.cy().data('firstNode', 'root')
e.data('root', true)
const children = e.cy().edges(`[source="${id}"]`).targets()
e.data('layer', layer)
@ -341,7 +333,6 @@ console.log(e.cy.data('firstNode'),"SKEEER",e.cy.data('changed'))
queue.push({ parent: e, children: children })
while (queue.length) {
const task = queue.pop()
console.log('WIIIING',task.parent.data(),task.children.length)
if (task.children.length === 0) {
task.parent.data('subtreeWidth', task.parent.height())
continue
@ -360,22 +351,20 @@ console.log('WIIIING',task.parent.data(),task.children.length)
task?.parent.data('subtreeWidth', task.children.reduce((p, n) => p + n.data('subtreeWidth'), 0))
}
const pos = { x: 0, y: 0 }
console.log(e.data())
e.data('oldPos', pos)
return pos
} else {
if (e.cy().data('firstNode') !== 'root') {
e.cy().data('firstNode','nonroot')
return {x:0,y:0}
if (e.cy().data('firstNode') !== 'root') {
e.cy().data('firstNode', 'nonroot')
return { x: 0, y: 0 }
}
if (e.cy().data('firstNode') === undefined)
e.cy().data('firstNode','nonroot')
e.cy().data('firstNode', 'nonroot')
const parent = e.cy().edges(`[target="${e.id()}"]`)[0].source()
const wing = (parent.data('children') === 1) ? 0 : parent.data('subtreeWidth') / 2 + 50
const lastOffset = parent.data('lastChild')
const step = wing * 2 / (parent.data('children') - 1)
//e.removeData('subtreeWidth')
console.log('AAAAAAAAAAa', e.data(), lastOffset, step, parent.position().y, wing)
if (lastOffset !== undefined) {
parent.data('lastChild', lastOffset + step)
const pos = { x: 250 * e.data('layer'), y: (lastOffset + step) }
@ -386,7 +375,7 @@ if (e.cy().data('firstNode') !== 'root') {
const pos = { x: 250 * e.data('layer'), y: (parent.position().y - wing) }
e.data('oldPos', pos)
return pos
}
}
}
}, // map of (node id) => (position obj); or function(node){ return somPos; }
zoom: undefined, // the zoom level to set (prob want fit = false if set)
@ -502,7 +491,7 @@ if (e.cy().data('firstNode') !== 'root') {
layoutElement.classList.add("popper-layout");
layoutElement.setAttribute("data-id", item.id());
layoutElement.addEventListener("mouseup", () => {
//Узнаём грани, идущие от этой ноды
//Узнаём грани, идущие от этой ноды
setModalQuestionParentContentId(item.id())
setOpenedModalQuestions(true)
});
@ -533,7 +522,7 @@ if (e.cy().data('firstNode') !== 'root') {
plusElement.addEventListener("mouseup", () => {
setStartCreate(node.id());
});
plusesContainer.current?.appendChild(plusElement);
return plusElement;
@ -594,7 +583,6 @@ if (e.cy().data('firstNode') !== 'root') {
gearElement.style.zIndex = "1"
gearsContainer.current?.appendChild(gearElement);
gearElement.addEventListener("mouseup", (e) => {
console.log(item.id())
updateOpenedModalSettingsId(item.id())
});
@ -672,23 +660,27 @@ if (e.cy().data('firstNode') !== 'root') {
return (
<>
<CytoscapeComponent
wheelSensitivity={0.1}
elements={[]}
// elements={createGraphElements(tree, quiz)}
style={{ height: "480px", background: "#F2F3F7" }}
stylesheet={stylesheet}
layout={(lyopts)}
cy={(cy) => {
cyRef.current = cy;
}}
/>
<button onClick={() => {
console.log("ELEMENTS____________________________")
cyRef.current?.elements().forEach((ele:any) => {
console.log(ele.data())
})
}}>elements</button>
<CytoscapeComponent
wheelSensitivity={0.1}
elements={[]}
// elements={createGraphElements(tree, quiz)}
style={{ height: "480px", background: "#F2F3F7" }}
stylesheet={stylesheet}
layout={(lyopts)}
cy={(cy) => {
cyRef.current = cy;
}}
/>
<button onClick={() => {
console.log("NODES____________________________")
cyRef.current?.elements().forEach((ele: any) => {
console.log(ele.data())
})
}}>nodes</button>
<button onClick={() => {
console.log("ELEMENTS____________________________")
console.log(questions)
}}>elements</button>
</>
);
};

@ -2,8 +2,8 @@ import { Box } from "@mui/material"
import { useEffect, useRef, useState } from "react";
import { updateDragQuestionContentId, updateQuestion } from "@root/questions/actions"
import { updateRootContentId } from "@root/quizes/actions"
import { useCurrentQuiz } from "@root/quizes/hooks"
import { useQuestionsStore } from "@root/questions/store"
import { useCurrentQuiz } from "@root/quizes/hooks";
import { enqueueSnackbar } from "notistack";
interface Props {
@ -11,17 +11,15 @@ interface Props {
modalQuestionTargetContentId: string;
}
export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetContentId }: Props) => {
const { dragQuestionContentId } = useQuestionsStore()
const Container = useRef<HTMLDivElement | null>(null);
const quiz = useCurrentQuiz();
console.log(dragQuestionContentId)
const { dragQuestionContentId, questions } = useQuestionsStore()
console.log(questions)
const Container = useRef<HTMLDivElement | null>(null);
const modalOpen = () => setOpenedModalQuestions(true)
const newRootNode = () => {
if (quiz) {
console.log(dragQuestionContentId)
if (dragQuestionContentId) {
updateRootContentId(quiz?.id, dragQuestionContentId)
updateQuestion(dragQuestionContentId, (question) => question.content.rule.parentId = "root")
@ -29,6 +27,7 @@ export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetCon
} else {
enqueueSnackbar("Нет информации о взятом опроснике")
}
}
useEffect(() => {
@ -53,6 +52,7 @@ export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetCon
}, [modalQuestionTargetContentId])
return (
<Box
ref={Container}

@ -15,11 +15,9 @@ interface Edges {
}
export const storeToNodes = (questions: AnyQuizQuestion[]) => {
console.log(questions)
const nodes: Nodes[] = []
const edges: Edges[] = []
questions.forEach((question) => {
console.log(question)
if (question.content.rule.parentId) {
nodes.push({data: {
id: question.content.id,
@ -38,6 +36,5 @@ export const storeToNodes = (questions: AnyQuizQuestion[]) => {
}})
}
})
console.log([...nodes, ...edges])
return [...nodes, ...edges];
}

@ -14,6 +14,8 @@ export const BranchingMap = () => {
const [modalQuestionTargetContentId, setModalQuestionTargetContentId] = useState<string>("")
const [openedModalQuestions, setOpenedModalQuestions] = useState<boolean>(false)
return (
<Box
id="cytoscape-container"

@ -32,35 +32,38 @@ export default function BranchingQuestions() {
const { openedModalSettingsId } = useQuestionsStore();
const [targetQuestion, setTargetQuestion] = useState<AnyQuizQuestion | null>(getQuestionById(openedModalSettingsId) || getQuestionByContentId(openedModalSettingsId))
console.log(targetQuestion)
const [parentQuestion, setParentQuestion] = useState<AnyQuizQuestion | null>(getQuestionByContentId(targetQuestion?.content.rule.parentId))
console.log(parentQuestion)
console.log(parentQuestion.content.rule.default)
console.log(targetQuestion.content.id)
console.log(parentQuestion.content.rule.default === targetQuestion.content.id)
useLayoutEffect(() => {
if (parentQuestion === null) return
if (parentQuestion.content.rule.main.length === 0) updateQuestion(parentQuestion.id, question => question.content.rule.main.push({
next: targetQuestion.content.id,
or: true,
rules: [{
question: parentQuestion.content.id,
answers: []
if (parentQuestion.content.rule.main.length === 0) {
let mutate = JSON.parse(JSON.stringify(parentQuestion))
mutate.content.rule.main = [{
next: targetQuestion.content.id,
or: true,
rules: [{
question: parentQuestion.content.id,
answers: []
}]
}]
}))
setParentQuestion(mutate)
}
})
console.log({targetQuestion, parentQuestion})
if (targetQuestion === null || parentQuestion === null) {
console.log(openedModalSettingsId)
enqueueSnackbar("Невозможно найти данные ветвления для этого вопроса")
return <></>
}
const saveData = () => {
console.log(parentQuestion)
if (parentQuestion !== null) {
updateQuestion(parentQuestion.content.id, question => question.content = parentQuestion.content)
updateQuestion(parentQuestion.content.id, question => question.content = parentQuestion.content)
}
handleClose()
@ -149,11 +152,11 @@ export default function BranchingQuestions() {
marginBottom: "10px",
cursor: "pointer"
}}
onClick={() => {
const mutate = JSON.parse(JSON.stringify(parentQuestion))
mutate.content.rule.main.push(createBranchingRuleMain(targetQuestion.content.id, parentQuestion.content.id))
setParentQuestion(mutate)
}}
onClick={() => {
const mutate = JSON.parse(JSON.stringify(parentQuestion))
mutate.content.rule.main.push(createBranchingRuleMain(targetQuestion.content.id, parentQuestion.content.id))
setParentQuestion(mutate)
}}
>
Добавить условие
</Link>
@ -164,7 +167,7 @@ export default function BranchingQuestions() {
sx={{
margin: 0
}}
checked={parentQuestion.content.rule.default === targetQuestion.id}
checked={parentQuestion.content.rule.default === targetQuestion.content.id}
onClick={() => {
let mutate = JSON.parse(JSON.stringify(parentQuestion))

@ -264,21 +264,43 @@ const NumberInputsType = ({ parentQuestion, targetQuestion, ruleIndex, setParent
(Укажите один или несколько вариантов)
</Typography>
</Box>
{/* <TextField
<TextField
sx={{
marginTop: "20px",
width: "100%"
}}
placeholder="от"
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
onChange={(event: React.FormEvent<HTMLInputElement>) => {
let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion))
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = [(event.target as HTMLInputElement).value.replace(/[^0-9,\s]/g, "")]
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] = Number((event.target as HTMLInputElement).value.replace(/[^0-9,\s]/g, ""))
if (newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[1] === undefined) newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[1] = 0
setParentQuestion(newParentQuestion)
}}
/> */}
/>
{parentQuestion.content.chooseRange &&
<TextField
placeholder="до"
sx={{
marginTop: "20px",
width: "100%"
}}
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[1]}
onChange={(event: React.FormEvent<HTMLInputElement>) => {
let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion))
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[1] = Number((event.target as HTMLInputElement).value.replace(/[^0-9,\s]/g, ""))
if (newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] === undefined) newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] = 0
setParentQuestion(newParentQuestion)
}}
/>
}
@ -338,7 +360,7 @@ const TextInputsType = ({ parentQuestion, targetQuestion, ruleIndex, setParentQu
(Укажите текст, при совпадении с которым пользователь попадёт на этот вопрос)
</Typography>
</Box>
{/* <TextField
<TextField
sx={{
marginTop: "20px",
width: "100%"
@ -351,7 +373,7 @@ const TextInputsType = ({ parentQuestion, targetQuestion, ruleIndex, setParentQu
setParentQuestion(newParentQuestion)
}}
/> */}
/>

@ -23,7 +23,6 @@ type AnyQuestion = UntypedQuizQuestion | AnyTypedQuizQuestion
export const QuestionsList = () => {
const { questions } = useQuestionsStore()
console.log(questions)
return (
<Box sx={{ padding: "15px" }}>

@ -1,24 +1,30 @@
import { useParams } from "react-router-dom";
import { Box, Modal, Button, Typography } from "@mui/material";
import { useQuestionsStore } from "@root/questions/store";
import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "@model/questionTypes/shared";
type AnyQuestion = UntypedQuizQuestion | AnyTypedQuizQuestion
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
interface Props {
openedModalQuestions: boolean;
setModalQuestionTargetContentId: (contentId:string) => void;
setOpenedModalQuestions: (open:boolean) => void;
setModalQuestionTargetContentId: (contentId: string) => void;
setOpenedModalQuestions: (open: boolean) => void;
setModalQuestionParentContentId: (open: string) => void;
}
export const BranchingQuestionsModal = ({ openedModalQuestions, setOpenedModalQuestions, setModalQuestionTargetContentId}:Props) => {
const quizId = Number(useParams().quizId);
export const BranchingQuestionsModal = ({
openedModalQuestions,
setOpenedModalQuestions,
setModalQuestionTargetContentId,
setModalQuestionParentContentId
}: Props) => {
const { questions } = useQuestionsStore();
const handleClose = () => {
setOpenedModalQuestions(false);
};
const typedQuestions: AnyTypedQuizQuestion[] = questions.filter(
(question) => question.type && !question.content.rule.parentId
) as AnyTypedQuizQuestion[];
return (
<Modal open={openedModalQuestions} onClose={handleClose}>
<Box
@ -37,14 +43,11 @@ export const BranchingQuestionsModal = ({ openedModalQuestions, setOpenedModalQu
}}
>
<Box sx={{ margin: "0 auto", maxWidth: "350px" }}>
{questions.filter((q:AnyQuestion) => {
console.log(q.content)
if (q.content === null) return true
return (q.type && !q.content.rule.parentId)}).map((question: AnyTypedQuizQuestion, index:number) => (
{typedQuestions.map((question) => (
<Button
key={question.content.id}
onClick={() => {
setModalQuestionTargetContentId(question.content.id)
setModalQuestionTargetContentId(question.content.id);
handleClose();
}}
sx={{

@ -20,6 +20,7 @@ import { HideIcon } from "../../assets/icons/questionsPage/hideIcon";
import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
import { updateOpenedModalSettingsId } from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import {enqueueSnackbar} from "notistack";
@ -38,6 +39,7 @@ export default function ButtonsOptions({
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920));
const quiz = useCurrentQuiz();
const openedModal = () => {
updateOpenedModalSettingsId(question.id)
@ -45,6 +47,7 @@ export default function ButtonsOptions({
const handleClickBranching = (_, value) => {
const parentId = question.content.rule.parentId
console.log(parentId)
console.log(parentId.length)
if (parentId.length === 0 ){
return enqueueSnackbar("Вопрос не учавствует в ветвлении")
@ -293,7 +296,7 @@ export default function ButtonsOptions({
// deleteTimeoutId: newTimeoutId,
// });
deleteQuestion(question.id);
deleteQuestion(question.id, quiz.id);
}}
data-cy="delete-question"
>

@ -26,6 +26,7 @@ import { updateOpenedModalSettingsId } from "@root/questions/actions";
import { updateOpenBranchingPanel } from "@root/questions/actions";
import {useQuestionsStore} from "@root/questions/store";
import { enqueueSnackbar } from "notistack";
import { useCurrentQuiz } from "@root/quizes/hooks";
interface Props {
@ -46,6 +47,7 @@ export default function ButtonsOptionsAndPict({
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isIconMobile = useMediaQuery(theme.breakpoints.down(1050));
const {openBranchingPanel} = useQuestionsStore.getState()
const quiz = useCurrentQuiz();
useEffect(() => {
if (question.deleteTimeoutId) {
@ -55,6 +57,7 @@ export default function ButtonsOptionsAndPict({
const handleClickBranching = (_, value) => {
const parentId = question.content.rule.parentId
console.log(parentId)
console.log(parentId.length)
if (parentId.length === 0 ) {
return enqueueSnackbar("Вопрос не учавствует в ветвлении")
@ -336,7 +339,7 @@ export default function ButtonsOptionsAndPict({
// deleteTimeoutId: newTimeoutId,
// });
deleteQuestion(question.id);
deleteQuestion(question.id, quiz?.id);
}}
data-cy="delete-question"
>

@ -39,7 +39,7 @@ import SwitchQuestionsPage from "../SwitchQuestionsPage";
import { ChooseAnswerModal } from "./ChooseAnswerModal";
import TypeQuestions from "../TypeQuestions";
import { QuestionType } from "@model/question/question";
import { useCurrentQuiz } from "@root/quizes/hooks"
interface Props {
question: AnyTypedQuizQuestion | UntypedQuizQuestion;
@ -55,6 +55,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const anchorRef = useRef(null);
const quiz = useCurrentQuiz();
const setTitle = useDebouncedCallback((title) => {
const updateQuestionFn = question.type === null ? updateUntypedQuestion : updateQuestion;
@ -253,7 +254,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
// deleteTimeoutId: newTimeoutId,
// });
deleteQuestion(question.id);
deleteQuestion(question.id, quiz.id);
}}
data-cy="delete-question"
>

@ -27,6 +27,7 @@ export default function QuestionsPage() {
const openBranchingPanel = useQuestionsStore.getState()
if (!quiz) return null;
return (
<>
<Box

@ -1,9 +1,12 @@
import { useState, useEffect } from "react";
import { Box, Typography, Button, useTheme } from "@mui/material";
import { useQuizViewStore, getAnswersByQuestionId } from "@root/quizView";
import { useQuizViewStore } from "@root/quizView";
import type { AnyTypedQuizQuestion, QuizQuestionBase } from "../../model/questionTypes/shared";
import type {
AnyTypedQuizQuestion,
QuizQuestionBase,
} from "../../model/questionTypes/shared";
import { getQuestionByContentId } from "@root/questions/actions";
import { enqueueSnackbar } from "notistack";
@ -12,10 +15,7 @@ type FooterProps = {
question: QuizQuestionBase;
};
export const Footer = ({
setCurrentQuestion,
question,
}: FooterProps) => {
export const Footer = ({ setCurrentQuestion, question }: FooterProps) => {
const [disabledQuestionsId, setDisabledQuestionsId] = useState<Set<string>>(
new Set()
);
@ -24,60 +24,67 @@ export const Footer = ({
const followPreviousStep = () => {
if (question?.content.rule.parentId !== "root") {
const parent = getQuestionByContentId(question?.content.rule.parentId)
const parent = getQuestionByContentId(question?.content.rule.parentId);
if (parent) {
setCurrentQuestion(parent)
setCurrentQuestion(parent);
} else {
enqueueSnackbar("не могу получить предыдущий вопрос")
enqueueSnackbar("не могу получить предыдущий вопрос");
}
} else {
enqueueSnackbar("вы находитесь на первом вопросе")
enqueueSnackbar("вы находитесь на первом вопросе");
}
};
const followNextStep = () => {
const answers = getAnswersByQuestionId(question.content.id) || []
console.log(answers)
if (answers) {
if (answers.length) {
let readyBeNextQuestion = "";
let readyBeNextQuestion = ""
question.content.rule.main.forEach(({ next, rules }) => {
console.log({ next, rules })
console.log({ next, rules });
console.log("[storeAnswers] ", rules[0].answers)
console.log("[answers.answer] ", [answers.answer])
console.log("[storeAnswers] ", rules[0].answers);
console.log("[answers.answer] ", [answers.at(-1)?.answer]);
let longerArray = Math.max(rules[0].answers.length, [answers.answer].length)
let longerArray = Math.max(
rules[0].answers.length,
[answers.at(-1)?.answer].length
);
for (var i = 0; i < longerArray; i++) // Цикл по всем эле­мен­там бОльшего массива
if (rules[0].answers[i] !== [answers.answer][i]) readyBeNextQuestion = next; // Ес­ли хоть один эле­мент от­ли­ча­ет­ся, мас­си­вы не рав­ны
})
if (readyBeNextQuestion) {
console.log("мы нашли совпадение в " + readyBeNextQuestion)
const nextQuestion = getQuestionByContentId(readyBeNextQuestion)
console.log("next question ", nextQuestion)
if (nextQuestion) {
console.log("я устанавливаю следующий вопрос " + question.title)
setCurrentQuestion(nextQuestion)
return
} else {
enqueueSnackbar("не могу получить последующий вопрос")
}
} else {
const nextQuestion = getQuestionByContentId(question.content.rule.default)
console.log("я устанавливаю дефолтный вопрос")
if (nextQuestion) {
setCurrentQuestion(nextQuestion)
} else {
enqueueSnackbar("не могу получить последующий вопрос (дефолтный)")
for (
var i = 0;
i < longerArray;
i++ // Цикл по всем эле­мен­там бОльшего массива
) {
if (rules[0].answers[i] === answers.at(-1)?.answer) {
readyBeNextQuestion = next; // Ес­ли хоть один эле­мент от­ли­ча­ет­ся, мас­си­вы не рав­ны
}
}
});
if (readyBeNextQuestion) {
console.log("мы нашли совпадение в " + readyBeNextQuestion);
const nextQuestion = getQuestionByContentId(readyBeNextQuestion);
console.log("next question ", nextQuestion);
if (nextQuestion) {
console.log("я устанавливаю следующий вопрос " + question.title);
setCurrentQuestion(nextQuestion);
return;
} else {
enqueueSnackbar("не могу получить последующий вопрос");
}
} else {
const nextQuestion = getQuestionByContentId(
question.content.rule.default
);
console.log("я устанавливаю дефолтный вопрос");
if (nextQuestion) {
setCurrentQuestion(nextQuestion);
} else {
enqueueSnackbar("не могу получить последующий вопрос (дефолтный)");
}
}
}
};
return (

@ -10,6 +10,8 @@ import { nanoid } from "nanoid";
import { enqueueSnackbar } from "notistack";
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
import { RequestQueue } from "../../utils/requestQueue";
import { updateRootContentId } from "@root/quizes/actions"
import { useCurrentQuiz } from "@root/quizes/hooks"
import { QuestionsStore, useQuestionsStore } from "./store";
@ -136,22 +138,20 @@ export const updateQuestion = (
// clearTimeout(requestTimeoutId);
// requestTimeoutId = setTimeout(() => {
requestQueue.enqueue(async () => {
const q = useQuestionsStore.getState().questions.find(q => q.id === questionId) || useQuestionsStore.getState().questions.find(q => q.content.id === questionId);
console.log("мы пытаемся найти вопрос ")
if (!q) return;
if (q.type === null) throw new Error("Cannot send update request for untyped question");
console.log(q.title)
requestQueue.enqueue(async () => {
const q = useQuestionsStore.getState().questions.find(q => q.id === questionId) || useQuestionsStore.getState().questions.find(q => q.content.id === questionId);
if (!q) return;
if (q.type === null) throw new Error("Cannot send update request for untyped question");
const response = await questionApi.edit(questionToEditQuestionRequest(q));
const response = await questionApi.edit(questionToEditQuestionRequest(q));
setQuestionBackendId(questionId, response.updated);
}).catch(error => {
if (isAxiosCanceledError(error)) return;
setQuestionBackendId(questionId, response.updated);
}).catch(error => {
if (isAxiosCanceledError(error)) return;
devlog("Error editing question", { error, questionId });
enqueueSnackbar("Не удалось сохранить вопрос");
});
devlog("Error editing question", { error, questionId });
enqueueSnackbar("Не удалось сохранить вопрос");
});
// }, REQUEST_DEBOUNCE);
};
@ -261,8 +261,10 @@ export const changeQuestionType = (
type: QuestionType,
) => {
updateQuestion(questionId, question => {
const oldId = question.content.id
question.type = type;
question.content = defaultQuestionByType[type].content;
question.content.id = oldId
});
};
@ -273,7 +275,7 @@ export const createTypedQuestion = async (
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
if (question.type !== null) throw new Error("Cannot upgrade already typed question");
try {
const createdQuestion = await questionApi.create({
quiz_id: question.quizId,
@ -302,7 +304,8 @@ export const createTypedQuestion = async (
}
});
export const deleteQuestion = async (questionId: string) => requestQueue.enqueue(async () => {
export const deleteQuestion = async (questionId: string, quizId: string) => requestQueue.enqueue(async () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
@ -312,7 +315,12 @@ export const deleteQuestion = async (questionId: string) => requestQueue.enqueue
}
try {
console.log(question.content.rule.parentId)
await questionApi.delete(question.backendId);
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quizId, "")
clearRoleForAll()
}
removeQuestion(questionId);
} catch (error) {
@ -325,9 +333,11 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
const frontId = nanoid();
if (question.type === null) {
const copiedQuestion = structuredClone(question);
copiedQuestion.id = nanoid();
copiedQuestion.id = frontId
copiedQuestion.content.id = frontId
setProducedState(state => {
state.questions.push(copiedQuestion);
@ -369,7 +379,7 @@ function setProducedState<A extends string | { type: unknown; }>(
export const cleardragQuestionContentId = () => {
useQuestionsStore.setState({dragQuestionContentId: null});
useQuestionsStore.setState({ dragQuestionContentId: null });
};
export const getQuestionById = (questionId: string | null) => {
@ -377,17 +387,28 @@ export const getQuestionById = (questionId: string | null) => {
return useQuestionsStore.getState().questions.find(q => q.id === questionId) || null;
};
export const getQuestionByContentId = (questionContentId: string | null) => {
console.log("questionContentId " + questionContentId)
if (questionContentId === null) return null;
return useQuestionsStore.getState().questions.find(q => {
console.log(q.content.id)
console.log(q.content.id === questionContentId)
return ( q.content.id === questionContentId)}) || null;
return (q.content.id === questionContentId)
}) || null;
};
export const updateOpenedModalSettingsId = (id?: string) => useQuestionsStore.setState({openedModalSettingsId: id ? id : null});
export const updateOpenedModalSettingsId = (id?: string) => useQuestionsStore.setState({ openedModalSettingsId: id ? id : null });
export const updateDragQuestionContentId = (contentId?: string) => {
console.log("contentId " + contentId)
useQuestionsStore.setState({dragQuestionContentId: contentId ? contentId : null});
};
useQuestionsStore.setState({ dragQuestionContentId: contentId ? contentId : null });
}
export const clearRoleForAll = () => {
const { questions } = useQuestionsStore.getState()
questions.forEach(question => {
if (question.type !== null && (question.content.rule.main.length > 0 || question.content.rule.default.length > 0 || question.content.rule.parentId.length > 0)) {
updateQuestion(question.content.id, question => {
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.default = ""
})
}
});
}
export const updateOpenBranchingPanel = (value: boolean) => useQuestionsStore.setState({openBranchingPanel: !value});

@ -4,7 +4,7 @@ import { devtools } from "zustand/middleware";
export type QuestionsStore = {
questions: (AnyTypedQuizQuestion | UntypedQuizQuestion);
questions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[];
openedModalSettingsId: string | null;
dragQuestionContentId: string | null;
openBranchingPanel: boolean;

@ -33,9 +33,3 @@ export const updateAnswer = (questionId: string, answer: string) => {
useQuizViewStore.setState({ answers });
};
export const getAnswersByQuestionId = (questionId: string) => {
if (questionId === null) return null;
const answers = [...useQuizViewStore.getState().answers];
return answers.find(a => a.questionId === questionId) || null;
}

@ -10,6 +10,7 @@ import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
import { RequestQueue } from "../../utils/requestQueue";
import { QuizStore, useQuizStore } from "./store";
import { createUntypedQuestion } from "@root/questions/actions";
import { useCurrentQuiz } from "./hooks"
export const setEditQuizId = (quizId: number | null) => setProducedState(state => {
@ -127,6 +128,7 @@ export const updateQuiz = async (
requestTimeoutId = setTimeout(async () => {
requestQueue.enqueue(async () => {
const quiz = useQuizStore.getState().quizes.find(q => q.id === quizId);
console.log("измененный квиз", quiz)
if (!quiz) return;
const response = await quizApi.edit(quizToEditQuizRequest(quiz));
@ -178,10 +180,11 @@ export const deleteQuiz = async (quizId: string) => requestQueue.enqueue(async (
export const updateRootContentId = (quizId: string, id:string) => updateQuiz(
quizId,
quiz => {
quiz.config.haveRoot = id;
quiz.config.haveRoot = id
},
);
// TODO copy quiz
export const uploadQuizImage = async (

1249
yarn.lock

File diff suppressed because it is too large Load Diff