Merge branch 'design-page' into tomainone

This commit is contained in:
Nastya 2024-02-27 00:52:18 +03:00
commit edac95e644
41 changed files with 5322 additions and 4429 deletions

@ -1 +1 @@
REACT_APP_DOMAIN="https://squiz.pena.digital"
REACT_APP_DOMAIN=""

@ -1,38 +1,32 @@
include:
- project: "devops/pena-continuous-integration"
file: "/templates/docker/build-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/clean-template.gitlab-ci.yml"
- project: "devops/pena-continuous-integration"
file: "/templates/docker/deploy-template.gitlab-ci.yml"
stages:
- clean
- build
- deploy
clear-old-images:
extends: .clean_template
variables:
STAGING_BRANCH: "main"
PRODUCTION_BRANCH: "main"
image:
name: docker/compose:1.28.0
entrypoint: [""]
before_script:
- docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
- docker images
script:
- docker system prune -af
build-app:
extends: .build_template
tags:
- frontbuild
variables:
DOCKER_BUILD_PATH: "./Dockerfile"
STAGING_BRANCH: "main"
STAGING_BRANCH: "staging"
PRODUCTION_BRANCH: "main"
deploy-to-staging:
extends: .deploy_template
variables:
DEPLOY_TO: "staging"
BRANCH: "main"
rules:
- if: "$CI_COMMIT_BRANCH == $STAGING_BRANCH"
tags:
- front
- staging
deploy-to-prod:
extends: .deploy_template
rules:
- if: "$CI_COMMIT_BRANCH == $PRODUCTION_BRANCH"
tags:
- front
- prod

