Merge branch 'backend-integration' into branching

This commit is contained in:
Nastya 2023-11-29 18:55:03 +03:00
commit a169dd0a49
39 changed files with 718 additions and 333 deletions

@ -5,15 +5,14 @@ import { GetQuestionListRequest, GetQuestionListResponse } from "@model/question
import { EditQuestionRequest, EditQuestionResponse } from "@model/question/edit"; import { EditQuestionRequest, EditQuestionResponse } from "@model/question/edit";
import { DeleteQuestionRequest, DeleteQuestionResponse } from "@model/question/delete"; import { DeleteQuestionRequest, DeleteQuestionResponse } from "@model/question/delete";
import { CopyQuestionRequest, CopyQuestionResponse } from "@model/question/copy"; import { CopyQuestionRequest, CopyQuestionResponse } from "@model/question/copy";
import { QUIZ_QUESTION_VARIANT } from "../constants/variant";
const baseUrl = process.env.NODE_ENV === "production" ? "/squiz" : "https://squiz.pena.digital/squiz"; const baseUrl = process.env.NODE_ENV === "production" ? "/squiz" : "https://squiz.pena.digital/squiz";
function createQuestion(body?: Partial<CreateQuestionRequest>) { function createQuestion(body: CreateQuestionRequest) {
return makeRequest<CreateQuestionRequest, RawQuestion>({ return makeRequest<CreateQuestionRequest, RawQuestion>({
url: `${baseUrl}/question/create`, url: `${baseUrl}/question/create`,
body: { ...defaultCreateQuestionBody, ...body }, body,
method: "POST", method: "POST",
}); });
} }
@ -64,16 +63,6 @@ export const questionApi = {
}; };
const defaultCreateQuestionBody: CreateQuestionRequest = {
"quiz_id": 0,
"title": "",
"description": "",
"type": "variant",
"required": true,
"page": 0,
"content": JSON.stringify(QUIZ_QUESTION_VARIANT.content),
};
const defaultGetQuestionListBody: GetQuestionListRequest = { const defaultGetQuestionListBody: GetQuestionListRequest = {
"limit": 100, "limit": 100,
"offset": 0, "offset": 0,

@ -10,6 +10,7 @@ import { RawQuiz } from "model/quiz/quiz";
const baseUrl = process.env.NODE_ENV === "production" ? "/squiz" : "https://squiz.pena.digital/squiz"; const baseUrl = process.env.NODE_ENV === "production" ? "/squiz" : "https://squiz.pena.digital/squiz";
const imagesUrl = process.env.NODE_ENV === "production" ? "/squizstorer" : "https://squiz.pena.digital/squizstorer";
function createQuiz(body?: Partial<CreateQuizRequest>) { function createQuiz(body?: Partial<CreateQuizRequest>) {
return makeRequest<CreateQuizRequest, RawQuiz>({ return makeRequest<CreateQuizRequest, RawQuiz>({
@ -69,7 +70,7 @@ function addQuizImages(quizId: number, image: Blob) {
formData.append("image", image); formData.append("image", image);
return makeRequest<FormData, never>({ return makeRequest<FormData, never>({
url: `${baseUrl}/quiz/putImages`, url: `${imagesUrl}/quiz/putImages`,
body: formData, body: formData,
method: "PUT", method: "PUT",
}); });

@ -1,5 +1,5 @@
import { QuestionType } from "@model/question/question"; import { QuestionType } from "@model/question/question";
import { AnyQuizQuestion } from "@model/questionTypes/shared"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { QUIZ_QUESTION_DATE } from "./date"; import { QUIZ_QUESTION_DATE } from "./date";
import { QUIZ_QUESTION_EMOJI } from "./emoji"; import { QUIZ_QUESTION_EMOJI } from "./emoji";
import { QUIZ_QUESTION_FILE } from "./file"; import { QUIZ_QUESTION_FILE } from "./file";
@ -13,7 +13,7 @@ import { QUIZ_QUESTION_VARIANT } from "./variant";
import { QUIZ_QUESTION_VARIMG } from "./varimg"; import { QUIZ_QUESTION_VARIMG } from "./varimg";
export const defaultQuestionByType: Record<QuestionType, Omit<AnyQuizQuestion, "id" | "backendId">> = { export const defaultQuestionByType: Record<QuestionType, Omit<AnyTypedQuizQuestion, "id" | "backendId">> = {
"date": QUIZ_QUESTION_DATE, "date": QUIZ_QUESTION_DATE,
"emoji": QUIZ_QUESTION_EMOJI, "emoji": QUIZ_QUESTION_EMOJI,
"file": QUIZ_QUESTION_FILE, "file": QUIZ_QUESTION_FILE,

@ -18,7 +18,8 @@ export const QUIZ_QUESTION_EMOJI: Omit<QuizQuestionEmoji, "id" | "backendId"> =
id: nanoid(), id: nanoid(),
answer: "", answer: "",
extendedText: "", extendedText: "",
hints: "" hints: "",
originalImageUrl: "",
}, },
], ],
}, },

@ -13,6 +13,6 @@ export const QUIZ_QUESTION_SELECT: Omit<QuizQuestionSelect, "id" | "backendId">
innerNameCheck: false, innerNameCheck: false,
innerName: "", innerName: "",
default: "", default: "",
variants: [{ id: nanoid(), answer: "", extendedText: "", hints: "" }], variants: [{ id: nanoid(), answer: "", extendedText: "", hints: "", originalImageUrl: "" }],
}, },
}; };

@ -14,6 +14,6 @@ export const QUIZ_QUESTION_VARIANT: Omit<QuizQuestionVariant, "id" | "backendId"
innerNameCheck: false, innerNameCheck: false,
required: false, required: false,
innerName: "", innerName: "",
variants: [{ id: nanoid(), answer: "", extendedText: "", hints: "" }], variants: [{ id: nanoid(), answer: "", extendedText: "", hints: "", originalImageUrl: "" }],
}, },
}; };

@ -5,15 +5,15 @@ export interface CreateQuestionRequest {
/** id of quiz for what question is creating */ /** id of quiz for what question is creating */
quiz_id: number; quiz_id: number;
/** title of question. max length 512 */ /** title of question. max length 512 */
title?: string; title: string;
/** description of question. html/text */ /** description of question. html/text */
description?: string; description: string;
/** type of question. allow only text, select, file, variant, images, varimg, emoji, date, number, page, rating */ /** type of question. allow only text, select, file, variant, images, varimg, emoji, date, number, page, rating */
type?: QuestionType; type: QuestionType;
/** set true if user MUST answer this question */ /** set true if user MUST answer this question */
required?: boolean; required: boolean;
/** page of question */ /** page of question */
page?: number; page: number;
/** json serialized of question content settings */ /** json serialized of question content settings */
content?: string; content: string;
} }

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

@ -24,5 +24,5 @@ export interface GetQuestionListRequest {
export interface GetQuestionListResponse { export interface GetQuestionListResponse {
count: number; count: number;
items: RawQuestion[]; items: RawQuestion[] | null;
} }

@ -1,4 +1,4 @@
import { AnyQuizQuestion } from "@model/questionTypes/shared"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { defaultQuestionByType } from "../../constants/default"; import { defaultQuestionByType } from "../../constants/default";
import { nanoid } from "nanoid"; import { nanoid } from "nanoid";
@ -44,7 +44,7 @@ export interface RawQuestion {
updated_at: string; updated_at: string;
} }
export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyQuizQuestion { export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyTypedQuizQuestion {
let content = defaultQuestionByType[rawQuestion.type].content; let content = defaultQuestionByType[rawQuestion.type].content;
try { try {
@ -67,5 +67,5 @@ export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyQuizQuestion
deleted: false, deleted: false,
deleteTimeoutId: 0, deleteTimeoutId: 0,
content, content,
} as AnyQuizQuestion; } as AnyTypedQuizQuestion;
} }

