WIP
This commit is contained in:
parent
a57de5fe4d
commit
dd46a3833f
@ -1,6 +1,6 @@
|
|||||||
import { makeRequest } from "@frontend/kitui";
|
import { makeRequest } from "@frontend/kitui";
|
||||||
import { CreateQuestionRequest } from "model/question/create";
|
import { CreateQuestionRequest } from "model/question/create";
|
||||||
import { Question } from "model/question/question";
|
import { RawQuestion } from "model/question/question";
|
||||||
import { GetQuestionListRequest, GetQuestionListResponse } from "model/question/getList";
|
import { GetQuestionListRequest, GetQuestionListResponse } from "model/question/getList";
|
||||||
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";
|
||||||
@ -9,32 +9,27 @@ import { CopyQuestionRequest, CopyQuestionResponse } from "model/question/copy";
|
|||||||
|
|
||||||
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";
|
||||||
|
|
||||||
export function createQuestion(body: CreateQuestionRequest = defaultCreateQuestionBody) {
|
function createQuestion(body?: Partial<CreateQuestionRequest>) {
|
||||||
return makeRequest<CreateQuestionRequest, Question>({
|
return makeRequest<CreateQuestionRequest, RawQuestion>({
|
||||||
url: `${baseUrl}/question/create`,
|
url: `${baseUrl}/question/create`,
|
||||||
body,
|
body: { ...defaultCreateQuestionBody, ...body },
|
||||||
method: "POST",
|
method: "POST",
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function getQuestionList(body: GetQuestionListRequest = defaultGetQuestionListBody) {
|
async function getQuestionList(body?: Partial<GetQuestionListRequest>) {
|
||||||
return makeRequest<GetQuestionListRequest, GetQuestionListResponse>({
|
if (!body?.quiz_id) return null;
|
||||||
|
|
||||||
|
const response = await makeRequest<GetQuestionListRequest, GetQuestionListResponse>({
|
||||||
url: `${baseUrl}/question/getList`,
|
url: `${baseUrl}/question/getList`,
|
||||||
body,
|
body: { ...defaultGetQuestionListBody, ...body },
|
||||||
method: "GET",
|
method: "POST",
|
||||||
});
|
});
|
||||||
|
|
||||||
|
return response.items;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function editQuestion(updatedQuestion: Question, signal?: AbortSignal) {
|
function editQuestion(body: EditQuestionRequest, signal?: AbortSignal) {
|
||||||
const body: EditQuestionRequest = {
|
|
||||||
id: updatedQuestion.id,
|
|
||||||
title: updatedQuestion.title,
|
|
||||||
desc: updatedQuestion.description,
|
|
||||||
type: updatedQuestion.type,
|
|
||||||
required: updatedQuestion.required,
|
|
||||||
page: updatedQuestion.page,
|
|
||||||
};
|
|
||||||
|
|
||||||
return makeRequest<EditQuestionRequest, EditQuestionResponse>({
|
return makeRequest<EditQuestionRequest, EditQuestionResponse>({
|
||||||
url: `${baseUrl}/question/edit`,
|
url: `${baseUrl}/question/edit`,
|
||||||
body,
|
body,
|
||||||
@ -43,7 +38,7 @@ export function editQuestion(updatedQuestion: Question, signal?: AbortSignal) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function copyQuestion(copyQuestionBody: CopyQuestionRequest) {
|
function copyQuestion(copyQuestionBody: CopyQuestionRequest) {
|
||||||
return makeRequest<CopyQuestionRequest, CopyQuestionResponse>({
|
return makeRequest<CopyQuestionRequest, CopyQuestionResponse>({
|
||||||
url: `${baseUrl}/question/copy`,
|
url: `${baseUrl}/question/copy`,
|
||||||
body: copyQuestionBody,
|
body: copyQuestionBody,
|
||||||
@ -51,7 +46,7 @@ export function copyQuestion(copyQuestionBody: CopyQuestionRequest) {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
export function deleteQuestion(id: number) {
|
function deleteQuestion(id: number) {
|
||||||
return makeRequest<DeleteQuestionRequest, DeleteQuestionResponse>({
|
return makeRequest<DeleteQuestionRequest, DeleteQuestionResponse>({
|
||||||
url: `${baseUrl}/question/delete`,
|
url: `${baseUrl}/question/delete`,
|
||||||
body: { id },
|
body: { id },
|
||||||
@ -72,7 +67,7 @@ const defaultCreateQuestionBody: CreateQuestionRequest = {
|
|||||||
"quiz_id": 0,
|
"quiz_id": 0,
|
||||||
"title": "string",
|
"title": "string",
|
||||||
"description": "string",
|
"description": "string",
|
||||||
"type": "string",
|
"type": "variant",
|
||||||
"required": true,
|
"required": true,
|
||||||
"page": 0,
|
"page": 0,
|
||||||
"content": "string",
|
"content": "string",
|
||||||
@ -87,14 +82,4 @@ const defaultGetQuestionListBody: GetQuestionListRequest = {
|
|||||||
"type": "string",
|
"type": "string",
|
||||||
"deleted": true,
|
"deleted": true,
|
||||||
"required": true,
|
"required": true,
|
||||||
"quiz_id": 0
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultEditQuestionBody: EditQuestionRequest = {
|
|
||||||
"id": 0,
|
|
||||||
"title": "string",
|
|
||||||
"desc": "string",
|
|
||||||
"type": "",
|
|
||||||
"required": true,
|
|
||||||
"page": 0
|
|
||||||
};
|
};
|
||||||
|
|||||||
@ -38,7 +38,6 @@ function getQuiz(body?: Partial<GetQuizRequest>) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
async function editQuiz(body: EditQuizRequest, signal?: AbortSignal) {
|
async function editQuiz(body: EditQuizRequest, signal?: AbortSignal) {
|
||||||
// await new Promise((resolve) => setTimeout(resolve, 1000));
|
|
||||||
return makeRequest<EditQuizRequest, EditQuizResponse>({
|
return makeRequest<EditQuizRequest, EditQuizResponse>({
|
||||||
url: `${baseUrl}/quiz/edit`,
|
url: `${baseUrl}/quiz/edit`,
|
||||||
body,
|
body,
|
||||||
@ -106,26 +105,6 @@ const defaultCreateQuizBody: CreateQuizRequest = {
|
|||||||
"group_id": 0,
|
"group_id": 0,
|
||||||
};
|
};
|
||||||
|
|
||||||
const defaultEditQuizBody: EditQuizRequest = {
|
|
||||||
"id": 0,
|
|
||||||
"fp": true,
|
|
||||||
"rep": true,
|
|
||||||
"note_prevented": true,
|
|
||||||
"mailing": true,
|
|
||||||
"uniq": true,
|
|
||||||
"name": "string",
|
|
||||||
"desc": "string",
|
|
||||||
"conf": "string",
|
|
||||||
"status": "string",
|
|
||||||
"limit": 0,
|
|
||||||
"due_to": 0,
|
|
||||||
"time_of_passing": 0,
|
|
||||||
"pausable": true,
|
|
||||||
"question_cnt": 0,
|
|
||||||
"super": true,
|
|
||||||
"group_id": 0,
|
|
||||||
};
|
|
||||||
|
|
||||||
const defaultGetQuizBody: GetQuizRequest = {
|
const defaultGetQuizBody: GetQuizRequest = {
|
||||||
"quiz_id": "string",
|
"quiz_id": "string",
|
||||||
"limit": 0,
|
"limit": 0,
|
||||||
|
|||||||
@ -1,8 +1,11 @@
|
|||||||
import type { QuizQuestionInitial } from "../model/questionTypes/shared";
|
import type { QuizQuestionBase } from "../model/questionTypes/shared";
|
||||||
|
|
||||||
export const QUIZ_QUESTION_BASE: Omit<QuizQuestionInitial, "id"> = {
|
|
||||||
|
export const QUIZ_QUESTION_BASE: Omit<QuizQuestionBase, "id"> = {
|
||||||
|
quizId: 0,
|
||||||
|
description: "",
|
||||||
|
page: 0,
|
||||||
title: "",
|
title: "",
|
||||||
type: "nonselected",
|
|
||||||
expanded: true,
|
expanded: true,
|
||||||
openedModalSettings: false,
|
openedModalSettings: false,
|
||||||
required: false,
|
required: false,
|
||||||
|
|||||||
28
src/constants/default.ts
Normal file
28
src/constants/default.ts
Normal file
@ -0,0 +1,28 @@
|
|||||||
|
import { QuestionType } from "@model/question/question";
|
||||||
|
import { QUIZ_QUESTION_DATE } from "./date";
|
||||||
|
import { QUIZ_QUESTION_EMOJI } from "./emoji";
|
||||||
|
import { QUIZ_QUESTION_FILE } from "./file";
|
||||||
|
import { QUIZ_QUESTION_IMAGES } from "./images";
|
||||||
|
import { QUIZ_QUESTION_NUMBER } from "./number";
|
||||||
|
import { QUIZ_QUESTION_PAGE } from "./page";
|
||||||
|
import { QUIZ_QUESTION_RATING } from "./rating";
|
||||||
|
import { QUIZ_QUESTION_SELECT } from "./select";
|
||||||
|
import { QUIZ_QUESTION_TEXT } from "./text";
|
||||||
|
import { QUIZ_QUESTION_VARIANT } from "./variant";
|
||||||
|
import { QUIZ_QUESTION_VARIMG } from "./varimg";
|
||||||
|
import { AnyQuestionContent } from "@model/questionTypes/shared";
|
||||||
|
|
||||||
|
|
||||||
|
export const defaultQuestionContentByType: Record<QuestionType, AnyQuestionContent> = {
|
||||||
|
"date": QUIZ_QUESTION_DATE.content,
|
||||||
|
"emoji": QUIZ_QUESTION_EMOJI.content,
|
||||||
|
"file": QUIZ_QUESTION_FILE.content,
|
||||||
|
"images": QUIZ_QUESTION_IMAGES.content,
|
||||||
|
"number": QUIZ_QUESTION_NUMBER.content,
|
||||||
|
"page": QUIZ_QUESTION_PAGE.content,
|
||||||
|
"rating": QUIZ_QUESTION_RATING.content,
|
||||||
|
"select": QUIZ_QUESTION_SELECT.content,
|
||||||
|
"text": QUIZ_QUESTION_TEXT.content,
|
||||||
|
"variant": QUIZ_QUESTION_VARIANT.content,
|
||||||
|
"varimg": QUIZ_QUESTION_VARIMG.content,
|
||||||
|
} as const;
|
||||||
@ -1,12 +1,26 @@
|
|||||||
|
import { AnyQuizQuestion, DefiniteQuestionType } from "@model/questionTypes/shared";
|
||||||
|
|
||||||
|
|
||||||
export interface EditQuestionRequest {
|
export interface EditQuestionRequest {
|
||||||
id: number;
|
id: number;
|
||||||
title: string;
|
title?: string;
|
||||||
desc: string;
|
desc?: string;
|
||||||
type: "test" | "button" | "file" | "checkbox" | "select" | "none" | "";
|
type?: DefiniteQuestionType;
|
||||||
required: boolean;
|
required?: boolean;
|
||||||
page: number;
|
page?: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface EditQuestionResponse {
|
export interface EditQuestionResponse {
|
||||||
updated: number;
|
updated: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function questionToEditQuestionRequest(question: AnyQuizQuestion): EditQuestionRequest {
|
||||||
|
return {
|
||||||
|
id: question.id,
|
||||||
|
title: question.title,
|
||||||
|
desc: question.description,
|
||||||
|
type: question.type,
|
||||||
|
required: question.required,
|
||||||
|
page: question.page,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|||||||
@ -1,4 +1,4 @@
|
|||||||
import { Question } from "./question";
|
import { RawQuestion } from "./question";
|
||||||
|
|
||||||
|
|
||||||
export interface GetQuestionListRequest {
|
export interface GetQuestionListRequest {
|
||||||
@ -24,5 +24,5 @@ export interface GetQuestionListRequest {
|
|||||||
|
|
||||||
export interface GetQuestionListResponse {
|
export interface GetQuestionListResponse {
|
||||||
count: number;
|
count: number;
|
||||||
items: Question[];
|
items: RawQuestion[];
|
||||||
}
|
}
|
||||||
|
|||||||
@ -1,15 +1,69 @@
|
|||||||
export interface Question {
|
import { AnyQuizQuestion } from "@model/questionTypes/shared";
|
||||||
|
import { defaultQuestionContentByType } from "../../constants/default";
|
||||||
|
|
||||||
|
|
||||||
|
export type QuestionType =
|
||||||
|
| "variant"
|
||||||
|
| "images"
|
||||||
|
| "varimg"
|
||||||
|
| "emoji"
|
||||||
|
| "text"
|
||||||
|
| "select"
|
||||||
|
| "date"
|
||||||
|
| "number"
|
||||||
|
| "file"
|
||||||
|
| "page"
|
||||||
|
| "rating";
|
||||||
|
|
||||||
|
/** Type that comes from server */
|
||||||
|
export interface RawQuestion {
|
||||||
|
/** Id of created question */
|
||||||
id: number;
|
id: number;
|
||||||
|
/** relation to quiz */
|
||||||
quiz_id: number;
|
quiz_id: number;
|
||||||
|
/** title of question. max 512 length */
|
||||||
title: string;
|
title: string;
|
||||||
|
/** description of question */
|
||||||
description: string;
|
description: string;
|
||||||
type: "test" | "button" | "file" | "checkbox" | "select" | "none" | "";
|
/** status of question. allow only text, select, file, variant, images, varimg, emoji, date, number, page, rating */
|
||||||
|
type: QuestionType;
|
||||||
|
/** user must pass this question */
|
||||||
required: boolean;
|
required: boolean;
|
||||||
|
/** true if question is deleted */
|
||||||
deleted: boolean;
|
deleted: boolean;
|
||||||
|
/** page if question */
|
||||||
page: number;
|
page: number;
|
||||||
|
/** serialized json of created question */
|
||||||
content: string;
|
content: string;
|
||||||
|
/** version of quiz */
|
||||||
version: number;
|
version: number;
|
||||||
|
/** array of previous versions of quiz */
|
||||||
parent_ids: number[];
|
parent_ids: number[];
|
||||||
created_at: string;
|
created_at: string;
|
||||||
updated_at: string;
|
updated_at: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function rawQuestionToQuestion(rawQuestion: RawQuestion): AnyQuizQuestion {
|
||||||
|
let content = defaultQuestionContentByType[rawQuestion.type];
|
||||||
|
|
||||||
|
try {
|
||||||
|
content = JSON.parse(rawQuestion.content);
|
||||||
|
} catch (error) {
|
||||||
|
console.warn("Cannot parse question content from string, using default content", error);
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
id: rawQuestion.id,
|
||||||
|
description: rawQuestion.description,
|
||||||
|
page: rawQuestion.page,
|
||||||
|
quizId: rawQuestion.quiz_id,
|
||||||
|
required: rawQuestion.required,
|
||||||
|
title: rawQuestion.title,
|
||||||
|
type: rawQuestion.type,
|
||||||
|
expanded: true,
|
||||||
|
openedModalSettings: false,
|
||||||
|
deleted: false,
|
||||||
|
deleteTimeoutId: 0,
|
||||||
|
content,
|
||||||
|
} as AnyQuizQuestion;
|
||||||
|
}
|
||||||
|
|||||||
@ -1,3 +1,4 @@
|
|||||||
|
import { QuestionType } from "@model/question/question";
|
||||||
import type { QuizQuestionDate } from "./date";
|
import type { QuizQuestionDate } from "./date";
|
||||||
import type { QuizQuestionEmoji } from "./emoji";
|
import type { QuizQuestionEmoji } from "./emoji";
|
||||||
import type { QuizQuestionFile } from "./file";
|
import type { QuizQuestionFile } from "./file";
|
||||||
@ -10,6 +11,7 @@ import type { QuizQuestionText } from "./text";
|
|||||||
import type { QuizQuestionVariant } from "./variant";
|
import type { QuizQuestionVariant } from "./variant";
|
||||||
import type { QuizQuestionVarImg } from "./varimg";
|
import type { QuizQuestionVarImg } from "./varimg";
|
||||||
|
|
||||||
|
|
||||||
export interface QuestionBranchingRule {
|
export interface QuestionBranchingRule {
|
||||||
/** Радиокнопка "Все условия обязательны" */
|
/** Радиокнопка "Все условия обязательны" */
|
||||||
or: boolean;
|
or: boolean;
|
||||||
@ -45,8 +47,11 @@ export interface ImageQuestionVariant extends QuestionVariant {
|
|||||||
|
|
||||||
export interface QuizQuestionBase {
|
export interface QuizQuestionBase {
|
||||||
id: number;
|
id: number;
|
||||||
|
quizId: number;
|
||||||
title: string;
|
title: string;
|
||||||
type: string;
|
description: string;
|
||||||
|
page: number;
|
||||||
|
type?: QuestionType;
|
||||||
expanded: boolean;
|
expanded: boolean;
|
||||||
openedModalSettings: boolean;
|
openedModalSettings: boolean;
|
||||||
required: boolean;
|
required: boolean;
|
||||||
@ -61,9 +66,9 @@ export interface QuizQuestionBase {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface QuizQuestionInitial extends QuizQuestionBase {
|
// export interface QuizQuestionInitial extends QuizQuestionBase {
|
||||||
type: "nonselected";
|
// type: "nonselected";
|
||||||
}
|
// }
|
||||||
|
|
||||||
export type AnyQuizQuestion =
|
export type AnyQuizQuestion =
|
||||||
| QuizQuestionVariant
|
| QuizQuestionVariant
|
||||||
@ -76,9 +81,11 @@ export type AnyQuizQuestion =
|
|||||||
| QuizQuestionNumber
|
| QuizQuestionNumber
|
||||||
| QuizQuestionFile
|
| QuizQuestionFile
|
||||||
| QuizQuestionPage
|
| QuizQuestionPage
|
||||||
| QuizQuestionRating
|
| QuizQuestionRating;
|
||||||
| QuizQuestionInitial;
|
// | QuizQuestionInitial;
|
||||||
|
|
||||||
export type QuizQuestionType = AnyQuizQuestion["type"];
|
export type QuizQuestionType = AnyQuizQuestion["type"];
|
||||||
|
|
||||||
|
export type AnyQuestionContent = AnyQuizQuestion["content"];
|
||||||
|
|
||||||
export type DefiniteQuestionType = Exclude<QuizQuestionType, "nonselected">;
|
export type DefiniteQuestionType = Exclude<QuizQuestionType, "nonselected">;
|
||||||
|
|||||||
@ -1,5 +1,4 @@
|
|||||||
import { useState } from "react";
|
import { useState } from "react";
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Typography,
|
Typography,
|
||||||
@ -15,7 +14,6 @@ import {
|
|||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
questionStore,
|
|
||||||
updateQuestionsList,
|
updateQuestionsList,
|
||||||
removeQuestionForce,
|
removeQuestionForce,
|
||||||
createQuestion,
|
createQuestion,
|
||||||
@ -26,13 +24,14 @@ import type { RefObject } from "react";
|
|||||||
import type {
|
import type {
|
||||||
QuizQuestionType,
|
QuizQuestionType,
|
||||||
QuizQuestionBase,
|
QuizQuestionBase,
|
||||||
|
AnyQuizQuestion,
|
||||||
} from "../../../model/questionTypes/shared";
|
} from "../../../model/questionTypes/shared";
|
||||||
|
|
||||||
type ChooseAnswerModalProps = {
|
type ChooseAnswerModalProps = {
|
||||||
open: boolean;
|
open: boolean;
|
||||||
onClose: () => void;
|
onClose: () => void;
|
||||||
anchorRef: RefObject<HTMLDivElement>;
|
anchorRef: RefObject<HTMLDivElement>;
|
||||||
totalIndex: number;
|
question: AnyQuizQuestion;
|
||||||
switchState: string;
|
switchState: string;
|
||||||
};
|
};
|
||||||
|
|
||||||
@ -40,13 +39,11 @@ export const ChooseAnswerModal = ({
|
|||||||
open,
|
open,
|
||||||
onClose,
|
onClose,
|
||||||
anchorRef,
|
anchorRef,
|
||||||
totalIndex,
|
question,
|
||||||
switchState,
|
switchState,
|
||||||
}: ChooseAnswerModalProps) => {
|
}: ChooseAnswerModalProps) => {
|
||||||
const [openModal, setOpenModal] = useState<boolean>(false);
|
const [openModal, setOpenModal] = useState<boolean>(false);
|
||||||
const [selectedValue, setSelectedValue] = useState<QuizQuestionType>("text");
|
const [selectedValue, setSelectedValue] = useState<QuizQuestionType>("text");
|
||||||
const quizId = Number(useParams().quizId);
|
|
||||||
const { listQuestions } = questionStore();
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -126,17 +123,17 @@ export const ChooseAnswerModal = ({
|
|||||||
<Button
|
<Button
|
||||||
variant="contained"
|
variant="contained"
|
||||||
sx={{ minWidth: "150px" }}
|
sx={{ minWidth: "150px" }}
|
||||||
onClick={() => {
|
onClick={() => { // TODO
|
||||||
setOpenModal(false);
|
// setOpenModal(false);
|
||||||
|
|
||||||
const question = { ...listQuestions[quizId][totalIndex] };
|
// const question = { ...listQuestions[quizId][totalIndex] };
|
||||||
|
|
||||||
removeQuestionForce(quizId, question.id);
|
// removeQuestionForce(quizId, question.id);
|
||||||
createQuestion(quizId, selectedValue, totalIndex);
|
// createQuestion(quizId, selectedValue, totalIndex);
|
||||||
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
// updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
||||||
title: question.title,
|
// title: question.title,
|
||||||
expanded: question.expanded,
|
// expanded: question.expanded,
|
||||||
});
|
// });
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
Подтвердить
|
Подтвердить
|
||||||
|
|||||||
@ -1,34 +1,28 @@
|
|||||||
import { memo } from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { Draggable } from "react-beautiful-dnd";
|
|
||||||
import { Box, ListItem, Typography, useTheme } from "@mui/material";
|
import { Box, ListItem, Typography, useTheme } from "@mui/material";
|
||||||
|
import { memo } from "react";
|
||||||
|
import { Draggable } from "react-beautiful-dnd";
|
||||||
import QuestionsPageCard from "./QuestionPageCard";
|
import QuestionsPageCard from "./QuestionPageCard";
|
||||||
|
import { AnyQuizQuestion } from "@model/questionTypes/shared";
|
||||||
|
|
||||||
import { updateQuestionsList } from "@root/questions";
|
|
||||||
|
|
||||||
import { QuizQuestionBase } from "../../../model/questionTypes/shared";
|
type Props = {
|
||||||
|
question: AnyQuizQuestion;
|
||||||
type DraggableListItemProps = {
|
|
||||||
index: number;
|
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
questionData: QuizQuestionBase;
|
index: number;
|
||||||
};
|
};
|
||||||
|
|
||||||
export default memo(
|
function DraggableListItem({ question, isDragging, index }: Props) {
|
||||||
({ index, isDragging, questionData }: DraggableListItemProps) => {
|
|
||||||
const quizId = Number(useParams().quizId);
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Draggable draggableId={String(index)} index={index}>
|
<Draggable draggableId={question.id.toString()} index={index}>
|
||||||
{(provided) => (
|
{(provided) => (
|
||||||
<ListItem
|
<ListItem
|
||||||
ref={provided.innerRef}
|
ref={provided.innerRef}
|
||||||
{...provided.draggableProps}
|
{...provided.draggableProps}
|
||||||
sx={{ userSelect: "none", padding: 0 }}
|
sx={{ userSelect: "none", padding: 0 }}
|
||||||
>
|
>
|
||||||
{questionData.deleted ? (
|
{/* questionData.deleted TODO */ true ? (
|
||||||
<Box
|
<Box
|
||||||
{...provided.dragHandleProps}
|
{...provided.dragHandleProps}
|
||||||
sx={{
|
sx={{
|
||||||
@ -49,11 +43,11 @@ export default memo(
|
|||||||
Вопрос удалён.
|
Вопрос удалён.
|
||||||
</Typography>
|
</Typography>
|
||||||
<Typography
|
<Typography
|
||||||
onClick={() => {
|
onClick={() => { // TODO
|
||||||
updateQuestionsList<QuizQuestionBase>(quizId, index, {
|
// updateQuestionsList<QuizQuestionBase>(quizId, index, {
|
||||||
...questionData,
|
// ...questionData,
|
||||||
deleted: false,
|
// deleted: false,
|
||||||
});
|
// });
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
cursor: "pointer",
|
cursor: "pointer",
|
||||||
@ -69,8 +63,7 @@ export default memo(
|
|||||||
) : (
|
) : (
|
||||||
<Box sx={{ width: "100%", position: "relative" }}>
|
<Box sx={{ width: "100%", position: "relative" }}>
|
||||||
<QuestionsPageCard
|
<QuestionsPageCard
|
||||||
key={index}
|
question={question}
|
||||||
totalIndex={index}
|
|
||||||
draggableProps={provided.dragHandleProps}
|
draggableProps={provided.dragHandleProps}
|
||||||
isDragging={isDragging}
|
isDragging={isDragging}
|
||||||
/>
|
/>
|
||||||
@ -81,4 +74,7 @@ export default memo(
|
|||||||
</Draggable>
|
</Draggable>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
);
|
|
||||||
|
const DraggableListItemMemo = memo(DraggableListItem);
|
||||||
|
|
||||||
|
export default DraggableListItemMemo;
|
||||||
|
|||||||
@ -1,5 +1,3 @@
|
|||||||
import { useState, useRef, useEffect } from "react";
|
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import {
|
import {
|
||||||
Box,
|
Box,
|
||||||
Checkbox,
|
Checkbox,
|
||||||
@ -12,165 +10,67 @@ import {
|
|||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
|
import { useRef, useState } from "react";
|
||||||
|
import { useParams } from "react-router-dom";
|
||||||
import { useDebouncedCallback } from "use-debounce";
|
import { useDebouncedCallback } from "use-debounce";
|
||||||
|
|
||||||
import { ChooseAnswerModal } from "./ChooseAnswerModal";
|
|
||||||
import TypeQuestions from "../TypeQuestions";
|
|
||||||
import SwitchQuestionsPage from "../SwitchQuestionsPage";
|
import SwitchQuestionsPage from "../SwitchQuestionsPage";
|
||||||
|
import TypeQuestions from "../TypeQuestions";
|
||||||
|
import { ChooseAnswerModal } from "./ChooseAnswerModal";
|
||||||
|
|
||||||
import {
|
import {
|
||||||
questionStore,
|
|
||||||
updateQuestionsList,
|
|
||||||
createQuestion,
|
|
||||||
copyQuestion,
|
copyQuestion,
|
||||||
|
createQuestion,
|
||||||
removeQuestion,
|
removeQuestion,
|
||||||
removeQuestionForce,
|
removeQuestionForce,
|
||||||
|
updateQuestionsList
|
||||||
} from "@root/questions";
|
} from "@root/questions";
|
||||||
|
|
||||||
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
import { CrossedEyeIcon } from "@icons/CrossedEyeIcon";
|
||||||
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
import { ArrowDownIcon } from "@icons/questionsPage/ArrowDownIcon";
|
||||||
|
import { CopyIcon } from "@icons/questionsPage/CopyIcon";
|
||||||
import { OneIcon } from "@icons/questionsPage/OneIcon";
|
import { OneIcon } from "@icons/questionsPage/OneIcon";
|
||||||
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
|
import { PointsIcon } from "@icons/questionsPage/PointsIcon";
|
||||||
import { CopyIcon } from "@icons/questionsPage/CopyIcon";
|
|
||||||
import { CrossedEyeIcon } from "@icons/CrossedEyeIcon";
|
|
||||||
import { HideIcon } from "@icons/questionsPage/hideIcon";
|
|
||||||
import Answer from "@icons/questionsPage/answer";
|
import Answer from "@icons/questionsPage/answer";
|
||||||
import OptionsPict from "@icons/questionsPage/options_pict";
|
|
||||||
import OptionsAndPict from "@icons/questionsPage/options_and_pict";
|
|
||||||
import Emoji from "@icons/questionsPage/emoji";
|
|
||||||
import Input from "@icons/questionsPage/input";
|
|
||||||
import DropDown from "@icons/questionsPage/drop_down";
|
|
||||||
import Date from "@icons/questionsPage/date";
|
import Date from "@icons/questionsPage/date";
|
||||||
import Slider from "@icons/questionsPage/slider";
|
import { DeleteIcon } from "@icons/questionsPage/deleteIcon";
|
||||||
import Download from "@icons/questionsPage/download";
|
import Download from "@icons/questionsPage/download";
|
||||||
|
import DropDown from "@icons/questionsPage/drop_down";
|
||||||
|
import Emoji from "@icons/questionsPage/emoji";
|
||||||
|
import { HideIcon } from "@icons/questionsPage/hideIcon";
|
||||||
|
import Input from "@icons/questionsPage/input";
|
||||||
|
import OptionsAndPict from "@icons/questionsPage/options_and_pict";
|
||||||
|
import OptionsPict from "@icons/questionsPage/options_pict";
|
||||||
import Page from "@icons/questionsPage/page";
|
import Page from "@icons/questionsPage/page";
|
||||||
import RatingIcon from "@icons/questionsPage/rating";
|
import RatingIcon from "@icons/questionsPage/rating";
|
||||||
import { ArrowDownIcon } from "@icons/questionsPage/ArrowDownIcon";
|
import Slider from "@icons/questionsPage/slider";
|
||||||
import { ReactComponent as PlusIcon } from "../../../assets/icons/plus.svg";
|
import ExpandLessIcon from "@mui/icons-material/ExpandLess";
|
||||||
|
|
||||||
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
|
import type { DraggableProvidedDragHandleProps } from "react-beautiful-dnd";
|
||||||
import type {
|
import { ReactComponent as PlusIcon } from "../../../assets/icons/plus.svg";
|
||||||
AnyQuizQuestion,
|
import type { AnyQuizQuestion } from "../../../model/questionTypes/shared";
|
||||||
QuizQuestionInitial,
|
|
||||||
} from "../../../model/questionTypes/shared";
|
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
totalIndex: number;
|
question: AnyQuizQuestion;
|
||||||
draggableProps: DraggableProvidedDragHandleProps | null | undefined;
|
draggableProps: DraggableProvidedDragHandleProps | null | undefined;
|
||||||
isDragging: boolean;
|
isDragging: boolean;
|
||||||
}
|
}
|
||||||
|
|
||||||
const IconAndrom = (isExpanded: boolean, switchState: string) => {
|
export default function QuestionsPageCard({ question, draggableProps, isDragging }: Props) {
|
||||||
switch (switchState) {
|
|
||||||
case "variant":
|
|
||||||
return (
|
|
||||||
<Answer
|
|
||||||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
|
||||||
sx={{ height: "22px", width: "20px" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "images":
|
|
||||||
return (
|
|
||||||
<OptionsPict
|
|
||||||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
|
||||||
sx={{ height: "22px", width: "20px" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "varimg":
|
|
||||||
return (
|
|
||||||
<OptionsAndPict
|
|
||||||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
|
||||||
sx={{ height: "22px", width: "20px" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "emoji":
|
|
||||||
return (
|
|
||||||
<Emoji
|
|
||||||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
|
||||||
sx={{ height: "22px", width: "20px" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "text":
|
|
||||||
return (
|
|
||||||
<Input
|
|
||||||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
|
||||||
sx={{ height: "22px", width: "20px" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "select":
|
|
||||||
return (
|
|
||||||
<DropDown
|
|
||||||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
|
||||||
sx={{ height: "22px", width: "20px" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "date":
|
|
||||||
return (
|
|
||||||
<Date
|
|
||||||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
|
||||||
sx={{ height: "22px", width: "20px" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "number":
|
|
||||||
return (
|
|
||||||
<Slider
|
|
||||||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
|
||||||
sx={{ height: "22px", width: "20px" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "file":
|
|
||||||
return (
|
|
||||||
<Download
|
|
||||||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
|
||||||
sx={{ height: "22px", width: "20px" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "page":
|
|
||||||
return (
|
|
||||||
<Page
|
|
||||||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
|
||||||
sx={{ height: "22px", width: "20px" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
case "rating":
|
|
||||||
return (
|
|
||||||
<RatingIcon
|
|
||||||
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
|
||||||
sx={{ height: "22px", width: "20px" }}
|
|
||||||
/>
|
|
||||||
);
|
|
||||||
default:
|
|
||||||
return <></>;
|
|
||||||
}
|
|
||||||
};
|
|
||||||
export default function QuestionsPageCard({
|
|
||||||
totalIndex,
|
|
||||||
draggableProps,
|
|
||||||
isDragging,
|
|
||||||
}: Props) {
|
|
||||||
const [plusVisible, setPlusVisible] = useState<boolean>(false);
|
const [plusVisible, setPlusVisible] = useState<boolean>(false);
|
||||||
const [open, setOpen] = useState<boolean>(false);
|
const [open, setOpen] = useState<boolean>(false);
|
||||||
const quizId = Number(useParams().quizId);
|
const quizId = Number(useParams().quizId);
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
const isTablet = useMediaQuery(theme.breakpoints.down(1000));
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
const isMobile = useMediaQuery(theme.breakpoints.down(790));
|
||||||
const { listQuestions } = questionStore();
|
|
||||||
const question = listQuestions[quizId][totalIndex];
|
|
||||||
const anchorRef = useRef(null);
|
const anchorRef = useRef(null);
|
||||||
const debounced = useDebouncedCallback((title) => {
|
const debounced = useDebouncedCallback((title) => { // TODO update title
|
||||||
updateQuestionsList<QuizQuestionInitial>(quizId, totalIndex, { title });
|
// updateQuestionsList<QuizQuestionInitial>(quizId, totalIndex, { title });
|
||||||
}, 200);
|
}, 200);
|
||||||
|
|
||||||
useEffect(() => {
|
|
||||||
if (question.deleteTimeoutId) {
|
|
||||||
clearTimeout(question.deleteTimeoutId);
|
|
||||||
}
|
|
||||||
}, [question]);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<Paper
|
<Paper
|
||||||
id={String(totalIndex)}
|
|
||||||
data-cy="quiz-question-card"
|
data-cy="quiz-question-card"
|
||||||
sx={{
|
sx={{
|
||||||
maxWidth: "796px",
|
maxWidth: "796px",
|
||||||
@ -217,7 +117,7 @@ export default function QuestionsPageCard({
|
|||||||
open={open}
|
open={open}
|
||||||
onClose={() => setOpen(false)}
|
onClose={() => setOpen(false)}
|
||||||
anchorRef={anchorRef}
|
anchorRef={anchorRef}
|
||||||
totalIndex={totalIndex}
|
question={question}
|
||||||
switchState={question.type}
|
switchState={question.type}
|
||||||
/>
|
/>
|
||||||
</Box>
|
</Box>
|
||||||
@ -402,9 +302,9 @@ export default function QuestionsPageCard({
|
|||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{question.type === "nonselected" ? (
|
{question.type === "nonselected" ? (
|
||||||
<TypeQuestions totalIndex={totalIndex} />
|
<TypeQuestions question={question} />
|
||||||
) : (
|
) : (
|
||||||
<SwitchQuestionsPage totalIndex={totalIndex} />
|
<SwitchQuestionsPage question={question} />
|
||||||
)}
|
)}
|
||||||
</Box>
|
</Box>
|
||||||
)}
|
)}
|
||||||
@ -447,3 +347,87 @@ export default function QuestionsPageCard({
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
const IconAndrom = (isExpanded: boolean, switchState: string) => {
|
||||||
|
switch (switchState) {
|
||||||
|
case "variant":
|
||||||
|
return (
|
||||||
|
<Answer
|
||||||
|
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||||||
|
sx={{ height: "22px", width: "20px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "images":
|
||||||
|
return (
|
||||||
|
<OptionsPict
|
||||||
|
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||||||
|
sx={{ height: "22px", width: "20px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "varimg":
|
||||||
|
return (
|
||||||
|
<OptionsAndPict
|
||||||
|
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||||||
|
sx={{ height: "22px", width: "20px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "emoji":
|
||||||
|
return (
|
||||||
|
<Emoji
|
||||||
|
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||||||
|
sx={{ height: "22px", width: "20px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "text":
|
||||||
|
return (
|
||||||
|
<Input
|
||||||
|
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||||||
|
sx={{ height: "22px", width: "20px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "select":
|
||||||
|
return (
|
||||||
|
<DropDown
|
||||||
|
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||||||
|
sx={{ height: "22px", width: "20px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "date":
|
||||||
|
return (
|
||||||
|
<Date
|
||||||
|
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||||||
|
sx={{ height: "22px", width: "20px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "number":
|
||||||
|
return (
|
||||||
|
<Slider
|
||||||
|
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||||||
|
sx={{ height: "22px", width: "20px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "file":
|
||||||
|
return (
|
||||||
|
<Download
|
||||||
|
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||||||
|
sx={{ height: "22px", width: "20px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "page":
|
||||||
|
return (
|
||||||
|
<Page
|
||||||
|
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||||||
|
sx={{ height: "22px", width: "20px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
case "rating":
|
||||||
|
return (
|
||||||
|
<RatingIcon
|
||||||
|
color={isExpanded ? "#9A9AAF" : "#7E2AEA"}
|
||||||
|
sx={{ height: "22px", width: "20px" }}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
default:
|
||||||
|
return <></>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|||||||
@ -1,29 +1,40 @@
|
|||||||
import { useParams } from "react-router-dom";
|
|
||||||
import { Box } from "@mui/material";
|
import { Box } from "@mui/material";
|
||||||
import { DragDropContext, Droppable } from "react-beautiful-dnd";
|
import { DragDropContext, Droppable } from "react-beautiful-dnd";
|
||||||
|
|
||||||
import DraggableListItem from "./DraggableListItem";
|
import DraggableListItem from "./DraggableListItem";
|
||||||
|
|
||||||
import { questionStore, updateQuestionsListDragAndDrop } from "@root/questions";
|
|
||||||
|
|
||||||
import { reorder } from "./helper";
|
|
||||||
|
|
||||||
import type { DropResult } from "react-beautiful-dnd";
|
import type { DropResult } from "react-beautiful-dnd";
|
||||||
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
|
import { useQuestionArray } from "@root/questions/hooks";
|
||||||
|
import useSWR from "swr";
|
||||||
|
import { questionApi } from "@api/question";
|
||||||
|
import { setQuestions } from "@root/questions/actions";
|
||||||
|
import { isAxiosError } from "axios";
|
||||||
|
import { devlog } from "@frontend/kitui";
|
||||||
|
import { enqueueSnackbar } from "notistack";
|
||||||
|
|
||||||
|
|
||||||
export const DraggableList = () => {
|
export const DraggableList = () => {
|
||||||
const quizId = Number(useParams().quizId);
|
const { quiz } = useCurrentQuiz();
|
||||||
const { listQuestions } = questionStore();
|
useSWR(["questions", quiz?.id], ([, id]) => questionApi.getList({ quiz_id: id }), {
|
||||||
|
onSuccess: setQuestions,
|
||||||
|
onError: error => {
|
||||||
|
const message = isAxiosError<string>(error) ? (error.response?.data ?? "") : "";
|
||||||
|
|
||||||
const onDragEnd = ({ destination, source }: DropResult) => {
|
devlog("Error getting question list", error);
|
||||||
if (destination) {
|
enqueueSnackbar(`Не удалось получить вопросы. ${message}`);
|
||||||
const newItems = reorder(
|
|
||||||
listQuestions[quizId],
|
|
||||||
source.index,
|
|
||||||
destination.index
|
|
||||||
);
|
|
||||||
|
|
||||||
updateQuestionsListDragAndDrop(quizId, newItems);
|
|
||||||
}
|
}
|
||||||
|
});
|
||||||
|
const questions = useQuestionArray();
|
||||||
|
|
||||||
|
const onDragEnd = ({ destination, source }: DropResult) => { // TODO
|
||||||
|
// if (destination) {
|
||||||
|
// const newItems = reorder(
|
||||||
|
// listQuestions[quizId],
|
||||||
|
// source.index,
|
||||||
|
// destination.index
|
||||||
|
// );
|
||||||
|
|
||||||
|
// updateQuestionsListDragAndDrop(quizId, newItems);
|
||||||
|
// }
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@ -31,12 +42,12 @@ export const DraggableList = () => {
|
|||||||
<Droppable droppableId="droppable-list">
|
<Droppable droppableId="droppable-list">
|
||||||
{(provided, snapshot) => (
|
{(provided, snapshot) => (
|
||||||
<Box ref={provided.innerRef} {...provided.droppableProps}>
|
<Box ref={provided.innerRef} {...provided.droppableProps}>
|
||||||
{listQuestions[quizId]?.map((_, index) => (
|
{questions.map((question, index) => (
|
||||||
<DraggableListItem
|
<DraggableListItem
|
||||||
key={index}
|
key={question.id}
|
||||||
index={index}
|
question={question}
|
||||||
isDragging={snapshot.isDraggingOver}
|
isDragging={snapshot.isDraggingOver}
|
||||||
questionData={_}
|
index={index}
|
||||||
/>
|
/>
|
||||||
))}
|
))}
|
||||||
{provided.placeholder}
|
{provided.placeholder}
|
||||||
|
|||||||
@ -6,45 +6,31 @@ import {
|
|||||||
useMediaQuery,
|
useMediaQuery,
|
||||||
useTheme,
|
useTheme,
|
||||||
} from "@mui/material";
|
} from "@mui/material";
|
||||||
import AddPlus from "../../assets/icons/questionsPage/addPlus";
|
import { createQuestion } from "@root/questions/actions";
|
||||||
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
|
import { incrementCurrentStep } from "@root/quizes/actions";
|
||||||
import { quizStore } from "@root/quizes";
|
import { useCurrentQuiz } from "@root/quizes/hooks";
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
import {
|
|
||||||
questionStore,
|
|
||||||
createQuestion,
|
|
||||||
updateQuestionsList,
|
|
||||||
} from "@root/questions";
|
|
||||||
import { DraggableList } from "./DraggableList";
|
|
||||||
|
|
||||||
import type { AnyQuizQuestion } from "../../model/questionTypes/shared";
|
|
||||||
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 AddPlus from "../../assets/icons/questionsPage/addPlus";
|
||||||
|
import ArrowLeft from "../../assets/icons/questionsPage/arrowLeft";
|
||||||
|
import { DraggableList } from "./DraggableList";
|
||||||
|
|
||||||
|
|
||||||
export default function QuestionsPage() {
|
export default function QuestionsPage() {
|
||||||
const { listQuizes, updateQuizesList } = quizStore();
|
|
||||||
const quizId = Number(useParams().quizId);
|
|
||||||
const { listQuestions } = questionStore();
|
|
||||||
const handleNext = () => {
|
|
||||||
updateQuizesList(quizId, { step: listQuizes[quizId].step + 1 });
|
|
||||||
};
|
|
||||||
|
|
||||||
const handleBack = () => {
|
|
||||||
let result = listQuizes[quizId].step - 1;
|
|
||||||
updateQuizesList(quizId, { step: result ? result : 1 });
|
|
||||||
};
|
|
||||||
|
|
||||||
const collapseEverything = () => {
|
|
||||||
listQuestions[quizId].forEach((item, index) => {
|
|
||||||
updateQuestionsList<AnyQuizQuestion>(quizId, index, {
|
|
||||||
...item,
|
|
||||||
expanded: false,
|
|
||||||
});
|
|
||||||
});
|
|
||||||
};
|
|
||||||
|
|
||||||
const theme = useTheme();
|
const theme = useTheme();
|
||||||
const isMobile = useMediaQuery(theme.breakpoints.down(660));
|
const isMobile = useMediaQuery(theme.breakpoints.down(660));
|
||||||
|
const { quiz } = useCurrentQuiz();
|
||||||
|
|
||||||
|
const collapseEverything = () => { // TODO
|
||||||
|
// listQuestions[quizId].forEach((item, index) => {
|
||||||
|
// updateQuestionsList<AnyQuizQuestion>(quizId, index, {
|
||||||
|
// ...item,
|
||||||
|
// expanded: false,
|
||||||
|
// });
|
||||||
|
// });
|
||||||
|
};
|
||||||
|
|
||||||
|
if (!quiz) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@ -82,7 +68,7 @@ export default function QuestionsPage() {
|
|||||||
>
|
>
|
||||||
<IconButton
|
<IconButton
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
createQuestion(quizId);
|
createQuestion(quiz.id);
|
||||||
}}
|
}}
|
||||||
sx={{
|
sx={{
|
||||||
position: "fixed",
|
position: "fixed",
|
||||||
@ -108,7 +94,7 @@ export default function QuestionsPage() {
|
|||||||
background: theme.palette.brightPurple.main,
|
background: theme.palette.brightPurple.main,
|
||||||
fontSize: "18px",
|
fontSize: "18px",
|
||||||
}}
|
}}
|
||||||
onClick={handleNext}
|
onClick={incrementCurrentStep}
|
||||||
>
|
>
|
||||||
Следующий шаг
|
Следующий шаг
|
||||||
</Button>
|
</Button>
|
||||||
|
|||||||
@ -1,33 +1,24 @@
|
|||||||
|
import { Box } from "@mui/material";
|
||||||
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 OptionsPict from "../../assets/icons/questionsPage/options_pict";
|
import Date from "../../assets/icons/questionsPage/date";
|
||||||
import OptionsAndPict from "../../assets/icons/questionsPage/options_and_pict";
|
import Download from "../../assets/icons/questionsPage/download";
|
||||||
|
import DropDown from "../../assets/icons/questionsPage/drop_down";
|
||||||
import Emoji from "../../assets/icons/questionsPage/emoji";
|
import Emoji from "../../assets/icons/questionsPage/emoji";
|
||||||
import Input from "../../assets/icons/questionsPage/input";
|
import Input from "../../assets/icons/questionsPage/input";
|
||||||
import DropDown from "../../assets/icons/questionsPage/drop_down";
|
import OptionsAndPict from "../../assets/icons/questionsPage/options_and_pict";
|
||||||
import Date from "../../assets/icons/questionsPage/date";
|
import OptionsPict from "../../assets/icons/questionsPage/options_pict";
|
||||||
import Slider from "../../assets/icons/questionsPage/slider";
|
|
||||||
import Download from "../../assets/icons/questionsPage/download";
|
|
||||||
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 { Box } from "@mui/material";
|
import Slider from "../../assets/icons/questionsPage/slider";
|
||||||
import React from "react";
|
import { setQuestionFieldOptimistic } from "@root/questions/actions";
|
||||||
import { useParams } from "react-router-dom";
|
|
||||||
|
|
||||||
import {
|
|
||||||
questionStore,
|
|
||||||
updateQuestionsList,
|
|
||||||
createQuestion,
|
|
||||||
removeQuestionForce,
|
|
||||||
} from "@root/questions";
|
|
||||||
|
|
||||||
import type {
|
import type {
|
||||||
QuizQuestionType,
|
AnyQuizQuestion,
|
||||||
QuizQuestionBase,
|
QuizQuestionType
|
||||||
} from "../../model/questionTypes/shared";
|
} from "../../model/questionTypes/shared";
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
totalIndex: number;
|
question: AnyQuizQuestion;
|
||||||
}
|
}
|
||||||
|
|
||||||
type ButtonTypeQuestion = {
|
type ButtonTypeQuestion = {
|
||||||
@ -36,6 +27,30 @@ type ButtonTypeQuestion = {
|
|||||||
value: QuizQuestionType;
|
value: QuizQuestionType;
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default function TypeQuestions({ question }: Props) {
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Box
|
||||||
|
sx={{
|
||||||
|
display: "flex",
|
||||||
|
flexWrap: "wrap",
|
||||||
|
gap: "20px",
|
||||||
|
padding: "8px 20px 20px",
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{BUTTON_TYPE_QUESTIONS.map(({ icon, title, value }) => (
|
||||||
|
<QuestionsMiniButton
|
||||||
|
key={title}
|
||||||
|
dataCy={`select-questiontype-${value}`}
|
||||||
|
onClick={() => setQuestionFieldOptimistic(question.id, "type", value)}
|
||||||
|
icon={icon}
|
||||||
|
text={title}
|
||||||
|
/>
|
||||||
|
))}
|
||||||
|
</Box>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
export const BUTTON_TYPE_QUESTIONS: ButtonTypeQuestion[] = [
|
export const BUTTON_TYPE_QUESTIONS: ButtonTypeQuestion[] = [
|
||||||
{
|
{
|
||||||
icon: <Answer color="#9A9AAF" />,
|
icon: <Answer color="#9A9AAF" />,
|
||||||
@ -93,38 +108,3 @@ export const BUTTON_TYPE_QUESTIONS: ButtonTypeQuestion[] = [
|
|||||||
value: "rating",
|
value: "rating",
|
||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
export default function TypeQuestions({ totalIndex }: Props) {
|
|
||||||
const quizId = Number(useParams().quizId);
|
|
||||||
const { listQuestions } = questionStore();
|
|
||||||
|
|
||||||
return (
|
|
||||||
<Box
|
|
||||||
sx={{
|
|
||||||
display: "flex",
|
|
||||||
flexWrap: "wrap",
|
|
||||||
gap: "20px",
|
|
||||||
padding: "8px 20px 20px",
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{BUTTON_TYPE_QUESTIONS.map(({ icon, title, value }) => (
|
|
||||||
<QuestionsMiniButton
|
|
||||||
key={title}
|
|
||||||
dataCy={`select-questiontype-${value}`}
|
|
||||||
onClick={() => {
|
|
||||||
const question = { ...listQuestions[quizId][totalIndex] };
|
|
||||||
|
|
||||||
removeQuestionForce(quizId, question.id);
|
|
||||||
createQuestion(quizId, value, totalIndex);
|
|
||||||
updateQuestionsList<QuizQuestionBase>(quizId, totalIndex, {
|
|
||||||
expanded: question.expanded,
|
|
||||||
type: value,
|
|
||||||
});
|
|
||||||
}}
|
|
||||||
icon={icon}
|
|
||||||
text={title}
|
|
||||||
/>
|
|
||||||
))}
|
|
||||||
</Box>
|
|
||||||
);
|
|
||||||
}
|
|
||||||
|
|||||||
@ -1,33 +1,35 @@
|
|||||||
import { questionApi } from "@api/question";
|
import { questionApi } from "@api/question";
|
||||||
import { devlog } from "@frontend/kitui";
|
import { devlog } from "@frontend/kitui";
|
||||||
import { Question } from "@model/question/question";
|
import { RawQuestion, rawQuestionToQuestion } from "@model/question/question";
|
||||||
import { produce } from "immer";
|
import { produce } from "immer";
|
||||||
import { enqueueSnackbar } from "notistack";
|
import { enqueueSnackbar } from "notistack";
|
||||||
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
|
import { isAxiosCanceledError } from "../../utils/isAxiosCanceledError";
|
||||||
import { QuestionsStore, useQuestionsStore } from "./store";
|
import { QuestionsStore, useQuestionsStore } from "./store";
|
||||||
|
import { questionToEditQuestionRequest } from "@model/question/edit";
|
||||||
|
import { AnyQuizQuestion } from "@model/questionTypes/shared";
|
||||||
|
|
||||||
|
|
||||||
export const setQuestions = (quizes: Question[] | null) => setProducedState(state => {
|
export const setQuestions = (questions: RawQuestion[] | null) => setProducedState(state => {
|
||||||
state.questionsById = {};
|
state.questionsById = {};
|
||||||
if (quizes === null) return;
|
if (questions === null) return;
|
||||||
|
|
||||||
quizes.forEach(question => state.questionsById[question.id] = question);
|
questions.forEach(question => state.questionsById[question.id] = rawQuestionToQuestion(question));
|
||||||
}, {
|
}, {
|
||||||
type: "setQuizes",
|
type: "setQuestions",
|
||||||
quizes,
|
questions,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setQuestion = (question: Question) => setProducedState(state => {
|
export const setQuestion = (question: AnyQuizQuestion) => setProducedState(state => {
|
||||||
state.questionsById[question.id] = question;
|
state.questionsById[question.id] = question;
|
||||||
}, {
|
}, {
|
||||||
type: "setQuestion",
|
type: "setQuestion",
|
||||||
question,
|
question,
|
||||||
});
|
});
|
||||||
|
|
||||||
export const setQuestionField = <T extends keyof Question>(
|
export const setQuestionField = <T extends keyof AnyQuizQuestion>(
|
||||||
questionId: number,
|
questionId: number,
|
||||||
field: T,
|
field: T,
|
||||||
value: Question[T],
|
value: AnyQuizQuestion[T],
|
||||||
) => setProducedState(state => {
|
) => setProducedState(state => {
|
||||||
const question = state.questionsById[questionId];
|
const question = state.questionsById[questionId];
|
||||||
if (!question) return;
|
if (!question) return;
|
||||||
@ -40,13 +42,13 @@ export const setQuestionField = <T extends keyof Question>(
|
|||||||
value,
|
value,
|
||||||
});
|
});
|
||||||
|
|
||||||
let savedOriginalQuestion: Question | null = null;
|
let savedOriginalQuestion: AnyQuizQuestion | null = null;
|
||||||
let controller: AbortController | null = null;
|
let controller: AbortController | null = null;
|
||||||
|
|
||||||
export const setQuestionFieldOptimistic = async <T extends keyof Question>(
|
export const setQuestionFieldOptimistic = async <T extends keyof AnyQuizQuestion>(
|
||||||
questionId: number,
|
questionId: number,
|
||||||
field: T,
|
field: T,
|
||||||
value: Question[T],
|
value: AnyQuizQuestion[T],
|
||||||
) => {
|
) => {
|
||||||
const question = useQuestionsStore.getState().questionsById[questionId] ?? null;
|
const question = useQuestionsStore.getState().questionsById[questionId] ?? null;
|
||||||
if (!question) return;
|
if (!question) return;
|
||||||
@ -60,9 +62,12 @@ export const setQuestionFieldOptimistic = async <T extends keyof Question>(
|
|||||||
|
|
||||||
setQuestion(currentUpdatedQuestion);
|
setQuestion(currentUpdatedQuestion);
|
||||||
try {
|
try {
|
||||||
const { updated } = await questionApi.edit(currentUpdatedQuestion, controller.signal);
|
const { updated } = await questionApi.edit(
|
||||||
|
questionToEditQuestionRequest(currentUpdatedQuestion),
|
||||||
|
controller.signal,
|
||||||
|
);
|
||||||
|
|
||||||
setQuestionField(question.id, "version", updated);
|
setQuestionField(question.id, "id", updated);
|
||||||
controller = null;
|
controller = null;
|
||||||
savedOriginalQuestion = null;
|
savedOriginalQuestion = null;
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
@ -83,7 +88,7 @@ export const setQuestionFieldOptimistic = async <T extends keyof Question>(
|
|||||||
|
|
||||||
export const updateQuestionWithFn = (
|
export const updateQuestionWithFn = (
|
||||||
questionId: number,
|
questionId: number,
|
||||||
updateFn: (question: Question) => void,
|
updateFn: (question: AnyQuizQuestion) => void,
|
||||||
) => setProducedState(state => {
|
) => setProducedState(state => {
|
||||||
const question = state.questionsById[questionId];
|
const question = state.questionsById[questionId];
|
||||||
if (!question) return;
|
if (!question) return;
|
||||||
@ -101,7 +106,7 @@ export const createQuestion = async (quizId: number) => {
|
|||||||
quiz_id: quizId,
|
quiz_id: quizId,
|
||||||
});
|
});
|
||||||
|
|
||||||
setQuestion(question);
|
setQuestion(rawQuestionToQuestion(question));
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
devlog("Error creating question", error);
|
devlog("Error creating question", error);
|
||||||
enqueueSnackbar("Не удалось создать вопрос");
|
enqueueSnackbar("Не удалось создать вопрос");
|
||||||
|
|||||||
@ -1,10 +1,10 @@
|
|||||||
import { Question } from "@model/question/question";
|
import { AnyQuizQuestion } 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 = {
|
||||||
questionsById: Record<number, Question | undefined>;
|
questionsById: Record<number, AnyQuizQuestion | undefined>;
|
||||||
};
|
};
|
||||||
|
|
||||||
const initialState: QuestionsStore = {
|
const initialState: QuestionsStore = {
|
||||||
|
|||||||
Loading…
Reference in New Issue
Block a user