добавление иконок, модалка выбора вопроса по клику

This commit is contained in:
Nastya 2023-12-01 22:56:13 +03:00
parent 2b38c15e3e
commit 7dc083907f
22 changed files with 462 additions and 144 deletions

@ -12,6 +12,7 @@ export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id" | "backendId"> = {
deleted: false, deleted: false,
deleteTimeoutId: 0, deleteTimeoutId: 0,
content: { content: {
id: "",
hint: { hint: {
text: "", text: "",
video: "", video: "",

@ -46,16 +46,18 @@ export interface RawQuestion {
export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyTypedQuizQuestion { export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyTypedQuizQuestion {
let content = defaultQuestionByType[rawQuestion.type].content; let content = defaultQuestionByType[rawQuestion.type].content;
const frontId = nanoid()
try { try {
content = JSON.parse(rawQuestion.content); content = JSON.parse(rawQuestion.content);
if (content.id.length === 0 || content.id.length === undefined) content.id = frontId
} catch (error) { } catch (error) {
console.warn("Cannot parse question content from string, using default content", error); console.warn("Cannot parse question content from string, using default content", error);
} }
return { return {
backendId: rawQuestion.id, backendId: rawQuestion.id,
id: nanoid(), id: frontId,
description: rawQuestion.description, description: rawQuestion.description,
page: rawQuestion.page, page: rawQuestion.page,
quizId: rawQuestion.quiz_id, quizId: rawQuestion.quiz_id,

@ -7,6 +7,7 @@ import type {
export interface QuizQuestionDate extends QuizQuestionBase { export interface QuizQuestionDate extends QuizQuestionBase {
type: "date"; type: "date";
content: { content: {
id: string;
/** Чекбокс "Необязательный вопрос" */ /** Чекбокс "Необязательный вопрос" */
required: boolean; required: boolean;
/** Чекбокс "Внутреннее название вопроса" */ /** Чекбокс "Внутреннее название вопроса" */

@ -8,6 +8,7 @@ import type {
export interface QuizQuestionEmoji extends QuizQuestionBase { export interface QuizQuestionEmoji extends QuizQuestionBase {
type: "emoji"; type: "emoji";
content: { content: {
id: string;
/** Чекбокс "Можно несколько" */ /** Чекбокс "Можно несколько" */
multi: boolean; multi: boolean;
/** Чекбокс "Вариант "свой ответ"" */ /** Чекбокс "Вариант "свой ответ"" */

@ -17,6 +17,7 @@ export type UploadFileType = keyof typeof UPLOAD_FILE_TYPES_MAP;
export interface QuizQuestionFile extends QuizQuestionBase { export interface QuizQuestionFile extends QuizQuestionBase {
type: "file"; type: "file";
content: { content: {
id: string;
/** Чекбокс "Необязательный вопрос" */ /** Чекбокс "Необязательный вопрос" */
required: boolean; required: boolean;
/** Чекбокс "Внутреннее название вопроса" */ /** Чекбокс "Внутреннее название вопроса" */

@ -8,6 +8,7 @@ import type {
export interface QuizQuestionImages extends QuizQuestionBase { export interface QuizQuestionImages extends QuizQuestionBase {
type: "images"; type: "images";
content: { content: {
id: string;
/** Чекбокс "Вариант "свой ответ"" */ /** Чекбокс "Вариант "свой ответ"" */
own: boolean; own: boolean;
/** Чекбокс "Можно несколько" */ /** Чекбокс "Можно несколько" */

@ -7,6 +7,7 @@ import type {
export interface QuizQuestionNumber extends QuizQuestionBase { export interface QuizQuestionNumber extends QuizQuestionBase {
type: "number"; type: "number";
content: { content: {
id: string;
/** Чекбокс "Необязательный вопрос" */ /** Чекбокс "Необязательный вопрос" */
required: boolean; required: boolean;
/** Чекбокс "Внутреннее название вопроса" */ /** Чекбокс "Внутреннее название вопроса" */

@ -7,6 +7,7 @@ import type {
export interface QuizQuestionPage extends QuizQuestionBase { export interface QuizQuestionPage extends QuizQuestionBase {
type: "page"; type: "page";
content: { content: {
id: string;
/** Чекбокс "Внутреннее название вопроса" */ /** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean; innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */ /** Поле "Внутреннее название вопроса" */

@ -7,6 +7,7 @@ import type {
export interface QuizQuestionRating extends QuizQuestionBase { export interface QuizQuestionRating extends QuizQuestionBase {
type: "rating"; type: "rating";
content: { content: {
id: string;
/** Чекбокс "Необязательный вопрос" */ /** Чекбокс "Необязательный вопрос" */
required: boolean; required: boolean;
/** Чекбокс "Внутреннее название вопроса" */ /** Чекбокс "Внутреннее название вопроса" */

@ -8,6 +8,7 @@ import type {
export interface QuizQuestionSelect extends QuizQuestionBase { export interface QuizQuestionSelect extends QuizQuestionBase {
type: "select"; type: "select";
content: { content: {
id: string;
/** Чекбокс "Можно несколько" */ /** Чекбокс "Можно несколько" */
multi: boolean; multi: boolean;
/** Чекбокс "Необязательный вопрос" */ /** Чекбокс "Необязательный вопрос" */

@ -62,6 +62,7 @@ export interface QuizQuestionBase {
deleted: boolean; deleted: boolean;
deleteTimeoutId: number; deleteTimeoutId: number;
content: { content: {
id: string;
hint: QuestionHint; hint: QuestionHint;
rule: QuestionBranchingRule; rule: QuestionBranchingRule;
back: string; back: string;

@ -7,6 +7,7 @@ import type {
export interface QuizQuestionText extends QuizQuestionBase { export interface QuizQuestionText extends QuizQuestionBase {
type: "text"; type: "text";
content: { content: {
id: string;
placeholder: string; placeholder: string;
/** Чекбокс "Внутреннее название вопроса" */ /** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean; innerNameCheck: boolean;

@ -8,6 +8,7 @@ import type {
export interface QuizQuestionVariant extends QuizQuestionBase { export interface QuizQuestionVariant extends QuizQuestionBase {
type: "variant"; type: "variant";
content: { content: {
id: string;
/** Чекбокс "Длинный текстовый ответ" */ /** Чекбокс "Длинный текстовый ответ" */
largeCheck: boolean; largeCheck: boolean;
/** Чекбокс "Можно несколько" */ /** Чекбокс "Можно несколько" */

@ -8,6 +8,7 @@ import type {
export interface QuizQuestionVarImg extends QuizQuestionBase { export interface QuizQuestionVarImg extends QuizQuestionBase {
type: "varimg"; type: "varimg";
content: { content: {
id: string;
/** Чекбокс "Вариант "свой ответ"" */ /** Чекбокс "Вариант "свой ответ"" */
own: boolean; own: boolean;
/** Чекбокс "Внутреннее название вопроса" */ /** Чекбокс "Внутреннее название вопроса" */

@ -5,7 +5,7 @@ import popper from "cytoscape-popper";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import { AnyQuizQuestion } from "@model/questionTypes/shared" import { AnyQuizQuestion } from "@model/questionTypes/shared"
import { useQuestionsStore } from "@root/questions/store"; import { useQuestionsStore } from "@root/questions/store";
import { clearDragQuestionId, getQuestionById, updateQuestion, updateOpenedModalSettingsId } from "@root/questions/actions"; import { cleardragQuestionContentId, getQuestionById, updateQuestion, updateOpenedModalSettingsId, getQuestionByContentId } from "@root/questions/actions";
import { storeToNodes } from "./helper"; import { storeToNodes } from "./helper";
@ -92,11 +92,26 @@ const stylesheet: Stylesheet[] = [
Cytoscape.use(popper); Cytoscape.use(popper);
export const CsComponent = () => { interface Props {
modalQuestionParentContentId: string;
modalQuestionTargetContentId: string;
setOpenedModalQuestions: (open:boolean) => void;
setModalQuestionParentContentId: (id:string) => void;
setModalQuestionTargetContentId: (id:string) => void;
}
export const CsComponent = ({
modalQuestionParentContentId,
modalQuestionTargetContentId,
setOpenedModalQuestions,
setModalQuestionParentContentId,
setModalQuestionTargetContentId
}:Props) => {
console.log("Я существую") console.log("Я существую")
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
const { dragQuestionId, questions } = useQuestionsStore() const { dragQuestionContentId, questions } = useQuestionsStore()
const [startCreate, setStartCreate] = useState(""); const [startCreate, setStartCreate] = useState("");
const [startRemove, setStartRemove] = useState(""); const [startRemove, setStartRemove] = useState("");
@ -106,73 +121,251 @@ export const CsComponent = () => {
const crossesContainer = useRef<HTMLDivElement | null>(null); const crossesContainer = useRef<HTMLDivElement | null>(null);
const gearsContainer = useRef<HTMLDivElement | null>(null); const gearsContainer = useRef<HTMLDivElement | null>(null);
useEffect(() =>{
if (modalQuestionTargetContentId.length !== 0 && modalQuestionParentContentId.length !== 0) {
console.log("был выбран вопрос " + modalQuestionTargetContentId)
}
}, [modalQuestionTargetContentId])
const addNode = ({ parentNodeId }: { parentNodeId: string }) => { const addNode = ({ parentNodeContentId }: { parentNodeContentId: string }) => {
const cy = cyRef?.current const cy = cyRef?.current
const parentNodeChildren = cy?.$('edge[source = "' + parentNodeId + '"]').length const parentNodeChildren = cy?.$('edge[source = "' + parentNodeContentId + '"]')?.length
const targetQuestion = { ...getQuestionById(dragQuestionId) } as AnyQuizQuestion const targetQuestion = { ...getQuestionByContentId(dragQuestionContentId) } as AnyQuizQuestion
console.log(parentNodeChildren, parentNodeId) console.log(parentNodeChildren, parentNodeContentId)
if (targetQuestion && targetQuestion && parentNodeId && parentNodeChildren !== undefined) { if (targetQuestion && targetQuestion && parentNodeContentId && parentNodeChildren !== undefined) {
console.log(targetQuestion, targetQuestion, parentNodeContentId, parentNodeChildren)
cy?.add([ cy?.add([
{ {
data: { data: {
id: targetQuestion.id, id: targetQuestion.content.id,
label: targetQuestion.title || "noname" label: targetQuestion.title || "noname"
} }
}, },
{ {
data: { data: {
source: parentNodeId, source: parentNodeContentId,
target: targetQuestion.id target: targetQuestion.content.id
} }
} }
]) ])
// clearDataAfterAddNode({ parentNodeContentId, targetQuestion, parentNodeChildren })
}
console.log(dragQuestionContentId)
}
const clearDataAfterAddNode = ({ parentNodeContentId, targetQuestion, parentNodeChildren }: { parentNodeContentId: string, targetQuestion: AnyQuizQuestion, parentNodeChildren:number }) => {
//предупреждаем добавленный вопрос о том, кто его родитель
updateQuestion(targetQuestion.id, question => {
question.content.rule.parentId = parentNodeContentId
question.content.rule.main = []
})
//Если детей больше 1 - предупреждаем стор вопросов об открытии модалки ветвления //Если детей больше 1 - предупреждаем стор вопросов об открытии модалки ветвления
if (parentNodeChildren > 1) { if (parentNodeChildren > 1) {
updateOpenedModalSettingsId(parentNodeId) updateOpenedModalSettingsId(parentNodeContentId)
} else { } else {
//Если ребёнок первый - добавляем его родителю как дефолтный //Если ребёнок первый - добавляем его родителю как дефолтный
updateQuestion(parentNodeId, question => question.content.rule.default = targetQuestion.id) updateQuestion(parentNodeContentId, question => question.content.rule.default = targetQuestion.content.id)
} }
}
const removeNode = ({ targetNodeContentId }: { targetNodeContentId: string }) => {
const cy = cyRef?.current
//получить можно только дочерние ноды
console.log(cy?.$('#'+targetNodeContentId))
console.log(cy?.$('#'+targetNodeContentId)?.data())
//предупреждаем добавленный вопрос о том, кто его родитель
updateQuestion(targetQuestion.id, question => question.content.rule.parentId = parentNodeId)
} }
const clearDataAfterRemoveNode = () => {
}
console.log(dragQuestionId)
}
useEffect(() => { useEffect(() => {
if (startCreate) { if (startCreate) {
addNode({ parentNodeId: startCreate }); addNode({ parentNodeContentId: startCreate });
clearDragQuestionId() cleardragQuestionContentId()
setStartCreate(""); setStartCreate("");
} }
}, [startCreate]); }, [startCreate]);
useEffect(() => { useEffect(() => {
if (startRemove) { if (startRemove) {
// removeNode(quiz, startRemove); // removeNode({ targetNodeContentId: startRemove });
setStartRemove(""); setStartRemove("");
} }
}, [startRemove]); }, [startRemove]);
useEffect(() => { useEffect(() => {
document.querySelector("#root")?.addEventListener("mouseup", clearDragQuestionId); document.querySelector("#root")?.addEventListener("mouseup", cleardragQuestionContentId);
const cy = cyRef.current; const cy = cyRef.current;
cy?.add(storeToNodes(questions)) // cy?.add(storeToNodes(questions))
cy?.layout(ly).run() cy?.add(
[
{
"data": {
"id": "1",
"label": "нет имени"
},
"position": {
"x": 250,
"y": 200
}
},
{
"data": {
"id": "1 2",
"label": "нет имени"
},
"position": {
"x": 500,
"y": 300
}
},
{
"data": {
"id": "1 3",
"label": "нет имени"
},
"position": {
"x": 500,
"y": 400
}
},
{
"data": {
"id": "1 2 4",
"label": "нет имени"
},
"position": {
"x": 750,
"y": 100
}
},
{
"data": {
"id": "1 2 6",
"label": "нет имени"
},
"position": {
"x": 750,
"y": 500
}
},
{
"data": {
"id": "1 3 5",
"label": "нет имени"
},
"position": {
"x": 750,
"y": 300
}
},
{
"data": {
"id": "1 3 7",
"label": "нет имени"
},
"position": {
"x": 750,
"y": 500
}
},
{
"data": {
"id": "1 2 6 9867874",
"label": "нет имени"
},
"position": {
"x": 1000,
"y": 300
}
},
{
"data": {
"id": "1 2 6 7398789",
"label": "нет имени"
},
"position": {
"x": 1000,
"y": 500
}
},
{
"data": {
"id": "1 2 6 9484789",
"label": "нет имени"
},
"position": {
"x": 1000,
"y": 700
}
},
{
"data": {
"source": "1",
"target": "1 2"
}
},
{
"data": {
"source": "1",
"target": "1 3"
}
},
{
"data": {
"source": "1 2",
"target": "1 2 4"
}
},
{
"data": {
"source": "1 2",
"target": "1 2 6"
}
},
{
"data": {
"source": "1 3",
"target": "1 3 5"
}
},
{
"data": {
"source": "1 3",
"target": "1 3 7"
}
},
{
"data": {
"source": "1 2 6",
"target": "1 2 6 9867874"
}
},
{
"data": {
"source": "1 2 6",
"target": "1 2 6 7398789"
}
},
{
"data": {
"source": "1 2 6",
"target": "1 2 6 9484789"
}
}
]
)
//cy?.layout(ly).run()
return () => { return () => {
document.querySelector("#root")?.removeEventListener("mouseup", clearDragQuestionId); document.querySelector("#root")?.removeEventListener("mouseup", cleardragQuestionContentId);
layoutsContainer.current?.remove(); layoutsContainer.current?.remove();
plusesContainer.current?.remove(); plusesContainer.current?.remove();
crossesContainer.current?.remove(); crossesContainer.current?.remove();
@ -206,85 +399,11 @@ export const CsComponent = () => {
data.id && removeButtons(data.id); data.id && removeButtons(data.id);
}) })
initialPopperIcons(e) initialPopperIcons(e)
console.log('ready', e) // console.log('ready', e)
console.log(e.cy.nodes()) // console.log(e.cy.nodes())
console.log(e.cy.nodes().data()) // console.log(e.cy.nodes().data())
} }
const [ly, setly] = useState<any>({
name: 'preset',
positions: (e) => {
console.log("идёт расчёт позиции")
const id = e.id()
const incomming = e.cy().edges(`[target="${id}"]`)
const layer = 0
e.removeData('lastChild')
if (incomming.length === 0) {
const children = e.cy().edges(`[source="${id}"]`)
e.data('layer', layer)
e.data('children', children.targets().length)
const queue = []
children.forEach(n => {
queue.push({ task: n.target(), layer: layer + 1 })
})
while (queue.length) {
const task = queue.pop()
task.task.data('layer', task.layer)
const children = e.cy().edges(`[source="${task.task.id()}"]`)
task.task.data('children', children.targets().length)
if (children.length !== 0) {
children.forEach(n => queue.push({ task: n.target(), layer: task.layer + 1 }))
}
}
queue.push({ parent: e, children: children.targets() })
while (queue.length) {
const task = queue.pop()
if (task.children.length === 0) {
task.parent.data('subtreeWidth', task.parent.height())
continue
}
const unprocessed = task?.children.filter(e => e.data('subtreeWidth') === undefined)
if (unprocessed.length !== 0) {
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))
}
return { x: 200 * e.data('layer'), y: 0 }
} else {
const parent = e.cy().edges(`[target="${e.id()}"]`)[0].source()
// console.log(e.data('subtreeWidth'), e.id(),(parent.data('children')-1) )
const wing = parent.data('subtreeWidth') / 2
const lastOffset = parent.data('lastChild')
const step = wing * 2 / (parent.data('children') - 1)
if (e.data('layer') === 1)
// console.log(e.data('subtreeWidth'), e.id(), (parent.data('children') - 1), step, wing, e.data('layer'), parent.id(), lastOffset)
//e.removeData('subtreeWidth')
//console.log('poss', e.id(), 'children', parent.data('children'),'lo', lastOffset, 'v', wing)
if (lastOffset !== undefined) {
parent.data('lastChild', lastOffset + step)
return { x: 200 * e.data('layer'), y: (lastOffset + step) || 1 }
} else {
parent.data('lastChild', parent.position().y - wing)
return { x: 200 * e.data('layer'), y: (parent.position().y - wing) || 200 }
}
}
}, // 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: undefined, // 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 true; }, // 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
// stop: (e) => console.log('stop', e), // callback on layoutstop
transform: function (node, position) { return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
})
const initialPopperIcons = (e) => { const initialPopperIcons = (e) => {
const cy = e.cy const cy = e.cy
@ -325,7 +444,7 @@ export const CsComponent = () => {
.toArray() .toArray()
?.forEach((item) => { ?.forEach((item) => {
const node = item as NodeSingularWithPopper; const node = item as NodeSingularWithPopper;
console.log(node) //console.log(node)
const layoutsPopper = node.popper({ const layoutsPopper = node.popper({
popper: { popper: {
@ -347,7 +466,7 @@ export const CsComponent = () => {
// layoutElement.addEventListener("mouseup", () =>{} // layoutElement.addEventListener("mouseup", () =>{}
// // setStartCreate(node.id()) // // setStartCreate(node.id())
// ); // );
console.log(layoutsContainer.current) //console.log(layoutsContainer.current)
layoutsContainer.current?.appendChild(layoutElement); layoutsContainer.current?.appendChild(layoutElement);
return layoutElement; return layoutElement;
@ -402,7 +521,7 @@ export const CsComponent = () => {
// setStartRemove(node.id()) // setStartRemove(node.id())
// ); // );
console.log(crossElement) //console.log(crossElement)
return crossElement; return crossElement;
}, },
}); });
@ -413,10 +532,10 @@ export const CsComponent = () => {
modifiers: [{ name: "flip", options: { boundary: node } }], modifiers: [{ name: "flip", options: { boundary: node } }],
}, },
content: ([item]) => { content: ([item]) => {
console.log(item.data()) //console.log(item.data())
const itemId = item.id(); const itemId = item.id();
console.log(item.data()) //console.log(item.data())
console.log(item.data().lastChild) //console.log(item.data().lastChild)
if (item.data().lastChild === NaN || item.data().lastChild === undefined) { if (item.data().lastChild === NaN || item.data().lastChild === undefined) {
return; return;
} }
@ -517,7 +636,85 @@ export const CsComponent = () => {
// elements={createGraphElements(tree, quiz)} // elements={createGraphElements(tree, quiz)}
style={{ height: "480px", background: "#F2F3F7" }} style={{ height: "480px", background: "#F2F3F7" }}
stylesheet={stylesheet} stylesheet={stylesheet}
layout={ly} layout={{
name: 'preset',
positions: (e) => {
//console.log("идёт расчёт позиции")
const id = e.id()
const incomming = e.cy().edges(`[target="${id}"]`)
const layer = 0
e.removeData('lastChild')
if (incomming.length === 0) {
const children = e.cy().edges(`[source="${id}"]`)
e.data('layer', layer)
e.data('children', children.targets().length)
const queue = []
children.forEach(n => {
queue.push({ task: n.target(), layer: layer + 1 })
})
while (queue.length) {
const task = queue.pop()
task.task.data('layer', task.layer)
const children = e.cy().edges(`[source="${task.task.id()}"]`)
task.task.data('children', children.targets().length)
if (children.length !== 0) {
children.forEach(n => queue.push({ task: n.target(), layer: task.layer + 1 }))
}
}
queue.push({ parent: e, children: children.targets() })
while (queue.length) {
const task = queue.pop()
if (task.children.length === 0) {
task.parent.data('subtreeWidth', task.parent.height())
continue
}
const unprocessed = task?.children.filter(e => e.data('subtreeWidth') === undefined)
if (unprocessed.length !== 0) {
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))
}
return { x: 200 * e.data('layer'), y: 0 }
} else {
const parent = e.cy().edges(`[target="${e.id()}"]`)[0].source()
//console.log('batya', parent)
// console.log(e.data('subtreeWidth'), e.id(),(parent.data('children')-1) )
const wing = parent.data('subtreeWidth') / 2
const lastOffset = parent.data('lastChild')
const step = wing * 2 / (parent.data('children') - 1)
console.log(parent.data('subtreeWidth'), e.id(), (parent.data('children') - 1), step, wing, e.data('layer'), parent.id(), lastOffset)
//e.removeData('subtreeWidth')
//console.log('poss', e.id(), 'children', parent.data('children'),'lo', lastOffset, 'v', wing)
if (lastOffset !== undefined) {
parent.data('lastChild', lastOffset + step)
//console.log('lastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChild')
// console.log('lastChild', lastOffset + step)
// return { x: 200 * e.data('layer'), y: (lastOffset + step) }
return { x: 200 * e.data('layer'), y: (lastOffset + step) }
} else {
parent.data('lastChild', parent.position().y - wing)
// console.log('lastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChildlastChild')
// console.log('lastChild', parent.position().y - wing)
// return { x: 200 * e.data('layer'), y: (parent.position().y - wing)}
return { x: 200 * e.data('layer'), y: (parent.position().y - wing) }
}
}
}, // 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: undefined, // 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 true; }, // 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
// stop: (e) => console.log('stop', e), // callback on layoutstop
transform: function (node, position) { return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
}}
cy={(cy) => { cy={(cy) => {
cyRef.current = cy; cyRef.current = cy;
}} }}

@ -1,28 +1,30 @@
import { Box } from "@mui/material" import { Box } from "@mui/material"
import { useEffect, useRef, useState } from "react"; import { useEffect, useRef, useState } from "react";
import { updateDragQuestionId, updateQuestion } from "@root/questions/actions" import { updateDragQuestionContentId, updateQuestion } from "@root/questions/actions"
import { updateRootInfo } from "@root/quizes/actions" import { updateRootInfo } from "@root/quizes/actions"
import { useQuestionsStore } from "@root/questions/store" import { useQuestionsStore } from "@root/questions/store"
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
interface Props { interface Props {
setOpenedModalQuestionsId:(str: string|null) => void setOpenedModalQuestions: (open: boolean) => void;
modalQuestionTargetContentId: string;
} }
export const FirstNodeField = ({setOpenedModalQuestionsId}:Props) => { export const FirstNodeField = ({ setOpenedModalQuestions, modalQuestionTargetContentId }: Props) => {
const { dragQuestionId } = useQuestionsStore() const { dragQuestionContentId } = useQuestionsStore()
const Container = useRef<HTMLDivElement | null>(null); const Container = useRef<HTMLDivElement | null>(null);
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
console.log(dragQuestionId) console.log(dragQuestionContentId)
const modalOpen = () => setOpenedModalQuestions(true)
const newRootNode = () => { const newRootNode = () => {
if (quiz) { if (quiz) {
console.log(dragQuestionId) console.log(dragQuestionContentId)
if (dragQuestionId) { if (dragQuestionContentId) {
updateRootInfo(quiz?.id, true) updateRootInfo(quiz?.id, true)
updateQuestion(dragQuestionId, (question) => question.content.rule.parentId = "root") updateQuestion(dragQuestionContentId, (question) => question.content.rule.parentId = "root")
} }
} else { } else {
enqueueSnackbar("Нет информации о взятом опроснике") enqueueSnackbar("Нет информации о взятом опроснике")
@ -31,12 +33,28 @@ console.log(dragQuestionId)
useEffect(() => { useEffect(() => {
Container.current?.addEventListener("mouseup", newRootNode) Container.current?.addEventListener("mouseup", newRootNode)
return () => { Container.current?.removeEventListener("mouseup", newRootNode) } Container.current?.addEventListener("click", modalOpen)
return () => {
Container.current?.removeEventListener("mouseup", newRootNode)
Container.current?.removeEventListener("click", modalOpen)
}
}, []) }, [])
useEffect(() => {
if (quiz) {
if (dragQuestionContentId) {
updateRootInfo(quiz?.id, true)
updateQuestion(modalQuestionTargetContentId, (question) => question.content.rule.parentId = "root")
}
} else {
enqueueSnackbar("Нет информации о взятом опроснике")
}
}, [modalQuestionTargetContentId])
return ( return (
<Box <Box
onclick
ref={Container} ref={Container}
sx={{ sx={{
height: "100%", height: "100%",

@ -21,12 +21,12 @@ export const storeToNodes = (questions: AnyQuizQuestion[]) => {
console.log(question) console.log(question)
if (question.content.rule.parentId) { if (question.content.rule.parentId) {
nodes.push({data: { nodes.push({data: {
id: question.id, id: question.content.id,
label: question.title ? question.title : "noname" label: question.title ? question.title : "noname"
}}) }})
if (question.content.rule.parentId !== "root") edges.push({data: { if (question.content.rule.parentId !== "root") edges.push({data: {
source: question.content.rule.parentId, source: question.content.rule.parentId,
target: question.id target: question.content.id
}}) }})
} }
}) })

@ -4,12 +4,15 @@ import { CsComponent } from "./CsComponent";
import { useQuestionsStore } from "@root/questions/store" import { useQuestionsStore } from "@root/questions/store"
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import { useState } from "react"; import { useState } from "react";
import {BranchingQuestionsModal} from "../BranchingQuestionsModal"
export const BranchingMap = () => { export const BranchingMap = () => {
const quiz = useCurrentQuiz(); const quiz = useCurrentQuiz();
const { dragQuestionId } = useQuestionsStore() const { dragQuestionContentId } = useQuestionsStore()
const [openedModalQuestionsId, setOpenedModalQuestionsId] = useState<string>() const [modalQuestionParentContentId, setModalQuestionParentContentId] = useState<string>("")
const [modalQuestionTargetContentId, setModalQuestionTargetContentId] = useState<string>("")
const [openedModalQuestions, setOpenedModalQuestions] = useState<boolean>(false)
return ( return (
<Box <Box
@ -22,17 +25,17 @@ export const BranchingMap = () => {
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)", boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
marginBottom: "40px", marginBottom: "40px",
height: "521px", height: "521px",
border: dragQuestionId === null ? "none" : "#7e2aea 2px dashed" border: dragQuestionContentId === null ? "none" : "#7e2aea 2px dashed"
}} }}
> >
{ {
quiz?.config.haveRoot ? quiz?.config.haveRoot ?
<CsComponent /> <CsComponent modalQuestionParentContentId={modalQuestionParentContentId} modalQuestionTargetContentId={modalQuestionTargetContentId} setOpenedModalQuestions={setOpenedModalQuestions} setModalQuestionParentContentId={setModalQuestionParentContentId} setModalQuestionTargetContentId={setModalQuestionTargetContentId}/>
: :
<FirstNodeField setOpenedModalQuestionsId={setOpenedModalQuestionsId} /> <FirstNodeField setOpenedModalQuestions={setOpenedModalQuestions} modalQuestionTargetContentId={modalQuestionTargetContentId}/>
} }
<BranchingQuestionsModal openedModalQuestions={openedModalQuestions} setOpenedModalQuestions={setOpenedModalQuestions} setModalQuestionTargetContentId={setModalQuestionTargetContentId} />
</Box> </Box>
); );
}; };

@ -2,7 +2,7 @@ import { useParams } from "react-router-dom";
import { Box, Button, Typography } from "@mui/material"; import { Box, Button, Typography } from "@mui/material";
import { ReactComponent as CheckedIcon } from "@icons/checked.svg"; import { ReactComponent as CheckedIcon } from "@icons/checked.svg";
import { useQuestionsStore } from "@root/questions/store"; import { useQuestionsStore } from "@root/questions/store";
import { updateDragQuestionId } from "@root/questions/actions"; import { updateDragQuestionContentId } from "@root/questions/actions";
import { useEffect } from "react"; import { useEffect } from "react";
import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "@model/questionTypes/shared"; import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "@model/questionTypes/shared";
@ -23,6 +23,7 @@ type AnyQuestion = UntypedQuizQuestion | AnyTypedQuizQuestion
export const QuestionsList = () => { export const QuestionsList = () => {
const { questions } = useQuestionsStore() const { questions } = useQuestionsStore()
console.log(questions)
return ( return (
<Box sx={{ padding: "15px" }}> <Box sx={{ padding: "15px" }}>
@ -54,7 +55,7 @@ export const QuestionsList = () => {
{questions.filter((q:AnyQuestion) => q.type).map(({ title, id, content }, index) => ( {questions.filter((q:AnyQuestion) => q.type).map(({ title, id, content }, index) => (
<Button <Button
onMouseDown={() => {//Разрешаем добавить этот вопрос если у него нет родителя (не добавляли ещё в дерево) onMouseDown={() => {//Разрешаем добавить этот вопрос если у него нет родителя (не добавляли ещё в дерево)
if (!content.rule.parentId) updateDragQuestionId(id) if (!content.rule.parentId) updateDragQuestionContentId(id)
}} }}
key={index} key={index}
sx={{ sx={{

@ -0,0 +1,77 @@
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
interface Props {
openedModalQuestions: boolean;
setModalQuestionTargetContentId: (contentId:string) => void;
setOpenedModalQuestions: (open:boolean) => void;
}
export const BranchingQuestionsModal = ({ openedModalQuestions, setOpenedModalQuestions, setModalQuestionTargetContentId}:Props) => {
const quizId = Number(useParams().quizId);
const { questions } = useQuestionsStore();
const handleClose = () => {
setOpenedModalQuestions(false);
};
return (
<Modal open={openedModalQuestions} onClose={handleClose}>
<Box
sx={{
position: "absolute",
overflow: "hidden",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: "620px",
width: "100%",
bgcolor: "background.paper",
borderRadius: "12px",
boxShadow: 24,
padding: "30px 0",
}}
>
<Box sx={{ margin: "0 auto", maxWidth: "350px" }}>
{questions.filter((q:AnyQuestion) => (q.type && !q.content.rule.parentId)).map((question: AnyTypedQuizQuestion, index:number) => (
<Button
key={question.content.id}
onClick={() => {
setModalQuestionTargetContentId(question.content.id)
handleClose();
}}
sx={{
width: "100%",
cursor: "pointer",
display: "flex",
justifyContent: "space-between",
alignItems: "center",
padding: "12px",
background: "#FFFFFF",
borderRadius: "8px",
marginBottom: "20px",
boxShadow: "0px 10px 30px #e7e7e7",
backgroundImage: `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='rgb(154, 154, 175)' stroke-width='2' stroke-dasharray='8 8' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
border-radius: 8px;`,
"&:last-child": { marginBottom: 0 },
}}
>
<Typography
sx={{
width: "100%",
color: "#000",
}}
>
{question.title || "нет заголовка"}
</Typography>
</Button>
))}
</Box>
</Box>
</Modal>
);
};

@ -122,7 +122,10 @@ export const updateQuestion = (
updateFn: (question: AnyTypedQuizQuestion) => void, updateFn: (question: AnyTypedQuizQuestion) => void,
) => { ) => {
setProducedState(state => { setProducedState(state => {
const question = state.questions.find(q => q.id === questionId); const question = state.questions.find(q => q.id === questionId) || state.questions.find(q => {
console.log(q)
return q.content.id === questionId
});
if (!question) return; if (!question) return;
if (question.type === null) throw new Error("Cannot update untyped question, use 'updateUntypedQuestion' instead"); if (question.type === null) throw new Error("Cannot update untyped question, use 'updateUntypedQuestion' instead");
@ -426,16 +429,20 @@ function setProducedState<A extends string | { type: unknown; }>(
}; };
export const clearDragQuestionId = () => { export const cleardragQuestionContentId = () => {
console.log("чищу чищу") console.log("чищу чищу")
useQuestionsStore.setState({dragQuestionId: null}); useQuestionsStore.setState({dragQuestionContentId: null});
}; };
export const getQuestionById = (questionId: string | null) => { export const getQuestionById = (questionId: string | null) => {
if (questionId === null) return null; if (questionId === null) return null;
return useQuestionsStore.getState().questions.find(q => q.id === questionId) || null; return useQuestionsStore.getState().questions.find(q => q.id === questionId) || null;
}; };
export const getQuestionByContentId = (questionContentId: string | null) => {
if (questionContentId === null) return null;
return useQuestionsStore.getState().questions.find(q => 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 updateDragQuestionId = (id?: string) => useQuestionsStore.setState({dragQuestionId: id ? id : null}); export const updateDragQuestionContentId = (contentId?: string) => useQuestionsStore.setState({dragQuestionContentId: contentId ? contentId : null});

@ -6,13 +6,13 @@ import { devtools } from "zustand/middleware";
export type QuestionsStore = { export type QuestionsStore = {
questions: (AnyTypedQuizQuestion | UntypedQuizQuestion); questions: (AnyTypedQuizQuestion | UntypedQuizQuestion);
openedModalSettingsId: string | null; openedModalSettingsId: string | null;
dragQuestionId: string | null; dragQuestionContentId: string | null;
}; };
const initialState: QuestionsStore = { const initialState: QuestionsStore = {
questions: [], questions: [],
openedModalSettingsId: null as null, openedModalSettingsId: null as null,
dragQuestionId: null, dragQuestionContentId: null,
}; };
export const useQuestionsStore = create<QuestionsStore>()( export const useQuestionsStore = create<QuestionsStore>()(