@ -1,7 +1,7 @@
import type { import type {
ImageQuestionVariant,
QuestionBranchingRule, QuestionBranchingRule,
QuestionHint, QuestionHint,
QuestionVariant,
QuizQuestionBase QuizQuestionBase
} from "./shared"; } from "./shared";
@ -25,7 +25,7 @@ export interface QuizQuestionImages extends QuizQuestionBase {
/** Чекбокс "Необязательный вопрос" */ /** Чекбокс "Необязательный вопрос" */
required: boolean; required: boolean;
/** Варианты (картинки) */ /** Варианты (картинки) */
variants: ImageQuestionVariant[]; variants: QuestionVariant[];
hint: QuestionHint; hint: QuestionHint;
rule: QuestionBranchingRule; rule: QuestionBranchingRule;
back: string; back: string;

@ -43,12 +43,9 @@ export type QuestionVariant = {
hints: string; hints: string;
/** Дополнительное поле для текста, emoji, ссылки на картинку */ /** Дополнительное поле для текста, emoji, ссылки на картинку */
extendedText: string; extendedText: string;
};
export interface ImageQuestionVariant extends QuestionVariant {
/** Оригинал изображения (до кропа) */ /** Оригинал изображения (до кропа) */
originalImageUrl: string; originalImageUrl: string;
} };
export interface QuizQuestionBase { export interface QuizQuestionBase {
backendId: number; backendId: number;
@ -58,7 +55,7 @@ export interface QuizQuestionBase {
title: string; title: string;
description: string; description: string;
page: number; page: number;
type?: QuestionType; type?: QuestionType | null;
expanded: boolean; expanded: boolean;
openedModalSettings: boolean; openedModalSettings: boolean;
required: boolean; required: boolean;
@ -73,11 +70,17 @@ export interface QuizQuestionBase {
}; };
} }
// export interface QuizQuestionInitial extends QuizQuestionBase { export interface UntypedQuizQuestion {
// type: "nonselected"; type: null;
// } id: string;
quizId: number;
title: string;
description: string;
expanded: boolean;
deleted: boolean;
}
export type AnyQuizQuestion = export type AnyTypedQuizQuestion =
| QuizQuestionVariant | QuizQuestionVariant
| QuizQuestionImages | QuizQuestionImages
| QuizQuestionVarImg | QuizQuestionVarImg
@ -89,13 +92,12 @@ export type AnyQuizQuestion =
| QuizQuestionFile | QuizQuestionFile
| QuizQuestionPage | QuizQuestionPage
| QuizQuestionRating; | QuizQuestionRating;
// | QuizQuestionInitial;
type FilterQuestionsWithVariants<T> = T extends { type FilterQuestionsWithVariants<T> = T extends {
content: { variants: QuestionVariant[] | ImageQuestionVariant[]; }; content: { variants: QuestionVariant[]; };
} ? T : never; } ? T : never;
export type QuizQuestionsWithVariants = FilterQuestionsWithVariants<AnyQuizQuestion>; export type QuizQuestionsWithVariants = FilterQuestionsWithVariants<AnyTypedQuizQuestion>;
export const createBranchingRuleMain: (targetId:string, parentId:string) => QuestionBranchingRuleMain = (targetId, parentId) => ({ export const createBranchingRuleMain: (targetId:string, parentId:string) => QuestionBranchingRuleMain = (targetId, parentId) => ({
@ -111,9 +113,5 @@ export const createQuestionVariant: () => QuestionVariant = () => ({
answer: "", answer: "",
extendedText: "", extendedText: "",
hints: "", hints: "",
});
export const createQuestionImageVariant: () => ImageQuestionVariant = () => ({
...createQuestionVariant(),
originalImageUrl: "", originalImageUrl: "",
}); });

@ -1,7 +1,7 @@
import type { import type {
ImageQuestionVariant,
QuestionBranchingRule, QuestionBranchingRule,
QuestionHint, QuestionHint,
QuestionVariant,
QuizQuestionBase QuizQuestionBase
} from "./shared"; } from "./shared";
@ -16,7 +16,7 @@ export interface QuizQuestionVarImg extends QuizQuestionBase {
innerName: string; innerName: string;
/** Чекбокс "Необязательный вопрос" */ /** Чекбокс "Необязательный вопрос" */
required: boolean; required: boolean;
variants: ImageQuestionVariant[]; variants: QuestionVariant[];
hint: QuestionHint; hint: QuestionHint;
rule: QuestionBranchingRule; rule: QuestionBranchingRule;
back: string; back: string;

@ -1,7 +1,7 @@
import { MessageIcon } from "@icons/messagIcon"; import { MessageIcon } from "@icons/messagIcon";
import { PointsIcon } from "@icons/questionsPage/PointsIcon"; import { PointsIcon } from "@icons/questionsPage/PointsIcon";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon"; import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import { TextareaAutosize } from "@mui/base/TextareaAutosize"; import {TextareaAutosize} from "@mui/base/TextareaAutosize";
import { import {
Box, Box,
FormControl, FormControl,
@ -16,14 +16,14 @@ import { addQuestionVariant, deleteQuestionVariant, setQuestionVariantField } fr
import type { KeyboardEvent, ReactNode } from "react"; import type { KeyboardEvent, ReactNode } from "react";
import { useState } from "react"; import { useState } from "react";
import { Draggable } from "react-beautiful-dnd"; import { Draggable } from "react-beautiful-dnd";
import type { QuestionVariant } from "../../../model/questionTypes/shared";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import type { ImageQuestionVariant, QuestionVariant } from "../../../model/questionTypes/shared";
type AnswerItemProps = { type AnswerItemProps = {
index: number; index: number;
questionId: string; questionId: string;
variant: QuestionVariant | ImageQuestionVariant; variant: QuestionVariant;
largeCheck: boolean; largeCheck: boolean;
additionalContent?: ReactNode; additionalContent?: ReactNode;
additionalMobile?: ReactNode; additionalMobile?: ReactNode;
@ -40,7 +40,11 @@ export const AnswerItem = ({
const theme = useTheme(); const theme = useTheme();
const isTablet = useMediaQuery(theme.breakpoints.down(790)); const isTablet = useMediaQuery(theme.breakpoints.down(790));
const [isOpen, setIsOpen] = useState(false); const [isOpen, setIsOpen] = useState(false);
const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null); const [anchorEl, setAnchorEl] = useState<HTMLButtonElement | null>(null);
const setQuestionVariantAnswer = useDebouncedCallback((value) => {
setQuestionVariantField(questionId, variant.id, "answer", value);
}, 200);
const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => { const handleClick = (event: React.MouseEvent<HTMLButtonElement>) => {
setAnchorEl(event.currentTarget); setAnchorEl(event.currentTarget);
@ -72,8 +76,8 @@ export const AnswerItem = ({
focused={false} focused={false}
placeholder={"Добавьте ответ"} placeholder={"Добавьте ответ"}
multiline={largeCheck} multiline={largeCheck}
onChange={({ target }: React.ChangeEvent<HTMLInputElement>) => { onChange={({ target }) => {
setQuestionVariantField(questionId, variant.id, "answer", target.value) setQuestionVariantAnswer(target.value);
}} }}
onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => { onKeyDown={(event: KeyboardEvent<HTMLInputElement>) => {
if (event.code === "Enter" && !largeCheck) { if (event.code === "Enter" && !largeCheck) {
@ -120,7 +124,7 @@ export const AnswerItem = ({
style={{ margin: "10px" }} style={{ margin: "10px" }}
placeholder="Подсказка для этого ответа" placeholder="Подсказка для этого ответа"
value={variant.hints} value={variant.hints}
onChange={e => setQuestionVariantField(questionId, variant.id, "hints", e.target.value)} onChange={e => setQuestionVariantAnswer(e.target.value)}
onKeyDown={( onKeyDown={(
event: KeyboardEvent<HTMLTextAreaElement> event: KeyboardEvent<HTMLTextAreaElement>
) => event.stopPropagation()} ) => event.stopPropagation()}

@ -3,47 +3,47 @@ import { reorderQuestionVariants } from "@root/questions/actions";
import { type ReactNode } from "react"; import { type ReactNode } from "react";
import type { DropResult } from "react-beautiful-dnd"; import type { DropResult } from "react-beautiful-dnd";
import { DragDropContext, Droppable } from "react-beautiful-dnd"; import { DragDropContext, Droppable } from "react-beautiful-dnd";
import type { ImageQuestionVariant, QuestionVariant, QuizQuestionsWithVariants } from "../../../model/questionTypes/shared"; import type { QuestionVariant, QuizQuestionsWithVariants } from "../../../model/questionTypes/shared";
import { AnswerItem } from "./AnswerItem"; import { AnswerItem } from "./AnswerItem";
type AnswerDraggableListProps = { type AnswerDraggableListProps = {
question: QuizQuestionsWithVariants; question: QuizQuestionsWithVariants;
additionalContent?: (variant: QuestionVariant | ImageQuestionVariant, index: number) => ReactNode; additionalContent?: (variant: QuestionVariant, index: number) => ReactNode;
additionalMobile?: (variant: QuestionVariant | ImageQuestionVariant, index: number) => ReactNode; additionalMobile?: (variant: QuestionVariant, index: number) => ReactNode;
}; };
export const AnswerDraggableList = ({ export const AnswerDraggableList = ({
question, question,
additionalContent, additionalContent,
additionalMobile, additionalMobile,
}: AnswerDraggableListProps) => { }: AnswerDraggableListProps) => {
const onDragEnd = ({ destination, source }: DropResult) => { const onDragEnd = ({ destination, source }: DropResult) => {
if (destination) { if (destination) {
reorderQuestionVariants(question.id, source.index, destination.index); reorderQuestionVariants(question.id, source.index, destination.index);
} }
}; };
return ( return (
<DragDropContext onDragEnd={onDragEnd}> <DragDropContext onDragEnd={onDragEnd}>
<Droppable droppableId="droppable-answer-list"> <Droppable droppableId="droppable-answer-list">
{(provided) => ( {(provided) => (
<Box ref={provided.innerRef} {...provided.droppableProps}> <Box ref={provided.innerRef} {...provided.droppableProps}>
{question.content.variants.map((variant, index) => ( {question.content.variants.map((variant, index) => (
<AnswerItem <AnswerItem
key={variant.id} key={variant.id}
index={index} index={index}
questionId={question.id} questionId={question.id}
largeCheck={("largeCheck" in question.content) ? question.content.largeCheck : false} largeCheck={("largeCheck" in question.content) ? question.content.largeCheck : false}
variant={variant} variant={variant}
additionalContent={additionalContent?.(variant, index)} additionalContent={additionalContent?.(variant, index)}
additionalMobile={additionalMobile?.(variant, index)} additionalMobile={additionalMobile?.(variant, index)}
/> />
))} ))}
{provided.placeholder} {provided.placeholder}
</Box> </Box>
)} )}
</Droppable> </Droppable>
</DragDropContext> </DragDropContext>
); );
}; };

@ -18,13 +18,13 @@ import Branching from "../../assets/icons/questionsPage/branching";
import Clue from "../../assets/icons/questionsPage/clue"; import Clue from "../../assets/icons/questionsPage/clue";
import { HideIcon } from "../../assets/icons/questionsPage/hideIcon"; import { HideIcon } from "../../assets/icons/questionsPage/hideIcon";
import SettingIcon from "../../assets/icons/questionsPage/settingIcon"; import SettingIcon from "../../assets/icons/questionsPage/settingIcon";
import type { AnyQuizQuestion } from "../../model/questionTypes/shared"; import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared";
interface Props { interface Props {
switchState: string; switchState: string;
SSHC: (data: string) => void; SSHC: (data: string) => void;
question: AnyQuizQuestion; question: AnyTypedQuizQuestion;
sx?: SxProps; sx?: SxProps;
} }

@ -1,3 +1,4 @@
import { QuestionType } from "@model/question/question";
import { import {
Box, Box,
Button, Button,
@ -11,19 +12,19 @@ import {
Typography, Typography,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { useState } from "react"; import { changeQuestionType } from "@root/questions/actions";
import { BUTTON_TYPE_QUESTIONS } from "../TypeQuestions";
import type { RefObject } from "react"; import type { RefObject } from "react";
import type { AnyQuizQuestion } from "../../../model/questionTypes/shared"; import { useState } from "react";
import { QuestionType } from "@model/question/question"; import type { AnyTypedQuizQuestion, UntypedQuizQuestion } from "../../../model/questionTypes/shared";
import { BUTTON_TYPE_QUESTIONS } from "../TypeQuestions";
type ChooseAnswerModalProps = { type ChooseAnswerModalProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
anchorRef: RefObject<HTMLDivElement>; anchorRef: RefObject<HTMLDivElement>;
question: AnyQuizQuestion; question: AnyTypedQuizQuestion | UntypedQuizQuestion;
switchState: string; questionType: QuestionType | null;
}; };
export const ChooseAnswerModal = ({ export const ChooseAnswerModal = ({
@ -31,7 +32,7 @@ export const ChooseAnswerModal = ({
onClose, onClose,
anchorRef, anchorRef,
question, question,
switchState, questionType,
}: ChooseAnswerModalProps) => { }: ChooseAnswerModalProps) => {
const [openModal, setOpenModal] = useState<boolean>(false); const [openModal, setOpenModal] = useState<boolean>(false);
const [selectedValue, setSelectedValue] = useState<QuestionType>("text"); const [selectedValue, setSelectedValue] = useState<QuestionType>("text");
@ -54,7 +55,7 @@ export const ChooseAnswerModal = ({
<MenuItem <MenuItem
key={value} key={value}
sx={{ display: "flex", gap: "10px" }} sx={{ display: "flex", gap: "10px" }}
{...(value !== switchState && { {...(value !== questionType && {
onClick: () => { onClick: () => {
onClose(); onClose();
setOpenModal(true); setOpenModal(true);
@ -66,7 +67,7 @@ export const ChooseAnswerModal = ({
<Typography <Typography
sx={{ sx={{
color: color:
value === switchState value === questionType
? theme.palette.brightPurple.main ? theme.palette.brightPurple.main
: theme.palette.grey2.main, : theme.palette.grey2.main,
}} }}
@ -114,17 +115,9 @@ export const ChooseAnswerModal = ({
<Button <Button
variant="contained" variant="contained"
sx={{ minWidth: "150px" }} sx={{ minWidth: "150px" }}
onClick={() => { // TODO onClick={() => {
// setOpenModal(false); setOpenModal(false);
changeQuestionType(question.id, selectedValue);
// const question = { ...listQuestions[quizId][totalIndex] };
// removeQuestionForce(quizId, question.id);
// createQuestion(quizId, selectedValue, totalIndex);
// updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
// title: question.title,
// expanded: question.expanded,
// });
}} }}
> >
Подтвердить Подтвердить

@ -1,4 +1,4 @@
import { AnyQuizQuestion } from "@model/questionTypes/shared"; import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "@model/questionTypes/shared";
import { Box, ListItem, Typography, useTheme } from "@mui/material"; import { Box, ListItem, Typography, useTheme } from "@mui/material";
import { memo } from "react"; import { memo } from "react";
import { Draggable } from "react-beautiful-dnd"; import { Draggable } from "react-beautiful-dnd";
@ -6,7 +6,7 @@ import QuestionsPageCard from "./QuestionPageCard";
type Props = { type Props = {
question: AnyQuizQuestion; question: AnyTypedQuizQuestion | UntypedQuizQuestion;
isDragging: boolean; isDragging: boolean;
index: number; index: number;
}; };

@ -29,18 +29,20 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { copyQuestion, createQuestion, deleteQuestion, toggleExpandQuestion, updateQuestion } from "@root/questions/actions"; import { copyQuestion, createUntypedQuestion, deleteQuestion, toggleExpandQuestion, updateQuestion, updateUntypedQuestion } from "@root/questions/actions";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import { ReactComponent as PlusIcon } from "../../../assets/icons/plus.svg"; import { ReactComponent as PlusIcon } from "../../../assets/icons/plus.svg";
import type { AnyQuizQuestion } from "../../../model/questionTypes/shared"; import type { AnyTypedQuizQuestion, UntypedQuizQuestion } from "../../../model/questionTypes/shared";
import SwitchQuestionsPage from "../SwitchQuestionsPage"; import SwitchQuestionsPage from "../SwitchQuestionsPage";
import { ChooseAnswerModal } from "./ChooseAnswerModal"; import { ChooseAnswerModal } from "./ChooseAnswerModal";
import TypeQuestions from "../TypeQuestions";
import { QuestionType } from "@model/question/question";
interface Props { interface Props {
question: AnyQuizQuestion; question: AnyTypedQuizQuestion | UntypedQuizQuestion;
draggableProps: DraggableProvidedDragHandleProps | null | undefined; draggableProps: DraggableProvidedDragHandleProps | null | undefined;
isDragging: boolean; isDragging: boolean;
} }
@ -54,7 +56,9 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
const anchorRef = useRef(null); const anchorRef = useRef(null);
const setTitle = useDebouncedCallback((title) => { const setTitle = useDebouncedCallback((title) => {
updateQuestion(question.id, question => { const updateQuestionFn = question.type === null ? updateUntypedQuestion : updateQuestion;
updateQuestionFn(question.id, question => {
question.title = title; question.title = title;
}); });
}, 200); }, 200);
@ -109,7 +113,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
anchorRef={anchorRef} anchorRef={anchorRef}
question={question} question={question}
switchState={question.type} questionType={question.type}
/> />
</Box> </Box>
), ),
@ -138,7 +142,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
fontSize: "18px", fontSize: "18px",
lineHeight: "21px", lineHeight: "21px",
py: 0, py: 0,
paddingLeft: question.type.length === 0 ? 0 : "18px", paddingLeft: question.type === null ? 0 : "18px",
}, },
"data-cy": "quiz-question-title", "data-cy": "quiz-question-title",
}} }}
@ -291,11 +295,11 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
borderRadius: "12px", borderRadius: "12px",
}} }}
> >
{/* {question.type === "nonselected" ? ( {question.type === null ? (
<TypeQuestions question={question} /> <TypeQuestions question={question} />
) : ( */} ) : (
<SwitchQuestionsPage question={question} /> <SwitchQuestionsPage question={question} />
{/* )} */} )}
</Box> </Box>
)} )}
</Paper> </Paper>
@ -311,7 +315,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
}} }}
> >
<Box <Box
onClick={() => createQuestion(question.quizId)} onClick={() => createUntypedQuestion(question.quizId)}
sx={{ sx={{
display: plusVisible && !isDragging ? "flex" : "none", display: plusVisible && !isDragging ? "flex" : "none",
width: "100%", width: "100%",
@ -339,8 +343,8 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging
); );
} }
const IconAndrom = (isExpanded: boolean, switchState: string) => { const IconAndrom = (isExpanded: boolean, questionType: QuestionType | null) => {
switch (switchState) { switch (questionType) {
case "variant": case "variant":
return ( return (
<Answer <Answer

@ -1,29 +1,13 @@
import { questionApi } from "@api/question";
import { devlog } from "@frontend/kitui";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { reorderQuestions, setQuestions } from "@root/questions/actions"; import { reorderQuestions } from "@root/questions/actions";
import { useQuestionsStore } from "@root/questions/store"; import { useQuestions } from "@root/questions/hooks";
import { useCurrentQuiz } from "@root/quizes/hooks";
import { isAxiosError } from "axios";
import { enqueueSnackbar } from "notistack";
import type { DropResult } from "react-beautiful-dnd"; import type { DropResult } from "react-beautiful-dnd";
import { DragDropContext, Droppable } from "react-beautiful-dnd"; import { DragDropContext, Droppable } from "react-beautiful-dnd";
import useSWR from "swr";
import DraggableListItem from "./DraggableListItem"; import DraggableListItem from "./DraggableListItem";
export const DraggableList = () => { export const DraggableList = () => {
const quiz = useCurrentQuiz(); const { questions, isLoading } = useQuestions();
const { isLoading } = useSWR(["questions", quiz?.backendId], ([, id]) => questionApi.getList({ quiz_id: id }), {
onSuccess: setQuestions,
onError: error => {
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
devlog("Error getting question list", error);
enqueueSnackbar(`Не удалось получить вопросы. ${message}`);
}
});
const questions = useQuestionsStore(state => state.questions);
const onDragEnd = ({ destination, source }: DropResult) => { const onDragEnd = ({ destination, source }: DropResult) => {
if (destination) reorderQuestions(source.index, destination.index); if (destination) reorderQuestions(source.index, destination.index);

@ -1,3 +1,4 @@
import { QuestionType } from "@model/question/question";
import { import {
Box, Box,
Button, Button,
@ -11,20 +12,19 @@ import {
Typography, Typography,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { useState } from "react"; import { changeQuestionType } from "@root/questions/actions";
import { BUTTON_TYPE_QUESTIONS } from "../../TypeQuestions";
import { QuestionType } from "@model/question/question";
import { updateQuestion } from "@root/questions/actions";
import type { RefObject } from "react"; import type { RefObject } from "react";
import type { AnyQuizQuestion } from "../../../../model/questionTypes/shared"; import { useState } from "react";
import type { AnyTypedQuizQuestion, UntypedQuizQuestion } from "../../../../model/questionTypes/shared";
import { BUTTON_TYPE_QUESTIONS } from "../../TypeQuestions";
type ChooseAnswerModalProps = { type ChooseAnswerModalProps = {
open: boolean; open: boolean;
onClose: () => void; onClose: () => void;
anchorRef: RefObject<HTMLDivElement>; anchorRef: RefObject<HTMLDivElement>;
question: AnyQuizQuestion; question: AnyTypedQuizQuestion | UntypedQuizQuestion;
switchState: string; questionType: QuestionType | null;
}; };
export const ChooseAnswerModal = ({ export const ChooseAnswerModal = ({
@ -32,7 +32,7 @@ export const ChooseAnswerModal = ({
onClose, onClose,
anchorRef, anchorRef,
question, question,
switchState, questionType,
}: ChooseAnswerModalProps) => { }: ChooseAnswerModalProps) => {
const [openModal, setOpenModal] = useState<boolean>(false); const [openModal, setOpenModal] = useState<boolean>(false);
const [selectedValue, setSelectedValue] = useState<QuestionType>("text"); const [selectedValue, setSelectedValue] = useState<QuestionType>("text");
@ -55,7 +55,7 @@ export const ChooseAnswerModal = ({
<MenuItem <MenuItem
key={value} key={value}
sx={{ display: "flex", gap: "10px" }} sx={{ display: "flex", gap: "10px" }}
{...(value !== switchState && { {...(value !== questionType && {
onClick: () => { onClick: () => {
onClose(); onClose();
setOpenModal(true); setOpenModal(true);
@ -67,7 +67,7 @@ export const ChooseAnswerModal = ({
<Typography <Typography
sx={{ sx={{
color: color:
value === switchState value === questionType
? theme.palette.brightPurple.main ? theme.palette.brightPurple.main
: theme.palette.grey2.main, : theme.palette.grey2.main,
}} }}
@ -117,10 +117,7 @@ export const ChooseAnswerModal = ({
sx={{ minWidth: "150px" }} sx={{ minWidth: "150px" }}
onClick={() => { onClick={() => {
setOpenModal(false); setOpenModal(false);
changeQuestionType(question.id, selectedValue)
updateQuestion(question.id, question => {
question.type = selectedValue;
});
}} }}
> >
Подтвердить Подтвердить

@ -2,18 +2,17 @@ import { Box, ListItem, Typography, useTheme } from "@mui/material";
import { updateQuestion } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
import { memo } from "react"; import { memo } from "react";
import { Draggable } from "react-beautiful-dnd"; import { Draggable } from "react-beautiful-dnd";
import { AnyQuizQuestion, QuizQuestionBase } from "../../../../model/questionTypes/shared"; import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "../../../../model/questionTypes/shared";
import QuestionsPageCard from "./QuestionPageCard"; import QuestionsPageCard from "./QuestionPageCard";
type FormDraggableListItemProps = { type FormDraggableListItemProps = {
question: AnyQuizQuestion; question: AnyTypedQuizQuestion | UntypedQuizQuestion;
questionIndex: number; questionIndex: number;
questionData: QuizQuestionBase;
}; };
export default memo( export default memo(
({ question, questionIndex, questionData }: FormDraggableListItemProps) => { ({ question, questionIndex }: FormDraggableListItemProps) => {
const theme = useTheme(); const theme = useTheme();
return ( return (
@ -24,7 +23,7 @@ export default memo(
{...(questionIndex !== 0 ? provided.draggableProps : {})} {...(questionIndex !== 0 ? provided.draggableProps : {})}
sx={{ userSelect: "none", padding: 0 }} sx={{ userSelect: "none", padding: 0 }}
> >
{questionData.deleted ? ( {question.deleted ? (
<Box <Box
{...provided.dragHandleProps} {...provided.dragHandleProps}
sx={{ sx={{

@ -12,18 +12,20 @@ import Page from "@icons/questionsPage/page";
import RatingIcon from "@icons/questionsPage/rating"; import RatingIcon from "@icons/questionsPage/rating";
import Slider from "@icons/questionsPage/slider"; import Slider from "@icons/questionsPage/slider";
import { Box, InputAdornment, Paper } from "@mui/material"; import { Box, InputAdornment, Paper } from "@mui/material";
import { updateQuestion } from "@root/questions/actions"; import { updateQuestion, updateUntypedQuestion } from "@root/questions/actions";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
import { useDebouncedCallback } from "use-debounce"; import { useDebouncedCallback } from "use-debounce";
import type { AnyQuizQuestion } from "../../../../model/questionTypes/shared"; import type { AnyTypedQuizQuestion, UntypedQuizQuestion } from "../../../../model/questionTypes/shared";
import SwitchQuestionsPage from "../../SwitchQuestionsPage"; import SwitchQuestionsPage from "../../SwitchQuestionsPage";
import { ChooseAnswerModal } from "./ChooseAnswerModal"; import { ChooseAnswerModal } from "./ChooseAnswerModal";
import FormTypeQuestions from "../FormTypeQuestions";
import { QuestionType } from "@model/question/question";
interface Props { interface Props {
question: AnyQuizQuestion; question: AnyTypedQuizQuestion | UntypedQuizQuestion;
questionIndex: number; questionIndex: number;
draggableProps: DraggableProvidedDragHandleProps | null | undefined; draggableProps: DraggableProvidedDragHandleProps | null | undefined;
} }
@ -37,7 +39,9 @@ export default function QuestionsPageCard({
const anchorRef = useRef(null); const anchorRef = useRef(null);
const setTitle = useDebouncedCallback((title) => { const setTitle = useDebouncedCallback((title) => {
updateQuestion(question.id, question => { const updateQuestionFn = question.type === null ? updateUntypedQuestion : updateQuestion;
updateQuestionFn(question.id, question => {
question.title = title; question.title = title;
}); });
}, 200); }, 200);
@ -86,7 +90,7 @@ export default function QuestionsPageCard({
onClose={() => setOpen(false)} onClose={() => setOpen(false)}
anchorRef={anchorRef} anchorRef={anchorRef}
question={question} question={question}
switchState={question.type} questionType={question.type}
/> />
</Box> </Box>
), ),
@ -103,19 +107,19 @@ export default function QuestionsPageCard({
), ),
}} }}
/> />
{/* {question.type === "" ? ( {question.type === null ? (
<FormTypeQuestions totalIndex={totalIndex} /> <FormTypeQuestions question={question} />
) : ( */} ) : (
<SwitchQuestionsPage question={question} /> <SwitchQuestionsPage question={question} />
{/* )} */} )}
</Box> </Box>
</Paper> </Paper>
</> </>
); );
} }
const IconAndrom = (switchState: string) => { const IconAndrom = (questionType: QuestionType | null) => {
switch (switchState) { switch (questionType) {
case "variant": case "variant":
return <Answer color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />; return <Answer color="#9A9AAF" sx={{ height: "22px", width: "20px" }} />;
case "images": case "images":

@ -1,29 +1,13 @@
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { reorderQuestions } from "@root/questions/actions";
import { useQuestions } from "@root/questions/hooks";
import type { DropResult } from "react-beautiful-dnd"; import type { DropResult } from "react-beautiful-dnd";
import { DragDropContext, Droppable } from "react-beautiful-dnd"; import { DragDropContext, Droppable } from "react-beautiful-dnd";
import FormDraggableListItem from "./FormDraggableListItem"; import FormDraggableListItem from "./FormDraggableListItem";
import { useQuestionsStore } from "@root/questions/store";
import { reorderQuestions, setQuestions } from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks";
import useSWR from "swr";
import { questionApi } from "@api/question";
import { devlog } from "@frontend/kitui";
import { isAxiosError } from "axios";
import { enqueueSnackbar } from "notistack";
export const FormDraggableList = () => { export const FormDraggableList = () => {
const quiz = useCurrentQuiz(); const { questions } = useQuestions();
useSWR(["questions", quiz?.backendId], ([, id]) => questionApi.getList({ quiz_id: id }), {
onSuccess: setQuestions,
onError: error => {
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
devlog("Error getting question list", error);
enqueueSnackbar(`Не удалось получить вопросы. ${message}`);
}
});
const questions = useQuestionsStore(state => state.questions);
const onDragEnd = ({ destination, source }: DropResult) => { const onDragEnd = ({ destination, source }: DropResult) => {
if (destination) reorderQuestions(source.index, destination.index); if (destination) reorderQuestions(source.index, destination.index);
@ -39,7 +23,6 @@ export const FormDraggableList = () => {
key={question.id} key={question.id}
question={question} question={question}
questionIndex={index} questionIndex={index}
questionData={question}
/> />
))} ))}
{provided.placeholder} {provided.placeholder}

@ -1,11 +1,11 @@
import { Box, Button, Typography, useTheme } from "@mui/material"; import { Box, Button, Typography, useTheme } from "@mui/material";
import { incrementCurrentStep } from "@root/quizes/actions"; import { decrementCurrentStep, incrementCurrentStep } from "@root/quizes/actions";
import QuizPreview from "@ui_kit/QuizPreview/QuizPreview"; import QuizPreview from "@ui_kit/QuizPreview/QuizPreview";
import { createPortal } from "react-dom"; import { createPortal } from "react-dom";
import AddAnswer from "../../../assets/icons/questionsPage/addAnswer"; import AddAnswer from "../../../assets/icons/questionsPage/addAnswer";
import ArrowLeft from "../../../assets/icons/questionsPage/arrowLeft"; import ArrowLeft from "../../../assets/icons/questionsPage/arrowLeft";
import { FormDraggableList } from "./FormDraggableList"; import { FormDraggableList } from "./FormDraggableList";
import { collapseAllQuestions, createQuestion } from "@root/questions/actions"; import { collapseAllQuestions, createUntypedQuestion } from "@root/questions/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
@ -68,7 +68,7 @@ export default function FormQuestionsPage() {
}, },
}} }}
onClick={() => { onClick={() => {
createQuestion(quiz.backendId); createUntypedQuestion(quiz.backendId);
}} }}
data-cy="create-question" data-cy="create-question"
> >
@ -89,6 +89,7 @@ export default function FormQuestionsPage() {
<Button <Button
variant="outlined" variant="outlined"
sx={{ padding: "10px 20px", borderRadius: "8px", height: "44px" }} sx={{ padding: "10px 20px", borderRadius: "8px", height: "44px" }}
onClick={decrementCurrentStep}
> >
<ArrowLeft /> <ArrowLeft />
</Button> </Button>

@ -1,23 +1,20 @@
import { useState } from "react";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import QuestionsMiniButton from "@ui_kit/QuestionsMiniButton"; import QuestionsMiniButton from "@ui_kit/QuestionsMiniButton";
import ButtonsOptions from "../ButtonsOptions";
import SwitchAnswerOptions from "../answerOptions/switchAnswerOptions";
import { BUTTON_TYPE_QUESTIONS } from "../TypeQuestions"; import { BUTTON_TYPE_QUESTIONS } from "../TypeQuestions";
import Answer from "../../../assets/icons/questionsPage/answer"; import Answer from "../../../assets/icons/questionsPage/answer";
import Input from "../../../assets/icons/questionsPage/input";
import DropDown from "../../../assets/icons/questionsPage/drop_down";
import Date from "../../../assets/icons/questionsPage/date"; import Date from "../../../assets/icons/questionsPage/date";
import Download from "../../../assets/icons/questionsPage/download";
import DropDown from "../../../assets/icons/questionsPage/drop_down";
import Input from "../../../assets/icons/questionsPage/input";
import Slider from "../../../assets/icons/questionsPage/slider"; import Slider from "../../../assets/icons/questionsPage/slider";
import Download from "../../../assets/icons/questionsPage/download";
import type {
AnyQuizQuestion,
} from "../../../model/questionTypes/shared";
import { QuestionType } from "@model/question/question"; import { QuestionType } from "@model/question/question";
import { updateQuestion } from "@root/questions/actions"; import { createTypedQuestion } from "@root/questions/actions";
import type {
UntypedQuizQuestion
} from "../../../model/questionTypes/shared";
type ButtonTypeQuestion = { type ButtonTypeQuestion = {
@ -60,11 +57,10 @@ const BUTTON_TYPE_SHORT_QUESTIONS: ButtonTypeQuestion[] = [
]; ];
interface Props { interface Props {
question: AnyQuizQuestion; question: UntypedQuizQuestion;
} }
export default function FormTypeQuestions({ question }: Props) { export default function FormTypeQuestions({ question }: Props) {
const [switchState, setSwitchState] = useState("");
return ( return (
<Box> <Box>
@ -83,22 +79,13 @@ export default function FormTypeQuestions({ question }: Props) {
<QuestionsMiniButton <QuestionsMiniButton
key={title} key={title}
onClick={() => { onClick={() => {
updateQuestion(question.id, question => { createTypedQuestion(question.id, questionType);
question.type = questionType;
})
}} }}
icon={icon} icon={icon}
text={title} text={title}
/> />
))} ))}
</Box> </Box>
<ButtonsOptions
switchState={switchState}
SSHC={setSwitchState}
question={question}
/>
{/* TODO конфликт типов */}
{/* <SwitchAnswerOptions switchState={switchState} question={question} /> */}
</Box> </Box>
); );
} }

@ -7,7 +7,7 @@ import {
useMediaQuery, useMediaQuery,
useTheme, useTheme,
} from "@mui/material"; } from "@mui/material";
import { collapseAllQuestions, createQuestion } from "@root/questions/actions"; import { collapseAllQuestions, createUntypedQuestion } from "@root/questions/actions";
import { decrementCurrentStep, incrementCurrentStep } from "@root/quizes/actions"; import { decrementCurrentStep, incrementCurrentStep } from "@root/quizes/actions";
import { useCurrentQuiz } from "@root/quizes/hooks"; import { useCurrentQuiz } from "@root/quizes/hooks";
import QuizPreview from "@ui_kit/QuizPreview/QuizPreview"; import QuizPreview from "@ui_kit/QuizPreview/QuizPreview";
@ -66,7 +66,7 @@ export default function QuestionsPage() {
> >
<IconButton <IconButton
onClick={() => { onClick={() => {
createQuestion(quiz.backendId); createUntypedQuestion(quiz.backendId);
}} }}
sx={{ sx={{
position: "fixed", position: "fixed",

@ -1,4 +1,4 @@
import { AnyQuizQuestion } from "@model/questionTypes/shared"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import DataOptions from "./DataOptions/DataOptions"; import DataOptions from "./DataOptions/DataOptions";
import DropDown from "./DropDown/DropDown"; import DropDown from "./DropDown/DropDown";
import Emoji from "./Emoji/Emoji"; import Emoji from "./Emoji/Emoji";
@ -10,10 +10,11 @@ import RatingOptions from "./RatingOptions/RatingOptions";
import SliderOptions from "./SliderOptions/SliderOptions"; import SliderOptions from "./SliderOptions/SliderOptions";
import UploadFile from "./UploadFile/UploadFile"; import UploadFile from "./UploadFile/UploadFile";
import AnswerOptions from "./answerOptions/AnswerOptions"; import AnswerOptions from "./answerOptions/AnswerOptions";
import { notReachable } from "../../utils/notReachable";
interface Props { interface Props {
question: AnyQuizQuestion; question: AnyTypedQuizQuestion;
} }
export default function SwitchQuestionsPage({ question }: Props) { export default function SwitchQuestionsPage({ question }: Props) {
@ -53,6 +54,6 @@ export default function SwitchQuestionsPage({ question }: Props) {
return <RatingOptions question={question} />; return <RatingOptions question={question} />;
default: default:
return <></>; notReachable(question)
} }
} }

@ -1,4 +1,6 @@
import { QuestionType } from "@model/question/question";
import { Box } from "@mui/material"; import { Box } from "@mui/material";
import { createTypedQuestion } from "@root/questions/actions";
import QuestionsMiniButton from "@ui_kit/QuestionsMiniButton"; import QuestionsMiniButton from "@ui_kit/QuestionsMiniButton";
import Answer from "../../assets/icons/questionsPage/answer"; import Answer from "../../assets/icons/questionsPage/answer";
import Date from "../../assets/icons/questionsPage/date"; import Date from "../../assets/icons/questionsPage/date";
@ -11,14 +13,12 @@ import OptionsPict from "../../assets/icons/questionsPage/options_pict";
import Page from "../../assets/icons/questionsPage/page"; import Page from "../../assets/icons/questionsPage/page";
import RatingIcon from "../../assets/icons/questionsPage/rating"; import RatingIcon from "../../assets/icons/questionsPage/rating";
import Slider from "../../assets/icons/questionsPage/slider"; import Slider from "../../assets/icons/questionsPage/slider";
import { updateQuestion } from "@root/questions/actions";
import type { import type {
AnyQuizQuestion, UntypedQuizQuestion,
} from "../../model/questionTypes/shared"; } from "../../model/questionTypes/shared";
import { QuestionType } from "@model/question/question";
interface Props { interface Props {
question: AnyQuizQuestion; question: UntypedQuizQuestion;
} }
type ButtonTypeQuestion = { type ButtonTypeQuestion = {
@ -42,9 +42,7 @@ export default function TypeQuestions({ question }: Props) {
<QuestionsMiniButton <QuestionsMiniButton
key={title} key={title}
dataCy={`select-questiontype-${value}`} dataCy={`select-questiontype-${value}`}
onClick={() => updateQuestion(question.id, question => { onClick={() => createTypedQuestion(question.id, value)}
question.type = value;
})}
icon={icon} icon={icon}
text={title} text={title}
/> />

@ -1,4 +1,4 @@
import { AnyQuizQuestion } from "@model/questionTypes/shared"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { Box, ButtonBase, Typography, useTheme } from "@mui/material"; import { Box, ButtonBase, Typography, useTheme } from "@mui/material";
import { openCropModal } from "@root/cropModal"; import { openCropModal } from "@root/cropModal";
import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal"; import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal";
@ -11,7 +11,7 @@ import { UploadImageModal } from "./UploadImageModal";
type UploadImageProps = { type UploadImageProps = {
question: AnyQuizQuestion; question: AnyTypedQuizQuestion;
}; };
export default function UploadImage({ question }: UploadImageProps) { export default function UploadImage({ question }: UploadImageProps) {

@ -0,0 +1,338 @@
import InfoIcon from "@icons/Info";
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import {
Box,
Button,
Chip,
FormControl,
FormControlLabel,
IconButton,
Link,
Modal,
Radio,
RadioGroup,
Tooltip,
Typography,
useTheme,
} from "@mui/material";
import { updateQuestion } from "@root/questions/actions";
import RadioCheck from "@ui_kit/RadioCheck";
import RadioIcon from "@ui_kit/RadioIcon";
import { useEffect, useRef, useState } from "react";
import { Select } from "./Select";
type BranchingQuestionsProps = {
question: AnyTypedQuizQuestion;
};
const ACTIONS = ["Показать", "Скрыть"];
const STIPULATIONS = ["Условие 1", "Условие 2", "Условие 3"];
const ANSWERS = ["Ответ 1", "Ответ 2", "Ответ 3"];
const CONDITIONS = [
"Все условия обязательны",
"Обязательно хотя бы одно условие",
];
export default function BranchingQuestions({
question,
}: BranchingQuestionsProps) {
const theme = useTheme();
const [title, setTitle] = useState<string>("");
const [titleInputWidth, setTitleInputWidth] = useState<number>(0);
const titleRef = useRef<HTMLDivElement>(null);
useEffect(() => {
setTitleInputWidth(titleRef.current?.offsetWidth || 0);
}, [title]);
const handleClose = () => {
updateQuestion(question.id, question => {
question.openedModalSettings = false;
});
};
return (
<>
<Modal open={question.openedModalSettings} onClose={handleClose}>
<Box
sx={{
position: "absolute",
overflow: "hidden",
top: "50%",
left: "50%",
transform: "translate(-50%, -50%)",
maxWidth: "620px",
width: "100%",
bgcolor: "background.paper",
borderRadius: "12px",
boxShadow: 24,
p: 0,
}}
>
<Box
sx={{
boxSizing: "border-box",
background: "#F2F3F7",
height: "70px",
padding: "0 25px",
display: "flex",
alignItems: "center",
}}
>
<Box sx={{ color: "#9A9AAF" }}>
<Typography component="span">(</Typography>
<Box sx={{ display: "inline" }}>
<Typography
ref={titleRef}
sx={{
position: "absolute",
opacity: 0,
zIndex: "-100",
whiteSpace: "pre",
}}
>
{title}
</Typography>
<input
type="text"
value={title}
placeholder="Заголовок вопроса"
onChange={({ target }) => setTitle(target.value)}
style={{
width: titleInputWidth ? titleInputWidth : 170,
outline: "none",
background: "transparent",
border: "none",
fontSize: "18px",
minWidth: "50px",
maxWidth: "500px",
fontFamily: "Rubik",
transition: ".2s",
}}
/>
</Box>
<Typography component="span">)</Typography>
</Box>
<Tooltip
title="Настройте условия, при которых данный вопрос будет отображаться в квизе."
placement="top"
>
<Box>
<InfoIcon />
</Box>
</Tooltip>
</Box>
<Box
sx={{
padding: "20px",
display: "flex",
flexDirection: "column",
gap: "30px",
}}
>
<Box
sx={{
display: "flex",
gap: "20px",
alignItems: "center",
}}
>
<Select
items={ACTIONS}
activeItemIndex={question.content.rule.show ? 0 : 1}
sx={{ maxWidth: "140px" }}
onChange={(action) => {
updateQuestion(question.id, question => {
question.content.rule.show = action === ACTIONS[0];
});
}}
/>
<Typography sx={{ color: theme.palette.grey2.main }}>
если в ответе на вопрос
</Typography>
</Box>
{question.content.rule.reqs.map((request, index) => (
<Box
key={index}
sx={{
padding: "20px",
borderRadius: "8px",
height: "100%",
bgcolor: "#F2F3F7",
}}
>
<Box
sx={{
display: "flex",
justifyContent: "space-between",
alignItems: "center",
pb: "5px",
}}
>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
Условие 1
</Typography>
<IconButton
sx={{ borderRadius: "6px", padding: "2px" }}
onClick={() => {
updateQuestion(question.id, question => {
question.content.rule.reqs.splice(index, 1);
});
}}
>
<DeleteIcon color={"#4D4D4D"} />
</IconButton>
</Box>
<Select
empty
activeItemIndex={request.id ? Number(request.id) : -1}
items={STIPULATIONS}
onChange={(stipulation) => {
updateQuestion(question.id, question => {
question.content.rule.reqs[index].id = String(
STIPULATIONS.findIndex((item) => item.includes(stipulation))
);
});
}}
sx={{ marginBottom: "15px" }}
/>
{request.id && (
<>
<Box
sx={{
display: "flex",
alignItems: "center",
pb: "10px",
}}
>
<Typography sx={{ color: "#4D4D4D", fontWeight: "500" }}>
Дан ответ
</Typography>
<Typography sx={{ color: "#7E2AEA", pl: "10px" }}>
(Укажите один или несколько вариантов)
</Typography>
</Box>
<Select
empty
activeItemIndex={-1}
items={ANSWERS}
onChange={(answer) => {
const answerItemIndex = ANSWERS.findIndex(
(answerItem) => answerItem === answer
);
updateQuestion(question.id, question => {
const vars = question.content.rule.reqs[index].vars;
if (vars.includes(answerItemIndex)) {
vars.push(answerItemIndex);
}
});
}}
sx={{
marginBottom: "10px",
".MuiSelect-select.MuiInputBase-input": {
color: "transparent",
},
}}
/>
<Box
sx={{
display: "flex",
gap: "10px",
}}
>
{question.content.rule.reqs[index].vars.map(
(item, varIndex) => (
<Chip
key={varIndex}
label={ANSWERS[item]}
variant="outlined"
onDelete={() => {
updateQuestion(question.id, question => {
const vars = question.content.rule.reqs[index].vars;
const removedItemIndex = vars.findIndex((varItem) => varItem === item);
if (removedItemIndex === -1) return;
vars.splice(removedItemIndex, 1);
});
}}
/>
)
)}
</Box>
</>
)}
</Box>
))}
<Box
sx={{
display: "flex",
flexDirection: "column",
alignItems: "baseline",
}}
>
<Link
variant="body2"
sx={{
color: theme.palette.brightPurple.main,
marginBottom: "10px",
}}
onClick={() => {
updateQuestion(question.id, question => {
question.content.rule.reqs.push({ id: "", vars: [] });
});
}}
>
Добавить условие
</Link>
<FormControl>
<RadioGroup
aria-labelledby="demo-controlled-radio-buttons-group"
value={question.content.rule.or ? 1 : 0}
onChange={(_, value) => {
updateQuestion(question.id, question => {
question.content.rule.or = Boolean(Number(value));
});
}}
>
{CONDITIONS.map((condition, index) => (
<FormControlLabel
key={index}
sx={{ color: theme.palette.grey2.main }}
value={index}
control={
<Radio
checkedIcon={<RadioCheck />}
icon={<RadioIcon />}
/>
}
label={condition}
/>
))}
</RadioGroup>
</FormControl>
</Box>
<Box sx={{ display: "flex", justifyContent: "end", gap: "10px" }}>
<Button
variant="outlined"
onClick={handleClose}
sx={{ width: "100%", maxWidth: "130px" }}
>
Отмена
</Button>
<Button
variant="contained"
sx={{ width: "100%", maxWidth: "130px" }}
onClick={handleClose}
>
Готово
</Button>
</Box>
</Box>
</Box>
</Modal>
</>
);
}

@ -1,4 +1,4 @@
import { AnyQuizQuestion } from "@model/questionTypes/shared"; import { AnyTypedQuizQuestion } from "@model/questionTypes/shared";
import { Box, ButtonBase, Typography } from "@mui/material"; import { Box, ButtonBase, Typography } from "@mui/material";
import { updateQuestion } from "@root/questions/actions"; import { updateQuestion } from "@root/questions/actions";
import CustomTextField from "@ui_kit/CustomTextField"; import CustomTextField from "@ui_kit/CustomTextField";
@ -13,7 +13,7 @@ import { UploadVideoModal } from "./UploadVideoModal";
type BackgroundType = "text" | "video"; type BackgroundType = "text" | "video";
type HelpQuestionsProps = { type HelpQuestionsProps = {
question: AnyQuizQuestion; question: AnyTypedQuizQuestion;
}; };
export default function HelpQuestions({ question }: HelpQuestionsProps) { export default function HelpQuestions({ question }: HelpQuestionsProps) {

@ -29,6 +29,7 @@ import { useEffect, useState } from "react";
import { Link, useNavigate } from "react-router-dom"; import { Link, useNavigate } from "react-router-dom";
import useSWR from "swr"; import useSWR from "swr";
import { SidebarMobile } from "./Sidebar/SidebarMobile"; import { SidebarMobile } from "./Sidebar/SidebarMobile";
import { cleanQuestions } from "@root/questions/actions";
export default function StartPage() { export default function StartPage() {
@ -56,7 +57,10 @@ export default function StartPage() {
if (editQuizId === null) navigate("/list"); if (editQuizId === null) navigate("/list");
}, [navigate, editQuizId]); }, [navigate, editQuizId]);
useEffect(() => () => resetEditConfig(), []); useEffect(() => () => {
resetEditConfig();
cleanQuestions();
}, []);
return ( return (
<> <>

@ -2,7 +2,7 @@ import { create } from "zustand";
import { devtools, persist } from "zustand/middleware"; import { devtools, persist } from "zustand/middleware";
import type { import type {
AnyQuizQuestion AnyTypedQuizQuestion
} from "../model/questionTypes/shared"; } from "../model/questionTypes/shared";
import { QuestionType } from "@model/question/question"; import { QuestionType } from "@model/question/question";
@ -23,7 +23,7 @@ import { QUIZ_QUESTION_VARIMG } from "../constants/varimg";
setAutoFreeze(false); setAutoFreeze(false);
interface QuestionStore { interface QuestionStore {
listQuestions: Record<string, AnyQuizQuestion[]>; listQuestions: Record<string, AnyTypedQuizQuestion[]>;
} }
let isFirstPartialize = true; let isFirstPartialize = true;
@ -107,7 +107,7 @@ export const questionStore = create<QuestionStore>()(
); );
/** @deprecated */ /** @deprecated */
export const updateQuestionsList = <T = AnyQuizQuestion>( export const updateQuestionsList = <T = AnyTypedQuizQuestion>(
quizId: number, quizId: number,
index: number, index: number,
data: Partial<T> data: Partial<T>
@ -121,7 +121,7 @@ export const updateQuestionsList = <T = AnyQuizQuestion>(
}; };
/** @deprecated */ /** @deprecated */
export const updateQuestion = <T extends AnyQuizQuestion>( export const updateQuestion = <T extends AnyTypedQuizQuestion>(
quizId: number, quizId: number,
questionIndex: number, questionIndex: number,
recipe: (question: T) => void, recipe: (question: T) => void,
@ -256,7 +256,7 @@ export const setQuestionOriginalBackgroundImage = (
/** @deprecated */ /** @deprecated */
export const updateQuestionsListDragAndDrop = ( export const updateQuestionsListDragAndDrop = (
quizId: number, quizId: number,
updatedQuestions: AnyQuizQuestion[] updatedQuestions: AnyTypedQuizQuestion[]
) => { ) => {
const questionListClone = { ...questionStore.getState()["listQuestions"] }; const questionListClone = { ...questionStore.getState()["listQuestions"] };
questionStore.setState({ questionStore.setState({
@ -361,7 +361,7 @@ export const findQuestionById = (quizId: number) => {
let found = null; let found = null;
questionStore questionStore
.getState() .getState()
["listQuestions"][quizId].some((quiz: AnyQuizQuestion, index: number) => { ["listQuestions"][quizId].some((quiz: AnyTypedQuizQuestion, index: number) => {
if (quiz.backendId === quizId) { if (quiz.backendId === quizId) {
found = { quiz, index }; found = { quiz, index };
return true; return true;

@ -2,28 +2,39 @@ import { questionApi } from "@api/question";
import { devlog } from "@frontend/kitui"; import { devlog } from "@frontend/kitui";
import { questionToEditQuestionRequest } from "@model/question/edit"; import { questionToEditQuestionRequest } from "@model/question/edit";
import { QuestionType, RawQuestion, rawQuestionToQuestion } from "@model/question/question"; import { QuestionType, RawQuestion, rawQuestionToQuestion } from "@model/question/question";
import { AnyQuizQuestion, ImageQuestionVariant, QuestionVariant, createQuestionImageVariant, createQuestionVariant } from "@model/questionTypes/shared"; import { AnyTypedQuizQuestion, QuestionVariant, UntypedQuizQuestion, createQuestionVariant } from "@model/questionTypes/shared";
import { defaultQuestionByType } from "../../constants/default";
import { produce } from "immer"; import { produce } from "immer";
import { nanoid } from "nanoid";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError"; import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
import { notReachable } from "../../utils/notReachable";
import { RequestQueue } from "../../utils/requestQueue"; import { RequestQueue } from "../../utils/requestQueue";
import { QuestionsStore, useQuestionsStore } from "./store"; import { QuestionsStore, useQuestionsStore } from "./store";
import { nanoid } from "nanoid";
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => { export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => {
const untypedQuestions = state.questions.filter(q => q.type === null);
state.questions = questions?.map(rawQuestionToQuestion) ?? []; state.questions = questions?.map(rawQuestionToQuestion) ?? [];
state.questions.push(...untypedQuestions);
}, { }, {
type: "setQuestions", type: "setQuestions",
questions, questions,
}); });
const addQuestion = (question: AnyQuizQuestion) => setProducedState(state => { export const createUntypedQuestion = (quizId: number) => setProducedState(state => {
state.questions.push(question); state.questions.push({
id: nanoid(),
quizId,
type: null,
title: "",
description: "",
deleted: false,
expanded: true,
});
}, { }, {
type: "addQuestion", type: "createUntypedQuestion",
question, quizId,
}); });
const removeQuestion = (questionId: string) => setProducedState(state => { const removeQuestion = (questionId: string) => setProducedState(state => {
@ -36,9 +47,33 @@ const removeQuestion = (questionId: string) => setProducedState(state => {
questionId, questionId,
}); });
export const updateUntypedQuestion = (
questionId: string,
updateFn: (question: UntypedQuizQuestion) => void,
) => {
setProducedState(state => {
const question = state.questions.find(q => q.id === questionId);
if (!question) return;
if (question.type !== null) throw new Error("Cannot update typed question, use 'updateQuestion' instead");
updateFn(question);
}, {
type: "updateUntypedQuestion",
questionId,
updateFn: updateFn.toString(),
});
};
export const cleanQuestions = () => setProducedState(state => {
state.questions = [];
}, {
type: "cleanQuestions",
});
const setQuestionBackendId = (questionId: string, backendId: number) => setProducedState(state => { const setQuestionBackendId = (questionId: string, backendId: number) => setProducedState(state => {
const question = state.questions.find(q => q.id === questionId); const question = state.questions.find(q => q.id === questionId);
if (!question) return; if (!question) return;
if (question.type === null) throw new Error("Cannot set backend id for untyped question");
question.backendId = backendId; question.backendId = backendId;
}, { }, {
@ -56,6 +91,10 @@ export const reorderQuestions = (
setProducedState(state => { setProducedState(state => {
const [removed] = state.questions.splice(sourceIndex, 1); const [removed] = state.questions.splice(sourceIndex, 1);
state.questions.splice(destinationIndex, 0, removed); state.questions.splice(destinationIndex, 0, removed);
}, {
type: "reorderQuestions",
sourceIndex,
destinationIndex,
}); });
}; };
@ -64,11 +103,54 @@ export const toggleExpandQuestion = (questionId: string) => setProducedState(sta
if (!question) return; if (!question) return;
question.expanded = !question.expanded; question.expanded = !question.expanded;
}, {
type: "toggleExpandQuestion",
questionId,
}); });
export const collapseAllQuestions = () => setProducedState(state => { export const collapseAllQuestions = () => setProducedState(state => {
state.questions.forEach(question => question.expanded = false); state.questions.forEach(question => question.expanded = false);
}); }, "collapseAllQuestions");
const REQUEST_DEBOUNCE = 200;
const requestQueue = new RequestQueue();
let requestTimeoutId: ReturnType<typeof setTimeout>;
export const updateQuestion = (
questionId: string,
updateFn: (question: AnyTypedQuizQuestion) => void,
) => {
setProducedState(state => {
const question = state.questions.find(q => q.id === questionId);
if (!question) return;
if (question.type === null) throw new Error("Cannot update untyped question, use 'updateUntypedQuestion' instead");
updateFn(question);
}, {
type: "updateQuestion",
questionId,
updateFn: updateFn.toString(),
});
clearTimeout(requestTimeoutId);
requestTimeoutId = setTimeout(() => {
requestQueue.enqueue(async () => {
const q = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!q) return;
if (q.type === null) throw new Error("Cannot send update request for untyped question");
const response = await questionApi.edit(questionToEditQuestionRequest(q));
setQuestionBackendId(questionId, response.updated);
}).catch(error => {
if (isAxiosCanceledError(error)) return;
devlog("Error editing question", { error, questionId });
enqueueSnackbar("Не удалось сохранить вопрос");
});
}, REQUEST_DEBOUNCE);
};
export const addQuestionVariant = (questionId: string) => { export const addQuestionVariant = (questionId: string) => {
updateQuestion(questionId, question => { updateQuestion(questionId, question => {
@ -76,20 +158,11 @@ export const addQuestionVariant = (questionId: string) => {
case "variant": case "variant":
case "emoji": case "emoji":
case "select": case "select":
question.content.variants.push(createQuestionVariant());
break;
case "images": case "images":
case "varimg": case "varimg":
question.content.variants.push(createQuestionImageVariant()); question.content.variants.push(createQuestionVariant());
break; break;
case "text": default: throw new Error(`Cannot add variant to question of type "${question.type}"`);
case "date":
case "number":
case "file":
case "page":
case "rating":
throw new Error(`Cannot add variant to question of type "${question.type}"`);
default: notReachable(question);
} }
}); });
}; };
@ -122,25 +195,6 @@ export const setQuestionVariantField = (
}); });
}; };
export const setQuestionImageVariantField = (
questionId: string,
variantId: string,
field: keyof ImageQuestionVariant,
value: ImageQuestionVariant[keyof ImageQuestionVariant],
) => {
updateQuestion(questionId, question => {
if (!("variants" in question.content)) return;
const variantIndex = question.content.variants.findIndex(variant => variant.id === variantId);
if (variantIndex === -1) return;
const variant = question.content.variants[variantIndex];
if (!("originalImageUrl" in variant)) return;
variant[field] = value;
});
};
export const reorderQuestionVariants = ( export const reorderQuestionVariants = (
questionId: string, questionId: string,
sourceIndex: number, sourceIndex: number,
@ -192,7 +246,7 @@ export const setVariantImageUrl = (
if (!("variants" in question.content)) return; if (!("variants" in question.content)) return;
const variant = question.content.variants.find(variant => variant.id === variantId); const variant = question.content.variants.find(variant => variant.id === variantId);
if (!variant || !("originalImageUrl" in variant)) return; if (!variant) return;
if (variant.extendedText === url) return; if (variant.extendedText === url) return;
@ -211,8 +265,8 @@ export const setVariantOriginalImageUrl = (
const variant = question.content.variants.find( const variant = question.content.variants.find(
variant => variant.id === variantId variant => variant.id === variantId
) as ImageQuestionVariant | undefined; ) as QuestionVariant | undefined;
if (!variant || !("originalImageUrl" in variant)) return; if (!variant) return;
if (variant.originalImageUrl === url) return; if (variant.originalImageUrl === url) return;
@ -260,51 +314,46 @@ export const setQuestionInnerName = (
}); });
}; };
const REQUEST_DEBOUNCE = 200; export const changeQuestionType = (
const requestQueue = new RequestQueue();
let requestTimeoutId: ReturnType<typeof setTimeout>;
export const updateQuestion = (
questionId: string, questionId: string,
updateFn: (question: AnyQuizQuestion) => void, type: QuestionType,
) => { ) => {
setProducedState(state => { updateQuestion(questionId, question => {
const question = state.questions.find(q => q.id === questionId); question.type = type;
if (!question) return; question.content = defaultQuestionByType[type].content;
updateFn(question);
}, {
type: "updateQuestion",
questionId,
updateFn: updateFn.toString(),
}); });
clearTimeout(requestTimeoutId);
requestTimeoutId = setTimeout(() => {
requestQueue.enqueue(async () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
const response = await questionApi.edit(questionToEditQuestionRequest(question));
setQuestionBackendId(questionId, response.updated);
}).catch(error => {
if (isAxiosCanceledError(error)) return;
devlog("Error editing question", { error, questionId });
enqueueSnackbar("Не удалось сохранить вопрос");
});
}, REQUEST_DEBOUNCE);
}; };
export const createQuestion = async (quizId: number, type: QuestionType = "variant") => requestQueue.enqueue(async () => { export const createTypedQuestion = async (
questionId: string,
type: QuestionType,
) => requestQueue.enqueue(async () => {
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return;
if (question.type !== null) throw new Error("Cannot upgrade already typed question");
try { try {
const question = await questionApi.create({ const createdQuestion = await questionApi.create({
quiz_id: quizId, quiz_id: question.quizId,
type, type,
title: question.title,
description: question.description,
page: 0,
required: true,
content: JSON.stringify(defaultQuestionByType[type].content),
}); });
addQuestion(rawQuestionToQuestion(question)); setProducedState(state => {
const questionIndex = state.questions.findIndex(q => q.id === questionId);
if (questionIndex !== -1) state.questions.splice(
questionIndex,
1,
rawQuestionToQuestion(createdQuestion)
);
}, {
type: "createTypedQuestion",
question,
});
} catch (error) { } catch (error) {
devlog("Error creating question", error); devlog("Error creating question", error);
enqueueSnackbar("Не удалось создать вопрос"); enqueueSnackbar("Не удалось создать вопрос");
@ -315,6 +364,11 @@ export const deleteQuestion = async (questionId: string) => requestQueue.enqueue
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId); const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return; if (!question) return;
if (question.type === null) {
removeQuestion(questionId);
return;
}
try { try {
await questionApi.delete(question.backendId); await questionApi.delete(question.backendId);
@ -329,6 +383,21 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques
const question = useQuestionsStore.getState().questions.find(q => q.id === questionId); const question = useQuestionsStore.getState().questions.find(q => q.id === questionId);
if (!question) return; if (!question) return;
if (question.type === null) {
const copiedQuestion = structuredClone(question);
copiedQuestion.id = nanoid();
setProducedState(state => {
state.questions.push(copiedQuestion);
}, {
type: "copyQuestion",
questionId,
quizId,
});
return;
}
try { try {
const { updated: newQuestionId } = await questionApi.copy(question.backendId, quizId); const { updated: newQuestionId } = await questionApi.copy(question.backendId, quizId);
@ -340,7 +409,7 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques
state.questions.push(copiedQuestion); state.questions.push(copiedQuestion);
}, { }, {
type: "copyQuestion", type: "copyQuestion",
questionId: questionId, questionId,
quizId, quizId,
}); });
} catch (error) { } catch (error) {

@ -0,0 +1,25 @@
import { questionApi } from "@api/question";
import { devlog } from "@frontend/kitui";
import { isAxiosError } from "axios";
import { enqueueSnackbar } from "notistack";
import useSWR from "swr";
import { setQuestions } from "./actions";
import { useQuestionsStore } from "./store";
import { useCurrentQuiz } from "@root/quizes/hooks";
export function useQuestions() {
const quiz = useCurrentQuiz();
const { isLoading, error, isValidating } = useSWR(["questions", quiz?.backendId], ([, id]) => questionApi.getList({ quiz_id: id }), {
onSuccess: setQuestions,
onError: error => {
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
devlog("Error getting question list", error);
enqueueSnackbar(`Не удалось получить вопросы. ${message}`);
}
});
const questions = useQuestionsStore(state => state.questions);
return { questions, isLoading, error, isValidating };
}

@ -1,10 +1,10 @@
import { AnyQuizQuestion } from "@model/questionTypes/shared"; import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "@model/questionTypes/shared";
import { create } from "zustand"; import { create } from "zustand";
import { devtools } from "zustand/middleware"; import { devtools } from "zustand/middleware";
export type QuestionsStore = { export type QuestionsStore = {
questions: AnyQuizQuestion[]; questions: (AnyTypedQuizQuestion | UntypedQuizQuestion);
openedModalSettingsId: string | null; openedModalSettingsId: string | null;
dragQuestionId: string | null; dragQuestionId: string | null;
}; };

@ -3,13 +3,13 @@ import { devlog, getMessageFromFetchError } from "@frontend/kitui";
import { quizToEditQuizRequest } from "@model/quiz/edit"; import { quizToEditQuizRequest } from "@model/quiz/edit";
import { Quiz, RawQuiz, rawQuizToQuiz } from "@model/quiz/quiz"; import { Quiz, RawQuiz, rawQuizToQuiz } from "@model/quiz/quiz";
import { QuizConfig, maxQuizSetupSteps } from "@model/quizSettings"; import { QuizConfig, maxQuizSetupSteps } from "@model/quizSettings";
import { createQuestion } from "@root/questions/actions";
import { produce } from "immer"; import { produce } from "immer";
import { enqueueSnackbar } from "notistack"; import { enqueueSnackbar } from "notistack";
import { NavigateFunction } from "react-router-dom"; import { NavigateFunction } from "react-router-dom";
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError"; import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
import { RequestQueue } from "../../utils/requestQueue"; import { RequestQueue } from "../../utils/requestQueue";
import { QuizStore, useQuizStore } from "./store"; import { QuizStore, useQuizStore } from "./store";
import { createUntypedQuestion } from "@root/questions/actions";
export const setEditQuizId = (quizId: number | null) => setProducedState(state => { export const setEditQuizId = (quizId: number | null) => setProducedState(state => {
@ -22,7 +22,7 @@ export const setEditQuizId = (quizId: number | null) => setProducedState(state =
export const resetEditConfig = () => setProducedState(state => { export const resetEditConfig = () => setProducedState(state => {
state.editQuizId = null; state.editQuizId = null;
state.currentStep = 0; state.currentStep = 0;
}); }, "resetEditConfig");
export const setQuizes = (quizes: RawQuiz[] | null) => setProducedState(state => { export const setQuizes = (quizes: RawQuiz[] | null) => setProducedState(state => {
state.quizes = quizes?.map(rawQuizToQuiz) ?? []; state.quizes = quizes?.map(rawQuizToQuiz) ?? [];
@ -73,6 +73,9 @@ export const decrementCurrentStep = () => setProducedState(state => {
export const setCurrentStep = (step: number) => setProducedState(state => { export const setCurrentStep = (step: number) => setProducedState(state => {
state.currentStep = Math.max(0, Math.min(maxQuizSetupSteps - 1, step)); state.currentStep = Math.max(0, Math.min(maxQuizSetupSteps - 1, step));
}, {
type: "setCurrentStep",
step,
}); });
export const setQuizType = ( export const setQuizType = (
@ -148,7 +151,7 @@ export const createQuiz = async (navigate: NavigateFunction) => requestQueue.enq
setEditQuizId(quiz.backendId); setEditQuizId(quiz.backendId);
navigate("/edit"); navigate("/edit");
await createQuestion(rawQuiz.id); await createUntypedQuestion(rawQuiz.id);
} catch (error) { } catch (error) {
devlog("Error creating quiz", error); devlog("Error creating quiz", error);

@ -5,7 +5,7 @@ import {
incrementCurrentQuestionIndex, incrementCurrentQuestionIndex,
useQuizPreviewStore, useQuizPreviewStore,
} from "@root/quizPreview"; } from "@root/quizPreview";
import { AnyQuizQuestion } from "model/questionTypes/shared"; import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "model/questionTypes/shared";
import { useEffect } from "react"; import { useEffect } from "react";
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft"; import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
import Date from "./QuizPreviewQuestionTypes/Date"; import Date from "./QuizPreviewQuestionTypes/Date";
@ -139,8 +139,10 @@ export default function QuizPreviewLayout() {
} }
function QuestionPreviewComponent({ question }: { function QuestionPreviewComponent({ question }: {
question: AnyQuizQuestion; question: AnyTypedQuizQuestion | UntypedQuizQuestion;
}) { }) {
if (question.type === null) return null;
switch (question.type) { switch (question.type) {
case "variant": return <Variant question={question} />; case "variant": return <Variant question={question} />;
case "images": return <Images question={question} />; case "images": return <Images question={question} />;