Merge remote-tracking branch 'origin/dev' into video-upload-modal

This commit is contained in:
nflnkr 2024-06-21 12:02:45 +03:00
commit f8a1a21444
134 changed files with 2065 additions and 25303 deletions

21621
package-lock.json generated

File diff suppressed because it is too large Load Diff

@ -7,7 +7,7 @@
"@emotion/react": "^11.10.5",
"@emotion/styled": "^11.10.5",
"@frontend/kitui": "^1.0.82",
"@frontend/squzanswerer": "^1.0.45",
"@frontend/squzanswerer": "^1.0.51",
"@mui/icons-material": "^5.10.14",
"@mui/material": "^5.10.14",
"@mui/x-charts": "^6.19.5",

@ -1,3 +1,4 @@
import { QuestionKeys } from "@/pages/IntegrationsPage/IntegrationsModal/types";
import { makeRequest } from "@api/makeRequest";
import { parseAxiosError } from "@utils/parse-error";
@ -11,24 +12,18 @@ const API_URL = `${process.env.REACT_APP_DOMAIN}/squiz/amocrm`;
// получение информации об аккаунте
export type AccountResponse = {
ID: number;
AmoUserID: number;
AccountID: string;
AmoID: number;
Name: string;
Email: string;
Role: string;
Group: number;
Deleted: boolean;
CreatedAt: number;
Subdomain: string;
Amoiserid: number;
Country: string;
id: number;
accountID: string;
amoID: number;
name: string;
deleted: boolean;
createdAt: string;
subdomain: string;
country: string;
driveURL: string;
};
export const getAccount = async (): Promise<
[AccountResponse | null, string?]
> => {
export const getAccount = async (): Promise<[AccountResponse | null, string?]> => {
try {
const response = await makeRequest<void, AccountResponse>({
method: "GET",
@ -98,10 +93,7 @@ export type TagsResponse = {
items: Tag[];
};
export const getTags = async ({
page,
size,
}: PaginationRequest): Promise<[TagsResponse | null, string?]> => {
export const getTags = async ({ page, size }: PaginationRequest): Promise<[TagsResponse | null, string?]> => {
try {
const tagsResponse = await makeRequest<PaginationRequest, TagsResponse>({
method: "GET",
@ -117,18 +109,18 @@ export const getTags = async ({
//получение списка пользователей
export type User = {
ID: number;
AccountID: string;
AmoID: number;
Name: string;
Email: string;
Role: string;
Group: number;
Deleted: boolean;
CreatedAt: number;
Subdomain: string;
Amoiserid: number;
Country: string;
id: number;
amoID: number;
name: string;
email: string;
role: number;
group: number;
deleted: boolean;
createdAt: string;
amoUserID: number;
// Subdomain: string;
// AccountID: string;
};
export type UsersResponse = {
@ -136,10 +128,7 @@ export type UsersResponse = {
items: User[];
};
export const getUsers = async ({
page,
size,
}: PaginationRequest): Promise<[UsersResponse | null, string?]> => {
export const getUsers = async ({ page, size }: PaginationRequest): Promise<[UsersResponse | null, string?]> => {
try {
const usersResponse = await makeRequest<PaginationRequest, UsersResponse>({
method: "GET",
@ -174,14 +163,9 @@ export const getSteps = async ({
page,
size,
pipelineId,
}: PaginationRequest & { pipelineId: number }): Promise<
[StepsResponse | null, string?]
> => {
}: PaginationRequest & { pipelineId: number }): Promise<[StepsResponse | null, string?]> => {
try {
const stepsResponse = await makeRequest<
PaginationRequest & { pipelineId: number },
StepsResponse
>({
const stepsResponse = await makeRequest<PaginationRequest & { pipelineId: number }, StepsResponse>({
method: "GET",
url: `${API_URL}/steps?page=${page}&size=${size}&pipelineID=${pipelineId}`,
});
@ -209,15 +193,9 @@ export type PipelinesResponse = {
items: Pipeline[];
};
export const getPipelines = async ({
page,
size,
}: PaginationRequest): Promise<[PipelinesResponse | null, string?]> => {
export const getPipelines = async ({ page, size }: PaginationRequest): Promise<[PipelinesResponse | null, string?]> => {
try {
const pipelinesResponse = await makeRequest<
PaginationRequest,
PipelinesResponse
>({
const pipelinesResponse = await makeRequest<PaginationRequest, PipelinesResponse>({
method: "GET",
url: `${API_URL}/pipelines?page=${page}&size=${size}`,
});
@ -229,28 +207,23 @@ export const getPipelines = async ({
};
//получение настроек интеграции
export type QuestionID = Record<string, number>;
export type IntegrationRules = {
ID: number;
AccountID: number;
QuizID: number;
PerformerID: number;
PipelineID: number;
StepID: number;
UTMs: number[];
FieldsRule: {
Lead: { QuestionID: number }[];
Contact: { ContactRuleMap: string }[];
Company: { QuestionID: number }[];
Customer: { QuestionID: number }[];
PerformerID?: number;
FieldsRule: FieldsRule;
TagsToAdd: {
Lead: number[] | null;
Contact: number[] | null;
Company: number[] | null;
Customer: number[] | null;
};
Deleted: boolean;
CreatedAt: number;
};
export type FieldsRule = Record<Partial<QuestionKeys>, null | [{ QuestionID: QuestionID }]>;
export const getIntegrationRules = async (
quizID: string,
): Promise<[IntegrationRules | null, string?]> => {
export const getIntegrationRules = async (quizID: string): Promise<[IntegrationRules | null, string?]> => {
try {
const settingsResponse = await makeRequest<void, IntegrationRules>({
method: "GET",
@ -271,7 +244,7 @@ export type IntegrationRulesUpdate = {
PipelineID: number;
StepID: number;
Utms: number[];
Fieldsrule: {
FieldsRule: {
Lead: { QuestionID: number }[];
Contact: { ContactRuleMap: string }[];
Company: { QuestionID: number }[];
@ -281,7 +254,7 @@ export type IntegrationRulesUpdate = {
export const setIntegrationRules = async (
quizID: string,
settings: IntegrationRulesUpdate,
settings: IntegrationRulesUpdate
): Promise<[string | null, string?]> => {
try {
const updateResponse = await makeRequest<IntegrationRulesUpdate, string>({
@ -297,7 +270,7 @@ export const setIntegrationRules = async (
};
export const updateIntegrationRules = async (
quizID: string,
settings: IntegrationRulesUpdate,
settings: IntegrationRulesUpdate
): Promise<[string | null, string?]> => {
try {
const updateResponse = await makeRequest<IntegrationRulesUpdate, string>({
@ -332,13 +305,10 @@ export type CustomFieldsResponse = {
};
export const getCustomFields = async (
pagination: PaginationRequest,
pagination: PaginationRequest
): Promise<[CustomFieldsResponse | null, string?]> => {
try {
const fieldsResponse = await makeRequest<
PaginationRequest,
CustomFieldsResponse
>({
const fieldsResponse = await makeRequest<PaginationRequest, CustomFieldsResponse>({
method: "GET",
url: `${API_URL}/fields?page=${pagination.page}&size=${pagination.size}`,
});
@ -362,4 +332,4 @@ export const removeAmoAccount = async (): Promise<[void | null, string?]> => {
const [error] = parseAxiosError(nativeError);
return [null, `Не удалось отвязать аккаунт. ${error}`];
}
};
};

@ -1,177 +1,58 @@
import * as React from "react";
import { FC, useEffect, useMemo, useRef, useState } from "react";
import Radio from "@mui/material/Radio";
import RadioGroup from "@mui/material/RadioGroup";
import FormControlLabel from "@mui/material/FormControlLabel";
import Box from "@mui/material/Box";
import { FC, useMemo } from "react";
import CheckboxIcon from "@icons/Checkbox";
import { Typography, useMediaQuery, useTheme } from "@mui/material";
import {
getPipelines,
getSteps,
getTags,
PaginationRequest,
Pipeline,
Step,
Tag,
} from "@api/integration";
import type { TagQuestionObject } from "@/pages/IntegrationsPage/IntegrationsModal/AmoCRMModal"
import { SelectChangeEvent, Typography, useTheme, Box, FormControlLabel, RadioGroup, Radio } from "@mui/material";
import { MinifiedData, TagKeys } from "@/pages/IntegrationsPage/IntegrationsModal/types";
type CustomRadioGroupProps = {
type?: string;
selectedValue: string | null;
setSelectedValue: (value: string | null) => void;
pipelineId?: number | null;
items: TagQuestionObject[]
tags: Tag[]
setTags?: (setValueFunc: (value: Tag[]) => Tag[]) => void;
items: MinifiedData[] | [];
selectedItemId?: string | null;
setSelectedItem: (value: string | null) => void;
handleScroll: () => void;
activeScope?: TagKeys;
};
const SIZE = 25;
export const CustomRadioGroup: FC<CustomRadioGroupProps> = ({
type,
selectedValue,
setSelectedValue,
pipelineId,
items,
tags = [],
setTags,
selectedItemId = "",
setSelectedItem,
handleScroll,
activeScope,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(500));
const currentItem = useMemo(() => {
if (selectedItemId !== null && selectedItemId.length > 0) {
return items.find((item) => item.id === selectedItemId) || null;
}
return null;
}, [selectedItemId, items]);
const [currentValue, setCurrentValue] = useState<string | null>(
selectedValue,
);
const [page, setPage] = useState(1);
const [isLoading, setIsLoading] = useState(false);
const filteredItems = useMemo(() => {
let newArray = items;
if (activeScope !== undefined)
newArray = newArray.filter((item) => {
return item.entity === activeScope;
});
return newArray;
}, items);
const [steps, setSteps] = useState<Step[]>([]);
const [pipelines, setPipelines] = useState<Pipeline[]>([]);
const [hasMoreItems, setHasMoreItems] = useState(true);
const boxRef = useRef(null);
const handleChange = (event: React.ChangeEvent<HTMLInputElement>) => {
console.log("применился хенделчейндж")
console.log((event.target as HTMLInputElement).value)
setSelectedValue((event.target as HTMLInputElement).value);
setCurrentValue((event.target as HTMLInputElement).value);
};
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const onScroll = React.useCallback((e: React.UIEvent<HTMLDivElement>) => {
const scrollHeight = e.currentTarget.scrollHeight;
const scrollTop = e.currentTarget.scrollTop;
const clientHeight = e.currentTarget.clientHeight;
const scrolledToBottom = scrollTop / (scrollHeight - clientHeight) > 0.9;
if (scrolledToBottom && !isLoading && hasMoreItems) {
setPage((prevPage) => prevPage + 1);
if (scrolledToBottom) {
handleScroll();
}
};
console.log(type)
console.log(items)
console.log(type === "typeQuestions" && items && items.length !== 0)
useEffect(() => {
if (type === "typeTags" && hasMoreItems && setTags !== undefined) {
setIsLoading(true);
const pagination: PaginationRequest = {
page: page,
size: SIZE,
};
getTags(pagination).then(([response]) => {
if (response && response.items !== null) {
setTags((prevItems:Tag[]) => [...prevItems, ...response.items]);
if (response.items.length < SIZE) {
setHasMoreItems(false);
}
}
setIsLoading(false);
});
}
if (type === "typeSteps" && hasMoreItems && pipelineId) {
setIsLoading(true);
const pagination: PaginationRequest & { pipelineId: number } = {
page: page,
size: SIZE,
pipelineId: pipelineId,
};
getSteps(pagination).then(([response]) => {
if (response && response.items !== null) {
setSteps((prevItems) => [...prevItems, ...response.items]);
if (response.items.length < SIZE) {
setHasMoreItems(false);
}
}
setIsLoading(false);
});
}
if (type === "typePipelines" && hasMoreItems) {
setIsLoading(true);
const pagination: PaginationRequest = {
page: page,
size: SIZE,
};
getPipelines(pagination).then(([response]) => {
if (response && response.items !== null) {
setPipelines((prevItems) => [...prevItems, ...response.items]);
if (response.items.length < SIZE) {
setHasMoreItems(false);
}
}
setIsLoading(false);
});
}
}, [page, type, hasMoreItems, pipelineId]);
}, []);
const formControlLabels = useMemo(() => {
if (type === "typeTags" && tags && tags.length !== 0) {
return tags.map((item) => (
if (filteredItems.length !== 0) {
return filteredItems.map((item) => (
<FormControlLabel
key={item.AmoID}
sx={{
padding: "15px",
borderBottom: `1px solid ${theme.palette.background.default}`,
display: "flex",
justifyContent: "space-between",
borderRadius: "12px",
margin: 0,
backgroundColor:
currentValue === Number(item.AmoID)
? theme.palette.background.default
: theme.palette.common.white,
}}
value={Number(item.AmoID)}
control={
<Radio
checkedIcon={
<CheckboxIcon
checked
isRounded
color={theme.palette.brightPurple.main}
/>
}
icon={<CheckboxIcon isRounded />}
/>
}
label={
<Box sx={{ display: "flex", flexDirection: "column" }}>
<Typography sx={{ color: `${item.Color}` }}>
{item.Name}
</Typography>
<Typography>{item.Entity}</Typography>
</Box>
}
labelPlacement={"start"}
/>
));
}
if (type === "typeSteps" && steps && steps.length !== 0) {
return steps.map((step) => (
<FormControlLabel
key={step.ID}
key={item.id}
sx={{
color: "black",
padding: "15px",
@ -181,11 +62,14 @@ export const CustomRadioGroup: FC<CustomRadioGroupProps> = ({
borderRadius: "12px",
margin: 0,
backgroundColor:
currentValue === step.Name
? theme.palette.background.default
: theme.palette.common.white,
currentItem?.id === item.id ? theme.palette.background.default : theme.palette.common.white,
"&.MuiFormControlLabel-root > .MuiTypography-root": {
width: "200px",
overflow: "hidden",
textOverflow: "ellipsis",
},
}}
value={step.AmoID}
value={item.id}
control={
<Radio
checkedIcon={
@ -198,87 +82,7 @@ export const CustomRadioGroup: FC<CustomRadioGroupProps> = ({
icon={<CheckboxIcon isRounded />}
/>
}
label={step.Name}
labelPlacement={"start"}
/>
));
}
if (type === "typePipelines" && pipelines && pipelines.length !== 0) {
return pipelines.map((pipeline) => (
<FormControlLabel
key={pipeline.ID}
sx={{
color: "black",
padding: "15px",
borderBottom: `1px solid ${theme.palette.background.default}`,
display: "flex",
justifyContent: "space-between",
borderRadius: "12px",
margin: 0,
backgroundColor:
currentValue === pipeline.Name
? theme.palette.background.default
: theme.palette.common.white,
"&.MuiFormControlLabel-root > .MuiTypography-root": {
width: "200px",
overflow: "hidden",
textOverflow: "ellipsis"
}
}}
value={pipeline.AmoID}
control={
<Radio
checkedIcon={
<CheckboxIcon
checked
isRounded
color={theme.palette.brightPurple.main}
/>
}
icon={<CheckboxIcon isRounded />}
/>
}
label={pipeline.Name}
labelPlacement={"start"}
/>
));
}
if (type === "typeQuestions" && items && items.length !== 0) {
return items.map(({ backendId, title }) => (
<FormControlLabel
key={backendId}
sx={{
color: "black",
padding: "15px",
borderBottom: `1px solid ${theme.palette.background.default}`,
display: "flex",
justifyContent: "space-between",
borderRadius: "12px",
margin: 0,
backgroundColor:
currentValue === Number(backendId)
? theme.palette.background.default
: theme.palette.common.white,
"&.MuiFormControlLabel-root > .MuiTypography-root": {
width: "200px",
overflow: "hidden",
textOverflow: "ellipsis"
}
}}
value={Number(backendId)}
control={
<Radio
checkedIcon={
<CheckboxIcon
checked
isRounded
color={theme.palette.brightPurple.main}
/>
}
icon={<CheckboxIcon isRounded />}
/>
}
label={title}
label={item.title}
labelPlacement={"start"}
/>
));
@ -296,11 +100,10 @@ export const CustomRadioGroup: FC<CustomRadioGroupProps> = ({
<Typography>Нет элементов</Typography>
</Box>
);
}, [tags, steps, currentValue, type, pipelines]);
}, [filteredItems, selectedItemId]);
return (
<Box
ref={boxRef}
onScroll={handleScroll}
sx={{
border: `1px solid ${theme.palette.grey2.main}`,
@ -313,13 +116,9 @@ export const CustomRadioGroup: FC<CustomRadioGroupProps> = ({
<RadioGroup
aria-labelledby="demo-controlled-radio-buttons-group"
name="controlled-radio-buttons-group"
value={currentValue}
onChange={(e) => {
console.log("klick")
console.log(e.target.value)
console.log(typeof e.target.value)
handleChange(e)
}}
value={selectedItemId}
onChange={({ target }: SelectChangeEvent<string>) => setSelectedItem(target.value)}
onScroll={onScroll}
>
{formControlLabels}
</RadioGroup>

@ -1,22 +0,0 @@
.MuiOutlinedInput-root .MuiOutlinedInput-notchedOutline {
border: 0;
}
.MuiPaper-root.MuiMenu-paper {
padding-top: 50px;
margin-top: -50px;
border-radius: 28px;
}
.MuiInputBase-root.MuiOutlinedInput-root {
display: block;
}
.MuiInputBase-root.MuiOutlinedInput-root > div:first-child,
.MuiInputBase-root.MuiOutlinedInput-root .MuiSelect-icon {
display: none;
}
.MuiMenu-root.MuiModal-root {
z-index: 0;
}

@ -1,101 +1,46 @@
import {
Avatar,
MenuItem,
Select,
SelectChangeEvent,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import Box from "@mui/material/Box";
import * as React from "react";
import { FC, useCallback, useEffect, useMemo, useRef, useState } from "react";
import "./CustomSelect.css";
import { FC, useCallback, useMemo, useRef, useState } from "react";
import { Avatar, MenuItem, Select, SelectChangeEvent, Typography, useMediaQuery, useTheme, Box } from "@mui/material";
import arrow_down from "../../assets/icons/arrow_down.svg";
import { getUsers, PaginationRequest, User } from "@api/integration";
import { MinifiedData } from "@/pages/IntegrationsPage/IntegrationsModal/types";
type CustomSelectProps = {
selectedItem: string | null;
items: MinifiedData[] | [];
selectedItemId: string | null;
setSelectedItem: (value: string | null) => void;
type?: string;
handleScroll: () => void;
};
export const CustomSelect: FC<CustomSelectProps> = ({
selectedItem,
setSelectedItem,
type,
}) => {
export const CustomSelect: FC<CustomSelectProps> = ({ items, selectedItemId, setSelectedItem, handleScroll }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const [opened, setOpened] = useState<boolean>(false);
const [currentValue, setCurrentValue] = useState<string | null>(selectedItem);
const [users, setUsers] = useState<User[]>([]);
const [isLoading, setIsLoading] = useState(false);
const [page, setPage] = useState(1);
const [hasMoreItems, setHasMoreItems] = useState(true);
const SIZE = 25;
const ref = useRef<HTMLDivElement | null>(null);
const selectWidth = ref.current ? ref.current.offsetWidth : undefined;
const [savedValue, setSavedValue] = useState<number | null>(null);
const onSelectItem = useCallback(
(event: SelectChangeEvent<HTMLDivElement>) => {
const newValue = event.target.value.toString();
const selectedUser = users.find((user) => user.Name === newValue);
if (selectedUser) {
//для сохранения ID выбранного пользователя в стейт или конфиг...
setSavedValue(selectedUser.ID);
}
setCurrentValue(newValue);
setSelectedItem(newValue);
},
[setSelectedItem, setCurrentValue, setSavedValue, users],
);
const [opened, setOpened] = useState<boolean>(false);
const toggleOpened = useCallback(() => {
setOpened((isOpened) => !isOpened);
}, []);
const handleScroll = (e: React.UIEvent<HTMLDivElement>) => {
const onScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
const scrollHeight = e.currentTarget.scrollHeight;
const scrollTop = e.currentTarget.scrollTop;
const clientHeight = e.currentTarget.clientHeight;
const scrolledToBottom = scrollTop / (scrollHeight - clientHeight) > 0.9;
if (scrolledToBottom && !isLoading && hasMoreItems) {
setPage((prevPage) => prevPage + 1);
if (scrolledToBottom) {
handleScroll();
}
};
}, []);
useEffect(() => {
if (type === "typeUsers" && hasMoreItems) {
setIsLoading(true);
const pagination: PaginationRequest = {
page: page,
size: SIZE,
};
getUsers(pagination).then(([response]) => {
if (response && response.items !== null) {
setUsers((prevItems) => [...prevItems, ...response.items]);
if (response.items.length < SIZE) {
setHasMoreItems(false);
}
}
setIsLoading(false);
});
}
}, [page, type, hasMoreItems]);
const currentItem = useMemo(() => items.find((item) => item.id === selectedItemId) || null, [selectedItemId, items]);
const menuItems = useMemo(() => {
if (type === "typeUsers" && users && users.length !== 0) {
console.log(type)
console.log(users)
return users.map((user) => (
if (items.length !== 0) {
return items.map((item) => (
<MenuItem
key={user.ID}
value={user.AmocrmID}
key={item.id}
value={item.id}
sx={{
padding: "6px 0",
zIndex: 2,
@ -116,45 +61,36 @@ export const CustomSelect: FC<CustomSelectProps> = ({
<Typography
sx={{
width: "33%",
borderRight: isMobile
? "none"
: "1px solid rgba(154, 154, 175, 0.1)",
borderRight: isMobile ? "none" : "1px solid rgba(154, 154, 175, 0.1)",
padding: isMobile ? "5px 0 5px 20px" : "10px 0 10px 20px",
}}
>
{user.Name}
{item.title}
</Typography>
<Typography
sx={{
width: "33%",
borderRight: isMobile
? "none"
: "1px solid rgba(154, 154, 175, 0.1)",
borderRight: isMobile ? "none" : "1px solid rgba(154, 154, 175, 0.1)",
padding: isMobile ? "5px 0 5px 20px" : "10px 0 10px 20px",
color: isMobile ? "#9A9AAF" : "#4D4D4D",
}}
>
{user.Email}
</Typography>
<Typography
sx={{
width: "33%",
padding: isMobile ? "5px 0 5px 20px" : "10px 0 10px 20px",
color: isMobile ? "#9A9AAF" : "#4D4D4D",
}}
>
{user.Role}
{item.subTitle}
</Typography>
</Box>
</MenuItem>
));
}
return (
<MenuItem key={"-1"} disabled sx={{ padding: "12px", zIndex: 2 }}>
<MenuItem
key={"-1"}
disabled
sx={{ padding: "12px", zIndex: 2 }}
>
нет данных
</MenuItem>
);
}, [users, type]);
}, [items, selectedItemId]);
return (
<Box>
@ -165,10 +101,7 @@ export const CustomSelect: FC<CustomSelectProps> = ({
width: "100%",
height: "56px",
padding: "5px",
color:
currentValue === null
? theme.palette.grey2.main
: theme.palette.brightPurple.main,
color: currentItem === null ? theme.palette.grey2.main : theme.palette.brightPurple.main,
border: `2px solid ${theme.palette.common.white}`,
borderRadius: "30px",
background: "#EFF0F5",
@ -176,13 +109,15 @@ export const CustomSelect: FC<CustomSelectProps> = ({
alignItems: "center",
cursor: "pointer",
}}
onClick={() => ref.current?.click()}
onClick={() => {
if (ref.current !== null) ref.current?.click();
}}
>
<Avatar sx={{ width: 46, height: 46, marginRight: 1 }} />
<Typography
sx={{
marginLeft: isMobile ? "10px" : "20px",
fontWeight: currentValue === null ? "400" : "500",
fontWeight: currentItem === null ? "400" : "500",
fontSize: isMobile ? "14px" : "16px",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
@ -190,7 +125,7 @@ export const CustomSelect: FC<CustomSelectProps> = ({
flexGrow: 1,
}}
>
{currentValue || "Выберите ответственного за сделку"}
{currentItem?.title || "Выберите ответственного за сделку"}
</Typography>
<img
src={arrow_down}
@ -209,23 +144,39 @@ export const CustomSelect: FC<CustomSelectProps> = ({
<Select
ref={ref}
className="select"
value={""}
value={selectedItemId || ""}
open={opened}
MenuProps={{
disablePortal: true,
PaperProps: {
onScroll: handleScroll,
onScroll: onScroll,
style: {
zIndex: 2,
maxHeight: "300px",
overflow: "auto",
overflowX: "auto",
maxWidth: selectWidth,
paddingTop: "50px",
marginTop: "-50px",
borderRadius: "28px",
},
},
}}
sx={{}}
onChange={onSelectItem}
sx={{
display: "block",
"& .MuiSelect-select.MuiSelect-outlined.MuiInputBase-input": {
display: "none",
},
"& .MuiSelect-icon": {
display: "none",
},
"& .MuiOutlinedInput-notchedOutline": {
border: 0,
},
"& .MuiMenu-root.MuiModal-root": {
zIndex: 0,
},
}}
onChange={({ target }: SelectChangeEvent<string>) => setSelectedItem(target.value)}
onClick={toggleOpened}
>
{menuItems}

@ -1,7 +1,4 @@
import type {
QuizQuestionBase,
QuestionBranchingRuleMain,
} from "../model/questionTypes/shared";
import { QuizQuestionBase } from "@frontend/squzanswerer";
export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id" | "backendId"> = {
quizId: 0,
@ -21,7 +18,7 @@ export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id" | "backendId"> = {
},
rule: {
children: [],
main: [] as QuestionBranchingRuleMain[],
main: [],
parentId: "",
default: "",
},

@ -1,7 +1,6 @@
import { QuizQuestionDate } from "@frontend/squzanswerer";
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionDate } from "../model/questionTypes/date";
export const QUIZ_QUESTION_DATE: Omit<QuizQuestionDate, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE,
type: "date",

@ -1,5 +1,4 @@
import { QuestionType } from "@model/question/question";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { QUIZ_QUESTION_DATE } from "./date";
import { QUIZ_QUESTION_EMOJI } from "./emoji";
import { QUIZ_QUESTION_FILE } from "./file";
@ -12,11 +11,9 @@ import { QUIZ_QUESTION_TEXT } from "./text";
import { QUIZ_QUESTION_VARIANT } from "./variant";
import { QUIZ_QUESTION_VARIMG } from "./varimg";
import { QUIZ_QUESTION_RESULT } from "./result";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
export const defaultQuestionByType: Record<
QuestionType,
Omit<AnyTypedQuizQuestion, "id" | "backendId">
> = {
export const defaultQuestionByType: Record<QuestionType, Omit<AnyTypedQuizQuestion, "id" | "backendId">> = {
date: QUIZ_QUESTION_DATE,
emoji: QUIZ_QUESTION_EMOJI,
file: QUIZ_QUESTION_FILE,

@ -1,27 +1,26 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionEmoji } from "../model/questionTypes/emoji";
import type { QuizQuestionEmoji } from "@frontend/squzanswerer";
import { nanoid } from "nanoid";
export const QUIZ_QUESTION_EMOJI: Omit<QuizQuestionEmoji, "id" | "backendId"> =
{
...QUIZ_QUESTION_BASE,
type: "emoji",
content: {
...QUIZ_QUESTION_BASE.content,
multi: false,
own: false,
innerNameCheck: false,
innerName: "",
required: false,
variants: [
{
id: nanoid(),
answer: "",
extendedText: "",
hints: "",
originalImageUrl: "",
},
],
},
};
export const QUIZ_QUESTION_EMOJI: Omit<QuizQuestionEmoji, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE,
type: "emoji",
content: {
...QUIZ_QUESTION_BASE.content,
multi: false,
own: false,
innerNameCheck: false,
innerName: "",
required: false,
variants: [
{
id: nanoid(),
answer: "",
extendedText: "",
hints: "",
originalImageUrl: "",
},
],
},
};

@ -1,6 +1,6 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionFile } from "../model/questionTypes/file";
import type { QuizQuestionFile } from "@frontend/squzanswerer";
export const QUIZ_QUESTION_FILE: Omit<QuizQuestionFile, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE,

@ -1,12 +1,9 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionImages } from "../model/questionTypes/images";
import type { QuizQuestionImages } from "@frontend/squzanswerer";
import { nanoid } from "nanoid";
export const QUIZ_QUESTION_IMAGES: Omit<
QuizQuestionImages,
"id" | "backendId"
> = {
export const QUIZ_QUESTION_IMAGES: Omit<QuizQuestionImages, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE,
type: "images",
content: {

@ -1,11 +1,8 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionNumber } from "../model/questionTypes/number";
import type { QuizQuestionNumber } from "@frontend/squzanswerer";
export const QUIZ_QUESTION_NUMBER: Omit<
QuizQuestionNumber,
"id" | "backendId"
> = {
export const QUIZ_QUESTION_NUMBER: Omit<QuizQuestionNumber, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE,
type: "number",
content: {

@ -1,6 +1,6 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionPage } from "../model/questionTypes/page";
import type { QuizQuestionPage } from "@frontend/squzanswerer";
export const QUIZ_QUESTION_PAGE: Omit<QuizQuestionPage, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE,
@ -13,5 +13,6 @@ export const QUIZ_QUESTION_PAGE: Omit<QuizQuestionPage, "id" | "backendId"> = {
picture: "",
originalPicture: "",
video: "",
useImage: true,
},
};

@ -1,290 +0,0 @@
import type { AnyTypedQuizQuestion } from "../model/questionTypes/shared";
export const QUESTIONS_DUMMY: AnyTypedQuizQuestion[] = [
{
id: "1",
title: "",
type: "variant",
expanded: true,
required: false,
deleted: false,
deleteTimeoutId: 0,
backendId: 1111,
description: "",
openedModalSettings: false,
page: 1,
quizId: 1,
content: {
hint: {
text: "",
video: "",
},
rule: {
main: [],
default: "1",
},
back: "",
originalBack: "",
autofill: false,
largeCheck: false,
multi: false,
own: false,
innerNameCheck: false,
required: false,
innerName: "",
variants: [],
},
},
{
id: "2",
title: "Вы идёте в школу",
type: "page",
expanded: true,
required: false,
deleted: false,
deleteTimeoutId: 0,
backendId: 1112,
description: "",
openedModalSettings: false,
page: 1,
quizId: 1,
content: {
hint: {
text: "",
video: "",
},
rule: {
main: [],
default: "1",
},
back: "",
originalBack: "",
autofill: false,
innerNameCheck: false,
innerName: "",
text: "",
picture: "",
originalPicture: "",
video: "",
},
},
{
id: "3",
title: "Вы немного опоздали, как поступите?",
type: "variant",
expanded: true,
required: true,
deleted: false,
deleteTimeoutId: 0,
backendId: 1113,
description: "",
openedModalSettings: false,
page: 1,
quizId: 1,
content: {
hint: {
text: "",
video: "",
},
rule: {
main: [],
default: "2",
},
back: "",
originalBack: "",
autofill: false,
largeCheck: false,
multi: false,
own: false,
innerNameCheck: false,
required: false,
innerName: "",
variants: [
{
id: "answer1",
answer: "Извинюсь за опоздание и займу своё место",
extendedText: "",
hints: "",
},
{
id: "answer2",
answer:
"Вынесу дверь с ноги и распинав всех направлюсь к месту у розетки",
extendedText: "",
hints: "",
},
{
id: "answer3",
answer: "Влечу в кабинет, пробегу его и выпрыгну в окно",
extendedText: "",
hints: "",
},
],
},
},
{
id: "4",
title: "Вы открываете дверь кабинета, приготовившись исполнить задуманное",
type: "page",
expanded: true,
required: false,
deleted: false,
deleteTimeoutId: 0,
backendId: 1114,
description: "",
openedModalSettings: false,
page: 1,
quizId: 1,
content: {
hint: {
text: "",
video: "",
},
rule: {
main: [
//момент выбора куда мы пойдём
{
next: "7",
or: true,
rules: [
{
question: "2",
answers: ["answer1"],
},
],
},
{
next: "6",
or: true,
rules: [
{
question: "2",
answers: ["answer2"],
},
],
},
{
next: "5",
or: true,
rules: [
{
question: "2",
answers: ["answer3"],
},
],
},
],
default: "2",
},
back: "",
originalBack: "",
autofill: false,
innerNameCheck: false,
innerName: "",
text: "",
picture: "",
originalPicture: "",
video: "",
},
},
{
id: "5",
title: "Вы умерли",
type: "page",
expanded: true,
required: false,
deleted: false,
deleteTimeoutId: 0,
backendId: 1115,
description: "",
openedModalSettings: false,
page: 1,
quizId: 1,
content: {
hint: {
text: "",
video: "",
},
rule: {
main: [],
default: "",
},
back: "",
originalBack: "",
autofill: false,
innerNameCheck: false,
innerName: "",
text: "",
picture: "",
originalPicture: "",
video: "",
},
},
{
id: "6",
title: "Вас вызвали к директору",
type: "page",
expanded: true,
required: false,
deleted: false,
deleteTimeoutId: 0,
backendId: 1116,
description: "",
openedModalSettings: false,
page: 1,
quizId: 1,
content: {
hint: {
text: "",
video: "",
},
rule: {
main: [],
default: "",
},
back: "",
originalBack: "",
autofill: false,
innerNameCheck: false,
innerName: "",
text: "",
picture: "",
originalPicture: "",
video: "",
},
},
{
id: "7",
title: "Вы получили отлично",
type: "page",
expanded: true,
required: false,
deleted: false,
deleteTimeoutId: 0,
backendId: 1117,
description: "",
openedModalSettings: false,
page: 1,
quizId: 1,
content: {
hint: {
text: "",
video: "",
},
rule: {
main: [],
default: "",
},
back: "",
originalBack: "",
autofill: false,
innerNameCheck: false,
innerName: "",
text: "",
picture: "",
originalPicture: "",
video: "",
},
},
];

@ -1,11 +1,8 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionRating } from "../model/questionTypes/rating";
import type { QuizQuestionRating } from "@frontend/squzanswerer";
export const QUIZ_QUESTION_RATING: Omit<
QuizQuestionRating,
"id" | "backendId"
> = {
export const QUIZ_QUESTION_RATING: Omit<QuizQuestionRating, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE,
type: "rating",
content: {

@ -1,12 +1,8 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionResult } from "../model/questionTypes/result";
import { nanoid } from "nanoid";
import type { QuizQuestionResult } from "@frontend/squzanswerer";
export const QUIZ_QUESTION_RESULT: Omit<
QuizQuestionResult,
"id" | "backendId"
> = {
export const QUIZ_QUESTION_RESULT: Omit<QuizQuestionResult, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE,
type: "result",
content: {

@ -1,12 +1,9 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionSelect } from "../model/questionTypes/select";
import type { QuizQuestionSelect } from "@frontend/squzanswerer";
import { nanoid } from "nanoid";
export const QUIZ_QUESTION_SELECT: Omit<
QuizQuestionSelect,
"id" | "backendId"
> = {
export const QUIZ_QUESTION_SELECT: Omit<QuizQuestionSelect, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE,
type: "select",
content: {

@ -1,6 +1,6 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionText } from "../model/questionTypes/text";
import type { QuizQuestionText } from "@frontend/squzanswerer";
export const QUIZ_QUESTION_TEXT: Omit<QuizQuestionText, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE,

@ -1,12 +1,9 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionVariant } from "../model/questionTypes/variant";
import type { QuizQuestionVariant } from "@frontend/squzanswerer";
import { nanoid } from "nanoid";
export const QUIZ_QUESTION_VARIANT: Omit<
QuizQuestionVariant,
"id" | "backendId"
> = {
export const QUIZ_QUESTION_VARIANT: Omit<QuizQuestionVariant, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE,
type: "variant",
content: {

@ -1,12 +1,9 @@
import { QUIZ_QUESTION_BASE } from "./base";
import type { QuizQuestionVarImg } from "../model/questionTypes/varimg";
import type { QuizQuestionVarImg } from "@frontend/squzanswerer";
import { nanoid } from "nanoid";
export const QUIZ_QUESTION_VARIMG: Omit<
QuizQuestionVarImg,
"id" | "backendId"
> = {
export const QUIZ_QUESTION_VARIMG: Omit<QuizQuestionVarImg, "id" | "backendId"> = {
...QUIZ_QUESTION_BASE,
type: "varimg",
content: {

@ -1,4 +1,4 @@
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
import { QuestionType } from "./question";
export interface EditQuestionRequest {
@ -15,9 +15,7 @@ export interface EditQuestionResponse {
updated: number;
}
export function questionToEditQuestionRequest(
question: AnyTypedQuizQuestion,
): EditQuestionRequest {
export function questionToEditQuestionRequest(question: AnyTypedQuizQuestion): EditQuestionRequest {
return {
id: question.backendId,
title: question.title,

@ -1,4 +1,4 @@
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
import { defaultQuestionByType } from "../../constants/default";
import { nanoid } from "nanoid";
@ -44,21 +44,15 @@ export interface RawQuestion {
updated_at: string;
}
export function rawQuestionToQuestion(
rawQuestion: RawQuestion,
): AnyTypedQuizQuestion {
export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyTypedQuizQuestion {
let content = defaultQuestionByType[rawQuestion.type].content;
const frontId = nanoid();
try {
content = JSON.parse(rawQuestion.content);
if (content.id.length === 0 || content.id.length === undefined)
content.id = frontId;
if (content.id.length === 0 || content.id.length === undefined) content.id = frontId;
} catch (error) {
console.warn(
"Cannot parse question content from string, using default content",
error,
);
console.warn("Cannot parse question content from string, using default content", error);
}
return {

@ -1,21 +0,0 @@
import type { QuizQuestionBase, QuestionHint, PreviewRule } from "./shared";
export interface QuizQuestionDate extends QuizQuestionBase {
type: "date";
content: {
id: string;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
dateRange: boolean;
time: boolean;
hint: QuestionHint;
rule: PreviewRule;
back: string;
originalBack: string;
autofill: boolean;
};
}

@ -1,29 +0,0 @@
import type {
QuizQuestionBase,
QuestionVariant,
QuestionHint,
PreviewRule,
} from "./shared";
export interface QuizQuestionEmoji extends QuizQuestionBase {
type: "emoji";
content: {
id: string;
/** Чекбокс "Можно несколько" */
multi: boolean;
/** Чекбокс "Вариант "свой ответ"" */
own: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
variants: QuestionVariant[];
hint: QuestionHint;
rule: PreviewRule;
back: string;
originalBack: string;
autofill: boolean;
};
}

@ -1,30 +0,0 @@
import type { QuizQuestionBase, QuestionHint, PreviewRule } from "./shared";
export const UPLOAD_FILE_TYPES_MAP = {
picture: "Изображения",
video: "Видео",
audio: "Аудио",
document: "Документ",
} as const;
export type UploadFileType = keyof typeof UPLOAD_FILE_TYPES_MAP;
export interface QuizQuestionFile extends QuizQuestionBase {
type: "file";
content: {
id: string;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Чекбокс "Автозаполнение адреса" */
autofill: boolean;
type: UploadFileType;
hint: QuestionHint;
rule: PreviewRule;
back: string;
originalBack: string;
};
}

@ -1,37 +0,0 @@
import type {
QuestionHint,
QuestionVariant,
QuizQuestionBase,
PreviewRule,
} from "./shared";
export interface QuizQuestionImages extends QuizQuestionBase {
type: "images";
content: {
id: string;
/** Чекбокс "Вариант "свой ответ"" */
own: boolean;
/** Чекбокс "Можно несколько" */
multi: boolean;
/** Пропорции */
xy: "1:1" | "1:2" | "2:1";
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Чекбокс "Большие картинки" */
large: boolean;
/** Форма */
format: "carousel" | "masonry";
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Варианты (картинки) */
variants: QuestionVariant[];
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string | null;
originalBack: string | null;
autofill: boolean;
largeCheck: boolean;
};
}

@ -1,31 +0,0 @@
import type { QuizQuestionBase, QuestionHint, PreviewRule } from "./shared";
export interface QuizQuestionNumber extends QuizQuestionBase {
type: "number";
content: {
id: string;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Диапазон */
range: string;
/** Начальное значение */
start: number;
/** Начальное значение */
defaultValue: number;
/** Шаг */
step: number;
steps: number;
/** Чекбокс "Выбор диапазона (два ползунка)" */
chooseRange: boolean;
hint: QuestionHint;
rule: PreviewRule;
back: string;
originalBack: string;
autofill: boolean;
form: "star" | "trophie" | "flag" | "heart" | "like" | "bubble" | "hashtag";
};
}

@ -1,22 +0,0 @@
import type { QuizQuestionBase, QuestionHint, PreviewRule } from "./shared";
export interface QuizQuestionPage extends QuizQuestionBase {
type: "page";
content: {
id: string;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
text: string;
picture: string;
originalPicture: string;
useImage: boolean;
video: string | null;
hint: QuestionHint;
rule: PreviewRule;
back: string;
originalBack: string;
autofill: boolean;
};
}

@ -1,27 +0,0 @@
import type { QuizQuestionBase, QuestionHint, PreviewRule } from "./shared";
export interface QuizQuestionRating extends QuizQuestionBase {
type: "rating";
content: {
id: string;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
steps: number;
ratingExpanded: boolean;
/** Форма иконки */
form: string;
hint: QuestionHint;
rule: PreviewRule;
back: string;
originalBack: string;
autofill: boolean;
/** Позитивное описание рейтинга */
ratingPositiveDescription: string;
/** Негативное описание рейтинга */
ratingNegativeDescription: string;
};
}

@ -1,20 +0,0 @@
import type { QuizQuestionBase, QuestionBranchingRule, QuestionHint } from "./shared";
export interface QuizQuestionResult extends QuizQuestionBase {
type: "result";
content: {
id: string;
back: string;
originalBack: string;
video: string | null;
innerName: string;
text: string;
price: [number] | [number, number];
useImage: boolean;
rule: QuestionBranchingRule;
hint: QuestionHint;
autofill: boolean;
usage: boolean;
redirect: string;
};
}

@ -1,29 +0,0 @@
import type {
QuizQuestionBase,
QuestionVariant,
QuestionHint,
PreviewRule,
} from "./shared";
export interface QuizQuestionSelect extends QuizQuestionBase {
type: "select";
content: {
id: string;
/** Чекбокс "Можно несколько" */
multi: boolean;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Поле "Текст в выпадающем списке" */
default: string;
variants: QuestionVariant[];
rule: PreviewRule;
hint: QuestionHint;
back: string;
originalBack: string;
autofill: boolean;
};
}

@ -1,77 +1,6 @@
import { QuestionType } from "@model/question/question";
import type { QuizQuestionDate } from "./date";
import type { QuizQuestionEmoji } from "./emoji";
import type { QuizQuestionFile } from "./file";
import type { QuizQuestionImages } from "./images";
import type { QuizQuestionNumber } from "./number";
import type { QuizQuestionPage } from "./page";
import type { QuizQuestionRating } from "./rating";
import type { QuizQuestionSelect } from "./select";
import type { QuizQuestionText } from "./text";
import type { QuizQuestionVariant } from "./variant";
import type { QuizQuestionVarImg } from "./varimg";
import type { QuizQuestionResult } from "./result";
import { QuestionBranchingRuleMain, QuestionVariant } from "@frontend/squzanswerer";
import { nanoid } from "nanoid";
export interface QuestionBranchingRuleMain {
next: string;
or: boolean;
rules: {
question: string; //id родителя (пока что)
answers: string[];
}[];
}
export interface QuestionBranchingRule {
children: string[];
//список условий
main: QuestionBranchingRuleMain[];
parentId: string | null | "root";
default: string;
}
export interface QuestionHint {
/** Текст подсказки */
text: string;
/** URL видео подсказки */
video: string;
}
export type QuestionVariant = {
id: string;
/** Текст */
answer: string;
/** Текст подсказки */
hints: string;
/** Дополнительное поле для текста, emoji, ссылки на картинку */
extendedText: string;
/** Оригинал изображения (до кропа) */
originalImageUrl: string;
};
export interface QuizQuestionBase {
backendId: number;
/** Stable id, generated on client */
id: string;
quizId: number;
title: string;
description: string;
page: number;
type?: QuestionType | null;
expanded: boolean;
openedModalSettings: boolean;
deleted: boolean;
deleteTimeoutId: number;
required: boolean;
content: {
id: string;
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string;
originalBack: string;
autofill: boolean;
};
}
export interface UntypedQuizQuestion {
type: null;
id: string;
@ -82,50 +11,25 @@ export interface UntypedQuizQuestion {
deleted: boolean;
}
export type AnyTypedQuizQuestion =
| QuizQuestionVariant
| QuizQuestionImages
| QuizQuestionVarImg
| QuizQuestionEmoji
| QuizQuestionText
| QuizQuestionSelect
| QuizQuestionDate
| QuizQuestionNumber
| QuizQuestionFile
| QuizQuestionPage
| QuizQuestionRating
| QuizQuestionResult;
export type AllTypesQuestion =
| AnyTypedQuizQuestion
| UntypedQuizQuestion;
type FilterQuestionsWithVariants<T> = T extends {
content: { variants: QuestionVariant[] };
export function createBranchingRuleMain(targetId: string, parentId: string): QuestionBranchingRuleMain {
return {
next: targetId,
or: false,
rules: [
{
question: parentId,
answers: [],
},
],
};
}
? T
: never;
export type QuizQuestionsWithVariants =
FilterQuestionsWithVariants<AnyTypedQuizQuestion>;
export const createBranchingRuleMain: (
targetId: string,
parentId: string,
) => QuestionBranchingRuleMain = (targetId, parentId) => ({
next: targetId,
or: false,
rules: [
{
question: parentId,
answers: [] as string[],
},
],
});
export const createQuestionVariant: () => QuestionVariant = () => ({
id: nanoid(),
answer: "",
extendedText: "",
hints: "",
originalImageUrl: "",
});
export function createQuestionVariant(): QuestionVariant {
return {
id: nanoid(),
answer: "",
extendedText: "",
hints: "",
originalImageUrl: "",
};
}

@ -1,23 +0,0 @@
import type { PreviewRule, QuestionHint, QuizQuestionBase } from "./shared";
export interface QuizQuestionText extends QuizQuestionBase {
type: "text";
content: {
id: string;
placeholder: string;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Чекбокс "Автозаполнение адреса" */
autofill: boolean;
answerType: "single" | "multi" | "numberOnly";
hint: QuestionHint;
rule: PreviewRule;
back: string;
originalBack: string;
onlyNumbers: boolean;
};
}

@ -1,32 +0,0 @@
import type {
QuizQuestionBase,
QuestionVariant,
QuestionHint,
PreviewRule,
} from "./shared";
export interface QuizQuestionVariant extends QuizQuestionBase {
type: "variant";
content: {
id: string;
/** Чекбокс "Длинный текстовый ответ" */
largeCheck: boolean;
/** Чекбокс "Можно несколько" */
multi: boolean;
/** Чекбокс "Вариант "свой ответ"" */
own: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Варианты ответов */
variants: QuestionVariant[];
hint: QuestionHint;
rule: PreviewRule;
back: string;
originalBack: string;
autofill: boolean;
};
}

@ -1,29 +0,0 @@
import type {
QuestionHint,
QuestionVariant,
QuizQuestionBase,
PreviewRule,
} from "./shared";
export interface QuizQuestionVarImg extends QuizQuestionBase {
type: "varimg";
content: {
id: string;
/** Чекбокс "Вариант "свой ответ"" */
own: boolean;
/** Чекбокс "Внутреннее название вопроса" */
innerNameCheck: boolean;
/** Поле "Внутреннее название вопроса" */
innerName: string;
/** Чекбокс "Необязательный вопрос" */
required: boolean;
variants: QuestionVariant[];
hint: QuestionHint;
rule: QuestionBranchingRule;
back: string;
originalBack: string;
autofill: boolean;
largeCheck: boolean;
replText: string;
};
}

@ -26,7 +26,7 @@ export interface CreateQuizRequest {
/** true if it is allowed for pause quiz */
pausable: boolean;
/** count of questions */
question_cnt?: number;
questions_count?: number;
/** set true if squiz realize group functionality */
super: boolean;
/** group of new quiz */

@ -30,7 +30,7 @@ export interface EditQuizRequest {
/** allow to pause quiz to user */
pausable: boolean;
/** count of questions */
question_cnt?: number;
questions_count?: number;
/** set true if squiz realize group functionality */
super?: boolean;
/** group of new quiz */
@ -58,7 +58,7 @@ export function quizToEditQuizRequest(quiz: Quiz): EditQuizRequest {
due_to: quiz.due_to,
time_of_passing: quiz.time_of_passing,
pausable: quiz.pausable,
question_cnt: quiz.question_cnt,
questions_count: quiz.questions_count,
super: quiz.super,
group_id: quiz.group_id,
};

@ -1,4 +1,5 @@
import { QuizConfig, defaultQuizConfig } from "@model/quizSettings";
import { QuizConfig } from "@frontend/squzanswerer";
import { defaultQuizConfig } from "@model/quizSettings";
import { nanoid } from "nanoid";
export interface Quiz {
@ -47,7 +48,7 @@ export interface Quiz {
created_at: string;
updated_at: string;
/** count of questions */
question_cnt: number;
questions_count: number;
/** count passings */
passed_count: number;
session_count: number;
@ -55,8 +56,6 @@ export interface Quiz {
average_time: number;
/** set true if squiz realize group functionality */
super: boolean;
questions_count: number;
/** group of new quiz */
group_id: number;
}
@ -106,7 +105,7 @@ export interface RawQuiz {
created_at: string;
updated_at: string;
/** count of questions */
question_cnt: number;
questions_count: number;
/** count passings */
passed_count: number;
session_count: number;
@ -124,10 +123,7 @@ export function rawQuizToQuiz(rawQuiz: RawQuiz): Quiz {
try {
config = JSON.parse(rawQuiz.config);
} catch (error) {
console.warn(
"Cannot parse quiz config from string, using default config",
error,
);
console.warn("Cannot parse quiz config from string, using default config", error);
}
return {

@ -1,3 +1,4 @@
import { QuizConfig } from "@frontend/squzanswerer";
import ChartPieIcon from "@icons/ChartPieIcon";
import ContactBookIcon from "@icons/ContactBookIcon";
import FlowArrowIcon from "@icons/FlowArrowIcon";
@ -65,80 +66,12 @@ export type QuizTheme =
| "Design9"
| "Design10";
export interface QuizConfig {
spec: undefined | true;
type: QuizType;
noStartPage: boolean;
startpageType: QuizStartpageType;
results: QuizResultsType;
haveRoot: string | null;
theme: QuizTheme;
design: boolean;
resultInfo: {
when: "email" | "";
share: true | false;
replay: true | false;
theme: string;
reply: string;
replname: string;
showResultForm: "before" | "after";
};
startpage: {
description: string;
button: string;
position: QuizStartpageAlignType;
favIcon: string | null;
logo: string | null;
originalLogo: string | null;
background: {
type: null | "image" | "video";
desktop: string | null;
originalDesktop: string | null;
mobile: string | null;
originalMobile: string | null;
video: string | null;
cycle: boolean;
};
};
formContact: {
title: string;
desc: string;
fields: Record<FormContactFieldName, FormContactFieldData>;
button: string;
};
info: {
phonenumber: string;
clickable: boolean;
orgname: string;
site: string;
law?: string;
};
meta: string;
antifraud: boolean;
showfc: boolean;
yandexMetricsNumber: number | undefined;
vkMetricsNumber: number | undefined;
}
export enum QuizMetricType {
yandex = "yandexMetricsNumber",
vk = "vkMetricsNumber",
}
export type FormContactFieldName =
| "name"
| "email"
| "phone"
| "text"
| "address";
type FormContactFieldData = {
text: string;
innerText: string;
key: string;
required: boolean;
used: boolean;
};
export type FormContactFieldName = "name" | "email" | "phone" | "text" | "address";
export type FieldSettingsDrawerState = {
field: FormContactFieldName | "all" | "";

@ -8,10 +8,7 @@ type AmoAccountInfoProps = {
accountInfo: AccountResponse;
};
export const AmoAccountInfo: FC<AmoAccountInfoProps> = ({
handleNextStep,
accountInfo,
}) => {
export const AmoAccountInfo: FC<AmoAccountInfoProps> = ({ handleNextStep, accountInfo }) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
@ -24,9 +21,7 @@ export const AmoAccountInfo: FC<AmoAccountInfoProps> = ({
}}
>
<Box sx={{ width: isMobile ? "100%" : "45%" }}>
<Typography sx={{ color: theme.palette.grey2.main }}>
{title}:
</Typography>
<Typography sx={{ color: theme.palette.grey2.main }}>{title}:</Typography>
</Box>
<Box sx={{ width: isMobile ? "100%" : "45%" }}>
<Typography>{value || "нет данных"}</Typography>
@ -43,12 +38,15 @@ export const AmoAccountInfo: FC<AmoAccountInfoProps> = ({
}}
>
<Box sx={{ width: isMobile ? "100%" : "45%" }}>
<Typography sx={{ color: theme.palette.grey2.main }}>
{title}:
</Typography>
<Typography sx={{ color: theme.palette.grey2.main }}>{title}:</Typography>
</Box>
<Box sx={{ width: isMobile ? "100%" : "45%" }}>
<a target="_blank" href={link}>{link}</a>
<a
target="_blank"
href={link}
>
{link}
</a>
</Box>
</Box>
);
@ -71,12 +69,12 @@ export const AmoAccountInfo: FC<AmoAccountInfoProps> = ({
gap: isMobile ? "10px" : "20px",
}}
>
{infoItem("Amo ID", accountInfo.AmoUserID)}
{infoItem("Имя аккаунта", accountInfo.Name)}
{infoItem("Email аккаунта", accountInfo.Email)}
{infoItemLink("ЛК в amo", `https://${accountInfo.Subdomain}.amocrm.ru/dashboard/`)}
{infoItemLink("Профиль пользователя в amo", `https://${accountInfo.Subdomain}.amocrm.ru/settings/users/`)}
{infoItem("Страна пользователя", accountInfo.Country)}
{infoItem("Amo ID", accountInfo.amoUserID)}
{infoItem("Имя аккаунта", accountInfo.name)}
{infoItem("Email аккаунта", accountInfo.email)}
{infoItemLink("ЛК в amo", `https://${accountInfo.subdomain}.amocrm.ru/dashboard/`)}
{infoItemLink("Профиль пользователя в amo", `https://${accountInfo.subdomain}.amocrm.ru/settings/users/`)}
{infoItem("Страна пользователя", accountInfo.country)}
</Box>
<StepButtonsBlock
isSmallBtnDisabled={true}

@ -1,35 +1,42 @@
import {
Dialog,
IconButton,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import React, { FC, useEffect, useMemo, useState } from "react";
import Box from "@mui/material/Box";
import CloseIcon from "@mui/icons-material/Close";
import { AmoRemoveAccount } from "./AmoRemoveAccount/AmoRemoveAccount";
import { AmoLogin } from "./AmoLogin/AmoLogin";
import { AmoStep2 } from "./AmoStep2/AmoStep2";
import { AmoStep3 } from "./AmoStep3/AmoStep3";
import { AmoStep4 } from "./AmoStep4/AmoStep4";
import { AmoStep6 } from "./IntegrationStep6/AmoStep6";
import { AmoStep7 } from "./IntegrationStep7/AmoStep7";
import { AmoModalTitle } from "./AmoModalTitle/AmoModalTitle";
import { AmoSettingsBlock } from "./SettingsBlock/AmoSettingsBlock";
import { AmoAccountInfo } from "./AmoAccountInfo/AmoAccountInfo";
import { AccountResponse, IntegrationRules, getAccount, getIntegrationRules, setIntegrationRules, updateIntegrationRules } from "@api/integration";
import React, { FC, useCallback, useEffect, useMemo, useState } from "react";
import { Dialog, IconButton, Typography, useMediaQuery, useTheme, Box } from "@mui/material";
import { useQuestions } from "@/stores/questions/hooks";
import { redirect } from "react-router-dom";
import { enqueueSnackbar } from "notistack";
export type TitleKeys = "Contact" | "Company" | "Lead" | "Customer";
export type TagQuestionObject = {
backendId: string;
title: string;
};
import CloseIcon from "@mui/icons-material/Close";
import { AmoRemoveAccount } from "./AmoRemoveAccount/AmoRemoveAccount";
import { AmoDeleteTagQuestion } from "./AmoRemoveAccount/AmoDeleteTagQuestion";
import { AmoLogin } from "./AmoLogin/AmoLogin";
import { Pipelines } from "./Pipelines/Pipelines";
import { PipelineSteps } from "./PipelineSteps/PipelineSteps";
import { DealPerformers } from "./DealPerformers/DealPerformers";
import { AmoTags } from "./AmoTags/AmoTags";
import { AmoQuestions } from "./AmoQuestions/AmoQuestions";
import { AmoModalTitle } from "./AmoModalTitle/AmoModalTitle";
import { AmoSettingsBlock } from "./SettingsBlock/AmoSettingsBlock";
import { AmoAccountInfo } from "./AmoAccountInfo/AmoAccountInfo";
import {
AccountResponse,
FieldsRule,
IntegrationRules,
Pipeline,
Step,
User,
getAccount,
getIntegrationRules,
getPipelines,
getSteps,
getTags,
getUsers,
setIntegrationRules,
updateIntegrationRules,
} from "@api/integration";
import type { QuestionID } from "@api/integration";
import { useAmoIntegration } from "./useAmoIntegration";
import { QuestionKeys, TagKeys, TagQuestionHC } from "./types";
export type TQuestionEntity = Record<TitleKeys, number[] | []>;
type IntegrationsModalProps = {
isModalOpen: boolean;
handleCloseModal: () => void;
@ -37,85 +44,108 @@ type IntegrationsModalProps = {
quizID: number | undefined;
};
export type TagKeys = "Contact" | "Company" | "Lead" | "Customer";
export type TTags = Record<TagKeys, number[] | []>;
export const AmoCRMModal: FC<IntegrationsModalProps> = ({ isModalOpen, handleCloseModal, companyName, quizID }) => {
//Если нет контекста квиза, то и делать на этой страничке нечего
if (quizID === undefined) {
redirect("/list");
return null;
}
export const AmoCRMModal: FC<IntegrationsModalProps> = ({
isModalOpen,
handleCloseModal,
companyName,
quizID,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
const { questions } = useQuestions();
const minifiedQuestions = useMemo(
() =>
questions
.filter((q) => q.type !== "result" && q.type !== null)
.map(({ backendId, title }) => ({
id: backendId.toString() as string,
title,
})),
[questions]
);
const [step, setStep] = useState<number>(0);
const [isSettingsBlock, setIsSettingsBlock] = useState<boolean>(false);
const [isRemoveAccount, setIsRemoveAccount] = useState<boolean>(false);
const [isTryRemoveAccount, setIsTryRemoveAccount] = useState<boolean>(false);
const [openDelete, setOpenDelete] = useState<TagQuestionHC | null>(null);
const [firstRules, setFirstRules] = useState<boolean>(false);
const [accountInfo, setAccountInfo] = useState<AccountResponse | null>(null);
const [selectedPipelinePerformer, setSelectedPipelinePerformer] = useState<
string | null
>(null);
const [selectedPipeline, setSelectedPipeline] = useState<string | null>(null);
const [selectedStepsPerformer, setSelectedStepsPerformer] = useState<
string | null
>(null);
const [selectedStep, setSelectedStep] = useState<string | null>(null);
const [selectedDealPerformer, setSelectedDealPerformer] = useState<
string | null
>(null);
const [questionEntity, setQuestionEntity] = useState<TQuestionEntity>({
Lead: [],
Contact: [],
Company: [],
Customer: [],
const {
isloadingPage,
firstRules,
accountInfo,
arrayOfPipelines,
arrayOfPipelinesSteps,
arrayOfUsers,
arrayOfTags,
selectedPipeline,
setSelectedPipeline,
selectedPipelineStep,
setSelectedPipelineStep,
selectedDealUser,
setSelectedDealPerformer,
questionsBackend,
selectedTags,
setSelectedTags,
selectedQuestions,
setSelectedQuestions,
setPageOfPipelines,
setPageOfPipelinesSteps,
setPageOfUsers,
setPageOfTags,
} = useAmoIntegration({
quizID,
isModalOpen,
isTryRemoveAccount,
});
const [tags, setTags] = useState<TTags>({
Lead: [],
Contact: [],
Company: [],
Customer: [],
});
console.log(questionEntity)
console.log(tags)
useEffect(() => {
if (isModalOpen && quizID !== undefined && !isRemoveAccount) {
const fetchAccount = async () => {
const [account, error] = await getAccount();
const handleAddTagQuestion = useCallback(
(scope: QuestionKeys | TagKeys, id: string, type: "question" | "tag") => {
if (!scope || !id) return;
if (error) {
if (!error.includes("Not Found")) enqueueSnackbar(error)
setAccountInfo(null);
}
if (account) {
setAccountInfo(account);
}
};
const fetchRules = async () => {
const [settingsResponse, error] = await getIntegrationRules(quizID.toString());
if (type === "tag") {
setSelectedTags((prevState) => ({
...prevState,
[scope]: [...prevState[scope as TagKeys], id],
}));
}
if (error) {
if (error === "first") setFirstRules(true);
if (!error.includes("Not Found") && !error.includes("first")) enqueueSnackbar(error);
}
if (settingsResponse) {
console.log(settingsResponse);
setFirstRules(false);
}
};
if (type === "question") {
setSelectedQuestions((prevState) => ({
...prevState,
[scope]: [...prevState[scope as QuestionKeys], id],
}));
}
},
[setSelectedQuestions, setSelectedTags]
);
const handleDeleteTagQuestion = useCallback(() => {
if (openDelete === null || !openDelete.scope || !openDelete.id || !openDelete.type) return;
if (openDelete.type === "tag") {
let newArray = selectedTags[openDelete.scope];
const index = newArray.indexOf(openDelete.id);
if (index !== -1) newArray.splice(index, 1);
fetchAccount();
fetchRules();
setSelectedTags((prevState) => ({
...prevState,
[openDelete.scope]: newArray,
}));
}
}, [isModalOpen, isRemoveAccount]);
if (openDelete.type === "question") {
let newArray = selectedQuestions[openDelete.scope as QuestionKeys];
const index = newArray.indexOf(openDelete.id);
if (index !== -1) newArray.splice(index, 1);
setSelectedQuestions((prevState) => ({
...prevState,
[openDelete.scope]: newArray,
}));
}
setOpenDelete(null);
}, [openDelete]);
const handleNextStep = () => {
setStep((prevState) => prevState + 1);
@ -124,60 +154,53 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
setStep((prevState) => prevState - 1);
};
const handleSave = () => {
console.log("На отправку")
console.log({
PerformerID: selectedDealPerformer,
PipelineID: selectedPipeline,
StepID: selectedStep,
Fieldsrule: {
...questionEntity
},
TagsToAdd: {
...tags
}
})
if (quizID === undefined) return
if (selectedPipeline?.toString().length === 0) return enqueueSnackbar("Выберите воронку")
if (selectedPipeline?.toString().length === 0) return enqueueSnackbar("Выберите этап воронки")
if (quizID === undefined) return;
if (selectedPipeline === null) return enqueueSnackbar("Выберите воронку");
if (selectedPipeline === null) return enqueueSnackbar("Выберите этап воронки");
const body = {
PipelineID: Number(selectedPipeline),
StepID: Number(selectedPipelineStep),
PerformerID: Number(selectedDealUser),
// FieldsRule: questionsBackend,
TagsToAdd: selectedTags,
};
const FieldsRule = {
Company: [{ QuestionID: {} }],
Lead: [{ QuestionID: {} }],
Customer: [{ QuestionID: {} }],
};
console.log("selectedQuestions");
console.log(selectedQuestions);
for (let key in FieldsRule) {
console.log("current key ", key);
selectedQuestions[key as QuestionKeys].forEach((id) => {
FieldsRule[key as QuestionKeys][0].QuestionID[id] = 0;
});
}
for (let key in body.TagsToAdd) {
body.TagsToAdd[key as TagKeys] = body.TagsToAdd[key as TagKeys].map((id) => Number(id));
}
body.FieldsRule = FieldsRule;
console.log("На отправку");
console.log(body);
if (firstRules) {
setIntegrationRules(quizID.toString(), {
PerformerID: Number(selectedDealPerformer),
PipelineID: Number(selectedPipeline),
StepID: Number(selectedStep),
Fieldsrule: {
...questionEntity
},
TagsToAdd: {
...tags
}
})
setIntegrationRules(quizID.toString(), body);
} else {
const body = {
Fieldsrule: {
...questionEntity
},
TagsToAdd: {
...tags
}
};
if (selectedDealPerformer?.toString() > 0) body.PerformerID = Number(selectedDealPerformer)
if (selectedPipeline?.toString() > 0) body.PipelineID = Number(selectedPipeline)
if (selectedStep?.toString() > 0) body.StepID = Number(selectedStep)
updateIntegrationRules(quizID.toString(), body)
updateIntegrationRules(quizID.toString(), body);
}
// handleCloseModal();
// setStep(1);
handleCloseModal();
setStep(1);
};
const steps = useMemo(
() => [
{
title: accountInfo
? "Информация об аккаунте"
: "Авторизация в аккаунте",
title: accountInfo ? "Информация об аккаунте" : "Авторизация в аккаунте",
isSettingsAvailable: false,
component: accountInfo ? (
<AmoAccountInfo
@ -192,11 +215,12 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
title: "Выбор воронки",
isSettingsAvailable: true,
component: (
<AmoStep2
<Pipelines
users={arrayOfUsers}
pipelines={arrayOfPipelines}
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
selectedPipelinePerformer={selectedDealPerformer}
setSelectedPipelinePerformer={setSelectedDealPerformer}
selectedDealUser={selectedDealUser}
setSelectedDealPerformer={setSelectedDealPerformer}
selectedPipeline={selectedPipeline}
setSelectedPipeline={setSelectedPipeline}
@ -207,14 +231,15 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
title: "Выбор этапа воронки",
isSettingsAvailable: true,
component: (
<AmoStep3
<PipelineSteps
users={arrayOfUsers}
selectedDealUser={selectedDealUser}
selectedStep={selectedPipelineStep}
steps={arrayOfPipelinesSteps}
setSelectedDealPerformer={setSelectedDealPerformer}
setSelectedStep={setSelectedPipelineStep}
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
selectedStepsPerformer={selectedDealPerformer}
setSelectedStepsPerformer={setSelectedDealPerformer}
selectedStep={selectedStep}
setSelectedStep={setSelectedStep}
pipelineId={selectedPipeline}
/>
),
},
@ -222,10 +247,11 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
title: "Сделка",
isSettingsAvailable: true,
component: (
<AmoStep4
<DealPerformers
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
selectedDealPerformer={selectedDealPerformer}
users={arrayOfUsers}
selectedDealUser={selectedDealUser}
setSelectedDealPerformer={setSelectedDealPerformer}
/>
),
@ -234,11 +260,14 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
title: "Добавление тегов",
isSettingsAvailable: true,
component: (
<AmoStep6
tagsNames={tags}
<AmoTags
tagsItems={arrayOfTags}
selectedTags={selectedTags}
openDelete={setOpenDelete}
handleScroll={() => {}}
handleAddTag={handleAddTagQuestion}
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
setTagsNames={setTags}
/>
),
},
@ -246,33 +275,37 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
title: "Соотнесение вопросов и сущностей",
isSettingsAvailable: true,
component: (
<AmoStep7
questionEntity={questionEntity}
setQuestionEntity={setQuestionEntity}
<AmoQuestions
questionsItems={minifiedQuestions}
selectedQuestions={selectedQuestions}
openDelete={setOpenDelete}
handleAddQuestion={handleAddTagQuestion}
handlePrevStep={handlePrevStep}
handleNextStep={handleSave}
questions={questions}
/>
),
},
],
[
accountInfo,
questionEntity,
selectedPipelinePerformer,
arrayOfPipelines,
arrayOfPipelinesSteps,
arrayOfUsers,
arrayOfTags,
selectedPipeline,
selectedStepsPerformer,
selectedStep,
selectedDealPerformer,
tags,
],
selectedPipelineStep,
selectedDealUser,
selectedQuestions,
selectedTags,
arrayOfPipelines,
arrayOfPipelinesSteps,
arrayOfUsers,
minifiedQuestions,
arrayOfTags,
]
);
const stepTitles = steps.map((step) => step.title);
//Если нет контекста квиза, то и делать на этой страничке нечего
if (quizID === undefined) redirect("/list")
return (
<Dialog
open={isModalOpen}
@ -315,9 +348,7 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
top: "15px",
}}
>
<CloseIcon
sx={{ width: "12px", height: "12px", transform: "scale(1.5)" }}
/>
<CloseIcon sx={{ width: "12px", height: "12px", transform: "scale(1.5)" }} />
</IconButton>
<Box
sx={{
@ -335,31 +366,36 @@ export const AmoCRMModal: FC<IntegrationsModalProps> = ({
isSettingsBlock={isSettingsBlock}
setIsSettingsBlock={setIsSettingsBlock}
setStep={setStep}
startRemoveAccount={() => setIsRemoveAccount(true)}
startRemoveAccount={() => setIsTryRemoveAccount(true)}
/>
{isRemoveAccount && (
<AmoRemoveAccount
stopThisPage={() => setIsRemoveAccount(false)}
{openDelete !== null ? (
<AmoDeleteTagQuestion
close={() => setOpenDelete(null)}
deleteItem={handleDeleteTagQuestion}
/>
)}
{isSettingsBlock && (
<Box sx={{ flexGrow: 1, width: "100%" }}>
<AmoSettingsBlock
stepTitles={stepTitles}
setIsSettingsBlock={setIsSettingsBlock}
setStep={setStep}
selectedDealPerformer={selectedDealPerformer}
selectedFunnelPerformer={selectedPipelinePerformer}
selectedFunnel={selectedPipeline}
selectedStagePerformer={selectedStepsPerformer}
selectedStage={selectedStep}
questionEntity={questionEntity}
tags={tags}
/>
</Box>
)}
{!isSettingsBlock && !isRemoveAccount && (
<Box sx={{ flexGrow: 1, width: "100%" }}>{steps[step].component}</Box>
) : (
<>
{isTryRemoveAccount && <AmoRemoveAccount stopThisPage={() => setIsTryRemoveAccount(false)} />}
{isSettingsBlock && (
<Box sx={{ flexGrow: 1, width: "100%" }}>
<AmoSettingsBlock
stepTitles={stepTitles}
setIsSettingsBlock={setIsSettingsBlock}
setStep={setStep}
selectedDealUser={arrayOfUsers.find((u) => u.id === selectedDealUser)?.title || "не указан"}
selectedFunnel={arrayOfPipelines.find((p) => p.id === selectedPipeline)?.title || "нет данных"}
selectedStage={
arrayOfPipelinesSteps.find((s) => s.id === selectedPipelineStep)?.title || "нет данных"
}
selectedQuestions={selectedQuestions}
selectedTags={selectedTags}
/>
</Box>
)}
{!isSettingsBlock && !isTryRemoveAccount && (
<Box sx={{ flexGrow: 1, width: "100%" }}>{steps[step].component}</Box>
)}
</>
)}
</Box>
</Dialog>

@ -27,7 +27,6 @@ export const AmoLogin: FC<IntegrationStep1Props> = ({ handleNextStep }) => {
const isMobile = useMediaQuery(theme.breakpoints.down(600));
const onAmoClick = async () => {
console.log("Amo click");
const [url, error] = await connectAmo();
if (url && !error) {
window.open(url, "_blank");
@ -131,9 +130,8 @@ export const AmoLogin: FC<IntegrationStep1Props> = ({ handleNextStep }) => {
lineHeight: "1",
}}
>
После нажатия на кнопку - "Подключить", вас переадресует на страницу
подключения интеграции в ваш аккаунт AmoCRM. Пожалуйста, согласитесь
на всё, что мы предлагаем, иначе чуда не случится.
После нажатия на кнопку - "Подключить", вас переадресует на страницу подключения интеграции в ваш аккаунт
AmoCRM. Пожалуйста, согласитесь на всё, что мы предлагаем, иначе чуда не случится.
</Typography>
</Box>
<Box sx={{ marginTop: "50px" }}>

@ -19,16 +19,15 @@ export const AmoModalTitle: FC<AmoModalTitleProps> = ({
setIsSettingsBlock,
isSettingsBlock,
setStep,
startRemoveAccount
startRemoveAccount,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
console.log(isSettingsBlock)
const handleClick = useCallback(async () => {
if (isSettingsBlock) {
startRemoveAccount();
setIsSettingsBlock(false)
setIsSettingsBlock(false);
setStep(0);
return;
}

@ -0,0 +1,85 @@
import { FC, useState } from "react";
import { ItemsSelectionView } from "./ItemsSelectionView/ItemsSelectionView";
import { ItemDetailsView } from "./ItemDetailsView/ItemDetailsView";
import { Box } from "@mui/material";
import { QuestionKeys, SelectedQuestions, TagKeys, TagQuestionHC } from "../types";
type MinifiedData = {
id: string;
title: string;
subTitle?: string;
};
type Props = {
questionsItems: MinifiedData[] | [];
selectedQuestions: SelectedQuestions;
handleAddQuestion: (scope: QuestionKeys | TagKeys, id: string, type: "question" | "tag") => void;
openDelete: (data: TagQuestionHC) => void;
handlePrevStep: () => void;
handleNextStep: () => void;
};
export const AmoQuestions: FC<Props> = ({
questionsItems,
selectedQuestions,
handleAddQuestion,
handlePrevStep,
handleNextStep,
openDelete,
}) => {
const [isSelection, setIsSelection] = useState<boolean>(false);
const [activeScope, setActiveScope] = useState<QuestionKeys | null>(null);
const [selectedQuestion, setSelectedQuestion] = useState<string | null>(null);
const handleAdd = () => {
if (activeScope === null || selectedQuestion === null) return;
setActiveScope(null);
handleAddQuestion(activeScope, selectedQuestion, "question");
};
const handleDelete = (id: string, scope: QuestionKeys) => {
openDelete({
id,
scope,
type: "question",
});
};
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100%",
flexGrow: 1,
}}
>
{isSelection && activeScope !== null ? (
// Здесь выбираем элемент в табличку
<ItemsSelectionView
items={questionsItems}
selectedItemId={selectedQuestion}
setSelectedItem={setSelectedQuestion}
onSmallBtnClick={() => {
setActiveScope(null);
setIsSelection(false);
}}
onLargeBtnClick={() => {
handleAdd();
setActiveScope(null);
setIsSelection(false);
}}
/>
) : (
// Табличка
<ItemDetailsView
items={questionsItems}
setActiveScope={setActiveScope}
selectedQuestions={selectedQuestions}
setIsSelection={setIsSelection}
handleLargeBtn={handleNextStep}
handleSmallBtn={handlePrevStep}
deleteHC={handleDelete}
/>
)}
</Box>
);
};

@ -0,0 +1,69 @@
import { Box, IconButton, Typography, useTheme } from "@mui/material";
import { FC } from "react";
import Trash from "@icons/trash";
type AnswerItemProps = {
fieldName: string;
fieldValue: string;
deleteHC: () => void;
};
export const AnswerItem: FC<AnswerItemProps> = ({ fieldName, fieldValue, deleteHC }) => {
const theme = useTheme();
return (
<Box
sx={{
padding: "10px 20px",
height: "140px",
borderBottom: `1px solid ${theme.palette.background.default}`,
display: "flex",
alignItems: "center",
flexDirection: "column",
justifyContent: "space-between",
}}
>
<Box
sx={{
overflow: "hidden",
width: "100%",
}}
>
<Typography
sx={{
fontSize: "14px",
fontWeight: 500,
color: theme.palette.grey3.main,
textOverflow: "ellipsis",
overflow: "hidden",
width: "100%",
whiteSpace: "nowrap",
}}
>
{fieldName}
</Typography>
<Typography
sx={{
fontSize: "14px",
fontWeight: 400,
color: theme.palette.grey3.main,
textOverflow: "ellipsis",
overflow: "hidden",
width: "100%",
whiteSpace: "nowrap",
}}
>
{fieldValue}
</Typography>
</Box>
<IconButton
sx={{
m: "auto",
}}
onClick={deleteHC}
>
<Trash />
</IconButton>
</Box>
);
};

@ -1,19 +1,19 @@
import { Box, Typography, useTheme } from "@mui/material";
import { Box, IconButton, Typography, useTheme } from "@mui/material";
import { FC } from "react";
import { IconBtnAdd } from "./IconBtnAdd/IconBtnAdd";
import { AnswerItem } from "./AnswerItem/AnswerItem";
import { TagKeys, TitleKeys, TQuestionEntity, TTags } from "../../AmoCRMModal";
import { QuestionKeys, SelectedQuestions, TagKeys, SelectedTags, MinifiedData } from "../../types";
type ItemProps = {
title: TitleKeys | TagKeys;
items: MinifiedData[];
title: QuestionKeys | TagKeys;
onAddBtnClick: () => void;
data: TQuestionEntity | TTags;
data: SelectedTags | SelectedQuestions;
deleteHC: (id: string, scope: QuestionKeys | TagKeys) => void;
};
export const Item: FC<ItemProps> = ({ title, onAddBtnClick, data }) => {
export const Item: FC<ItemProps> = ({ items, title, onAddBtnClick, data, deleteHC }) => {
const theme = useTheme();
console.log("title")
console.log(data)
const titleDictionary = {
Company: "Компания",
Lead: "Сделка",
@ -44,16 +44,15 @@ export const Item: FC<ItemProps> = ({ title, onAddBtnClick, data }) => {
height: "40px",
}}
>
<Typography sx={{ fontSize: "16px", fontWeight: 500 }}>
{translatedTitle}
</Typography>
<Typography sx={{ fontSize: "16px", fontWeight: 500 }}>{translatedTitle}</Typography>
</Box>
{selectedOptions &&
selectedOptions.map((text, index) => (
selectedOptions.map((id, index) => (
<AnswerItem
key={text + index}
key={id + index}
fieldValue={"Значение поля"}
fieldName={text}
fieldName={items.find((e) => e.id === id)?.title || id}
deleteHC={() => deleteHC(selectedOptions[index], title)}
/>
))}

@ -2,24 +2,28 @@ import { Box, useTheme } from "@mui/material";
import { Item } from "../Item/Item";
import { StepButtonsBlock } from "../../StepButtonsBlock/StepButtonsBlock";
import { FC } from "react";
import { TQuestionEntity } from "../../AmoCRMModal";
import { MinifiedData, QuestionKeys, SelectedQuestions } from "../../types";
type TitleKeys = "Contact" | "Company" | "Lead" | "Customer";
type ItemDetailsViewProps = {
items: MinifiedData[];
setIsSelection: (value: boolean) => void;
handleSmallBtn: () => void;
handleLargeBtn: () => void;
questionEntity: TQuestionEntity;
setActiveItem: (value: string | null) => void;
selectedQuestions: SelectedQuestions;
setActiveScope: (value: QuestionKeys | null) => void;
deleteHC: (id: string, scope: QuestionKeys) => void;
};
export const ItemDetailsView: FC<ItemDetailsViewProps> = ({
items,
handleSmallBtn,
handleLargeBtn,
questionEntity,
setActiveItem,
selectedQuestions,
setIsSelection,
setActiveScope,
deleteHC,
}) => {
const theme = useTheme();
@ -48,16 +52,18 @@ export const ItemDetailsView: FC<ItemDetailsViewProps> = ({
justifyContent: "start",
}}
>
{questionEntity &&
Object.keys(questionEntity).map((item) => (
{selectedQuestions &&
Object.keys(selectedQuestions).map((item) => (
<Item
key={item}
title={item as TitleKeys}
title={item as QuestionKeys}
onAddBtnClick={() => {
setIsSelection(true);
setActiveItem(item);
setActiveScope(item as QuestionKeys);
}}
data={questionEntity}
items={items}
data={selectedQuestions}
deleteHC={deleteHC}
/>
))}
</Box>

@ -2,33 +2,32 @@ import { Box } from "@mui/material";
import { CustomRadioGroup } from "../../../../../components/CustomRadioGroup/CustomRadioGroup";
import { StepButtonsBlock } from "../../StepButtonsBlock/StepButtonsBlock";
import { FC } from "react";
import { AllTypesQuestion } from "@/model/questionTypes/shared";
import type { TagQuestionObject } from "@/pages/IntegrationsPage/IntegrationsModal/AmoCRMModal"
import { Tag } from "@api/integration";
import { TagKeys } from "../../types";
type MinifiedData = {
id: string;
title: string;
subTitle?: string;
};
type ItemsSelectionViewProps = {
type?: string;
items?: TagQuestionObject[];
selectedValue: string | null;
setSelectedValue: (value: string | null) => void;
items: MinifiedData[] | [];
selectedItemId?: string | null;
setSelectedItem: (value: string | null) => void;
handleScroll?: () => void;
onLargeBtnClick: () => void;
onSmallBtnClick: () => void;
setTags: (setValueFunc: (value: Tag[]) => Tag[]) => void;
parentTags: Tag[]
activeScope: TagKeys;
};
export const ItemsSelectionView: FC<ItemsSelectionViewProps> = ({
items,
selectedValue,
setSelectedValue,
selectedItemId,
setSelectedItem,
handleScroll,
onLargeBtnClick,
onSmallBtnClick,
type,
parentTags,
setTags
activeScope,
}) => {
console.log("items тегов")
console.log(items)
return (
<Box
sx={{
@ -48,12 +47,11 @@ export const ItemsSelectionView: FC<ItemsSelectionViewProps> = ({
}}
>
<CustomRadioGroup
type={type}
items={items}
tags={parentTags}
setTags={setTags}
selectedValue={selectedValue}
setSelectedValue={setSelectedValue}
selectedItemId={selectedItemId}
setSelectedItem={setSelectedItem}
handleScroll={handleScroll}
activeScope={activeScope}
/>
</Box>
<Box

@ -0,0 +1,48 @@
import { FC } from "react";
import { Button, Typography, useTheme, Box } from "@mui/material";
interface Props {
deleteItem: () => void;
close: () => void;
}
export const AmoDeleteTagQuestion: FC<Props> = ({ close, deleteItem }) => {
const theme = useTheme();
return (
<Box
sx={{
mt: "30px",
}}
>
<Typography textAlign="center">Вы хотите удалить элемент?</Typography>
<Box
sx={{
display: "flex",
justifyContent: "space-evenly",
flexWrap: "wrap",
margin: "30px auto",
}}
>
<Button
variant="contained"
sx={{
width: "150px",
}}
onClick={close}
>
отмена
</Button>
<Button
variant="contained"
sx={{
width: "150px",
}}
onClick={deleteItem}
>
удалить
</Button>
</Box>
</Box>
);
};

@ -0,0 +1,84 @@
import { FC, useState } from "react";
import { Box } from "@mui/material";
import { ItemsSelectionView } from "../AmoQuestions/ItemsSelectionView/ItemsSelectionView";
import { TagsDetailsView } from "./TagsDetailsView/TagsDetailsView";
import { MinifiedData, QuestionKeys, SelectedTags, TagKeys, TagQuestionHC } from "../types";
type Props = {
tagsItems: MinifiedData[] | [];
selectedTags: SelectedTags;
handleAddTag: (scope: QuestionKeys | TagKeys, id: string, type: "question" | "tag") => void;
openDelete: (data: TagQuestionHC) => void;
handleScroll: () => void;
handlePrevStep: () => void;
handleNextStep: () => void;
};
export const AmoTags: FC<Props> = ({
tagsItems,
selectedTags,
handleAddTag,
openDelete,
handleScroll,
handlePrevStep,
handleNextStep,
}) => {
const [isSelection, setIsSelection] = useState<boolean>(false);
const [activeScope, setActiveScope] = useState<TagKeys | null>(null);
const [selectedTag, setSelectedTag] = useState<string | null>(null);
const handleAdd = () => {
if (activeScope === null || selectedTag === null) return;
setActiveScope(null);
handleAddTag(activeScope, selectedTag, "tag");
};
const handleDelete = (id: string, scope: TagKeys) => {
openDelete({
id,
scope,
type: "tag",
});
};
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100%",
flexGrow: 1,
}}
>
{isSelection && activeScope !== null ? (
// Здесь выбираем элемент в табличку
<ItemsSelectionView
items={tagsItems}
selectedItemId={selectedTag}
setSelectedItem={setSelectedTag}
handleScroll={handleScroll}
activeScope={activeScope}
onSmallBtnClick={() => {
setActiveScope(null);
setIsSelection(false);
}}
onLargeBtnClick={() => {
handleAdd();
setActiveScope(null);
setIsSelection(false);
}}
/>
) : (
// Табличка
<TagsDetailsView
items={tagsItems}
setActiveScope={setActiveScope}
selectedTags={selectedTags}
setIsSelection={setIsSelection}
handleNextStep={handleNextStep}
handlePrevStep={handlePrevStep}
deleteHC={handleDelete}
/>
)}
</Box>
);
};

@ -1,23 +1,27 @@
import { Box, Typography, useTheme } from "@mui/material";
import { StepButtonsBlock } from "../../StepButtonsBlock/StepButtonsBlock";
import { FC } from "react";
import { TagKeys, TTags } from "../../AmoCRMModal";
import { Item } from "../../IntegrationStep7/Item/Item";
import { Item } from "../../AmoQuestions/Item/Item";
import { MinifiedData, SelectedTags, TagKeys } from "../../types";
type TagsDetailsViewProps = {
items: MinifiedData[];
setIsSelection: (value: boolean) => void;
handlePrevStep: () => void;
handleNextStep: () => void;
tagsNames: TTags;
setActiveItem: (value: string | null) => void;
setActiveScope: (value: TagKeys | null) => void;
selectedTags: SelectedTags;
deleteHC: (id: string, scope: TagKeys) => void;
};
export const TagsDetailsView: FC<TagsDetailsViewProps> = ({
items,
setActiveScope,
selectedTags,
setIsSelection,
handlePrevStep,
handleNextStep,
tagsNames,
setActiveItem,
setIsSelection,
deleteHC,
}) => {
const theme = useTheme();
@ -52,11 +56,7 @@ export const TagsDetailsView: FC<TagsDetailsViewProps> = ({
alignItems: "center",
}}
>
<Typography
sx={{ fontSize: "14px", color: theme.palette.grey2.main }}
>
Результат
</Typography>
<Typography sx={{ fontSize: "14px", color: theme.palette.grey2.main }}>Результат</Typography>
</Box>
<Box
sx={{
@ -68,16 +68,18 @@ export const TagsDetailsView: FC<TagsDetailsViewProps> = ({
justifyContent: "start",
}}
>
{tagsNames &&
Object.keys(tagsNames).map((item) => (
{selectedTags &&
Object.keys(selectedTags).map((item) => (
<Item
key={item}
items={items}
title={item as TagKeys}
onAddBtnClick={() => {
setIsSelection(true);
setActiveItem(item);
setActiveScope(item as TagKeys);
}}
data={tagsNames}
data={selectedTags}
deleteHC={deleteHC}
/>
))}
</Box>

@ -2,18 +2,21 @@ import { Box, useMediaQuery, useTheme } from "@mui/material";
import { FC } from "react";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
import { CustomSelect } from "../../../../components/CustomSelect/CustomSelect";
import { MinifiedData } from "../types";
type AmoStep4Props = {
type Props = {
users: MinifiedData[];
handlePrevStep: () => void;
handleNextStep: () => void;
selectedDealPerformer: string | null;
selectedDealUser: string | null;
setSelectedDealPerformer: (value: string | null) => void;
};
export const AmoStep4: FC<AmoStep4Props> = ({
export const DealPerformers: FC<Props> = ({
users,
handlePrevStep,
handleNextStep,
selectedDealPerformer,
selectedDealUser,
setSelectedDealPerformer,
}) => {
const theme = useTheme();
@ -32,9 +35,10 @@ export const AmoStep4: FC<AmoStep4Props> = ({
>
<Box sx={{ width: "100%", marginTop: "20px", zIndex: 3 }}>
<CustomSelect
selectedItem={selectedDealPerformer}
items={users}
selectedItemId={selectedDealUser}
setSelectedItem={setSelectedDealPerformer}
type={"typeUsers"}
handleScroll={() => {}}
/>
</Box>
<Box

@ -1,87 +0,0 @@
import { useTheme } from "@mui/material";
import {
Dispatch,
FC,
SetStateAction,
useCallback,
useEffect,
useMemo,
useState,
} from "react";
import { TagKeys, TTags } from "../AmoCRMModal";
import Box from "@mui/material/Box";
import { ItemsSelectionView } from "../IntegrationStep7/ItemsSelectionView/ItemsSelectionView";
import { TagsDetailsView } from "./TagsDetailsView/TagsDetailsView";
import { Tag } from "@api/integration";
type Props = {
handleNextStep: () => void;
handlePrevStep: () => void;
tagsNames: TTags;
setTagsNames: Dispatch<SetStateAction<TTags>>;
};
export const AmoStep6: FC<Props> = ({
handleNextStep,
handlePrevStep,
tagsNames,
setTagsNames
}) => {
const theme = useTheme();
const [isSelection, setIsSelection] = useState<boolean>(false);
const [activeItem, setActiveItem] = useState<string | null>(null);
const [selectedValue, setSelectedValue] = useState<string | null>(null);
const [tags, setTags] = useState<Tag[]>([]);
const handleAdd = useCallback(() => {
if (!activeItem || !selectedValue) return;
setTagsNames((prevState) => ({
...prevState,
[activeItem]: [...prevState[activeItem as TagKeys], Number(selectedValue)],
}));
}, [activeItem, setTagsNames, selectedValue]);
const items = useMemo(
() => ["#тег с результатом 1", "#еще один тег с результатом 2", "#тег"],
[],
);
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100%",
flexGrow: 1,
}}
>
{isSelection ? (
<ItemsSelectionView
parentTags={tags}
setTags={setTags}
selectedValue={selectedValue}
setSelectedValue={setSelectedValue}
type={"typeTags"}
onSmallBtnClick={() => {
setActiveItem(null);
setIsSelection(false);
}}
onLargeBtnClick={() => {
handleAdd();
setActiveItem(null);
setIsSelection(false);
}}
/>
) : (
<TagsDetailsView
setIsSelection={setIsSelection}
handlePrevStep={handlePrevStep}
handleNextStep={handleNextStep}
tagsNames={tagsNames}
setActiveItem={setActiveItem}
/>
)}
</Box>
);
};

@ -1,110 +0,0 @@
import { useTheme } from "@mui/material";
import {
Dispatch,
FC,
SetStateAction,
useCallback,
useMemo,
useState,
} from "react";
import { ItemsSelectionView } from "./ItemsSelectionView/ItemsSelectionView";
import { ItemDetailsView } from "./ItemDetailsView/ItemDetailsView";
import { TagQuestionObject, TitleKeys, TQuestionEntity } from "../AmoCRMModal";
import Box from "@mui/material/Box";
import type { AllTypesQuestion } from "@model/questionTypes/shared"
import { getQuestionById } from "@/stores/questions/actions";
import { useQuestionsStore } from "@/stores/questions/store";
type Props = {
handlePrevStep: () => void;
handleNextStep: () => void;
questionEntity: TQuestionEntity;
setQuestionEntity: Dispatch<SetStateAction<TQuestionEntity>>;
questions: AllTypesQuestion[];
};
export const AmoStep7: FC<Props> = ({
handlePrevStep,
handleNextStep,
questionEntity,
setQuestionEntity,
questions,
}) => {
const theme = useTheme();
const [isSelection, setIsSelection] = useState<boolean>(false);
const [activeItem, setActiveItem] = useState<string | null>(null);
const [selectedValue, setSelectedValue] = useState<string | null>(null);
const handleAdd = useCallback(() => {
if (!activeItem || !selectedValue) return;
setQuestionEntity((prevState) => ({
...prevState,
[activeItem]: [...prevState[activeItem as TitleKeys], Number(selectedValue)],
}));
}, [activeItem, setQuestionEntity, selectedValue]);
const items: TagQuestionObject[] = useMemo(
() => Object.values(questions)
.filter(({ type }) =>
type !== "result"
&& type !== null)
.map(({ backendId, title }) => ({
backendId: backendId,
title
})),
[],
);
const translatedQuestionEntity = useMemo(() => {
const translated = {} as TQuestionEntity;
for (let key in questionEntity) {
translated[key] = questionEntity[key].map((id) =>
questions.find((q) => q.backendId === Number(id))?.title || id
)
}
console.log("questionEntity", questionEntity)
console.log("translated", translated)
return translated
},
[questionEntity],
)
return (
<Box
sx={{
display: "flex",
flexDirection: "column",
height: "100%",
flexGrow: 1,
}}
>
{isSelection ? (
<ItemsSelectionView
items={items}
type="typeQuestions"
selectedValue={selectedValue}
setSelectedValue={setSelectedValue}
onSmallBtnClick={() => {
setActiveItem(null);
setIsSelection(false);
}}
onLargeBtnClick={() => {
handleAdd();
setActiveItem(null);
setIsSelection(false);
}}
/>
) : (
<ItemDetailsView
setIsSelection={setIsSelection}
handleLargeBtn={handleNextStep}
handleSmallBtn={handlePrevStep}
questionEntity={translatedQuestionEntity}
setActiveItem={setActiveItem}
/>
)}
</Box>
);
};

@ -1,39 +0,0 @@
import { Box, Typography, useTheme } from "@mui/material";
import { FC } from "react";
type AnswerItemProps = {
fieldName: string;
fieldValue: string;
};
export const AnswerItem: FC<AnswerItemProps> = ({ fieldName, fieldValue }) => {
const theme = useTheme();
return (
<Box
sx={{
padding: "10px 20px",
height: "140px",
borderBottom: `1px solid ${theme.palette.background.default}`,
}}
>
<Typography
sx={{
fontSize: "14px",
fontWeight: 500,
color: theme.palette.grey3.main,
}}
>
{fieldName}
</Typography>
<Typography
sx={{
fontSize: "14px",
fontWeight: 400,
color: theme.palette.grey3.main,
}}
>
{fieldValue}
</Typography>
</Box>
);
};

@ -3,25 +3,30 @@ import { FC } from "react";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
import { CustomSelect } from "../../../../components/CustomSelect/CustomSelect";
import { CustomRadioGroup } from "../../../../components/CustomRadioGroup/CustomRadioGroup";
import { MinifiedData } from "../types";
type AmoStep3Props = {
type Props = {
users: MinifiedData[];
steps: MinifiedData[];
handlePrevStep: () => void;
handleNextStep: () => void;
selectedStepsPerformer: string | null;
setSelectedStepsPerformer: (value: string | null) => void;
selectedDealUser: string | null;
setSelectedDealPerformer: (value: string | null) => void;
selectedStep: string | null;
setSelectedStep: (value: string | null) => void;
pipelineId: string | null;
};
export const AmoStep3: FC<AmoStep3Props> = ({
handlePrevStep,
handleNextStep,
selectedStepsPerformer,
setSelectedStepsPerformer,
export const PipelineSteps: FC<Props> = ({
users,
selectedDealUser,
setSelectedDealPerformer,
steps,
selectedStep,
setSelectedStep,
pipelineId,
handlePrevStep,
handleNextStep,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
@ -39,9 +44,10 @@ export const AmoStep3: FC<AmoStep3Props> = ({
>
<Box sx={{ width: "100%", marginTop: "20px", zIndex: 3 }}>
<CustomSelect
selectedItem={selectedStepsPerformer}
type={"typeUsers"}
setSelectedItem={setSelectedStepsPerformer}
items={users}
selectedItemId={selectedDealUser}
setSelectedItem={setSelectedDealPerformer}
handleScroll={() => {}}
/>
</Box>
<Box
@ -53,11 +59,10 @@ export const AmoStep3: FC<AmoStep3Props> = ({
}}
>
<CustomRadioGroup
// @ts-ignore
pipelineId={pipelineId}
type={"typeSteps"}
selectedValue={selectedStep}
setSelectedValue={setSelectedStep}
items={steps}
selectedItemId={selectedStep}
setSelectedItem={setSelectedStep}
handleScroll={() => {}}
/>
</Box>
<Box

@ -3,24 +3,30 @@ import { FC } from "react";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
import { CustomSelect } from "../../../../components/CustomSelect/CustomSelect";
import { CustomRadioGroup } from "../../../../components/CustomRadioGroup/CustomRadioGroup";
import { MinifiedData } from "../types";
type AmoStep2Props = {
type Props = {
pipelines: MinifiedData[];
users: MinifiedData[];
handlePrevStep: () => void;
handleNextStep: () => void;
selectedPipelinePerformer: string | null;
setSelectedPipelinePerformer: (value: string | null) => void;
selectedDealUser: string | null;
setSelectedDealPerformer: (value: string | null) => void;
selectedPipeline: string | null;
setSelectedPipeline: (value: string | null) => void;
};
export const AmoStep2: FC<AmoStep2Props> = ({
handlePrevStep,
handleNextStep,
selectedPipelinePerformer,
setSelectedPipelinePerformer,
export const Pipelines: FC<Props> = ({
pipelines,
selectedPipeline,
setSelectedPipeline,
users,
selectedDealUser,
setSelectedDealPerformer,
handlePrevStep,
handleNextStep,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
@ -37,9 +43,10 @@ export const AmoStep2: FC<AmoStep2Props> = ({
>
<Box sx={{ width: "100%", marginTop: "20px", zIndex: 3 }}>
<CustomSelect
selectedItem={selectedPipelinePerformer}
setSelectedItem={setSelectedPipelinePerformer}
type={"typeUsers"}
items={users}
selectedItemId={selectedDealUser}
setSelectedItem={setSelectedDealPerformer}
handleScroll={() => {}}
/>
</Box>
<Box
@ -51,9 +58,10 @@ export const AmoStep2: FC<AmoStep2Props> = ({
}}
>
<CustomRadioGroup
selectedValue={selectedPipeline}
setSelectedValue={setSelectedPipeline}
type={"typePipelines"}
items={pipelines}
selectedItemId={selectedPipeline}
setSelectedItem={setSelectedPipeline}
handleScroll={() => {}}
/>
</Box>
<Box

@ -2,32 +2,28 @@ import { FC } from "react";
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { StepButtonsBlock } from "../StepButtonsBlock/StepButtonsBlock";
import { SettingItem } from "./SettingItem/SettingItem";
import { TQuestionEntity, TTags } from "../AmoCRMModal";
import { SelectedQuestions, SelectedTags } from "../types";
type AmoSettingsBlockProps = {
stepTitles: string[];
setStep: (value: number) => void;
setIsSettingsBlock: (value: boolean) => void;
selectedFunnelPerformer: string | null;
selectedFunnel: string | null;
selectedStagePerformer: string | null;
selectedStage: string | null;
selectedDealPerformer: string | null;
questionEntity: TQuestionEntity;
tags: TTags;
selectedDealUser: string | null;
selectedQuestions: SelectedQuestions;
selectedTags: SelectedTags;
};
export const AmoSettingsBlock: FC<AmoSettingsBlockProps> = ({
stepTitles,
setStep,
setIsSettingsBlock,
selectedFunnelPerformer,
selectedFunnel,
selectedStagePerformer,
selectedDealPerformer,
selectedDealUser,
selectedStage,
questionEntity,
tags,
selectedQuestions,
selectedTags,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
@ -61,13 +57,11 @@ export const AmoSettingsBlock: FC<AmoSettingsBlockProps> = ({
title={title}
setIsSettingsBlock={setIsSettingsBlock}
setStep={setStep}
selectedFunnelPerformer={selectedFunnelPerformer}
selectedDealUser={selectedDealUser}
selectedFunnel={selectedFunnel}
selectedStagePerformer={selectedStagePerformer}
selectedDealPerformer={selectedDealPerformer}
selectedStage={selectedStage}
questionEntity={questionEntity}
tags={tags}
selectedQuestions={selectedQuestions}
selectedTags={selectedTags}
/>
))}
</Box>

@ -4,7 +4,7 @@ import { Typography, useMediaQuery, useTheme } from "@mui/material";
import { SettingItemHeader } from "./SettingItemHeader/SettingItemHeader";
import { ResponsiblePerson } from "./ResponsiblePerson/ResponsiblePerson";
import { SelectedParameter } from "./SelectedParameter/SelectedParameter";
import { TQuestionEntity, TTags } from "../../AmoCRMModal";
import { SelectedQuestions, SelectedTags } from "../../types";
type SettingItemProps = {
step: number;
@ -14,10 +14,10 @@ type SettingItemProps = {
selectedFunnelPerformer: string | null;
selectedFunnel: string | null;
selectedStagePerformer: string | null;
selectedDealPerformer: string | null;
selectedDealUser: string | null;
selectedStage: string | null;
questionEntity: TQuestionEntity;
tags: TTags;
selectedQuestions: SelectedQuestions;
selectedTags: SelectedTags;
};
export const SettingItem: FC<SettingItemProps> = ({
@ -28,14 +28,13 @@ export const SettingItem: FC<SettingItemProps> = ({
selectedFunnelPerformer,
selectedFunnel,
selectedStagePerformer,
selectedDealPerformer,
selectedDealUser,
selectedStage,
questionEntity,
tags,
selectedQuestions,
selectedTags,
}) => {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(600));
if (step === 0) {
return;
}
@ -44,7 +43,7 @@ export const SettingItem: FC<SettingItemProps> = ({
if (step === 1) {
return (
<>
<ResponsiblePerson performer={selectedFunnelPerformer} />
<ResponsiblePerson performer={selectedDealUser} />
<SelectedParameter parameter={selectedFunnel} />
</>
);
@ -52,7 +51,7 @@ export const SettingItem: FC<SettingItemProps> = ({
if (step === 2) {
return (
<>
<ResponsiblePerson performer={selectedStagePerformer} />
<ResponsiblePerson performer={selectedDealUser} />
<SelectedParameter parameter={selectedStage} />
</>
);
@ -60,14 +59,12 @@ export const SettingItem: FC<SettingItemProps> = ({
if (step === 3) {
return (
<>
<ResponsiblePerson performer={selectedDealPerformer} />
<ResponsiblePerson performer={selectedDealUser} />
</>
);
}
if (step === 4) {
const isFilled = Object.values(questionEntity).some(
(array) => array.length > 0,
);
const isFilled = Object.values(selectedTags).some((array) => array.length > 0);
const status = isFilled ? "Заполнено" : "Не заполнено";
return (
@ -97,7 +94,7 @@ export const SettingItem: FC<SettingItemProps> = ({
);
}
if (step === 5) {
const isFilled = Object.values(tags).some((array) => array.length > 0);
const isFilled = Object.values(selectedQuestions).some((array) => array.length > 0);
const status = isFilled ? "Заполнено" : "Не заполнено";
return (
@ -132,10 +129,10 @@ export const SettingItem: FC<SettingItemProps> = ({
selectedFunnelPerformer,
selectedFunnel,
selectedStagePerformer,
selectedDealPerformer,
selectedDealUser,
selectedStage,
questionEntity,
tags,
selectedQuestions,
selectedTags,
]);
return (

@ -0,0 +1,18 @@
export type TagKeys = "Contact" | "Company" | "Lead" | "Customer";
export type SelectedTags = Record<TagKeys, number[]>;
export type QuestionKeys = "Company" | "Lead" | "Customer";
export type SelectedQuestions = Record<QuestionKeys, string[]>;
export type MinifiedData = {
id: string;
title: string;
subTitle?: string;
entity?: TagKeys;
};
export type TagQuestionHC = {
scope: QuestionKeys | TagKeys;
id: string;
type: "question" | "tag";
};

@ -0,0 +1,250 @@
import { useEffect, useState } from "react";
import { enqueueSnackbar } from "notistack";
import type { TagKeys, SelectedTags, QuestionKeys, SelectedQuestions, MinifiedData } from "./types";
import {
AccountResponse,
getIntegrationRules,
getPipelines,
getSteps,
getTags,
getUsers,
getAccount,
FieldsRule,
} from "@/api/integration";
const SIZE = 75;
interface Props {
isModalOpen: boolean;
isTryRemoveAccount: boolean;
quizID: number;
}
export const useAmoIntegration = ({ isModalOpen, isTryRemoveAccount, quizID }: Props) => {
const [isloadingPage, setIsLoadingPage] = useState<boolean>(true);
const [firstRules, setFirstRules] = useState<boolean>(false);
const [accountInfo, setAccountInfo] = useState<AccountResponse | null>(null);
const [arrayOfPipelines, setArrayOfPipelines] = useState<MinifiedData[]>([]);
const [arrayOfPipelinesSteps, setArrayOfPipelinesSteps] = useState<MinifiedData[]>([]);
const [arrayOfUsers, setArrayOfUsers] = useState<MinifiedData[]>([]);
const [arrayOfTags, setArrayOfTags] = useState<MinifiedData[]>([]);
const [selectedPipeline, setSelectedPipeline] = useState<string | null>(null);
const [selectedPipelineStep, setSelectedPipelineStep] = useState<string | null>(null);
const [selectedDealUser, setSelectedDealPerformer] = useState<string | null>(null);
const [questionsBackend, setQuestionsBackend] = useState<FieldsRule>({} as FieldsRule);
const [selectedTags, setSelectedTags] = useState<SelectedTags>({
Lead: [],
Contact: [],
Company: [],
Customer: [],
});
const [selectedQuestions, setSelectedQuestions] = useState<SelectedQuestions>({
Lead: [],
Company: [],
Customer: [],
});
const [pageOfPipelines, setPageOfPipelines] = useState(1);
const [pageOfPipelinesSteps, setPageOfPipelinesSteps] = useState(1);
const [pageOfUsers, setPageOfUsers] = useState(1);
const [pageOfTags, setPageOfTags] = useState(1);
useEffect(() => {
if (isModalOpen && !isTryRemoveAccount) {
const fetchAccountRules = async () => {
setIsLoadingPage(true);
const [account, accountError] = await getAccount();
if (accountError) {
if (!accountError.includes("Not Found")) enqueueSnackbar(accountError);
setAccountInfo(null);
}
if (account) {
setAccountInfo(account);
}
const [settingsResponse, rulesError] = await getIntegrationRules(quizID.toString());
if (rulesError) {
if (rulesError === "first") setFirstRules(true);
if (!rulesError.includes("Not Found") && !rulesError.includes("first")) enqueueSnackbar(rulesError);
}
if (settingsResponse) {
if (settingsResponse.PipelineID) setSelectedPipeline(settingsResponse.PipelineID.toString());
if (settingsResponse.StepID) setSelectedPipelineStep(settingsResponse.StepID.toString());
if (settingsResponse.PerformerID) setSelectedDealPerformer(settingsResponse.PerformerID.toString());
if (Boolean(settingsResponse.FieldsRule) && Object.keys(settingsResponse?.FieldsRule).length > 0) {
const gottenQuestions = { ...selectedQuestions };
setQuestionsBackend(settingsResponse.FieldsRule);
for (let key in settingsResponse.FieldsRule) {
if (
settingsResponse.FieldsRule[key as QuestionKeys] !== null &&
Array.isArray(settingsResponse.FieldsRule[key as QuestionKeys])
) {
const gottenList = settingsResponse.FieldsRule[key as QuestionKeys];
if (gottenList !== null) gottenQuestions[key as QuestionKeys] = Object.keys(gottenList[0].QuestionID);
}
}
setSelectedQuestions(gottenQuestions);
}
if (Boolean(settingsResponse.TagsToAdd) && Object.keys(settingsResponse.TagsToAdd).length > 0) {
const gottenTags = { ...selectedTags };
for (let key in settingsResponse.TagsToAdd) {
const gottenList = settingsResponse.TagsToAdd[key as TagKeys];
if (gottenList !== null && Array.isArray(gottenList)) {
gottenTags[key as TagKeys] = gottenList.map((e) => e.toString());
}
}
setSelectedTags(gottenTags);
}
setFirstRules(false);
}
setIsLoadingPage(false);
};
fetchAccountRules();
} else {
//Вот по-хорошему компонент должен размонтироваться и стереть всё. Но это будет сделано позже
setArrayOfPipelines([]);
setArrayOfPipelinesSteps([]);
setArrayOfUsers([]);
setArrayOfTags([]);
setSelectedPipeline(null);
setSelectedPipelineStep(null);
setSelectedDealPerformer(null);
setQuestionsBackend({} as FieldsRule);
setSelectedTags({
Lead: [],
Contact: [],
Company: [],
Customer: [],
});
setSelectedQuestions({
Lead: [],
Company: [],
Customer: [],
});
setPageOfPipelines(1);
setPageOfPipelinesSteps(1);
setPageOfUsers(1);
setPageOfTags(1);
}
}, [isModalOpen, isTryRemoveAccount]);
useEffect(() => {
getPipelines({
page: pageOfPipelines,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedPipelines: MinifiedData[] = [];
response.items.forEach((step) => {
minifiedPipelines.push({
id: step.AmoID.toString(),
title: step.Name,
});
});
setArrayOfPipelines((prevItems) => [...prevItems, ...minifiedPipelines]);
setPageOfPipelinesSteps(1);
}
});
}, [pageOfPipelines]);
useEffect(() => {
const oldData = pageOfPipelinesSteps === 1 ? [] : arrayOfPipelinesSteps;
if (selectedPipeline !== null)
getSteps({
page: pageOfPipelinesSteps,
size: SIZE,
pipelineId: Number(selectedPipeline),
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedSteps: MinifiedData[] = [];
response.items.forEach((step) => {
minifiedSteps.push({
id: step.AmoID.toString(),
title: step.Name,
});
});
setArrayOfPipelinesSteps([...oldData, ...minifiedSteps]);
}
});
}, [selectedPipeline, pageOfPipelinesSteps]);
useEffect(() => {
getUsers({
page: pageOfUsers,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedUsers: MinifiedData[] = [];
response.items.forEach((step) => {
minifiedUsers.push({
id: step.amoUserID.toString(),
title: step.name,
});
});
setArrayOfUsers((prevItems) => [...prevItems, ...minifiedUsers]);
}
});
}, [pageOfUsers]);
useEffect(() => {
getTags({
page: pageOfTags,
size: SIZE,
}).then(([response]) => {
if (response && response.items !== null) {
const minifiedTags: MinifiedData[] = [];
response.items.forEach((step) => {
minifiedTags.push({
id: step.AmoID.toString(),
title: step.Name,
entity:
step.Entity === "leads"
? "Lead"
: step.Entity === "contacts"
? "Contact"
: step.Entity === "companies"
? "Company"
: "Customer",
});
});
setArrayOfTags((prevItems) => [...prevItems, ...minifiedTags]);
}
});
}, [pageOfTags]);
return {
isloadingPage,
firstRules,
accountInfo,
arrayOfPipelines,
arrayOfPipelinesSteps,
arrayOfUsers,
arrayOfTags,
selectedPipeline,
setSelectedPipeline,
selectedPipelineStep,
setSelectedPipelineStep,
selectedDealUser,
setSelectedDealPerformer,
questionsBackend,
selectedTags,
setSelectedTags,
selectedQuestions,
setSelectedQuestions,
setPageOfPipelines,
setPageOfPipelinesSteps,
setPageOfUsers,
setPageOfTags,
};
};

@ -11,13 +11,13 @@ import { useCurrentQuiz } from "@/stores/quizes/hooks";
const AnalyticsModal = lazy(() =>
import("./AnalyticsModal/AnalyticsModal").then((module) => ({
default: module.AnalyticsModal,
})),
}))
);
const AmoCRMModal = lazy(() =>
import("../IntegrationsModal/AmoCRMModal").then((module) => ({
default: module.AmoCRMModal,
})),
}))
);
type PartnersBoardProps = {
@ -122,7 +122,7 @@ export const PartnersBoard: FC<PartnersBoardProps> = ({
/>
</Suspense>
)}
{companyName && (
{companyName && isAmoCrmModalOpen && (
<Suspense>
<AmoCRMModal
isModalOpen={isAmoCrmModalOpen}

@ -10,21 +10,11 @@ import {
useMediaQuery,
useTheme,
} from "@mui/material";
import {
addQuestionVariant,
deleteQuestionVariant,
setQuestionVariantField,
} from "@root/questions/actions";
import { addQuestionVariant, deleteQuestionVariant, setQuestionVariantField } from "@root/questions/actions";
import { enqueueSnackbar } from "notistack";
import {
memo,
type ChangeEvent,
type FC,
type KeyboardEvent,
type ReactNode,
} from "react";
import { memo, type ChangeEvent, type FC, type KeyboardEvent, type ReactNode } from "react";
import { Draggable } from "react-beautiful-dnd";
import type { QuestionVariant } from "../../../model/questionTypes/shared";
import type { QuestionVariant } from "@frontend/squzanswerer";
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
@ -39,22 +29,20 @@ type AnswerItemProps = {
};
const AnswerItem = memo<AnswerItemProps>(
({
index,
variant,
questionId,
largeCheck = false,
additionalContent,
additionalMobile,
disableKeyDown,
}) => {
({ index, variant, questionId, largeCheck = false, additionalContent, additionalMobile, disableKeyDown }) => {
const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(790));
return (
<Draggable draggableId={String(index)} index={index}>
<Draggable
draggableId={String(index)}
index={index}
>
{(provided) => (
<Box ref={provided.innerRef} {...provided.draggableProps}>
<Box
ref={provided.innerRef}
{...provided.draggableProps}
>
<FormControl
key={index}
fullWidth
@ -74,12 +62,7 @@ const AnswerItem = memo<AnswerItemProps>(
multiline={largeCheck}
onChange={({ target }: ChangeEvent<HTMLInputElement>) => {
if (target.value.length <= 1000) {
setQuestionVariantField(
questionId,
variant.id,
"answer",
target.value || " ",
);
setQuestionVariantField(questionId, variant.id, "answer", target.value || " ");
}
}}
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
@ -96,9 +79,7 @@ const AnswerItem = memo<AnswerItemProps>(
{...provided.dragHandleProps}
position="start"
>
<PointsIcon
style={{ color: "#9A9AAF", fontSize: "30px" }}
/>
<PointsIcon style={{ color: "#9A9AAF", fontSize: "30px" }} />
</InputAdornment>
{additionalContent}
</>
@ -107,9 +88,7 @@ const AnswerItem = memo<AnswerItemProps>(
<InputAdornment position="end">
<IconButton
sx={{ padding: "0" }}
onClick={() =>
deleteQuestionVariant(questionId, variant.id)
}
onClick={() => deleteQuestionVariant(questionId, variant.id)}
>
<DeleteIcon
style={{
@ -153,7 +132,7 @@ const AnswerItem = memo<AnswerItemProps>(
)}
</Draggable>
);
},
}
);
AnswerItem.displayName = "AnswerItem";

@ -1,6 +1,6 @@
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import { devlog } from "@frontend/kitui";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { clearRuleForAll } from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store";
import { updateRootContentId } from "@root/quizes/actions";
@ -14,18 +14,18 @@ import {
import { useUiTools } from "@root/uiTools/store";
import type { Core } from "cytoscape";
import { enqueueSnackbar } from "notistack";
import { useEffect, useLayoutEffect, useMemo, useRef, useState } from "react";
import CytoscapeComponent from "react-cytoscapejs";
import { withErrorBoundary } from "react-error-boundary";
import { DeleteNodeModal } from "../DeleteNodeModal";
import CsNodeButtons from "./CsNodeButtons";
import { InfoBanner } from "./InfoBanner/InfoBanner";
import { PositionControl } from "./PositionControl/PositionControl";
import { ZoomControl } from "./ZoomControl/ZoomControl";
import { addNode, layoutOptions, storeToNodes } from "./helper";
import { useRemoveNode } from "./hooks/useRemoveNode";
import "./style/styles.css";
import { stylesheet } from "./style/stylesheet";
import { Box, useMediaQuery, useTheme } from "@mui/material";
import { InfoBanner } from "./InfoBanner/InfoBanner";
import { PositionControl } from "./PositionControl/PositionControl";
import { ZoomControl } from "./ZoomControl/ZoomControl";
function CsComponent() {
const theme = useTheme();
@ -93,7 +93,10 @@ function CsComponent() {
width: "100%",
}}
>
<CsNodeButtons csElements={csElements} cyRef={cyRef} />
<CsNodeButtons
csElements={csElements}
cyRef={cyRef}
/>
<Box sx={{ position: "relative" }}>
{isBannerVisible && <InfoBanner setBannerVisible={setBannerVisible} />}
<PositionControl cyRef={cyRef} />

@ -1,15 +1,7 @@
import { QuizQuestionResult } from "@model/questionTypes/result";
import {
AnyTypedQuizQuestion,
QuestionBranchingRule,
QuestionBranchingRuleMain,
UntypedQuizQuestion,
} from "@model/questionTypes/shared";
import {
createResult,
getQuestionByContentId,
updateQuestion,
} from "@root/questions/actions";
import { AnyTypedQuizQuestion, QuestionBranchingRule, QuestionBranchingRuleMain } from "@frontend/squzanswerer";
import { QuizQuestionResult } from "@frontend/squzanswerer";
import { 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";
@ -61,10 +53,7 @@ export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => {
const parentQuestion = {
...getQuestionByContentId(question.content.rule.parentId),
} as AnyTypedQuizQuestion;
let label =
question.title === "" || question.title === " "
? "noname"
: question.title;
let label = question.title === "" || question.title === " " ? "noname" : question.title;
label = label.trim().replace(/\s+/g, " ");
if (label.length > 20) label = label.slice(0, 20).toLowerCase() + "…";
@ -73,10 +62,7 @@ export const storeToNodes = (questions: AnyTypedQuizQuestion[]) => {
isRoot: question.content.rule.parentId === "root",
id: question.content.id,
label,
qtype:
question.content.rule.parentId === "root"
? "root"
: parentQuestion.type,
qtype: question.content.rule.parentId === "root" ? "root" : parentQuestion.type,
type: question.type,
children: question.content.rule.children.length,
},
@ -130,15 +116,10 @@ export function clearDataAfterAddNode({
//смотрим не добавлен ли родителю result. Если да - делаем его неактивным. Веточкам result не нужен
useQuestionsStore
.getState()
.questions.filter(
(question): question is QuizQuestionResult => question.type === "result",
)
.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<QuizQuestionResult>(targetQuestion.id, (q) => (q.content.usage = false));
}
});
@ -156,14 +137,9 @@ export function clearDataAfterAddNode({
//предупреждаем родителя о новом потомке (если он ещё не знает о нём)
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.children = [...question.content.rule.children, targetQuestion.content.id];
//единственному ребёнку даём дефолт по-умолчанию
question.content.rule.default = noChild
? targetQuestion.content.id
: question.content.rule.default;
question.content.rule.default = noChild ? targetQuestion.content.id : question.content.rule.default;
});
if (!noChild) {
@ -194,9 +170,7 @@ export function clearDataAfterRemoveNode({
//Делаем результат родителя активным
const parentResult = trashQuestions.find(
(q): q is QuizQuestionResult =>
q.type === "result" &&
q.content.rule.parentId === parentQuestionContentId,
(q): q is QuizQuestionResult => q.type === "result" && q.content.rule.parentId === parentQuestionContentId
);
if (parentResult) {
updateQuestion<QuizQuestionResult>(parentResult.content.id, (q) => {
@ -212,21 +186,14 @@ export function clearDataAfterRemoveNode({
}
const newChildren = [...parentQuestion.content.rule.children];
newChildren.splice(
parentQuestion.content.rule.children.indexOf(targetQuestionContentId),
1,
);
newChildren.splice(parentQuestion.content.rule.children.indexOf(targetQuestionContentId), 1);
const newRule: QuestionBranchingRule = {
children: newChildren,
default:
parentQuestion.content.rule.default === targetQuestionContentId
? ""
: parentQuestion.content.rule.default,
default: parentQuestion.content.rule.default === targetQuestionContentId ? "" : parentQuestion.content.rule.default,
//удаляем условия перехода от родителя к этому вопросу,
main: parentQuestion.content.rule.main.filter(
(data: QuestionBranchingRuleMain) =>
data.next !== targetQuestionContentId,
(data: QuestionBranchingRuleMain) => data.next !== targetQuestionContentId
),
parentId: parentQuestion.content.rule.parentId,
};
@ -243,8 +210,7 @@ export function calcNodePosition(node: any) {
node.removeData("lastChild");
if (incomming.length === 0) {
if (node.cy().data("firstNode") === undefined)
node.cy().data("firstNode", "root");
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);
@ -257,15 +223,10 @@ export function calcNodePosition(node: any) {
const task = queue.pop();
task.task.data("layer", task.layer);
task.task.removeData("subtreeWidth");
const children = node
.cy()
.edges(`[source="${task.task.id()}"]`)
.targets();
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 }),
);
children.forEach((n: any) => queue.push({ task: n, layer: task.layer + 1 }));
}
}
queue.push({ parent: node, children: children });
@ -291,7 +252,7 @@ export function calcNodePosition(node: any) {
task?.parent.data(
"subtreeWidth",
task.children.reduce((p: any, n: any) => p + n.data("subtreeWidth"), 0),
task.children.reduce((p: any, n: any) => p + n.data("subtreeWidth"), 0)
);
}
@ -366,23 +327,16 @@ export const addNode = ({
//если есть инфо о выбранном вопросе из модалки - берём родителя из инфо модалки. Иначе из значения дропа
const targetQuestion = {
...getQuestionByContentId(
targetNodeContentId || useUiTools.getState().dragQuestionContentId,
),
...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(
"Добавляемый вопрос не найден. Перетащите вопрос из списка",
);
enqueueSnackbar("Добавляемый вопрос не найден. Перетащите вопрос из списка");
}
};
export const isQuestionProhibited = (parentQType: string) =>
parentQType === "text" ||
parentQType === "date" ||
parentQType === "number" ||
parentQType === "page";
parentQType === "text" || parentQType === "date" || parentQType === "number" || parentQType === "page";

@ -1,18 +1,10 @@
import { devlog } from "@frontend/kitui";
import { QuizQuestionResult } from "@model/questionTypes/result";
import {
clearRuleForAll,
getQuestionByContentId,
updateQuestion,
} from "@root/questions/actions";
import { QuizQuestionResult } from "@frontend/squzanswerer";
import { clearRuleForAll, getQuestionByContentId, updateQuestion } from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store";
import { updateRootContentId } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import type {
CollectionReturnValue,
Core,
SingularElementArgument,
} from "cytoscape";
import type { CollectionReturnValue, Core } from "cytoscape";
import type { MutableRefObject } from "react";
import { clearDataAfterRemoveNode } from "../helper";
@ -52,11 +44,7 @@ export const useRemoveNode = ({ cyRef }: UseRemoveNodeArgs) => {
const targetQuestion = getQuestionByContentId(targetNodeContentId);
if (
targetQuestion?.type &&
targetQuestion.content.rule.parentId === "root" &&
quiz
) {
if (targetQuestion?.type && targetQuestion.content.rule.parentId === "root" && quiz) {
updateRootContentId(quiz.id, "");
updateQuestion(targetNodeContentId, (question) => {
question.content.rule.parentId = "";
@ -71,10 +59,7 @@ export const useRemoveNode = ({ cyRef }: UseRemoveNodeArgs) => {
?.toArray()?.[0]
?.data()?.source;
if (targetNodeContentId && parentQuestionContentId) {
if (
quiz &&
cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0
) {
if (quiz && cy?.edges(`[source="${parentQuestionContentId}"]`).length === 0) {
devlog(parentQuestionContentId);
//createFrontResult(quiz.backendId, parentQuestionContentId);
}
@ -103,8 +88,7 @@ export const useRemoveNode = ({ cyRef }: UseRemoveNodeArgs) => {
if (
qr.type === "result" &&
(deleteNodes.includes(qr.content.rule.parentId || "") ||
(targetQuestion?.type &&
qr.content.rule.parentId === targetQuestion.content.id))
(targetQuestion?.type && qr.content.rule.parentId === targetQuestion.content.id))
) {
updateQuestion<QuizQuestionResult>(qr.content.id, (q) => {
q.content.usage = false;

@ -11,37 +11,28 @@ import {
useMediaQuery,
useTheme,
} from "@mui/material";
import {
AnyTypedQuizQuestion,
createBranchingRuleMain,
} from "../../../model/questionTypes/shared";
import { createBranchingRuleMain } from "../../../model/questionTypes/shared";
import InfoIcon from "@icons/Info";
import { TypeSwitch } from "./Settings";
import {
getQuestionByContentId,
getQuestionById,
updateQuestion,
} from "@root/questions/actions";
import { getQuestionByContentId, getQuestionById, updateQuestion } from "@root/questions/actions";
import { updateOpenedModalSettingsId } from "@root/uiTools/actions";
import { useUiTools } from "@root/uiTools/store";
import { enqueueSnackbar } from "notistack";
import TooltipClickInfo from "@ui_kit/Toolbars/TooltipClickInfo";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
export default function BranchingQuestions() {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const isSmallMobile = useMediaQuery(theme.breakpoints.down(400));
const { openedModalSettingsId } = useUiTools();
const [targetQuestion, setTargetQuestion] =
useState<AnyTypedQuizQuestion | null>(
getQuestionById(openedModalSettingsId) ||
getQuestionByContentId(openedModalSettingsId),
);
const [parentQuestion, setParentQuestion] =
useState<AnyTypedQuizQuestion | null>(
getQuestionByContentId(targetQuestion?.content.rule.parentId),
);
const [targetQuestion, setTargetQuestion] = useState<AnyTypedQuizQuestion | null>(
getQuestionById(openedModalSettingsId) || getQuestionByContentId(openedModalSettingsId)
);
const [parentQuestion, setParentQuestion] = useState<AnyTypedQuizQuestion | null>(
getQuestionByContentId(targetQuestion?.content.rule.parentId)
);
useLayoutEffect(() => {
if (parentQuestion === null) return;
@ -70,10 +61,7 @@ export default function BranchingQuestions() {
const saveData = () => {
if (parentQuestion !== null) {
updateQuestion(
parentQuestion.content.id,
(question) => (question.content = parentQuestion.content),
);
updateQuestion(parentQuestion.content.id, (question) => (question.content = parentQuestion.content));
}
handleClose();
};
@ -84,7 +72,10 @@ export default function BranchingQuestions() {
return (
<>
<Modal open={openedModalSettingsId !== null} onClose={handleClose}>
<Modal
open={openedModalSettingsId !== null}
onClose={handleClose}
>
<Box
sx={{
position: "absolute",
@ -120,22 +111,14 @@ export default function BranchingQuestions() {
overflow: "hidden",
textOverflow: "ellipsis",
whiteSpace: "nowrap",
maxWidth: isSmallMobile
? "250px"
: isMobile
? "350px"
: "450px",
maxWidth: isSmallMobile ? "250px" : isMobile ? "350px" : "450px",
display: "inline-block",
}}
>
{targetQuestion.title}
</Typography>
{isMobile ? (
<TooltipClickInfo
title={
"Настройте условия, при которых данный вопрос будет отображаться в quiz."
}
/>
<TooltipClickInfo title={"Настройте условия, при которых данный вопрос будет отображаться в quiz."} />
) : (
<Tooltip
title="Настройте условия, при которых данный вопрос будет отображаться в quiz."
@ -197,10 +180,7 @@ export default function BranchingQuestions() {
onClick={() => {
const mutate = JSON.parse(JSON.stringify(parentQuestion));
mutate.content.rule.main.push(
createBranchingRuleMain(
targetQuestion.content.id,
parentQuestion.content.id,
),
createBranchingRuleMain(targetQuestion.content.id, parentQuestion.content.id)
);
setParentQuestion(mutate);
}}
@ -214,10 +194,7 @@ export default function BranchingQuestions() {
sx={{
margin: 0,
}}
checked={
parentQuestion.content.rule.default ===
targetQuestion.content.id
}
checked={parentQuestion.content.rule.default === targetQuestion.content.id}
onClick={() => {
let mutate = JSON.parse(JSON.stringify(parentQuestion));
@ -227,8 +204,7 @@ export default function BranchingQuestions() {
} else {
//Изменять чекбокс можно только если много потомков
mutate.content.rule.default =
parentQuestion.content.rule.default ===
targetQuestion.content.id
parentQuestion.content.rule.default === targetQuestion.content.id
? ""
: targetQuestion.content.id;
}

@ -1,40 +1,31 @@
import CalendarIcon from "@icons/CalendarIcon";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import {
Box,
MenuItem,
FormControl,
Checkbox,
FormControl,
FormControlLabel,
IconButton,
MenuItem,
Radio,
RadioGroup,
Typography,
useTheme,
Select,
useMediaQuery,
IconButton,
TextField,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { SelectChangeEvent } from "@mui/material/Select";
import { DatePicker } from "@mui/x-date-pickers";
import { TimePicker } from "@mui/x-date-pickers/TimePicker";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { QuizQuestionBase } from "model/questionTypes/shared";
import { useState, useRef, useEffect } from "react";
import { useParams } from "react-router-dom";
import { useQuestionsStore } from "@root/questions/store";
import { updateQuestion, getQuestionById } from "@root/questions/actions";
import { SelectChangeEvent } from "@mui/material/Select";
import CalendarIcon from "@icons/CalendarIcon";
import { DatePicker } from "@mui/x-date-pickers";
import moment from "moment";
import { TimePicker } from "@mui/x-date-pickers/TimePicker";
import InfoIcon from "@icons/Info";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import { useEffect, useState } from "react";
import { useParams } from "react-router-dom";
import type { AnyTypedQuizQuestion, QuizQuestionNumber } from "@frontend/squzanswerer";
import type { AnyTypedQuizQuestion } from "../../../model/questionTypes/shared";
import type { QuizQuestionNumber } from "../../../model/questionTypes/number";
const CONDITIONS = [
"Все условия обязательны",
"Обязательно хотя бы одно условие",
];
const CONDITIONS = ["Все условия обязательны", "Обязательно хотя бы одно условие"];
interface Props {
parentQuestion: AnyTypedQuizQuestion;
targetQuestion: AnyTypedQuizQuestion;
@ -43,12 +34,7 @@ interface Props {
}
// Этот компонент вызывается 1 раз на каждое условие родителя для перехода к этому вопросу. Поэтому для изменения стора мы знаем индекс
export const TypeSwitch = ({
parentQuestion,
targetQuestion,
ruleIndex,
setParentQuestion,
}: Props) => {
export const TypeSwitch = ({ parentQuestion, targetQuestion, ruleIndex, setParentQuestion }: Props) => {
switch (parentQuestion.type) {
case "variant":
case "images":
@ -91,9 +77,7 @@ export const TypeSwitch = ({
break;
case "page":
return (
<BlockRule text={"У такого родителя может быть только один потомок"} />
);
return <BlockRule text={"У такого родителя может быть только один потомок"} />;
break;
case "text":
@ -147,12 +131,7 @@ export const BlockRule = ({ text }: { text: string }) => {
</Typography>
);
};
const SelectorType = ({
parentQuestion,
targetQuestion,
ruleIndex,
setParentQuestion,
}: Props) => {
const SelectorType = ({ parentQuestion, targetQuestion, ruleIndex, setParentQuestion }: Props) => {
const theme = useTheme();
const quizId = Number(useParams().quizId);
return (
@ -174,9 +153,7 @@ const SelectorType = ({
pb: "5px",
}}
>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
Новое условие
</Typography>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>Новое условие</Typography>
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => {
@ -196,23 +173,15 @@ const SelectorType = ({
pb: "10px",
}}
>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
Дан ответ
</Typography>
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>
(Укажите один или несколько вариантов)
</Typography>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>Дан ответ</Typography>
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>(Укажите один или несколько вариантов)</Typography>
</Box>
<Select
multiple
value={
parentQuestion.content?.rule?.main[ruleIndex]?.rules[0]?.answers || []
}
value={parentQuestion.content?.rule?.main[ruleIndex]?.rules[0]?.answers || []}
onChange={(event: SelectChangeEvent) => {
let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion));
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = (
event.target as HTMLSelectElement
).value;
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = (event.target as HTMLSelectElement).value;
setParentQuestion(newParentQuestion);
}}
sx={{
@ -247,7 +216,10 @@ const SelectorType = ({
sx={{ color: theme.palette.grey2.main }}
value={Boolean(Number(totalIndex))}
control={
<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />
<Radio
checkedIcon={<RadioCheck />}
icon={<RadioIcon />}
/>
}
label={condition}
/>
@ -257,12 +229,7 @@ const SelectorType = ({
</Box>
);
};
const DateInputsType = ({
parentQuestion,
targetQuestion,
ruleIndex,
setParentQuestion,
}: Props) => {
const DateInputsType = ({ parentQuestion, targetQuestion, ruleIndex, setParentQuestion }: Props) => {
const theme = useTheme();
const upLg = useMediaQuery(theme.breakpoints.up("md"));
@ -277,8 +244,7 @@ const DateInputsType = ({
let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion));
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] = time;
if (newParentQuestion.content.dateRange)
parentQuestion.content.rule.main[ruleIndex].rules[0].answers[1] = time;
if (newParentQuestion.content.dateRange) parentQuestion.content.rule.main[ruleIndex].rules[0].answers[1] = time;
setParentQuestion(newParentQuestion);
}, [firstDate, secondDate, firstTime, secondTime]);
@ -302,9 +268,7 @@ const DateInputsType = ({
pb: "5px",
}}
>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
Новое условие
</Typography>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>Новое условие</Typography>
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => {
@ -324,12 +288,8 @@ const DateInputsType = ({
pb: "10px",
}}
>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
Дан ответ
</Typography>
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>
(Укажите один или несколько вариантов)
</Typography>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>Дан ответ</Typography>
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>(Укажите один или несколько вариантов)</Typography>
</Box>
<Box
@ -339,15 +299,11 @@ const DateInputsType = ({
}}
>
{parentQuestion.content.dateRange && (
<Typography sx={{ color: "#4D4D4D", p: "10px" }}>
(Начало периода)
</Typography>
<Typography sx={{ color: "#4D4D4D", p: "10px" }}>(Начало периода)</Typography>
)}
<DatePicker
defaultValue={moment(
new Date(
parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0],
).toLocaleDateString(),
new Date(parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]).toLocaleDateString()
)}
onChange={(dateString) => {
const date = dateString?.toDate().toLocaleDateString("ru-RU", {
@ -356,9 +312,7 @@ const DateInputsType = ({
day: "2-digit",
});
let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion));
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = [
date,
];
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = [date];
setParentQuestion(newParentQuestion);
}}
slots={{
@ -392,9 +346,7 @@ const DateInputsType = ({
/>
{parentQuestion.content.time && (
<TimePicker
value={
parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]
}
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
sx={{
p: "10px",
"& .MuiInputBase-root": {
@ -424,14 +376,10 @@ const DateInputsType = ({
}}
>
{parentQuestion.content.dateRange && (
<Typography sx={{ color: "#4D4D4D", p: "10px" }}>
(Конец периода)
</Typography>
<Typography sx={{ color: "#4D4D4D", p: "10px" }}>(Конец периода)</Typography>
)}
<DatePicker
value={
parentQuestion.content.rule.main[ruleIndex].rules[0].answers[1]
}
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[1]}
onChange={() => {}}
slots={{
openPickerIcon: () => <CalendarIcon />,
@ -464,9 +412,7 @@ const DateInputsType = ({
/>
{parentQuestion.content.time && (
<TimePicker
value={
parentQuestion.content.rule.main[ruleIndex].rules[0].answers[1]
}
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[1]}
sx={{
p: "10px",
"& .MuiInputBase-root": {
@ -491,12 +437,7 @@ const DateInputsType = ({
</Box>
);
};
const NumberInputsType = ({
parentQuestion,
targetQuestion,
ruleIndex,
setParentQuestion,
}: Props) => {
const NumberInputsType = ({ parentQuestion, targetQuestion, ruleIndex, setParentQuestion }: Props) => {
const theme = useTheme();
const quizId = Number(useParams().quizId);
@ -519,9 +460,7 @@ const NumberInputsType = ({
pb: "5px",
}}
>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
Новое условие
</Typography>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>Новое условие</Typography>
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => {
@ -541,35 +480,23 @@ const NumberInputsType = ({
pb: "10px",
}}
>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
Дан ответ
</Typography>
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>
(Укажите один или несколько вариантов)
</Typography>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>Дан ответ</Typography>
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>(Укажите один или несколько вариантов)</Typography>
</Box>
<Box>
<TextField
sx={{ marginTop: "20px", width: "100%" }}
placeholder="от"
value={
parentQuestion.content.rule.main[
ruleIndex
].rules[0].answers[0]?.split("—")[0]
}
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]?.split("—")[0]}
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
const newParentQuestion = JSON.parse(
JSON.stringify(parentQuestion),
);
const previousValue =
newParentQuestion.content.rule.main[ruleIndex].rules[0]
.answers[0];
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] =
(parentQuestion as QuizQuestionNumber).content.chooseRange
? previousValue
? `${target.value}${previousValue.split("—")[1] || 0}`
: `${target.value}—0`
: target.value;
const newParentQuestion = JSON.parse(JSON.stringify(parentQuestion));
const previousValue = newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0];
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] = (parentQuestion as QuizQuestionNumber)
.content.chooseRange
? previousValue
? `${target.value}${previousValue.split("—")[1] || 0}`
: `${target.value}—0`
: target.value;
setParentQuestion(newParentQuestion);
}}
/>
@ -577,21 +504,11 @@ const NumberInputsType = ({
<TextField
placeholder="до"
sx={{ marginTop: "20px", width: "100%" }}
value={
parentQuestion.content.rule.main[
ruleIndex
].rules[0].answers[0]?.split("—")[1]
}
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]?.split("—")[1]}
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => {
const newParentQuestion = JSON.parse(
JSON.stringify(parentQuestion),
);
const previousValue =
newParentQuestion.content.rule.main[ruleIndex].rules[0]
.answers[0];
newParentQuestion.content.rule.main[
ruleIndex
].rules[0].answers[0] = previousValue
const newParentQuestion = JSON.parse(JSON.stringify(parentQuestion));
const previousValue = newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0];
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers[0] = previousValue
? `${previousValue.split("—")[0] || 0}${target.value}`
: `0—${target.value}`;
setParentQuestion(newParentQuestion);
@ -602,12 +519,7 @@ const NumberInputsType = ({
</Box>
);
};
const TextInputsType = ({
parentQuestion,
targetQuestion,
ruleIndex,
setParentQuestion,
}: Props) => {
const TextInputsType = ({ parentQuestion, targetQuestion, ruleIndex, setParentQuestion }: Props) => {
const theme = useTheme();
const quizId = Number(useParams().quizId);
@ -630,9 +542,7 @@ const TextInputsType = ({
pb: "5px",
}}
>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
Новое условие
</Typography>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>Новое условие</Typography>
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => {
@ -652,12 +562,9 @@ const TextInputsType = ({
pb: "10px",
}}
>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
Дан ответ
</Typography>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>Дан ответ</Typography>
<Typography sx={{ color: "#7E2AEA", pl: "10px", fontSize: "12px" }}>
(Укажите текст, при совпадении с которым пользователь попадёт на этот
вопрос)
(Укажите текст, при совпадении с которым пользователь попадёт на этот вопрос)
</Typography>
</Box>
<TextField
@ -668,21 +575,14 @@ const TextInputsType = ({
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
onChange={(event: React.FormEvent<HTMLInputElement>) => {
let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion));
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = [
(event.target as HTMLInputElement).value,
];
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = [(event.target as HTMLInputElement).value];
setParentQuestion(newParentQuestion);
}}
/>
</Box>
);
};
const FileInputsType = ({
parentQuestion,
targetQuestion,
ruleIndex,
setParentQuestion,
}: Props) => {
const FileInputsType = ({ parentQuestion, targetQuestion, ruleIndex, setParentQuestion }: Props) => {
const theme = useTheme();
const quizId = Number(useParams().quizId);
@ -705,9 +605,7 @@ const FileInputsType = ({
pb: "5px",
}}
>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
Новое условие
</Typography>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>Новое условие</Typography>
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => {
@ -737,15 +635,12 @@ const FileInputsType = ({
sx={{
margin: 0,
}}
checked={
parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]
}
checked={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
onChange={(event: React.FormEvent<HTMLInputElement>) => {
let newParentQuestion = JSON.parse(
JSON.stringify(parentQuestion),
);
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers =
[(event.target as HTMLInputElement).checked];
let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion));
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = [
(event.target as HTMLInputElement).checked,
];
setParentQuestion(newParentQuestion);
}}
/>
@ -755,12 +650,7 @@ const FileInputsType = ({
</Box>
);
};
const RatingInputsType = ({
parentQuestion,
targetQuestion,
ruleIndex,
setParentQuestion,
}: Props) => {
const RatingInputsType = ({ parentQuestion, targetQuestion, ruleIndex, setParentQuestion }: Props) => {
const theme = useTheme();
const quizId = Number(useParams().quizId);
@ -783,9 +673,7 @@ const RatingInputsType = ({
pb: "5px",
}}
>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
Новое условие
</Typography>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>Новое условие</Typography>
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => {
@ -817,16 +705,9 @@ const RatingInputsType = ({
value={parentQuestion.content.rule.main[ruleIndex].rules[0].answers[0]}
onChange={(event: React.FormEvent<HTMLInputElement>) => {
let newParentQuestion = JSON.parse(JSON.stringify(parentQuestion));
let valueNumber = Number(
(event.target as HTMLInputElement).value.replace(/[^0-9,\s]/g, ""),
);
valueNumber =
valueNumber > parentQuestion.content.steps
? parentQuestion.content.steps
: valueNumber;
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = [
valueNumber,
];
let valueNumber = Number((event.target as HTMLInputElement).value.replace(/[^0-9,\s]/g, ""));
valueNumber = valueNumber > parentQuestion.content.steps ? parentQuestion.content.steps : valueNumber;
newParentQuestion.content.rule.main[ruleIndex].rules[0].answers = [valueNumber];
setParentQuestion(newParentQuestion);
}}
/>

@ -10,12 +10,9 @@ import {
useTheme,
} from "@mui/material";
import { useQuestionsStore } from "@root/questions/store";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
import { useUiTools } from "@root/uiTools/store";
import {
setModalQuestionTargetContentId,
setOpenedModalQuestions,
} from "@root/uiTools/actions";
import { setModalQuestionTargetContentId, setOpenedModalQuestions } from "@root/uiTools/actions";
import React, { useEffect, useState } from "react";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
@ -26,20 +23,16 @@ export const BranchingQuestionsModal = () => {
const isMobile = useMediaQuery(theme.breakpoints.down(650));
const trashQuestions = useQuestionsStore().questions;
const questions = trashQuestions.filter(
(question) => question.type !== "result",
);
const questions = trashQuestions.filter((question) => question.type !== "result");
const openedModalQuestions = useUiTools(
(state) => state.openedModalQuestions,
);
const openedModalQuestions = useUiTools((state) => state.openedModalQuestions);
const handleClose = () => {
setOpenedModalQuestions(false);
};
const typedQuestions: AnyTypedQuizQuestion[] = questions.filter(
(question) => question.type && question.type !== "result",
(question) => question.type && question.type !== "result"
) as AnyTypedQuizQuestion[];
if (typedQuestions.length === 0) return <></>;
@ -64,7 +57,10 @@ export const BranchingQuestionsModal = () => {
};
return (
<Modal open={openedModalQuestions} onClose={handleClose}>
<Modal
open={openedModalQuestions}
onClose={handleClose}
>
<Box
sx={{
position: "absolute",
@ -79,9 +75,7 @@ export const BranchingQuestionsModal = () => {
boxShadow: 24,
}}
>
<Box
sx={{ width: "100%", background: theme.palette.background.default }}
>
<Box sx={{ width: "100%", background: theme.palette.background.default }}>
<Typography
sx={{
padding: "25px 0 25px 20px",
@ -110,13 +104,7 @@ export const BranchingQuestionsModal = () => {
control={
<Radio
checkedIcon={<RadioCheck />}
icon={
question.content.rule.parentId ? (
<RoundedCheckedIcon />
) : (
<RadioIcon />
)
}
icon={question.content.rule.parentId ? <RoundedCheckedIcon /> : <RadioIcon />}
/>
}
label={question.title || "нет заголовка"}
@ -125,10 +113,7 @@ export const BranchingQuestionsModal = () => {
padding: "8px 12px",
margin: 0,
width: "100%",
backgroundColor:
index % 2 === 0
? theme.palette.common.white
: "rgba(242, 243, 247, 0.5)",
backgroundColor: index % 2 === 0 ? theme.palette.common.white : "rgba(242, 243, 247, 0.5)",
color: theme.palette.grey3.main,
}}
/>

@ -1,19 +1,7 @@
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import type { SxProps } from "@mui/material";
import {
Box,
Button,
IconButton,
Modal,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import {
copyQuestion,
deleteQuestion,
deleteQuestionWithTimeout,
} from "@root/questions/actions";
import { Box, Button, IconButton, Modal, Typography, useMediaQuery, useTheme } from "@mui/material";
import { copyQuestion, deleteQuestion, deleteQuestionWithTimeout } from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { updateDesireToOpenABranchingModal } from "@root/uiTools/actions";
@ -24,8 +12,7 @@ import { CopyIcon } from "../../assets/icons/questionsPage/CopyIcon";
import Branching from "../../assets/icons/questionsPage/branching";
import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
import { QuestionType } from "@model/question/question";
import type { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import type { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
interface Props {
switchState: string;
@ -52,9 +39,7 @@ const ButtonsOptions = memo<Props>(function ({
const isWrappMiniButtonSetting = useMediaQuery(theme.breakpoints.down(920));
const quiz = useCurrentQuiz();
const [openDelete, setOpenDelete] = useState<boolean>(false);
const isQuestionFirst = useQuestionsStore(
(state) => state.questions[0]?.id === questionId,
);
const isQuestionFirst = useQuestionsStore((state) => state.questions[0]?.id === questionId);
if (!quiz) return null;
@ -70,24 +55,12 @@ const ButtonsOptions = memo<Props>(function ({
myFunc?: any;
}[] = [
{
icon: (
<SettingIcon
color={
switchState === "setting" ? "#ffffff" : theme.palette.grey3.main
}
/>
),
icon: <SettingIcon color={switchState === "setting" ? "#ffffff" : theme.palette.grey3.main} />,
title: "Настройки",
value: "setting",
},
{
icon: (
<Branching
color={
switchState === "branching" ? "#ffffff" : theme.palette.grey3.main
}
/>
),
icon: <Branching color={switchState === "branching" ? "#ffffff" : theme.palette.grey3.main} />,
title: "Ветвление",
value: "branching",
myFunc: (question: AnyTypedQuizQuestion) => {
@ -119,9 +92,7 @@ const ButtonsOptions = memo<Props>(function ({
{buttonSetting.map(({ icon, title, value, myFunc }) => (
<Box key={value}>
{value === "branching" ? (
["page", "text", "date", "number"].includes(
questionType,
) ? null : (
["page", "text", "date", "number"].includes(questionType) ? null : (
<MiniButtonSetting
key={title}
onClick={() => {
@ -129,14 +100,8 @@ const ButtonsOptions = memo<Props>(function ({
}}
sx={{
display: quiz.config.type === "form" ? "none" : "flex",
backgroundColor:
switchState === value
? theme.palette.brightPurple.main
: "transparent",
color:
switchState === value
? "#ffffff"
: theme.palette.grey3.main,
backgroundColor: switchState === value ? theme.palette.brightPurple.main : "transparent",
color: switchState === value ? "#ffffff" : theme.palette.grey3.main,
minWidth: isWrappMiniButtonSetting ? "30px" : "64px",
height: "30px",
"&:hover": {
@ -157,14 +122,8 @@ const ButtonsOptions = memo<Props>(function ({
myFunc();
}}
sx={{
backgroundColor:
switchState === value
? theme.palette.brightPurple.main
: "transparent",
color:
switchState === value
? "#ffffff"
: theme.palette.grey3.main,
backgroundColor: switchState === value ? theme.palette.brightPurple.main : "transparent",
color: switchState === value ? "#ffffff" : theme.palette.grey3.main,
minWidth: isWrappMiniButtonSetting ? "30px" : "64px",
height: "30px",
"&:hover": {
@ -203,9 +162,7 @@ const ButtonsOptions = memo<Props>(function ({
if (questionHasParent) {
setOpenDelete(true);
} else {
deleteQuestionWithTimeout(questionId, () =>
DeleteFunction(questionId),
);
deleteQuestionWithTimeout(questionId, () => DeleteFunction(questionId));
}
}}
data-cy="delete-question"
@ -213,7 +170,10 @@ const ButtonsOptions = memo<Props>(function ({
<DeleteIcon color={"#4D4D4D"} />
</IconButton>
)}
<Modal open={openDelete} onClose={() => setOpenDelete(false)}>
<Modal
open={openDelete}
onClose={() => setOpenDelete(false)}
>
<Box
sx={{
position: "absolute",
@ -225,9 +185,12 @@ const ButtonsOptions = memo<Props>(function ({
background: "#FFFFFF",
}}
>
<Typography variant="h6" sx={{ textAlign: "center" }}>
Вы удаляете вопрос, участвующий в ветвлении. Все его потомки
потеряют данные ветвления. Вы уверены, что хотите удалить вопрос?
<Typography
variant="h6"
sx={{ textAlign: "center" }}
>
Вы удаляете вопрос, участвующий в ветвлении. Все его потомки потеряют данные ветвления. Вы уверены, что
хотите удалить вопрос?
</Typography>
<Box
sx={{
@ -248,9 +211,7 @@ const ButtonsOptions = memo<Props>(function ({
variant="contained"
sx={{ minWidth: "150px" }}
onClick={() => {
deleteQuestionWithTimeout(questionId, () =>
DeleteFunction(questionId),
);
deleteQuestionWithTimeout(questionId, () => DeleteFunction(questionId));
}}
>
Подтвердить

@ -1,16 +1,10 @@
import {
Box,
Tooltip,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { useState } from "react";
import InfoIcon from "../../../assets/icons/InfoIcon";
import ButtonsOptions from "../ButtonsOptions";
import SwitchData from "./switchData";
import { QuizQuestionDate } from "@model/questionTypes/date";
import TooltipClickInfo from "@ui_kit/Toolbars/TooltipClickInfo";
import { QuizQuestionDate } from "@frontend/squzanswerer";
interface Props {
question: QuizQuestionDate;
@ -18,11 +12,7 @@ interface Props {
setOpenBranchingPage: (a: boolean) => void;
}
export default function DataOptions({
question,
openBranchingPage,
setOpenBranchingPage,
}: Props) {
export default function DataOptions({ question, openBranchingPage, setOpenBranchingPage }: Props) {
const [switchState, setSwitchState] = useState("setting");
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
@ -61,7 +51,10 @@ export default function DataOptions({
{isMobile ? (
<TooltipClickInfo title={"Выбор даты."} />
) : (
<Tooltip title="Выбор даты." placement="top">
<Tooltip
title="Выбор даты."
placement="top"
>
<Box>
<InfoIcon />
</Box>
@ -75,10 +68,13 @@ export default function DataOptions({
questionId={question.id}
questionContentId={question.content.id}
questionType={question.type}
questionHasParent={question.content.rule.parentId.length !== 0}
questionHasParent={question.content.rule.parentId?.length !== 0}
setOpenBranchingPage={setOpenBranchingPage}
/>
<SwitchData switchState={switchState} question={question} />
<SwitchData
switchState={switchState}
question={question}
/>
</>
);
}

@ -1,7 +1,7 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import type { QuizQuestionDate } from "../../../model/questionTypes/date";
import type { QuizQuestionDate } from "@frontend/squzanswerer";
type SettingsDataProps = {
questionId: string;
@ -12,7 +12,6 @@ type SettingsDataProps = {
export default function SettingsData({ questionId, isRequired, isDateRange, isTime }: SettingsDataProps) {
const theme = useTheme();
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isTablet = useMediaQuery(theme.breakpoints.down(900));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));

@ -1,4 +1,4 @@
import { QuizQuestionDate } from "@model/questionTypes/date";
import { QuizQuestionDate } from "@frontend/squzanswerer";
import HelpQuestions from "../helpQuestions";
import SettingData from "./settingData";

@ -1,7 +1,4 @@
import {
AnyTypedQuizQuestion,
UntypedQuizQuestion,
} from "@model/questionTypes/shared";
import { UntypedQuizQuestion } from "@model/questionTypes/shared";
import { Box, ListItem, Typography, useTheme } from "@mui/material";
import { cancelQuestionDeletion } from "@root/questions/actions";
import { updateEditSomeQuestion } from "@root/uiTools/actions";
@ -9,6 +6,7 @@ import { useUiTools } from "@root/uiTools/store";
import { memo, useEffect } from "react";
import { Draggable } from "react-beautiful-dnd";
import QuestionsPageCard from "./QuestionPageCard";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
type Props = {
question: AnyTypedQuizQuestion | UntypedQuizQuestion;
@ -48,7 +46,10 @@ const DraggableListItem = memo<Props>(function ({
}, [editSomeQuestion]);
return (
<Draggable draggableId={question.id.toString()} index={index}>
<Draggable
draggableId={question.id.toString()}
index={index}
>
{(provided) => (
<ListItem
id={question.type === null ? "" : question.content.id}

@ -3,13 +3,11 @@ import { createUntypedQuestion } from "@root/questions/actions";
import { useState } from "react";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { ReactComponent as PlusIcon } from "../../../assets/icons/plus.svg";
import type {
AnyTypedQuizQuestion,
UntypedQuizQuestion,
} from "../../../model/questionTypes/shared";
import type { UntypedQuizQuestion } from "../../../model/questionTypes/shared";
import SwitchQuestionsPage from "../SwitchQuestionsPage";
import TypeQuestions from "../TypeQuestions";
import QuestionPageCardTitle from "./QuestionPageCardTitle";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
interface Props {
question: AnyTypedQuizQuestion | UntypedQuizQuestion;
@ -52,11 +50,7 @@ export default function QuestionsPageCard({
questionType={question.type}
page={"page" in question ? question.page : null}
isExpanded={question.expanded}
questionHasParent={
"content" in question
? question.content.rule.parentId.length > 0
: false
}
questionHasParent={"content" in question ? question.content.rule.parentId.length > 0 : false}
/>
{question.expanded && (
<Box
@ -108,8 +102,7 @@ export default function QuestionsPageCard({
backgroundPosition: "bottom",
backgroundRepeat: "repeat-x",
backgroundSize: "20px 1px",
backgroundImage:
"radial-gradient(circle, #7E2AEA 6px, #F2F3F7 1px)",
backgroundImage: "radial-gradient(circle, #7E2AEA 6px, #F2F3F7 1px)",
}}
/>
<PlusIcon />

@ -6,7 +6,7 @@ import { AnswerDraggableList } from "../AnswerDraggableList";
import ButtonsOptions from "../ButtonsOptions";
import SwitchDropDown from "./switchDropDown";
import type { QuizQuestionSelect } from "../../../model/questionTypes/select";
import type { QuizQuestionSelect } from "@frontend/squzanswerer";
import AnswerItem from "../AnswerDraggableList/AnswerItem";
interface Props {
@ -15,11 +15,7 @@ interface Props {
setOpenBranchingPage: (a: boolean) => void;
}
export default function DropDown({
question,
openBranchingPage,
setOpenBranchingPage,
}: Props) {
export default function DropDown({ question, openBranchingPage, setOpenBranchingPage }: Props) {
const onClickAddAnAnswer = useAddAnswer();
const [switchState, setSwitchState] = useState("setting");
const theme = useTheme();
@ -108,10 +104,13 @@ export default function DropDown({
questionId={question.id}
questionContentId={question.content.id}
questionType={question.type}
questionHasParent={question.content.rule.parentId.length !== 0}
questionHasParent={question.content.rule.parentId?.length !== 0}
setOpenBranchingPage={setOpenBranchingPage}
/>
<SwitchDropDown switchState={switchState} question={question} />
<SwitchDropDown
switchState={switchState}
question={question}
/>
</>
);
}

@ -3,7 +3,7 @@ import { updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField";
import { memo } from "react";
import type { QuizQuestionSelect } from "../../../model/questionTypes/select";
import type { QuizQuestionSelect } from "@frontend/squzanswerer";
type SettingDropDownProps = {
questionId: string;
@ -11,11 +11,7 @@ type SettingDropDownProps = {
isRequired: boolean;
};
const SettingDropDown = memo<SettingDropDownProps>(function ({
questionId,
questionContentDefault,
isRequired,
}) {
const SettingDropDown = memo<SettingDropDownProps>(function ({ questionId, questionContentDefault, isRequired }) {
const theme = useTheme();
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
const isMobile = useMediaQuery(theme.breakpoints.down(790));

@ -1,4 +1,4 @@
import { QuizQuestionSelect } from "@model/questionTypes/select";
import { QuizQuestionSelect } from "@frontend/squzanswerer";
import HelpQuestions from "../helpQuestions";
import SettingDropDown from "./settingDropDown";
@ -7,10 +7,7 @@ interface Props {
question: QuizQuestionSelect;
}
export default function SwitchDropDown({
switchState = "setting",
question,
}: Props) {
export default function SwitchDropDown({ switchState = "setting", question }: Props) {
switch (switchState) {
case "setting":
return (

@ -1,16 +1,9 @@
import {
Box,
Link,
Popover,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { Box, Link, Popover, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import { EmojiPicker } from "@ui_kit/EmojiPicker";
import { useState } from "react";
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji";
import type { QuizQuestionEmoji } from "@frontend/squzanswerer";
import { useAddAnswer } from "../../../utils/hooks/useAddAnswer";
import { AnswerDraggableList } from "../AnswerDraggableList";
import ButtonsOptions from "../ButtonsOptions";
@ -23,17 +16,11 @@ interface Props {
setOpenBranchingPage: (a: boolean) => void;
}
export default function Emoji({
question,
openBranchingPage,
setOpenBranchingPage,
}: Props) {
export default function Emoji({ question, openBranchingPage, setOpenBranchingPage }: Props) {
const [switchState, setSwitchState] = useState<string>("setting");
const onClickAddAnAnswer = useAddAnswer();
const [open, setOpen] = useState<boolean>(false);
const [anchorElement, setAnchorElement] = useState<HTMLDivElement | null>(
null,
);
const [anchorElement, setAnchorElement] = useState<HTMLDivElement | null>(null);
const [selectedVariant, setSelectedVariant] = useState<string | null>(null);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
@ -79,9 +66,7 @@ export default function Emoji({
updateQuestion(question.id, (question) => {
if (question.type !== "emoji") return;
const variant = question.content.variants.find(
(v) => v.id === selectedVariant,
);
const variant = question.content.variants.find((v) => v.id === selectedVariant);
if (!variant) return;
variant.extendedText = native;
@ -134,10 +119,13 @@ export default function Emoji({
questionId={question.id}
questionContentId={question.content.id}
questionType={question.type}
questionHasParent={question.content.rule.parentId.length !== 0}
questionHasParent={question.content.rule.parentId?.length !== 0}
setOpenBranchingPage={setOpenBranchingPage}
/>
<SwitchEmoji switchState={switchState} question={question} />
<SwitchEmoji
switchState={switchState}
question={question}
/>
</>
);
}

@ -1,9 +1,8 @@
import type { QuizQuestionEmoji, QuizQuestionVariant } from "@frontend/squzanswerer";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import type { QuizQuestionEmoji } from "../../../model/questionTypes/emoji";
import { memo } from "react";
import type { QuizQuestionVariant } from "@model/questionTypes/variant";
type SettingEmojiProps = {
questionId: string;

@ -1,4 +1,4 @@
import { QuizQuestionEmoji } from "@model/questionTypes/emoji";
import { QuizQuestionEmoji } from "@frontend/squzanswerer";
import HelpQuestions from "../helpQuestions";
import SettingEmoji from "./settingEmoji";

@ -15,11 +15,9 @@ import {
import { changeQuestionType } from "@root/questions/actions";
import type { RefObject } from "react";
import { useState } from "react";
import type {
AnyTypedQuizQuestion,
UntypedQuizQuestion,
} from "../../../../model/questionTypes/shared";
import type { UntypedQuizQuestion } from "../../../../model/questionTypes/shared";
import { BUTTON_TYPE_QUESTIONS } from "../../TypeQuestions";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
type ChooseAnswerModalProps = {
open: boolean;
@ -29,13 +27,7 @@ type ChooseAnswerModalProps = {
questionType: QuestionType | null;
};
export const ChooseAnswerModal = ({
open,
onClose,
anchorRef,
question,
questionType,
}: ChooseAnswerModalProps) => {
export const ChooseAnswerModal = ({ open, onClose, anchorRef, question, questionType }: ChooseAnswerModalProps) => {
const [openModal, setOpenModal] = useState<boolean>(false);
const [selectedValue, setSelectedValue] = useState<QuestionType>("text");
const theme = useTheme();
@ -69,10 +61,7 @@ export const ChooseAnswerModal = ({
<Box>{icon}</Box>
<Typography
sx={{
color:
value === questionType
? theme.palette.brightPurple.main
: theme.palette.grey2.main,
color: value === questionType ? theme.palette.brightPurple.main : theme.palette.grey2.main,
}}
>
{title}
@ -85,7 +74,10 @@ export const ChooseAnswerModal = ({
</Grow>
)}
</Popper>
<Modal open={openModal} onClose={() => setOpenModal(false)}>
<Modal
open={openModal}
onClose={() => setOpenModal(false)}
>
<Box
sx={{
position: "absolute",
@ -97,9 +89,7 @@ export const ChooseAnswerModal = ({
background: "#FFFFFF",
}}
>
<Typography variant="h6">
Все настройки, кроме заголовка вопроса будут сброшены
</Typography>
<Typography variant="h6">Все настройки, кроме заголовка вопроса будут сброшены</Typography>
<Box
sx={{
marginTop: "30px",

@ -2,13 +2,11 @@ import { Box, ListItem, Typography, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import { memo } from "react";
import { Draggable } from "react-beautiful-dnd";
import {
AnyTypedQuizQuestion,
UntypedQuizQuestion,
} from "../../../../model/questionTypes/shared";
import { UntypedQuizQuestion } from "../../../../model/questionTypes/shared";
import QuestionsPageCard from "./QuestionPageCard";
import type { DraggableProvided } from "react-beautiful-dnd";
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
type FormDraggableListItemProps = {
question: AnyTypedQuizQuestion | UntypedQuizQuestion;
@ -19,11 +17,7 @@ type FormItemProps = FormDraggableListItemProps & {
provided?: DraggableProvided;
};
export const FormItem = ({
question,
questionIndex,
provided,
}: FormItemProps) => {
export const FormItem = ({ question, questionIndex, provided }: FormItemProps) => {
const theme = useTheme();
return (
@ -85,24 +79,25 @@ export const FormItem = ({
);
};
export const FormDraggableListItem = memo(
({ question, questionIndex }: FormDraggableListItemProps) => (
<Draggable draggableId={String(question.id)} index={questionIndex}>
{(provided) => (
<ListItem
ref={provided.innerRef}
{...provided.draggableProps}
sx={{ userSelect: "none", padding: 0 }}
>
<FormItem
question={question}
questionIndex={questionIndex}
provided={provided}
/>
</ListItem>
)}
</Draggable>
),
);
export const FormDraggableListItem = memo(({ question, questionIndex }: FormDraggableListItemProps) => (
<Draggable
draggableId={String(question.id)}
index={questionIndex}
>
{(provided) => (
<ListItem
ref={provided.innerRef}
{...provided.draggableProps}
sx={{ userSelect: "none", padding: 0 }}
>
<FormItem
question={question}
questionIndex={questionIndex}
provided={provided}
/>
</ListItem>
)}
</Draggable>
));
FormDraggableListItem.displayName = "FormDraggableListItem";

@ -1,7 +1,10 @@
import { AnyTypedQuizQuestion } from "@frontend/squzanswerer";
import { ArrowDownIcon } from "@icons/questionsPage/ArrowDownIcon";
import { CopyIcon } from "@icons/questionsPage/CopyIcon";
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
import Answer from "@icons/questionsPage/answer";
import AnswerGroup from "@icons/questionsPage/answerGroup";
import Date from "@icons/questionsPage/date";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import Download from "@icons/questionsPage/download";
import DropDown from "@icons/questionsPage/drop_down";
import Emoji from "@icons/questionsPage/emoji";
@ -11,19 +14,19 @@ import OptionsPict from "@icons/questionsPage/options_pict";
import Page from "@icons/questionsPage/page";
import RatingIcon from "@icons/questionsPage/rating";
import Slider from "@icons/questionsPage/slider";
import { QuestionType } from "@model/question/question";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import {
Box,
Checkbox,
FormControl,
FormControlLabel,
IconButton,
InputAdornment,
Paper,
TextField as MuiTextField,
Paper,
TextFieldProps,
Typography,
useMediaQuery,
useTheme,
TextFieldProps,
} from "@mui/material";
import {
copyQuestion,
@ -33,28 +36,12 @@ import {
updateQuestion,
updateUntypedQuestion,
} from "@root/questions/actions";
import CustomTextField from "@ui_kit/CustomTextField";
import { FC, useRef, useState } from "react";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { useDebouncedCallback } from "use-debounce";
import type {
AnyTypedQuizQuestion,
UntypedQuizQuestion,
} from "../../../../model/questionTypes/shared";
import type { UntypedQuizQuestion } from "../../../../model/questionTypes/shared";
import SwitchQuestionsPage from "../../SwitchQuestionsPage";
import { ChooseAnswerModal } from "./ChooseAnswerModal";
import FormTypeQuestions from "../FormTypeQuestions";
import { QuestionType } from "@model/question/question";
import { CrossedEyeIcon } from "@icons/CrossedEyeIcon";
import { ArrowDownIcon } from "@icons/questionsPage/ArrowDownIcon";
import { CopyIcon } from "@icons/questionsPage/CopyIcon";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import { HideIcon } from "@icons/questionsPage/hideIcon";
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
import {
NoLuggageOutlined,
SignalCellularNullOutlined,
} from "@mui/icons-material";
import { ChooseAnswerModal } from "./ChooseAnswerModal";
const TextField = MuiTextField as unknown as FC<TextFieldProps>;
@ -64,11 +51,7 @@ interface Props {
draggableProps: DraggableProvidedDragHandleProps | null | undefined;
}
export default function QuestionsPageCard({
question,
questionIndex,
draggableProps,
}: Props) {
export default function QuestionsPageCard({ question, questionIndex, draggableProps }: Props) {
const maxLengthTextField = 225;
const [open, setOpen] = useState<boolean>(false);
const [isTextFieldtActive, setIsTextFieldtActive] = useState(false);
@ -78,8 +61,7 @@ export default function QuestionsPageCard({
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const setTitle = (title: string) => {
const updateQuestionFn =
question.type === null ? updateUntypedQuestion : updateQuestion;
const updateQuestionFn = question.type === null ? updateUntypedQuestion : updateQuestion;
updateQuestionFn(question.id, (question) => {
question.title = title;
@ -148,9 +130,7 @@ export default function QuestionsPageCard({
margin: isMobile ? "10px 0" : 0,
"& .MuiInputBase-root": {
color: "#000000",
backgroundColor: question.expanded
? theme.palette.background.default
: "transparent",
backgroundColor: question.expanded ? theme.palette.background.default : "transparent",
height: "48px",
borderRadius: "10px",
".MuiOutlinedInput-notchedOutline": {
@ -186,27 +166,22 @@ export default function QuestionsPageCard({
/>
</Box>
),
endAdornment: isTextFieldtActive &&
question.title.length >= maxLengthTextField - 7 && (
<Box
sx={{
display: "flex",
marginTop: "5px",
marginLeft: "auto",
position: "absolute",
bottom: "-28px",
right: "0",
}}
>
<Typography fontSize="14px">
{question.title.length}
</Typography>
<span>/</span>
<Typography fontSize="14px">
{maxLengthTextField}
</Typography>
</Box>
),
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>
@ -268,9 +243,7 @@ export default function QuestionsPageCard({
sx={{ padding: "0" }}
onClick={() => copyQuestion(question.id, question.quizId)}
>
<CopyIcon
style={{ color: theme.palette.brightPurple.main }}
/>
<CopyIcon style={{ color: theme.palette.brightPurple.main }} />
</IconButton>
{questionIndex > 0 && (
<IconButton
@ -281,14 +254,10 @@ export default function QuestionsPageCard({
margin: "0 5px 0 10px",
}}
onClick={() => {
deleteQuestionWithTimeout(question.id, () =>
deleteQuestion(question.id),
);
deleteQuestionWithTimeout(question.id, () => deleteQuestion(question.id));
}}
>
<DeleteIcon
style={{ color: theme.palette.brightPurple.main }}
/>
<DeleteIcon style={{ color: theme.palette.brightPurple.main }} />
</IconButton>
)}
</Box>
@ -304,12 +273,8 @@ export default function QuestionsPageCard({
marginLeft: "3px",
borderRadius: "50%",
fontSize: "16px",
color: question.expanded
? theme.palette.brightPurple.main
: "#FFF",
background: question.expanded
? "#EEE4FC"
: theme.palette.brightPurple.main,
color: question.expanded ? theme.palette.brightPurple.main : "#FFF",
background: question.expanded ? "#EEE4FC" : theme.palette.brightPurple.main,
}}
>
{questionIndex + 1}
@ -349,10 +314,18 @@ export default function QuestionsPageCard({
const IconAndrom = (questionType: QuestionType | null) => {
switch (questionType) {
case "variant":
return <Answer color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
return (
<Answer
color="#9A9AAF"
sx={{ height: "22px", width: "20px" }}
/>
);
case "images":
return (
<OptionsPict color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />
<OptionsPict
color="#9A9AAF"
sx={{ height: "22px", width: "20px" }}
/>
);
case "varimg":
return (
@ -362,26 +335,60 @@ const IconAndrom = (questionType: QuestionType | null) => {
/>
);
case "emoji":
return <Emoji color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
return (
<Emoji
color="#9A9AAF"
sx={{ height: "22px", width: "20px" }}
/>
);
case "text":
return <Input color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
return (
<Input
color="#9A9AAF"
sx={{ height: "22px", width: "20px" }}
/>
);
case "select":
return (
<DropDown color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />
<DropDown
color="#9A9AAF"
sx={{ height: "22px", width: "20px" }}
/>
);
case "date":
return <Date color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
return (
<Date
color="#9A9AAF"
sx={{ height: "22px", width: "20px" }}
/>
);
case "number":
return <Slider color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
return (
<Slider
color="#9A9AAF"
sx={{ height: "22px", width: "20px" }}
/>
);
case "file":
return (
<Download color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />
<Download
color="#9A9AAF"
sx={{ height: "22px", width: "20px" }}
/>
);
case "page":
return <Page color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
return (
<Page
color="#9A9AAF"
sx={{ height: "22px", width: "20px" }}
/>
);
case "rating":
return (
<RatingIcon color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />
<RatingIcon
color="#9A9AAF"
sx={{ height: "22px", width: "20px" }}
/>
);
default:
return null;

@ -1,14 +1,10 @@
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import {
addQuestionVariant,
clearQuestionImages,
uploadQuestionImage,
} from "@root/questions/actions";
import { addQuestionVariant, clearQuestionImages, uploadQuestionImage } from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import { useEffect, useState } from "react";
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
import type { QuizQuestionVarImg } from "@frontend/squzanswerer";
import { useDisclosure } from "../../../utils/useDisclosure";
import { AnswerDraggableList } from "../AnswerDraggableList";
import ImageEditAnswerItem from "../AnswerDraggableList/ImageEditAnswerItem";
@ -22,50 +18,31 @@ interface Props {
setOpenBranchingPage: (a: boolean) => void;
}
export default function OptionsAndPicture({
question,
setOpenBranchingPage,
}: Props) {
export default function OptionsAndPicture({ question, setOpenBranchingPage }: Props) {
const [switchState, setSwitchState] = useState("setting");
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(
null,
);
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const quizQid = useCurrentQuiz()?.qid;
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] =
useDisclosure();
const {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
originalImageUrl,
setCropModalImageBlob,
} = useCropModalState();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
const { isCropModalOpen, openCropModal, closeCropModal, imageBlob, originalImageUrl, setCropModalImageBlob } =
useCropModalState();
const handleImageUpload = async (file: File) => {
if (!selectedVariantId) return;
setPictureUploading(true);
const url = await uploadQuestionImage(
question.id,
quizQid,
file,
(question, url) => {
if (!("variants" in question.content)) return;
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
if (!("variants" in question.content)) return;
const variant = question.content.variants.find(
(variant) => variant.id === selectedVariantId,
);
if (!variant) return;
const variant = question.content.variants.find((variant) => variant.id === selectedVariantId);
if (!variant) return;
variant.extendedText = url;
variant.originalImageUrl = url;
},
);
variant.extendedText = url;
variant.originalImageUrl = url;
});
closeImageUploadModal();
openCropModal(file, url);
@ -78,9 +55,7 @@ export default function OptionsAndPicture({
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
if (!("variants" in question.content)) return;
const variant = question.content.variants.find(
(variant) => variant.id === selectedVariantId,
);
const variant = question.content.variants.find((variant) => variant.id === selectedVariantId);
if (!variant) return;
variant.extendedText = url;
@ -127,8 +102,7 @@ export default function OptionsAndPicture({
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
onDeleteClick={() => {
if (selectedVariantId)
clearQuestionImages(question.id, selectedVariantId);
if (selectedVariantId) clearQuestionImages(question.id, selectedVariantId);
}}
cropAspectRatio={{ width: 300, height: 300 }}
/>
@ -183,12 +157,15 @@ export default function OptionsAndPicture({
SSHC={setSwitchState}
questionId={question.id}
questionContentId={question.content.id}
questionHasParent={question.content.rule.parentId.length !== 0}
questionHasParent={question.content.rule.parentId?.length !== 0}
questionType={question.type}
openBranchingPage={false}
setOpenBranchingPage={setOpenBranchingPage}
/>
<SwitchOptionsAndPict switchState={switchState} question={question} />
<SwitchOptionsAndPict
switchState={switchState}
question={question}
/>
</>
);
}

@ -1,10 +1,9 @@
import type { QuizQuestionVarImg, QuizQuestionVariant } from "@frontend/squzanswerer";
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import CustomTextField from "@ui_kit/CustomTextField";
import type { QuizQuestionVarImg } from "../../../model/questionTypes/varimg";
import { memo } from "react";
import type { QuizQuestionVariant } from "@model/questionTypes/variant";
type SettingOptionsAndPictProps = {
questionId: string;

@ -1,4 +1,4 @@
import { QuizQuestionVarImg } from "@model/questionTypes/varimg";
import { QuizQuestionVarImg } from "@frontend/squzanswerer";
import UploadImage from "../UploadImage";
import HelpQuestions from "../helpQuestions";
import SettingOptionsAndPict from "./SettingOptionsAndPict";
@ -28,7 +28,12 @@ export default function SwitchOptionsAndPict({ switchState = "setting", question
/>
);
case "image":
return <UploadImage question={question} cropAspectRatio={{ width: 380, height: 300 }} />;
return (
<UploadImage
question={question}
cropAspectRatio={{ width: 380, height: 300 }}
/>
);
default:
return null;
}

@ -1,13 +1,10 @@
import { Box, Link, Typography, useMediaQuery, useTheme } from "@mui/material";
import {
clearQuestionImages,
uploadQuestionImage,
} from "@root/questions/actions";
import { clearQuestionImages, uploadQuestionImage } from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { CropModal, useCropModalState } from "@ui_kit/Modal/CropModal";
import { useState } from "react";
import { EnterIcon } from "../../../assets/icons/questionsPage/enterIcon";
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
import type { QuizQuestionImages } from "@frontend/squzanswerer";
import { useAddAnswer } from "../../../utils/hooks/useAddAnswer";
import { useDisclosure } from "../../../utils/useDisclosure";
import { AnswerDraggableList } from "../AnswerDraggableList";
@ -22,52 +19,32 @@ interface Props {
setOpenBranchingPage: (a: boolean) => void;
}
export default function OptionsPicture({
question,
openBranchingPage,
setOpenBranchingPage,
}: Props) {
export default function OptionsPicture({ question, openBranchingPage, setOpenBranchingPage }: Props) {
const theme = useTheme();
const onClickAddAnAnswer = useAddAnswer();
const quizQid = useCurrentQuiz()?.qid;
const [pictureUploding, setPictureUploading] = useState<boolean>(false);
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(
null,
);
const [selectedVariantId, setSelectedVariantId] = useState<string | null>(null);
const [switchState, setSwitchState] = useState("setting");
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] =
useDisclosure();
const {
isCropModalOpen,
openCropModal,
closeCropModal,
imageBlob,
originalImageUrl,
setCropModalImageBlob,
} = useCropModalState();
const [isImageUploadOpen, openImageUploadModal, closeImageUploadModal] = useDisclosure();
const { isCropModalOpen, openCropModal, closeCropModal, imageBlob, originalImageUrl, setCropModalImageBlob } =
useCropModalState();
const handleImageUpload = async (file: File) => {
if (!selectedVariantId) return;
setPictureUploading(true);
const url = await uploadQuestionImage(
question.id,
quizQid,
file,
(question, url) => {
if (!("variants" in question.content)) return;
const url = await uploadQuestionImage(question.id, quizQid, file, (question, url) => {
if (!("variants" in question.content)) return;
const variant = question.content.variants.find(
(variant) => variant.id === selectedVariantId,
);
if (!variant) return;
const variant = question.content.variants.find((variant) => variant.id === selectedVariantId);
if (!variant) return;
variant.extendedText = url;
variant.originalImageUrl = url;
},
);
variant.extendedText = url;
variant.originalImageUrl = url;
});
closeImageUploadModal();
openCropModal(file, url);
@ -81,9 +58,7 @@ export default function OptionsPicture({
uploadQuestionImage(question.id, quizQid, imageBlob, (question, url) => {
if (!("variants" in question.content)) return;
const variant = question.content.variants.find(
(variant) => variant.id === selectedVariantId,
);
const variant = question.content.variants.find((variant) => variant.id === selectedVariantId);
if (!variant) return;
variant.extendedText = url;
@ -124,8 +99,7 @@ export default function OptionsPicture({
onClose={closeCropModal}
onSaveImageClick={handleCropModalSaveClick}
onDeleteClick={() => {
if (selectedVariantId)
clearQuestionImages(question.id, selectedVariantId);
if (selectedVariantId) clearQuestionImages(question.id, selectedVariantId);
}}
cropAspectRatio={{ width: 452, height: 300 }}
/>
@ -167,10 +141,13 @@ export default function OptionsPicture({
questionId={question.id}
questionContentId={question.content.id}
questionType={question.type}
questionHasParent={question.content.rule.parentId.length !== 0}
questionHasParent={question.content.rule.parentId?.length !== 0}
setOpenBranchingPage={setOpenBranchingPage}
/>
<SwitchAnswerOptionsPict switchState={switchState} question={question} />
<SwitchAnswerOptionsPict
switchState={switchState}
question={question}
/>
</>
);
}

@ -1,14 +1,13 @@
import type { QuizQuestionImages, QuizQuestionVariant } from "@frontend/squzanswerer";
import { Box, Button, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import { memo } from "react";
import FormatIcon1 from "../../../assets/icons/questionsPage/FormatIcon1";
import FormatIcon2 from "../../../assets/icons/questionsPage/FormatIcon2";
import ProportionsIcon11 from "../../../assets/icons/questionsPage/ProportionsIcon11";
import ProportionsIcon12 from "../../../assets/icons/questionsPage/ProportionsIcon12";
import ProportionsIcon21 from "../../../assets/icons/questionsPage/ProportionsIcon21";
import type { QuizQuestionImages } from "../../../model/questionTypes/images";
import { memo } from "react";
import type { QuizQuestionVariant } from "@model/questionTypes/variant";
import FormatIcon1 from "../../../assets/icons/questionsPage/FormatIcon1";
import FormatIcon2 from "../../../assets/icons/questionsPage/FormatIcon2";
type Proportion = "1:1" | "2:1" | "1:2";

@ -1,4 +1,4 @@
import { QuizQuestionImages } from "@model/questionTypes/images";
import { QuizQuestionImages } from "@frontend/squzanswerer";
import HelpQuestions from "../helpQuestions";
import SettingOpytionsPict from "./settingOpytionsPict";

@ -1,16 +1,10 @@
import {
Box,
Tooltip,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { Box, Tooltip, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import CustomTextField from "@ui_kit/CustomTextField";
import TooltipClickInfo from "@ui_kit/Toolbars/TooltipClickInfo";
import { useEffect, useState } from "react";
import InfoIcon from "../../../assets/icons/InfoIcon";
import type { QuizQuestionText } from "../../../model/questionTypes/text";
import type { QuizQuestionText } from "@frontend/squzanswerer";
import ButtonsOptionsAndPict from "../ButtonsOptionsAndPict";
import SwitchTextField from "./switchTextField";
@ -20,11 +14,7 @@ interface Props {
setOpenBranchingPage: (a: boolean) => void;
}
export default function OwnTextField({
question,
openBranchingPage,
setOpenBranchingPage,
}: Props) {
export default function OwnTextField({ question, openBranchingPage, setOpenBranchingPage }: Props) {
const [switchState, setSwitchState] = useState("setting");
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));
@ -87,9 +77,7 @@ export default function OwnTextField({
Пользователю будет дано поле для ввода значения
</Typography>
{isMobile ? (
<TooltipClickInfo
title={"Будет использоваться для ввода значения."}
/>
<TooltipClickInfo title={"Будет использоваться для ввода значения."} />
) : (
<Tooltip
title="Будет использоваться для ввода значения."
@ -107,12 +95,15 @@ export default function OwnTextField({
SSHC={setSwitchState}
questionId={question.id}
questionContentId={question.content.id}
questionHasParent={question.content.rule.parentId.length !== 0}
questionHasParent={question.content.rule.parentId?.length !== 0}
questionType={question.type}
openBranchingPage={openBranchingPage}
setOpenBranchingPage={setOpenBranchingPage}
/>
<SwitchTextField switchState={switchState} question={question} />
<SwitchTextField
switchState={switchState}
question={question}
/>
</>
);
}

@ -1,7 +1,7 @@
import { Box, FormControlLabel, Radio, RadioGroup, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import type { QuizQuestionText } from "../../../model/questionTypes/text";
import type { QuizQuestionText } from "@frontend/squzanswerer";
import { memo } from "react";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
@ -28,7 +28,6 @@ type SettingTextFieldProps = {
const SettingTextField = memo<SettingTextFieldProps>(function ({ questionId, isRequired, isAutofill, answerType }) {
const theme = useTheme();
const isWrappColumn = useMediaQuery(theme.breakpoints.down(980));
const isMobile = useMediaQuery(theme.breakpoints.down(790));
const isTablet = useMediaQuery(theme.breakpoints.down(900));
const isFigmaTablte = useMediaQuery(theme.breakpoints.down(990));
@ -81,7 +80,12 @@ const SettingTextField = memo<SettingTextFieldProps>(function ({ questionId, isR
{ANSWER_TYPES.map((answerTypeItem, index) => (
<FormControlLabel
value={answerTypeItem.value}
control={<Radio checkedIcon={<RadioCheck />} icon={<RadioIcon />} />}
control={
<Radio
checkedIcon={<RadioCheck />}
icon={<RadioIcon />}
/>
}
label={answerTypeItem.name}
sx={{
color: theme.palette.grey2.main,

@ -1,4 +1,4 @@
import { QuizQuestionText } from "@model/questionTypes/text";
import { QuizQuestionText } from "@frontend/squzanswerer";
import HelpQuestions from "../helpQuestions";
import SettingTextField from "./settingTextField";
import UploadImage from "../UploadImage";
@ -16,12 +16,16 @@ export default function SwitchTextField({ switchState = "setting", question }: P
questionId={question.id}
isRequired={question.content.required}
isAutofill={question.content.autofill}
isOnlyNumbers={question.content.onlyNumbers}
answerType={question.content.answerType}
/>
);
case "image":
return <UploadImage question={question} cropAspectRatio={{ width: 400, height: 300 }} />;
return (
<UploadImage
question={question}
cropAspectRatio={{ width: 400, height: 300 }}
/>
);
case "help":
return (
<HelpQuestions

@ -7,7 +7,7 @@ import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import { MediaSelectionAndDisplay } from "@ui_kit/MediaSelectionAndDisplay";
import { DeleteFunction } from "@utils/deleteFunc";
import { useState } from "react";
import type { QuizQuestionPage } from "../../../model/questionTypes/page";
import type { QuizQuestionPage } from "@frontend/squzanswerer";
type Props = {
disableInput?: boolean;
@ -91,7 +91,7 @@ export default function PageOptions({ disableInput, question }: Props) {
if (question.type === null) {
deleteQuestion(question.id);
}
if (question.content.rule.parentId.length !== 0) {
if (question.content.rule.parentId?.length !== 0) {
setOpenDelete(true);
} else {
deleteQuestionWithTimeout(question.id, () => DeleteFunction(question.id));

@ -1,13 +1,11 @@
import { Box, Typography, useMediaQuery, useTheme } from "@mui/material";
import type { QuizQuestionPage } from "../../../model/questionTypes/page";
import type { QuizQuestionPage } from "@frontend/squzanswerer";
type SettingPageOptionsProps = {
question: QuizQuestionPage;
};
export default function SettingPageOptions({
question,
}: SettingPageOptionsProps) {
export default function SettingPageOptions({ question }: SettingPageOptionsProps) {
const theme = useTheme();
const isMobile = useMediaQuery(theme.breakpoints.down(790));

@ -1,4 +1,4 @@
import { QuizQuestionPage } from "@model/questionTypes/page";
import { QuizQuestionPage } from "@frontend/squzanswerer";
import HelpQuestions from "../helpQuestions";
import SettingPageOptions from "./SettingPageOptions";
@ -7,10 +7,7 @@ interface Props {
question: QuizQuestionPage;
}
export default function SwitchPageOptions({
switchState = "setting",
question,
}: Props) {
export default function SwitchPageOptions({ switchState = "setting", question }: Props) {
switch (switchState) {
case "setting":
return <SettingPageOptions question={question} />;

@ -1,11 +1,4 @@
import {
Box,
TextField as MuiTextField,
TextFieldProps,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { Box, TextField as MuiTextField, TextFieldProps, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import { FC, useLayoutEffect, useRef, useState } from "react";
import FlagIcon from "../../../assets/icons/questionsPage/FlagIcon";
@ -15,7 +8,7 @@ import HeartIcon from "../../../assets/icons/questionsPage/heartIcon";
import LightbulbIcon from "../../../assets/icons/questionsPage/lightbulbIcon";
import LikeIcon from "../../../assets/icons/questionsPage/likeIcon";
import TropfyIcon from "../../../assets/icons/questionsPage/tropfyIcon";
import type { QuizQuestionRating } from "../../../model/questionTypes/rating";
import type { QuizQuestionRating } from "@frontend/squzanswerer";
import ButtonsOptions from "../ButtonsOptions";
import SwitchRating from "./switchRating";
@ -32,11 +25,7 @@ export type ButtonRatingFrom = {
icon: JSX.Element;
};
export default function RatingOptions({
question,
openBranchingPage,
setOpenBranchingPage,
}: Props) {
export default function RatingOptions({ question, openBranchingPage, setOpenBranchingPage }: Props) {
const [switchState, setSwitchState] = useState("setting");
const [negativeTextWidth, setNegativeTextWidth] = useState<number>(0);
const [positiveTextWidth, setPositiveTextWidth] = useState<number>(0);
@ -156,10 +145,7 @@ export default function RatingOptions({
gap: isMobile ? "8px" : "30px",
}}
>
{Array.from(
{ length: question.content.steps },
(_, index) => index,
).map((itemNumber) => (
{Array.from({ length: question.content.steps }, (_, index) => index).map((itemNumber) => (
<Box
key={itemNumber}
{...(itemNumber === 0 || itemNumber === question.content.steps - 1
@ -182,11 +168,7 @@ export default function RatingOptions({
}
: { sx: { transform: "scale(1.5)" } })}
>
{
buttonRatingForm.find(
({ name }) => question.content.form === name,
)?.icon
}
{buttonRatingForm.find(({ name }) => question.content.form === name)?.icon}
</Box>
))}
</Box>
@ -311,10 +293,13 @@ export default function RatingOptions({
questionId={question.id}
questionContentId={question.content.id}
questionType={question.type}
questionHasParent={question.content.rule.parentId.length !== 0}
questionHasParent={question.content.rule.parentId?.length !== 0}
setOpenBranchingPage={setOpenBranchingPage}
/>
<SwitchRating switchState={switchState} question={question} />
<SwitchRating
switchState={switchState}
question={question}
/>
</>
);
}

@ -1,12 +1,5 @@
import { QuizQuestionRating } from "@model/questionTypes/rating";
import {
Box,
ButtonBase,
Slider,
Typography,
useMediaQuery,
useTheme,
} from "@mui/material";
import { QuizQuestionRating } from "@frontend/squzanswerer";
import { Box, ButtonBase, Slider, Typography, useMediaQuery, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import CustomCheckbox from "@ui_kit/CustomCheckbox";
import FlagIcon from "../../../assets/icons/questionsPage/FlagIcon";
@ -92,14 +85,8 @@ export default function SettingSlider({ question }: SettingSliderProps) {
});
}}
sx={{
backgroundColor:
question.content.form === name
? theme.palette.brightPurple.main
: "transparent",
color:
question.content.form === name
? "#ffffff"
: theme.palette.grey3.main,
backgroundColor: question.content.form === name ? theme.palette.brightPurple.main : "transparent",
color: question.content.form === name ? "#ffffff" : theme.palette.grey3.main,
width: "40px",
height: "40px",
borderRadius: "4px",

Some files were not shown because too many files have changed in this diff Show More