Merge branch 'design-page' into tomainone
@ -1 +1 @@
|
|||||||
REACT_APP_DOMAIN="https://squiz.pena.digital"
|
REACT_APP_DOMAIN=""
|
||||||
|
@ -1,38 +1,32 @@
|
|||||||
include:
|
include:
|
||||||
- project: "devops/pena-continuous-integration"
|
- project: "devops/pena-continuous-integration"
|
||||||
file: "/templates/docker/build-template.gitlab-ci.yml"
|
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"
|
- project: "devops/pena-continuous-integration"
|
||||||
file: "/templates/docker/deploy-template.gitlab-ci.yml"
|
file: "/templates/docker/deploy-template.gitlab-ci.yml"
|
||||||
stages:
|
stages:
|
||||||
- clean
|
|
||||||
- build
|
- build
|
||||||
- deploy
|
- 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:
|
build-app:
|
||||||
extends: .build_template
|
extends: .build_template
|
||||||
|
tags:
|
||||||
|
- frontbuild
|
||||||
variables:
|
variables:
|
||||||
DOCKER_BUILD_PATH: "./Dockerfile"
|
DOCKER_BUILD_PATH: "./Dockerfile"
|
||||||
STAGING_BRANCH: "main"
|
STAGING_BRANCH: "staging"
|
||||||
PRODUCTION_BRANCH: "main"
|
PRODUCTION_BRANCH: "main"
|
||||||
|
|
||||||
deploy-to-staging:
|
deploy-to-staging:
|
||||||
extends: .deploy_template
|
extends: .deploy_template
|
||||||
variables:
|
rules:
|
||||||
DEPLOY_TO: "staging"
|
- if: "$CI_COMMIT_BRANCH == $STAGING_BRANCH"
|
||||||
BRANCH: "main"
|
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
|
WORKDIR /usr/app
|
||||||
COPY . .
|
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/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/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 install --ignore-scripts --non-interactive --frozen-lockfile && yarn cache clean
|
||||||
RUN yarn build
|
RUN yarn build
|
||||||
|
|
||||||
|
7
deployments/main/docker-compose.yaml
Normal file
@ -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:
|
squiz:
|
||||||
container_name: squiz
|
container_name: squiz
|
||||||
restart: unless-stopped
|
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:
|
networks:
|
||||||
- marketplace_penahub_frontend
|
- marketplace_penahub_frontend
|
||||||
hostname: squiz
|
hostname: squiz
|
||||||
@ -10,4 +10,3 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
marketplace_penahub_frontend:
|
marketplace_penahub_frontend:
|
||||||
external: true
|
external: true
|
||||||
|
|
||||||
|
BIN
src/assets/icons/designs/design1.jpg
Normal file
After Width: | Height: | Size: 7.7 MiB |
BIN
src/assets/icons/designs/design10.jpg
Normal file
After Width: | Height: | Size: 6.1 MiB |
BIN
src/assets/icons/designs/design2.jpg
Normal file
After Width: | Height: | Size: 7.3 MiB |
BIN
src/assets/icons/designs/design3.jpg
Normal file
After Width: | Height: | Size: 4.3 MiB |
BIN
src/assets/icons/designs/design4.jpg
Normal file
After Width: | Height: | Size: 1.3 MiB |
BIN
src/assets/icons/designs/design5.jpg
Normal file
After Width: | Height: | Size: 7.8 MiB |
BIN
src/assets/icons/designs/design6.jpg
Normal file
After Width: | Height: | Size: 8.4 MiB |
BIN
src/assets/icons/designs/design7.jpg
Normal file
After Width: | Height: | Size: 8.2 MiB |
BIN
src/assets/icons/designs/design8.jpg
Normal file
After Width: | Height: | Size: 3.2 MiB |
BIN
src/assets/icons/designs/design9.jpg
Normal file
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 { LocalizationProvider } from "@mui/x-date-pickers";
|
||||||
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
import { AdapterDayjs } from "@mui/x-date-pickers/AdapterDayjs";
|
||||||
import { ruRU } from "@mui/x-date-pickers/locales";
|
import { ruRU } from "@mui/x-date-pickers/locales";
|
||||||
import App from "./App";
|
import App from "./App";
|
||||||
import dayjs from "dayjs";
|
import dayjs from "dayjs";
|
||||||
import "dayjs/locale/ru";
|
import "dayjs/locale/ru";
|
||||||
import { SnackbarProvider } from "notistack";
|
import { SnackbarProvider, closeSnackbar } from "notistack";
|
||||||
import { DndProvider } from "react-dnd";
|
import { DndProvider } from "react-dnd";
|
||||||
import { HTML5Backend } from "react-dnd-html5-backend";
|
import { HTML5Backend } from "react-dnd-html5-backend";
|
||||||
import { createRoot } from "react-dom/client";
|
import { createRoot } from "react-dom/client";
|
||||||
@ -15,6 +15,9 @@ import { SWRConfig } from "swr";
|
|||||||
import { BrowserRouter } from "react-router-dom";
|
import { BrowserRouter } from "react-router-dom";
|
||||||
import moment from "moment";
|
import moment from "moment";
|
||||||
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
|
import { polyfillCountryFlagEmojis } from "country-flag-emoji-polyfill";
|
||||||
|
import CloseIcon from "@icons/CloseBold";
|
||||||
|
|
||||||
|
import type { SnackbarKey } from "notistack";
|
||||||
|
|
||||||
dayjs.locale("ru");
|
dayjs.locale("ru");
|
||||||
moment.locale("ru");
|
moment.locale("ru");
|
||||||
@ -22,6 +25,19 @@ polyfillCountryFlagEmojis();
|
|||||||
const localeText =
|
const localeText =
|
||||||
ruRU.components.MuiLocalizationProvider.defaultProps.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")!);
|
const root = createRoot(document.getElementById("root")!);
|
||||||
|
|
||||||
root.render(
|
root.render(
|
||||||
@ -40,6 +56,8 @@ root.render(
|
|||||||
<ThemeProvider theme={lightTheme}>
|
<ThemeProvider theme={lightTheme}>
|
||||||
<BrowserRouter>
|
<BrowserRouter>
|
||||||
<SnackbarProvider
|
<SnackbarProvider
|
||||||
|
SnackbarProps={{ onTouchStart: () => closeSnackbar() }}
|
||||||
|
action={snackbarAction}
|
||||||
preventDuplicate={true}
|
preventDuplicate={true}
|
||||||
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
|
style={{ backgroundColor: lightTheme.palette.brightPurple.main }}
|
||||||
>
|
>
|
||||||
|
@ -46,24 +46,26 @@ export type QuizType = "quiz" | "form" | null;
|
|||||||
|
|
||||||
export type QuizResultsType = true | null;
|
export type QuizResultsType = true | null;
|
||||||
|
|
||||||
|
export type Theme =
|
||||||
|
| "StandardTheme"
|
||||||
|
| "StandardDarkTheme"
|
||||||
|
| "PinkTheme"
|
||||||
|
| "PinkDarkTheme"
|
||||||
|
| "BlackWhiteTheme"
|
||||||
|
| "OliveTheme"
|
||||||
|
| "YellowTheme"
|
||||||
|
| "GoldDarkTheme"
|
||||||
|
| "PurpleTheme"
|
||||||
|
| "BlueTheme"
|
||||||
|
| "BlueDarkTheme";
|
||||||
|
|
||||||
export interface QuizConfig {
|
export interface QuizConfig {
|
||||||
type: QuizType;
|
type: QuizType;
|
||||||
noStartPage: boolean;
|
noStartPage: boolean;
|
||||||
startpageType: QuizStartpageType;
|
startpageType: QuizStartpageType;
|
||||||
results: QuizResultsType;
|
results: QuizResultsType;
|
||||||
haveRoot: string | null;
|
haveRoot: string | null;
|
||||||
theme:
|
theme: Theme;
|
||||||
| "StandardTheme"
|
|
||||||
| "StandardDarkTheme"
|
|
||||||
| "PinkTheme"
|
|
||||||
| "PinkDarkTheme"
|
|
||||||
| "BlackWhiteTheme"
|
|
||||||
| "OliveTheme"
|
|
||||||
| "YellowTheme"
|
|
||||||
| "GoldDarkTheme"
|
|
||||||
| "PurpleTheme"
|
|
||||||
| "BlueTheme"
|
|
||||||
| "BlueDarkTheme";
|
|
||||||
resultInfo: {
|
resultInfo: {
|
||||||
when: "email" | "";
|
when: "email" | "";
|
||||||
share: true | false;
|
share: true | false;
|
||||||
|
@ -3,7 +3,7 @@ import { Box } from "@mui/material";
|
|||||||
interface Color {
|
interface Color {
|
||||||
color?: string;
|
color?: string;
|
||||||
}
|
}
|
||||||
export default function ColorRingIcon({ color = "#333647" }: Color) {
|
export default function ColorRingIcon({ color }: Color) {
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -27,8 +27,8 @@ export default function ColorRingIcon({ color = "#333647" }: Color) {
|
|||||||
width="16"
|
width="16"
|
||||||
height="16"
|
height="16"
|
||||||
rx="8"
|
rx="8"
|
||||||
fill={color}
|
fill={color || "#FFFFFF"}
|
||||||
stroke="#9A9AAF"
|
stroke={color ? "transparent" : "#9A9AAF"}
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -1,45 +1,109 @@
|
|||||||
|
import { useState } from "react";
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
ButtonBase,
|
Divider,
|
||||||
IconButton,
|
|
||||||
Paper,
|
Paper,
|
||||||
Typography,
|
Typography,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import ColorRingIcon from "./ColorRingIcon";
|
|
||||||
import { updateQuiz } from "@root/quizes/actions";
|
import { updateQuiz } from "@root/quizes/actions";
|
||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import { toggleQuizPreview } from "@root/quizPreview";
|
import { DesignGroup } from "./DesignGroup";
|
||||||
import VisibilityIcon from "@mui/icons-material/Visibility";
|
|
||||||
|
|
||||||
const ButtonsThemeLight = [
|
import Desgin1 from "@icons/designs/design1.jpg";
|
||||||
["Стандартный", "StandardTheme", "#7E2AEA", "#FFFFFF"],
|
import Desgin2 from "@icons/designs/design2.jpg";
|
||||||
["Черно-белый", "BlackWhiteTheme", "#4E4D51", "#FFFFFF"],
|
import Desgin3 from "@icons/designs/design3.jpg";
|
||||||
["Оливковый", "OliveTheme", "#758E4F", "#F9FBF1"],
|
import Desgin4 from "@icons/designs/design4.jpg";
|
||||||
["Фиолетовый", "PurpleTheme", "#7E2AEA", "#FBF8FF"],
|
import Desgin5 from "@icons/designs/design5.jpg";
|
||||||
["Желтый", "YellowTheme", "#F2B133", "#FFFCF6"],
|
import Desgin6 from "@icons/designs/design6.jpg";
|
||||||
["Голубой", "BlueTheme", "#4964ED", "#F5F7FF"],
|
import Desgin7 from "@icons/designs/design7.jpg";
|
||||||
["Розовый", "PinkTheme", "#D34085", "#FFF9FC"],
|
import Desgin8 from "@icons/designs/design8.jpg";
|
||||||
];
|
import Desgin9 from "@icons/designs/design9.jpg";
|
||||||
const ButtonsThemeDark = [
|
import Desgin10 from "@icons/designs/design10.jpg";
|
||||||
["Стандартный", "StandardDarkTheme", "#7E2AEA", "#FFFFFF"],
|
|
||||||
["Золотой", "GoldDarkTheme", "#E6AA37", "#FFFFFF"],
|
import type { Theme } from "@model/quizSettings";
|
||||||
["Розовый", "PinkDarkTheme", "#D34085", "#FFFFFF"],
|
import type { DesignItem } from "./DesignGroup";
|
||||||
["Бирюзовый", "BlueDarkTheme", "#07A0C3", "#FFFFFF"],
|
|
||||||
|
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;
|
mobileSidebar: boolean;
|
||||||
heightSidebar: number;
|
heightSidebar: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const DesignFilling = ({ mobileSidebar, heightSidebar }: Props) => {
|
export const DesignFilling = ({
|
||||||
|
mobileSidebar,
|
||||||
|
heightSidebar,
|
||||||
|
}: DesignFillingProps) => {
|
||||||
|
const [design, setDesign] = useState<string>("");
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(830));
|
const isMobile = useMediaQuery(theme.breakpoints.down(830));
|
||||||
const heightBar = heightSidebar + 51 + 88 + 36;
|
const heightBar = heightSidebar + 51 + 88 + 36;
|
||||||
console.log(mobileSidebar, "111");
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
sx={{
|
sx={{
|
||||||
@ -55,104 +119,53 @@ export const DesignFilling = ({ mobileSidebar, heightSidebar }: Props) => {
|
|||||||
<Typography variant="h5" sx={{ marginBottom: "40px", color: "#333647" }}>
|
<Typography variant="h5" sx={{ marginBottom: "40px", color: "#333647" }}>
|
||||||
Дизайн
|
Дизайн
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography sx={{ marginBottom: "30px", color: "#333647" }}>
|
|
||||||
Выберите цветовую схему для вашего опроса
|
|
||||||
</Typography>
|
|
||||||
<Paper
|
<Paper
|
||||||
sx={{
|
sx={{
|
||||||
padding: "20px",
|
padding: "20px",
|
||||||
maxWidth: "796px",
|
maxWidth: "796px",
|
||||||
width: "100%",
|
width: "100%",
|
||||||
display: "flex",
|
|
||||||
gap: "20px",
|
|
||||||
borderRadius: "12px",
|
borderRadius: "12px",
|
||||||
flexWrap: "wrap",
|
|
||||||
height: "calc(100vh - 280px)",
|
height: "calc(100vh - 280px)",
|
||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<Box
|
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap" }}>
|
||||||
sx={{
|
<DesignGroup
|
||||||
width: isMobile ? "100%" : "48%",
|
title="Со светлым фоном"
|
||||||
display: "flex",
|
value={quiz?.config.theme || ""}
|
||||||
flexDirection: "column",
|
list={LIGHT_THEME_BUTTONS}
|
||||||
gap: "12px",
|
onChange={(name) =>
|
||||||
}}
|
updateQuiz(quiz?.id, (quiz) => {
|
||||||
>
|
quiz.config.theme = name as Theme;
|
||||||
<Typography color={"#9A9AAF"}>Со светлым фоном</Typography>
|
})
|
||||||
|
}
|
||||||
{ButtonsThemeLight.map((e, i) => (
|
/>
|
||||||
<ButtonBase
|
<DesignGroup
|
||||||
sx={{
|
title="С тёмным фоном"
|
||||||
maxWidth: "368px",
|
value={quiz?.config.theme || ""}
|
||||||
width: "100%",
|
list={DARK_THEME_BUTTONS}
|
||||||
padding: "22px 21px",
|
onChange={(name) =>
|
||||||
background:
|
updateQuiz(quiz?.id, (quiz) => {
|
||||||
quiz.config.theme == e[1]
|
quiz.config.theme = name as Theme;
|
||||||
? "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>
|
</Box>
|
||||||
<Box
|
<Box>
|
||||||
sx={{
|
<Divider sx={{ margin: "20px 0", background: "#7E2AEA33" }} />
|
||||||
width: isMobile ? "100%" : "48%",
|
</Box>
|
||||||
display: "flex",
|
<Box sx={{ display: "flex", gap: "20px", flexWrap: "wrap" }}>
|
||||||
flexDirection: "column",
|
<DesignGroup
|
||||||
gap: "12px",
|
title="С картинкой"
|
||||||
}}
|
value={design}
|
||||||
>
|
list={DESIGNG_LIST_FIRST}
|
||||||
<Typography color={"#9A9AAF"}>С тёмным фоном</Typography>
|
onChange={setDesign}
|
||||||
{ButtonsThemeDark.map((e, i) => (
|
/>
|
||||||
<ButtonBase
|
<DesignGroup
|
||||||
sx={{
|
value={design}
|
||||||
maxWidth: "368px",
|
list={DESIGNG_LIST_SECOND}
|
||||||
width: "100%",
|
onChange={setDesign}
|
||||||
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>
|
</Box>
|
||||||
</Paper>
|
</Paper>
|
||||||
</Box>
|
</Box>
|
||||||
|
89
src/pages/DesignPage/DesignGroup.tsx
Normal file
@ -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}
|
mobileSidebar={mobileSidebar}
|
||||||
heightSidebar={heightSidebar}
|
heightSidebar={heightSidebar}
|
||||||
/>
|
/>
|
||||||
{createPortal(<QuizPreview />, document.body)}
|
|
||||||
</Box>
|
</Box>
|
||||||
<ConfirmLeaveModal
|
<ConfirmLeaveModal
|
||||||
open={showConfirmLeaveModal}
|
open={showConfirmLeaveModal}
|
||||||
follow={followNewPage}
|
follow={followNewPage}
|
||||||
cancel={() => setShowConfirmLeaveModal(false)}
|
cancel={() => setShowConfirmLeaveModal(false)}
|
||||||
/>
|
/>
|
||||||
|
{createPortal(<QuizPreview />, document.body)}
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -8,22 +8,25 @@ import {
|
|||||||
IconButton,
|
IconButton,
|
||||||
InputAdornment,
|
InputAdornment,
|
||||||
Popover,
|
Popover,
|
||||||
TextField,
|
TextField as MuiTextField,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
|
TextFieldProps,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import {
|
import {
|
||||||
addQuestionVariant,
|
addQuestionVariant,
|
||||||
deleteQuestionVariant,
|
deleteQuestionVariant,
|
||||||
setQuestionVariantField,
|
setQuestionVariantField,
|
||||||
} from "@root/questions/actions";
|
} from "@root/questions/actions";
|
||||||
import type { ChangeEvent, KeyboardEvent, ReactNode } from "react";
|
import type { ChangeEvent, FC, KeyboardEvent, ReactNode } from "react";
|
||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
import type { QuestionVariant } from "../../../model/questionTypes/shared";
|
import type { QuestionVariant } from "../../../model/questionTypes/shared";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
|
||||||
|
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
|
||||||
|
|
||||||
type AnswerItemProps = {
|
type AnswerItemProps = {
|
||||||
index: number;
|
index: number;
|
||||||
questionId: string;
|
questionId: string;
|
||||||
@ -60,7 +63,6 @@ export const AnswerItem = ({
|
|||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setIsOpen(false);
|
setIsOpen(false);
|
||||||
};
|
};
|
||||||
console.log(variant);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable draggableId={String(index)} index={index}>
|
<Draggable draggableId={String(index)} index={index}>
|
||||||
|
@ -1,117 +1,54 @@
|
|||||||
import { useEffect, useLayoutEffect, useRef, useState } from "react";
|
import { devlog } from "@frontend/kitui";
|
||||||
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 { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
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 { useQuestionsStore } from "@root/questions/store";
|
||||||
import { useUiTools } from "@root/uiTools/store";
|
import { updateRootContentId } from "@root/quizes/actions";
|
||||||
import {
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
deleteQuestion,
|
|
||||||
updateQuestion,
|
|
||||||
getQuestionByContentId,
|
|
||||||
clearRuleForAll,
|
|
||||||
createResult,
|
|
||||||
} from "@root/questions/actions";
|
|
||||||
import {
|
import {
|
||||||
|
cleardragQuestionContentId,
|
||||||
|
setModalQuestionParentContentId,
|
||||||
|
setModalQuestionTargetContentId,
|
||||||
updateModalInfoWhyCantCreate,
|
updateModalInfoWhyCantCreate,
|
||||||
updateOpenedModalSettingsId,
|
updateOpenedModalSettingsId,
|
||||||
} from "@root/uiTools/actions";
|
} from "@root/uiTools/actions";
|
||||||
import { cleardragQuestionContentId } from "@root/uiTools/actions";
|
import { useUiTools } from "@root/uiTools/store";
|
||||||
import { updateDeleteId } from "@root/uiTools/actions";
|
|
||||||
|
|
||||||
import { DeleteNodeModal } from "../DeleteNodeModal";
|
|
||||||
import { ProblemIcon } from "@ui_kit/ProblemIcon";
|
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 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);
|
function CsComponent() {
|
||||||
|
const desireToOpenABranchingModal = useUiTools(
|
||||||
interface CsComponentProps {
|
(state) => state.desireToOpenABranchingModal,
|
||||||
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,
|
|
||||||
);
|
);
|
||||||
const [startCreate, setStartCreate] = useState("");
|
const canCreatePublic = useUiTools((state) => state.canCreatePublic);
|
||||||
const [startRemove, setStartRemove] = useState("");
|
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 cyRef = useRef<Core | null>(null);
|
||||||
const layoutsContainer = useRef<HTMLDivElement | null>(null);
|
const { removeNode } = useRemoveNode({ cyRef });
|
||||||
const plusesContainer = useRef<HTMLDivElement | null>(null);
|
|
||||||
const crossesContainer = useRef<HTMLDivElement | null>(null);
|
|
||||||
const gearsContainer = useRef<HTMLDivElement | null>(null);
|
|
||||||
|
|
||||||
const { layoutOptions } = usePopper({
|
const csElements = useMemo(() => {
|
||||||
layoutsContainer,
|
const questions = trashQuestions.filter(
|
||||||
plusesContainer,
|
(question): question is AnyTypedQuizQuestion =>
|
||||||
crossesContainer,
|
question.type !== null && question.type !== "result",
|
||||||
gearsContainer,
|
);
|
||||||
setModalQuestionParentContentId,
|
|
||||||
setOpenedModalQuestions,
|
|
||||||
setStartCreate,
|
|
||||||
setStartRemove,
|
|
||||||
});
|
|
||||||
const { removeNode } = useRemoveNode({
|
|
||||||
cyRef,
|
|
||||||
layoutOptions,
|
|
||||||
layoutsContainer,
|
|
||||||
plusesContainer,
|
|
||||||
crossesContainer,
|
|
||||||
gearsContainer,
|
|
||||||
});
|
|
||||||
|
|
||||||
function fitGraphToRootNode() {
|
return storeToNodes(questions);
|
||||||
const cy = cyRef.current;
|
}, [trashQuestions]);
|
||||||
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,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
const cy = cyRef?.current;
|
const cy = cyRef?.current;
|
||||||
@ -125,19 +62,14 @@ function CsComponent({
|
|||||||
cy?.elements().data("eroticeyeblink", false);
|
cy?.elements().data("eroticeyeblink", false);
|
||||||
}
|
}
|
||||||
}, [desireToOpenABranchingModal]);
|
}, [desireToOpenABranchingModal]);
|
||||||
//Техническая штучка. Гарантирует не отрисовку модалки по первому входу на страничку. И очистка данных по расскоменчиванию
|
|
||||||
//Быстро просто дешево и сердито :)
|
|
||||||
useLayoutEffect(() => {
|
|
||||||
updateOpenedModalSettingsId();
|
|
||||||
// updateRootContentId(quiz.id, "")
|
|
||||||
// clearRuleForAll()
|
|
||||||
}, []);
|
|
||||||
//Отлов mouseup для отрисовки ноды
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (
|
if (
|
||||||
modalQuestionTargetContentId.length !== 0 &&
|
modalQuestionTargetContentId.length !== 0 &&
|
||||||
modalQuestionParentContentId.length !== 0
|
modalQuestionParentContentId.length !== 0
|
||||||
) {
|
) {
|
||||||
|
if (!cyRef.current) return;
|
||||||
|
|
||||||
addNode({
|
addNode({
|
||||||
parentNodeContentId: modalQuestionParentContentId,
|
parentNodeContentId: modalQuestionParentContentId,
|
||||||
targetNodeContentId: modalQuestionTargetContentId,
|
targetNodeContentId: modalQuestionTargetContentId,
|
||||||
@ -147,195 +79,27 @@ function CsComponent({
|
|||||||
setModalQuestionTargetContentId("");
|
setModalQuestionTargetContentId("");
|
||||||
}, [modalQuestionTargetContentId]);
|
}, [modalQuestionTargetContentId]);
|
||||||
|
|
||||||
const addNode = ({
|
useEffect(function onMount() {
|
||||||
parentNodeContentId,
|
updateOpenedModalSettingsId();
|
||||||
targetNodeContentId,
|
document.addEventListener("pointerup", cleardragQuestionContentId);
|
||||||
}: {
|
|
||||||
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;
|
|
||||||
}
|
|
||||||
|
|
||||||
return () => {
|
return () => {
|
||||||
document
|
document.removeEventListener("pointerup", cleardragQuestionContentId);
|
||||||
.querySelector("#root")
|
|
||||||
?.removeEventListener("mouseup", cleardragQuestionContentId);
|
|
||||||
layoutsContainer.current?.remove();
|
|
||||||
plusesContainer.current?.remove();
|
|
||||||
crossesContainer.current?.remove();
|
|
||||||
gearsContainer.current?.remove();
|
|
||||||
};
|
};
|
||||||
}, [someWorkBackend]);
|
}, []);
|
||||||
|
|
||||||
|
useEffect(
|
||||||
|
function rerunLayout() {
|
||||||
|
cyRef.current?.layout(layoutOptions).run();
|
||||||
|
cyRef.current?.fit(undefined, 70);
|
||||||
|
},
|
||||||
|
[csElements],
|
||||||
|
);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box
|
<CsNodeButtons csElements={csElements} cyRef={cyRef} />
|
||||||
sx={{
|
<Box mb="20px">
|
||||||
mb: "20px",
|
|
||||||
display: "flex",
|
|
||||||
justifyContent: "space-between",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
<Button
|
<Button
|
||||||
sx={{
|
sx={{
|
||||||
height: "27px",
|
height: "27px",
|
||||||
@ -344,7 +108,9 @@ function CsComponent({
|
|||||||
fontSize: "16px",
|
fontSize: "16px",
|
||||||
}}
|
}}
|
||||||
variant="text"
|
variant="text"
|
||||||
onClick={fitGraphToRootNode}
|
onClick={() => {
|
||||||
|
cyRef.current?.fit(undefined, 70);
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Выровнять
|
Выровнять
|
||||||
</Button>
|
</Button>
|
||||||
@ -353,20 +119,22 @@ function CsComponent({
|
|||||||
onClick={() => updateModalInfoWhyCantCreate(true)}
|
onClick={() => updateModalInfoWhyCantCreate(true)}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
|
|
||||||
<CytoscapeComponent
|
<CytoscapeComponent
|
||||||
wheelSensitivity={0.1}
|
wheelSensitivity={0.1}
|
||||||
elements={[]}
|
elements={csElements}
|
||||||
// elements={createGraphElements(tree, quiz)}
|
style={{
|
||||||
style={{ height: "480px", background: "#F2F3F7" }}
|
height: "480px",
|
||||||
|
background: "#F2F3F7",
|
||||||
|
overflow: "hidden",
|
||||||
|
}}
|
||||||
stylesheet={stylesheet}
|
stylesheet={stylesheet}
|
||||||
layout={layoutOptions}
|
layout={layoutOptions}
|
||||||
cy={(cy) => {
|
cy={(cy) => {
|
||||||
cyRef.current = cy;
|
cyRef.current = cy;
|
||||||
}}
|
}}
|
||||||
autoungrabify={true}
|
autoungrabify={true}
|
||||||
zoom={0.6}
|
autounselectify={true}
|
||||||
zoomingEnabled={false}
|
boxSelectionEnabled={false}
|
||||||
/>
|
/>
|
||||||
<DeleteNodeModal removeNode={removeNode} />
|
<DeleteNodeModal removeNode={removeNode} />
|
||||||
</>
|
</>
|
||||||
@ -386,7 +154,7 @@ export default withErrorBoundary(CsComponent, {
|
|||||||
fallback: <Clear />,
|
fallback: <Clear />,
|
||||||
onError: (error, info) => {
|
onError: (error, info) => {
|
||||||
enqueueSnackbar("Дерево порвалось");
|
enqueueSnackbar("Дерево порвалось");
|
||||||
console.log(info);
|
devlog(info);
|
||||||
console.log(error);
|
devlog(error);
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
368
src/pages/Questions/BranchingMap/CsNodeButtons.tsx
Normal file
@ -1,35 +1,34 @@
|
|||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import { useEffect, useRef, useLayoutEffect } from "react";
|
|
||||||
import {
|
import {
|
||||||
deleteQuestion,
|
|
||||||
clearRuleForAll,
|
clearRuleForAll,
|
||||||
updateQuestion,
|
|
||||||
createResult,
|
createResult,
|
||||||
|
updateQuestion,
|
||||||
} from "@root/questions/actions";
|
} from "@root/questions/actions";
|
||||||
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
|
|
||||||
import { updateRootContentId } from "@root/quizes/actions";
|
import { updateRootContentId } from "@root/quizes/actions";
|
||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import { useQuestionsStore } from "@root/questions/store";
|
import { useQuizStore } from "@root/quizes/store";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import {
|
||||||
import { useUiTools } from "@root/uiTools/store";
|
|
||||||
|
|
||||||
interface Props {
|
|
||||||
setOpenedModalQuestions: (open: boolean) => void;
|
|
||||||
modalQuestionTargetContentId: string;
|
|
||||||
}
|
|
||||||
export const FirstNodeField = ({
|
|
||||||
setOpenedModalQuestions,
|
setOpenedModalQuestions,
|
||||||
modalQuestionTargetContentId,
|
updateOpenedModalSettingsId,
|
||||||
}: Props) => {
|
} 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 quiz = useCurrentQuiz();
|
||||||
|
const modalQuestionTargetContentId = useUiTools(
|
||||||
|
(state) => state.modalQuestionTargetContentId,
|
||||||
|
);
|
||||||
|
|
||||||
useLayoutEffect(() => {
|
useLayoutEffect(() => {
|
||||||
|
if (!quiz) return;
|
||||||
|
|
||||||
updateOpenedModalSettingsId();
|
updateOpenedModalSettingsId();
|
||||||
updateRootContentId(quiz.id, "");
|
updateRootContentId(quiz.id, "");
|
||||||
clearRuleForAll();
|
clearRuleForAll();
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
const { questions } = useQuestionsStore();
|
|
||||||
const { dragQuestionContentId } = useUiTools();
|
const { dragQuestionContentId } = useUiTools();
|
||||||
const Container = useRef<HTMLDivElement | null>(null);
|
const Container = useRef<HTMLDivElement | null>(null);
|
||||||
|
|
||||||
@ -43,18 +42,18 @@ export const FirstNodeField = ({
|
|||||||
dragQuestionContentId,
|
dragQuestionContentId,
|
||||||
(question) => (question.content.rule.parentId = "root"),
|
(question) => (question.content.rule.parentId = "root"),
|
||||||
);
|
);
|
||||||
createResult(quiz?.backendId, dragQuestionContentId);
|
createResult(useQuizStore.getState().editQuizId, dragQuestionContentId);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
enqueueSnackbar("Нет информации о взятом опросе");
|
enqueueSnackbar("Нет информации о взятом опроснике");
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
Container.current?.addEventListener("mouseup", newRootNode);
|
Container.current?.addEventListener("pointerup", newRootNode);
|
||||||
Container.current?.addEventListener("click", modalOpen);
|
Container.current?.addEventListener("click", modalOpen);
|
||||||
return () => {
|
return () => {
|
||||||
Container.current?.removeEventListener("mouseup", newRootNode);
|
Container.current?.removeEventListener("pointerup", newRootNode);
|
||||||
Container.current?.removeEventListener("click", modalOpen);
|
Container.current?.removeEventListener("click", modalOpen);
|
||||||
};
|
};
|
||||||
}, [dragQuestionContentId]);
|
}, [dragQuestionContentId]);
|
||||||
@ -67,10 +66,13 @@ export const FirstNodeField = ({
|
|||||||
modalQuestionTargetContentId,
|
modalQuestionTargetContentId,
|
||||||
(question) => (question.content.rule.parentId = "root"),
|
(question) => (question.content.rule.parentId = "root"),
|
||||||
);
|
);
|
||||||
createResult(quiz?.backendId, modalQuestionTargetContentId);
|
createResult(
|
||||||
|
useQuizStore.getState().editQuizId,
|
||||||
|
modalQuestionTargetContentId,
|
||||||
|
);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
enqueueSnackbar("Нет информации о взятом опросе");
|
enqueueSnackbar("Нет информации о взятом опроснике");
|
||||||
}
|
}
|
||||||
}, [modalQuestionTargetContentId]);
|
}, [modalQuestionTargetContentId]);
|
||||||
|
|
||||||
|
@ -1,34 +1,73 @@
|
|||||||
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
import { QuizQuestionResult } from "@model/questionTypes/result";
|
||||||
import { nameCutter } from "./nameCutter";
|
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: {
|
data: {
|
||||||
|
isRoot: boolean;
|
||||||
id: string;
|
id: string;
|
||||||
label: string;
|
label: string;
|
||||||
parent?: string;
|
parent?: string;
|
||||||
};
|
};
|
||||||
|
classes: string;
|
||||||
}
|
}
|
||||||
interface Edges {
|
|
||||||
|
export interface Edge {
|
||||||
data: {
|
data: {
|
||||||
source: string;
|
source: string;
|
||||||
target: 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[]) => {
|
export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => {
|
||||||
const nodes: Nodes[] = [];
|
const nodes: Node[] = [];
|
||||||
const edges: Edges[] = [];
|
const edges: Edge[] = [];
|
||||||
questions.forEach((question) => {
|
questions.forEach((question) => {
|
||||||
if (question.content.rule.parentId) {
|
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({
|
nodes.push({
|
||||||
data: {
|
data: {
|
||||||
|
isRoot: question.content.rule.parentId === "root",
|
||||||
id: question.content.id,
|
id: question.content.id,
|
||||||
label:
|
label,
|
||||||
question.title === "" || question.title === " "
|
|
||||||
? "noname №" + question.page
|
|
||||||
: nameCutter(question.title),
|
|
||||||
parentType: question.content.rule.parentId,
|
|
||||||
},
|
},
|
||||||
|
classes: "multiline-auto",
|
||||||
});
|
});
|
||||||
// nodes.push({
|
// nodes.push({
|
||||||
// data: {
|
// data: {
|
||||||
@ -48,3 +87,260 @@ export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => {
|
|||||||
});
|
});
|
||||||
return [...nodes, ...edges];
|
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 {
|
||||||
|
cleardragQuestionContentId,
|
||||||
import type { MutableRefObject } from "react";
|
setModalQuestionParentContentId,
|
||||||
|
setOpenedModalQuestions,
|
||||||
|
updateDeleteId,
|
||||||
|
updateOpenedModalSettingsId,
|
||||||
|
} from "@root/uiTools/actions";
|
||||||
import type {
|
import type {
|
||||||
PresetLayoutOptions,
|
|
||||||
LayoutEventObject,
|
|
||||||
NodeSingular,
|
|
||||||
AbstractEventObject,
|
AbstractEventObject,
|
||||||
|
Core,
|
||||||
|
NodeSingular,
|
||||||
|
SingularData,
|
||||||
} from "cytoscape";
|
} from "cytoscape";
|
||||||
import { getQuestionByContentId } from "@root/questions/actions";
|
import { getPopperInstance } from "cytoscape-popper";
|
||||||
|
import { useCallback, type MutableRefObject, useRef } from "react";
|
||||||
type usePopperArgs = {
|
import { addNode } from "../helper";
|
||||||
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;
|
|
||||||
};
|
|
||||||
|
|
||||||
type PopperItem = {
|
type PopperItem = {
|
||||||
id: () => string;
|
id: () => string;
|
||||||
@ -37,231 +32,175 @@ type PopperConfig = {
|
|||||||
content: (items: PopperItem[]) => void;
|
content: (items: PopperItem[]) => void;
|
||||||
};
|
};
|
||||||
|
|
||||||
type Popper = {
|
type PopperInstance = ReturnType<getPopperInstance<SingularData>>;
|
||||||
update: () => Promise<void>;
|
|
||||||
setOptions: (modifiers: { modifiers?: Modifier[] }) => void;
|
|
||||||
};
|
|
||||||
|
|
||||||
type NodeSingularWithPopper = NodeSingular & {
|
type NodeSingularWithPopper = NodeSingular & {
|
||||||
popper: (config: PopperConfig) => Popper;
|
popper: (config: PopperConfig) => PopperInstance;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
/** @deprecated */
|
||||||
export const usePopper = ({
|
export const usePopper = ({
|
||||||
layoutsContainer,
|
cyRef,
|
||||||
plusesContainer,
|
}: {
|
||||||
crossesContainer,
|
cyRef: MutableRefObject<Core | null>;
|
||||||
gearsContainer,
|
}) => {
|
||||||
setModalQuestionParentContentId,
|
const popperContainerRef = useRef<HTMLDivElement | null>(null);
|
||||||
setOpenedModalQuestions,
|
const popperInstancesRef = useRef<PopperInstance[]>([]);
|
||||||
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();
|
|
||||||
};
|
|
||||||
|
|
||||||
const initialPopperIcons = ({ cy }: LayoutEventObject) => {
|
const removeAllPoppers = useCallback(() => {
|
||||||
const container =
|
cyRef.current?.removeListener("zoom render");
|
||||||
(document.body.querySelector(
|
|
||||||
".__________cytoscape_container",
|
popperInstancesRef.current.forEach((p) => p.destroy());
|
||||||
) as HTMLDivElement) || null;
|
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) {
|
if (!container) {
|
||||||
|
console.warn("Cannot create popper container");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
container.style.overflow = "hidden";
|
if (!popperContainerRef.current) {
|
||||||
|
popperContainerRef.current = document.createElement("div");
|
||||||
if (!plusesContainer.current) {
|
popperContainerRef.current.setAttribute("id", "poppers-container");
|
||||||
plusesContainer.current = document.createElement("div");
|
container.append(popperContainerRef.current);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
cy?.removeAllListeners();
|
cy.nodes().forEach((item) => {
|
||||||
|
const node = item as NodeSingularWithPopper;
|
||||||
|
|
||||||
cy
|
const layoutsPopper = node.popper({
|
||||||
.nodes()
|
popper: {
|
||||||
.toArray()
|
placement: "left",
|
||||||
?.forEach((item) => {
|
modifiers: [{ name: "flip", options: { boundary: node } }],
|
||||||
const node = item as NodeSingularWithPopper;
|
},
|
||||||
|
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: {
|
popper: {
|
||||||
placement: "left",
|
placement: "left",
|
||||||
modifiers: [{ name: "flip", options: { boundary: node } }],
|
modifiers: [{ name: "flip", options: { boundary: node } }],
|
||||||
},
|
},
|
||||||
content: ([item]) => {
|
content: ([item]) => {
|
||||||
const itemId = item.id();
|
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) {
|
if (itemElement) {
|
||||||
return itemElement;
|
return itemElement;
|
||||||
}
|
}
|
||||||
|
|
||||||
const layoutElement = document.createElement("div");
|
const gearElement = document.createElement("div");
|
||||||
layoutElement.style.zIndex = "0";
|
gearElement.classList.add("popper-gear");
|
||||||
layoutElement.classList.add("popper-layout");
|
gearElement.setAttribute("data-id", item.id());
|
||||||
layoutElement.setAttribute("data-id", item.id());
|
gearElement.style.zIndex = "1";
|
||||||
layoutElement.addEventListener("mouseup", () => {
|
popperContainerRef.current?.appendChild(gearElement);
|
||||||
//Узнаём грани, идущие от этой ноды
|
gearElement.addEventListener("pointerup", () => {
|
||||||
setModalQuestionParentContentId(item.id());
|
updateOpenedModalSettingsId(item.id());
|
||||||
setOpenedModalQuestions(true);
|
|
||||||
});
|
});
|
||||||
layoutElement.addEventListener("touchstart", () => {
|
|
||||||
//Узнаём грани, идущие от этой ноды
|
|
||||||
setModalQuestionParentContentId(item.id());
|
|
||||||
setOpenedModalQuestions(true);
|
|
||||||
});
|
|
||||||
layoutsContainer.current?.appendChild(layoutElement);
|
|
||||||
|
|
||||||
return layoutElement;
|
return gearElement;
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
popperInstancesRef.current.push(gearsPopper);
|
||||||
|
}
|
||||||
|
|
||||||
const plusesPopper = node.popper({
|
const onZoom = (event: AbstractEventObject) => {
|
||||||
popper: {
|
const zoom = event.cy.zoom();
|
||||||
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();
|
|
||||||
|
|
||||||
crossesPopper.setOptions({
|
crossesPopper.setOptions({
|
||||||
modifiers: [
|
modifiers: [
|
||||||
@ -279,7 +218,7 @@ export const usePopper = ({
|
|||||||
plusesPopper.setOptions({
|
plusesPopper.setOptions({
|
||||||
modifiers: [
|
modifiers: [
|
||||||
{ name: "flip", options: { boundary: node } },
|
{ name: "flip", options: { boundary: node } },
|
||||||
{ name: "offset", options: { offset: [0, 0 * zoom] } },
|
{ name: "offset", options: { offset: [0, 0] } },
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
gearsPopper?.setOptions({
|
gearsPopper?.setOptions({
|
||||||
@ -289,16 +228,16 @@ export const usePopper = ({
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
layoutsContainer.current
|
popperContainerRef.current
|
||||||
?.querySelectorAll("#popper-layouts > .popper-layout")
|
?.querySelectorAll(".popper-layout")
|
||||||
.forEach((item) => {
|
.forEach((item) => {
|
||||||
const element = item as HTMLDivElement;
|
const element = item as HTMLDivElement;
|
||||||
element.style.width = `${130 * zoom}px`;
|
element.style.width = `${130 * zoom}px`;
|
||||||
element.style.height = `${130 * zoom}px`;
|
element.style.height = `${130 * zoom}px`;
|
||||||
});
|
});
|
||||||
|
|
||||||
plusesContainer.current
|
popperContainerRef.current
|
||||||
?.querySelectorAll("#popper-pluses > .popper-plus")
|
?.querySelectorAll(".popper-plus")
|
||||||
.forEach((item) => {
|
.forEach((item) => {
|
||||||
const element = item as HTMLDivElement;
|
const element = item as HTMLDivElement;
|
||||||
element.style.width = `${40 * zoom}px`;
|
element.style.width = `${40 * zoom}px`;
|
||||||
@ -307,8 +246,8 @@ export const usePopper = ({
|
|||||||
element.style.borderRadius = `${6 * zoom}px`;
|
element.style.borderRadius = `${6 * zoom}px`;
|
||||||
});
|
});
|
||||||
|
|
||||||
crossesContainer.current
|
popperContainerRef.current
|
||||||
?.querySelectorAll("#popper-crosses > .popper-cross")
|
?.querySelectorAll(".popper-cross")
|
||||||
.forEach((item) => {
|
.forEach((item) => {
|
||||||
const element = item as HTMLDivElement;
|
const element = item as HTMLDivElement;
|
||||||
element.style.width = `${24 * zoom}px`;
|
element.style.width = `${24 * zoom}px`;
|
||||||
@ -317,188 +256,18 @@ export const usePopper = ({
|
|||||||
element.style.borderRadius = `${6 * zoom}px`;
|
element.style.borderRadius = `${6 * zoom}px`;
|
||||||
});
|
});
|
||||||
|
|
||||||
gearsContainer?.current
|
popperContainerRef?.current
|
||||||
?.querySelectorAll("#popper-gears > .popper-gear")
|
?.querySelectorAll(".popper-gear")
|
||||||
.forEach((item) => {
|
.forEach((item) => {
|
||||||
const element = item as HTMLDivElement;
|
const element = item as HTMLDivElement;
|
||||||
element.style.width = `${60 * zoom}px`;
|
element.style.width = `${60 * zoom}px`;
|
||||||
element.style.height = `${40 * zoom}px`;
|
element.style.height = `${40 * zoom}px`;
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
//node?.on("position", update);
|
cy.on("zoom render", onZoom);
|
||||||
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);
|
|
||||||
});
|
});
|
||||||
|
}, []);
|
||||||
|
|
||||||
initialPopperIcons(event);
|
return { removeAllPoppers, recreatePoppers };
|
||||||
};
|
|
||||||
|
|
||||||
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 };
|
|
||||||
};
|
};
|
||||||
|
@ -1,130 +1,34 @@
|
|||||||
|
import { devlog } from "@frontend/kitui";
|
||||||
|
import { QuizQuestionResult } from "@model/questionTypes/result";
|
||||||
import {
|
import {
|
||||||
deleteQuestion,
|
|
||||||
updateQuestion,
|
|
||||||
getQuestionByContentId,
|
|
||||||
clearRuleForAll,
|
clearRuleForAll,
|
||||||
createResult,
|
getQuestionByContentId,
|
||||||
|
updateQuestion,
|
||||||
} from "@root/questions/actions";
|
} from "@root/questions/actions";
|
||||||
import { useQuestionsStore } from "@root/questions/store";
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
|
||||||
import { updateRootContentId } from "@root/quizes/actions";
|
import { updateRootContentId } from "@root/quizes/actions";
|
||||||
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import type { MutableRefObject } from "react";
|
|
||||||
import type {
|
import type {
|
||||||
Core,
|
|
||||||
CollectionReturnValue,
|
CollectionReturnValue,
|
||||||
PresetLayoutOptions,
|
Core,
|
||||||
|
SingularElementArgument,
|
||||||
} from "cytoscape";
|
} from "cytoscape";
|
||||||
import type {
|
import type { MutableRefObject } from "react";
|
||||||
AnyTypedQuizQuestion,
|
import { clearDataAfterRemoveNode } from "../helper";
|
||||||
QuestionBranchingRule,
|
|
||||||
QuestionBranchingRuleMain,
|
|
||||||
} from "../../../../model/questionTypes/shared";
|
|
||||||
|
|
||||||
type UseRemoveNodeArgs = {
|
type UseRemoveNodeArgs = {
|
||||||
cyRef: MutableRefObject<Core | null>;
|
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 = ({
|
export const useRemoveNode = ({ cyRef }: UseRemoveNodeArgs) => {
|
||||||
cyRef,
|
|
||||||
layoutOptions,
|
|
||||||
layoutsContainer,
|
|
||||||
plusesContainer,
|
|
||||||
crossesContainer,
|
|
||||||
gearsContainer,
|
|
||||||
}: UseRemoveNodeArgs) => {
|
|
||||||
const { questions: trashQuestions } = useQuestionsStore();
|
const { questions: trashQuestions } = useQuestionsStore();
|
||||||
const quiz = useCurrentQuiz();
|
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 removeNode = (targetNodeContentId: string) => {
|
||||||
const deleteNodes: string[] = [];
|
const deleteNodes: string[] = [];
|
||||||
const deleteEdges: any = [];
|
|
||||||
const cy = cyRef?.current;
|
const cy = cyRef?.current;
|
||||||
|
|
||||||
const findChildrenToDelete = (node: CollectionReturnValue) => {
|
const deleteNodesRecursively = (node: CollectionReturnValue) => {
|
||||||
//Узнаём грани, идущие от этой ноды
|
//Узнаём грани, идущие от этой ноды
|
||||||
cy
|
cy
|
||||||
?.$('edge[source = "' + node.id() + '"]')
|
?.$('edge[source = "' + node.id() + '"]')
|
||||||
@ -132,20 +36,18 @@ export const useRemoveNode = ({
|
|||||||
.forEach((edge) => {
|
.forEach((edge) => {
|
||||||
const edgeData = edge.data();
|
const edgeData = edge.data();
|
||||||
|
|
||||||
//записываем id грани для дальнейшего удаления
|
|
||||||
deleteEdges.push(edge);
|
|
||||||
//ищем ноду на конце грани, записываем её ID для дальнейшего удаления
|
//ищем ноду на конце грани, записываем её ID для дальнейшего удаления
|
||||||
const targetNode = cy?.$("#" + edgeData.target);
|
const targetNode = cy?.$("#" + edgeData.target);
|
||||||
deleteNodes.push(targetNode.data().id);
|
deleteNodes.push(targetNode.data().id);
|
||||||
//вызываем функцию для анализа потомков уже у этой ноды
|
//вызываем функцию для анализа потомков уже у этой ноды
|
||||||
findChildrenToDelete(targetNode);
|
deleteNodesRecursively(targetNode);
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
const elementToDelete = cy?.getElementById(targetNodeContentId);
|
const elementToDelete = cy?.getElementById(targetNodeContentId);
|
||||||
|
|
||||||
if (elementToDelete) {
|
if (elementToDelete) {
|
||||||
findChildrenToDelete(elementToDelete);
|
deleteNodesRecursively(elementToDelete);
|
||||||
}
|
}
|
||||||
|
|
||||||
const targetQuestion = getQuestionByContentId(targetNodeContentId);
|
const targetQuestion = getQuestionByContentId(targetNodeContentId);
|
||||||
@ -155,7 +57,7 @@ export const useRemoveNode = ({
|
|||||||
targetQuestion.content.rule.parentId === "root" &&
|
targetQuestion.content.rule.parentId === "root" &&
|
||||||
quiz
|
quiz
|
||||||
) {
|
) {
|
||||||
updateRootContentId(quiz?.id, "");
|
updateRootContentId(quiz.id, "");
|
||||||
updateQuestion(targetNodeContentId, (question) => {
|
updateQuestion(targetNodeContentId, (question) => {
|
||||||
question.content.rule.parentId = "";
|
question.content.rule.parentId = "";
|
||||||
question.content.rule.main = [];
|
question.content.rule.main = [];
|
||||||
@ -173,16 +75,14 @@ export const useRemoveNode = ({
|
|||||||
quiz &&
|
quiz &&
|
||||||
cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0
|
cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0
|
||||||
) {
|
) {
|
||||||
|
devlog(parentQuestionContentId);
|
||||||
//createFrontResult(quiz.backendId, parentQuestionContentId);
|
//createFrontResult(quiz.backendId, parentQuestionContentId);
|
||||||
}
|
}
|
||||||
clearDataAfterRemoveNode({
|
clearDataAfterRemoveNode({
|
||||||
|
trashQuestions,
|
||||||
targetQuestionContentId: targetNodeContentId,
|
targetQuestionContentId: targetNodeContentId,
|
||||||
parentQuestionContentId,
|
parentQuestionContentId,
|
||||||
});
|
});
|
||||||
cy
|
|
||||||
?.remove(cy?.$("#" + targetNodeContentId))
|
|
||||||
.layout(layoutOptions)
|
|
||||||
.run();
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -190,8 +90,6 @@ export const useRemoveNode = ({
|
|||||||
|
|
||||||
deleteNodes.forEach((nodeId) => {
|
deleteNodes.forEach((nodeId) => {
|
||||||
//Ноды
|
//Ноды
|
||||||
cy?.remove(cy?.$("#" + nodeId));
|
|
||||||
removeButtons(nodeId);
|
|
||||||
updateQuestion(nodeId, (question) => {
|
updateQuestion(nodeId, (question) => {
|
||||||
question.content.rule.parentId = "";
|
question.content.rule.parentId = "";
|
||||||
question.content.rule.main = [];
|
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 всех потомков неактивными
|
//делаем result всех потомков неактивными
|
||||||
trashQuestions.forEach((qr) => {
|
trashQuestions.forEach((qr) => {
|
||||||
if (
|
if (
|
||||||
@ -217,7 +106,7 @@ export const useRemoveNode = ({
|
|||||||
(targetQuestion?.type &&
|
(targetQuestion?.type &&
|
||||||
qr.content.rule.parentId === targetQuestion.content.id))
|
qr.content.rule.parentId === targetQuestion.content.id))
|
||||||
) {
|
) {
|
||||||
updateQuestion(qr.content.id, (q) => {
|
updateQuestion<QuizQuestionResult>(qr.content.id, (q) => {
|
||||||
q.content.usage = false;
|
q.content.usage = false;
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -1,20 +1,15 @@
|
|||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import { FirstNodeField } from "./FirstNodeField";
|
|
||||||
import CsComponent from "./CsComponent";
|
|
||||||
import { useCurrentQuiz } from "@root/quizes/hooks";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import { useEffect, useState } from "react";
|
|
||||||
import { BranchingQuestionsModal } from "../BranchingQuestionsModal";
|
|
||||||
import { useUiTools } from "@root/uiTools/store";
|
import { useUiTools } from "@root/uiTools/store";
|
||||||
|
import { BranchingQuestionsModal } from "../BranchingQuestionsModal";
|
||||||
|
import CsComponent from "./CsComponent";
|
||||||
|
import { FirstNodeField } from "./FirstNodeField";
|
||||||
|
|
||||||
export const BranchingMap = () => {
|
export const BranchingMap = () => {
|
||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
const { dragQuestionContentId } = useUiTools();
|
const dragQuestionContentId = useUiTools(
|
||||||
const [modalQuestionParentContentId, setModalQuestionParentContentId] =
|
(state) => state.dragQuestionContentId,
|
||||||
useState<string>("");
|
);
|
||||||
const [modalQuestionTargetContentId, setModalQuestionTargetContentId] =
|
|
||||||
useState<string>("");
|
|
||||||
const [openedModalQuestions, setOpenedModalQuestions] =
|
|
||||||
useState<boolean>(false);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Box
|
<Box
|
||||||
@ -30,25 +25,8 @@ export const BranchingMap = () => {
|
|||||||
border: dragQuestionContentId === null ? "none" : "#7e2aea 2px dashed",
|
border: dragQuestionContentId === null ? "none" : "#7e2aea 2px dashed",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{quiz?.config.haveRoot ? (
|
{quiz?.config.haveRoot ? <CsComponent /> : <FirstNodeField />}
|
||||||
<CsComponent
|
<BranchingQuestionsModal />
|
||||||
modalQuestionParentContentId={modalQuestionParentContentId}
|
|
||||||
modalQuestionTargetContentId={modalQuestionTargetContentId}
|
|
||||||
setOpenedModalQuestions={setOpenedModalQuestions}
|
|
||||||
setModalQuestionParentContentId={setModalQuestionParentContentId}
|
|
||||||
setModalQuestionTargetContentId={setModalQuestionTargetContentId}
|
|
||||||
/>
|
|
||||||
) : (
|
|
||||||
<FirstNodeField
|
|
||||||
setOpenedModalQuestions={setOpenedModalQuestions}
|
|
||||||
modalQuestionTargetContentId={modalQuestionTargetContentId}
|
|
||||||
/>
|
|
||||||
)}
|
|
||||||
<BranchingQuestionsModal
|
|
||||||
openedModalQuestions={openedModalQuestions}
|
|
||||||
setOpenedModalQuestions={setOpenedModalQuestions}
|
|
||||||
setModalQuestionTargetContentId={setModalQuestionTargetContentId}
|
|
||||||
/>
|
|
||||||
</Box>
|
</Box>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
#popper-pluses > .popper-plus {
|
.popper-plus {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -9,13 +9,13 @@
|
|||||||
font-size: 0px;
|
font-size: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#popper-pluses > .popper-plus::before {
|
.popper-plus::before {
|
||||||
content: "+";
|
content: "+";
|
||||||
color: rgba(154, 154, 175, 0.5);
|
color: rgba(154, 154, 175, 0.5);
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#popper-crosses > .popper-cross {
|
.popper-cross {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
@ -25,14 +25,14 @@
|
|||||||
font-size: 0px;
|
font-size: 0px;
|
||||||
}
|
}
|
||||||
|
|
||||||
#popper-crosses > .popper-cross::before {
|
.popper-cross::before {
|
||||||
content: "+";
|
content: "+";
|
||||||
transform: rotate(45deg);
|
transform: rotate(45deg);
|
||||||
color: #fff;
|
color: #fff;
|
||||||
font-size: inherit;
|
font-size: inherit;
|
||||||
}
|
}
|
||||||
|
|
||||||
#popper-gears > .popper-gear {
|
.popper-gear {
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
@ -1,22 +1,20 @@
|
|||||||
import { Box, Modal, Button, Typography } from "@mui/material";
|
import { Box, Modal, Button, Typography } from "@mui/material";
|
||||||
import { useQuestionsStore } from "@root/questions/store";
|
import { useQuestionsStore } from "@root/questions/store";
|
||||||
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
|
||||||
|
import { useUiTools } from "@root/uiTools/store";
|
||||||
interface Props {
|
import {
|
||||||
openedModalQuestions: boolean;
|
|
||||||
setModalQuestionTargetContentId: (contentId: string) => void;
|
|
||||||
setOpenedModalQuestions: (open: boolean) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export const BranchingQuestionsModal = ({
|
|
||||||
openedModalQuestions,
|
|
||||||
setOpenedModalQuestions,
|
|
||||||
setModalQuestionTargetContentId,
|
setModalQuestionTargetContentId,
|
||||||
}: Props) => {
|
setOpenedModalQuestions,
|
||||||
|
} from "@root/uiTools/actions";
|
||||||
|
|
||||||
|
export const BranchingQuestionsModal = () => {
|
||||||
const trashQuestions = useQuestionsStore().questions;
|
const trashQuestions = useQuestionsStore().questions;
|
||||||
const questions = trashQuestions.filter(
|
const questions = trashQuestions.filter(
|
||||||
(question) => question.type !== "result",
|
(question) => question.type !== "result",
|
||||||
);
|
);
|
||||||
|
const openedModalQuestions = useUiTools(
|
||||||
|
(state) => state.openedModalQuestions,
|
||||||
|
);
|
||||||
|
|
||||||
const handleClose = () => {
|
const handleClose = () => {
|
||||||
setOpenedModalQuestions(false);
|
setOpenedModalQuestions(false);
|
||||||
|
@ -50,6 +50,7 @@ export const ChooseAnswerModal = ({
|
|||||||
open={open}
|
open={open}
|
||||||
anchorEl={anchorRef.current}
|
anchorEl={anchorRef.current}
|
||||||
transition
|
transition
|
||||||
|
sx={{ zIndex: 1 }}
|
||||||
>
|
>
|
||||||
{({ TransitionProps }) => (
|
{({ TransitionProps }) => (
|
||||||
<Grow {...TransitionProps}>
|
<Grow {...TransitionProps}>
|
||||||
|
@ -47,6 +47,7 @@ export const ChooseAnswerModal = ({
|
|||||||
open={open}
|
open={open}
|
||||||
anchorEl={anchorRef.current}
|
anchorEl={anchorRef.current}
|
||||||
transition
|
transition
|
||||||
|
sx={{ zIndex: 1 }}
|
||||||
>
|
>
|
||||||
{({ TransitionProps }) => (
|
{({ TransitionProps }) => (
|
||||||
<Grow {...TransitionProps}>
|
<Grow {...TransitionProps}>
|
||||||
|
@ -20,6 +20,7 @@ import {
|
|||||||
InputAdornment,
|
InputAdornment,
|
||||||
Paper,
|
Paper,
|
||||||
TextField,
|
TextField,
|
||||||
|
Typography,
|
||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
@ -65,7 +66,9 @@ export default function QuestionsPageCard({
|
|||||||
questionIndex,
|
questionIndex,
|
||||||
draggableProps,
|
draggableProps,
|
||||||
}: Props) {
|
}: Props) {
|
||||||
|
const maxLengthTextField = 225;
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
|
const [isTextFieldtActive, setIsTextFieldtActive] = useState(false);
|
||||||
const anchorRef = useRef(null);
|
const anchorRef = useRef(null);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||||
@ -80,6 +83,14 @@ export default function QuestionsPageCard({
|
|||||||
});
|
});
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
|
const handleInputFocus = () => {
|
||||||
|
setIsTextFieldtActive(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleInputBlur = (event: React.FocusEvent<HTMLInputElement>) => {
|
||||||
|
setIsTextFieldtActive(false);
|
||||||
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Paper
|
<Paper
|
||||||
@ -128,6 +139,8 @@ export default function QuestionsPageCard({
|
|||||||
if ((target.value, toString().length <= 225))
|
if ((target.value, toString().length <= 225))
|
||||||
setTitle(target.value);
|
setTitle(target.value);
|
||||||
}}
|
}}
|
||||||
|
onFocus={handleInputFocus}
|
||||||
|
onBlur={handleInputBlur}
|
||||||
sx={{
|
sx={{
|
||||||
width: "100%",
|
width: "100%",
|
||||||
margin: isMobile ? "10px 0" : 0,
|
margin: isMobile ? "10px 0" : 0,
|
||||||
@ -148,6 +161,9 @@ export default function QuestionsPageCard({
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
inputProps={{
|
||||||
|
maxLength: maxLengthTextField,
|
||||||
|
}}
|
||||||
InputProps={{
|
InputProps={{
|
||||||
startAdornment: (
|
startAdornment: (
|
||||||
<Box>
|
<Box>
|
||||||
@ -168,6 +184,27 @@ export default function QuestionsPageCard({
|
|||||||
/>
|
/>
|
||||||
</Box>
|
</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>
|
</FormControl>
|
||||||
|
@ -24,7 +24,6 @@ export default function SettingOptionsAndPict({
|
|||||||
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
|
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(680));
|
const isMobile = useMediaQuery(theme.breakpoints.down(680));
|
||||||
|
|
||||||
console.log("question.content.replText ", question.content.replText);
|
|
||||||
const setReplText = useDebouncedCallback((replText) => {
|
const setReplText = useDebouncedCallback((replText) => {
|
||||||
updateQuestion(question.id, (question) => {
|
updateQuestion(question.id, (question) => {
|
||||||
if (question.type !== "varimg") return;
|
if (question.type !== "varimg") return;
|
||||||
|
@ -59,7 +59,6 @@ export default function EditPage({
|
|||||||
const quiz = useCurrentQuiz();
|
const quiz = useCurrentQuiz();
|
||||||
const { editQuizId } = useQuizStore();
|
const { editQuizId } = useQuizStore();
|
||||||
const { questions } = useQuestionsStore();
|
const { questions } = useQuestionsStore();
|
||||||
console.log(questions);
|
|
||||||
const { whyCantCreatePublic, showConfirmLeaveModal, nextStep } = useUiTools();
|
const { whyCantCreatePublic, showConfirmLeaveModal, nextStep } = useUiTools();
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const navigate = useNavigate();
|
const navigate = useNavigate();
|
||||||
@ -114,7 +113,6 @@ export default function EditPage({
|
|||||||
const isConditionMet =
|
const isConditionMet =
|
||||||
[1].includes(currentStep) && quizConfig.type !== "form";
|
[1].includes(currentStep) && quizConfig.type !== "form";
|
||||||
|
|
||||||
console.log("quiz", quiz);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Box
|
<Box
|
||||||
|
@ -36,14 +36,16 @@ export const ModalInfoWhyCantCreate = () => {
|
|||||||
overflow: "auto",
|
overflow: "auto",
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{Object.values(whyCantCreatePublic).map((data) => {
|
{Object.entries(whyCantCreatePublic).map(([id, data]) => {
|
||||||
return (
|
return (
|
||||||
<Box>
|
<Box key={id}>
|
||||||
<Typography color="#7e2aea">
|
<Typography color="#7e2aea">
|
||||||
{data.name === "quiz" ? "У квиза" : `У вопроса "${data.name}"`}
|
{data.name === "quiz" ? "У квиза" : `У вопроса "${data.name}"`}
|
||||||
</Typography>
|
</Typography>
|
||||||
{data.problems.map((problem) => (
|
{data.problems.map((problem, index) => (
|
||||||
<Typography p="5px 0">{problem}</Typography>
|
<Typography key={index} p="5px 0">
|
||||||
|
{problem}
|
||||||
|
</Typography>
|
||||||
))}
|
))}
|
||||||
<Divider />
|
<Divider />
|
||||||
</Box>
|
</Box>
|
||||||
|
@ -26,8 +26,10 @@ import { useUiTools } from "../uiTools/store";
|
|||||||
import { withErrorBoundary } from "react-error-boundary";
|
import { withErrorBoundary } from "react-error-boundary";
|
||||||
import { QuizQuestionResult } from "@model/questionTypes/result";
|
import { QuizQuestionResult } from "@model/questionTypes/result";
|
||||||
import { replaceEmptyLinesToSpace } from "../../utils/replaceEmptyLinesToSpace";
|
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(
|
setProducedState(
|
||||||
(state) => {
|
(state) => {
|
||||||
const untypedResultQuestions = state.questions.filter(
|
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 () => {
|
requestQueue.enqueue(async () => {
|
||||||
if (!quizId || !parentContentId) {
|
if (!quizId || !parentContentId) {
|
||||||
console.error(
|
console.error(
|
||||||
|
@ -43,3 +43,12 @@ export const updateSomeWorkBackend = (someWorkBackend: boolean) =>
|
|||||||
|
|
||||||
export const updateNextStep = (nextStep: number) =>
|
export const updateNextStep = (nextStep: number) =>
|
||||||
useUiTools.setState({ nextStep });
|
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;
|
showConfirmLeaveModal: boolean;
|
||||||
someWorkBackend: boolean;
|
someWorkBackend: boolean;
|
||||||
nextStep: number;
|
nextStep: number;
|
||||||
|
modalQuestionParentContentId: string;
|
||||||
|
modalQuestionTargetContentId: string;
|
||||||
|
openedModalQuestions: boolean;
|
||||||
};
|
};
|
||||||
|
|
||||||
export type WhyCantCreatePublic = {
|
export type WhyCantCreatePublic = {
|
||||||
@ -32,6 +35,9 @@ const initialState: UiTools = {
|
|||||||
showConfirmLeaveModal: false,
|
showConfirmLeaveModal: false,
|
||||||
someWorkBackend: false,
|
someWorkBackend: false,
|
||||||
nextStep: -1,
|
nextStep: -1,
|
||||||
|
modalQuestionParentContentId: "",
|
||||||
|
modalQuestionTargetContentId: "",
|
||||||
|
openedModalQuestions: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
export const useUiTools = create<UiTools>()(
|
export const useUiTools = create<UiTools>()(
|
||||||
|