Merge branch 'result-page' into dev

This commit is contained in:
Nastya 2023-12-11 18:51:17 +03:00
commit e0cc10dcac
28 changed files with 20357 additions and 647 deletions

19013
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

@ -8,7 +8,7 @@ export default function ExpandIcon({ sx }: Props) {
const theme = useTheme();
return (
<Box sx={{ ...sx }}>
<Box sx={{ ...sx, display: "flex", alignItems:"center", justifyContent: "center" }}>
<svg width="30" height="30" viewBox="0 0 30 30" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect width="30" height="30" rx="6" fill="#EEE4FC" />
<path d="M22.5 11.25L15 18.75L7.5 11.25" stroke="#7E2AEA" stroke-width="1.5" stroke-linecap="round" strokeLinejoin="round" />

@ -1,13 +1,20 @@
import { IconButton } from "@mui/material";
import { IconButton, SxProps } from "@mui/material";
type InfoProps = {
width?: number;
height?: number;
sx?: SxProps;
onClick?: any;
className?: string
};
export default function Info({ width = 20, height = 20 }: InfoProps) {
export default function Info({ width = 20, height = 20, sx, onClick, className }: InfoProps) {
return (
<IconButton>
<IconButton
sx={sx}
className={className}
onClick={onClick}
>
<svg
width={width}
height={height}

@ -1,7 +1,7 @@
import { Box } from "@mui/material";
interface Props {
color: string;
color?: string;
}
export default function SettingIcon({ color }: Props) {

@ -0,0 +1,21 @@
import { useTheme, SxProps, Box } from "@mui/material";
interface Props {
sx?: SxProps;
}
export default function Trash({ sx }: Props) {
const theme = useTheme();
return (
<Box sx={{ ...sx, display: "flex", alignItems: "center", justifyContent: "center" }}>
<svg width="24" height="25" viewBox="0 0 24 25" fill="none" xmlns="http://www.w3.org/2000/svg">
<path d="M20.25 5.73438H3.75" stroke="#4D4D4D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M9.75 10.2344V16.2344" stroke="#4D4D4D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M14.25 10.2344V16.2344" stroke="#4D4D4D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M18.75 5.73438V19.9844C18.75 20.1833 18.671 20.3741 18.5303 20.5147C18.3897 20.6554 18.1989 20.7344 18 20.7344H6C5.80109 20.7344 5.61032 20.6554 5.46967 20.5147C5.32902 20.3741 5.25 20.1833 5.25 19.9844V5.73438" stroke="#4D4D4D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
<path d="M15.75 5.73438V4.23438C15.75 3.83655 15.592 3.45502 15.3107 3.17371C15.0294 2.89241 14.6478 2.73438 14.25 2.73438H9.75C9.35218 2.73438 8.97064 2.89241 8.68934 3.17371C8.40804 3.45502 8.25 3.83655 8.25 4.23438V5.73438" stroke="#4D4D4D" stroke-width="1.5" stroke-linecap="round" stroke-linejoin="round" />
</svg>
</Box>
);
}

@ -18,6 +18,7 @@ export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id" | "backendId"> = {
video: "",
},
rule: {
children: [],
main: [] as QuestionBranchingRuleMain[],
parentId: "",
default: ""

@ -12,6 +12,6 @@ export const QUIZ_QUESTION_RESULT: Omit<QuizQuestionResult, "id" | "backendId">
innerName: "",
text: "",
price: [0],
rangePrice: false
useImage: true
},
};

@ -10,4 +10,20 @@ body {
code {
font-family: source-code-pro, Menlo, Monaco, Consolas, 'Courier New',
monospace;
}
@keyframes blinking {
0% {
opacity: 100;
}
50% {
opacity: 0;
}
100% {
opacity: 100;
}
}
.blink {
animation: blinking 2s infinite ;
}

@ -14,7 +14,7 @@ export interface QuizQuestionResult extends QuizQuestionBase {
innerName: string;
text: string;
price: [number] | [number, number];
rangePrice: boolean;
useImage: boolean;
rule: QuestionBranchingRule,
hint: QuestionHint;
autofill: boolean;

@ -23,6 +23,7 @@ export interface QuestionBranchingRuleMain {
}
export interface QuestionBranchingRule {
children: string[],
//список условий
main: QuestionBranchingRuleMain[];
parentId: string | null | "root";

@ -1,12 +1,13 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import Cytoscape from "cytoscape";
import { Button } from "@mui/material";
import CytoscapeComponent from "react-cytoscapejs";
import popper from "cytoscape-popper";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateRootContentId } from "@root/quizes/actions"
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"
import { useQuestionsStore } from "@root/questions/store";
import { cleardragQuestionContentId, updateQuestion, updateOpenedModalSettingsId, getQuestionByContentId, clearRuleForAll } from "@root/questions/actions";
import { deleteQuestion, cleardragQuestionContentId, updateQuestion, updateOpenedModalSettingsId, getQuestionByContentId, clearRuleForAll } from "@root/questions/actions";
import { withErrorBoundary } from "react-error-boundary";
import { storeToNodes } from "./helper";
@ -20,9 +21,7 @@ import type {
AbstractEventObject,
ElementDefinition,
} from "cytoscape";
import { QuestionsList } from "../SwitchBranchingPanel/QuestionsList";
import { enqueueSnackbar } from "notistack";
import { Typography } from "@mui/material";
type PopperItem = {
id: () => string;
@ -113,18 +112,18 @@ interface Props {
}
function CsComponent ({
function CsComponent({
modalQuestionParentContentId,
modalQuestionTargetContentId,
setOpenedModalQuestions,
setModalQuestionParentContentId,
setModalQuestionTargetContentId
}: Props) {
}: Props) {
const quiz = useCurrentQuiz();
const { dragQuestionContentId, desireToOpenABranchingModal } = useQuestionsStore()
const trashQuestions = useQuestionsStore().questions
const questions = trashQuestions.filter((question) => question.type !== "result")
const questions = trashQuestions.filter((question) => question.type !== "result" && question.type !== null)
const [startCreate, setStartCreate] = useState("");
const [startRemove, setStartRemove] = useState("");
@ -166,7 +165,7 @@ function CsComponent ({
if (Object.keys(targetQuestion).length !== 0 && Object.keys(targetQuestion).length !== 0 && parentNodeContentId && parentNodeChildren !== undefined) {
clearDataAfterAddNode({ parentNodeContentId, targetQuestion, parentNodeChildren })
cy?.data('changed', true)
cy?.add([
const es = cy?.add([
{
data: {
id: targetQuestion.content.id,
@ -181,24 +180,39 @@ function CsComponent ({
}
])
cy?.layout(lyopts).run()
cy?.fit(es, 200)
} else {
enqueueSnackbar("Добавляемый вопрос не найден")
}
}
const clearDataAfterAddNode = ({ parentNodeContentId, targetQuestion, parentNodeChildren }: { parentNodeContentId: string, targetQuestion: AnyTypedQuizQuestion, parentNodeChildren: number }) => {
const parentQuestion = { ...getQuestionByContentId(parentNodeContentId) } as AnyTypedQuizQuestion
//смотрим не добавлен ли родителю result. Если да - убираем его. Веточкам result не нужен
trashQuestions.forEach((targetQuestion) => {
if (targetQuestion.type === "result" && targetQuestion.content.rule.parentId === parentQuestion.content.id) {
deleteQuestion(targetQuestion.id);
}
})
//предупреждаем добавленный вопрос о том, кто его родитель
updateQuestion(targetQuestion.content.id, question => {
question.content.rule.parentId = parentNodeContentId
question.content.rule.main = []
})
//предупреждаем родителя о новом потомке (если он ещё не знает о нём)
if (!parentQuestion.content.rule.children.includes(targetQuestion.content.id)) updateQuestion(parentNodeContentId, question => {
question.content.rule.children = [...question.content.rule.children, targetQuestion.content.id]
})
//Если детей больше 1 - предупреждаем стор вопросов об открытии модалки ветвления
if (parentNodeChildren >= 1) {
if (parentQuestion.content.rule.children >= 1) {
updateOpenedModalSettingsId(targetQuestion.content.id)
} else {
//Если ребёнок первый - добавляем его родителю как дефолтный
updateQuestion(parentNodeContentId, question => question.content.rule.default = targetQuestion.content.id)
}
}
@ -232,10 +246,18 @@ function CsComponent ({
updateQuestion(targetNodeContentId, question => {
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.children = []
question.content.rule.default = ""
})
trashQuestions.forEach(q => {
if (q.type === "result") {
deleteQuestion(q.id);
}
});
clearRuleForAll()
} else {
const parentQuestionContentId = cy?.$('edge[target = "' + targetNodeContentId + '"]')?.toArray()?.[0]?.data()?.source
if (targetNodeContentId && parentQuestionContentId) {
@ -245,7 +267,7 @@ function CsComponent ({
}
//После всех манипуляций удаляем грани из CS и ноды из бекенда
//После всех манипуляций удаляем грани и ноды из CS Чистим rule потомков на беке
deleteNodes.forEach((nodeId) => {//Ноды
cy?.remove(cy?.$("#" + nodeId))
@ -254,7 +276,10 @@ function CsComponent ({
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.default = ""
question.content.rule.children = []
})
})
deleteEdges.forEach((edge: any) => {//Грани
@ -264,26 +289,42 @@ function CsComponent ({
removeButtons(targetNodeContentId)
cy?.data('changed', true)
cy?.layout(lyopts).run()
//удаляем result всех потомков
trashQuestions.forEach((qr) => {
if (qr.type === "result") {
if (deleteNodes.includes(qr.content.rule.parentId) || qr.content.rule.parentId === targetQuestion.content.id) {
deleteQuestion(qr.id);
}
}
})
}
const clearDataAfterRemoveNode = ({ targetQuestionContentId, parentQuestionContentId }: { targetQuestionContentId: string, parentQuestionContentId: string }) => {
console.log("target ", targetQuestionContentId, "parent ", parentQuestionContentId)
updateQuestion(targetQuestionContentId, question => {
question.content.rule.parentId = ""
question.content.rule.children = []
question.content.rule.main = []
question.content.rule.default = ""
})
//чистим rule родителя
const parentQuestion = getQuestionByContentId(parentQuestionContentId)
console.log(parentQuestion.content.rule.parentId)
const newRule = {}
const newChildren = [...parentQuestion.content.rule.children]
newChildren.splice(parentQuestion.content.rule.children.indexOf(targetQuestionContentId), 1);
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== targetQuestionContentId) //удаляем условия перехода от родителя к этому вопросу
newRule.parentId = parentQuestion.content.rule.parentId
newRule.default = questions.filter((q) => {
return q.content.rule.parentId === parentQuestionContentId && q.content.id !== targetQuestionContentId
})[0]?.content.id || ""
//Если этот вопрос был дефолтным у родителя - чистим дефолт
//Смотрим можем ли мы заменить id на один из main
newRule.default = parentQuestion.content.rule.default === targetQuestionContentId ? "" : parentQuestion.content.rule.default
newRule.children = newChildren
console.log(newRule)
updateQuestion(parentQuestionContentId, (PQ) => {
PQ.content.rule = newRule
})
@ -332,7 +373,7 @@ function CsComponent ({
positions: (e) => {
if (!e.cy().data('changed')) {
return e.data('oldPos')
} else { e.removeData('oldPos') }
}
const id = e.id()
const incomming = e.cy().edges(`[target="${id}"]`)
const layer = 0
@ -363,7 +404,7 @@ function CsComponent ({
while (queue.length) {
const task = queue.pop()
if (task.children.length === 0) {
task.parent.data('subtreeWidth', task.parent.height())
task.parent.data('subtreeWidth', task.parent.height() + 50)
continue
}
const unprocessed = task?.children.filter(e => {
@ -379,31 +420,31 @@ function CsComponent ({
task?.parent.data('subtreeWidth', task.children.reduce((p, n) => p + n.data('subtreeWidth'), 0))
}
const pos = { x: 0, y: 0 }
e.data('oldPos', pos)
queue.push({ task: children, parent: e })
while (queue.length) {
const task = queue.pop()
const oldPos = task.parent.data('oldPos')
let yoffset = oldPos.y - task.parent.data('subtreeWidth') / 2
task.task.forEach(n => {
const width = n.data('subtreeWidth')
console.log('ORORORORO', n.data(), yoffset, width, oldPos, task.parent.data('subtreeWidth'))
n.data('oldPos', { x: 250 * n.data('layer'), y: yoffset + width / 2 })
yoffset += width
queue.push({ task: n.cy().edges(`[source="${n.id()}"]`).targets(), parent: n })
})
}
e.cy().data('changed', false)
return pos
} else {
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')
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')
if (lastOffset !== undefined) {
parent.data('lastChild', lastOffset + step)
const pos = { x: 250 * e.data('layer'), y: (lastOffset + step) }
e.data('oldPos', pos)
return pos
} else {
parent.data('lastChild', parent.position().y - wing)
const pos = { x: 250 * e.data('layer'), y: (parent.position().y - wing) }
e.data('oldPos', pos)
return pos
const opos = e.data('oldPos')
if (opos) {
return opos
}
}
}, // map of (node id) => (position obj); or function(node){ return somPos; }
@ -422,7 +463,7 @@ function CsComponent ({
useEffect(() => {
document.querySelector("#root")?.addEventListener("mouseup", cleardragQuestionContentId);
const cy = cyRef.current;
const eles = cy?.add(storeToNodes(questions))
const eles = cy?.add(storeToNodes(questions.filter((question: AnyTypedQuizQuestion) => (question.type !== "result" && question.type !== null))))
cy.data('changed', true)
// cy.data('changed', true)
const elecs = eles.layout(lyopts).run()
@ -690,6 +731,23 @@ function CsComponent ({
return (
<>
<Button
sx={{
mb: "20px",
height: "27px",
color: "#7E2AEA",
textDecoration: "underline",
fontSize: "16px",
}}
variant="text"
onClick={() => {
//код сюда
}}
>
Выровнять
</Button>
<CytoscapeComponent
wheelSensitivity={0.1}
elements={[]}
@ -700,7 +758,7 @@ function CsComponent ({
cy={(cy) => {
cyRef.current = cy;
}}
// autolock
autoungrabify={true}
/>
<button onClick={() => {
console.log("NODES____________________________")
@ -716,18 +774,18 @@ function CsComponent ({
);
};
function Clear () {
const quiz = useCurrentQuiz();
updateRootContentId(quiz.id, "")
clearRuleForAll()
return <></>
function Clear() {
const quiz = useCurrentQuiz();
updateRootContentId(quiz.id, "")
clearRuleForAll()
return <></>
}
export default withErrorBoundary(CsComponent, {
fallback: <Clear/>,
fallback: <Clear />,
onError: (error, info) => {
enqueueSnackbar("Дерево порвалось")
console.log(info)
console.log(error)
},
});
});

@ -26,7 +26,7 @@ export const BranchingMap = () => {
borderRadius: "12px",
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
marginBottom: "40px",
height: "521px",
height: "568px",
border: dragQuestionContentId === null ? "none" : "#7e2aea 2px dashed"
}}
>

@ -46,20 +46,6 @@ export default function ButtonsOptions({
updateOpenedModalSettingsId(question.id)
};
const handleClickBranching = (_, value) => {
const parentId = question.content.rule.parentId
if (parentId.length === 0 ){
return enqueueSnackbar("Вопрос не учавствует в ветвлении")
}
if (parentId === "root") {
return enqueueSnackbar("У корня нет условий ветвления")
}
if (parentId.length !== 0) {
// updateOpenBranchingPanel(value)
openedModal()
}
}
const buttonSetting: {
icon: JSX.Element;
@ -300,7 +286,59 @@ export default function ButtonsOptions({
// deleteTimeoutId: newTimeoutId,
// });
deleteQuestion(question.id, quiz.id);
if (question.type !== null) {
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quiz.id, "")
clearRuleForAll()
questions.forEach(q => {
if (q.type === "result") {
deleteQuestion(q.id);
}
});
deleteQuestion(question.id);
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
const clearQuestions = [] as string[]
//записываем потомков , а их результаты удаляем
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
questions.forEach((targetQuestion) => {
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
if (targetQuestion.type === "result") {
deleteQuestion(targetQuestion.id);
} else {
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id)
getChildren(targetQuestion) //и ищем его потомков
}
}
})
}
getChildren(question)
//чистим потомков от инфы ветвления
clearQuestions.forEach((id) => {
updateQuestion(id, question => {
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.default = ""
})
})
//чистим rule родителя
const parentQuestion = getQuestionByContentId(question.content.rule.parentId)
const newRule = {}
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== question.content.id) //удаляем условия перехода от родителя к этому вопросу
newRule.parentId = parentQuestion.content.rule.parentId
newRule.default = parentQuestion.content.rule.parentId === question.content.id ? "" : parentQuestion.content.rule.parentId
newRule.children = [...parentQuestion.content.rule.children].splice(parentQuestion.content.rule.children.indexOf(question.content.id), 1);
updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule
})
deleteQuestion(question.id)
}
deleteQuestion(question.id)
}
}}
data-cy="delete-question"
>

@ -10,7 +10,7 @@ import {
useMediaQuery,
useTheme,
} from "@mui/material";
import { copyQuestion, deleteQuestion, updateQuestion } from "@root/questions/actions";
import { copyQuestion, deleteQuestion, updateQuestion, clearRuleForAll, getQuestionByContentId } from "@root/questions/actions";
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import { ReallyChangingModal } from "@ui_kit/Modal/ReallyChangingModal/ReallyChangingModal";
import { useEffect, useState } from "react";
@ -27,6 +27,7 @@ import { updateOpenBranchingPanel, updateDesireToOpenABranchingModal } from "@ro
import { useQuestionsStore } from "@root/questions/store";
import { enqueueSnackbar } from "notistack";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateRootContentId } from "@root/quizes/actions";
interface Props {
@ -46,7 +47,7 @@ export default function ButtonsOptionsAndPict({
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isIconMobile = useMediaQuery(theme.breakpoints.down(1050));
const { openBranchingPanel } = useQuestionsStore.getState()
const { questions } = useQuestionsStore.getState()
const quiz = useCurrentQuiz();
useEffect(() => {
@ -323,7 +324,59 @@ export default function ButtonsOptionsAndPict({
// deleteTimeoutId: newTimeoutId,
// });
deleteQuestion(question.id, quiz?.id);
if (question.type !== null) {
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quiz.id, "")
clearRuleForAll()
questions.forEach(q => {
if (q.type === "result") {
deleteQuestion(q.id);
}
});
deleteQuestion(question.id);
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
const clearQuestions = [] as string[]
//записываем потомков , а их результаты удаляем
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
questions.forEach((targetQuestion) => {
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
if (targetQuestion.type === "result") {
deleteQuestion(targetQuestion.id);
} else {
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id)
getChildren(targetQuestion) //и ищем его потомков
}
}
})
}
getChildren(question)
//чистим потомков от инфы ветвления
clearQuestions.forEach((id) => {
updateQuestion(id, question => {
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.default = ""
})
})
//чистим rule родителя
const parentQuestion = getQuestionByContentId(question.content.rule.parentId)
const newRule = {}
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== question.content.id) //удаляем условия перехода от родителя к этому вопросу
newRule.parentId = parentQuestion.content.rule.parentId
newRule.default = parentQuestion.content.rule.parentId === question.content.id ? "" : parentQuestion.content.rule.parentId
newRule.children = [...parentQuestion.content.rule.children].splice(parentQuestion.content.rule.children.indexOf(question.content.id), 1);
updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule
})
deleteQuestion(question.id)
}
deleteQuestion(question.id)
}
}}
data-cy="delete-question"
>

@ -21,7 +21,6 @@ function DraggableListItem({ question, isDragging, index }: Props) {
if (editSomeQuestion !== null) {
const setI = setInterval(() => {
let comp = document.getElementById(editSomeQuestion)
console.log(comp)
if(comp !== null) {
clearInterval(setI)
comp.scrollIntoView({behavior: 'instant'})
@ -30,7 +29,6 @@ function DraggableListItem({ question, isDragging, index }: Props) {
}, 200)
}
console.log(editSomeQuestion)
}, [editSomeQuestion])
return (

@ -29,7 +29,8 @@ import {
useMediaQuery,
useTheme,
} from "@mui/material";
import { copyQuestion, createUntypedQuestion, deleteQuestion, toggleExpandQuestion, updateQuestion, updateUntypedQuestion } from "@root/questions/actions";
import { copyQuestion, createUntypedQuestion, deleteQuestion, clearRuleForAll, toggleExpandQuestion, updateQuestion, updateUntypedQuestion, getQuestionByContentId } from "@root/questions/actions";
import { updateRootContentId } from "@root/quizes/actions";
import { useRef, useState } from "react";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { useDebouncedCallback } from "use-debounce";
@ -40,6 +41,7 @@ import { ChooseAnswerModal } from "./ChooseAnswerModal";
import TypeQuestions from "../TypeQuestions";
import { QuestionType } from "@model/question/question";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useQuestionsStore } from "@root/questions/store";
interface Props {
question: AnyTypedQuizQuestion | UntypedQuizQuestion;
@ -49,6 +51,7 @@ interface Props {
}
export default function QuestionsPageCard({ question, draggableProps, isDragging, index }: Props) {
const { questions } = useQuestionsStore()
const [plusVisible, setPlusVisible] = useState<boolean>(false);
const [open, setOpen] = useState<boolean>(false);
const theme = useTheme();
@ -254,8 +257,63 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
// ...question,
// deleteTimeoutId: newTimeoutId,
// });
console.log(question.type)
if (question.type !== null) {
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quiz.id, "")
clearRuleForAll()
deleteQuestion(question.id);
questions.forEach(q => {
if (q.type === "result") {
deleteQuestion(q.id);
}
});
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
const clearQuestions = [] as string[]
deleteQuestion(question.id, quiz.id);
//записываем потомков , а их результаты удаляем
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
questions.forEach((targetQuestion) => {
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
if (targetQuestion.type === "result") {
deleteQuestion(targetQuestion.id);
} else {
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id)
getChildren(targetQuestion) //и ищем его потомков
}
}
})
}
getChildren(question)
//чистим потомков от инфы ветвления
clearQuestions.forEach((id) => {
updateQuestion(id, question => {
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.default = ""
})
})
//чистим rule родителя
const parentQuestion = getQuestionByContentId(question.content.rule.parentId)
const newRule = {}
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== question.content.id) //удаляем условия перехода от родителя к этому вопросу
newRule.parentId = parentQuestion.content.rule.parentId
newRule.default = parentQuestion.content.rule.parentId === question.content.id ? "" : parentQuestion.content.rule.parentId
newRule.children = [...parentQuestion.content.rule.children].splice(parentQuestion.content.rule.children.indexOf(question.content.id), 1);
updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule
})
deleteQuestion(question.id)
}
deleteQuestion(question.id)
} else {
console.log("удаляю безтипогово")
deleteQuestion(question.id)
}
}}
data-cy="delete-question"
>

@ -9,8 +9,6 @@ import { useQuestionsStore } from "@root/questions/store";
export const DraggableList = () => {
const { questions } = useQuestionsStore()
const filteredQuestions = questions.filter((question) => question.type !== "result")
console.log(questions)
console.log(filteredQuestions)
const onDragEnd = ({ destination, source }: DropResult) => {
if (destination) reorderQuestions(source.index, destination.index);
};

@ -9,8 +9,8 @@ import {useQuestionsStore} from "@root/questions/store";
export const QuestionSwitchWindowTool = () => {
const {openBranchingPanel} = useQuestionsStore.getState()
console.log(openBranchingPanel)
const {openBranchingPanel, questions} = useQuestionsStore.getState()
console.log("questions ", questions)
return (
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap" }}>
<Box sx={{ flexBasis: "796px" }}>

@ -25,7 +25,6 @@ export default function QuestionsPage() {
const { openedModalSettingsId, openBranchingPanel } = useQuestionsStore();
const isMobile = useMediaQuery(theme.breakpoints.down(660));
const quiz = useCurrentQuiz();
console.log(quiz)
useLayoutEffect(() => {
updateOpenBranchingPanel(false)
updateEditSomeQuestion()

@ -10,7 +10,6 @@ export const SwitchBranchingPanel = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(660));
const {openBranchingPanel} = useQuestionsStore.getState()
console.log(openBranchingPanel)
const ref = useRef()
return (
<Box sx={{ userSelect: "none", maxWidth: "350px", width: "100%" }}>
@ -28,7 +27,6 @@ export const SwitchBranchingPanel = () => {
<Switch
value={openBranchingPanel}
onChange={(_, value) => {
console.log("меняю на " + value)
updateOpenBranchingPanel(value)
}}
sx={{

@ -7,6 +7,7 @@ import {
Typography,
} from "@mui/material";
import CustomTextField from "@ui_kit/CustomTextField";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
const priceButtonsArray: { title: string; type: string; sx: SxProps<Theme> }[] =
@ -42,38 +43,31 @@ const priceButtonsArray: { title: string; type: string; sx: SxProps<Theme> }[] =
];
type Props = {
ButtonsActive: (index: number, type: string) => void;
priceButtonsActive: number | undefined;
resultData: QuizQuestionResult
};
export default function PriceButtons({
ButtonsActive,
priceButtonsActive,
resultData
}: Props) {
return (
<Box>
<Box sx={{ display: "flex", alignItems: "center", mb: "14xp" }}>
<Typography component={"h6"} sx={{ weight: "500", fontSize: "18px" }}>
Стоимость
Заголовок
</Typography>
</Box>
<Box
component="div"
sx={{ display: "flex", flexWrap: "wrap", gap: "8px", mb: "20px" }}
>
{priceButtonsArray.map(({ title, type, sx }, index) => (
<Button
onClick={() => ButtonsActive(index, type)}
key={title}
sx={{
bgcolor: priceButtonsActive === index ? "#7E2AEA" : "#F2F3F7",
color: priceButtonsActive === index ? "#FFFF" : "#9A9AAF",
...sx,
}}
>
{title}
</Button>
))}
<CustomTextField
placeholder={"Вы прошли опрос"}
sx={{
borderRadius: "8px",
height: "48px",
width: "100%",
}}
/>
</Box>
</Box>
);

@ -6,6 +6,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
import { Box, Typography, useTheme, useMediaQuery, Button } from "@mui/material";
import image from "../../assets/Rectangle 110.png";
import { enqueueSnackbar } from "notistack";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
export const FirstEntry = () => {
const theme = useTheme();
@ -15,17 +16,18 @@ export const FirstEntry = () => {
const create = () => {
if (quiz?.config.haveRoot) {
if (questions.length === 0) {
enqueueSnackbar("У вас не добавлено ни одного вопроса")
return
}
console.log("createFrontResult")
questions
.filter((question) => question.content.rule.parentId.length !== 0 && question.content.rule.default.length === 0)
.filter((question:AnyTypedQuizQuestion) => {
console.log(question)
return question.type !== null && question.content.rule.parentId.length !== 0 && question.content.rule.children.length === 0
})
.forEach(question => {
createFrontResult(quiz.id, question.content.id)
})
} else {
createFrontResult(quiz.id)
console.log("createFrontResult")
createFrontResult(quiz.id, "line")
}
}

@ -2,22 +2,46 @@ import IconPlus from "@icons/IconPlus";
import Info from "@icons/Info";
import Plus from "@icons/Plus";
import ArrowLeft from "@icons/questionsPage/arrowLeft";
import { Box, Button, Typography, Paper, FormControl, TextField } from "@mui/material";
import { Box, Button, Typography, Paper, Modal, TextField } from "@mui/material";
import { incrementCurrentStep } from "@root/quizes/actions";
import CustomWrapper from "@ui_kit/CustomWrapper";
import { DescriptionForm } from "./DescriptionForm/DescriptionForm";
import { ResultListForm } from "./ResultListForm";
import { SettingForm } from "./SettingForm";
import { useState } from "react";
import { useEffect, useRef, useState } from "react";
import { WhenCard } from "./cards/WhenCard";
import { ResultCard } from "./cards/ResultCard";
import { ResultCard, checkEmptyData } from "./cards/ResultCard";
import { EmailSettingsCard } from "./cards/EmailSettingsCard";
import { useCurrentQuiz } from "@root/quizes/hooks"
import { useQuestionsStore } from "@root/questions/store";
import { createFrontResult, deleteQuestion } from "@root/questions/actions";
import { QuizQuestionResult } from "@model/questionTypes/result";
export const ResultSettings = () => {
const { questions } = useQuestionsStore()
const quiz = useCurrentQuiz()
const results = useQuestionsStore().questions.filter((q): q is QuizQuestionResult => q.type === "result")
console.log("опросник ", quiz)
const [quizExpand, setQuizExpand] = useState(true)
const [resultContract, setResultContract] = useState(true)
const isReadyToLeaveRef = useRef(true);
useEffect(function calcIsReadyToLeave(){
let isReadyToLeave = true;
results.forEach((result) => {
if (checkEmptyData({ resultData: result })) {
isReadyToLeave = false;
}
});
console.log(`setting isReadyToLeaveRef to ${isReadyToLeave}`);
isReadyToLeaveRef.current = isReadyToLeave;
}, [results])
useEffect(() => {
return () => {
if (isReadyToLeaveRef.current === false) alert("Пожалуйста, проверьте, что вы заполнили все результаты");
};
}, []);
return (
<Box sx={{ maxWidth: "796px" }}>
@ -83,8 +107,17 @@ export const ResultSettings = () => {
</Button>
</Box>
<ResultCard resultContract={resultContract} />
{
results.map((resultQuestion) => <ResultCard resultContract={resultContract} resultData={resultQuestion} key={resultQuestion.id} />)
}
<Modal
open={false}
// onClose={handleClose}
aria-labelledby="modal-modal-title"
aria-describedby="modal-modal-description"
>
<></>
</Modal>
</Box>
);
};

@ -1,5 +1,4 @@
import { useEffect, useState } from "react";
import { useDebouncedCallback } from "use-debounce";
import { useCurrentQuiz } from "@root/quizes/hooks"
@ -34,10 +33,7 @@ export const EmailSettingsCard = ({ quizExpand }: Props) => {
useEffect(() => {
setExpand(false)
}, [quizExpand])
const debouncedCallback = useDebouncedCallback((callback) => {
callback();
}, 200);
return (
<Paper
@ -122,9 +118,9 @@ export const EmailSettingsCard = ({ quizExpand }: Props) => {
<TextField
value={quiz.config.resultInfo.theme}
placeholder={"Заголовок вопроса"}
onChange={({ target }: { target: HTMLInputElement }) => {debouncedCallback(updateQuiz(quiz.id, (quiz) => {
onChange={({ target }: { target: HTMLInputElement }) => {updateQuiz(quiz.id, (quiz) => {
quiz.config.resultInfo.theme = target.value
})) }}
}) }}
sx={{
margin: isMobile ? "10px 0" : 0,
width:"100%",
@ -167,9 +163,9 @@ export const EmailSettingsCard = ({ quizExpand }: Props) => {
<TextField
value={quiz.config.resultInfo.reply}
placeholder={"noreplay@example.ru"}
onChange={({ target }: { target: HTMLInputElement }) => {debouncedCallback(updateQuiz(quiz.id, (quiz) => {
onChange={({ target }: { target: HTMLInputElement }) => {updateQuiz(quiz.id, (quiz) => {
quiz.config.resultInfo.reply = target.value
})) }}
}) }}
sx={{
margin: isMobile ? "10px 0" : 0,
width:"100%",
@ -212,9 +208,9 @@ export const EmailSettingsCard = ({ quizExpand }: Props) => {
<TextField
value={quiz.config.resultInfo.replname}
placeholder={"Название компании"}
onChange={({ target }: { target: HTMLInputElement }) => {debouncedCallback(updateQuiz(quiz.id, (quiz) => {
onChange={({ target }: { target: HTMLInputElement }) => {updateQuiz(quiz.id, (quiz) => {
quiz.config.resultInfo.replname = target.value
})) }}
}) }}
sx={{
margin: isMobile ? "10px 0" : 0,
width:"100%",

@ -1,135 +1,274 @@
import { useEffect, useState } from "react";
import * as React from "react";
import { updateQuiz } from "@root/quizes/actions"
import { getQuestionByContentId, updateQuestion, uploadQuestionImage } from "@root/questions/actions"
import { useCurrentQuiz } from "@root/quizes/hooks"
import { SwitchSetting } from "../SwichResult";
import CustomTextField from "@ui_kit/CustomTextField";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import { UploadImageModal } from "../../Questions/UploadImage/UploadImageModal";
import { useDisclosure } from "../../../utils/useDisclosure";
import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton";
import {
Box,
IconButton,
Paper,
Button,
Typography,
TextField,
useMediaQuery,
useTheme,
Box,
IconButton,
Paper,
Button,
Typography,
TextField,
useMediaQuery,
useTheme,
FormControl,
Popover
} from "@mui/material";
import MiniButtonSetting from "@ui_kit/MiniButtonSetting";
import ExpandLessIconBG from "@icons/ExpandLessIconBG";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import ShareNetwork from "@icons/ShareNetwork.svg";
import ArrowCounterClockWise from "@icons/ArrowCounterClockWise.svg";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
import SwitchResult from "../DescriptionForm/SwitchResult";
import ButtonsOptionsForm from "../DescriptionForm/ButtinsOptionsForm";
import PriceButtons from "../DescriptionForm/PriceButton";
import DiscountButtons from "../DescriptionForm/DiscountButtons";
import CustomTextField from "@ui_kit/CustomTextField";
import { OneIcon } from "@icons/questionsPage/OneIcon";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
import Trash from "@icons/trash";
import Info from "@icons/Info";
import ImageAndVideoButtons from "../DescriptionForm/ImageAndVideoButtons";
import SettingIcon from "@icons/questionsPage/settingIcon";
import { QuizQuestionResult } from "@model/questionTypes/result";
import { MutableRefObject } from "react";
interface Props {
resultContract: boolean;
resultContract: boolean;
resultData: QuizQuestionResult;
}
export const ResultCard = ({ resultContract }:Props) => {
const quiz = useCurrentQuiz()
const theme = useTheme();
export const checkEmptyData = ({ resultData }: { resultData: QuizQuestionResult }) => {
let check = true
if (
resultData.title.length > 0 ||
resultData.description.length > 0 ||
resultData.content.back.length > 0 ||
resultData.content.originalBack.length > 0 ||
resultData.content.innerName.length > 0 ||
resultData.content.text.length > 0 ||
resultData.content.video.length > 0 ||
resultData.content.hint.text.length > 0
) check = false
return check
}
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isSmallMonitor = useMediaQuery(theme.breakpoints.down(1100));
const InfoView = ({ resultData }: { resultData: QuizQuestionResult }) => {
const checkEmpty = checkEmptyData({ resultData })
const question = getQuestionByContentId(resultData.content.rule.parentId)
const [anchorEl, setAnchorEl] = React.useState<HTMLButtonElement | null>(null);
const [expand, setExpand] = useState(true)
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget);
};
useEffect(() => {
setExpand(true)
}, [resultContract])
const handleClose = () => {
setAnchorEl(null);
};
const open = Boolean(anchorEl);
const id = open ? 'simple-popover' : undefined;
const [switchState, setSwitchState] = useState<string>("");
const [priceButtonsActive, setPriceButtonsActive] = useState<number>(0);
const [priceButtonsType, setPriceButtonsType] = useState<string>();
const [forwarding, setForwarding] = useState<boolean>(false);
const buttonsActive = (index: number, type: string) => {
setPriceButtonsActive(index);
setPriceButtonsType(type);
};
const SSHC = (data: string) => {
setSwitchState(data);
};
return(
<Paper
data-cy="quiz-question-card"
return (
<>
<Info
sx={{
maxWidth: "796px",
width: "100%",
borderRadius: "12px",
backgroundColor: expand ? "white" : "#EEE4FC",
border: expand ? "none" : "1px solid #9A9AAF",
boxShadow: "0px 10px 30px #e7e7e7",
m: "20px 0"
}}
>
<Box
sx={{
display: "flex",
alignItems: "center",
padding: isMobile ? "10px" : "20px",
flexDirection: isMobile ? "column" : null,
justifyContent: "space-between",
minHeight: "40px",
"MuiIconButton-root": {
}}
boxShadow: "0 0 10px 10px red"
}
}}
className={checkEmpty ? "blink" : ""}
onClick={handleClick}
/>
<Popover
id={id}
open={open}
anchorEl={anchorEl}
onClose={handleClose}
anchorOrigin={{
vertical: 'bottom',
horizontal: 'left',
}}
>
<Paper
sx={{
p: '20px',
display: "flex",
justifyContent: "center",
alignItems: "center",
flexDirection: "column"
}}
>
<Typography
sx={{
margin: isMobile ? "10px 0" : 0,
color: expand ? "#9A9AAF" : "#000000",
}}
>
Заголовок результата
<Typography>
{resultData?.content.rule.parentId === "line" ? "Единый результат в конце прохождения опросника без ветвления"
:
`Заголовок вопроса, после которого появится результат: "${question?.title || "нет заголовка"}"`
}
</Typography>
{checkEmpty &&
<Typography color="red">
Вы не заполнили этот результат никакими данными
</Typography>
<Box
}
</Paper>
</Popover>
</>
)
}
export const ResultCard = ({ resultContract, resultData }: Props) => {
console.log("resultData", resultData)
const quizQid = useCurrentQuiz()?.qid;
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isTablet = useMediaQuery(theme.breakpoints.down(800));
const [expand, setExpand] = React.useState(true)
const [resultCardSettings, setResultCardSettings] = React.useState(false)
const [buttonPlus, setButtonPlus] = React.useState(true)
React.useEffect(() => {
setExpand(true)
}, [resultContract])
const {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
originalImageUrl,
setCropModalImageBlob,
} = useCropModalState();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
async function handleImageUpload(file: File) {
const url = await uploadQuestionImage(resultData.id, quizQid, file, (question, url) => {
question.content.back = url;
question.content.originalBack = url;
});
closeImageUploadModal();
openCropModal(file, url);
}
function handleCropModalSaveClick(imageBlob: Blob) {
uploadQuestionImage(resultData.id, quizQid, imageBlob, (question, url) => {
question.content.back = url;
});
}
return (
<Paper
data-cy="quiz-question-card"
sx={{
maxWidth: "796px",
width: "100%",
borderRadius: "12px",
backgroundColor: expand ? "white" : "#EEE4FC",
border: expand ? "none" : "1px solid #9A9AAF",
boxShadow: "0px 10px 30px #e7e7e7",
m: "20px 0"
}}
>
<Box
sx={{
display: expand ? "none" : "flex",
alignItems: "center",
padding: isMobile ? "10px" : "20px",
flexDirection: isMobile ? "column" : null,
justifyContent: "space-between",
minHeight: "40px",
}}
>
<FormControl
variant="standard"
sx={{
p: 0,
maxWidth: isTablet ? "549px" : "640px",
width: "100%",
marginRight: isMobile ? "0px" : "16.1px",
}}
>
<TextField
value={resultData.title}
placeholder={"Заголовок результата"}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, question => question.title = target.value)}
sx={{
margin: isMobile ? "10px 0" : 0,
"& .MuiInputBase-root": {
color: "#000000",
backgroundColor: expand
? theme.palette.background.default
: "transparent",
height: "48px",
borderRadius: "10px",
".MuiOutlinedInput-notchedOutline": {
borderWidth: "1px !important",
border: !expand ? "none" : null,
},
"& .MuiInputBase-input::placeholder": {
color: "#4D4D4D",
opacity: 0.8,
},
},
}}
inputProps={{
sx: {
p: 0,
fontSize: "18px",
lineHeight: "21px",
},
}}
/>
</FormControl>
<Box
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
width: isMobile ? "100%" : "auto",
position: "relative",
}}
>
<IconButton
sx={{ padding: "0", margin: "5px" }}
disableRipple
data-cy="expand-question"
onClick={() => setExpand(!expand)}
>
{expand ? (
<ExpandLessIconBG />
) : (
<ExpandLessIcon
sx={{
display: "flex",
alignItems: "center",
justifyContent: "flex-end",
width: isMobile ? "100%" : "auto",
position: "relative",
boxSizing: "border-box",
fill: theme.palette.brightPurple.main,
background: "#FFF",
borderRadius: "6px",
height: "30px",
width: "30px",
}}
>
<IconButton
sx={{ padding: "0", margin: "5px" }}
disableRipple
data-cy="expand-question"
onClick={() => setExpand(!expand)}
>
{expand ? (
<ExpandLessIconBG />
) : (
<ExpandLessIcon
sx={{
boxSizing: "border-box",
fill: theme.palette.brightPurple.main,
background: "#FFF",
borderRadius: "6px",
height: "30px",
width: "30px",
}}
/>
)}
</IconButton>
</Box>
/>
)}
</IconButton>
<InfoView resultData={resultData} />
</Box>
{expand && (
<Box
</Box>
{expand && (
<>
<Box
sx={{
overflow: "hidden",
maxWidth: "796px",
@ -150,24 +289,48 @@ export const ResultCard = ({ resultContract }:Props) => {
mb: "19px",
}}
>
<CustomTextField placeholder="Заголовок вопроса" text={""} />
<IconButton>
<ExpandMoreIcon />
<CustomTextField
value={resultData.title}
placeholder={"Заголовок результата"}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, question => question.title = target.value)} />
<IconButton
sx={{ padding: "0", margin: "5px" }}
disableRipple
data-cy="expand-question"
onClick={() => setExpand(!expand)}
>
<ExpandLessIconBG />
</IconButton>
<OneIcon />
<PointsIcon style={{ color: "#9A9AAF" }} />
<InfoView resultData={resultData} />
</Box>
<Box sx={{ display: "flex" }}>
<PriceButtons
ButtonsActive={buttonsActive}
priceButtonsActive={priceButtonsActive}
<Box
sx={{
margin: "20px 0"
}}
>
<CustomTextField
value={resultData.description}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, (question) => question.description = target.value)}
placeholder={"Заголовок пожирнее"}
sx={{
borderRadius: "8px",
height: "48px",
width: "100%",
}}
/>
</Box>
<TextField
value={resultData.content.text}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, (question) => question.content.text = target.value)}
fullWidth
placeholder="Описание"
multiline
rows={4}
sx={{
"& .MuiInputBase-root": {
backgroundColor: "#F2F3F7",
@ -178,6 +341,7 @@ export const ResultCard = ({ resultContract }:Props) => {
}}
inputProps={{
sx: {
height: "85px",
borderRadius: "10px",
fontSize: "18px",
lineHeight: "21px",
@ -185,35 +349,183 @@ export const ResultCard = ({ resultContract }:Props) => {
},
}}
/>
<ImageAndVideoButtons />
{priceButtonsType === "smooth" ? (
<Box sx={{ mb: "20px" }}>
<Box sx={{ display: "flex", alignItems: "center", mb: "14xp" }}>
<Typography
component={"h6"}
sx={{ weight: "500", fontSize: "18px" }}
>
Призыв к действию
</Typography>
<IconButton sx={{ borderRadius: "6px", padding: "2px" }}>
<DeleteIcon style={{ color: "#4D4D4D" }} />
</IconButton>
<Box
sx={{
mt: "20px",
display: "flex",
gap: "10px",
flexDirection: "column"
}}
>
<Box
sx={{
display: "flex",
}}
>
<Button
sx={{
color: resultData.content.useImage ? "#7E2AEA" : "#9A9AAF",
fontSize: "16px",
"&:hover": {
background: "none",
},
}}
variant="text"
onClick={() => updateQuestion(resultData.id, (question) => question.content.useImage = true)}
>
Изображение
</Button>
<Button
sx={{
color: resultData.content.useImage ? "#9A9AAF" : "#7E2AEA",
fontSize: "16px",
"&:hover": {
background: "none",
},
}}
variant="text"
onClick={() => updateQuestion(resultData.id, (question) => question.content.useImage = false)}
>
Видео
</Button>
</Box>
<Box
sx={{
display: "flex",
flexDirection: "column",
}}
>
<UploadImageModal
isOpen={isImageUploadOpen}
onClose={closeImageUploadModal}
handleImageChange={handleImageUpload}
/>
<CropModal
isOpen={isCropModalOpen}
imageBlob={imageBlob}
originalImageUrl={originalImageUrl}
setCropModalImageBlob={setCropModalImageBlob}
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
/>
</Box>
{
resultData.content.useImage &&
<Box
sx={{
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "20px",
mb: "30px"
}}
>
<AddOrEditImageButton
imageSrc={resultData.content.back}
onImageClick={() => {
if (resultData.content.back) {
return openCropModal(
resultData.content.back,
resultData.content.originalBack
);
}
openImageUploadModal();
}}
onPlusClick={() => {
openImageUploadModal();
}}
/>
</Box>
<Box sx={{ display: "flex" }}>
}
{
!resultData.content.useImage &&
<Box
sx={{
cursor: "pointer",
display: "flex",
alignItems: "center",
gap: "20px",
mb: "30px"
}}
>
<CustomTextField
placeholder="URL видео"
text={resultData.content.video ?? ""}
onChange={e => updateQuestion(resultData.id, q => {
resultData.content.video = e.target.value;
})}
/>
</Box>
}
</Box>
{
buttonPlus ?
<Button
onClick={() => {
setButtonPlus(false)
}}
sx={{
display: "inline flex",
height: "48px",
padding: "10px 20px",
justifyContent: "center",
alignItems: "center",
gap: "8px",
flexShrink: 0,
borderRadius: "8px",
border: "1px solid #9A9AAF",
background: " #F2F3F7",
color: "#9A9AAF",
mb: "30px"
}}
>
Кнопка +
</Button>
:
<Box
sx={{
mb: "30px"
}}
>
<Box>
<Typography component={"span"} sx={{ weight: "500", fontSize: "18px", mb: "10px" }}>
Призыв к действию
</Typography>
<IconButton
onClick={() => {
setButtonPlus(true)
updateQuestion(resultData.id, (q) => q.content.hint.text = "")
}}
>
<Trash />
</IconButton>
</Box>
<TextField
placeholder="Узнать подробнее"
value={resultData.content.hint.text}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, (question) => question.content.hint.text = target.value)}
fullWidth
placeholder="Например: узнать подробнее"
sx={{
width: "410px",
"& .MuiInputBase-root": {
backgroundColor: "#F2F3F7",
width: "410px",
width: "409px",
height: "48px",
borderRadius: "10px",
borderRadius: "8px",
},
}}
inputProps={{
sx: {
height: "85px",
borderRadius: "10px",
fontSize: "18px",
lineHeight: "21px",
@ -221,71 +533,78 @@ export const ResultCard = ({ resultContract }:Props) => {
},
}}
/>
<Button
onClick={() => setForwarding(true)}
variant="outlined"
sx={{
display: forwarding ? "none" : "",
ml: "20px",
mb: "20px",
}}
>
Переадресация +
</Button>
{forwarding ? (
<Box sx={{ ml: "20px", mt: "-36px" }}>
<Box
sx={{ display: "flex", alignItems: "center", mb: "14xp" }}
>
<Typography
component={"h6"}
sx={{ weight: "500", fontSize: "18px" }}
>
Переадресация
</Typography>
<IconButton sx={{ borderRadius: "6px", padding: "2px" }}>
<DeleteIcon style={{ color: "#4D4D4D" }} />
</IconButton>
<Info />
</Box>
<Box>
<TextField
placeholder="https://exemple.ru"
fullWidth
sx={{
"& .MuiInputBase-root": {
backgroundColor: "#F2F3F7",
width: "326px",
height: "48px",
borderRadius: "10px",
},
}}
inputProps={{
sx: {
borderRadius: "10px",
fontSize: "18px",
lineHeight: "21px",
py: 0,
},
}}
/>
</Box>
</Box>
) : (
<></>
)}
</Box>
</Box>
) : (
<Button variant="outlined" sx={{ mb: "20px" }}>
Кнопка +
</Button>
)}
}
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
width: "100%",
background: "#F2F3F7",
}}
>
<Box
sx={{
padding: "20px",
display: "flex",
flexWrap: "wrap",
gap: "10px",
}}
>
<MiniButtonSetting
onClick={() => {
setResultCardSettings(!resultCardSettings)
}}
sx={{
backgroundColor:
resultCardSettings
? theme.palette.brightPurple.main
: "transparent",
color:
resultCardSettings ? "#ffffff" : theme.palette.grey3.main,
"&:hover": {
backgroundColor: resultCardSettings ? "#581CA7" : "#7E2AEA",
color: "white"
}
}}
>
<SettingIcon
color={
resultCardSettings ? "#ffffff" : theme.palette.grey3.main
}
/>
{!isTablet && "Настройки"}
</MiniButtonSetting>
</Box>
</Box>
<ButtonsOptionsForm switchState={switchState} SSHC={SSHC} />
<SwitchResult switchState={switchState} totalIndex={0} />
</Box>
)}
</Paper>
)
}
{
resultCardSettings &&
<Box
sx={{
backgroundColor: "white",
p: "20px",
borderRadius: "0 0 12px 12px"
}}
>
<CustomTextField
placeholder={"Внутреннее описание вопроса"}
value={resultData.innerName}
onChange={({ target }: { target: HTMLInputElement; }) => updateQuestion(resultData.id, (question) => question.content.innerName = target.value)}
/>
</Box>
}
</>
)
}
</Paper >
)
}

@ -10,8 +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 { updateRootContentId } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { QuestionsStore, useQuestionsStore } from "./store";
import { withErrorBoundary } from "react-error-boundary";
@ -90,6 +90,7 @@ const updateQuestionOrders = () => {
const questions = useQuestionsStore.getState().questions.filter(
(question): question is AnyTypedQuizQuestion => question.type !== null && question.type !== "result"
);
console.log(questions);
questions.forEach((question, index) => {
updateQuestion(question.id, question => {
@ -162,6 +163,10 @@ export const updateQuestion = (
try {
const response = await questionApi.edit(questionToEditQuestionRequest(q));
//Если мы делаем листочек веточкой - удаляем созданный к нему результ
const questionResult = useQuestionsStore.getState().questions.find(questionResult => questionResult.type === "result" && questionResult.content.rule.parentId === q.content.id);
if (questionResult && q.content.rule.default.length !== 0) deleteQuestion(questionResult.quizId);
deleteQuestion;
setQuestionBackendId(questionId, response.updated);
} catch (error) {
if (isAxiosCanceledError(error)) return;
@ -306,13 +311,15 @@ export const createTypedQuestion = async (
if (!question) return;
if (question.type !== null) throw new Error("Cannot upgrade already typed question");
const untypedOrResultQuestionsLength = questions.filter(q => q.type === "result" || q.type === null).length;
try {
const createdQuestion = await questionApi.create({
quiz_id: question.quizId,
type,
title: question.title,
description: question.description,
page: questions.length,
page: questions.length - untypedOrResultQuestionsLength,
required: false,
content: JSON.stringify(defaultQuestionByType[type].content),
});
@ -334,11 +341,13 @@ export const createTypedQuestion = async (
}
});
export const deleteQuestion = async (questionId: string, quizId: string) => requestQueue.enqueue(async () => {
export const deleteQuestion = async (questionId: string) => requestQueue.enqueue(async () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
if (question.type === null) {
removeQuestion(questionId);
return;
@ -346,47 +355,10 @@ export const deleteQuestion = async (questionId: string, quizId: string) => requ
try {
await questionApi.delete(question.backendId);
if (question.content.rule.parentId === "root") { //удалить из стора root и очистить rule всем вопросам
updateRootContentId(quizId, "")
clearRuleForAll()
} else if (question.content.rule.parentId.length > 0) { //удалить из стора вопрос из дерева и очистить его потомков
const clearQuestions = [] as string[]
const getChildren = (parentQuestion: AnyTypedQuizQuestion) => {
questions.forEach((targetQuestion) => {
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {//если у вопроса совпал родитель с родителем => он потомок, в кучу его
if (!clearQuestions.includes(targetQuestion.content.id)) clearQuestions.push(targetQuestion.content.id)
getChildren(targetQuestion) //и ищем его потомков
}
})
}
getChildren(question)
//чистим потомков от инфы ветвления
clearQuestions.forEach((id) => {
updateQuestion(id, question => {
question.content.rule.parentId = ""
question.content.rule.main = []
question.content.rule.default = ""
})
})
//чистим rule родителя
const parentQuestion = getQuestionByContentId(question.content.rule.parentId)
const newRule = {}
newRule.main = parentQuestion.content.rule.main.filter((data) => data.next !== question.content.id) //удаляем условия перехода от родителя к этому вопросу
newRule.parentId = parentQuestion.content.rule.parentId
newRule.default = questions.filter((q) => {
return q.content.rule.parentId === question.content.rule.parentId && q.content.id !== question.content.id
})[0]?.content.id || ""
//Если этот вопрос был дефолтным у родителя - чистим дефолт
//Смотрим можем ли мы заменить id на один из main
console.log(newRule)
updateQuestion(question.content.rule.parentId, (PQ) => {
PQ.content.rule = newRule
})
}
removeQuestion(questionId);
updateQuestionOrders();
} catch (error) {
devlog("Error deleting question", error);
enqueueSnackbar("Не удалось удалить вопрос");
@ -400,8 +372,8 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques
const frontId = nanoid();
if (question.type === null) {
const copiedQuestion = structuredClone(question);
copiedQuestion.id = frontId
copiedQuestion.content.id = frontId
copiedQuestion.id = frontId;
copiedQuestion.content.id = frontId;
setProducedState(state => {
state.questions.push(copiedQuestion);
@ -421,7 +393,7 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques
copiedQuestion.backendId = newQuestionId;
copiedQuestion.id = frontId;
copiedQuestion.content.id = frontId;
copiedQuestion.content.rule = { main: [], parentId: "", default: "" };
copiedQuestion.content.rule = { main: [], parentId: "", default: "", children: [] };
setProducedState(state => {
state.questions.push(copiedQuestion);
@ -469,41 +441,41 @@ export const updateDragQuestionContentId = (contentId?: string) => {
};
export const clearRuleForAll = () => {
const { questions } = useQuestionsStore.getState()
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 = ""
})
question.content.rule.parentId = "";
question.content.rule.main = [];
question.content.rule.default = "";
});
}
});
}
};
export const updateOpenBranchingPanel = (value: boolean) => useQuestionsStore.setState({ openBranchingPanel: value });
let UDTOABM: ReturnType<typeof setTimeout>;
export const updateDesireToOpenABranchingModal = (contentId: string) => {
useQuestionsStore.setState({ desireToOpenABranchingModal: contentId })
clearTimeout(UDTOABM)
useQuestionsStore.setState({ desireToOpenABranchingModal: contentId });
clearTimeout(UDTOABM);
UDTOABM = setTimeout(() => {
useQuestionsStore.setState({ desireToOpenABranchingModal: null })
}, 7000)
}
useQuestionsStore.setState({ desireToOpenABranchingModal: null });
}, 7000);
};
export const clearDesireToOpenABranchingModal = () => {
useQuestionsStore.setState({ desireToOpenABranchingModal: null })
}
useQuestionsStore.setState({ desireToOpenABranchingModal: null });
};
export const updateEditSomeQuestion = (contentId?: string) => {
useQuestionsStore.setState({ editSomeQuestion: contentId === undefined ? null : contentId })
}
useQuestionsStore.setState({ editSomeQuestion: contentId === undefined ? null : contentId });
};
export const createFrontResult = (quizId: number, parentContentId?: string) => setProducedState(state => {
const frontId = nanoid()
const content = JSON.parse(JSON.stringify(defaultQuestionByType["result"].content))
content.id = frontId
if (parentContentId) content.rule.parentId = parentContentId
const frontId = nanoid();
const content = JSON.parse(JSON.stringify(defaultQuestionByType["result"].content));
content.id = frontId;
if (parentContentId) content.rule.parentId = parentContentId;
state.questions.push({
id: frontId,
quizId,

@ -9,7 +9,6 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
export function useQuestions() {
console.log("вызываю вопросы")
const quiz = useCurrentQuiz();
const { isLoading, error, isValidating } = useSWR(["questions", quiz?.backendId], ([, id]) => questionApi.getList({ quiz_id: id }), {
onSuccess: setQuestions,

642
yarn.lock

File diff suppressed because it is too large Load Diff