diff --git a/src/constants/base.ts b/src/constants/base.ts index fbb62c3a..f588bfc6 100644 --- a/src/constants/base.ts +++ b/src/constants/base.ts @@ -18,6 +18,7 @@ export const QUIZ_QUESTION_BASE: Omit = { video: "", }, rule: { + children: [], main: [] as QuestionBranchingRuleMain[], parentId: "", default: "" diff --git a/src/model/questionTypes/shared.ts b/src/model/questionTypes/shared.ts index e0607c83..fb1ed933 100644 --- a/src/model/questionTypes/shared.ts +++ b/src/model/questionTypes/shared.ts @@ -23,6 +23,7 @@ export interface QuestionBranchingRuleMain { } export interface QuestionBranchingRule { + children: string[], //список условий main: QuestionBranchingRuleMain[]; parentId: string | null | "root"; diff --git a/src/pages/Questions/BranchingMap/CsComponent.tsx b/src/pages/Questions/BranchingMap/CsComponent.tsx index f34e47b7..6d4acac4 100644 --- a/src/pages/Questions/BranchingMap/CsComponent.tsx +++ b/src/pages/Questions/BranchingMap/CsComponent.tsx @@ -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"; @@ -111,13 +112,13 @@ interface Props { } -function CsComponent ({ +function CsComponent({ modalQuestionParentContentId, modalQuestionTargetContentId, setOpenedModalQuestions, setModalQuestionParentContentId, setModalQuestionTargetContentId -}: Props) { +}: Props) { const quiz = useCurrentQuiz(); const { dragQuestionContentId, desireToOpenABranchingModal } = useQuestionsStore() @@ -186,18 +187,32 @@ function CsComponent ({ } 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) } } @@ -231,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) { @@ -244,7 +267,7 @@ function CsComponent ({ } - //После всех манипуляций удаляем грани из CS и ноды из бекенда + //После всех манипуляций удаляем грани и ноды из CS Чистим rule потомков на беке deleteNodes.forEach((nodeId) => {//Ноды cy?.remove(cy?.$("#" + nodeId)) @@ -253,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) => {//Грани @@ -263,28 +289,41 @@ 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) - - + + 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 updateQuestion(parentQuestionContentId, (PQ) => { PQ.content.rule = newRule @@ -335,7 +374,7 @@ function CsComponent ({ positions: (e) => { if (!e.cy().data('changed')) { return e.data('oldPos') - } + } const id = e.id() const incomming = e.cy().edges(`[target="${id}"]`) const layer = 0 @@ -366,7 +405,7 @@ function CsComponent ({ while (queue.length) { const task = queue.pop() if (task.children.length === 0) { - task.parent.data('subtreeWidth', task.parent.height()+50) + task.parent.data('subtreeWidth', task.parent.height() + 50) continue } const unprocessed = task?.children.filter(e => { @@ -385,19 +424,19 @@ function CsComponent ({ const pos = { x: 0, y: 0 } e.data('oldPos', pos) - - queue.push({task: children, parent: e}) + + 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 + 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}) + 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) @@ -407,7 +446,7 @@ function CsComponent ({ const opos = e.data('oldPos') if (opos) { return opos - } + } } }, // map of (node id) => (position obj); or function(node){ return somPos; } zoom: undefined, // the zoom level to set (prob want fit = false if set) @@ -426,7 +465,7 @@ function CsComponent ({ console.log('KEKEKE') document.querySelector("#root")?.addEventListener("mouseup", cleardragQuestionContentId); const cy = cyRef.current; - const eles = cy?.add(storeToNodes(questions.filter((question:AnyTypedQuizQuestion) => (question.type !== "result" && question.type !== null)))) + 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() @@ -735,6 +774,23 @@ let pressed = false return ( <> + { cyRef.current = cy; }} - autoungrabify={true} + autoungrabify={true} /> - { - results.map((resultQuestion) => ) - } - + { + results.map((resultQuestion) => ) + } + + <> + ); }; diff --git a/src/pages/ResultPage/cards/ResultCard.tsx b/src/pages/ResultPage/cards/ResultCard.tsx index 07dbe86f..bd6702cd 100644 --- a/src/pages/ResultPage/cards/ResultCard.tsx +++ b/src/pages/ResultPage/cards/ResultCard.tsx @@ -1,18 +1,12 @@ 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 { useDebouncedCallback } from "use-debounce"; -import type { QuizQuestionPage } from "../../../model/questionTypes/page"; import { UploadImageModal } from "../../Questions/UploadImage/UploadImageModal"; -import { UploadVideoModal } from "../../Questions/UploadVideoModal"; import { useDisclosure } from "../../../utils/useDisclosure"; import AddOrEditImageButton from "@ui_kit/AddOrEditImageButton"; @@ -32,21 +26,18 @@ import MiniButtonSetting from "@ui_kit/MiniButtonSetting"; import ExpandLessIconBG from "@icons/ExpandLessIconBG"; import ExpandLessIcon from "@mui/icons-material/ExpandLess"; -import { OneIcon } from "@icons/questionsPage/OneIcon"; -import { DeleteIcon } from "@icons/questionsPage/deleteIcon"; 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; resultData: QuizQuestionResult; - setAlertLeave: () => void; } -const checkEmptyData = ({ resultData }: { resultData: QuizQuestionResult }) => { +export const checkEmptyData = ({ resultData }: { resultData: QuizQuestionResult }) => { let check = true if ( resultData.title.length > 0 || @@ -77,11 +68,6 @@ const InfoView = ({ resultData }: { resultData: QuizQuestionResult }) => { const open = Boolean(anchorEl); const id = open ? 'simple-popover' : undefined; - - - - - return ( <> { }} > - Заголовок вопроса, после которого появится результат: "{question?.title || "нет заголовка"}" + {resultData?.content.rule.parentId === "line" ? "Единый результат в конце прохождения опросника без ветвления" + : + `Заголовок вопроса, после которого появится результат: "${question?.title || "нет заголовка"}"` + } + {checkEmpty && @@ -129,13 +119,9 @@ const InfoView = ({ resultData }: { resultData: QuizQuestionResult }) => { ) } -export const ResultCard = ({ resultContract, resultData, setAlertLeave }: Props) => { +export const ResultCard = ({ resultContract, resultData }: Props) => { console.log("resultData", resultData) - React.useEffect(() => { - if (checkEmptyData({resultData})) setAlertLeave() - }, [resultData]) - const quizQid = useCurrentQuiz()?.qid; const theme = useTheme(); @@ -144,7 +130,7 @@ export const ResultCard = ({ resultContract, resultData, setAlertLeave }: Props) const [expand, setExpand] = React.useState(true) const [resultCardSettings, setResultCardSettings] = React.useState(false) - const [buttonPlus, setButtonPlus] = React.useState(false) + const [buttonPlus, setButtonPlus] = React.useState(true) React.useEffect(() => { setExpand(true) @@ -621,4 +607,4 @@ export const ResultCard = ({ resultContract, resultData, setAlertLeave }: Props) } ) -} \ No newline at end of file +} diff --git a/src/pages/ViewPublicationPage/Question.tsx b/src/pages/ViewPublicationPage/Question.tsx index a1af1373..c0d3cee2 100644 --- a/src/pages/ViewPublicationPage/Question.tsx +++ b/src/pages/ViewPublicationPage/Question.tsx @@ -20,8 +20,6 @@ import { useCurrentQuiz } from "@root/quizes/hooks"; import { getQuestionByContentId } from "@root/questions/actions"; type QuestionProps = { - stepNumber: number; - setStepNumber: (step: number) => void; questions: AnyTypedQuizQuestion[]; }; diff --git a/src/pages/ViewPublicationPage/index.tsx b/src/pages/ViewPublicationPage/index.tsx index e1212e42..38911b8f 100644 --- a/src/pages/ViewPublicationPage/index.tsx +++ b/src/pages/ViewPublicationPage/index.tsx @@ -11,7 +11,9 @@ import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared"; export const ViewPage = () => { const quiz = useCurrentQuiz(); const { questions } = useQuestions(); - const [visualStartPage, setVisualStartPage] = useState(!quiz?.config.noStartPage); + const [visualStartPage, setVisualStartPage] = useState( + !quiz?.config.noStartPage + ); useEffect(() => { const link = document.querySelector('link[rel="icon"]'); @@ -21,9 +23,9 @@ export const ViewPage = () => { } }, [quiz?.config.startpage.favIcon]); - const filteredQuestions = questions.filter( - ({ type }) => type - ) as AnyTypedQuizQuestion[]; + const filteredQuestions = ( + questions.filter(({ type }) => type) as AnyTypedQuizQuestion[] + ).sort((previousItem, item) => previousItem.page - item.page); return ( @@ -33,9 +35,7 @@ export const ViewPage = () => { showNextButton={!!filteredQuestions.length} /> ) : ( - + )} ); diff --git a/src/pages/ViewPublicationPage/questions/Date.tsx b/src/pages/ViewPublicationPage/questions/Date.tsx index ff64a02c..961a4daa 100644 --- a/src/pages/ViewPublicationPage/questions/Date.tsx +++ b/src/pages/ViewPublicationPage/questions/Date.tsx @@ -1,11 +1,12 @@ -import DatePicker from "react-datepicker"; +import { DatePicker } from "@mui/x-date-pickers"; import { Box, Typography } from "@mui/material"; import { useQuizViewStore, updateAnswer } from "@root/quizView"; -import "react-datepicker/dist/react-datepicker.css"; +// import "react-datepicker/dist/react-datepicker.css"; import type { QuizQuestionDate } from "../../../model/questionTypes/date"; +import CalendarIcon from "@icons/CalendarIcon"; type DateProps = { currentQuestion: QuizQuestionDate; @@ -31,6 +32,9 @@ export const Date = ({ currentQuestion }: DateProps) => { }} > , + }} selected={ answer ? new window.Date(`${month}.${day}.${year}`) @@ -48,6 +52,30 @@ export const Date = ({ currentQuestion }: DateProps) => { ) ) } + slotProps={{ + openPickerButton: { + sx: { + p: 0, + }, + "data-cy": "open-datepicker", + }, + }} + sx={{ + "& .MuiInputBase-root": { + backgroundColor: "#F2F3F7", + borderRadius: "10px", + maxWidth: "250px", + pr: "22px", + "& input": { + py: "11px", + pl: "20px", + lineHeight: "19px", + }, + "& fieldset": { + borderColor: "#9A9AAF", + }, + }, + }} /> diff --git a/src/pages/ViewPublicationPage/questions/Emoji.tsx b/src/pages/ViewPublicationPage/questions/Emoji.tsx index da34a1b5..42b69f6e 100644 --- a/src/pages/ViewPublicationPage/questions/Emoji.tsx +++ b/src/pages/ViewPublicationPage/questions/Emoji.tsx @@ -1,14 +1,14 @@ -import { useEffect } from "react"; import { Box, Typography, RadioGroup, FormControlLabel, Radio, - useTheme, FormControl, + useTheme, + FormControl, } from "@mui/material"; -import { useQuizViewStore, updateAnswer } from "@root/quizView"; +import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView"; import RadioCheck from "@ui_kit/RadioCheck"; import RadioIcon from "@ui_kit/RadioIcon"; @@ -22,20 +22,19 @@ type EmojiProps = { export const Emoji = ({ currentQuestion }: EmojiProps) => { const { answers } = useQuizViewStore(); const theme = useTheme(); - const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {}; - - useEffect(() => { - if (!answer) { - updateAnswer(currentQuestion.content.id, currentQuestion.content.variants[0].id); - } - }, []); + const { answer } = + answers.find( + ({ questionId }) => questionId === currentQuestion.content.id + ) ?? {}; return ( {currentQuestion.title} answer === id)} + value={currentQuestion.content.variants.findIndex( + ({ id }) => answer === id + )} onChange={({ target }) => updateAnswer( currentQuestion.content.id, @@ -51,50 +50,73 @@ export const Emoji = ({ currentQuestion }: EmojiProps) => { }} > - {currentQuestion.content.variants.map( - ({ id, answer, extendedText }, index) => ( - ( + + + - - - {extendedText && ( - {extendedText} - )} - + {variant.extendedText && ( + + {variant.extendedText} + + )} + + + { + event.preventDefault(); + + updateAnswer( + currentQuestion.content.id, + currentQuestion.content.variants[index].id + ); + + if (answer === currentQuestion.content.variants[index].id) { + deleteAnswer(currentQuestion.content.id); + } + }} + control={ + } icon={} /> + } + label={ + + {variant.answer} - } icon={} /> - } - label={ - - {answer} - - } - /> - - ) - )} + } + /> + + ))} diff --git a/src/pages/ViewPublicationPage/questions/Images.tsx b/src/pages/ViewPublicationPage/questions/Images.tsx index cdf07d89..3d131232 100644 --- a/src/pages/ViewPublicationPage/questions/Images.tsx +++ b/src/pages/ViewPublicationPage/questions/Images.tsx @@ -1,15 +1,14 @@ -import { useEffect } from "react"; import { - Box, - Typography, - RadioGroup, - FormControlLabel, - Radio, - useTheme, - useMediaQuery, FormControl, + Box, + Typography, + RadioGroup, + FormControlLabel, + Radio, + useTheme, + useMediaQuery, } from "@mui/material"; -import { useQuizViewStore, updateAnswer } from "@root/quizView"; +import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView"; import RadioCheck from "@ui_kit/RadioCheck"; import RadioIcon from "@ui_kit/RadioIcon"; @@ -22,28 +21,21 @@ type ImagesProps = { export const Images = ({ currentQuestion }: ImagesProps) => { const { answers } = useQuizViewStore(); const theme = useTheme(); - const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {}; + const { answer } = + answers.find( + ({ questionId }) => questionId === currentQuestion.content.id + ) ?? {}; const isTablet = useMediaQuery(theme.breakpoints.down(1000)); const isMobile = useMediaQuery(theme.breakpoints.down(500)); - useEffect(() => { - if (!answer) { - updateAnswer(currentQuestion.content.id, currentQuestion.content.variants[0].id); - } - }, []); - return ( {currentQuestion.title} answer === id)} - onChange={({ target }) => - updateAnswer( - currentQuestion.content.id, - currentQuestion.content.variants[Number(target.value)].id - ) - } + value={currentQuestion.content.variants.findIndex( + ({ id }) => answer === id + )} sx={{ display: "flex", flexWrap: "wrap", @@ -64,50 +56,58 @@ export const Images = ({ currentQuestion }: ImagesProps) => { width: "100%", }} > - {currentQuestion.content.variants.map( - ({ id, answer, extendedText }, index) => ( - - - - {extendedText && ( - - )} - + {currentQuestion.content.variants.map((variant, index) => ( + + + + {variant.extendedText && ( + + )} - } icon={} /> - } - label={answer} - /> - ) - )} + { + event.preventDefault(); + + updateAnswer( + currentQuestion.content.id, + currentQuestion.content.variants[index].id + ); + + if (answer === currentQuestion.content.variants[index].id) { + deleteAnswer(currentQuestion.content.id); + } + }} + value={index} + control={ + } icon={} /> + } + label={variant.answer} + /> + + ))} diff --git a/src/pages/ViewPublicationPage/questions/Number.tsx b/src/pages/ViewPublicationPage/questions/Number.tsx index 073b1102..6181161f 100644 --- a/src/pages/ViewPublicationPage/questions/Number.tsx +++ b/src/pages/ViewPublicationPage/questions/Number.tsx @@ -1,5 +1,6 @@ -import { useEffect } from "react"; +import { useState, useEffect } from "react"; import { Box, Typography, Slider, useTheme } from "@mui/material"; +import { useDebouncedCallback } from "use-debounce"; import CustomTextField from "@ui_kit/CustomTextField"; @@ -12,8 +13,30 @@ type NumberProps = { }; export const Number = ({ currentQuestion }: NumberProps) => { + const [minRange, setMinRange] = useState("0"); + const [maxRange, setMaxRange] = useState("100"); const theme = useTheme(); const { answers } = useQuizViewStore(); + const updateMinRangeDebounced = useDebouncedCallback( + (value, crowded = false) => { + if (crowded) { + setMinRange(maxRange); + } + + updateAnswer(currentQuestion.content.id, value); + }, + 1000 + ); + const updateMaxRangeDebounced = useDebouncedCallback( + (value, crowded = false) => { + if (crowded) { + setMaxRange(minRange); + } + + updateAnswer(currentQuestion.content.id, value); + }, + 1000 + ); const { answer } = answers.find( ({ questionId }) => questionId === currentQuestion.content.id @@ -23,6 +46,11 @@ export const Number = ({ currentQuestion }: NumberProps) => { const max = window.Number(currentQuestion.content.range.split("—")[1]); useEffect(() => { + console.log("ans", currentQuestion.content.start); + if (answer) { + setMinRange(answer.split("—")[0]); + setMaxRange(answer.split("—")[1]); + } if (!answer) { updateAnswer( currentQuestion.content.id, @@ -31,8 +59,11 @@ export const Number = ({ currentQuestion }: NumberProps) => { : String(currentQuestion.content.start), false ); + + setMinRange(String(currentQuestion.content.start)); + setMaxRange(String(max)); } - }, [answer]); + }, []); return ( @@ -45,14 +76,66 @@ export const Number = ({ currentQuestion }: NumberProps) => { marginTop: "20px", }} > + + 1 + ? answer?.split("—").map((item) => window.Number(item)) + : [min, min + 1] + : window.Number(answer || 1) + } + min={min} + max={max} + step={currentQuestion.content.step || 1} + sx={{ + color: theme.palette.brightPurple.main, + padding: "0", + marginTop: "75px", + "& .MuiSlider-valueLabel":{ + background: theme.palette.brightPurple.main, + borderRadius: "8px", + width: "60px", + height: "36px" + }, + "& .MuiSlider-valueLabel::before": { + width: "6px", + height: "2px", + transform: "translate(-50%, 50%) rotate(90deg)", + bottom: "-5px" + }, + "& .MuiSlider-rail": { + backgroundColor: "#F2F3F7", + border: `1px solid #9A9AAF`, + height: "12px" + }, + "& .MuiSlider-thumb": { + border: "3px #f2f3f7 solid", + height: "23px", + width: "23px" + }, + "& .MuiSlider-track": { + height: "12px" + } + }} + onChange={(_, value) => { + const range = String(value).replace(",", "—"); + updateAnswer(currentQuestion.content.id, range); + }} + onChangeCommitted={(_, value) => { + if (currentQuestion.content.chooseRange) { + const range = value as number[]; + + setMinRange(String(range[0])); + setMaxRange(String(range[1])); + } + }} + /> + {!currentQuestion.content.chooseRange && ( { updateAnswer( currentQuestion.content.id, @@ -69,6 +152,7 @@ export const Number = ({ currentQuestion }: NumberProps) => { }} /> )} + {currentQuestion.content.chooseRange && ( { > { - updateAnswer( - currentQuestion.content.id, - window.Number(target.value) > - window.Number(answer?.split("—")[1]) - ? `${answer?.split("—")[1]}—${answer?.split("—")[1]}` - : window.Number(target.value) < min - ? `${min}—${answer?.split("—")[1]}` - : `${target.value}—${answer?.split("—")[1]}` - ); + setMinRange(target.value); + + if (window.Number(target.value) >= window.Number(maxRange)) { + updateMinRangeDebounced(`${maxRange}—${maxRange}`, true); + + return; + } + + updateMinRangeDebounced(`${target.value}—${maxRange}`); }} sx={{ maxWidth: "80px", "& .MuiInputBase-input": { textAlign: "center" }, }} /> + + до + { - updateAnswer( - currentQuestion.content.id, - window.Number(target.value) > max - ? `${answer?.split("—")[0]}—${max}` - : window.Number(target.value) < - window.Number(answer?.split("—")[0]) - ? `${answer?.split("—")[0]}—${answer?.split("—")[0]}` - : `${answer?.split("—")[0]}—${target.value}` - ); + setMaxRange(target.value); + + if (window.Number(target.value) <= window.Number(minRange)) { + updateMaxRangeDebounced(`${minRange}—${minRange}`, true); + + return; + } + + updateMaxRangeDebounced(`${minRange}—${target.value}`); }} sx={{ maxWidth: "80px", @@ -121,29 +204,7 @@ export const Number = ({ currentQuestion }: NumberProps) => { /> )} - 1 - ? answer?.split("—").map((item) => window.Number(item)) - : [min, min + 1] - : window.Number(answer || 1) - } - min={min} - max={max} - step={currentQuestion.content.step || 1} - sx={{ - color: theme.palette.brightPurple.main, - padding: "0", - marginTop: "25px", - }} - onChange={(_, value) => { - updateAnswer( - currentQuestion.content.id, - String(value).replace(",", "—") - ); - }} - /> + ); diff --git a/src/pages/ViewPublicationPage/questions/Rating.tsx b/src/pages/ViewPublicationPage/questions/Rating.tsx index 83b8ea09..c3eb94a5 100644 --- a/src/pages/ViewPublicationPage/questions/Rating.tsx +++ b/src/pages/ViewPublicationPage/questions/Rating.tsx @@ -18,36 +18,38 @@ type RatingProps = { export const Rating = ({ currentQuestion }: RatingProps) => { const { answers } = useQuizViewStore(); const theme = useTheme(); - const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {}; + const { answer } = + answers.find( + ({ questionId }) => questionId === currentQuestion.content.id + ) ?? {}; return ( {currentQuestion.title} updateAnswer(currentQuestion.content.id, String(value))} + onChange={(_, value) => + updateAnswer(currentQuestion.content.id, String(value)) + } sx={{ height: "50px" }} max={currentQuestion.content.steps} icon={ } emptyIcon={ } /> @@ -59,8 +61,12 @@ export const Rating = ({ currentQuestion }: RatingProps) => { color: theme.palette.grey2.main, }} > - {currentQuestion.content.ratingNegativeDescription} - {currentQuestion.content.ratingPositiveDescription} + + {currentQuestion.content.ratingNegativeDescription} + + + {currentQuestion.content.ratingPositiveDescription} + diff --git a/src/pages/ViewPublicationPage/questions/Select.tsx b/src/pages/ViewPublicationPage/questions/Select.tsx index 2c90f3d4..0a8c568b 100644 --- a/src/pages/ViewPublicationPage/questions/Select.tsx +++ b/src/pages/ViewPublicationPage/questions/Select.tsx @@ -12,7 +12,10 @@ type SelectProps = { export const Select = ({ currentQuestion }: SelectProps) => { const { answers } = useQuizViewStore(); - const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {}; + const { answer } = + answers.find( + ({ questionId }) => questionId === currentQuestion.content.id + ) ?? {}; return ( @@ -26,7 +29,7 @@ export const Select = ({ currentQuestion }: SelectProps) => { }} > answer)} onChange={(_, value) => { updateAnswer(currentQuestion.content.id, String(value)); diff --git a/src/pages/ViewPublicationPage/questions/Variant.tsx b/src/pages/ViewPublicationPage/questions/Variant.tsx index ad98195a..d3857aac 100644 --- a/src/pages/ViewPublicationPage/questions/Variant.tsx +++ b/src/pages/ViewPublicationPage/questions/Variant.tsx @@ -1,4 +1,3 @@ -import { useEffect } from "react"; import { Box, Typography, @@ -8,7 +7,7 @@ import { useTheme, } from "@mui/material"; -import { useQuizViewStore, updateAnswer } from "@root/quizView"; +import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView"; import RadioCheck from "@ui_kit/RadioCheck"; import RadioIcon from "@ui_kit/RadioIcon"; @@ -23,13 +22,10 @@ type VariantProps = { export const Variant = ({ currentQuestion }: VariantProps) => { const { answers } = useQuizViewStore(); const theme = useTheme(); - const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {}; - - useEffect(() => { - if (!answer) { - updateAnswer(currentQuestion.content.id, currentQuestion.content.variants[0].id); - } - }, []); + const { answer } = + answers.find( + ({ questionId }) => questionId === currentQuestion.content.id + ) ?? {}; return ( @@ -37,13 +33,9 @@ export const Variant = ({ currentQuestion }: VariantProps) => { answer === id)} - onChange={({ target }) => - updateAnswer( - currentQuestion.content.id, - currentQuestion.content.variants[Number(target.value)].id - ) - } + value={currentQuestion.content.variants.findIndex( + ({ id }) => answer === id + )} sx={{ display: "flex", flexWrap: "wrap", @@ -53,10 +45,18 @@ export const Variant = ({ currentQuestion }: VariantProps) => { marginTop: "20px", }} > - - {currentQuestion.content.variants.map(({ id, answer }, index) => ( + + {currentQuestion.content.variants.map((variant, index) => ( { display: "flex", maxWidth: "685px", justifyContent: "space-between", - width: "100%" + width: "100%", }} value={index} labelPlacement="start" control={ } icon={} /> } - label={answer} + label={variant.answer} + onClick={(event) => { + event.preventDefault(); + + updateAnswer( + currentQuestion.content.id, + currentQuestion.content.variants[index].id + ); + + if (answer === currentQuestion.content.variants[index].id) { + deleteAnswer(currentQuestion.content.id); + } + }} /> ))} diff --git a/src/pages/ViewPublicationPage/questions/Varimg.tsx b/src/pages/ViewPublicationPage/questions/Varimg.tsx index 83dee1d9..87c0a1fb 100644 --- a/src/pages/ViewPublicationPage/questions/Varimg.tsx +++ b/src/pages/ViewPublicationPage/questions/Varimg.tsx @@ -1,4 +1,3 @@ -import { useEffect } from "react"; import { Box, Typography, @@ -8,7 +7,7 @@ import { useTheme, } from "@mui/material"; -import { useQuizViewStore, updateAnswer } from "@root/quizView"; +import { useQuizViewStore, updateAnswer, deleteAnswer } from "@root/quizView"; import RadioCheck from "@ui_kit/RadioCheck"; import RadioIcon from "@ui_kit/RadioIcon"; @@ -22,28 +21,23 @@ type VarimgProps = { export const Varimg = ({ currentQuestion }: VarimgProps) => { const { answers } = useQuizViewStore(); const theme = useTheme(); - const { answer } = answers.find(({ questionId }) => questionId === currentQuestion.content.id) ?? {}; - const variant = currentQuestion.content.variants.find(({ id }) => answer === id); - - useEffect(() => { - if (!answer) { - updateAnswer(currentQuestion.content.id, currentQuestion.content.variants[0].id); - } - }, []); + const { answer } = + answers.find( + ({ questionId }) => questionId === currentQuestion.content.id + ) ?? {}; + const variant = currentQuestion.content.variants.find( + ({ id }) => answer === id + ); return ( {currentQuestion.title} - + answer === id)} - onChange={({ target }) => - updateAnswer( - currentQuestion.content.id, - currentQuestion.content.variants[Number(target.value)].id - ) - } + value={currentQuestion.content.variants.findIndex( + ({ id }) => answer === id + )} sx={{ display: "flex", flexWrap: "wrap", @@ -53,9 +47,9 @@ export const Varimg = ({ currentQuestion }: VarimgProps) => { }} > - {currentQuestion.content.variants.map(({ id, answer }, index) => ( + {currentQuestion.content.variants.map((variant, index) => ( { display: "flex", }} value={index} + onClick={(event) => { + event.preventDefault(); + + updateAnswer( + currentQuestion.content.id, + currentQuestion.content.variants[index].id + ); + + if (answer === currentQuestion.content.variants[index].id) { + deleteAnswer(currentQuestion.content.id); + } + }} control={ } icon={} /> } - label={answer} + label={variant.answer} /> ))} {(variant?.extendedText || currentQuestion.content.back) && ( - + diff --git a/src/stores/questions/actions.ts b/src/stores/questions/actions.ts index 20f4125e..920b64c9 100644 --- a/src/stores/questions/actions.ts +++ b/src/stores/questions/actions.ts @@ -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,7 +90,7 @@ const updateQuestionOrders = () => { const questions = useQuestionsStore.getState().questions.filter( (question): question is AnyTypedQuizQuestion => question.type !== null && question.type !== "result" ); - console.log(questions) + console.log(questions); questions.forEach((question, index) => { updateQuestion(question.id, question => { @@ -163,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; @@ -307,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), }); @@ -335,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; @@ -347,46 +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 - - 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; 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, diff --git a/src/stores/quizView.ts b/src/stores/quizView.ts index e8a1b8ba..a8321f44 100644 --- a/src/stores/quizView.ts +++ b/src/stores/quizView.ts @@ -41,3 +41,12 @@ export const updateAnswer = ( useQuizViewStore.setState({ answers }); }; + +export const deleteAnswer = (questionId: string) => { + const answers = [...useQuizViewStore.getState().answers]; + const filteredItems = answers.filter( + (answer) => questionId !== answer.questionId + ); + + useQuizViewStore.setState({ answers: filteredItems }); +}; diff --git a/src/ui_kit/StartPagePreview/QuizPreviewLayout.tsx b/src/ui_kit/StartPagePreview/QuizPreviewLayout.tsx index bc9b9ee3..e5984300 100644 --- a/src/ui_kit/StartPagePreview/QuizPreviewLayout.tsx +++ b/src/ui_kit/StartPagePreview/QuizPreviewLayout.tsx @@ -82,17 +82,15 @@ export default function QuizPreviewLayout() { {quiz.config.startpage.description} - {quiz.config.startpage.button && ( - - )} +