diff --git a/src/api/question.ts b/src/api/question.ts index 06fdee45..56983d76 100644 --- a/src/api/question.ts +++ b/src/api/question.ts @@ -5,15 +5,14 @@ import { GetQuestionListRequest, GetQuestionListResponse } from "@model/question import { EditQuestionRequest, EditQuestionResponse } from "@model/question/edit"; import { DeleteQuestionRequest, DeleteQuestionResponse } from "@model/question/delete"; 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"; -function createQuestion(body?: Partial) { +function createQuestion(body: CreateQuestionRequest) { return makeRequest({ url: `${baseUrl}/question/create`, - body: { ...defaultCreateQuestionBody, ...body }, + body, 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 = { "limit": 100, "offset": 0, diff --git a/src/api/quiz.ts b/src/api/quiz.ts index 1921ca7a..bdc2502b 100644 --- a/src/api/quiz.ts +++ b/src/api/quiz.ts @@ -10,6 +10,7 @@ import { RawQuiz } from "model/quiz/quiz"; 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) { return makeRequest({ @@ -69,7 +70,7 @@ function addQuizImages(quizId: number, image: Blob) { formData.append("image", image); return makeRequest({ - url: `${baseUrl}/quiz/putImages`, + url: `${imagesUrl}/quiz/putImages`, body: formData, method: "PUT", }); diff --git a/src/constants/default.ts b/src/constants/default.ts index 2fb3702d..648b0b08 100644 --- a/src/constants/default.ts +++ b/src/constants/default.ts @@ -1,5 +1,5 @@ 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_EMOJI } from "./emoji"; import { QUIZ_QUESTION_FILE } from "./file"; @@ -13,7 +13,7 @@ import { QUIZ_QUESTION_VARIANT } from "./variant"; import { QUIZ_QUESTION_VARIMG } from "./varimg"; -export const defaultQuestionByType: Record> = { +export const defaultQuestionByType: Record> = { "date": QUIZ_QUESTION_DATE, "emoji": QUIZ_QUESTION_EMOJI, "file": QUIZ_QUESTION_FILE, diff --git a/src/model/question/create.ts b/src/model/question/create.ts index ad8e44b3..89190b60 100644 --- a/src/model/question/create.ts +++ b/src/model/question/create.ts @@ -5,15 +5,15 @@ export interface CreateQuestionRequest { /** id of quiz for what question is creating */ quiz_id: number; /** title of question. max length 512 */ - title?: string; + title: string; /** 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?: QuestionType; + type: QuestionType; /** set true if user MUST answer this question */ - required?: boolean; + required: boolean; /** page of question */ - page?: number; + page: number; /** json serialized of question content settings */ - content?: string; + content: string; } diff --git a/src/model/question/edit.ts b/src/model/question/edit.ts index 66b95bf9..f17e84ff 100644 --- a/src/model/question/edit.ts +++ b/src/model/question/edit.ts @@ -1,4 +1,4 @@ -import { AnyQuizQuestion } from "@model/questionTypes/shared"; +import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import { QuestionType } from "./question"; @@ -16,7 +16,7 @@ export interface EditQuestionResponse { updated: number; } -export function questionToEditQuestionRequest(question: AnyQuizQuestion): EditQuestionRequest { +export function questionToEditQuestionRequest(question: AnyTypedQuizQuestion): EditQuestionRequest { return { id: question.backendId, title: question.title, diff --git a/src/model/question/getList.ts b/src/model/question/getList.ts index 738957dc..a0af41bc 100644 --- a/src/model/question/getList.ts +++ b/src/model/question/getList.ts @@ -24,5 +24,5 @@ export interface GetQuestionListRequest { export interface GetQuestionListResponse { count: number; - items: RawQuestion[]; + items: RawQuestion[] | null; } diff --git a/src/model/question/question.ts b/src/model/question/question.ts index f25d9cb8..e1269eda 100644 --- a/src/model/question/question.ts +++ b/src/model/question/question.ts @@ -1,4 +1,4 @@ -import { AnyQuizQuestion } from "@model/questionTypes/shared"; +import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import { defaultQuestionByType } from "../../constants/default"; import { nanoid } from "nanoid"; @@ -44,7 +44,7 @@ export interface RawQuestion { updated_at: string; } -export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyQuizQuestion { +export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyTypedQuizQuestion { let content = defaultQuestionByType[rawQuestion.type].content; try { @@ -67,5 +67,5 @@ export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyQuizQuestion deleted: false, deleteTimeoutId: 0, content, - } as AnyQuizQuestion; + } as AnyTypedQuizQuestion; } diff --git a/src/model/questionTypes/shared.ts b/src/model/questionTypes/shared.ts index a09f4ede..14e7d847 100644 --- a/src/model/questionTypes/shared.ts +++ b/src/model/questionTypes/shared.ts @@ -52,7 +52,7 @@ export interface QuizQuestionBase { title: string; description: string; page: number; - type?: QuestionType; + type?: QuestionType | null; expanded: boolean; openedModalSettings: boolean; required: boolean; @@ -67,11 +67,17 @@ export interface QuizQuestionBase { }; } -// export interface QuizQuestionInitial extends QuizQuestionBase { -// type: "nonselected"; -// } +export interface UntypedQuizQuestion { + type: null; + id: string; + quizId: number; + title: string; + description: string; + expanded: boolean; + deleted: boolean; +} -export type AnyQuizQuestion = +export type AnyTypedQuizQuestion = | QuizQuestionVariant | QuizQuestionImages | QuizQuestionVarImg @@ -83,13 +89,12 @@ export type AnyQuizQuestion = | QuizQuestionFile | QuizQuestionPage | QuizQuestionRating; -// | QuizQuestionInitial; type FilterQuestionsWithVariants = T extends { content: { variants: QuestionVariant[]; }; } ? T : never; -export type QuizQuestionsWithVariants = FilterQuestionsWithVariants; +export type QuizQuestionsWithVariants = FilterQuestionsWithVariants; export const createQuestionVariant: () => QuestionVariant = () => ({ @@ -98,4 +103,4 @@ export const createQuestionVariant: () => QuestionVariant = () => ({ extendedText: "", hints: "", originalImageUrl: "", -}); +}); diff --git a/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx b/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx index e9278fa1..413d9290 100644 --- a/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx +++ b/src/pages/Questions/AnswerDraggableList/AnswerItem.tsx @@ -17,6 +17,7 @@ import type { KeyboardEvent, ReactNode } from "react"; import { useState } from "react"; import { Draggable } from "react-beautiful-dnd"; import type { QuestionVariant } from "../../../model/questionTypes/shared"; +import { useDebouncedCallback } from "use-debounce"; type AnswerItemProps = { @@ -41,6 +42,10 @@ export const AnswerItem = ({ const [isOpen, setIsOpen] = useState(false); const [anchorEl, setAnchorEl] = useState(null); + const setQuestionVariantAnswer = useDebouncedCallback((value) => { + setQuestionVariantField(questionId, variant.id, "answer", value); + }, 200); + const handleClick = (event: React.MouseEvent) => { setAnchorEl(event.currentTarget); setIsOpen(true); @@ -72,7 +77,7 @@ export const AnswerItem = ({ placeholder={"Добавьте ответ"} multiline={largeCheck} onChange={({ target }) => { - setQuestionVariantField(questionId, variant.id, "answer", target.value); + setQuestionVariantAnswer(target.value); }} onKeyDown={(event: KeyboardEvent) => { if (event.code === "Enter" && !largeCheck) { @@ -119,7 +124,7 @@ export const AnswerItem = ({ style={{ margin: "10px" }} placeholder="Подсказка для этого ответа" value={variant.hints} - onChange={e => setQuestionVariantField(questionId, variant.id, "hints", e.target.value)} + onChange={e => setQuestionVariantAnswer(e.target.value)} onKeyDown={( event: KeyboardEvent ) => event.stopPropagation()} diff --git a/src/pages/Questions/ButtonsOptions.tsx b/src/pages/Questions/ButtonsOptions.tsx index 011aa4ce..07f9ece9 100644 --- a/src/pages/Questions/ButtonsOptions.tsx +++ b/src/pages/Questions/ButtonsOptions.tsx @@ -18,13 +18,13 @@ import Branching from "../../assets/icons/questionsPage/branching"; import Clue from "../../assets/icons/questionsPage/clue"; import { HideIcon } from "../../assets/icons/questionsPage/hideIcon"; import SettingIcon from "../../assets/icons/questionsPage/settingIcon"; -import type { AnyQuizQuestion } from "../../model/questionTypes/shared"; +import type { AnyTypedQuizQuestion } from "../../model/questionTypes/shared"; interface Props { switchState: string; SSHC: (data: string) => void; - question: AnyQuizQuestion; + question: AnyTypedQuizQuestion; sx?: SxProps; } diff --git a/src/pages/Questions/DraggableList/ChooseAnswerModal.tsx b/src/pages/Questions/DraggableList/ChooseAnswerModal.tsx index d0115f72..bfbdebf8 100644 --- a/src/pages/Questions/DraggableList/ChooseAnswerModal.tsx +++ b/src/pages/Questions/DraggableList/ChooseAnswerModal.tsx @@ -1,3 +1,4 @@ +import { QuestionType } from "@model/question/question"; import { Box, Button, @@ -11,19 +12,19 @@ import { Typography, useTheme, } from "@mui/material"; -import { useState } from "react"; -import { BUTTON_TYPE_QUESTIONS } from "../TypeQuestions"; +import { changeQuestionType } from "@root/questions/actions"; import type { RefObject } from "react"; -import type { AnyQuizQuestion } from "../../../model/questionTypes/shared"; -import { QuestionType } from "@model/question/question"; +import { useState } from "react"; +import type { AnyTypedQuizQuestion, UntypedQuizQuestion } from "../../../model/questionTypes/shared"; +import { BUTTON_TYPE_QUESTIONS } from "../TypeQuestions"; type ChooseAnswerModalProps = { open: boolean; onClose: () => void; anchorRef: RefObject; - question: AnyQuizQuestion; - switchState: string; + question: AnyTypedQuizQuestion | UntypedQuizQuestion; + questionType: QuestionType | null; }; export const ChooseAnswerModal = ({ @@ -31,7 +32,7 @@ export const ChooseAnswerModal = ({ onClose, anchorRef, question, - switchState, + questionType, }: ChooseAnswerModalProps) => { const [openModal, setOpenModal] = useState(false); const [selectedValue, setSelectedValue] = useState("text"); @@ -54,7 +55,7 @@ export const ChooseAnswerModal = ({ { onClose(); setOpenModal(true); @@ -66,7 +67,7 @@ export const ChooseAnswerModal = ({ { // TODO - // setOpenModal(false); - - // const question = { ...listQuestions[quizId][totalIndex] }; - - // removeQuestionForce(quizId, question.id); - // createQuestion(quizId, selectedValue, totalIndex); - // updateQuestionsList(quizId, totalIndex, { - // title: question.title, - // expanded: question.expanded, - // }); + onClick={() => { + setOpenModal(false); + changeQuestionType(question.id, selectedValue); }} > Подтвердить diff --git a/src/pages/Questions/DraggableList/DraggableListItem.tsx b/src/pages/Questions/DraggableList/DraggableListItem.tsx index 19dcaa36..417b5ef9 100644 --- a/src/pages/Questions/DraggableList/DraggableListItem.tsx +++ b/src/pages/Questions/DraggableList/DraggableListItem.tsx @@ -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 { memo } from "react"; import { Draggable } from "react-beautiful-dnd"; @@ -6,7 +6,7 @@ import QuestionsPageCard from "./QuestionPageCard"; type Props = { - question: AnyQuizQuestion; + question: AnyTypedQuizQuestion | UntypedQuizQuestion; isDragging: boolean; index: number; }; diff --git a/src/pages/Questions/DraggableList/QuestionPageCard.tsx b/src/pages/Questions/DraggableList/QuestionPageCard.tsx index a639bd31..b9bb3f4d 100644 --- a/src/pages/Questions/DraggableList/QuestionPageCard.tsx +++ b/src/pages/Questions/DraggableList/QuestionPageCard.tsx @@ -29,18 +29,20 @@ import { useMediaQuery, useTheme, } 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 type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd"; import { useDebouncedCallback } from "use-debounce"; 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 { ChooseAnswerModal } from "./ChooseAnswerModal"; +import TypeQuestions from "../TypeQuestions"; +import { QuestionType } from "@model/question/question"; interface Props { - question: AnyQuizQuestion; + question: AnyTypedQuizQuestion | UntypedQuizQuestion; draggableProps: DraggableProvidedDragHandleProps | null | undefined; isDragging: boolean; } @@ -54,7 +56,9 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging const anchorRef = useRef(null); const setTitle = useDebouncedCallback((title) => { - updateQuestion(question.id, question => { + const updateQuestionFn = question.type === null ? updateUntypedQuestion : updateQuestion; + + updateQuestionFn(question.id, question => { question.title = title; }); }, 200); @@ -109,7 +113,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging onClose={() => setOpen(false)} anchorRef={anchorRef} question={question} - switchState={question.type} + questionType={question.type} /> ), @@ -138,7 +142,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging fontSize: "18px", lineHeight: "21px", py: 0, - paddingLeft: question.type.length === 0 ? 0 : "18px", + paddingLeft: question.type === null ? 0 : "18px", }, "data-cy": "quiz-question-title", }} @@ -291,11 +295,11 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging borderRadius: "12px", }} > - {/* {question.type === "nonselected" ? ( + {question.type === null ? ( - ) : ( */} - - {/* )} */} + ) : ( + + )} )} @@ -311,7 +315,7 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging }} > createQuestion(question.quizId)} + onClick={() => createUntypedQuestion(question.quizId)} sx={{ display: plusVisible && !isDragging ? "flex" : "none", width: "100%", @@ -339,8 +343,8 @@ export default function QuestionsPageCard({ question, draggableProps, isDragging ); } -const IconAndrom = (isExpanded: boolean, switchState: string) => { - switch (switchState) { +const IconAndrom = (isExpanded: boolean, questionType: QuestionType | null) => { + switch (questionType) { case "variant": return ( { - const quiz = useCurrentQuiz(); - const { isLoading } = useSWR(["questions", quiz?.backendId], ([, id]) => questionApi.getList({ quiz_id: id }), { - onSuccess: setQuestions, - onError: error => { - const message = isAxiosError(error) ? (error.response?.data ?? "") : ""; - - devlog("Error getting question list", error); - enqueueSnackbar(`Не удалось получить вопросы. ${message}`); - } - }); - const questions = useQuestionsStore(state => state.questions); + const { questions, isLoading } = useQuestions(); const onDragEnd = ({ destination, source }: DropResult) => { if (destination) reorderQuestions(source.index, destination.index); diff --git a/src/pages/Questions/Form/FormDraggableList/ChooseAnswerModal.tsx b/src/pages/Questions/Form/FormDraggableList/ChooseAnswerModal.tsx index 04a7743a..4a16b407 100644 --- a/src/pages/Questions/Form/FormDraggableList/ChooseAnswerModal.tsx +++ b/src/pages/Questions/Form/FormDraggableList/ChooseAnswerModal.tsx @@ -1,3 +1,4 @@ +import { QuestionType } from "@model/question/question"; import { Box, Button, @@ -11,20 +12,19 @@ import { Typography, useTheme, } from "@mui/material"; -import { useState } from "react"; -import { BUTTON_TYPE_QUESTIONS } from "../../TypeQuestions"; -import { QuestionType } from "@model/question/question"; -import { updateQuestion } from "@root/questions/actions"; +import { changeQuestionType } from "@root/questions/actions"; 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 = { open: boolean; onClose: () => void; anchorRef: RefObject; - question: AnyQuizQuestion; - switchState: string; + question: AnyTypedQuizQuestion | UntypedQuizQuestion; + questionType: QuestionType | null; }; export const ChooseAnswerModal = ({ @@ -32,7 +32,7 @@ export const ChooseAnswerModal = ({ onClose, anchorRef, question, - switchState, + questionType, }: ChooseAnswerModalProps) => { const [openModal, setOpenModal] = useState(false); const [selectedValue, setSelectedValue] = useState("text"); @@ -55,7 +55,7 @@ export const ChooseAnswerModal = ({ { onClose(); setOpenModal(true); @@ -67,7 +67,7 @@ export const ChooseAnswerModal = ({ { setOpenModal(false); - - updateQuestion(question.id, question => { - question.type = selectedValue; - }); + changeQuestionType(question.id, selectedValue) }} > Подтвердить diff --git a/src/pages/Questions/Form/FormDraggableList/FormDraggableListItem.tsx b/src/pages/Questions/Form/FormDraggableList/FormDraggableListItem.tsx index 51dcb772..61b62039 100644 --- a/src/pages/Questions/Form/FormDraggableList/FormDraggableListItem.tsx +++ b/src/pages/Questions/Form/FormDraggableList/FormDraggableListItem.tsx @@ -2,18 +2,17 @@ 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 { AnyQuizQuestion, QuizQuestionBase } from "../../../../model/questionTypes/shared"; +import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "../../../../model/questionTypes/shared"; import QuestionsPageCard from "./QuestionPageCard"; type FormDraggableListItemProps = { - question: AnyQuizQuestion; + question: AnyTypedQuizQuestion | UntypedQuizQuestion; questionIndex: number; - questionData: QuizQuestionBase; }; export default memo( - ({ question, questionIndex, questionData }: FormDraggableListItemProps) => { + ({ question, questionIndex }: FormDraggableListItemProps) => { const theme = useTheme(); return ( @@ -24,7 +23,7 @@ export default memo( {...(questionIndex !== 0 ? provided.draggableProps : {})} sx={{ userSelect: "none", padding: 0 }} > - {questionData.deleted ? ( + {question.deleted ? ( setOpen(false)} anchorRef={anchorRef} question={question} - switchState={question.type} + questionType={question.type} /> ), @@ -103,19 +105,19 @@ export default function QuestionsPageCard({ ), }} /> - {/* {question.type === "" ? ( - - ) : ( */} - - {/* )} */} + {question.type === null ? ( + null // // TODO + ) : ( + + )} ); } -const IconAndrom = (switchState: string) => { - switch (switchState) { +const IconAndrom = (questionType: QuestionType | null) => { + switch (questionType) { case "variant": return ; case "images": diff --git a/src/pages/Questions/Form/FormDraggableList/index.tsx b/src/pages/Questions/Form/FormDraggableList/index.tsx index cf3673a7..c2e27214 100644 --- a/src/pages/Questions/Form/FormDraggableList/index.tsx +++ b/src/pages/Questions/Form/FormDraggableList/index.tsx @@ -1,29 +1,13 @@ 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 { DragDropContext, Droppable } from "react-beautiful-dnd"; 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 = () => { - const quiz = useCurrentQuiz(); - useSWR(["questions", quiz?.backendId], ([, id]) => questionApi.getList({ quiz_id: id }), { - onSuccess: setQuestions, - onError: error => { - const message = isAxiosError(error) ? (error.response?.data ?? "") : ""; - - devlog("Error getting question list", error); - enqueueSnackbar(`Не удалось получить вопросы. ${message}`); - } - }); - const questions = useQuestionsStore(state => state.questions); + const { questions } = useQuestions(); const onDragEnd = ({ destination, source }: DropResult) => { if (destination) reorderQuestions(source.index, destination.index); @@ -39,7 +23,6 @@ export const FormDraggableList = () => { key={question.id} question={question} questionIndex={index} - questionData={question} /> ))} {provided.placeholder} diff --git a/src/pages/Questions/Form/FormQuestionsPage.tsx b/src/pages/Questions/Form/FormQuestionsPage.tsx index a5b7b695..814053b3 100644 --- a/src/pages/Questions/Form/FormQuestionsPage.tsx +++ b/src/pages/Questions/Form/FormQuestionsPage.tsx @@ -5,7 +5,7 @@ import { createPortal } from "react-dom"; import AddAnswer from "../../../assets/icons/questionsPage/addAnswer"; import ArrowLeft from "../../../assets/icons/questionsPage/arrowLeft"; import { FormDraggableList } from "./FormDraggableList"; -import { collapseAllQuestions, createQuestion } from "@root/questions/actions"; +import { collapseAllQuestions, createUntypedQuestion } from "@root/questions/actions"; import { useCurrentQuiz } from "@root/quizes/hooks"; @@ -68,7 +68,7 @@ export default function FormQuestionsPage() { }, }} onClick={() => { - createQuestion(quiz.backendId); + createUntypedQuestion(quiz.backendId); }} data-cy="create-question" > diff --git a/src/pages/Questions/Form/FormTypeQuestions.tsx b/src/pages/Questions/Form/FormTypeQuestions.tsx index 6c5c7ff4..f8a741a6 100644 --- a/src/pages/Questions/Form/FormTypeQuestions.tsx +++ b/src/pages/Questions/Form/FormTypeQuestions.tsx @@ -14,7 +14,7 @@ import Slider from "../../../assets/icons/questionsPage/slider"; import Download from "../../../assets/icons/questionsPage/download"; import type { - AnyQuizQuestion, + AnyTypedQuizQuestion, } from "../../../model/questionTypes/shared"; import { QuestionType } from "@model/question/question"; import { updateQuestion } from "@root/questions/actions"; @@ -60,7 +60,7 @@ const BUTTON_TYPE_SHORT_QUESTIONS: ButtonTypeQuestion[] = [ ]; interface Props { - question: AnyQuizQuestion; + question: AnyTypedQuizQuestion; } export default function FormTypeQuestions({ question }: Props) { diff --git a/src/pages/Questions/QuestionsPage.tsx b/src/pages/Questions/QuestionsPage.tsx index c731c4c3..11ba309e 100755 --- a/src/pages/Questions/QuestionsPage.tsx +++ b/src/pages/Questions/QuestionsPage.tsx @@ -6,7 +6,7 @@ import { useMediaQuery, useTheme, } 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 { useCurrentQuiz } from "@root/quizes/hooks"; import QuizPreview from "@ui_kit/QuizPreview/QuizPreview"; @@ -59,7 +59,7 @@ export default function QuestionsPage() { > { - createQuestion(quiz.backendId); + createUntypedQuestion(quiz.backendId); }} sx={{ position: "fixed", diff --git a/src/pages/Questions/SwitchQuestionsPage.tsx b/src/pages/Questions/SwitchQuestionsPage.tsx index 52e06fc1..31e29c15 100644 --- a/src/pages/Questions/SwitchQuestionsPage.tsx +++ b/src/pages/Questions/SwitchQuestionsPage.tsx @@ -1,4 +1,4 @@ -import { AnyQuizQuestion } from "@model/questionTypes/shared"; +import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import DataOptions from "./DataOptions/DataOptions"; import DropDown from "./DropDown/DropDown"; import Emoji from "./Emoji/Emoji"; @@ -10,10 +10,11 @@ import RatingOptions from "./RatingOptions/RatingOptions"; import SliderOptions from "./SliderOptions/SliderOptions"; import UploadFile from "./UploadFile/UploadFile"; import AnswerOptions from "./answerOptions/AnswerOptions"; +import { notReachable } from "../../utils/notReachable"; interface Props { - question: AnyQuizQuestion; + question: AnyTypedQuizQuestion; } export default function SwitchQuestionsPage({ question }: Props) { @@ -53,6 +54,6 @@ export default function SwitchQuestionsPage({ question }: Props) { return ; default: - return <>; + notReachable(question) } } diff --git a/src/pages/Questions/TypeQuestions.tsx b/src/pages/Questions/TypeQuestions.tsx index 7cab9887..b8ce7367 100755 --- a/src/pages/Questions/TypeQuestions.tsx +++ b/src/pages/Questions/TypeQuestions.tsx @@ -1,4 +1,6 @@ +import { QuestionType } from "@model/question/question"; import { Box } from "@mui/material"; +import { createTypedQuestion } from "@root/questions/actions"; import QuestionsMiniButton from "@ui_kit/QuestionsMiniButton"; import Answer from "../../assets/icons/questionsPage/answer"; 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 RatingIcon from "../../assets/icons/questionsPage/rating"; import Slider from "../../assets/icons/questionsPage/slider"; -import { updateQuestion } from "@root/questions/actions"; import type { - AnyQuizQuestion, + UntypedQuizQuestion, } from "../../model/questionTypes/shared"; -import { QuestionType } from "@model/question/question"; interface Props { - question: AnyQuizQuestion; + question: UntypedQuizQuestion; } type ButtonTypeQuestion = { @@ -42,9 +42,7 @@ export default function TypeQuestions({ question }: Props) { updateQuestion(question.id, question => { - question.type = value; - })} + onClick={() => createTypedQuestion(question.id, value)} icon={icon} text={title} /> diff --git a/src/pages/Questions/UploadImage/index.tsx b/src/pages/Questions/UploadImage/index.tsx index 44b52517..7f674c42 100644 --- a/src/pages/Questions/UploadImage/index.tsx +++ b/src/pages/Questions/UploadImage/index.tsx @@ -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 { openCropModal } from "@root/cropModal"; import { closeImageUploadModal, openImageUploadModal } from "@root/imageUploadModal"; @@ -11,7 +11,7 @@ import { UploadImageModal } from "./UploadImageModal"; type UploadImageProps = { - question: AnyQuizQuestion; + question: AnyTypedQuizQuestion; }; export default function UploadImage({ question }: UploadImageProps) { diff --git a/src/pages/Questions/branchingQuestions.tsx b/src/pages/Questions/branchingQuestions.tsx index 83cefc74..4a5836a5 100644 --- a/src/pages/Questions/branchingQuestions.tsx +++ b/src/pages/Questions/branchingQuestions.tsx @@ -1,6 +1,6 @@ import InfoIcon from "@icons/Info"; import { DeleteIcon } from "@icons/questionsPage/deleteIcon"; -import { AnyQuizQuestion } from "@model/questionTypes/shared"; +import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import { Box, Button, @@ -24,7 +24,7 @@ import { Select } from "./Select"; type BranchingQuestionsProps = { - question: AnyQuizQuestion; + question: AnyTypedQuizQuestion; }; const ACTIONS = ["Показать", "Скрыть"]; diff --git a/src/pages/Questions/helpQuestions.tsx b/src/pages/Questions/helpQuestions.tsx index a997db2d..6e68264d 100644 --- a/src/pages/Questions/helpQuestions.tsx +++ b/src/pages/Questions/helpQuestions.tsx @@ -1,4 +1,4 @@ -import { AnyQuizQuestion } from "@model/questionTypes/shared"; +import { AnyTypedQuizQuestion } from "@model/questionTypes/shared"; import { Box, ButtonBase, Typography } from "@mui/material"; import { updateQuestion } from "@root/questions/actions"; import CustomTextField from "@ui_kit/CustomTextField"; @@ -13,7 +13,7 @@ import { UploadVideoModal } from "./UploadVideoModal"; type BackgroundType = "text" | "video"; type HelpQuestionsProps = { - question: AnyQuizQuestion; + question: AnyTypedQuizQuestion; }; export default function HelpQuestions({ question }: HelpQuestionsProps) { diff --git a/src/pages/startPage/StartPage.tsx b/src/pages/startPage/StartPage.tsx index c11ffed1..6b2a32a0 100755 --- a/src/pages/startPage/StartPage.tsx +++ b/src/pages/startPage/StartPage.tsx @@ -29,6 +29,7 @@ import { useEffect, useState } from "react"; import { Link, useNavigate } from "react-router-dom"; import useSWR from "swr"; import { SidebarMobile } from "./Sidebar/SidebarMobile"; +import { cleanQuestions } from "@root/questions/actions"; export default function StartPage() { @@ -56,7 +57,10 @@ export default function StartPage() { if (editQuizId === null) navigate("/list"); }, [navigate, editQuizId]); - useEffect(() => () => resetEditConfig(), []); + useEffect(() => () => { + resetEditConfig(); + cleanQuestions(); + }, []); return ( <> diff --git a/src/stores/questions.ts b/src/stores/questions.ts index 0dfdc280..d98aa87a 100644 --- a/src/stores/questions.ts +++ b/src/stores/questions.ts @@ -2,7 +2,7 @@ import { create } from "zustand"; import { devtools, persist } from "zustand/middleware"; import type { - AnyQuizQuestion + AnyTypedQuizQuestion } from "../model/questionTypes/shared"; import { QuestionType } from "@model/question/question"; @@ -23,7 +23,7 @@ import { QUIZ_QUESTION_VARIMG } from "../constants/varimg"; setAutoFreeze(false); interface QuestionStore { - listQuestions: Record; + listQuestions: Record; } let isFirstPartialize = true; @@ -107,7 +107,7 @@ export const questionStore = create()( ); /** @deprecated */ -export const updateQuestionsList = ( +export const updateQuestionsList = ( quizId: number, index: number, data: Partial @@ -121,7 +121,7 @@ export const updateQuestionsList = ( }; /** @deprecated */ -export const updateQuestion = ( +export const updateQuestion = ( quizId: number, questionIndex: number, recipe: (question: T) => void, @@ -256,7 +256,7 @@ export const setQuestionOriginalBackgroundImage = ( /** @deprecated */ export const updateQuestionsListDragAndDrop = ( quizId: number, - updatedQuestions: AnyQuizQuestion[] + updatedQuestions: AnyTypedQuizQuestion[] ) => { const questionListClone = { ...questionStore.getState()["listQuestions"] }; questionStore.setState({ @@ -361,7 +361,7 @@ export const findQuestionById = (quizId: number) => { let found = null; questionStore .getState() - ["listQuestions"][quizId].some((quiz: AnyQuizQuestion, index: number) => { + ["listQuestions"][quizId].some((quiz: AnyTypedQuizQuestion, index: number) => { if (quiz.backendId === quizId) { found = { quiz, index }; return true; diff --git a/src/stores/questions/actions.ts b/src/stores/questions/actions.ts index ae351fe4..5088ce3c 100644 --- a/src/stores/questions/actions.ts +++ b/src/stores/questions/actions.ts @@ -2,7 +2,8 @@ import { questionApi } from "@api/question"; import { devlog } from "@frontend/kitui"; import { questionToEditQuestionRequest } from "@model/question/edit"; import { QuestionType, RawQuestion, rawQuestionToQuestion } from "@model/question/question"; -import { AnyQuizQuestion, QuestionVariant, createQuestionVariant } from "@model/questionTypes/shared"; +import { AnyTypedQuizQuestion, QuestionVariant, UntypedQuizQuestion, createQuestionVariant } from "@model/questionTypes/shared"; +import { defaultQuestionByType } from "../../constants/default"; import { produce } from "immer"; import { nanoid } from "nanoid"; import { enqueueSnackbar } from "notistack"; @@ -12,17 +13,28 @@ import { QuestionsStore, useQuestionsStore } from "./store"; export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => { + const untypedQuestions = state.questions.filter(q => q.type === null); + state.questions = questions?.map(rawQuestionToQuestion) ?? []; + state.questions.push(...untypedQuestions); }, { type: "setQuestions", questions, }); -const addQuestion = (question: AnyQuizQuestion) => setProducedState(state => { - state.questions.push(question); +export const createUntypedQuestion = (quizId: number) => setProducedState(state => { + state.questions.push({ + id: nanoid(), + quizId, + type: null, + title: "", + description: "", + deleted: false, + expanded: true, + }); }, { - type: "addQuestion", - question, + type: "createUntypedQuestion", + quizId, }); const removeQuestion = (questionId: string) => setProducedState(state => { @@ -35,9 +47,33 @@ const removeQuestion = (questionId: string) => setProducedState(state => { 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 question = state.questions.find(q => q.id === questionId); if (!question) return; + if (question.type === null) throw new Error("Cannot set backend id for untyped question"); question.backendId = backendId; }, { @@ -55,6 +91,10 @@ export const reorderQuestions = ( setProducedState(state => { const [removed] = state.questions.splice(sourceIndex, 1); state.questions.splice(destinationIndex, 0, removed); + }, { + type: "reorderQuestions", + sourceIndex, + destinationIndex, }); }; @@ -63,11 +103,54 @@ export const toggleExpandQuestion = (questionId: string) => setProducedState(sta if (!question) return; question.expanded = !question.expanded; +}, { + type: "toggleExpandQuestion", + questionId, }); export const collapseAllQuestions = () => setProducedState(state => { state.questions.forEach(question => question.expanded = false); -}); +}, "collapseAllQuestions"); + + +const REQUEST_DEBOUNCE = 200; +const requestQueue = new RequestQueue(); +let requestTimeoutId: ReturnType; + +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) => { updateQuestion(questionId, question => { @@ -231,51 +314,46 @@ export const setQuestionInnerName = ( }); }; -const REQUEST_DEBOUNCE = 200; -const requestQueue = new RequestQueue(); -let requestTimeoutId: ReturnType; - -export const updateQuestion = ( +export const changeQuestionType = ( questionId: string, - updateFn: (question: AnyQuizQuestion) => void, + type: QuestionType, ) => { - setProducedState(state => { - const question = state.questions.find(q => q.id === questionId); - if (!question) return; - - updateFn(question); - }, { - type: "updateQuestion", - questionId, - updateFn: updateFn.toString(), + updateQuestion(questionId, question => { + question.type = type; + question.content = defaultQuestionByType[type].content; }); - - 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 { - const question = await questionApi.create({ - quiz_id: quizId, + const createdQuestion = await questionApi.create({ + quiz_id: question.quizId, 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) { devlog("Error creating question", error); enqueueSnackbar("Не удалось создать вопрос"); @@ -286,6 +364,11 @@ export const deleteQuestion = async (questionId: string) => requestQueue.enqueue const question = useQuestionsStore.getState().questions.find(q => q.id === questionId); if (!question) return; + if (question.type === null) { + removeQuestion(questionId); + return; + } + try { await questionApi.delete(question.backendId); @@ -300,6 +383,21 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques const question = useQuestionsStore.getState().questions.find(q => q.id === questionId); 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 { const { updated: newQuestionId } = await questionApi.copy(question.backendId, quizId); @@ -311,7 +409,7 @@ export const copyQuestion = async (questionId: string, quizId: number) => reques state.questions.push(copiedQuestion); }, { type: "copyQuestion", - questionId: questionId, + questionId, quizId, }); } catch (error) { diff --git a/src/stores/questions/hooks.ts b/src/stores/questions/hooks.ts new file mode 100644 index 00000000..a1abe430 --- /dev/null +++ b/src/stores/questions/hooks.ts @@ -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(error) ? (error.response?.data ?? "") : ""; + + devlog("Error getting question list", error); + enqueueSnackbar(`Не удалось получить вопросы. ${message}`); + } + }); + const questions = useQuestionsStore(state => state.questions); + + return { questions, isLoading, error, isValidating }; +} diff --git a/src/stores/questions/store.ts b/src/stores/questions/store.ts index 347b7e08..35abe46b 100644 --- a/src/stores/questions/store.ts +++ b/src/stores/questions/store.ts @@ -1,10 +1,10 @@ -import { AnyQuizQuestion } from "@model/questionTypes/shared"; +import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "@model/questionTypes/shared"; import { create } from "zustand"; import { devtools } from "zustand/middleware"; export type QuestionsStore = { - questions: AnyQuizQuestion[]; + questions: (AnyTypedQuizQuestion | UntypedQuizQuestion)[]; }; const initialState: QuestionsStore = { diff --git a/src/stores/quizes/actions.ts b/src/stores/quizes/actions.ts index c8dee4cc..72495c18 100644 --- a/src/stores/quizes/actions.ts +++ b/src/stores/quizes/actions.ts @@ -3,13 +3,13 @@ import { devlog, getMessageFromFetchError } from "@frontend/kitui"; import { quizToEditQuizRequest } from "@model/quiz/edit"; import { Quiz, RawQuiz, rawQuizToQuiz } from "@model/quiz/quiz"; import { QuizConfig, maxQuizSetupSteps } from "@model/quizSettings"; -import { createQuestion } from "@root/questions/actions"; import { produce } from "immer"; import { enqueueSnackbar } from "notistack"; import { NavigateFunction } from "react-router-dom"; import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError"; import { RequestQueue } from "../../utils/requestQueue"; import { QuizStore, useQuizStore } from "./store"; +import { createUntypedQuestion } from "@root/questions/actions"; 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 => { state.editQuizId = null; state.currentStep = 0; -}); +}, "resetEditConfig"); export const setQuizes = (quizes: RawQuiz[] | null) => setProducedState(state => { state.quizes = quizes?.map(rawQuizToQuiz) ?? []; @@ -73,6 +73,9 @@ export const decrementCurrentStep = () => setProducedState(state => { export const setCurrentStep = (step: number) => setProducedState(state => { state.currentStep = Math.max(0, Math.min(maxQuizSetupSteps - 1, step)); +}, { + type: "setCurrentStep", + step, }); export const setQuizType = ( @@ -148,7 +151,7 @@ export const createQuiz = async (navigate: NavigateFunction) => requestQueue.enq setEditQuizId(quiz.backendId); navigate("/edit"); - await createQuestion(rawQuiz.id); + await createUntypedQuestion(rawQuiz.id); } catch (error) { devlog("Error creating quiz", error); diff --git a/src/ui_kit/QuizPreview/QuizPreviewLayout.tsx b/src/ui_kit/QuizPreview/QuizPreviewLayout.tsx index 4c8ebb26..fc03434d 100644 --- a/src/ui_kit/QuizPreview/QuizPreviewLayout.tsx +++ b/src/ui_kit/QuizPreview/QuizPreviewLayout.tsx @@ -5,7 +5,7 @@ import { incrementCurrentQuestionIndex, useQuizPreviewStore, } from "@root/quizPreview"; -import { AnyQuizQuestion } from "model/questionTypes/shared"; +import { AnyTypedQuizQuestion, UntypedQuizQuestion } from "model/questionTypes/shared"; import { useEffect } from "react"; import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft"; import Date from "./QuizPreviewQuestionTypes/Date"; @@ -139,8 +139,10 @@ export default function QuizPreviewLayout() { } function QuestionPreviewComponent({ question }: { - question: AnyQuizQuestion; + question: AnyTypedQuizQuestion | UntypedQuizQuestion; }) { + if (question.type === null) return null; + switch (question.type) { case "variant": return ; case "images": return ;