@ -4,8 +4,11 @@ RUN apk update && rm -rf /var/cache/apk/*
WORKDIR /usr/app
COPY . .
RUN yarn config set '//penahub.gitlab.yandexcloud.net/api/v4/projects/43/packages/npm/:_authToken' "glpat-JL_7wSM1QpW7xGfd-oWX"
RUN npm config set //penahub.gitlab.yandexcloud.net/api/v4/projects/43/packages/npm/:_authToken=glpat-JL_7wSM1QpW7xGfd-oWX
RUN npm config set -- //penahub.gitlab.yandexcloud.net/api/v4/packages/npm/:_authToken=glpat-JL_7wSM1QpW7xGfd-oWX
RUN npm config set -- //penahub.gitlab.yandexcloud.net/api/v4/projects/43/packages/npm/:_authToken=glpat-JL_7wSM1QpW7xGfd-oWX
RUN npm config set @frontend:registry https://penahub.gitlab.yandexcloud.net/api/v4/packages/npm/
RUN yarn config set '//penahub.gitlab.yandexcloud.net/api/v4/packages/npm/:_authToken' "glpat-JL_7wSM1QpW7xGfd-oWX"
RUN yarn config set '//penahub.gitlab.yandexcloud.net/api/v4/projects/:_authToken' "glpat-JL_7wSM1QpW7xGfd-oWX"
RUN yarn install --ignore-scripts --non-interactive --frozen-lockfile && yarn cache clean
RUN yarn build

@ -0,0 +1,7 @@
services:
squiz:
container_name: squiz
restart: unless-stopped
image: $CI_REGISTRY_IMAGE/main:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
hostname: squiz
tty: true

@ -2,7 +2,7 @@ services:
squiz:
container_name: squiz
restart: unless-stopped
image: $CI_REGISTRY_IMAGE:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
image: $CI_REGISTRY_IMAGE/staging:$CI_COMMIT_REF_SLUG.$CI_PIPELINE_ID
networks:
- marketplace_penahub_frontend
hostname: squiz
@ -10,4 +10,3 @@ services:
networks:
marketplace_penahub_frontend:
external: true

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.7 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 6.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.8 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.4 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 8.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.2 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 7.3 MiB

@ -1,11 +1,11 @@
import { CssBaseline, ThemeProvider } from "@mui/material";
import { CssBaseline, ThemeProvider, Button } from "@mui/material";
import { LocalizationProvider } from "@mui/x-date-pickers";
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
import { ruRU } from "@mui/x-date-pickers/locales";
import App from "./App";
import dayjs from "dayjs";
import "dayjs/locale/ru";
import { SnackbarProvider } from "notistack";
import { SnackbarProvider, closeSnackbar } from "notistack";
import { DndProvider } from "react-dnd";
import { HTML5Backend } from "react-dnd-html5-backend";
import { createRoot } from "react-dom/client";
@ -15,6 +15,9 @@ import { SWRConfig } from "swr";
import { BrowserRouter } from "react-router-dom";
import moment from "moment";
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
import CloseIcon from "@icons/CloseBold";
import type { SnackbarKey } from "notistack";
dayjs.locale("ru");
moment.locale("ru");
@ -22,6 +25,19 @@ polyfillCountryFlagEmojis();
const localeText =
ruRU.components.MuiLocalizationProvider.defaultProps.localeText;
const snackbarAction = (snackbarId: SnackbarKey) => (
<Button
onClick={() => closeSnackbar(snackbarId)}
sx={{
minWidth: "auto",
padding: "0px",
"&:hover": { backgroundColor: "transparent" },
}}
>
<CloseIcon />
</Button>
);
const root = createRoot(document.getElementById("root")!);
root.render(
@ -40,6 +56,8 @@ root.render(
<ThemeProvider theme={lightTheme}>
<BrowserRouter>
<SnackbarProvider
SnackbarProps={{ onTouchStart: () => closeSnackbar() }}
action={snackbarAction}
preventDuplicate={true}
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
>

@ -46,24 +46,26 @@ export type QuizType = "quiz" | "form" | null;
export type QuizResultsType = true | null;
export type Theme =
| "StandardTheme"
| "StandardDarkTheme"
| "PinkTheme"
| "PinkDarkTheme"
| "BlackWhiteTheme"
| "OliveTheme"
| "YellowTheme"
| "GoldDarkTheme"
| "PurpleTheme"
| "BlueTheme"
| "BlueDarkTheme";
export interface QuizConfig {
type: QuizType;
noStartPage: boolean;
startpageType: QuizStartpageType;
results: QuizResultsType;
haveRoot: string | null;
theme:
| "StandardTheme"
| "StandardDarkTheme"
| "PinkTheme"
| "PinkDarkTheme"
| "BlackWhiteTheme"
| "OliveTheme"
| "YellowTheme"
| "GoldDarkTheme"
| "PurpleTheme"
| "BlueTheme"
| "BlueDarkTheme";
theme: Theme;
resultInfo: {
when: "email" | "";
share: true | false;

@ -3,7 +3,7 @@ import { Box } from "@mui/material";
interface Color {
color?: string;
}
export default function ColorRingIcon({ color = "#333647" }: Color) {
export default function ColorRingIcon({ color }: Color) {
return (
<Box
sx={{
@ -27,8 +27,8 @@ export default function ColorRingIcon({ color = "#333647" }: Color) {
width="16"
height="16"
rx="8"
fill={color}
stroke="#9A9AAF"
fill={color || "#FFFFFF"}
stroke={color ? "transparent" : "#9A9AAF"}
/>
</svg>
</Box>

@ -1,45 +1,109 @@
import { useState } from "react";
import {
Box,
ButtonBase,
IconButton,
Divider,
Paper,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import ColorRingIcon from "./ColorRingIcon";
import { updateQuiz } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { toggleQuizPreview } from "@root/quizPreview";
import VisibilityIcon from "@mui/icons-material/Visibility";
import { DesignGroup } from "./DesignGroup";
const ButtonsThemeLight = [
["Стандартный", "StandardTheme", "#7E2AEA", "#FFFFFF"],
["Черно-белый", "BlackWhiteTheme", "#4E4D51", "#FFFFFF"],
["Оливковый", "OliveTheme", "#758E4F", "#F9FBF1"],
["Фиолетовый", "PurpleTheme", "#7E2AEA", "#FBF8FF"],
["Желтый", "YellowTheme", "#F2B133", "#FFFCF6"],
["Голубой", "BlueTheme", "#4964ED", "#F5F7FF"],
["Розовый", "PinkTheme", "#D34085", "#FFF9FC"],
];
const ButtonsThemeDark = [
["Стандартный", "StandardDarkTheme", "#7E2AEA", "#FFFFFF"],
["Золотой", "GoldDarkTheme", "#E6AA37", "#FFFFFF"],
["Розовый", "PinkDarkTheme", "#D34085", "#FFFFFF"],
["Бирюзовый", "BlueDarkTheme", "#07A0C3", "#FFFFFF"],
import Desgin1 from "@icons/designs/design1.jpg";
import Desgin2 from "@icons/designs/design2.jpg";
import Desgin3 from "@icons/designs/design3.jpg";
import Desgin4 from "@icons/designs/design4.jpg";
import Desgin5 from "@icons/designs/design5.jpg";
import Desgin6 from "@icons/designs/design6.jpg";
import Desgin7 from "@icons/designs/design7.jpg";
import Desgin8 from "@icons/designs/design8.jpg";
import Desgin9 from "@icons/designs/design9.jpg";
import Desgin10 from "@icons/designs/design10.jpg";
import type { Theme } from "@model/quizSettings";
import type { DesignItem } from "./DesignGroup";
const LIGHT_THEME_BUTTONS: DesignItem[] = [
{
label: "Стандартный",
name: "StandardTheme",
colors: ["#7E2AEA", "#333647", ""],
},
{
label: "Черно-белый",
name: "BlackWhiteTheme",
colors: ["#4E4D51", "#333647", ""],
},
{
label: "Оливковый",
name: "OliveTheme",
colors: ["#758E4F", "#333647", ""],
},
{
label: "Фиолетовый",
name: "PurpleTheme",
colors: ["#7E2AEA", "#333647", ""],
},
{ label: "Желтый", name: "YellowTheme", colors: ["#F2B133", "#333647", ""] },
{ label: "Голубой", name: "BlueTheme", colors: ["#4964ED", "#333647", ""] },
{ label: "Розовый", name: "PinkTheme", colors: ["#D34085", "#333647", ""] },
];
interface Props {
const DARK_THEME_BUTTONS: DesignItem[] = [
{
label: "Стандартный",
name: "StandardDarkTheme",
colors: ["#7E2AEA", "", "#333647"],
},
{
label: "Золотой",
name: "GoldDarkTheme",
colors: ["#E6AA37", "", "#333647"],
},
{
label: "Розовый",
name: "PinkDarkTheme",
colors: ["#D34085", "", "#333647"],
},
{
label: "Бирюзовый",
name: "BlueDarkTheme",
colors: ["#07A0C3", "", "#333647"],
},
];
const DESIGNG_LIST_FIRST: DesignItem[] = [
{ label: "Дизайн 1", name: "design1", picture: Desgin1 },
{ label: "Дизайн 2", name: "design2", picture: Desgin2 },
{ label: "Дизайн 3", name: "design3", picture: Desgin3 },
{ label: "Дизайн 4", name: "design4", picture: Desgin4 },
{ label: "Дизайн 5", name: "design5", picture: Desgin5 },
];
const DESIGNG_LIST_SECOND: DesignItem[] = [
{ label: "Дизайн 6", name: "design6", picture: Desgin6 },
{ label: "Дизайн 7", name: "design7", picture: Desgin7 },
{ label: "Дизайн 8", name: "design8", picture: Desgin8 },
{ label: "Дизайн 9", name: "design9", picture: Desgin9 },
{ label: "Дизайн 10", name: "design10", picture: Desgin10 },
];
interface DesignFillingProps {
mobileSidebar: boolean;
heightSidebar: number;
}
export const DesignFilling = ({ mobileSidebar, heightSidebar }: Props) => {
export const DesignFilling = ({
mobileSidebar,
heightSidebar,
}: DesignFillingProps) => {
const [design, setDesign] = useState<string>("");
const quiz = useCurrentQuiz();
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(830));
const heightBar = heightSidebar + 51 + 88 + 36;
console.log(mobileSidebar, "111");
return (
<Box
sx={{
@ -55,104 +119,53 @@ export const DesignFilling = ({ mobileSidebar, heightSidebar }: Props) => {
<Typography variant="h5" sx={{ marginBottom: "40px", color: "#333647" }}>
Дизайн
</Typography>
<Typography sx={{ marginBottom: "30px", color: "#333647" }}>
Выберите цветовую схему для вашего опроса
</Typography>
<Paper
sx={{
padding: "20px",
maxWidth: "796px",
width: "100%",
display: "flex",
gap: "20px",
borderRadius: "12px",
flexWrap: "wrap",
height: "calc(100vh - 280px)",
overflow: "auto",
}}
>
<Box
sx={{
width: isMobile ? "100%" : "48%",
display: "flex",
flexDirection: "column",
gap: "12px",
}}
>
<Typography color={"#9A9AAF"}>Со светлым фоном</Typography>
{ButtonsThemeLight.map((e, i) => (
<ButtonBase
sx={{
maxWidth: "368px",
width: "100%",
padding: "22px 21px",
background:
quiz.config.theme == e[1]
? "linear-gradient(0deg, rgba(126, 42, 234, 0.10) 0%, rgba(126, 42, 234, 0.10) 100%)"
: "#F2F3F7",
borderRadius: "12px",
justifyContent: "space-between",
border:
quiz.config.theme == e[1] ? "1px solid #7E2AEA" : "none",
}}
key={i}
value={e[1]}
onClick={() =>
updateQuiz(quiz.id, (quiz) => {
quiz.config.theme = e[1];
})
}
>
<Typography color={"#4D4D4D"}>{e[0]}</Typography>
<Box sx={{ display: "flex", gap: "7px" }}>
<ColorRingIcon color={e[2]} />
<ColorRingIcon />
<ColorRingIcon color={e[3]} />
</Box>
</ButtonBase>
))}
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap" }}>
<DesignGroup
title="Со светлым фоном"
value={quiz?.config.theme || ""}
list={LIGHT_THEME_BUTTONS}
onChange={(name) =>
updateQuiz(quiz?.id, (quiz) => {
quiz.config.theme = name as Theme;
})
}
/>
<DesignGroup
title="С тёмным фоном"
value={quiz?.config.theme || ""}
list={DARK_THEME_BUTTONS}
onChange={(name) =>
updateQuiz(quiz?.id, (quiz) => {
quiz.config.theme = name as Theme;
})
}
/>
</Box>
<Box
sx={{
width: isMobile ? "100%" : "48%",
display: "flex",
flexDirection: "column",
gap: "12px",
}}
>
<Typography color={"#9A9AAF"}>С тёмным фоном</Typography>
{ButtonsThemeDark.map((e, i) => (
<ButtonBase
sx={{
maxWidth: "368px",
width: "100%",
padding: "22px 21px",
background:
quiz.config.theme == e[1]
? "linear-gradient(0deg, rgba(126, 42, 234, 0.10) 0%, rgba(126, 42, 234, 0.10) 100%)"
: "#F2F3F7",
borderRadius: "12px",
justifyContent: "space-between",
border:
quiz.config.theme == e[1] ? "1px solid #7E2AEA" : "none",
}}
key={i}
value={e[1]}
onClick={() =>
updateQuiz(quiz.id, (quiz) => {
quiz.config.theme = e[1];
})
}
>
<Typography color={"#4D4D4D"}>{e[0]}</Typography>
<Box sx={{ display: "flex", gap: "7px" }}>
<ColorRingIcon color={e[2]} />
<ColorRingIcon color={e[3]} />
<ColorRingIcon />
</Box>
</ButtonBase>
))}
<Box>
<Divider sx={{ margin: "20px 0", background: "#7E2AEA33" }} />
</Box>
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap" }}>
<DesignGroup
title="С картинкой"
value={design}
list={DESIGNG_LIST_FIRST}
onChange={setDesign}
/>
<DesignGroup
value={design}
list={DESIGNG_LIST_SECOND}
onChange={setDesign}
/>
</Box>
</Paper>
</Box>

@ -0,0 +1,89 @@
import {
Typography,
Box,
ButtonBase,
useTheme,
useMediaQuery,
} from "@mui/material";
import ColorRingIcon from "./ColorRingIcon";
export type DesignItem = {
name: string;
label: string;
colors?: string[];
picture?: string;
};
type DesignGroupProps = {
title?: string;
list: DesignItem[];
value: string;
onChange: (name: string) => void;
};
export const DesignGroup = ({
title,
list,
value,
onChange,
}: DesignGroupProps) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(830));
return (
<Box
sx={{
width: isMobile ? "100%" : "48%",
display: "flex",
flexDirection: "column",
gap: "12px",
paddingTop: title ? 0 : "33px",
}}
>
{title && <Typography color="#9A9AAF">{title}</Typography>}
{list.map(({ label, name, colors, picture }) => (
<ButtonBase
key={name}
value={name}
onClick={() => onChange(name)}
sx={{
maxWidth: "368px",
width: "100%",
padding: "5px",
background:
value === name
? "linear-gradient(0deg, rgba(126, 42, 234, 0.10) 0%, rgba(126, 42, 234, 0.10) 100%)"
: "#F2F3F7",
borderRadius: "12px",
justifyContent: "space-between",
border:
value === name ? "1px solid #7E2AEA" : "1px solid transparent",
}}
>
<Typography sx={{ marginLeft: "15px", color: "#4D4D4D" }}>
{label}
</Typography>
{picture ? (
<img
src={picture}
alt={label}
style={{
width: "100%",
height: "56px",
maxWidth: "115px",
borderRadius: "12px",
}}
/>
) : (
<Box sx={{ display: "flex", gap: "5px", padding: "18px" }}>
{colors?.map((color, index) => (
<ColorRingIcon key={index} color={color} />
))}
</Box>
)}
</ButtonBase>
))}
</Box>
);
};

@ -77,13 +77,13 @@ export const DesignPage = ({ heightSidebar, mobileSidebar }: Props) => {
mobileSidebar={mobileSidebar}
heightSidebar={heightSidebar}
/>
{createPortal(<QuizPreview />, document.body)}
</Box>
<ConfirmLeaveModal
open={showConfirmLeaveModal}
follow={followNewPage}
cancel={() => setShowConfirmLeaveModal(false)}
/>
{createPortal(<QuizPreview />, document.body)}
</>
);
};

@ -8,22 +8,25 @@ import {
IconButton,
InputAdornment,
Popover,
TextField,
TextField as MuiTextField,
useMediaQuery,
useTheme,
TextFieldProps,
} from "@mui/material";
import {
addQuestionVariant,
deleteQuestionVariant,
setQuestionVariantField,
} from "@root/questions/actions";
import type { ChangeEvent, KeyboardEvent, ReactNode } from "react";
import type { ChangeEvent, FC, KeyboardEvent, ReactNode } from "react";
import { useState } from "react";
import { Draggable } from "react-beautiful-dnd";
import type { QuestionVariant } from "../../../model/questionTypes/shared";
import { useDebouncedCallback } from "use-debounce";
import { enqueueSnackbar } from "notistack";
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
type AnswerItemProps = {
index: number;
questionId: string;
@ -60,7 +63,6 @@ export const AnswerItem = ({
const handleClose = () => {
setIsOpen(false);
};
console.log(variant);
return (
<Draggable draggableId={String(index)} index={index}>

@ -1,117 +1,54 @@
import { useEffect, useLayoutEffect, useRef, useState } from "react";
import Cytoscape from "cytoscape";
import CytoscapeComponent from "react-cytoscapejs";
import popper from "cytoscape-popper";
import { Button, Box } from "@mui/material";
import { withErrorBoundary } from "react-error-boundary";
import { enqueueSnackbar } from "notistack";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateRootContentId } from "@root/quizes/actions";
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 { useUiTools } from "@root/uiTools/store";
import {
deleteQuestion,
updateQuestion,
getQuestionByContentId,
clearRuleForAll,
createResult,
} from "@root/questions/actions";
import { updateRootContentId } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import {
cleardragQuestionContentId,
setModalQuestionParentContentId,
setModalQuestionTargetContentId,
updateModalInfoWhyCantCreate,
updateOpenedModalSettingsId,
} from "@root/uiTools/actions";
import { cleardragQuestionContentId } from "@root/uiTools/actions";
import { updateDeleteId } from "@root/uiTools/actions";
import { DeleteNodeModal } from "../DeleteNodeModal";
import { useUiTools } from "@root/uiTools/store";
import { ProblemIcon } from "@ui_kit/ProblemIcon";
import { useRemoveNode } from "./hooks/useRemoveNode";
import { usePopper } from "./hooks/usePopper";
import { storeToNodes } from "./helper";
import { stylesheet } from "./style/stylesheet";
import "./style/styles.css";
import type { Core } from "cytoscape";
import { nameCutter } from "./nameCutter";
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";
import CsNodeButtons from "./CsNodeButtons";
import { addNode, layoutOptions, storeToNodes } from "./helper";
import { useRemoveNode } from "./hooks/useRemoveNode";
import "./style/styles.css";
import { stylesheet } from "./style/stylesheet";
Cytoscape.use(popper);
interface CsComponentProps {
modalQuestionParentContentId: string;
modalQuestionTargetContentId: string;
setOpenedModalQuestions: (open: boolean) => void;
setModalQuestionParentContentId: (id: string) => void;
setModalQuestionTargetContentId: (id: string) => void;
}
function CsComponent({
modalQuestionParentContentId,
modalQuestionTargetContentId,
setOpenedModalQuestions,
setModalQuestionParentContentId,
setModalQuestionTargetContentId,
}: CsComponentProps) {
const quiz = useCurrentQuiz();
const {
dragQuestionContentId,
desireToOpenABranchingModal,
canCreatePublic,
someWorkBackend,
} = useUiTools();
const trashQuestions = useQuestionsStore().questions;
const questions = trashQuestions.filter(
(question) =>
question.type !== "result" && question.type !== null && !question.deleted,
function CsComponent() {
const desireToOpenABranchingModal = useUiTools(
(state) => state.desireToOpenABranchingModal,
);
const [startCreate, setStartCreate] = useState("");
const [startRemove, setStartRemove] = useState("");
const canCreatePublic = useUiTools((state) => state.canCreatePublic);
const modalQuestionParentContentId = useUiTools(
(state) => state.modalQuestionParentContentId,
);
const modalQuestionTargetContentId = useUiTools(
(state) => state.modalQuestionTargetContentId,
);
const trashQuestions = useQuestionsStore((state) => state.questions);
const cyRef = useRef<Core | null>(null);
const layoutsContainer = useRef<HTMLDivElement | null>(null);
const plusesContainer = useRef<HTMLDivElement | null>(null);
const crossesContainer = useRef<HTMLDivElement | null>(null);
const gearsContainer = useRef<HTMLDivElement | null>(null);
const { removeNode } = useRemoveNode({ cyRef });
const { layoutOptions } = usePopper({
layoutsContainer,
plusesContainer,
crossesContainer,
gearsContainer,
setModalQuestionParentContentId,
setOpenedModalQuestions,
setStartCreate,
setStartRemove,
});
const { removeNode } = useRemoveNode({
cyRef,
layoutOptions,
layoutsContainer,
plusesContainer,
crossesContainer,
gearsContainer,
});
const csElements = useMemo(() => {
const questions = trashQuestions.filter(
(question): question is AnyTypedQuizQuestion =>
question.type !== null && question.type !== "result",
);
function fitGraphToRootNode() {
const cy = cyRef.current;
if (!cy) return;
const rootNode = cy.nodes().filter((n) => n.data("root"))[0];
if (!rootNode) throw new Error("Root node not found");
const height = cy.height();
const position = rootNode.position();
const shift = rootNode.width() / 2;
cy.pan({
x: position.x + shift,
y: position.y + height / 2,
});
}
return storeToNodes(questions);
}, [trashQuestions]);
useLayoutEffect(() => {
const cy = cyRef?.current;
@ -125,19 +62,14 @@ function CsComponent({
cy?.elements().data("eroticeyeblink", false);
}
}, [desireToOpenABranchingModal]);
//Техническая штучка. Гарантирует не отрисовку модалки по первому входу на страничку. И очистка данных по расскоменчиванию
//Быстро просто дешево и сердито :)
useLayoutEffect(() => {
updateOpenedModalSettingsId();
// updateRootContentId(quiz.id, "")
// clearRuleForAll()
}, []);
//Отлов mouseup для отрисовки ноды
useEffect(() => {
if (
modalQuestionTargetContentId.length !== 0 &&
modalQuestionParentContentId.length !== 0
) {
if (!cyRef.current) return;
addNode({
parentNodeContentId: modalQuestionParentContentId,
targetNodeContentId: modalQuestionTargetContentId,
@ -147,195 +79,27 @@ function CsComponent({
setModalQuestionTargetContentId("");
}, [modalQuestionTargetContentId]);
const addNode = ({
parentNodeContentId,
targetNodeContentId,
}: {
parentNodeContentId: string;
targetNodeContentId?: string;
}) => {
if (quiz) {
//запрещаем работу родителя-ребенка если это один и тот же вопрос
if (parentNodeContentId === targetNodeContentId) return;
const cy = cyRef?.current;
const parentNodeChildren = cy?.$(
'edge[source = "' + parentNodeContentId + '"]',
)?.length;
const parentQuestion = getQuestionByContentId(parentNodeContentId);
//Нельзя добавлять больше 1 ребёнка вопросам типа страница, ползунок, своё поле для ввода и дата
if (
(parentQuestion?.type === "date" ||
parentQuestion?.type === "text" ||
parentQuestion?.type === "number" ||
parentQuestion?.type === "page") &&
parentQuestion.content.rule.children.length === 1
) {
enqueueSnackbar("у вопроса этого типа может быть только 1 потомок");
return;
}
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
const targetQuestion = {
...getQuestionByContentId(targetNodeContentId || dragQuestionContentId),
} as AnyTypedQuizQuestion;
if (
Object.keys(targetQuestion).length !== 0 &&
parentNodeContentId &&
parentNodeChildren !== undefined
) {
clearDataAfterAddNode({
parentNodeContentId,
targetQuestion,
parentNodeChildren,
});
cy?.data("changed", true);
createResult(quiz.backendId, targetQuestion.content.id);
const es = cy?.add([
{
data: {
id: targetQuestion.content.id,
label:
targetQuestion.title === "" || targetQuestion.title === " "
? "noname №" + targetQuestion.page
: nameCutter(targetQuestion.title),
parentType: parentNodeContentId,
},
},
{
data: {
source: parentNodeContentId,
target: targetQuestion.content.id,
},
},
]);
cy?.layout(layoutOptions).run();
cy?.center(es);
} else {
enqueueSnackbar("Перетащите на плюсик вопрос");
}
} else {
enqueueSnackbar("Quiz не найден");
}
};
const clearDataAfterAddNode = ({
parentNodeContentId,
targetQuestion,
parentNodeChildren,
}: {
parentNodeContentId: string;
targetQuestion: AnyTypedQuizQuestion;
parentNodeChildren: number;
}) => {
const parentQuestion = {
...getQuestionByContentId(parentNodeContentId),
} as AnyTypedQuizQuestion;
//смотрим не добавлен ли родителю result. Если да - делаем его неактивным. Веточкам result не нужен
trashQuestions.forEach((targetQuestion) => {
if (
targetQuestion.type === "result" &&
targetQuestion.content.rule.parentId === parentQuestion.content.id
) {
updateQuestion(targetQuestion.id, (q) => (q.content.usage = false));
}
});
//предупреждаем добавленный вопрос о том, кто его родитель
updateQuestion(targetQuestion.content.id, (question) => {
question.content.rule.parentId = parentNodeContentId;
question.content.rule.main = [];
//Это листик. Сбросим ему на всякий случай не листиковые поля
question.content.rule.children = [];
question.content.rule.default = "";
});
const noChild = parentQuestion.content.rule.children.length === 0;
//предупреждаем родителя о новом потомке (если он ещё не знает о нём)
if (
!parentQuestion.content.rule.children.includes(targetQuestion.content.id)
)
updateQuestion(parentNodeContentId, (question) => {
question.content.rule.children = [
...question.content.rule.children,
targetQuestion.content.id,
];
//единственному ребёнку даём дефолт по-умолчанию
question.content.rule.default = noChild
? targetQuestion.content.id
: question.content.rule.default;
});
if (!noChild) {
//детей больше 1
//- предупреждаем стор вопросов об открытии модалки ветвления
updateOpenedModalSettingsId(targetQuestion.content.id);
}
};
useEffect(() => {
if (startCreate) {
addNode({ parentNodeContentId: startCreate });
cleardragQuestionContentId();
setStartCreate("");
}
}, [startCreate]);
useEffect(() => {
if (startRemove) {
updateDeleteId(startRemove);
setStartRemove("");
}
}, [startRemove]);
//Отработка первичного рендера странички графика
const firstRender = useRef(true);
useEffect(() => {
if (!someWorkBackend && firstRender.current) {
document
.querySelector("#root")
?.addEventListener("mouseup", cleardragQuestionContentId);
const cy = cyRef.current;
const eles = cy?.add(
storeToNodes(
questions.filter(
(question) => question.type && question.type !== "result",
) as AnyTypedQuizQuestion[],
),
);
cy?.data("changed", true);
// cy.data('changed', true)
const elecs = eles?.layout(layoutOptions).run();
cy?.on("add", () => cy.data("changed", true));
fitGraphToRootNode();
//cy?.layout().run()
firstRender.current = false;
}
useEffect(function onMount() {
updateOpenedModalSettingsId();
document.addEventListener("pointerup", cleardragQuestionContentId);
return () => {
document
.querySelector("#root")
?.removeEventListener("mouseup", cleardragQuestionContentId);
layoutsContainer.current?.remove();
plusesContainer.current?.remove();
crossesContainer.current?.remove();
gearsContainer.current?.remove();
document.removeEventListener("pointerup", cleardragQuestionContentId);
};
}, [someWorkBackend]);
}, []);
useEffect(
function rerunLayout() {
cyRef.current?.layout(layoutOptions).run();
cyRef.current?.fit(undefined, 70);
},
[csElements],
);
return (
<>
<Box
sx={{
mb: "20px",
display: "flex",
justifyContent: "space-between",
}}
>
<CsNodeButtons csElements={csElements} cyRef={cyRef} />
<Box mb="20px">
<Button
sx={{
height: "27px",
@ -344,7 +108,9 @@ function CsComponent({
fontSize: "16px",
}}
variant="text"
onClick={fitGraphToRootNode}
onClick={() => {
cyRef.current?.fit(undefined, 70);
}}
>
Выровнять
</Button>
@ -353,20 +119,22 @@ function CsComponent({
onClick={() => updateModalInfoWhyCantCreate(true)}
/>
</Box>
<CytoscapeComponent
wheelSensitivity={0.1}
elements={[]}
// elements={createGraphElements(tree, quiz)}
style={{ height: "480px", background: "#F2F3F7" }}
elements={csElements}
style={{
height: "480px",
background: "#F2F3F7",
overflow: "hidden",
}}
stylesheet={stylesheet}
layout={layoutOptions}
cy={(cy) => {
cyRef.current = cy;
}}
autoungrabify={true}
zoom={0.6}
zoomingEnabled={false}
autounselectify={true}
boxSelectionEnabled={false}
/>
<DeleteNodeModal removeNode={removeNode} />
</>
@ -386,7 +154,7 @@ export default withErrorBoundary(CsComponent, {
fallback: <Clear />,
onError: (error, info) => {
enqueueSnackbar("Дерево порвалось");
console.log(info);
console.log(error);
devlog(info);
devlog(error);
},
});

File diff suppressed because one or more lines are too long

@ -1,35 +1,34 @@
import { Box } from "@mui/material";
import { useEffect, useRef, useLayoutEffect } from "react";
import {
deleteQuestion,
clearRuleForAll,
updateQuestion,
createResult,
updateQuestion,
} from "@root/questions/actions";
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
import { updateRootContentId } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useQuestionsStore } from "@root/questions/store";
import { enqueueSnackbar } from "notistack";
import { useUiTools } from "@root/uiTools/store";
interface Props {
setOpenedModalQuestions: (open: boolean) => void;
modalQuestionTargetContentId: string;
}
export const FirstNodeField = ({
import { useQuizStore } from "@root/quizes/store";
import {
setOpenedModalQuestions,
modalQuestionTargetContentId,
}: Props) => {
updateOpenedModalSettingsId,
} from "@root/uiTools/actions";
import { useUiTools } from "@root/uiTools/store";
import { enqueueSnackbar } from "notistack";
import { useEffect, useLayoutEffect, useRef } from "react";
export const FirstNodeField = () => {
const quiz = useCurrentQuiz();
const modalQuestionTargetContentId = useUiTools(
(state) => state.modalQuestionTargetContentId,
);
useLayoutEffect(() => {
if (!quiz) return;
updateOpenedModalSettingsId();
updateRootContentId(quiz.id, "");
clearRuleForAll();
}, []);
const { questions } = useQuestionsStore();
const { dragQuestionContentId } = useUiTools();
const Container = useRef<HTMLDivElement | null>(null);
@ -43,18 +42,18 @@ export const FirstNodeField = ({
dragQuestionContentId,
(question) => (question.content.rule.parentId = "root"),
);
createResult(quiz?.backendId, dragQuestionContentId);
createResult(useQuizStore.getState().editQuizId, dragQuestionContentId);
}
} else {
enqueueSnackbar("Нет информации о взятом опросе");
enqueueSnackbar("Нет информации о взятом опроснике");
}
};
useEffect(() => {
Container.current?.addEventListener("mouseup", newRootNode);
Container.current?.addEventListener("pointerup", newRootNode);
Container.current?.addEventListener("click", modalOpen);
return () => {
Container.current?.removeEventListener("mouseup", newRootNode);
Container.current?.removeEventListener("pointerup", newRootNode);
Container.current?.removeEventListener("click", modalOpen);
};
}, [dragQuestionContentId]);
@ -67,10 +66,13 @@ export const FirstNodeField = ({
modalQuestionTargetContentId,
(question) => (question.content.rule.parentId = "root"),
);
createResult(quiz?.backendId, modalQuestionTargetContentId);
createResult(
useQuizStore.getState().editQuizId,
modalQuestionTargetContentId,
);
}
} else {
enqueueSnackbar("Нет информации о взятом опросе");
enqueueSnackbar("Нет информации о взятом опроснике");
}
}, [modalQuestionTargetContentId]);

@ -1,34 +1,73 @@
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { nameCutter } from "./nameCutter";
import { QuizQuestionResult } from "@model/questionTypes/result";
import {
AnyTypedQuizQuestion,
QuestionBranchingRule,
QuestionBranchingRuleMain,
UntypedQuizQuestion,
} from "@model/questionTypes/shared";
import {
createResult,
getQuestionByContentId,
updateQuestion,
} from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store";
import { useQuizStore } from "@root/quizes/store";
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
import { useUiTools } from "@root/uiTools/store";
import { NodeSingular, PresetLayoutOptions } from "cytoscape";
import { enqueueSnackbar } from "notistack";
interface Nodes {
export interface Node {
data: {
isRoot: boolean;
id: string;
label: string;
parent?: string;
};
classes: string;
}
interface Edges {
export interface Edge {
data: {
source: string;
target: string;
};
}
export function isElementANode(element: Node | Edge): element is Node {
return !("source" in element.data && "target" in element.data);
}
export function isNodeInViewport(node: NodeSingular, padding: number = 0) {
const extent = node.cy().extent();
const bb = node.boundingBox();
return (
bb.x2 > extent.x1 - padding &&
bb.x1 < extent.x2 + padding &&
bb.y2 > extent.y1 - padding &&
bb.y1 < extent.y2 + padding
);
}
export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => {
const nodes: Nodes[] = [];
const edges: Edges[] = [];
const nodes: Node[] = [];
const edges: Edge[] = [];
questions.forEach((question) => {
if (question.content.rule.parentId) {
let label =
question.title === "" || question.title === " "
? "noname"
: question.title;
if (label.length > 25) label = label.slice(0, 25) + "…";
nodes.push({
data: {
isRoot: question.content.rule.parentId === "root",
id: question.content.id,
label:
question.title === "" || question.title === " "
? "noname №" + question.page
: nameCutter(question.title),
parentType: question.content.rule.parentId,
label,
},
classes: "multiline-auto",
});
// nodes.push({
// data: {
@ -48,3 +87,260 @@ export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => {
});
return [...nodes, ...edges];
};
export const layoutOptions: PresetLayoutOptions = {
name: "preset",
positions: calcNodePosition,
zoom: undefined,
pan: 1,
fit: false,
padding: 30,
animate: false,
animationDuration: 500,
animationEasing: undefined,
animateFilter: () => false,
ready: (event) => {
if (event.cy.data("firstNode") === "nonroot") {
event.cy.data("firstNode", "root");
event.cy.nodes().sort((a, b) => (a.data("root") ? 1 : -1));
} else {
event.cy.removeData("firstNode");
}
},
transform: (_, p) => p,
};
export function clearDataAfterAddNode({
parentNodeContentId,
targetQuestion,
}: {
parentNodeContentId: string;
targetQuestion: AnyTypedQuizQuestion;
}) {
const parentQuestion = {
...getQuestionByContentId(parentNodeContentId),
} as AnyTypedQuizQuestion;
//смотрим не добавлен ли родителю result. Если да - делаем его неактивным. Веточкам result не нужен
useQuestionsStore
.getState()
.questions.filter(
(question): question is QuizQuestionResult => question.type === "result",
)
.forEach((targetQuestion) => {
if (targetQuestion.content.rule.parentId === parentQuestion.content.id) {
updateQuestion<QuizQuestionResult>(
targetQuestion.id,
(q) => (q.content.usage = false),
);
}
});
//предупреждаем добавленный вопрос о том, кто его родитель
updateQuestion(targetQuestion.content.id, (question) => {
question.content.rule.parentId = parentNodeContentId;
question.content.rule.main = [];
//Это листик. Сбросим ему на всякий случай не листиковые поля
question.content.rule.children = [];
question.content.rule.default = "";
});
const noChild = parentQuestion.content.rule.children.length === 0;
//предупреждаем родителя о новом потомке (если он ещё не знает о нём)
if (!parentQuestion.content.rule.children.includes(targetQuestion.content.id))
updateQuestion(parentNodeContentId, (question) => {
question.content.rule.children = [
...question.content.rule.children,
targetQuestion.content.id,
];
//единственному ребёнку даём дефолт по-умолчанию
question.content.rule.default = noChild
? targetQuestion.content.id
: question.content.rule.default;
});
if (!noChild) {
//детей больше 1
//- предупреждаем стор вопросов об открытии модалки ветвления
updateOpenedModalSettingsId(targetQuestion.content.id);
}
}
export function clearDataAfterRemoveNode({
trashQuestions,
targetQuestionContentId,
parentQuestionContentId,
}: {
trashQuestions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[];
targetQuestionContentId: string;
parentQuestionContentId: string;
}) {
updateQuestion(targetQuestionContentId, (question) => {
question.content.rule.parentId = "";
question.content.rule.children = [];
question.content.rule.main = [];
question.content.rule.default = "";
});
//Ищём родителя
const parentQuestion = getQuestionByContentId(parentQuestionContentId);
//Делаем результат родителя активным
const parentResult = trashQuestions.find(
(q): q is QuizQuestionResult =>
q.type === "result" &&
q.content.rule.parentId === parentQuestionContentId,
);
if (parentResult) {
updateQuestion<QuizQuestionResult>(parentResult.content.id, (q) => {
q.content.usage = true;
});
} else {
createResult(useQuizStore.getState().editQuizId, parentQuestionContentId);
}
//чистим rule родителя
if (!parentQuestion?.type) {
return;
}
const newChildren = [...parentQuestion.content.rule.children];
newChildren.splice(
parentQuestion.content.rule.children.indexOf(targetQuestionContentId),
1,
);
const newRule: QuestionBranchingRule = {
children: newChildren,
default:
parentQuestion.content.rule.default === targetQuestionContentId
? ""
: parentQuestion.content.rule.default,
//удаляем условия перехода от родителя к этому вопросу,
main: parentQuestion.content.rule.main.filter(
(data: QuestionBranchingRuleMain) =>
data.next !== targetQuestionContentId,
),
parentId: parentQuestion.content.rule.parentId,
};
updateQuestion(parentQuestionContentId, (PQ) => {
PQ.content.rule = newRule;
});
}
export function calcNodePosition(node: any) {
const id = node.id();
const incomming = node.cy().edges(`[target="${id}"]`);
const layer = 0;
node.removeData("lastChild");
if (incomming.length === 0) {
if (node.cy().data("firstNode") === undefined)
node.cy().data("firstNode", "root");
node.data("root", true);
const children = node.cy().edges(`[source="${id}"]`).targets();
node.data("layer", layer);
node.data("children", children.length);
const queue: any[] = [];
children.forEach((n: any) => {
queue.push({ task: n, layer: layer + 1 });
});
while (queue.length) {
const task = queue.pop();
task.task.data("layer", task.layer);
task.task.removeData("subtreeWidth");
const children = node
.cy()
.edges(`[source="${task.task.id()}"]`)
.targets();
task.task.data("children", children.length);
if (children.length !== 0) {
children.forEach((n: any) =>
queue.push({ task: n, layer: task.layer + 1 }),
);
}
}
queue.push({ parent: node, children: children });
while (queue.length) {
const task = queue.pop();
if (task.children.length === 0) {
task.parent.data("subtreeWidth", task.parent.height() + 50);
continue;
}
const unprocessed = task?.children.filter((node: any) => {
return node.data("subtreeWidth") === undefined;
});
if (unprocessed.length !== 0) {
queue.push(task);
unprocessed.forEach((t: any) => {
queue.push({
parent: t,
children: t.cy().edges(`[source="${t.id()}"]`).targets(),
});
});
continue;
}
task?.parent.data(
"subtreeWidth",
task.children.reduce((p: any, n: any) => p + n.data("subtreeWidth"), 0),
);
}
const pos = { x: 0, y: 0 };
node.data("oldPos", pos);
queue.push({ task: children, parent: node });
while (queue.length) {
const task = queue.pop();
const oldPos = task.parent.data("oldPos");
let yoffset = oldPos.y - task.parent.data("subtreeWidth") / 2;
task.task.forEach((n: any) => {
const width = n.data("subtreeWidth");
n.data("oldPos", {
x: 250 * n.data("layer"),
y: yoffset + width / 2,
});
yoffset += width;
queue.push({
task: n.cy().edges(`[source="${n.id()}"]`).targets(),
parent: n,
});
});
}
return pos;
} else {
const opos = node.data("oldPos");
if (opos) {
return opos;
}
}
}
export const addNode = ({
parentNodeContentId,
targetNodeContentId,
}: {
parentNodeContentId: string;
targetNodeContentId?: string;
}) => {
//запрещаем работу родителя-ребенка если это один и тот же вопрос
if (parentNodeContentId === targetNodeContentId) return;
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
const targetQuestion = {
...getQuestionByContentId(
targetNodeContentId || useUiTools.getState().dragQuestionContentId,
),
} as AnyTypedQuizQuestion;
if (Object.keys(targetQuestion).length !== 0 && parentNodeContentId) {
clearDataAfterAddNode({ parentNodeContentId, targetQuestion });
createResult(useQuizStore.getState().editQuizId, targetQuestion.content.id);
} else {
enqueueSnackbar("Добавляемый вопрос не найден");
}
};

@ -1,24 +1,19 @@
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
import type { MutableRefObject } from "react";
import {
cleardragQuestionContentId,
setModalQuestionParentContentId,
setOpenedModalQuestions,
updateDeleteId,
updateOpenedModalSettingsId,
} from "@root/uiTools/actions";
import type {
PresetLayoutOptions,
LayoutEventObject,
NodeSingular,
AbstractEventObject,
Core,
NodeSingular,
SingularData,
} from "cytoscape";
import { getQuestionByContentId } from "@root/questions/actions";
type usePopperArgs = {
layoutsContainer: MutableRefObject<HTMLDivElement | null>;
plusesContainer: MutableRefObject<HTMLDivElement | null>;
crossesContainer: MutableRefObject<HTMLDivElement | null>;
gearsContainer: MutableRefObject<HTMLDivElement | null>;
setModalQuestionParentContentId: (id: string) => void;
setOpenedModalQuestions: (open: boolean) => void;
setStartCreate: (id: string) => void;
setStartRemove: (id: string) => void;
};
import { getPopperInstance } from "cytoscape-popper";
import { useCallback, type MutableRefObject, useRef } from "react";
import { addNode } from "../helper";
type PopperItem = {
id: () => string;
@ -37,231 +32,175 @@ type PopperConfig = {
content: (items: PopperItem[]) => void;
};
type Popper = {
update: () => Promise<void>;
setOptions: (modifiers: { modifiers?: Modifier[] }) => void;
};
type PopperInstance = ReturnType<getPopperInstance<SingularData>>;
type NodeSingularWithPopper = NodeSingular & {
popper: (config: PopperConfig) => Popper;
popper: (config: PopperConfig) => PopperInstance;
};
/** @deprecated */
export const usePopper = ({
layoutsContainer,
plusesContainer,
crossesContainer,
gearsContainer,
setModalQuestionParentContentId,
setOpenedModalQuestions,
setStartCreate,
setStartRemove,
}: usePopperArgs) => {
const removeButtons = (id: string) => {
layoutsContainer.current
?.querySelector(`.popper-layout[data-id='${id}']`)
?.remove();
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();
};
cyRef,
}: {
cyRef: MutableRefObject<Core | null>;
}) => {
const popperContainerRef = useRef<HTMLDivElement | null>(null);
const popperInstancesRef = useRef<PopperInstance[]>([]);
const initialPopperIcons = ({ cy }: LayoutEventObject) => {
const container =
(document.body.querySelector(
".__________cytoscape_container",
) as HTMLDivElement) || null;
const removeAllPoppers = useCallback(() => {
cyRef.current?.removeListener("zoom render");
popperInstancesRef.current.forEach((p) => p.destroy());
popperInstancesRef.current = [];
popperContainerRef.current?.remove();
popperContainerRef.current = null;
}, []);
const recreatePoppers = useCallback(() => {
removeAllPoppers();
const cy = cyRef.current;
if (!cy) return;
const container = cy.container();
if (!container) {
console.warn("Cannot create popper 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);
}
if (!layoutsContainer.current) {
layoutsContainer.current = document.createElement("div");
layoutsContainer.current.setAttribute("id", "popper-layouts");
container.append(layoutsContainer.current);
if (!popperContainerRef.current) {
popperContainerRef.current = document.createElement("div");
popperContainerRef.current.setAttribute("id", "poppers-container");
container.append(popperContainerRef.current);
}
cy?.removeAllListeners();
cy.nodes().forEach((item) => {
const node = item as NodeSingularWithPopper;
cy
.nodes()
.toArray()
?.forEach((item) => {
const node = item as NodeSingularWithPopper;
const layoutsPopper = node.popper({
popper: {
placement: "left",
modifiers: [{ name: "flip", options: { boundary: node } }],
},
content: (items) => {
const item = items[0];
const itemId = item.id();
const itemElement = popperContainerRef.current?.querySelector(
`.popper-layout[data-id='${itemId}']`,
);
if (itemElement) {
return itemElement;
}
const layoutsPopper = node.popper({
const layoutElement = document.createElement("div");
layoutElement.style.zIndex = "0";
layoutElement.classList.add("popper-layout");
layoutElement.setAttribute("data-id", item.id());
layoutElement.addEventListener("pointerup", () => {
//Узнаём грани, идущие от этой ноды
setModalQuestionParentContentId(item.id());
setOpenedModalQuestions(true);
});
popperContainerRef.current?.appendChild(layoutElement);
return layoutElement;
},
});
popperInstancesRef.current.push(layoutsPopper);
const plusesPopper = node.popper({
popper: {
placement: "right",
modifiers: [{ name: "flip", options: { boundary: node } }],
},
content: ([item]) => {
const itemId = item.id();
const itemElement = popperContainerRef.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());
plusElement.style.zIndex = "1";
plusElement.addEventListener("pointerup", () => {
addNode({ parentNodeContentId: node.id() });
cleardragQuestionContentId();
});
popperContainerRef.current?.appendChild(plusElement);
return plusElement;
},
});
popperInstancesRef.current.push(plusesPopper);
const crossesPopper = node.popper({
popper: {
placement: "top-end",
modifiers: [{ name: "flip", options: { boundary: node } }],
},
content: ([item]) => {
const itemId = item.id();
const itemElement = popperContainerRef.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());
crossElement.style.zIndex = "2";
popperContainerRef.current?.appendChild(crossElement);
crossElement.addEventListener("pointerup", () => {
updateDeleteId(node.id());
});
return crossElement;
},
});
popperInstancesRef.current.push(crossesPopper);
let gearsPopper: PopperInstance | null = null;
if (node.data().root !== true) {
gearsPopper = node.popper({
popper: {
placement: "left",
modifiers: [{ name: "flip", options: { boundary: node } }],
},
content: ([item]) => {
const itemId = item.id();
const itemElement = layoutsContainer.current?.querySelector(
`.popper-layout[data-id='${itemId}']`,
const itemElement = popperContainerRef.current?.querySelector(
`.popper-gear[data-id='${itemId}']`,
);
if (itemElement) {
return itemElement;
}
const layoutElement = document.createElement("div");
layoutElement.style.zIndex = "0";
layoutElement.classList.add("popper-layout");
layoutElement.setAttribute("data-id", item.id());
layoutElement.addEventListener("mouseup", () => {
//Узнаём грани, идущие от этой ноды
setModalQuestionParentContentId(item.id());
setOpenedModalQuestions(true);
const gearElement = document.createElement("div");
gearElement.classList.add("popper-gear");
gearElement.setAttribute("data-id", item.id());
gearElement.style.zIndex = "1";
popperContainerRef.current?.appendChild(gearElement);
gearElement.addEventListener("pointerup", () => {
updateOpenedModalSettingsId(item.id());
});
layoutElement.addEventListener("touchstart", () => {
//Узнаём грани, идущие от этой ноды
setModalQuestionParentContentId(item.id());
setOpenedModalQuestions(true);
});
layoutsContainer.current?.appendChild(layoutElement);
return layoutElement;
return gearElement;
},
});
popperInstancesRef.current.push(gearsPopper);
}
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());
plusElement.style.zIndex = "1";
plusElement.addEventListener("mouseup", () => {
setStartCreate(node.id());
});
plusElement.addEventListener("touchstart", () => {
setStartCreate(node.id());
});
plusesContainer.current?.appendChild(plusElement);
return plusElement;
},
});
const crossesPopper = node.popper({
popper: {
placement: "top-end",
modifiers: [{ name: "flip", options: { boundary: node } }],
},
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());
crossElement.style.zIndex = "2";
crossesContainer.current?.appendChild(crossElement);
crossElement.addEventListener("mouseup", () => {
setStartRemove(node.id());
});
crossElement.addEventListener("touchstart", () => {
setStartRemove(node.id());
});
return crossElement;
},
});
let gearsPopper: Popper | null = null;
if (node.data().root !== true) {
const parentQuestion = getQuestionByContentId(
node.data("parentType"),
);
gearsPopper = node.popper({
popper: {
placement: "left",
modifiers: [{ name: "flip", options: { boundary: node } }],
},
content: ([item]) => {
const itemId = item.id();
const itemElement = gearsContainer.current?.querySelector(
`.popper-gear[data-id='${itemId}']`,
);
if (itemElement) {
return itemElement;
}
const gearElement = document.createElement("div");
gearElement.classList.add("popper-gear");
gearElement.setAttribute("data-id", item.id());
gearElement.style.zIndex = "1";
gearsContainer.current?.appendChild(gearElement);
gearElement.addEventListener("mouseup", () => {
updateOpenedModalSettingsId(item.id());
});
gearElement.addEventListener("touchstart", () => {
updateOpenedModalSettingsId(item.id());
});
if (
parentQuestion?.type === "date" ||
parentQuestion?.type === "text" ||
parentQuestion?.type === "number" ||
parentQuestion?.type === "page"
) {
gearElement.classList.add("popper-gear-none");
}
return gearElement;
},
});
}
const update = async () => {
await plusesPopper.update();
await crossesPopper.update();
await gearsPopper?.update();
await layoutsPopper.update();
};
const zoom = cy.zoom();
//update();
const onZoom = (event: AbstractEventObject) => {
const zoom = event.cy.zoom();
crossesPopper.setOptions({
modifiers: [
@ -279,7 +218,7 @@ export const usePopper = ({
plusesPopper.setOptions({
modifiers: [
{ name: "flip", options: { boundary: node } },
{ name: "offset", options: { offset: [0, 0 * zoom] } },
{ name: "offset", options: { offset: [0, 0] } },
],
});
gearsPopper?.setOptions({
@ -289,16 +228,16 @@ export const usePopper = ({
],
});
layoutsContainer.current
?.querySelectorAll("#popper-layouts > .popper-layout")
popperContainerRef.current
?.querySelectorAll(".popper-layout")
.forEach((item) => {
const element = item as HTMLDivElement;
element.style.width = `${130 * zoom}px`;
element.style.height = `${130 * zoom}px`;
});
plusesContainer.current
?.querySelectorAll("#popper-pluses > .popper-plus")
popperContainerRef.current
?.querySelectorAll(".popper-plus")
.forEach((item) => {
const element = item as HTMLDivElement;
element.style.width = `${40 * zoom}px`;
@ -307,8 +246,8 @@ export const usePopper = ({
element.style.borderRadius = `${6 * zoom}px`;
});
crossesContainer.current
?.querySelectorAll("#popper-crosses > .popper-cross")
popperContainerRef.current
?.querySelectorAll(".popper-cross")
.forEach((item) => {
const element = item as HTMLDivElement;
element.style.width = `${24 * zoom}px`;
@ -317,188 +256,18 @@ export const usePopper = ({
element.style.borderRadius = `${6 * zoom}px`;
});
gearsContainer?.current
?.querySelectorAll("#popper-gears > .popper-gear")
popperContainerRef?.current
?.querySelectorAll(".popper-gear")
.forEach((item) => {
const element = item as HTMLDivElement;
element.style.width = `${60 * zoom}px`;
element.style.height = `${40 * zoom}px`;
});
};
//node?.on("position", update);
let pressed = false;
let hide = false;
cy?.on("mousedown", () => {
pressed = true;
});
cy?.on("mouseup", () => {
pressed = false;
hide = false;
const gc = gearsContainer.current;
if (gc) gc.style.display = "block";
const pc = plusesContainer.current;
const xc = crossesContainer.current;
const lc = layoutsContainer.current;
if (pc) pc.style.display = "block";
if (xc) xc.style.display = "block";
if (lc) lc.style.display = "block";
update();
});
cy?.on("mousemove", () => {
if (pressed && !hide) {
hide = true;
const gc = gearsContainer.current;
if (gc) gc.style.display = "none";
const pc = plusesContainer.current;
const xc = crossesContainer.current;
const lc = layoutsContainer.current;
if (pc) pc.style.display = "none";
if (xc) xc.style.display = "none";
if (lc) lc.style.display = "block";
}
});
cy.on("render", () => {
update();
});
});
};
const readyLO = (event: LayoutEventObject) => {
if (event.cy.data("firstNode") === "nonroot") {
event.cy.data("firstNode", "root");
event.cy
.nodes()
.sort((a, b) => (a.data("root") ? 1 : -1))
.layout(layoutOptions)
.run();
} else {
event.cy.data("changed", false);
event.cy.removeData("firstNode");
}
//удаляем иконки
event.cy.nodes().forEach((ele: any) => {
const data = ele.data();
data.id && removeButtons(data.id);
cy.on("zoom render", onZoom);
});
}, []);
initialPopperIcons(event);
};
const layoutOptions: PresetLayoutOptions = {
name: "preset",
positions: (node) => {
if (!node.cy().data("changed")) {
return node.data("oldPos");
}
const id = node.id();
const incomming = node.cy().edges(`[target="${id}"]`);
const layer = 0;
node.removeData("lastChild");
if (incomming.length === 0) {
if (node.cy().data("firstNode") === undefined)
node.cy().data("firstNode", "root");
node.data("root", true);
const children = node.cy().edges(`[source="${id}"]`).targets();
node.data("layer", layer);
node.data("children", children.length);
const queue = [];
children.forEach((n) => {
queue.push({ task: n, layer: layer + 1 });
});
while (queue.length) {
const task = queue.pop();
task.task.data("layer", task.layer);
task.task.removeData("subtreeWidth");
const children = node
.cy()
.edges(`[source="${task.task.id()}"]`)
.targets();
task.task.data("children", children.length);
if (children.length !== 0) {
children.forEach((n) =>
queue.push({ task: n, layer: task.layer + 1 }),
);
}
}
queue.push({ parent: node, children: children });
while (queue.length) {
const task = queue.pop();
if (task.children.length === 0) {
task.parent.data("subtreeWidth", task.parent.height() + 50);
continue;
}
const unprocessed = task?.children.filter((node) => {
return node.data("subtreeWidth") === undefined;
});
if (unprocessed.length !== 0) {
queue.push(task);
unprocessed.forEach((t) => {
queue.push({
parent: t,
children: t.cy().edges(`[source="${t.id()}"]`).targets(),
});
});
continue;
}
task?.parent.data(
"subtreeWidth",
task.children.reduce((p, n) => p + n.data("subtreeWidth"), 0),
);
}
const pos = { x: 0, y: 0 };
node.data("oldPos", pos);
queue.push({ task: children, parent: node });
while (queue.length) {
const task = queue.pop();
const oldPos = task.parent.data("oldPos");
let yoffset = oldPos.y - task.parent.data("subtreeWidth") / 2;
task.task.forEach((n) => {
const width = n.data("subtreeWidth");
n.data("oldPos", {
x: 250 * n.data("layer"),
y: yoffset + width / 2,
});
yoffset += width;
queue.push({
task: n.cy().edges(`[source="${n.id()}"]`).targets(),
parent: n,
});
});
}
node.cy().data("changed", false);
return pos;
} else {
const opos = node.data("oldPos");
if (opos) {
return opos;
}
}
}, // map of (node id) => (position obj); or function(node){ return somPos; }
zoom: undefined, // the zoom level to set (prob want fit = false if set)
pan: 1, // the pan level to set (prob want fit = false if set)
fit: false, // whether to fit to viewport
padding: 30, // padding on fit
animate: false, // whether to transition the node positions
animationDuration: 500, // duration of animation in ms if enabled
animationEasing: undefined, // easing of animation if enabled
animateFilter: function (node, i) {
return false;
}, // a function that determines whether the node should be animated. All nodes animated by default on animate enabled. Non-animated nodes are positioned immediately when the layout starts
ready: readyLO, // callback on layoutready
transform: function (node, position) {
return position;
}, // transform a given node position. Useful for changing flow direction in discrete layouts
};
return { layoutOptions };
return { removeAllPoppers, recreatePoppers };
};

@ -1,130 +1,34 @@
import { devlog } from "@frontend/kitui";
import { QuizQuestionResult } from "@model/questionTypes/result";
import {
deleteQuestion,
updateQuestion,
getQuestionByContentId,
clearRuleForAll,
createResult,
getQuestionByContentId,
updateQuestion,
} from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateRootContentId } from "@root/quizes/actions";
import type { MutableRefObject } from "react";
import { useCurrentQuiz } from "@root/quizes/hooks";
import type {
Core,
CollectionReturnValue,
PresetLayoutOptions,
Core,
SingularElementArgument,
} from "cytoscape";
import type {
AnyTypedQuizQuestion,
QuestionBranchingRule,
QuestionBranchingRuleMain,
} from "../../../../model/questionTypes/shared";
import type { MutableRefObject } from "react";
import { clearDataAfterRemoveNode } from "../helper";
type UseRemoveNodeArgs = {
cyRef: MutableRefObject<Core | null>;
layoutOptions: PresetLayoutOptions;
layoutsContainer: MutableRefObject<HTMLDivElement | null>;
plusesContainer: MutableRefObject<HTMLDivElement | null>;
crossesContainer: MutableRefObject<HTMLDivElement | null>;
gearsContainer: MutableRefObject<HTMLDivElement | null>;
};
export const useRemoveNode = ({
cyRef,
layoutOptions,
layoutsContainer,
plusesContainer,
crossesContainer,
gearsContainer,
}: UseRemoveNodeArgs) => {
export const useRemoveNode = ({ cyRef }: UseRemoveNodeArgs) => {
const { questions: trashQuestions } = useQuestionsStore();
const quiz = useCurrentQuiz();
const removeButtons = (id: string) => {
layoutsContainer.current
?.querySelector(`.popper-layout[data-id='${id}']`)
?.remove();
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 clearDataAfterRemoveNode = ({
targetQuestionContentId,
parentQuestionContentId,
}: {
targetQuestionContentId: string;
parentQuestionContentId: string;
}) => {
updateQuestion(targetQuestionContentId, (question) => {
question.content.rule.parentId = "";
question.content.rule.children = [];
question.content.rule.main = [];
question.content.rule.default = "";
});
//Ищём родителя
const parentQuestion = getQuestionByContentId(parentQuestionContentId);
if (parentQuestion.content.rule.children.length === 1) {
//если у родителя больше нет потомков
//Делаем результат родителя активным
const parentResult = trashQuestions.find(
(q) =>
q.type === "result" &&
q.content.rule.parentId === parentQuestionContentId,
);
if (parentResult) {
updateQuestion(parentResult.content.id, (q) => {
q.content.usage = true;
});
} else {
createResult(quiz?.backendId, parentQuestionContentId);
}
}
//чистим rule родителя
if (!parentQuestion?.type) {
return;
}
const newChildren = [...parentQuestion.content.rule.children];
newChildren.splice(
parentQuestion.content.rule.children.indexOf(targetQuestionContentId),
1,
);
const newRule: QuestionBranchingRule = {
children: newChildren,
default:
parentQuestion.content.rule.default === targetQuestionContentId
? ""
: parentQuestion.content.rule.default,
//удаляем условия перехода от родителя к этому вопросу,
main: parentQuestion.content.rule.main.filter(
(data: QuestionBranchingRuleMain) =>
data.next !== targetQuestionContentId,
),
parentId: parentQuestion.content.rule.parentId,
};
updateQuestion(parentQuestionContentId, (PQ) => {
PQ.content.rule = newRule;
});
};
const removeNode = (targetNodeContentId: string) => {
const deleteNodes: string[] = [];
const deleteEdges: any = [];
const cy = cyRef?.current;
const findChildrenToDelete = (node: CollectionReturnValue) => {
const deleteNodesRecursively = (node: CollectionReturnValue) => {
//Узнаём грани, идущие от этой ноды
cy
?.$('edge[source = "' + node.id() + '"]')
@ -132,20 +36,18 @@ export const useRemoveNode = ({
.forEach((edge) => {
const edgeData = edge.data();
//записываем id грани для дальнейшего удаления
deleteEdges.push(edge);
//ищем ноду на конце грани, записываем её ID для дальнейшего удаления
const targetNode = cy?.$("#" + edgeData.target);
deleteNodes.push(targetNode.data().id);
//вызываем функцию для анализа потомков уже у этой ноды
findChildrenToDelete(targetNode);
deleteNodesRecursively(targetNode);
});
};
const elementToDelete = cy?.getElementById(targetNodeContentId);
if (elementToDelete) {
findChildrenToDelete(elementToDelete);
deleteNodesRecursively(elementToDelete);
}
const targetQuestion = getQuestionByContentId(targetNodeContentId);
@ -155,7 +57,7 @@ export const useRemoveNode = ({
targetQuestion.content.rule.parentId === "root" &&
quiz
) {
updateRootContentId(quiz?.id, "");
updateRootContentId(quiz.id, "");
updateQuestion(targetNodeContentId, (question) => {
question.content.rule.parentId = "";
question.content.rule.main = [];
@ -173,16 +75,14 @@ export const useRemoveNode = ({
quiz &&
cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0
) {
devlog(parentQuestionContentId);
//createFrontResult(quiz.backendId, parentQuestionContentId);
}
clearDataAfterRemoveNode({
trashQuestions,
targetQuestionContentId: targetNodeContentId,
parentQuestionContentId,
});
cy
?.remove(cy?.$("#" + targetNodeContentId))
.layout(layoutOptions)
.run();
}
}
@ -190,8 +90,6 @@ export const useRemoveNode = ({
deleteNodes.forEach((nodeId) => {
//Ноды
cy?.remove(cy?.$("#" + nodeId));
removeButtons(nodeId);
updateQuestion(nodeId, (question) => {
question.content.rule.parentId = "";
question.content.rule.main = [];
@ -200,15 +98,6 @@ export const useRemoveNode = ({
});
});
deleteEdges.forEach((edge: any) => {
//Грани
cy?.remove(edge);
});
removeButtons(targetNodeContentId);
cy?.data("changed", true);
cy?.layout(layoutOptions).run();
//делаем result всех потомков неактивными
trashQuestions.forEach((qr) => {
if (
@ -217,7 +106,7 @@ export const useRemoveNode = ({
(targetQuestion?.type &&
qr.content.rule.parentId === targetQuestion.content.id))
) {
updateQuestion(qr.content.id, (q) => {
updateQuestion<QuizQuestionResult>(qr.content.id, (q) => {
q.content.usage = false;
});
}

@ -1,20 +1,15 @@
import { Box } from "@mui/material";
import { FirstNodeField } from "./FirstNodeField";
import CsComponent from "./CsComponent";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { useEffect, useState } from "react";
import { BranchingQuestionsModal } from "../BranchingQuestionsModal";
import { useUiTools } from "@root/uiTools/store";
import { BranchingQuestionsModal } from "../BranchingQuestionsModal";
import CsComponent from "./CsComponent";
import { FirstNodeField } from "./FirstNodeField";
export const BranchingMap = () => {
const quiz = useCurrentQuiz();
const { dragQuestionContentId } = useUiTools();
const [modalQuestionParentContentId, setModalQuestionParentContentId] =
useState<string>("");
const [modalQuestionTargetContentId, setModalQuestionTargetContentId] =
useState<string>("");
const [openedModalQuestions, setOpenedModalQuestions] =
useState<boolean>(false);
const dragQuestionContentId = useUiTools(
(state) => state.dragQuestionContentId,
);
return (
<Box
@ -30,25 +25,8 @@ export const BranchingMap = () => {
border: dragQuestionContentId === null ? "none" : "#7e2aea 2px dashed",
}}
>
{quiz?.config.haveRoot ? (
<CsComponent
modalQuestionParentContentId={modalQuestionParentContentId}
modalQuestionTargetContentId={modalQuestionTargetContentId}
setOpenedModalQuestions={setOpenedModalQuestions}
setModalQuestionParentContentId={setModalQuestionParentContentId}
setModalQuestionTargetContentId={setModalQuestionTargetContentId}
/>
) : (
<FirstNodeField
setOpenedModalQuestions={setOpenedModalQuestions}
modalQuestionTargetContentId={modalQuestionTargetContentId}
/>
)}
<BranchingQuestionsModal
openedModalQuestions={openedModalQuestions}
setOpenedModalQuestions={setOpenedModalQuestions}
setModalQuestionTargetContentId={setModalQuestionTargetContentId}
/>
{quiz?.config.haveRoot ? <CsComponent /> : <FirstNodeField />}
<BranchingQuestionsModal />
</Box>
);
};

@ -1,4 +1,4 @@
#popper-pluses > .popper-plus {
.popper-plus {
cursor: pointer;
display: flex;
align-items: center;
@ -9,13 +9,13 @@
font-size: 0px;
}
#popper-pluses > .popper-plus::before {
.popper-plus::before {
content: "+";
color: rgba(154, 154, 175, 0.5);
font-size: inherit;
}
#popper-crosses > .popper-cross {
.popper-cross {
cursor: pointer;
display: flex;
align-items: center;
@ -25,14 +25,14 @@
font-size: 0px;
}
#popper-crosses > .popper-cross::before {
.popper-cross::before {
content: "+";
transform: rotate(45deg);
color: #fff;
font-size: inherit;
}
#popper-gears > .popper-gear {
.popper-gear {
cursor: pointer;
display: flex;
align-items: center;

@ -1,22 +1,20 @@
import { Box, Modal, Button, Typography } from "@mui/material";
import { useQuestionsStore } from "@root/questions/store";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
interface Props {
openedModalQuestions: boolean;
setModalQuestionTargetContentId: (contentId: string) => void;
setOpenedModalQuestions: (open: boolean) => void;
}
export const BranchingQuestionsModal = ({
openedModalQuestions,
setOpenedModalQuestions,
import { useUiTools } from "@root/uiTools/store";
import {
setModalQuestionTargetContentId,
}: Props) => {
setOpenedModalQuestions,
} from "@root/uiTools/actions";
export const BranchingQuestionsModal = () => {
const trashQuestions = useQuestionsStore().questions;
const questions = trashQuestions.filter(
(question) => question.type !== "result",
);
const openedModalQuestions = useUiTools(
(state) => state.openedModalQuestions,
);
const handleClose = () => {
setOpenedModalQuestions(false);

@ -50,6 +50,7 @@ export const ChooseAnswerModal = ({
open={open}
anchorEl={anchorRef.current}
transition
sx={{ zIndex: 1 }}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>

@ -47,6 +47,7 @@ export const ChooseAnswerModal = ({
open={open}
anchorEl={anchorRef.current}
transition
sx={{ zIndex: 1 }}
>
{({ TransitionProps }) => (
<Grow {...TransitionProps}>

@ -20,6 +20,7 @@ import {
InputAdornment,
Paper,
TextField,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
@ -65,7 +66,9 @@ export default function QuestionsPageCard({
questionIndex,
draggableProps,
}: Props) {
const maxLengthTextField = 225;
const [open, setOpen] = useState<boolean>(false);
const [isTextFieldtActive, setIsTextFieldtActive] = useState(false);
const anchorRef = useRef(null);
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
@ -80,6 +83,14 @@ export default function QuestionsPageCard({
});
}, 200);
const handleInputFocus = () => {
setIsTextFieldtActive(true);
};
const handleInputBlur = (event: React.FocusEvent<HTMLInputElement>) => {
setIsTextFieldtActive(false);
};
return (
<>
<Paper
@ -128,6 +139,8 @@ export default function QuestionsPageCard({
if ((target.value, toString().length <= 225))
setTitle(target.value);
}}
onFocus={handleInputFocus}
onBlur={handleInputBlur}
sx={{
width: "100%",
margin: isMobile ? "10px 0" : 0,
@ -148,6 +161,9 @@ export default function QuestionsPageCard({
},
},
}}
inputProps={{
maxLength: maxLengthTextField,
}}
InputProps={{
startAdornment: (
<Box>
@ -168,6 +184,27 @@ export default function QuestionsPageCard({
/>
</Box>
),
endAdornment: isTextFieldtActive &&
question.title.length >= maxLengthTextField - 7 && (
<Box
sx={{
display: "flex",
marginTop: "5px",
marginLeft: "auto",
position: "absolute",
bottom: "-28px",
right: "0",
}}
>
<Typography fontSize="14px">
{question.title.length}
</Typography>
<span>/</span>
<Typography fontSize="14px">
{maxLengthTextField}
</Typography>
</Box>
),
}}
/>
</FormControl>

@ -24,7 +24,6 @@ export default function SettingOptionsAndPict({
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(680));
console.log("question.content.replText ", question.content.replText);
const setReplText = useDebouncedCallback((replText) => {
updateQuestion(question.id, (question) => {
if (question.type !== "varimg") return;

@ -59,7 +59,6 @@ export default function EditPage({
const quiz = useCurrentQuiz();
const { editQuizId } = useQuizStore();
const { questions } = useQuestionsStore();
console.log(questions);
const { whyCantCreatePublic, showConfirmLeaveModal, nextStep } = useUiTools();
const theme = useTheme();
const navigate = useNavigate();
@ -114,7 +113,6 @@ export default function EditPage({
const isConditionMet =
[1].includes(currentStep) && quizConfig.type !== "form";
console.log("quiz", quiz);
return (
<>
<Box

@ -36,14 +36,16 @@ export const ModalInfoWhyCantCreate = () => {
overflow: "auto",
}}
>
{Object.values(whyCantCreatePublic).map((data) => {
{Object.entries(whyCantCreatePublic).map(([id, data]) => {
return (
<Box>
<Box key={id}>
<Typography color="#7e2aea">
{data.name === "quiz" ? "У квиза" : `У вопроса "${data.name}"`}
</Typography>
{data.problems.map((problem) => (
<Typography p="5px 0">{problem}</Typography>
{data.problems.map((problem, index) => (
<Typography key={index} p="5px 0">
{problem}
</Typography>
))}
<Divider />
</Box>

@ -26,8 +26,10 @@ import { useUiTools } from "../uiTools/store";
import { withErrorBoundary } from "react-error-boundary";
import { QuizQuestionResult } from "@model/questionTypes/result";
import { replaceEmptyLinesToSpace } from "../../utils/replaceEmptyLinesToSpace";
import { useQuizPreviewStore } from "@root/quizPreview";
import { useQuizStore } from "@root/quizes/store";
export const setQuestions = (questions: RawQuestion[] | null) =>
export const setQuestions = (questions: RawQuestion[] | null | undefined) =>
setProducedState(
(state) => {
const untypedResultQuestions = state.questions.filter(
@ -629,7 +631,10 @@ export const clearRuleForAll = () => {
);
};
export const createResult = async (quizId: number, parentContentId?: string) =>
export const createResult = async (
quizId: number | null | undefined,
parentContentId?: string,
) =>
requestQueue.enqueue(async () => {
if (!quizId || !parentContentId) {
console.error(

@ -43,3 +43,12 @@ export const updateSomeWorkBackend = (someWorkBackend: boolean) =>
export const updateNextStep = (nextStep: number) =>
useUiTools.setState({ nextStep });
export const setModalQuestionParentContentId = (
modalQuestionParentContentId: string,
) => useUiTools.setState({ modalQuestionParentContentId });
export const setModalQuestionTargetContentId = (
modalQuestionTargetContentId: string,
) => useUiTools.setState({ modalQuestionTargetContentId });
export const setOpenedModalQuestions = (open: boolean) =>
useUiTools.setState({ openedModalQuestions: open });

@ -13,6 +13,9 @@ export type UiTools = {
showConfirmLeaveModal: boolean;
someWorkBackend: boolean;
nextStep: number;
modalQuestionParentContentId: string;
modalQuestionTargetContentId: string;
openedModalQuestions: boolean;
};
export type WhyCantCreatePublic = {
@ -32,6 +35,9 @@ const initialState: UiTools = {
showConfirmLeaveModal: false,
someWorkBackend: false,
nextStep: -1,
modalQuestionParentContentId: "",
modalQuestionTargetContentId: "",
openedModalQuestions: false,
};
export const useUiTools = create<UiTools>()(

7362
yarn.lock Executable file → Normal file

File diff suppressed because it is too large Load Diff