This commit is contained in:
Nastya 2024-06-08 05:05:14 +03:00
commit 0df1417dda
21 changed files with 739 additions and 183 deletions

@ -0,0 +1,22 @@
import { FC } from "react";
import { Box } from "@mui/material";
export const AlignIcon: FC = () => (
<Box sx={{ width: `20px`, height: "20px" }}>
<svg
width="20"
height="20"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M19 8.18348L11.8 8.22299V1M8.2 19V11.8165L1 11.7752"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);

File diff suppressed because one or more lines are too long

@ -0,0 +1,29 @@
import { FC } from "react";
import { Box } from "@mui/material";
export const ExpandIcon: FC = () => (
<Box sx={{ width: `24px`, height: "24px" }}>
<svg
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M18 6.58545L12.4984 2.17075C12.434 2.11677 12.3566 2.07382 12.271 2.04447C12.1853 2.01513 12.0931 2 12 2C11.9069 2 11.8147 2.01513 11.729 2.04447C11.6434 2.07382 11.566 2.11677 11.5016 2.17075L6 6.58545M18 17.4146L12.4984 21.8293C12.434 21.8832 12.3566 21.9262 12.271 21.9555C12.1853 21.9849 12.0931 22 12 22C11.9069 22 11.8147 21.9849 11.729 21.9555C11.6434 21.9262 11.566 21.8832 11.5016 21.8293L6 17.4146"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M11.9988 14.0054C13.0682 14.0054 13.9351 13.1385 13.9351 12.0691C13.9351 10.9997 13.0682 10.1328 11.9988 10.1328C10.9294 10.1328 10.0625 10.9997 10.0625 12.0691C10.0625 13.1385 10.9294 14.0054 11.9988 14.0054Z"
stroke="white"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);

@ -0,0 +1,29 @@
import { FC } from "react";
import { Box } from "@mui/material";
export const GrayPlus: FC = () => (
<Box sx={{ width: `32px` }}>
<svg
width="32"
height="32"
viewBox="0 0 32 32"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M16.0029 1V31"
stroke="#9A9AAF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
<path
d="M31 15.9941L1 15.9941"
stroke="#9A9AAF"
strokeWidth="1.5"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);

@ -0,0 +1,38 @@
import { FC } from "react";
import { Box } from "@mui/material";
export const RoundedCheckedIcon: FC = () => (
<Box
sx={{
width: `26px`,
height: "26px",
display: "flex",
alignItems: "center",
justifyContent: "center",
}}
>
<svg
width="26"
height="27"
viewBox="0 0 26 27"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<rect
x="0.5"
y="1"
width="25"
height="25"
rx="12.5"
fill="#F2F3F7"
stroke="#F2F3F7"
/>
<path
d="M8 13.5L12.2857 17.5L18 10"
stroke="#9A9AAF"
strokeLinecap="round"
strokeLinejoin="round"
/>
</svg>
</Box>
);

@ -1,6 +1,6 @@
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { devlog } from "@frontend/kitui";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { Box, Button } from "@mui/material";
import { clearRuleForAll } from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store";
import { updateRootContentId } from "@root/quizes/actions";
@ -14,7 +14,6 @@ import {
import { useUiTools } from "@root/uiTools/store";
import type { Core } from "cytoscape";
import { enqueueSnackbar } from "notistack";
import { useEffect, useLayoutEffect, useMemo, useRef } from "react";
import CytoscapeComponent from "react-cytoscapejs";
import { withErrorBoundary } from "react-error-boundary";
import { DeleteNodeModal } from "../DeleteNodeModal";
@ -23,8 +22,15 @@ import { addNode, layoutOptions, storeToNodes } from "./helper";
import { useRemoveNode } from "./hooks/useRemoveNode";
import "./style/styles.css";
import { stylesheet } from "./style/stylesheet";
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { InfoBanner } from "./InfoBanner/InfoBanner";
import { PositionControl } from "./PositionControl/PositionControl";
import { ZoomControl } from "./ZoomControl/ZoomControl";
function CsComponent() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const [isBannerVisible, setBannerVisible] = useState(true);
const desireToOpenABranchingModal = useUiTools((state) => state.desireToOpenABranchingModal);
const modalQuestionParentContentId = useUiTools((state) => state.modalQuestionParentContentId);
const modalQuestionTargetContentId = useUiTools((state) => state.modalQuestionTargetContentId);
@ -82,37 +88,25 @@ function CsComponent() {
);
return (
<>
<Box mb="20px">
<Button
sx={{
height: "27px",
color: "#7E2AEA",
textDecoration: "underline",
fontSize: "16px",
}}
variant="text"
onClick={() => {
cyRef.current?.fit(undefined, 70);
}}
>
Выровнять
</Button>
</Box>
<Box
sx={{
position: "relative",
height: "480px",
background: "#F2F3F7",
}}
>
<Box
sx={{
width: "100%",
}}
>
<CsNodeButtons csElements={csElements} cyRef={cyRef} />
<Box sx={{ position: "relative" }}>
{isBannerVisible && <InfoBanner setBannerVisible={setBannerVisible} />}
<PositionControl cyRef={cyRef} />
<ZoomControl cyRef={cyRef} />
<CytoscapeComponent
wheelSensitivity={0.1}
elements={csElements}
style={{
height: "100%",
width: "100%",
height: isMobile ? "327px" : "481px",
background: "#F2F3F7",
overflow: "hidden",
borderRadius: "12px",
width: "100%",
}}
stylesheet={stylesheet}
layout={layoutOptions}
@ -123,10 +117,9 @@ function CsComponent() {
autounselectify={true}
boxSelectionEnabled={false}
/>
<CsNodeButtons csElements={csElements} cyRef={cyRef} />
</Box>
<DeleteNodeModal removeNode={removeNode} />
</>
</Box>
);
}

@ -8,14 +8,29 @@ import {
updateOpenedModalSettingsId,
} from "@root/uiTools/actions";
import { Core, EventObject, NodeSingular } from "cytoscape";
import { MutableRefObject, forwardRef, useEffect, useMemo, useRef } from "react";
import { addNode, isElementANode, isNodeInViewport, isQuestionProhibited, storeToNodes } from "./helper";
import {
forwardRef,
MutableRefObject,
useEffect,
useMemo,
useRef,
} from "react";
import { createPortal } from "react-dom";
import {
canAddChildToQuestion,
isElementANode,
isNodeInViewport,
isQuestionProhibited,
storeToNodes,
} from "./helper";
const csButtonTypes = ["delete", "add", "settings", "select"] as const;
type CsButtonType = (typeof csButtonTypes)[number];
type CsNodeButtonsByType = Partial<Record<CsButtonType, HTMLButtonElement | null>>;
type CsNodeButtonsByType = Partial<
Record<CsButtonType, HTMLButtonElement | null>
>;
type CsButtonsById = Record<string, CsNodeButtonsByType | undefined>;
@ -29,7 +44,10 @@ export default function CsNodeButtons({ csElements, cyRef }: Props) {
const buttons = useMemo(() => {
const nodeElements = csElements.filter(isElementANode);
buttonRefsById.current = nodeElements.reduce<CsButtonsById>((acc, node) => ((acc[node.data.id] = {}), acc), {});
buttonRefsById.current = nodeElements.reduce<CsButtonsById>(
(acc, node) => ((acc[node.data.id] = {}), acc),
{},
);
return (
<Box
sx={{
@ -38,10 +56,7 @@ export default function CsNodeButtons({ csElements, cyRef }: Props) {
left: 0,
width: "100%",
height: "100%",
pointerEvents: "none",
"> *": {
pointerEvents: "auto",
},
borderRadius: "12px",
}}
>
{nodeElements.flatMap((csElement) => [
@ -61,23 +76,24 @@ export default function CsNodeButtons({ csElements, cyRef }: Props) {
const buttonData = buttonRefsById.current[csElement.data.id];
if (buttonData) buttonData.add = r;
}}
onPointerUp={() => {
addNode({ parentNodeContentId: csElement.data.id });
cleardragQuestionContentId();
onClick={() => {
setModalQuestionParentContentId(csElement.data.id);
setOpenedModalQuestions(canAddChildToQuestion(csElement.data.id));
}}
/>,
!csElement.data.isRoot && !isQuestionProhibited(csElement.data.qtype) && (
<CsSettingsButton
key={`settings-${csElement.data.id}`}
ref={(r) => {
const buttonData = buttonRefsById.current[csElement.data.id];
if (buttonData) buttonData.settings = r;
}}
onClick={() => {
updateOpenedModalSettingsId(csElement.data.id);
}}
/>
),
!csElement.data.isRoot &&
!isQuestionProhibited(csElement.data.qtype) && (
<CsSettingsButton
key={`settings-${csElement.data.id}`}
ref={(r) => {
const buttonData = buttonRefsById.current[csElement.data.id];
if (buttonData) buttonData.settings = r;
}}
onClick={() => {
updateOpenedModalSettingsId(csElement.data.id);
}}
/>
),
//оболочка узла
<CsSelectButton
key={`select-${csElement.data.id}`}
@ -87,7 +103,7 @@ export default function CsNodeButtons({ csElements, cyRef }: Props) {
}}
onClick={() => {
setModalQuestionParentContentId(csElement.data.id);
setOpenedModalQuestions(!(isQuestionProhibited(csElement.data.type) && csElement.data.children > 0));
setOpenedModalQuestions(canAddChildToQuestion(csElement.data.id));
}}
/>,
])}
@ -130,7 +146,10 @@ export default function CsNodeButtons({ csElements, cyRef }: Props) {
};
}, []);
return buttons;
const container = cyRef.current?.container();
const buttonsPortal = container ? createPortal(buttons, container) : null;
return buttonsPortal;
}
const applyButtonStyleByType: Record<
@ -139,8 +158,10 @@ const applyButtonStyleByType: Record<
> = {
delete(button, node, zoom) {
const nodePosition = node.renderedPosition();
const shiftX = node.renderedWidth() / 2 - (CLOSE_BUTTON_WIDTH / 2 + 6) * zoom;
const shiftY = node.renderedHeight() / 2 - (CLOSE_BUTTON_HEIGHT / 2 + 6) * zoom;
const shiftX =
node.renderedWidth() / 2 - (CLOSE_BUTTON_WIDTH / 2 + 6) * zoom;
const shiftY =
node.renderedHeight() / 2 - (CLOSE_BUTTON_HEIGHT / 2 + 6) * zoom;
if (!isNodeInViewport(node, 100)) {
return button.style.setProperty("display", "none");
@ -151,12 +172,13 @@ const applyButtonStyleByType: Record<
button.style.setProperty("top", `${nodePosition.y}px`);
button.style.setProperty(
"transform",
`translate3d(calc(-50% + ${shiftX}px), calc(-50% - ${shiftY}px), 0) scale(${zoom})`
`translate3d(calc(-50% + ${shiftX}px), calc(-50% - ${shiftY}px), 0) scale(${zoom})`,
);
},
add(button, node, zoom) {
const nodePosition = node.renderedPosition();
const shiftX = node.renderedWidth() / 2 + (ADD_BUTTON_WIDTH / 2) * zoom;
const shiftX =
node.renderedWidth() / 2 + (ADD_BUTTON_WIDTH / 2 - 10) * zoom;
if (!isNodeInViewport(node, 100)) {
return button.style.setProperty("display", "none");
@ -165,11 +187,15 @@ const applyButtonStyleByType: Record<
button.style.setProperty("display", "flex");
button.style.setProperty("left", `${nodePosition.x}px`);
button.style.setProperty("top", `${nodePosition.y}px`);
button.style.setProperty("transform", `translate3d(calc(-50% + ${shiftX}px), -50%, 0) scale(${zoom})`);
button.style.setProperty(
"transform",
`translate3d(calc(-50% + ${shiftX}px), -50%, 0) scale(${zoom})`,
);
},
settings(button, node, zoom) {
const nodePosition = node.renderedPosition();
const shiftX = -node.renderedWidth() / 2 - (SETTINGS_BUTTON_WIDTH / 2) * zoom;
const shiftX =
-node.renderedWidth() / 2 - (SETTINGS_BUTTON_WIDTH / 2) * zoom;
if (!isNodeInViewport(node, 100)) {
return button.style.setProperty("display", "none");
@ -178,7 +204,10 @@ const applyButtonStyleByType: Record<
button.style.setProperty("display", "flex");
button.style.setProperty("left", `${nodePosition.x}px`);
button.style.setProperty("top", `${nodePosition.y}px`);
button.style.setProperty("transform", `translate3d(calc(-50% + ${shiftX}px), -50%, 0) scale(${zoom})`);
button.style.setProperty(
"transform",
`translate3d(calc(-50% + ${shiftX}px), -50%, 0) scale(${zoom})`,
);
},
select(button, node, zoom) {
const nodePosition = node.renderedPosition();
@ -190,7 +219,10 @@ const applyButtonStyleByType: Record<
button.style.setProperty("display", "flex");
button.style.setProperty("left", `${nodePosition.x}px`);
button.style.setProperty("top", `${nodePosition.y}px`);
button.style.setProperty("transform", `translate3d(-50%, -50%, 0) scale(${zoom})`);
button.style.setProperty(
"transform",
`translate3d(-50%, -50%, 0) scale(${zoom})`,
);
},
};
@ -233,9 +265,9 @@ const ADD_BUTTON_HEIGHT = ADD_BUTTON_WIDTH;
const CsAddButton = forwardRef<
HTMLButtonElement,
{
onPointerUp: () => void;
onClick: () => void;
}
>(({ onPointerUp }, ref) => (
>(({ onClick }, ref) => (
<IconButton
ref={ref}
sx={{
@ -252,7 +284,7 @@ const CsAddButton = forwardRef<
backgroundColor: "#f9f9fc",
},
}}
onPointerUp={onPointerUp}
onClick={onClick}
onMouseDownCapture={(event) => event.stopPropagation()}
onTouchStartCapture={(event) => event.stopPropagation()}
>
@ -260,8 +292,8 @@ const CsAddButton = forwardRef<
</IconButton>
));
const SETTINGS_BUTTON_WIDTH = 70;
const SETTINGS_BUTTON_HEIGHT = 60;
const SETTINGS_BUTTON_WIDTH = 50;
const SETTINGS_BUTTON_HEIGHT = 40;
const CsSettingsButton = forwardRef<
HTMLButtonElement,
@ -329,7 +361,8 @@ const CsSelectButton = forwardRef<
width: SELECT_BUTTON_WIDTH,
height: SELECT_BUTTON_HEIGHT,
backgroundColor: "rgb(0 0 0 / 0)",
borderRadius: "8px",
border: "1px solid #9A9AAF",
borderRadius: "6px",
p: 0,
zIndex: 0,
}}

@ -1,4 +1,10 @@
import { Box } from "@mui/material";
import {
Box,
Button,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import {
clearRuleForAll,
createResult,
@ -14,8 +20,11 @@ import {
import { useUiTools } from "@root/uiTools/store";
import { enqueueSnackbar } from "notistack";
import { useEffect, useLayoutEffect, useRef } from "react";
import { GrayPlus } from "@icons/questionsPage/GrayPlus";
export const FirstNodeField = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const quiz = useCurrentQuiz();
const modalQuestionTargetContentId = useUiTools(
(state) => state.modalQuestionTargetContentId,
@ -80,17 +89,49 @@ export const FirstNodeField = () => {
<Box
ref={Container}
sx={{
height: "100%",
height: isMobile ? "327px" : "481px",
width: "100%",
backgroundColor: "#f2f3f7",
backgroundColor: theme.palette.background.default,
display: "flex",
alignItems: "center",
justifyContent: "center",
color: "#4d4d4d",
color: theme.palette.grey3.main,
fontSize: "50px",
borderRadius: "12px",
padding: "20px",
}}
>
+
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "center",
gap: "20px",
width: "275px",
}}
>
<Typography
sx={{
color: theme.palette.grey2.main,
textAlign: "center",
}}
>
Добавьте созданные вопросы и настройте связи между ними
</Typography>
<Button
sx={{
width: "70px",
height: "70px",
borderRadius: "12px",
background: "rgba(154, 154, 175, 0.09)",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<GrayPlus />
</Button>
</Box>
</Box>
);
};

@ -0,0 +1,99 @@
import {
Box,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { Add } from "@mui/icons-material";
import { EditIcon } from "@icons/questionsPage/EditIcon";
import CloseIcon from "@mui/icons-material/Close";
import { Dispatch, FC, SetStateAction } from "react";
type InfoBannerProps = {
setBannerVisible: Dispatch<SetStateAction<boolean>>;
};
export const InfoBanner: FC<InfoBannerProps> = ({ setBannerVisible }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
return (
<Box
sx={{
position: "absolute",
top: "30px",
right: "30px",
padding: "20px",
borderRadius: "12px",
background: "#fff",
display: "flex",
flexDirection: "column",
justifyContent: "center",
alignItems: "center",
zIndex: 2,
boxShadow: "0 0 20px rgba(0, 0, 0, 0.15)",
animation: "fadeIn 0.5s",
}}
>
<Box
sx={{
maxWidth: isMobile ? "253px" : "345px",
color: "#9A9AAF",
display: "flex",
flexDirection: "column",
gap: "10px",
}}
>
<Box sx={{ display: "flex", justifyContent: "space-between" }}>
<Typography>Добавьте больше вопросов кнопкой</Typography>
<Box
sx={{
width: "20px",
height: "20px",
color: "rgba(154, 154, 175, 0.5)",
backgroundColor: "#eeeff4",
borderRadius: "6px",
border: "1.5px dashed rgba(154, 154, 175, 0.5)",
display: "flex",
justifyContent: "center",
alignItems: "center",
}}
>
<Add fontSize={"small"} />
</Box>
</Box>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
}}
>
<Typography>
Настраивайте условия их отображения в квизе с помощью
</Typography>
<EditIcon />
</Box>
</Box>
<IconButton
sx={{
position: "absolute",
top: "-10px",
right: "-10px",
width: "30px",
height: "30px",
background: "#4D4D4D",
opacity: "0.2",
BorderRadius: "50%",
color: "#fff",
"&:hover": {
background: "black",
},
}}
onClick={() => setBannerVisible(false)}
>
<CloseIcon fontSize={"medium"} />
</IconButton>
</Box>
);
};

@ -0,0 +1,64 @@
import { Box, Button, useMediaQuery, useTheme } from "@mui/material";
import { ExpandIcon } from "@icons/questionsPage/ExpandIcon";
import { AlignIcon } from "@icons/questionsPage/AlignIcon";
import { FC, MutableRefObject } from "react";
import { Core } from "cytoscape";
type PositionControlProps = {
cyRef: MutableRefObject<Core | null>;
};
export const PositionControl: FC<PositionControlProps> = ({ cyRef }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
return (
<Box
sx={{
position: "absolute",
bottom: isMobile ? "15px" : "20px",
left: isMobile ? "15px" : "20px",
display: "flex",
gap: "10px",
}}
>
<Button
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minWidth: "36px",
width: "36px",
height: "36px",
background: "#9A9AAF",
opacity: "0.7",
zIndex: 2,
}}
onClick={() => {
cyRef.current?.zoom(1);
cyRef.current?.center();
}}
>
<ExpandIcon />
</Button>
<Button
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minWidth: "36px",
width: "36px",
height: "36px",
background: "#9A9AAF",
opacity: "0.7",
zIndex: 2,
}}
onClick={() => {
cyRef.current?.fit(undefined, 70);
}}
>
<AlignIcon />
</Button>
</Box>
);
};

@ -0,0 +1,72 @@
import { Box, Button, useMediaQuery, useTheme } from "@mui/material";
import { FC, MutableRefObject } from "react";
import AddIcon from "@mui/icons-material/Add";
import RemoveIcon from "@mui/icons-material/Remove";
import { Core } from "cytoscape";
type PositionControlProps = {
cyRef: MutableRefObject<Core | null>;
};
export const ZoomControl: FC<PositionControlProps> = ({ cyRef }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
return (
<Box
sx={{
position: "absolute",
bottom: isMobile ? "15px" : "20px",
right: isMobile ? "15px" : "20px",
display: "flex",
flexDirection: "column",
gap: "10px",
background: "#EEE4FC",
padding: "16px",
borderRadius: "8px",
zIndex: 2,
}}
>
<Button
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minWidth: "36px",
width: "36px",
height: "36px",
background: "#7E2AEA",
fontSize: "16px",
color: "#fff",
zIndex: 2,
}}
onClick={() => {
const currentZoom = cyRef.current?.zoom() || 1;
cyRef.current?.zoom(currentZoom + 0.1);
}}
>
<AddIcon />
</Button>
<Button
sx={{
display: "flex",
justifyContent: "center",
alignItems: "center",
minWidth: "36px",
width: "36px",
height: "36px",
background: "#7E2AEA",
fontSize: "16px",
color: "#fff",
zIndex: 2,
}}
onClick={() => {
const currentZoom = cyRef.current?.zoom() || 1;
cyRef.current?.zoom(currentZoom - 0.1);
}}
>
<RemoveIcon />
</Button>
</Box>
);
};

@ -1,4 +1,3 @@
import { QuestionType } from "@model/question/question";
import { QuizQuestionResult } from "@model/questionTypes/result";
import {
AnyTypedQuizQuestion,
@ -66,7 +65,8 @@ export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => {
question.title === "" || question.title === " "
? "noname"
: question.title;
if (label.length > 10) label = label.slice(0, 10).toLowerCase() + "…";
label = label.trim().replace(/\s+/g, " ");
if (label.length > 20) label = label.slice(0, 20).toLowerCase() + "…";
nodes.push({
data: {
@ -307,7 +307,7 @@ export function calcNodePosition(node: any) {
const width = n.data("subtreeWidth");
n.data("oldPos", {
x: 250 * n.data("layer"),
x: 350 * n.data("layer"),
y: yoffset + width / 2,
});
yoffset += width;
@ -326,6 +326,21 @@ export function calcNodePosition(node: any) {
}
}
export const canAddChildToQuestion = (parentNodeContentId: string) => {
const parentQuestion = {
...getQuestionByContentId(parentNodeContentId),
} as AnyTypedQuizQuestion;
if (
parentQuestion.type !== undefined &&
isQuestionProhibited(parentQuestion.type) &&
parentQuestion.content.rule.children.length > 0
) {
enqueueSnackbar("У вопроса этого типа может быть только 1 потомок");
return false;
}
return true;
};
export const addNode = ({
parentNodeContentId,
targetNodeContentId,

@ -1,4 +1,4 @@
import { Box } from "@mui/material";
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useUiTools } from "@root/uiTools/store";
import { BranchingQuestionsModal } from "../BranchingQuestionsModal";
@ -6,6 +6,8 @@ import CsComponent from "./CsComponent";
import { FirstNodeField } from "./FirstNodeField";
export const BranchingMap = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const quiz = useCurrentQuiz();
const dragQuestionContentId = useUiTools((state) => state.dragQuestionContentId);
@ -13,13 +15,15 @@ export const BranchingMap = () => {
<Box
sx={{
overflow: "hidden",
padding: "20px",
background: "#FFFFFF",
padding: isMobile ? "15px" : "20px",
background: theme.palette.common.white,
borderRadius: "12px",
boxShadow: "0px 8px 24px rgba(210, 208, 225, 0.4)",
marginBottom: "40px",
height: "568px",
border: dragQuestionContentId === null ? "none" : "#7e2aea 2px dashed",
width: "100%",
border:
dragQuestionContentId === null
? "none"
: `${theme.palette.brightPurple.main} 2px dashed`,
}}
>
{quiz?.config.haveRoot ? <CsComponent /> : <FirstNodeField />}

@ -9,13 +9,14 @@ export const stylesheet: Stylesheet[] = [
height: 130,
backgroundColor: "#FFFFFF",
label: "data(label)",
"font-size": "16",
"font-size": "12",
color: "#4D4D4D",
"text-halign": "center",
"text-halign": "right",
"text-valign": "center",
"text-wrap": "wrap",
"text-max-width": "130px",
"text-overflow-wrap": "whitespace",
"text-margin-x": -115,
},
},
{
@ -40,7 +41,7 @@ export const stylesheet: Stylesheet[] = [
"line-color": "#DEDFE7",
"curve-style": "taxi",
"taxi-direction": "horizontal",
"taxi-turn": 60,
"taxi-turn": 100,
},
},
{

@ -1,34 +1,26 @@
import { useState, useRef, useEffect, useLayoutEffect } from "react";
import { useLayoutEffect, useState } from "react";
import {
Box,
Button,
FormControl,
Checkbox,
FormControlLabel,
Link,
Modal,
Radio,
RadioGroup,
Tooltip,
Typography,
useTheme,
Checkbox,
useMediaQuery,
useTheme,
} from "@mui/material";
import {
AnyTypedQuizQuestion,
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 { TypeSwitch } from "./Settings";
import {
getQuestionById,
getQuestionByContentId,
getQuestionById,
updateQuestion,
} from "@root/questions/actions";
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
@ -101,7 +93,7 @@ export default function BranchingQuestions() {
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: "620px",
width: "100%",
width: "90%",
bgcolor: "background.paper",
borderRadius: "12px",
boxShadow: 24,
@ -114,30 +106,30 @@ export default function BranchingQuestions() {
boxSizing: "border-box",
background: "#F2F3F7",
height: "70px",
padding: "0 25px",
padding: "25px 20px",
display: "flex",
alignItems: "center",
color: "#9A9AAF",
gap: "10px",
}}
>
<Box sx={{ color: "#4d4d4d" }}>
<Typography
component="span"
sx={{
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
maxWidth: isSmallMobile
? "250px"
: isMobile
? "350px"
: "450px",
display: "inline-block",
width: "100%",
}}
>
{targetQuestion.title}
</Typography>
</Box>
<Typography
component="span"
sx={{
lineHeight: "1",
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
maxWidth: isSmallMobile
? "250px"
: isMobile
? "350px"
: "450px",
display: "inline-block",
}}
>
{targetQuestion.title}
</Typography>
{isMobile ? (
<TooltipClickInfo
title={
@ -150,7 +142,7 @@ export default function BranchingQuestions() {
placement="top"
>
<Box>
<InfoIcon />
<InfoIcon sx={{ padding: 0 }} />
</Box>
</Tooltip>
)}

@ -1,4 +1,14 @@
import { Box, Modal, Button, Typography } from "@mui/material";
import {
Box,
Button,
FormControlLabel,
Modal,
Radio,
RadioGroup,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { useQuestionsStore } from "@root/questions/store";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { useUiTools } from "@root/uiTools/store";
@ -6,12 +16,20 @@ import {
setModalQuestionTargetContentId,
setOpenedModalQuestions,
} from "@root/uiTools/actions";
import React, { useEffect, useState } from "react";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { RoundedCheckedIcon } from "@icons/questionsPage/RoundedCheckedIcon";
export const BranchingQuestionsModal = () => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const trashQuestions = useQuestionsStore().questions;
const questions = trashQuestions.filter(
(question) => question.type !== "result",
);
const openedModalQuestions = useUiTools(
(state) => state.openedModalQuestions,
);
@ -21,14 +39,30 @@ export const BranchingQuestionsModal = () => {
};
const typedQuestions: AnyTypedQuizQuestion[] = questions.filter(
(question) =>
question.type &&
!question.content.rule.parentId &&
question.type !== "result",
(question) => question.type && question.type !== "result",
) as AnyTypedQuizQuestion[];
if (typedQuestions.length === 0) return <></>;
const [selectedQuestion, setSelectedQuestion] = useState<string | null>(null);
useEffect(() => {
if (openedModalQuestions) {
setSelectedQuestion(null);
}
}, [openedModalQuestions]);
const handleRadioChange = (event: React.ChangeEvent<HTMLInputElement>) => {
setSelectedQuestion(event.target.value);
};
const handleConfirm = () => {
if (selectedQuestion) {
setModalQuestionTargetContentId(selectedQuestion);
}
handleClose();
};
return (
<Modal open={openedModalQuestions} onClose={handleClose}>
<Box
@ -39,48 +73,91 @@ export const BranchingQuestionsModal = () => {
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: "620px",
width: "100%",
width: "90%",
bgcolor: "background.paper",
borderRadius: "12px",
boxShadow: 24,
padding: "30px 0",
height: "80vh",
}}
>
<Box sx={{ margin: "0 auto", maxWidth: "350px" }}>
{typedQuestions.map((question) => (
<Button
<Box
sx={{ width: "100%", background: theme.palette.background.default }}
>
<Typography
sx={{
padding: "25px 0 25px 20px",
color: theme.palette.grey2.main,
}}
>
Выберите вопрос, который вы хотите добавить в ветвление
</Typography>
</Box>
<RadioGroup
value={selectedQuestion}
onChange={handleRadioChange}
sx={{
height: "346px",
width: "100%",
overflow: "auto",
display: "flex",
flexDirection: "column",
flexWrap: "nowrap",
}}
>
{typedQuestions.map((question, index) => (
<FormControlLabel
key={question.content.id}
onClick={() => {
setModalQuestionTargetContentId(question.content.id);
handleClose();
}}
value={question.content.id}
control={
<Radio
checkedIcon={<RadioCheck />}
icon={
question.content.rule.parentId ? (
<RoundedCheckedIcon />
) : (
<RadioIcon />
)
}
/>
}
label={question.title || "нет заголовка"}
disabled={!!question.content.rule.parentId}
sx={{
padding: "8px 12px",
margin: 0,
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)' strokeWidth='2' stroke-dasharray='8 8' stroke-dashoffset='0' strokeLinecap='square'/%3e%3c/svg%3e");
border-radius: 8px;`,
"&:last-child": { marginBottom: 0 },
backgroundColor:
index % 2 === 0
? theme.palette.common.white
: "rgba(242, 243, 247, 0.5)",
color: theme.palette.grey3.main,
}}
>
<Typography
sx={{
width: "100%",
color: "#000",
}}
>
{question.title || "нет заголовка"}
</Typography>
</Button>
/>
))}
</RadioGroup>
<Box
sx={{
margin: "20px",
width: "auto",
display: "flex",
justifyContent: isMobile ? "space-between" : "end",
gap: "10px",
}}
>
<Button
sx={{ width: isMobile ? "150px" : "130px", height: "44px" }}
onClick={handleClose}
variant={"outlined"}
>
Отмена
</Button>
<Button
sx={{ width: isMobile ? "150px" : "130px", height: "44px" }}
variant={"contained"}
onClick={handleConfirm}
>
Готово
</Button>
</Box>
</Box>
</Modal>

@ -1,10 +1,12 @@
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { Box, Skeleton, useMediaQuery, useTheme } from "@mui/material";
import { deleteTimeoutedQuestions } from "@utils/deleteTimeoutedQuestions";
import { useCallback } from "react";
import { BranchingMap } from "./BranchingMap";
import { lazy, Suspense, useCallback } from "react";
import { DraggableList } from "./DraggableList";
import { SwitchBranchingPanel } from "./SwitchBranchingPanel";
const BranchingMap = lazy(() =>
import("./BranchingMap").then((module) => ({ default: module.BranchingMap })),
);
interface Props {
openBranchingPage: boolean;
setOpenBranchingPage: (a: boolean) => void;
@ -18,6 +20,7 @@ export const QuestionSwitchWindowTool = ({
}: Props) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const openBranchingPageHC = useCallback(() => {
if (!openBranchingPage) {
@ -30,14 +33,26 @@ export const QuestionSwitchWindowTool = ({
<Box
sx={{
display: "flex",
gap: "20px",
flexWrap: "wrap",
marginBottom: isMobile ? "20px" : undefined,
marginBottom: isMobile ? "25px" : "30px",
}}
>
<Box sx={{ flexBasis: "796px" }}>
<Box sx={{ width: isTablet ? "100%" : "796px" }}>
{openBranchingPage ? (
<BranchingMap />
<Suspense
fallback={
<Skeleton
sx={{
maxWidth: "796px",
width: "100%",
height: isMobile ? "357px" : "521px",
transform: "none",
}}
/>
}
>
<BranchingMap />
</Suspense>
) : (
<DraggableList
openBranchingPage={openBranchingPage}

@ -1,4 +1,11 @@
import { Box, Button, IconButton, Typography, useTheme } from "@mui/material";
import {
Box,
Button,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import {
collapseAllQuestions,
createUntypedQuestion,
@ -11,7 +18,7 @@ import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateEditSomeQuestion } from "@root/uiTools/actions";
import { useUiTools } from "@root/uiTools/store";
import QuizPreview from "@ui_kit/QuizPreview/QuizPreview";
import { useLayoutEffect, useRef } from "react";
import { useLayoutEffect } from "react";
import { createPortal } from "react-dom";
import AddPlus from "../../assets/icons/questionsPage/addPlus";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
@ -30,6 +37,7 @@ export default function QuestionsPage({
widthMain,
}: Props) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const { openedModalSettingsId } = useUiTools();
const quiz = useCurrentQuiz();
useLayoutEffect(() => {
@ -47,7 +55,7 @@ export default function QuestionsPage({
width: "100%",
display: "flex",
justifyContent: "space-between",
margin: "60px 0 40px 0",
margin: isMobile ? "25px 0" : "40px 0",
}}
>
<Typography variant={"h5"} sx={{ wordBreak: "break-word" }}>

@ -1,14 +1,6 @@
import {
Box,
Typography,
Switch,
useTheme,
Button,
useMediaQuery,
} from "@mui/material";
import { QuestionsList } from "./QuestionsList";
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { PanelSwitchQuestionListGraph } from "@ui_kit/Toolbars/PanelSwitchQuestionListGraph";
interface Props {
openBranchingPage: boolean;
setOpenBranchingPage: () => void;
@ -31,9 +23,6 @@ export const SwitchBranchingPanel = ({
setOpenBranchingPage={setOpenBranchingPage}
/>
)}
{openBranchingPage && (
<QuestionsList setOpenBranchingPage={setOpenBranchingPage} />
)}
</Box>
) : (
<></>

@ -177,7 +177,7 @@ export default function Main({ sidebar, header, footer, Page }: Props) {
gap: isMobile ? "5px" : "15px",
background: "#FFF",
borderTop: "#f2f3f7 2px solid",
zIndex: 1,
zIndex: 3,
position: isMobile ? "fixed" : undefined,
bottom: isMobile ? 0 : undefined,
}}

@ -17,7 +17,7 @@ export const SmallSwitchQuestionListGraph = ({
width: "77px",
height: "51px",
position: "fixed",
zIndex: "1111",
zIndex: "999",
right: "0",
top: "200px",
background: "#333647",