панель бранчевания
This commit is contained in:
parent
cbfa4a13d8
commit
658da99220
3
.vscode/settings.json
vendored
Normal file
3
.vscode/settings.json
vendored
Normal file
@ -0,0 +1,3 @@
|
|||||||
|
{
|
||||||
|
"typescript.tsdk": "node_modules/typescript/lib"
|
||||||
|
}
|
18897
package-lock.json
generated
Normal file
18897
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
13
package.json
13
package.json
@ -13,6 +13,7 @@
|
|||||||
"@testing-library/jest-dom": "^5.14.1",
|
"@testing-library/jest-dom": "^5.14.1",
|
||||||
"@testing-library/react": "^13.0.0",
|
"@testing-library/react": "^13.0.0",
|
||||||
"@testing-library/user-event": "^13.2.1",
|
"@testing-library/user-event": "^13.2.1",
|
||||||
|
"@types/cytoscape": "^3.19.16",
|
||||||
"@types/file-saver": "^2.0.5",
|
"@types/file-saver": "^2.0.5",
|
||||||
"@types/jest": "^27.0.1",
|
"@types/jest": "^27.0.1",
|
||||||
"@types/node": "^16.7.13",
|
"@types/node": "^16.7.13",
|
||||||
@ -21,6 +22,7 @@
|
|||||||
"@types/react-dom": "^18.0.0",
|
"@types/react-dom": "^18.0.0",
|
||||||
"axios": "^1.5.1",
|
"axios": "^1.5.1",
|
||||||
"cytoscape": "^3.26.0",
|
"cytoscape": "^3.26.0",
|
||||||
|
"cytoscape-popper": "^2.0.0",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"emoji-mart": "^5.5.2",
|
"emoji-mart": "^5.5.2",
|
||||||
"file-saver": "^2.0.5",
|
"file-saver": "^2.0.5",
|
||||||
@ -43,7 +45,7 @@
|
|||||||
"react-router-dom": "^6.6.2",
|
"react-router-dom": "^6.6.2",
|
||||||
"react-scripts": "5.0.1",
|
"react-scripts": "5.0.1",
|
||||||
"swr": "^2.2.4",
|
"swr": "^2.2.4",
|
||||||
"typescript": "^4.4.2",
|
"typescript": "^5.2.2",
|
||||||
"use-debounce": "^9.0.4",
|
"use-debounce": "^9.0.4",
|
||||||
"web-vitals": "^2.1.0",
|
"web-vitals": "^2.1.0",
|
||||||
"yup": "^1.3.2",
|
"yup": "^1.3.2",
|
||||||
@ -56,12 +58,6 @@
|
|||||||
"eject": "craco eject",
|
"eject": "craco eject",
|
||||||
"cypress:open": "cypress open"
|
"cypress:open": "cypress open"
|
||||||
},
|
},
|
||||||
"eslintConfig": {
|
|
||||||
"extends": [
|
|
||||||
"react-app",
|
|
||||||
"react-app/jest"
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"browserslist": {
|
"browserslist": {
|
||||||
"production": [
|
"production": [
|
||||||
">0.2%",
|
">0.2%",
|
||||||
@ -77,8 +73,9 @@
|
|||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@emoji-mart/data": "^1.1.2",
|
"@emoji-mart/data": "^1.1.2",
|
||||||
"@emoji-mart/react": "^1.1.1",
|
"@emoji-mart/react": "^1.1.1",
|
||||||
|
"@types/cytoscape-popper": "^2.0.4",
|
||||||
"@types/react-beautiful-dnd": "^13.1.4",
|
"@types/react-beautiful-dnd": "^13.1.4",
|
||||||
"@types/react-cytoscapejs": "^1.2.4",
|
"@types/react-cytoscapejs": "^1.2.5",
|
||||||
"craco-alias": "^3.0.1",
|
"craco-alias": "^3.0.1",
|
||||||
"cypress": "^13.4.0"
|
"cypress": "^13.4.0"
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ const defaultCreateQuizBody: CreateQuizRequest = {
|
|||||||
"note_prevented": true,
|
"note_prevented": true,
|
||||||
"mail_notifications": false,
|
"mail_notifications": false,
|
||||||
"unique_answers": true,
|
"unique_answers": true,
|
||||||
"name": "Название квиза",
|
"name": "",
|
||||||
"description": "",
|
"description": "",
|
||||||
"config": JSON.stringify(defaultQuizConfig),
|
"config": JSON.stringify(defaultQuizConfig),
|
||||||
"status": "stop",
|
"status": "stop",
|
||||||
|
6
src/assets/icons/ArrowGear.svg
Normal file
6
src/assets/icons/ArrowGear.svg
Normal file
File diff suppressed because one or more lines are too long
After Width: | Height: | Size: 7.7 KiB |
4
src/assets/icons/checked.svg
Normal file
4
src/assets/icons/checked.svg
Normal file
@ -0,0 +1,4 @@
|
|||||||
|
<svg width="24" height="24" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<path d="M18 -2.62268e-07C21.3137 -1.17422e-07 24 2.68629 24 6L24 18C24 21.3137 21.3137 24 18 24L6 24C2.68629 24 -9.31652e-07 21.3137 -7.86805e-07 18L-5.24537e-07 12L-2.62268e-07 6C-1.17422e-07 2.68629 2.68629 -9.31652e-07 6 -7.86805e-07L18 -2.62268e-07Z" fill="#9A9AAF" fill-opacity="0.7"/>
|
||||||
|
<path d="M7 11.5L11.2857 15.5L17 8" stroke="white" stroke-linecap="round" stroke-linejoin="round"/>
|
||||||
|
</svg>
|
After Width: | Height: | Size: 495 B |
@ -1,4 +1,4 @@
|
|||||||
import type { QuizQuestionBase } from "../model/questionTypes/shared";
|
import type { QuizQuestionBase, QuestionBranchingRuleMain } from "../model/questionTypes/shared";
|
||||||
|
|
||||||
|
|
||||||
export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id" | "backendId"> = {
|
export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id" | "backendId"> = {
|
||||||
@ -17,15 +17,9 @@ export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id" | "backendId"> = {
|
|||||||
video: "",
|
video: "",
|
||||||
},
|
},
|
||||||
rule: {
|
rule: {
|
||||||
or: true,
|
main: [] as QuestionBranchingRuleMain[],
|
||||||
show: true,
|
parentId: "",
|
||||||
title: "",
|
default: ""
|
||||||
reqs: [
|
|
||||||
{
|
|
||||||
id: "",
|
|
||||||
vars: [],
|
|
||||||
},
|
|
||||||
],
|
|
||||||
},
|
},
|
||||||
back: "",
|
back: "",
|
||||||
originalBack: "",
|
originalBack: "",
|
||||||
|
@ -12,17 +12,20 @@ import type { QuizQuestionVariant } from "./variant";
|
|||||||
import type { QuizQuestionVarImg } from "./varimg";
|
import type { QuizQuestionVarImg } from "./varimg";
|
||||||
import { nanoid } from "nanoid";
|
import { nanoid } from "nanoid";
|
||||||
|
|
||||||
|
export interface QuestionBranchingRuleMain {
|
||||||
|
next: string;
|
||||||
|
or: boolean;
|
||||||
|
rules: {
|
||||||
|
question: string; //id родителя (пока что)
|
||||||
|
answers: string[]
|
||||||
|
}[]
|
||||||
|
}
|
||||||
export interface QuestionBranchingRule {
|
export interface QuestionBranchingRule {
|
||||||
/** Радиокнопка "Все условия обязательны" */
|
|
||||||
or: boolean;
|
//список условий
|
||||||
show: boolean;
|
main: QuestionBranchingRuleMain[];
|
||||||
title: string;
|
parentId: string | null | "root";
|
||||||
reqs: {
|
default: string;
|
||||||
id: string;
|
|
||||||
/** Список выбранных вариантов */
|
|
||||||
vars: number[];
|
|
||||||
}[];
|
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuestionHint {
|
export interface QuestionHint {
|
||||||
@ -95,6 +98,14 @@ type FilterQuestionsWithVariants<T> = T extends {
|
|||||||
export type QuizQuestionsWithVariants = FilterQuestionsWithVariants<AnyQuizQuestion>;
|
export type QuizQuestionsWithVariants = FilterQuestionsWithVariants<AnyQuizQuestion>;
|
||||||
|
|
||||||
|
|
||||||
|
export const createBranchingRuleMain: (targetId:string, parentId:string) => QuestionBranchingRuleMain = (targetId, parentId) => ({
|
||||||
|
next: targetId,
|
||||||
|
or: false,
|
||||||
|
rules: [{
|
||||||
|
question: parentId,
|
||||||
|
answers: [] as string[],
|
||||||
|
}]
|
||||||
|
})
|
||||||
export const createQuestionVariant: () => QuestionVariant = () => ({
|
export const createQuestionVariant: () => QuestionVariant = () => ({
|
||||||
id: nanoid(),
|
id: nanoid(),
|
||||||
answer: "",
|
answer: "",
|
||||||
|
@ -33,6 +33,7 @@ export interface QuizConfig {
|
|||||||
noStartPage: boolean;
|
noStartPage: boolean;
|
||||||
startpageType: QuizStartpageType;
|
startpageType: QuizStartpageType;
|
||||||
results: QuizResultsType;
|
results: QuizResultsType;
|
||||||
|
haveRoot: boolean;
|
||||||
startpage: {
|
startpage: {
|
||||||
description: string;
|
description: string;
|
||||||
button: string;
|
button: string;
|
||||||
@ -61,6 +62,7 @@ export const defaultQuizConfig: QuizConfig = {
|
|||||||
noStartPage: false,
|
noStartPage: false,
|
||||||
startpageType: null,
|
startpageType: null,
|
||||||
results: null,
|
results: null,
|
||||||
|
haveRoot: false,
|
||||||
startpage: {
|
startpage: {
|
||||||
description: "",
|
description: "",
|
||||||
button: "",
|
button: "",
|
||||||
|
@ -1,7 +1,7 @@
|
|||||||
import { MessageIcon } from "@icons/messagIcon";
|
import { MessageIcon } from "@icons/messagIcon";
|
||||||
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
|
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
|
||||||
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
||||||
import TextareaAutosize from "@mui/base/TextareaAutosize";
|
import { TextareaAutosize } from "@mui/base/TextareaAutosize";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
FormControl,
|
FormControl,
|
||||||
@ -72,7 +72,7 @@ export const AnswerItem = ({
|
|||||||
focused={false}
|
focused={false}
|
||||||
placeholder={"Добавьте ответ"}
|
placeholder={"Добавьте ответ"}
|
||||||
multiline={largeCheck}
|
multiline={largeCheck}
|
||||||
onChange={({ target }) => {
|
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
|
||||||
setQuestionVariantField(questionId, variant.id, "answer", target.value)
|
setQuestionVariantField(questionId, variant.id, "answer", target.value)
|
||||||
}}
|
}}
|
||||||
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
|
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
|
||||||
|
398
src/pages/Questions/BranchingMap/CsComponent.tsx
Normal file
398
src/pages/Questions/BranchingMap/CsComponent.tsx
Normal file
@ -0,0 +1,398 @@
|
|||||||
|
import { useEffect, useRef, useState } from "react";
|
||||||
|
import Cytoscape from "cytoscape";
|
||||||
|
import CytoscapeComponent from "react-cytoscapejs";
|
||||||
|
import popper from "cytoscape-popper";
|
||||||
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
|
|
||||||
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
|
import { clearDragQuestionId } from "@root/questions/actions";
|
||||||
|
|
||||||
|
import { storeToNodes } from "./helper";
|
||||||
|
|
||||||
|
import "./styles.css";
|
||||||
|
|
||||||
|
import type {
|
||||||
|
Stylesheet,
|
||||||
|
Core,
|
||||||
|
NodeSingular,
|
||||||
|
AbstractEventObject,
|
||||||
|
ElementDefinition,
|
||||||
|
} from "cytoscape";
|
||||||
|
import { QuestionsList } from "../BranchingPanel/QuestionsList";
|
||||||
|
|
||||||
|
type PopperItem = {
|
||||||
|
id: () => string;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Modifier = {
|
||||||
|
name: string;
|
||||||
|
options: unknown;
|
||||||
|
};
|
||||||
|
|
||||||
|
type PopperConfig = {
|
||||||
|
popper: {
|
||||||
|
placement: string;
|
||||||
|
modifiers?: Modifier[];
|
||||||
|
};
|
||||||
|
content: (items: PopperItem[]) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type Popper = {
|
||||||
|
update: () => Promise<void>;
|
||||||
|
setOptions: (modifiers: { modifiers?: Modifier[] }) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
type NodeSingularWithPopper = NodeSingular & {
|
||||||
|
popper: (config: PopperConfig) => Popper;
|
||||||
|
};
|
||||||
|
|
||||||
|
const stylesheet: Stylesheet[] = [
|
||||||
|
{
|
||||||
|
selector: "node",
|
||||||
|
style: {
|
||||||
|
shape: "round-rectangle",
|
||||||
|
width: 130,
|
||||||
|
height: 130,
|
||||||
|
backgroundColor: "#FFFFFF",
|
||||||
|
label: "data(label)",
|
||||||
|
"font-size": "16",
|
||||||
|
color: "#4D4D4D",
|
||||||
|
"text-halign": "center",
|
||||||
|
"text-valign": "center",
|
||||||
|
"text-wrap": "wrap",
|
||||||
|
"text-max-width": "80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: ".multiline-auto",
|
||||||
|
style: {
|
||||||
|
"text-wrap": "wrap",
|
||||||
|
"text-max-width": "80",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: "edge",
|
||||||
|
style: {
|
||||||
|
width: 30,
|
||||||
|
"line-color": "#DEDFE7",
|
||||||
|
"curve-style": "taxi",
|
||||||
|
"taxi-direction": "horizontal",
|
||||||
|
"taxi-turn": 60,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
selector: ":selected",
|
||||||
|
style: {
|
||||||
|
"border-style": "solid",
|
||||||
|
"border-width": 1.5,
|
||||||
|
"border-color": "#9A9AAF",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
];
|
||||||
|
|
||||||
|
Cytoscape.use(popper);
|
||||||
|
|
||||||
|
export const CsComponent = () => {
|
||||||
|
const quiz = useCurrentQuiz();
|
||||||
|
|
||||||
|
const { dragQuestionId, questions } = useQuestionsStore()
|
||||||
|
const [startCreate, setStartCreate] = useState("");
|
||||||
|
const [startRemove, setStartRemove] = useState("");
|
||||||
|
|
||||||
|
const cyRef = useRef<Core | null>(null);
|
||||||
|
const plusesContainer = useRef<HTMLDivElement | null>(null);
|
||||||
|
const crossesContainer = useRef<HTMLDivElement | null>(null);
|
||||||
|
const gearsContainer = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
|
const addNode = ({ parentNodeId }: { parentNodeId: string }) => {
|
||||||
|
const cy = cyRef?.current
|
||||||
|
|
||||||
|
//Если детей больше 1 - предупреждаем стор вопросов об открытии модалки ветвления
|
||||||
|
// if (Object.keys(currentNode.children).length > 1) {
|
||||||
|
// setOpenedModalSettings(question.index)
|
||||||
|
// } else {
|
||||||
|
// //Если ребёнок первый - добавляем его родителю как дефолтный
|
||||||
|
// parentQuestion.question.content.rule.default = Object.keys(newNode)[0].split("_").pop()
|
||||||
|
// updateQuestionsList(quiz, parentQuestion.index, parentQuestion.question);
|
||||||
|
// }
|
||||||
|
|
||||||
|
|
||||||
|
console.log(dragQuestionId)
|
||||||
|
// cy?.add({
|
||||||
|
|
||||||
|
// })
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (startCreate) {
|
||||||
|
addNode({ parentNodeId: startCreate });
|
||||||
|
clearDragQuestionId()
|
||||||
|
setStartCreate("");
|
||||||
|
}
|
||||||
|
}, [startCreate]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (startRemove) {
|
||||||
|
// removeNode(quiz, startRemove);
|
||||||
|
setStartRemove("");
|
||||||
|
}
|
||||||
|
}, [startRemove]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
document.querySelector("#root")?.addEventListener("mouseup", clearDragQuestionId);
|
||||||
|
const cy = cyRef.current;
|
||||||
|
cy?.add(storeToNodes(questions))
|
||||||
|
|
||||||
|
return () => {
|
||||||
|
document.querySelector("#root")?.removeEventListener("mouseup", clearDragQuestionId);
|
||||||
|
plusesContainer.current?.remove();
|
||||||
|
crossesContainer.current?.remove();
|
||||||
|
gearsContainer.current?.remove();
|
||||||
|
};
|
||||||
|
}, []);
|
||||||
|
|
||||||
|
|
||||||
|
const removeButtons = (id: string) => {
|
||||||
|
plusesContainer.current
|
||||||
|
?.querySelector(`.popper-plus[data-id='${id}']`)
|
||||||
|
?.remove();
|
||||||
|
crossesContainer.current
|
||||||
|
?.querySelector(`.popper-cross[data-id='${id}']`)
|
||||||
|
?.remove();
|
||||||
|
gearsContainer.current
|
||||||
|
?.querySelector(`.popper-gear[data-id='${id}']`)
|
||||||
|
?.remove();
|
||||||
|
};
|
||||||
|
|
||||||
|
const initialCS = () => {
|
||||||
|
const cy = cyRef.current;
|
||||||
|
const container =
|
||||||
|
(document.body.querySelector(
|
||||||
|
".__________cytoscape_container"
|
||||||
|
) as HTMLDivElement) || null;
|
||||||
|
|
||||||
|
if (!container) {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
container.style.overflow = "hidden";
|
||||||
|
|
||||||
|
if (!plusesContainer.current) {
|
||||||
|
plusesContainer.current = document.createElement("div");
|
||||||
|
plusesContainer.current.setAttribute("id", "popper-pluses");
|
||||||
|
container.append(plusesContainer.current);
|
||||||
|
}
|
||||||
|
if (!crossesContainer.current) {
|
||||||
|
crossesContainer.current = document.createElement("div");
|
||||||
|
crossesContainer.current.setAttribute("id", "popper-crosses");
|
||||||
|
container.append(crossesContainer.current);
|
||||||
|
}
|
||||||
|
if (!gearsContainer.current) {
|
||||||
|
gearsContainer.current = document.createElement("div");
|
||||||
|
gearsContainer.current.setAttribute("id", "popper-gears");
|
||||||
|
container.append(gearsContainer.current);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
cy?.nodes()
|
||||||
|
.toArray()
|
||||||
|
?.forEach((item) => {
|
||||||
|
const node = item as NodeSingularWithPopper;
|
||||||
|
|
||||||
|
const plusesPopper = node.popper({
|
||||||
|
popper: {
|
||||||
|
placement: "right",
|
||||||
|
modifiers: [{ name: "flip", options: { boundary: node } }],
|
||||||
|
},
|
||||||
|
content: ([item]) => {
|
||||||
|
const itemId = item.id();
|
||||||
|
const itemElement = plusesContainer.current?.querySelector(
|
||||||
|
`.popper-plus[data-id='${itemId}']`
|
||||||
|
);
|
||||||
|
if (itemElement) {
|
||||||
|
return itemElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const plusElement = document.createElement("div");
|
||||||
|
plusElement.classList.add("popper-plus");
|
||||||
|
plusElement.setAttribute("data-id", item.id());
|
||||||
|
plusesContainer.current?.appendChild(plusElement);
|
||||||
|
plusElement.addEventListener("mouseup", () =>
|
||||||
|
setStartCreate(node.id())
|
||||||
|
);
|
||||||
|
|
||||||
|
return plusElement;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const crossesPopper = node.popper({
|
||||||
|
popper: {
|
||||||
|
placement: "top-end",
|
||||||
|
modifiers: [
|
||||||
|
{ name: "flip", options: { boundary: node } },
|
||||||
|
{
|
||||||
|
name: "hide",
|
||||||
|
options: { enabled: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
},
|
||||||
|
content: ([item]) => {
|
||||||
|
const itemId = item.id();
|
||||||
|
const itemElement = crossesContainer.current?.querySelector(
|
||||||
|
`.popper-cross[data-id='${itemId}']`
|
||||||
|
);
|
||||||
|
if (itemElement) {
|
||||||
|
return itemElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const crossElement = document.createElement("div");
|
||||||
|
crossElement.classList.add("popper-cross");
|
||||||
|
crossElement.setAttribute("data-id", item.id());
|
||||||
|
crossesContainer.current?.appendChild(crossElement);
|
||||||
|
crossElement.addEventListener("click", () => {
|
||||||
|
|
||||||
|
console.log('#' + node.id() + ' node', cy.edges('[source = "' + node.id() + '"]'))
|
||||||
|
console.log('[source = "' + node.id() + '"]')
|
||||||
|
console.log(cy?.collection)
|
||||||
|
|
||||||
|
// cy?.remove('[source = "'+node.id()+'"]')
|
||||||
|
console.log("папа")
|
||||||
|
console.log(cy?.$('[target = "' + node.id() + '"]').data().source)
|
||||||
|
|
||||||
|
|
||||||
|
cy?.remove('#' + node.id())
|
||||||
|
|
||||||
|
// setStartRemove(node.id())
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
);
|
||||||
|
node.on('remove', evt => {
|
||||||
|
console.log(cy.edges())
|
||||||
|
// cy?.remove('#'+evt.target.target())
|
||||||
|
})
|
||||||
|
|
||||||
|
return crossElement;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const gearsPopper = node.popper({
|
||||||
|
popper: {
|
||||||
|
placement: "left",
|
||||||
|
modifiers: [{ name: "flip", options: { boundary: node } }],
|
||||||
|
},
|
||||||
|
content: ([item]) => {
|
||||||
|
const itemId = item.id();
|
||||||
|
// if (item.id() === elements[0].data.id) {
|
||||||
|
// return;
|
||||||
|
// }
|
||||||
|
|
||||||
|
const itemElement = gearsContainer.current?.querySelector(
|
||||||
|
`.popper-gear[data-id='${itemId}']`
|
||||||
|
);
|
||||||
|
if (itemElement) {
|
||||||
|
return itemElement;
|
||||||
|
}
|
||||||
|
|
||||||
|
const gearsElement = document.createElement("div");
|
||||||
|
gearsElement.classList.add("popper-gear");
|
||||||
|
gearsElement.setAttribute("data-id", item.id());
|
||||||
|
gearsContainer.current?.appendChild(gearsElement);
|
||||||
|
// gearsElement.addEventListener("click", () =>
|
||||||
|
// setOpenedModalSettings(
|
||||||
|
// findQuestionById(quiz, node.id().split("_").pop() || "").index
|
||||||
|
// )
|
||||||
|
// );
|
||||||
|
return gearsElement;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const update = async () => {
|
||||||
|
await plusesPopper.update();
|
||||||
|
await crossesPopper.update();
|
||||||
|
await gearsPopper.update();
|
||||||
|
};
|
||||||
|
|
||||||
|
const onZoom = (event: AbstractEventObject) => {
|
||||||
|
const zoom = event.cy.zoom();
|
||||||
|
|
||||||
|
update();
|
||||||
|
|
||||||
|
crossesPopper.setOptions({
|
||||||
|
modifiers: [
|
||||||
|
{ name: "flip", options: { boundary: node } },
|
||||||
|
{ name: "offset", options: { offset: [-5 * zoom, -30 * zoom] } },
|
||||||
|
{
|
||||||
|
name: "hide",
|
||||||
|
options: { enabled: true },
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
|
||||||
|
plusesContainer.current
|
||||||
|
?.querySelectorAll("#popper-pluses > .popper-plus")
|
||||||
|
.forEach((item) => {
|
||||||
|
const element = item as HTMLDivElement;
|
||||||
|
element.style.width = `${40 * zoom}px`;
|
||||||
|
element.style.height = `${40 * zoom}px`;
|
||||||
|
element.style.fontSize = `${40 * zoom}px`;
|
||||||
|
element.style.borderRadius = `${6 * zoom}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
crossesContainer.current
|
||||||
|
?.querySelectorAll("#popper-crosses > .popper-cross")
|
||||||
|
.forEach((item) => {
|
||||||
|
const element = item as HTMLDivElement;
|
||||||
|
element.style.width = `${24 * zoom}px`;
|
||||||
|
element.style.height = `${24 * zoom}px`;
|
||||||
|
element.style.fontSize = `${24 * zoom}px`;
|
||||||
|
element.style.borderRadius = `${6 * zoom}px`;
|
||||||
|
});
|
||||||
|
|
||||||
|
gearsContainer.current
|
||||||
|
?.querySelectorAll("#popper-gears > .popper-gear")
|
||||||
|
.forEach((item) => {
|
||||||
|
const element = item as HTMLDivElement;
|
||||||
|
element.style.width = `${33 * zoom}px`;
|
||||||
|
element.style.height = `${14 * zoom}px`;
|
||||||
|
element.style.fontSize = `${24 * zoom}px`;
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
node?.on("position", update);
|
||||||
|
cy?.on("pan zoom resize render", onZoom);
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<CytoscapeComponent
|
||||||
|
wheelSensitivity={0.1}
|
||||||
|
elements={[]}
|
||||||
|
// elements={createGraphElements(tree, quiz)}
|
||||||
|
style={{ height: "480px", background: "#F2F3F7" }}
|
||||||
|
layout={{
|
||||||
|
name: 'preset',
|
||||||
|
|
||||||
|
positions: (e) => {
|
||||||
|
console.log(e)
|
||||||
|
return {x:0,y:0}
|
||||||
|
}, // map of (node id) => (position obj); or function(node){ return somPos; }
|
||||||
|
zoom: undefined, // the zoom level to set (prob want fit = false if set)
|
||||||
|
pan: undefined, // the pan level to set (prob want fit = false if set)
|
||||||
|
fit: true, // whether to fit to viewport
|
||||||
|
padding: 30, // padding on fit
|
||||||
|
animate: false, // whether to transition the node positions
|
||||||
|
animationDuration: 500, // duration of animation in ms if enabled
|
||||||
|
animationEasing: undefined, // easing of animation if enabled
|
||||||
|
animateFilter: function ( node, i ){ return true; }, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
|
||||||
|
ready: undefined, // callback on layoutready
|
||||||
|
stop: undefined, // callback on layoutstop
|
||||||
|
transform: function (node, position ){ return position; } // transform a given node position. Useful for changing flow direction in discrete layouts
|
||||||
|
}}
|
||||||
|
stylesheet={stylesheet}
|
||||||
|
cy={(cy) => {
|
||||||
|
cyRef.current = cy;
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
};
|
50
src/pages/Questions/BranchingMap/FirstNodeField.tsx
Normal file
50
src/pages/Questions/BranchingMap/FirstNodeField.tsx
Normal file
@ -0,0 +1,50 @@
|
|||||||
|
import { Box } from "@mui/material"
|
||||||
|
import { useEffect, useRef } from "react";
|
||||||
|
import { updateDragQuestionId, updateQuestion } from "@root/questions/actions"
|
||||||
|
import { updateRootInfo } from "@root/quizes/actions"
|
||||||
|
import { useQuestionsStore } from "@root/questions/store"
|
||||||
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
|
||||||
|
export const FirstNodeField = () => {
|
||||||
|
const { dragQuestionId } = useQuestionsStore()
|
||||||
|
const Container = useRef<HTMLDivElement | null>(null);
|
||||||
|
const quiz = useCurrentQuiz();
|
||||||
|
|
||||||
|
|
||||||
|
const newRootNode = () => {
|
||||||
|
if (quiz) {
|
||||||
|
console.log(dragQuestionId)
|
||||||
|
|
||||||
|
if (dragQuestionId) {
|
||||||
|
updateRootInfo(quiz?.id, true)
|
||||||
|
updateQuestion(dragQuestionId, (question) => question.content.rule.parentId = "root")
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
enqueueSnackbar("Нет информации о взятом опроснике")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
Container.current?.addEventListener("mouseup", newRootNode)
|
||||||
|
return () => { Container.current?.removeEventListener("mouseup", newRootNode) }
|
||||||
|
}, [])
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
ref={Container}
|
||||||
|
sx={{
|
||||||
|
height: "100%",
|
||||||
|
width: "100%",
|
||||||
|
backgroundColor: "#f2f3f7",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
justifyContent: "center",
|
||||||
|
color: "#4d4d4d",
|
||||||
|
fontSize: "50px"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
+
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
56
src/pages/Questions/BranchingMap/helper.ts
Normal file
56
src/pages/Questions/BranchingMap/helper.ts
Normal file
@ -0,0 +1,56 @@
|
|||||||
|
import { AnyQuizQuestion } from "@model/questionTypes/shared"
|
||||||
|
|
||||||
|
interface Nodes {
|
||||||
|
data: {
|
||||||
|
id: string;
|
||||||
|
label: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
interface Edges {
|
||||||
|
data: {
|
||||||
|
source: string;
|
||||||
|
target: string;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
export const storeToNodes = (questions: AnyQuizQuestion[]) => {
|
||||||
|
const nodes: Nodes[] = []
|
||||||
|
const edges: Edges[] = []
|
||||||
|
questions.forEach((question) => {
|
||||||
|
if (question.content.rule.parentId) {
|
||||||
|
nodes.push({data: {
|
||||||
|
id: question.id,
|
||||||
|
label: question.title ? question.title : "noname"
|
||||||
|
}})
|
||||||
|
nodes.push({data:{
|
||||||
|
id: "111",
|
||||||
|
label: "111"
|
||||||
|
}})
|
||||||
|
nodes.push({data:{
|
||||||
|
id: "222",
|
||||||
|
label: "222"
|
||||||
|
}})
|
||||||
|
edges.push({data: {
|
||||||
|
source: question.id,
|
||||||
|
target: "111"
|
||||||
|
}})
|
||||||
|
nodes.push({data:{
|
||||||
|
id: "333",
|
||||||
|
label: "333"
|
||||||
|
}})
|
||||||
|
edges.push({data: {
|
||||||
|
source: "111",
|
||||||
|
target: "333"
|
||||||
|
}})
|
||||||
|
edges.push({data: {
|
||||||
|
source: question.id,
|
||||||
|
target: "222"
|
||||||
|
}})
|
||||||
|
if (question.content.rule.parentId !== "root") edges.push({data: {
|
||||||
|
source: question.content.rule.parentId,
|
||||||
|
target: question.id
|
||||||
|
}})
|
||||||
|
}
|
||||||
|
})
|
||||||
|
return [...nodes, ...edges];
|
||||||
|
}
|
36
src/pages/Questions/BranchingMap/index.tsx
Normal file
36
src/pages/Questions/BranchingMap/index.tsx
Normal file
@ -0,0 +1,36 @@
|
|||||||
|
import { Box } from "@mui/material";
|
||||||
|
import { FirstNodeField } from "./FirstNodeField";
|
||||||
|
import { CsComponent } from "./CsComponent";
|
||||||
|
import { useQuestionsStore } from "@root/questions/store"
|
||||||
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
|
|
||||||
|
|
||||||
|
export const BranchingMap = () => {
|
||||||
|
const quiz = useCurrentQuiz();
|
||||||
|
const { dragQuestionId } = useQuestionsStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
id="cytoscape-container"
|
||||||
|
sx={{
|
||||||
|
overflow: "hidden",
|
||||||
|
padding: "20px",
|
||||||
|
background: "#FFFFFF",
|
||||||
|
borderRadius: "12px",
|
||||||
|
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
|
||||||
|
marginBottom: "40px",
|
||||||
|
height: "521px",
|
||||||
|
border: dragQuestionId === null ? "none" : "#7e2aea 2px dashed"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
|
||||||
|
{
|
||||||
|
quiz?.config.haveRoot ?
|
||||||
|
<CsComponent />
|
||||||
|
:
|
||||||
|
<FirstNodeField />
|
||||||
|
|
||||||
|
}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
45
src/pages/Questions/BranchingMap/styles.css
Normal file
45
src/pages/Questions/BranchingMap/styles.css
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
#popper-pluses > .popper-plus {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: #eeeff4;
|
||||||
|
border: 1.5px dashed rgba(154, 154, 175, 0.5);
|
||||||
|
font-size: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popper-pluses > .popper-plus::before {
|
||||||
|
content: "+";
|
||||||
|
color: rgba(154, 154, 175, 0.5);
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popper-crosses > .popper-cross {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background: rgba(154, 154, 175, 0.7);
|
||||||
|
font-size: 0px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popper-crosses > .popper-cross::before {
|
||||||
|
content: "+";
|
||||||
|
transform: rotate(45deg);
|
||||||
|
color: #fff;
|
||||||
|
font-size: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
#popper-gears > .popper-gear {
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
border-radius: 6px;
|
||||||
|
background-image: url("../../../assets/icons/ArrowGear.svg");
|
||||||
|
font-size: 0px;
|
||||||
|
background-repeat: no-repeat;
|
||||||
|
background-size: contain;
|
||||||
|
}
|
178
src/pages/Questions/BranchingModal/BranchingQuestionsModal.tsx
Normal file
178
src/pages/Questions/BranchingModal/BranchingQuestionsModal.tsx
Normal file
@ -0,0 +1,178 @@
|
|||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
Button,
|
||||||
|
FormControl,
|
||||||
|
FormControlLabel,
|
||||||
|
Link,
|
||||||
|
Modal,
|
||||||
|
Radio,
|
||||||
|
RadioGroup,
|
||||||
|
Tooltip,
|
||||||
|
Typography,
|
||||||
|
useTheme,
|
||||||
|
Checkbox,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { AnyQuizQuestion, createBranchingRuleMain } from "../../../model/questionTypes/shared"
|
||||||
|
import { Select } from "../Select";
|
||||||
|
|
||||||
|
import RadioCheck from "@ui_kit/RadioCheck";
|
||||||
|
import RadioIcon from "@ui_kit/RadioIcon";
|
||||||
|
import InfoIcon from "@icons/Info";
|
||||||
|
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
||||||
|
|
||||||
|
import { TypeSwitch, BlockRule } from "./Settings";
|
||||||
|
import { getQuestionById, updateOpenedModalSettingsId, updateQuestion } from "@root/questions/actions";
|
||||||
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
|
||||||
|
|
||||||
|
export default function BranchingQuestions() {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
|
||||||
|
const { openedModalSettingsId } = useQuestionsStore();
|
||||||
|
const [targetQuestion, setTargetQuestion] = useState<AnyQuizQuestion | null>(getQuestionById(openedModalSettingsId))
|
||||||
|
const [parentQuestion, setParentQuestion] = useState<AnyQuizQuestion | null>(getQuestionById(openedModalSettingsId))
|
||||||
|
|
||||||
|
if (targetQuestion === null || parentQuestion === null) {
|
||||||
|
enqueueSnackbar("Невозможно найти данные ветвления для этого вопроса")
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
|
||||||
|
const saveData = () => {
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleClose = () => {
|
||||||
|
updateOpenedModalSettingsId()
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Modal open={openedModalSettingsId !== null} onClose={handleClose}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
position: "absolute",
|
||||||
|
overflow: "hidden",
|
||||||
|
top: "50%",
|
||||||
|
left: "50%",
|
||||||
|
transform: "translate(-50%, -50%)",
|
||||||
|
maxWidth: "620px",
|
||||||
|
width: "100%",
|
||||||
|
bgcolor: "background.paper",
|
||||||
|
borderRadius: "12px",
|
||||||
|
boxShadow: 24,
|
||||||
|
p: 0,
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
boxSizing: "border-box",
|
||||||
|
background: "#F2F3F7",
|
||||||
|
height: "70px",
|
||||||
|
padding: "0 25px",
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box sx={{ color: "#4d4d4d" }}>
|
||||||
|
<Typography component="span">{targetQuestion.title}</Typography>
|
||||||
|
</Box>
|
||||||
|
<Tooltip
|
||||||
|
title="Настройте условия, при которых данный вопрос будет отображаться в квизе."
|
||||||
|
placement="top"
|
||||||
|
>
|
||||||
|
<Box>
|
||||||
|
<InfoIcon />
|
||||||
|
</Box>
|
||||||
|
</Tooltip>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
height: "400px",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{
|
||||||
|
parentQuestion.content.rule.main.length ?
|
||||||
|
parentQuestion.content.rule.main.map((e: any, i: number) => {
|
||||||
|
if (e.next === targetQuestion.id) {
|
||||||
|
return <TypeSwitch key={i} targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={i} />
|
||||||
|
} else {
|
||||||
|
<></>
|
||||||
|
}
|
||||||
|
})
|
||||||
|
:
|
||||||
|
<TypeSwitch targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={0} />
|
||||||
|
}
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
margin: "20px 0 0 20px",
|
||||||
|
display: "flex",
|
||||||
|
flexDirection: "column"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Link
|
||||||
|
variant="body2"
|
||||||
|
sx={{
|
||||||
|
color: theme.palette.brightPurple.main,
|
||||||
|
marginBottom: "10px",
|
||||||
|
cursor: "pointer"
|
||||||
|
}}
|
||||||
|
onClick={() => {
|
||||||
|
const mutate = parentQuestion
|
||||||
|
mutate.content.rule.main.push(createBranchingRuleMain(targetQuestion.id, parentQuestion.id))
|
||||||
|
setParentQuestion(mutate)
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
Добавить условие
|
||||||
|
</Link>
|
||||||
|
|
||||||
|
|
||||||
|
<FormControlLabel control={<Checkbox
|
||||||
|
|
||||||
|
sx={{
|
||||||
|
margin: 0
|
||||||
|
}}
|
||||||
|
checked={parentQuestion.content.rule.default === targetQuestion.id}
|
||||||
|
|
||||||
|
onClick={() => {
|
||||||
|
const mutate = parentQuestion
|
||||||
|
mutate.content.rule.default = targetQuestion.id
|
||||||
|
setParentQuestion(mutate)
|
||||||
|
}}
|
||||||
|
/>} label="Следующий вопрос по-умолчанию" />
|
||||||
|
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
|
||||||
|
<Box sx={{ display: "flex", justifyContent: "end", gap: "10px", margin: "20px" }}>
|
||||||
|
<Button
|
||||||
|
variant="outlined"
|
||||||
|
onClick={handleClose}
|
||||||
|
sx={{ width: "100%", maxWidth: "130px" }}
|
||||||
|
>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="contained"
|
||||||
|
sx={{ width: "100%", maxWidth: "130px" }}
|
||||||
|
onClick={handleClose}
|
||||||
|
>
|
||||||
|
Готово
|
||||||
|
</Button>
|
||||||
|
</Box>
|
||||||
|
</>
|
||||||
|
</Box>
|
||||||
|
</Modal>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
516
src/pages/Questions/BranchingModal/Settings.tsx
Normal file
516
src/pages/Questions/BranchingModal/Settings.tsx
Normal file
@ -0,0 +1,516 @@
|
|||||||
|
import { Box, MenuItem, FormControl, Checkbox, FormControlLabel, Radio, RadioGroup, Typography, useTheme, Select, Chip, IconButton, TextField } from "@mui/material"
|
||||||
|
import RadioCheck from "@ui_kit/RadioCheck"
|
||||||
|
import RadioIcon from "@ui_kit/RadioIcon"
|
||||||
|
import { QuizQuestionBase } from "model/questionTypes/shared"
|
||||||
|
import { useState, useRef, useEffect } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
|
import { updateQuestion, getQuestionById } from "@root/questions/actions";
|
||||||
|
import { AnyQuizQuestion } from "../../../model/questionTypes/shared"
|
||||||
|
import { SelectChangeEvent } from '@mui/material/Select';
|
||||||
|
|
||||||
|
import InfoIcon from "@icons/Info";
|
||||||
|
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
||||||
|
|
||||||
|
const CONDITIONS = [
|
||||||
|
"Все условия обязательны",
|
||||||
|
"Обязательно хотя бы одно условие",
|
||||||
|
];
|
||||||
|
interface Props {
|
||||||
|
parentQuestion: AnyQuizQuestion;
|
||||||
|
targetQuestion: AnyQuizQuestion;
|
||||||
|
ruleIndex: number;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
//Этот компонент вызывается 1 раз на каждое условие родителя для перехода к этому вопросу. Поэтому для изменения стора мы знаем индекс
|
||||||
|
export const TypeSwitch = ({ parentQuestion, targetQuestion, ruleIndex }: Props) => {
|
||||||
|
|
||||||
|
switch (parentQuestion.type) {
|
||||||
|
// case 'nonselected':
|
||||||
|
// return <BlockRule text={"Не выбран тип родительского вопроса"} />
|
||||||
|
// break;
|
||||||
|
|
||||||
|
case "variant":
|
||||||
|
case "images":
|
||||||
|
case "varimg":
|
||||||
|
case "emoji":
|
||||||
|
case "select":
|
||||||
|
|
||||||
|
return (parentQuestion.content.variants === undefined ? <BlockRule text={"У родителя нет вариантов"} /> :
|
||||||
|
<SelectorType targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={ruleIndex} />
|
||||||
|
//Реализован
|
||||||
|
)
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "date":
|
||||||
|
// return <DateInputsType targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={ruleIndex} />
|
||||||
|
return <></>
|
||||||
|
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "number":
|
||||||
|
return <NumberInputsType targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={ruleIndex} />
|
||||||
|
//Реализован
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "page":
|
||||||
|
case "date":
|
||||||
|
return <BlockRule text={"У такого родителя может быть только один потомок"} />
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "text":
|
||||||
|
return <TextInputsType targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={ruleIndex} />
|
||||||
|
//Реализован
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "file":
|
||||||
|
return <FileInputsType targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={ruleIndex} />
|
||||||
|
//Реализован
|
||||||
|
break;
|
||||||
|
|
||||||
|
case "rating":
|
||||||
|
return <RatingInputsType targetQuestion={targetQuestion} parentQuestion={parentQuestion} ruleIndex={ruleIndex} />
|
||||||
|
//Реализован
|
||||||
|
break;
|
||||||
|
|
||||||
|
default:
|
||||||
|
return <BlockRule text={"Не распознан тип родительского вопроса"} />
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
export const BlockRule = ({ text }: { text: string }) => {
|
||||||
|
return (
|
||||||
|
<Typography
|
||||||
|
sx={{
|
||||||
|
margin: "100px 0",
|
||||||
|
textAlign: "center"
|
||||||
|
}}
|
||||||
|
>{text}</Typography>
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const SelectorType = ({ parentQuestion, targetQuestion, ruleIndex }: { parentQuestion: any, targetQuestion: any, ruleIndex: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const quizId = Number(useParams().quizId);
|
||||||
|
return (
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
margin: "20px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
bgcolor: "#F2F3F7",
|
||||||
|
height: "280px",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Новое условие
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
sx={{ borderRadius: "6px", padding: "2px" }}
|
||||||
|
onClick={() => {
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
newParentQuestion.content.rule.main.splice(ruleIndex, 1)
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon color={"#4D4D4D"} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Дан ответ
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>
|
||||||
|
(Укажите один или несколько вариантов)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<Select
|
||||||
|
multiple
|
||||||
|
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers}
|
||||||
|
onChange={(event: SelectChangeEvent) => {
|
||||||
|
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
parentQuestion.content.rule.main[ruleIndex].rules[0].answers = (event.target as HTMLSelectElement).value
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
|
||||||
|
}}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
height: "48px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
"& .MuiOutlinedInput-notchedOutline": {
|
||||||
|
border: `1px solid ${theme.palette.brightPurple.main} !important`,
|
||||||
|
height: "48px",
|
||||||
|
borderRadius: "10px",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{parentQuestion.content.variants.map((e: any) => {
|
||||||
|
return <MenuItem value={e.id}>
|
||||||
|
{e.answer}
|
||||||
|
</MenuItem>
|
||||||
|
})}
|
||||||
|
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<FormControl>
|
||||||
|
<RadioGroup
|
||||||
|
aria-labelledby="demo-controlled-radio-buttons-group"
|
||||||
|
value={parentQuestion.content.rule.main[ruleIndex].or}
|
||||||
|
onChange={(_, value) => {
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
parentQuestion.content.rule.main[ruleIndex].or = value
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{CONDITIONS.map((condition, totalIndex) => (
|
||||||
|
<FormControlLabel
|
||||||
|
key={totalIndex}
|
||||||
|
sx={{ color: theme.palette.grey2.main }}
|
||||||
|
value={Boolean(Number(totalIndex))}
|
||||||
|
control={
|
||||||
|
<Radio
|
||||||
|
checkedIcon={<RadioCheck />}
|
||||||
|
icon={<RadioIcon />}
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
label={condition}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</RadioGroup>
|
||||||
|
</FormControl>
|
||||||
|
|
||||||
|
|
||||||
|
</Box >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const DateInputsType = ({ parentQuestion, targetQuestion, ruleIndex }: { parentQuestion: any, targetQuestion: any, ruleIndex: number }) => {
|
||||||
|
|
||||||
|
return <></>
|
||||||
|
}
|
||||||
|
const NumberInputsType = ({ parentQuestion, targetQuestion, ruleIndex }: { parentQuestion: any, targetQuestion: any, ruleIndex: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const quizId = Number(useParams().quizId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
margin: "20px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
bgcolor: "#F2F3F7",
|
||||||
|
height: "280px",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Новое условие
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
sx={{ borderRadius: "6px", padding: "2px" }}
|
||||||
|
onClick={() => {
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
newParentQuestion.content.rule.main.splice(ruleIndex, 1)
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon color={"#4D4D4D"} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Дан ответ
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>
|
||||||
|
(Укажите один или несколько вариантов)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{/* <TextField
|
||||||
|
sx={{
|
||||||
|
marginTop: "20px",
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
|
||||||
|
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
|
||||||
|
onChange={(event: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
parentQuestion.content.rule.main[ruleIndex].rules[0].answers = [(event.target as HTMLInputElement).value.replace(/[^0-9,\s]/g, "")]
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Box >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const TextInputsType = ({ parentQuestion, targetQuestion, ruleIndex }: { parentQuestion: any, targetQuestion: any, ruleIndex: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const quizId = Number(useParams().quizId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
margin: "20px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
bgcolor: "#F2F3F7",
|
||||||
|
height: "280px",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Новое условие
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
sx={{ borderRadius: "6px", padding: "2px" }}
|
||||||
|
onClick={() => {
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
newParentQuestion.content.rule.main.splice(ruleIndex, 1)
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon color={"#4D4D4D"} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "inline",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Дан ответ
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: "#7E2AEA", pl: "10px", fontSize: "12px" }}>
|
||||||
|
(Укажите текст, при совпадении с которым пользователь попадёт на этот вопрос)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{/* <TextField
|
||||||
|
sx={{
|
||||||
|
marginTop: "20px",
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
|
||||||
|
onChange={(event: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = [(event.target as HTMLInputElement).value]
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Box >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const FileInputsType = ({ parentQuestion, targetQuestion, ruleIndex }: { parentQuestion: any, targetQuestion: any, ruleIndex: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const quizId = Number(useParams().quizId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
margin: "20px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
bgcolor: "#F2F3F7",
|
||||||
|
height: "280px",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Новое условие
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
sx={{ borderRadius: "6px", padding: "2px" }}
|
||||||
|
onClick={() => {
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
newParentQuestion.content.rule.main.splice(ruleIndex, 1)
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon color={"#4D4D4D"} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "inline",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Перевести на этот вопрос если пользователь загрузил файл
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
<FormControlLabel control={<Checkbox
|
||||||
|
|
||||||
|
sx={{
|
||||||
|
margin: 0
|
||||||
|
}}
|
||||||
|
checked={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
|
||||||
|
onChange={(event: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
parentQuestion.content.rule.main[ruleIndex].rules[0].answers = [(event.target as HTMLInputElement).checked]
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
|
||||||
|
}}
|
||||||
|
/>} label="да" />
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Box >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
const RatingInputsType = ({ parentQuestion, targetQuestion, ruleIndex }: { parentQuestion: any, targetQuestion: any, ruleIndex: number }) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
const quizId = Number(useParams().quizId);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
padding: "20px",
|
||||||
|
margin: "20px",
|
||||||
|
borderRadius: "8px",
|
||||||
|
bgcolor: "#F2F3F7",
|
||||||
|
height: "280px",
|
||||||
|
overflow: "auto"
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "5px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
||||||
|
Новое условие
|
||||||
|
</Typography>
|
||||||
|
<IconButton
|
||||||
|
sx={{ borderRadius: "6px", padding: "2px" }}
|
||||||
|
onClick={() => {
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
newParentQuestion.content.rule.main.splice(ruleIndex, 1)
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DeleteIcon color={"#4D4D4D"} />
|
||||||
|
</IconButton>
|
||||||
|
</Box>
|
||||||
|
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "inline",
|
||||||
|
alignItems: "center",
|
||||||
|
pb: "10px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ color: "#7E2AEA", pl: "10px", fontSize: "12px" }}>
|
||||||
|
(Ожидаемое количество ячеек)
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
{/* <TextField
|
||||||
|
sx={{
|
||||||
|
marginTop: "20px",
|
||||||
|
width: "100%"
|
||||||
|
}}
|
||||||
|
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
|
||||||
|
onChange={(event: React.FormEvent<HTMLInputElement>) => {
|
||||||
|
|
||||||
|
const newParentQuestion = { ...parentQuestion }
|
||||||
|
parentQuestion.content.rule.main[ruleIndex].rules[0].answers = [(event.target as HTMLInputElement).value.replace(/[^0-9,\s]/g, "")]
|
||||||
|
//updateQuestionsList(quizId, parentQuestion.index, parentQuestion);
|
||||||
|
|
||||||
|
}}
|
||||||
|
/> */}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
</Box >
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
// {
|
||||||
|
// content: {
|
||||||
|
// rule: {
|
||||||
|
// main: [
|
||||||
|
// {
|
||||||
|
// next: "id of next question", // 2 string
|
||||||
|
// or: true // 3 boolean
|
||||||
|
// rules: [
|
||||||
|
// {
|
||||||
|
// question: "question id", string
|
||||||
|
// answers: [] // ответы на вопросы. для вариантов выбора - конкретные айдишники, для полей ввода текста - текст по полному совпадению, для ввода файла ии ещё какой подобной дичи - просто факт того что файл ввели, т.е. boolean
|
||||||
|
// }
|
||||||
|
// ]
|
||||||
|
// }
|
||||||
|
// ],
|
||||||
|
// default: ID string
|
||||||
|
// }
|
||||||
|
// }
|
||||||
|
// }
|
86
src/pages/Questions/BranchingPanel/QuestionsList.tsx
Normal file
86
src/pages/Questions/BranchingPanel/QuestionsList.tsx
Normal file
@ -0,0 +1,86 @@
|
|||||||
|
import { useParams } from "react-router-dom";
|
||||||
|
import { Box, Button, Typography } from "@mui/material";
|
||||||
|
import { ReactComponent as CheckedIcon } from "@icons/checked.svg";
|
||||||
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
|
import { updateDragQuestionId } from "@root/questions/actions";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
|
||||||
|
|
||||||
|
const getItemStyle = (isDragging:any, draggableStyle:any) => ({
|
||||||
|
// some basic styles to make the items look a bit nicer
|
||||||
|
userSelect: "none",
|
||||||
|
padding: 5 * 2,
|
||||||
|
margin: `0 0 5px 0`,
|
||||||
|
|
||||||
|
// change background colour if dragging
|
||||||
|
background: isDragging ? "lightgreen" : "grey",
|
||||||
|
|
||||||
|
// styles we need to apply on draggables
|
||||||
|
...draggableStyle
|
||||||
|
});
|
||||||
|
|
||||||
|
export const QuestionsList = () => {
|
||||||
|
const { questions } = useQuestionsStore()
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ padding: "15px" }}>
|
||||||
|
<Typography
|
||||||
|
sx={{ fontSize: "12px", color: "#9A9AAF", marginBottom: "5px" }}
|
||||||
|
>
|
||||||
|
Перетащите вопросы в карту ветвления
|
||||||
|
</Typography>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
maxHeight: "400px",
|
||||||
|
overflowY: "scroll",
|
||||||
|
paddingRight: "12px",
|
||||||
|
"&::-webkit-scrollbar": { width: "8px" },
|
||||||
|
"&::-webkit-scrollbar-track": {
|
||||||
|
background: "#D4D4DF",
|
||||||
|
borderRadius: "4px",
|
||||||
|
},
|
||||||
|
"&::-webkit-scrollbar-thumb": {
|
||||||
|
boxSizing: "border-box",
|
||||||
|
background: "#9A9AAF",
|
||||||
|
borderRadius: "4px",
|
||||||
|
border: "1.5px solid transparent",
|
||||||
|
backgroundClip: "padding-box",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{/* тут нужно будет фильтровать с проверкой, что вопрос имеет тип*/}
|
||||||
|
{questions.map(({ title, id, content }, index) => (
|
||||||
|
<Button
|
||||||
|
onMouseDown={() => {//Разрешаем добавить этот вопрос если у него нет родителя (не добавляли ещё в дерево)
|
||||||
|
if (!content.rule.parentId) updateDragQuestionId(id)
|
||||||
|
}}
|
||||||
|
key={index}
|
||||||
|
sx={{
|
||||||
|
width: "100%",
|
||||||
|
cursor: "pointer",
|
||||||
|
display: "flex",
|
||||||
|
justifyContent: "space-between",
|
||||||
|
alignItems: "center",
|
||||||
|
padding: "12px",
|
||||||
|
background: "#FFFFFF",
|
||||||
|
borderRadius: "8px",
|
||||||
|
marginBottom: "20px",
|
||||||
|
boxShadow: "0px 10px 30px #e7e7e7",
|
||||||
|
backgroundImage: `url("data:image/svg+xml,%3csvg width='100%25' height='100%25' xmlns='http://www.w3.org/2000/svg'%3e%3crect width='100%25' height='100%25' fill='none' rx='8' ry='8' stroke='rgb(154, 154, 175)' stroke-width='2' stroke-dasharray='8 8' stroke-dashoffset='0' stroke-linecap='square'/%3e%3c/svg%3e");
|
||||||
|
border-radius: 8px;`,
|
||||||
|
"&:last-child": { marginBottom: 0 },
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Typography sx={{ width: "100%", color: content.rule.parentId ? "#9A9AAF" : "#000" }}>
|
||||||
|
{title || "нет заголовка"}
|
||||||
|
</Typography>
|
||||||
|
{content.rule.parentId && <CheckedIcon />}
|
||||||
|
</Button>
|
||||||
|
|
||||||
|
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
85
src/pages/Questions/BranchingPanel/index.tsx
Normal file
85
src/pages/Questions/BranchingPanel/index.tsx
Normal file
@ -0,0 +1,85 @@
|
|||||||
|
import { Box, Typography, Switch, useTheme } from "@mui/material";
|
||||||
|
|
||||||
|
import { QuestionsList } from "./QuestionsList";
|
||||||
|
|
||||||
|
type BranchingPanelProps = {
|
||||||
|
active: boolean;
|
||||||
|
setActive: (active: boolean) => void;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const BranchingPanel = ({ active, setActive }: BranchingPanelProps) => {
|
||||||
|
const theme = useTheme();
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ userSelect: "none", maxWidth: "350px", width: "100%" }}>
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
alignItems: "center",
|
||||||
|
gap: "15px",
|
||||||
|
padding: "18px",
|
||||||
|
background: "#fff",
|
||||||
|
borderRadius: "12px",
|
||||||
|
boxShadow: "0px 10px 30px #e7e7e7",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<Switch
|
||||||
|
value={active}
|
||||||
|
onChange={(_, value) => setTimeout(() => setActive(value), 10)}
|
||||||
|
sx={{
|
||||||
|
width: 50,
|
||||||
|
height: 30,
|
||||||
|
padding: 0,
|
||||||
|
"& .MuiSwitch-switchBase": {
|
||||||
|
padding: 0,
|
||||||
|
margin: "2px",
|
||||||
|
transitionDuration: "300ms",
|
||||||
|
"&.Mui-checked": {
|
||||||
|
transform: "translateX(20px)",
|
||||||
|
color: theme.palette.brightPurple.main,
|
||||||
|
"& + .MuiSwitch-track": {
|
||||||
|
backgroundColor: "#E8DCF9",
|
||||||
|
opacity: 1,
|
||||||
|
border: 0,
|
||||||
|
},
|
||||||
|
"&.Mui-disabled + .MuiSwitch-track": { opacity: 0.5 },
|
||||||
|
},
|
||||||
|
"&.Mui-disabled .MuiSwitch-thumb": {
|
||||||
|
color:
|
||||||
|
theme.palette.mode === "light"
|
||||||
|
? theme.palette.grey[100]
|
||||||
|
: theme.palette.grey[600],
|
||||||
|
},
|
||||||
|
"&.Mui-disabled + .MuiSwitch-track": {
|
||||||
|
opacity: theme.palette.mode === "light" ? 0.7 : 0.3,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
"& .MuiSwitch-thumb": {
|
||||||
|
boxSizing: "border-box",
|
||||||
|
width: 25,
|
||||||
|
height: 25,
|
||||||
|
},
|
||||||
|
"& .MuiSwitch-track": {
|
||||||
|
borderRadius: 13,
|
||||||
|
backgroundColor:
|
||||||
|
theme.palette.mode === "light" ? "#E9E9EA" : "#39393D",
|
||||||
|
opacity: 1,
|
||||||
|
transition: theme.transitions.create(["background-color"], {
|
||||||
|
duration: 500,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
/>
|
||||||
|
<Box>
|
||||||
|
<Typography sx={{ fontWeight: "bold", color: "#4D4D4D" }}>
|
||||||
|
Логика ветвления
|
||||||
|
</Typography>
|
||||||
|
<Typography sx={{ color: "#4D4D4D", fontSize: "12px" }}>
|
||||||
|
Настройте связи между вопросами
|
||||||
|
</Typography>
|
||||||
|
</Box>
|
||||||
|
</Box>
|
||||||
|
{ active && <QuestionsList /> }
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
};
|
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionDate } from "@model/questionTypes/date";
|
import { QuizQuestionDate } from "@model/questionTypes/date";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingData from "./settingData";
|
import SettingData from "./settingData";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchData({
|
|||||||
return <SettingData question={question} />;
|
return <SettingData question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
@ -92,7 +92,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
|
|||||||
<TextField
|
<TextField
|
||||||
defaultValue={question.title}
|
defaultValue={question.title}
|
||||||
placeholder={"Заголовок вопроса"}
|
placeholder={"Заголовок вопроса"}
|
||||||
onChange={({ target }) => setTitle(target.value)}
|
onChange={({ target }: {target: HTMLInputElement}) => setTitle(target.value)}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<Box>
|
<Box>
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionSelect } from "@model/questionTypes/select";
|
import { QuizQuestionSelect } from "@model/questionTypes/select";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingDropDown from "./settingDropDown";
|
import SettingDropDown from "./settingDropDown";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchDropDown({
|
|||||||
return <SettingDropDown question={question} />;
|
return <SettingDropDown question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionEmoji } from "@model/questionTypes/emoji";
|
import { QuizQuestionEmoji } from "@model/questionTypes/emoji";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingEmoji from "./settingEmoji";
|
import SettingEmoji from "./settingEmoji";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchEmoji({
|
|||||||
return <SettingEmoji question={question} />;
|
return <SettingEmoji question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { QuizQuestionVarImg } from "@model/questionTypes/varimg";
|
import { QuizQuestionVarImg } from "@model/questionTypes/varimg";
|
||||||
import UploadImage from "../UploadImage";
|
import UploadImage from "../UploadImage";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingOptionsAndPict from "./SettingOptionsAndPict";
|
import SettingOptionsAndPict from "./SettingOptionsAndPict";
|
||||||
|
|
||||||
@ -19,8 +18,6 @@ export default function SwitchOptionsAndPict({
|
|||||||
return <SettingOptionsAndPict question={question} />;
|
return <SettingOptionsAndPict question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
case "image":
|
case "image":
|
||||||
return <UploadImage question={question} />;
|
return <UploadImage question={question} />;
|
||||||
default:
|
default:
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import * as React from "react";
|
import * as React from "react";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import SettingOpytionsPict from "./settingOpytionsPict";
|
import SettingOpytionsPict from "./settingOpytionsPict";
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import { QuizQuestionImages } from "@model/questionTypes/images";
|
import { QuizQuestionImages } from "@model/questionTypes/images";
|
||||||
@ -18,8 +17,6 @@ export default function SwitchAnswerOptionsPict({
|
|||||||
return <SettingOpytionsPict question={question} />;
|
return <SettingOpytionsPict question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionText } from "@model/questionTypes/text";
|
import { QuizQuestionText } from "@model/questionTypes/text";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingTextField from "./settingTextField";
|
import SettingTextField from "./settingTextField";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchTextField({
|
|||||||
return <SettingTextField question={question} />;
|
return <SettingTextField question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionPage } from "@model/questionTypes/page";
|
import { QuizQuestionPage } from "@model/questionTypes/page";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingPageOptions from "./SettingPageOptions";
|
import SettingPageOptions from "./SettingPageOptions";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchPageOptions({
|
|||||||
return <SettingPageOptions question={question} />;
|
return <SettingPageOptions question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
27
src/pages/Questions/QuestionSwitchWindowTool.tsx
Normal file
27
src/pages/Questions/QuestionSwitchWindowTool.tsx
Normal file
@ -0,0 +1,27 @@
|
|||||||
|
|
||||||
|
import {
|
||||||
|
Box,
|
||||||
|
} from "@mui/material";
|
||||||
|
import { DraggableList } from "./DraggableList";
|
||||||
|
import { BranchingPanel } from "./BranchingPanel";
|
||||||
|
import { BranchingMap } from "./BranchingMap";
|
||||||
|
|
||||||
|
interface Props {
|
||||||
|
settingBranching: boolean;
|
||||||
|
setSettingBranching: (active: boolean) => void
|
||||||
|
}
|
||||||
|
|
||||||
|
export const QuestionSwitchWindowTool = ({settingBranching, setSettingBranching}:Props) => {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap" }}>
|
||||||
|
<Box sx={{ flexBasis: "796px" }}>
|
||||||
|
{settingBranching ? <BranchingMap /> : <DraggableList />}
|
||||||
|
</Box>
|
||||||
|
<BranchingPanel
|
||||||
|
active={settingBranching}
|
||||||
|
setActive={setSettingBranching}
|
||||||
|
/>
|
||||||
|
</Box>
|
||||||
|
)
|
||||||
|
}
|
@ -1,3 +1,4 @@
|
|||||||
|
import { useState } from "react"
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Button,
|
Button,
|
||||||
@ -13,14 +14,19 @@ import QuizPreview from "@ui_kit/QuizPreview/QuizPreview";
|
|||||||
import { createPortal } from "react-dom";
|
import { createPortal } from "react-dom";
|
||||||
import AddPlus from "../../assets/icons/questionsPage/addPlus";
|
import AddPlus from "../../assets/icons/questionsPage/addPlus";
|
||||||
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
|
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
|
||||||
import { DraggableList } from "./DraggableList";
|
import BranchingQuestions from "./BranchingModal/BranchingQuestionsModal"
|
||||||
|
import { QuestionSwitchWindowTool } from "./QuestionSwitchWindowTool";
|
||||||
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
|
|
||||||
|
|
||||||
export default function QuestionsPage() {
|
export default function QuestionsPage() {
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
const { openedModalSettingsId } = useQuestionsStore();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(660));
|
const isMobile = useMediaQuery(theme.breakpoints.down(660));
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
|
|
||||||
|
const [settingBranching, setSettingBranching] = useState<boolean>(false);
|
||||||
|
|
||||||
if (!quiz) return null;
|
if (!quiz) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -37,6 +43,7 @@ export default function QuestionsPage() {
|
|||||||
<Typography variant={"h5"}>Заголовок квиза</Typography>
|
<Typography variant={"h5"}>Заголовок квиза</Typography>
|
||||||
<Button
|
<Button
|
||||||
sx={{
|
sx={{
|
||||||
|
display: settingBranching ? "none" : "flex",
|
||||||
fontSize: "16px",
|
fontSize: "16px",
|
||||||
lineHeight: "19px",
|
lineHeight: "19px",
|
||||||
padding: 0,
|
padding: 0,
|
||||||
@ -49,7 +56,7 @@ export default function QuestionsPage() {
|
|||||||
Свернуть всё
|
Свернуть всё
|
||||||
</Button>
|
</Button>
|
||||||
</Box>
|
</Box>
|
||||||
<DraggableList />
|
<QuestionSwitchWindowTool settingBranching={settingBranching} setSettingBranching={setSettingBranching} />
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
display: "flex",
|
display: "flex",
|
||||||
@ -95,6 +102,7 @@ export default function QuestionsPage() {
|
|||||||
</Box>
|
</Box>
|
||||||
</Box>
|
</Box>
|
||||||
{createPortal(<QuizPreview />, document.body)}
|
{createPortal(<QuizPreview />, document.body)}
|
||||||
|
{openedModalSettingsId !== null && <BranchingQuestions/>}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
@ -172,13 +172,13 @@ export default function RatingOptions({ question }: Props) {
|
|||||||
defaultValue={question.content.ratingNegativeDescription}
|
defaultValue={question.content.ratingNegativeDescription}
|
||||||
value={negativeText}
|
value={negativeText}
|
||||||
placeholder="Негативно"
|
placeholder="Негативно"
|
||||||
onChange={({ target }) => {
|
onChange={({ target }: {target: HTMLInputElement}) => {
|
||||||
if (target.value.length <= 15) {
|
if (target.value.length <= 15) {
|
||||||
setNegativeText(target.value);
|
setNegativeText(target.value);
|
||||||
debounceNegativeDescription(target.value);
|
debounceNegativeDescription(target.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={({ target }) => debounceNegativeDescription(target.value)}
|
onBlur={({ target }: {target: HTMLInputElement}) => debounceNegativeDescription(target.value)}
|
||||||
sx={{
|
sx={{
|
||||||
width: negativeTextWidth + 10 + "px",
|
width: negativeTextWidth + 10 + "px",
|
||||||
maxWidth: isMobile ? "140px" : "230px",
|
maxWidth: isMobile ? "140px" : "230px",
|
||||||
@ -222,13 +222,13 @@ export default function RatingOptions({ question }: Props) {
|
|||||||
<TextField
|
<TextField
|
||||||
value={positiveText}
|
value={positiveText}
|
||||||
placeholder="Позитивно"
|
placeholder="Позитивно"
|
||||||
onChange={({ target }) => {
|
onChange={({ target }: {target: HTMLInputElement}) => {
|
||||||
if (target.value.length <= 15) {
|
if (target.value.length <= 15) {
|
||||||
setPositiveText(target.value);
|
setPositiveText(target.value);
|
||||||
debouncePositiveDescription(target.value);
|
debouncePositiveDescription(target.value);
|
||||||
}
|
}
|
||||||
}}
|
}}
|
||||||
onBlur={({ target }) => debouncePositiveDescription(target.value)}
|
onBlur={({ target }: {target: HTMLInputElement}) => debouncePositiveDescription(target.value)}
|
||||||
sx={{
|
sx={{
|
||||||
width: positiveTextWidth + 10 + "px",
|
width: positiveTextWidth + 10 + "px",
|
||||||
maxWidth: isMobile ? "140px" : "230px",
|
maxWidth: isMobile ? "140px" : "230px",
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionRating } from "@model/questionTypes/rating";
|
import { QuizQuestionRating } from "@model/questionTypes/rating";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingRating from "./settingRating";
|
import SettingRating from "./settingRating";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchRating({
|
|||||||
return <SettingRating question={question} />;
|
return <SettingRating question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionNumber } from "@model/questionTypes/number";
|
import { QuizQuestionNumber } from "@model/questionTypes/number";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingSlider from "./settingSlider";
|
import SettingSlider from "./settingSlider";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchSlider({
|
|||||||
return <SettingSlider question={question} />;
|
return <SettingSlider question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
@ -1,5 +1,4 @@
|
|||||||
import { QuizQuestionFile } from "@model/questionTypes/file";
|
import { QuizQuestionFile } from "@model/questionTypes/file";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import SettingsUpload from "./settingUpload";
|
import SettingsUpload from "./settingUpload";
|
||||||
|
|
||||||
@ -18,8 +17,6 @@ export default function SwitchUpload({
|
|||||||
return <SettingsUpload question={question} />;
|
return <SettingsUpload question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
default:
|
default:
|
||||||
return <></>;
|
return <></>;
|
||||||
}
|
}
|
||||||
|
@ -1,6 +1,5 @@
|
|||||||
import { QuizQuestionVariant } from "@model/questionTypes/variant";
|
import { QuizQuestionVariant } from "@model/questionTypes/variant";
|
||||||
import UploadImage from "../UploadImage";
|
import UploadImage from "../UploadImage";
|
||||||
import BranchingQuestions from "../branchingQuestions";
|
|
||||||
import HelpQuestions from "../helpQuestions";
|
import HelpQuestions from "../helpQuestions";
|
||||||
import ResponseSettings from "./responseSettings";
|
import ResponseSettings from "./responseSettings";
|
||||||
|
|
||||||
@ -19,8 +18,6 @@ export default function SwitchAnswerOptions({
|
|||||||
return <ResponseSettings question={question} />;
|
return <ResponseSettings question={question} />;
|
||||||
case "help":
|
case "help":
|
||||||
return <HelpQuestions question={question} />;
|
return <HelpQuestions question={question} />;
|
||||||
case "branching":
|
|
||||||
return <BranchingQuestions question={question} />;
|
|
||||||
case "image":
|
case "image":
|
||||||
return <UploadImage question={question} />;
|
return <UploadImage question={question} />;
|
||||||
default:
|
default:
|
||||||
|
@ -1,338 +0,0 @@
|
|||||||
import InfoIcon from "@icons/Info";
|
|
||||||
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
|
||||||
import { AnyQuizQuestion } from "@model/questionTypes/shared";
|
|
||||||
import {
|
|
||||||
Box,
|
|
||||||
Button,
|
|
||||||
Chip,
|
|
||||||
FormControl,
|
|
||||||
FormControlLabel,
|
|
||||||
IconButton,
|
|
||||||
Link,
|
|
||||||
Modal,
|
|
||||||
Radio,
|
|
||||||
RadioGroup,
|
|
||||||
Tooltip,
|
|
||||||
Typography,
|
|
||||||
useTheme,
|
|
||||||
} from "@mui/material";
|
|
||||||
import { updateQuestion } from "@root/questions/actions";
|
|
||||||
import RadioCheck from "@ui_kit/RadioCheck";
|
|
||||||
import RadioIcon from "@ui_kit/RadioIcon";
|
|
||||||
import { useEffect, useRef, useState } from "react";
|
|
||||||
import { Select } from "./Select";
|
|
||||||
|
|
||||||
|
|
||||||
type BranchingQuestionsProps = {
|
|
||||||
question: AnyQuizQuestion;
|
|
||||||
};
|
|
||||||
|
|
||||||
const ACTIONS = ["Показать", "Скрыть"];
|
|
||||||
const STIPULATIONS = ["Условие 1", "Условие 2", "Условие 3"];
|
|
||||||
const ANSWERS = ["Ответ 1", "Ответ 2", "Ответ 3"];
|
|
||||||
const CONDITIONS = [
|
|
||||||
"Все условия обязательны",
|
|
||||||
"Обязательно хотя бы одно условие",
|
|
||||||
];
|
|
||||||
|
|
||||||
export default function BranchingQuestions({
|
|
||||||
question,
|
|
||||||
}: BranchingQuestionsProps) {
|
|
||||||
const theme = useTheme();
|
|
||||||
const [title, setTitle] = useState<string>("");
|
|
||||||
const [titleInputWidth, setTitleInputWidth] = useState<number>(0);
|
|
||||||
const titleRef = useRef<HTMLDivElement>(null);
|
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
setTitleInputWidth(titleRef.current?.offsetWidth || 0);
|
|
||||||
}, [title]);
|
|
||||||
|
|
||||||
const handleClose = () => {
|
|
||||||
updateQuestion(question.id, question => {
|
|
||||||
question.openedModalSettings = false;
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Modal open={question.openedModalSettings} onClose={handleClose}>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
overflow: "hidden",
|
|
||||||
top: "50%",
|
|
||||||
left: "50%",
|
|
||||||
transform: "translate(-50%, -50%)",
|
|
||||||
maxWidth: "620px",
|
|
||||||
width: "100%",
|
|
||||||
bgcolor: "background.paper",
|
|
||||||
borderRadius: "12px",
|
|
||||||
boxShadow: 24,
|
|
||||||
p: 0,
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
boxSizing: "border-box",
|
|
||||||
background: "#F2F3F7",
|
|
||||||
height: "70px",
|
|
||||||
padding: "0 25px",
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box sx={{ color: "#9A9AAF" }}>
|
|
||||||
<Typography component="span">(</Typography>
|
|
||||||
<Box sx={{ display: "inline" }}>
|
|
||||||
<Typography
|
|
||||||
ref={titleRef}
|
|
||||||
sx={{
|
|
||||||
position: "absolute",
|
|
||||||
opacity: 0,
|
|
||||||
zIndex: "-100",
|
|
||||||
whiteSpace: "pre",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{title}
|
|
||||||
</Typography>
|
|
||||||
<input
|
|
||||||
type="text"
|
|
||||||
value={title}
|
|
||||||
placeholder="Заголовок вопроса"
|
|
||||||
onChange={({ target }) => setTitle(target.value)}
|
|
||||||
style={{
|
|
||||||
width: titleInputWidth ? titleInputWidth : 170,
|
|
||||||
outline: "none",
|
|
||||||
background: "transparent",
|
|
||||||
border: "none",
|
|
||||||
fontSize: "18px",
|
|
||||||
minWidth: "50px",
|
|
||||||
maxWidth: "500px",
|
|
||||||
fontFamily: "Rubik",
|
|
||||||
transition: ".2s",
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
</Box>
|
|
||||||
<Typography component="span">)</Typography>
|
|
||||||
</Box>
|
|
||||||
<Tooltip
|
|
||||||
title="Настройте условия, при которых данный вопрос будет отображаться в квизе."
|
|
||||||
placement="top"
|
|
||||||
>
|
|
||||||
<Box>
|
|
||||||
<InfoIcon />
|
|
||||||
</Box>
|
|
||||||
</Tooltip>
|
|
||||||
</Box>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
padding: "20px",
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
gap: "30px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
gap: "20px",
|
|
||||||
alignItems: "center",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Select
|
|
||||||
items={ACTIONS}
|
|
||||||
activeItemIndex={question.content.rule.show ? 0 : 1}
|
|
||||||
sx={{ maxWidth: "140px" }}
|
|
||||||
onChange={(action) => {
|
|
||||||
updateQuestion(question.id, question => {
|
|
||||||
question.content.rule.show = action === ACTIONS[0];
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Typography sx={{ color: theme.palette.grey2.main }}>
|
|
||||||
если в ответе на вопрос
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
{question.content.rule.reqs.map((request, index) => (
|
|
||||||
<Box
|
|
||||||
key={index}
|
|
||||||
sx={{
|
|
||||||
padding: "20px",
|
|
||||||
borderRadius: "8px",
|
|
||||||
height: "100%",
|
|
||||||
bgcolor: "#F2F3F7",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
alignItems: "center",
|
|
||||||
pb: "5px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
|
||||||
Условие 1
|
|
||||||
</Typography>
|
|
||||||
<IconButton
|
|
||||||
sx={{ borderRadius: "6px", padding: "2px" }}
|
|
||||||
onClick={() => {
|
|
||||||
updateQuestion(question.id, question => {
|
|
||||||
question.content.rule.reqs.splice(index, 1);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<DeleteIcon color={"#4D4D4D"} />
|
|
||||||
</IconButton>
|
|
||||||
</Box>
|
|
||||||
<Select
|
|
||||||
empty
|
|
||||||
activeItemIndex={request.id ? Number(request.id) : -1}
|
|
||||||
items={STIPULATIONS}
|
|
||||||
onChange={(stipulation) => {
|
|
||||||
updateQuestion(question.id, question => {
|
|
||||||
question.content.rule.reqs[index].id = String(
|
|
||||||
STIPULATIONS.findIndex((item) => item.includes(stipulation))
|
|
||||||
);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
sx={{ marginBottom: "15px" }}
|
|
||||||
/>
|
|
||||||
{request.id && (
|
|
||||||
<>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
alignItems: "center",
|
|
||||||
pb: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
|
|
||||||
Дан ответ
|
|
||||||
</Typography>
|
|
||||||
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>
|
|
||||||
(Укажите один или несколько вариантов)
|
|
||||||
</Typography>
|
|
||||||
</Box>
|
|
||||||
<Select
|
|
||||||
empty
|
|
||||||
activeItemIndex={-1}
|
|
||||||
items={ANSWERS}
|
|
||||||
onChange={(answer) => {
|
|
||||||
const answerItemIndex = ANSWERS.findIndex(
|
|
||||||
(answerItem) => answerItem === answer
|
|
||||||
);
|
|
||||||
updateQuestion(question.id, question => {
|
|
||||||
const vars = question.content.rule.reqs[index].vars;
|
|
||||||
if (vars.includes(answerItemIndex)) {
|
|
||||||
vars.push(answerItemIndex);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
sx={{
|
|
||||||
marginBottom: "10px",
|
|
||||||
".MuiSelect-select.MuiInputBase-input": {
|
|
||||||
color: "transparent",
|
|
||||||
},
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
gap: "10px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{question.content.rule.reqs[index].vars.map(
|
|
||||||
(item, varIndex) => (
|
|
||||||
<Chip
|
|
||||||
key={varIndex}
|
|
||||||
label={ANSWERS[item]}
|
|
||||||
variant="outlined"
|
|
||||||
onDelete={() => {
|
|
||||||
updateQuestion(question.id, question => {
|
|
||||||
const vars = question.content.rule.reqs[index].vars;
|
|
||||||
|
|
||||||
const removedItemIndex = vars.findIndex((varItem) => varItem === item);
|
|
||||||
if (removedItemIndex === -1) return;
|
|
||||||
|
|
||||||
vars.splice(removedItemIndex, 1);
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
/>
|
|
||||||
)
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
</>
|
|
||||||
)}
|
|
||||||
</Box>
|
|
||||||
))}
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexDirection: "column",
|
|
||||||
alignItems: "baseline",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Link
|
|
||||||
variant="body2"
|
|
||||||
sx={{
|
|
||||||
color: theme.palette.brightPurple.main,
|
|
||||||
marginBottom: "10px",
|
|
||||||
}}
|
|
||||||
onClick={() => {
|
|
||||||
updateQuestion(question.id, question => {
|
|
||||||
question.content.rule.reqs.push({ id: "", vars: [] });
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
Добавить условие
|
|
||||||
</Link>
|
|
||||||
<FormControl>
|
|
||||||
<RadioGroup
|
|
||||||
aria-labelledby="demo-controlled-radio-buttons-group"
|
|
||||||
value={question.content.rule.or ? 1 : 0}
|
|
||||||
onChange={(_, value) => {
|
|
||||||
updateQuestion(question.id, question => {
|
|
||||||
question.content.rule.or = Boolean(Number(value));
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{CONDITIONS.map((condition, index) => (
|
|
||||||
<FormControlLabel
|
|
||||||
key={index}
|
|
||||||
sx={{ color: theme.palette.grey2.main }}
|
|
||||||
value={index}
|
|
||||||
control={
|
|
||||||
<Radio
|
|
||||||
checkedIcon={<RadioCheck />}
|
|
||||||
icon={<RadioIcon />}
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
label={condition}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</RadioGroup>
|
|
||||||
</FormControl>
|
|
||||||
</Box>
|
|
||||||
<Box sx={{ display: "flex", justifyContent: "end", gap: "10px" }}>
|
|
||||||
<Button
|
|
||||||
variant="outlined"
|
|
||||||
onClick={handleClose}
|
|
||||||
sx={{ width: "100%", maxWidth: "130px" }}
|
|
||||||
>
|
|
||||||
Отмена
|
|
||||||
</Button>
|
|
||||||
<Button
|
|
||||||
variant="contained"
|
|
||||||
sx={{ width: "100%", maxWidth: "130px" }}
|
|
||||||
onClick={handleClose}
|
|
||||||
>
|
|
||||||
Готово
|
|
||||||
</Button>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Box>
|
|
||||||
</Modal>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
}
|
|
@ -1,6 +1,5 @@
|
|||||||
import Info from "@icons/Info";
|
import Info from "@icons/Info";
|
||||||
import { Box, TextField } from "@mui/material";
|
import { Box, TextField } from "@mui/material";
|
||||||
import BranchingQuestions from "../../Questions/branchingQuestions";
|
|
||||||
import PointsQuestions from "./PointsQuestions";
|
import PointsQuestions from "./PointsQuestions";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@ -45,10 +44,6 @@ export default function SwitchResult({
|
|||||||
case "setting":
|
case "setting":
|
||||||
return <ResponseSettings />;
|
return <ResponseSettings />;
|
||||||
break;
|
break;
|
||||||
case "branching":
|
|
||||||
// return <BranchingQuestions question={question} />;
|
|
||||||
return null
|
|
||||||
break;
|
|
||||||
case "points":
|
case "points":
|
||||||
return <PointsQuestions />;
|
return <PointsQuestions />;
|
||||||
break;
|
break;
|
||||||
|
@ -617,7 +617,7 @@ export default function StartPageSettings() {
|
|||||||
Заголовок
|
Заголовок
|
||||||
</Typography>
|
</Typography>
|
||||||
<CustomTextField
|
<CustomTextField
|
||||||
placeholder="Текст-заполнитель — это текст, который"
|
placeholder="Название квиза"
|
||||||
text={quiz.name}
|
text={quiz.name}
|
||||||
onChange={e => updateQuiz(quiz.id, quiz => {
|
onChange={e => updateQuiz(quiz.id, quiz => {
|
||||||
quiz.name = e.target.value;
|
quiz.name = e.target.value;
|
||||||
|
@ -354,4 +354,18 @@ function setProducedState<A extends string | { type: unknown; }>(
|
|||||||
action?: A,
|
action?: A,
|
||||||
) {
|
) {
|
||||||
useQuestionsStore.setState(state => produce(state, recipe), false, action);
|
useQuestionsStore.setState(state => produce(state, recipe), false, action);
|
||||||
}
|
};
|
||||||
|
|
||||||
|
|
||||||
|
export const clearDragQuestionId = () => {
|
||||||
|
useQuestionsStore.setState({dragQuestionId: null});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const getQuestionById = (questionId: string | null) => {
|
||||||
|
if (questionId === null) return null;
|
||||||
|
return useQuestionsStore.getState().questions.find(q => q.id === questionId) || null;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const updateOpenedModalSettingsId = (id?: string) => useQuestionsStore.setState({openedModalSettingsId: id ? id : null});
|
||||||
|
export const updateDragQuestionId = (id?: string) => useQuestionsStore.setState({dragQuestionId: id ? id : null});
|
||||||
|
|
||||||
|
@ -5,10 +5,14 @@ import { devtools } from "zustand/middleware";
|
|||||||
|
|
||||||
export type QuestionsStore = {
|
export type QuestionsStore = {
|
||||||
questions: AnyQuizQuestion[];
|
questions: AnyQuizQuestion[];
|
||||||
|
openedModalSettingsId: string | null;
|
||||||
|
dragQuestionId: string | null;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: QuestionsStore = {
|
const initialState: QuestionsStore = {
|
||||||
questions: [],
|
questions: [],
|
||||||
|
openedModalSettingsId: null as null,
|
||||||
|
dragQuestionId: null,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useQuestionsStore = create<QuestionsStore>()(
|
export const useQuestionsStore = create<QuestionsStore>()(
|
||||||
|
@ -172,6 +172,12 @@ export const deleteQuiz = async (quizId: string) => requestQueue.enqueue(async (
|
|||||||
enqueueSnackbar(`Не удалось удалить квиз. ${message}`);
|
enqueueSnackbar(`Не удалось удалить квиз. ${message}`);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
export const updateRootInfo = (quizId: string, have:boolean) => updateQuiz(
|
||||||
|
quizId,
|
||||||
|
quiz => {
|
||||||
|
quiz.config.haveRoot = have;
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
// TODO copy quiz
|
// TODO copy quiz
|
||||||
|
|
||||||
|
@ -86,7 +86,7 @@ export default () => {
|
|||||||
>E-mail</Typography>
|
>E-mail</Typography>
|
||||||
<TextField
|
<TextField
|
||||||
value={mail}
|
value={mail}
|
||||||
onChange={(e) => setContactFormMailField(e.target.value)}
|
onChange={(e: React.ChangeEvent<HTMLInputElement>) => setContactFormMailField(e.target.value)}
|
||||||
placeholder="username@penahaub.com"
|
placeholder="username@penahaub.com"
|
||||||
name="name"
|
name="name"
|
||||||
fullWidth
|
fullWidth
|
||||||
|
Loading…
Reference in New Issue
Block a user