Merge branch 'backend-integration' into branching
This commit is contained in:
commit
a169dd0a49
@ -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) {
|
||||||
|
338
src/pages/Questions/branchingQuestions.tsx
Normal file
338
src/pages/Questions/branchingQuestions.tsx
Normal file
@ -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) {
|
||||||
|
25
src/stores/questions/hooks.ts
Normal file
25
src/stores/questions/hooks.ts
Normal file
@ -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} />;
|
||||||
|
Loading…
Reference in New Issue
Block a user