сделал редактирование задачи

This commit is contained in:
Андрей Дувакин 2025-11-29 10:47:47 +05:00
parent ca6727d9bf
commit 23ba0edf17
3 changed files with 459 additions and 0 deletions

View File

@ -0,0 +1,139 @@
import useUpdateTaskModalForm from "./useUpdateTaskModalForm.js";
import {Button, Divider, Form, Input, InputNumber, Modal, Popconfirm, Row, Spin, Upload} from "antd";
import JoditEditor from "jodit-react";
import LoadingIndicator from "../../../../Widgets/LoadingIndicator/LoadingIndicator.jsx";
import {UploadOutlined} from "@ant-design/icons";
const {TextArea} = Input;
const UpdateTaskModalForm = ({courseId}) => {
const {
isModalOpen,
handleCancel,
handleOk,
form,
joditConfig,
editorRef,
isLoading,
initialContent,
currentLesson,
isFilesLoading,
downloadFile,
files,
downloadingFiles,
deletingFiles,
deleteFile,
draftFiles,
handleAddFile,
handleRemoveFile,
} = useUpdateTaskModalForm({courseId});
if (isLoading) {
return <Modal>
<LoadingIndicator/>
</Modal>
}
return (
<Modal
title="Редактирование задания"
open={isModalOpen}
onCancel={handleCancel}
width={1000}
footer={[
<Button key="cancel" onClick={handleCancel}>
Отмена
</Button>,
<Button
key="submit"
type="primary"
loading={isLoading}
onClick={handleOk}
>
Сохранить изменения
</Button>,
]}
maskClosable={false}
keyboard={false}
>
<Form form={form} layout="vertical">
<Form.Item
name="title"
label="Название задания"
rules={[{required: true, message: "Введите название задания"}]}
>
<Input size="large"/>
</Form.Item>
<Form.Item name="description" label="Краткое описание заачи">
<TextArea rows={2} placeholder="Что нужно будет сделать..."/>
</Form.Item>
<Form.Item name="number" label="Порядковый номер" initialValue={1}>
<InputNumber min={1} style={{width: "100%"}}/>
</Form.Item>
<Form.Item label="Содержание задания">
<div style={{border: "1px solid #d9d9d9", borderRadius: 6}}>
<JoditEditor
ref={editorRef}
config={joditConfig}
/>
</div>
</Form.Item>
{isFilesLoading ? (
<Spin/>
) : files.length > 0 ? (
files.map((file) => (
<Row key={file.id} align="middle" justify="space-between">
<span>{file.filename || "Не указан"}</span>
<div>
<Button
onClick={() => downloadFile(file.id, file.filename)}
loading={downloadingFiles[file.id] || false}
disabled={downloadingFiles[file.id] || deletingFiles[file.id] || false}
type={"dashed"}
style={{marginRight: 8}}
>
{downloadingFiles[file.id] ? "Загрузка..." : "Скачать"}
</Button>
<Popconfirm
title={"Вы уверены, что хотите удалить файл?"}
onConfirm={() => deleteFile(file.id, file.filename)}
>
<Button
loading={deletingFiles[file.id] || false}
disabled={deletingFiles[file.id] || downloadingFiles[file.id] || false}
type={"dashed"}
danger
>
{deletingFiles[file.id] ? "Удаление..." : "Удалить"}
</Button>
</Popconfirm>
</div>
<Divider/>
</Row>
))
) : (
<p>Файлы отсутствуют</p>
)}
<Form.Item name="files" label="Прикрепить файлы">
<Upload
fileList={draftFiles}
beforeUpload={(file) => {
handleAddFile(file);
return false;
}}
onRemove={(file) => handleRemoveFile(file)}
multiple
>
<Button icon={<UploadOutlined/>}>Выбрать файлы</Button>
</Upload>
</Form.Item>
</Form>
</Modal>
);
};
export default UpdateTaskModalForm;

View File

@ -0,0 +1,318 @@
import {useDispatch, useSelector} from "react-redux";
import {Form, notification} from "antd";
import {useEffect, useMemo, useRef, useState} from "react";
import CONFIG from "../../../../../Core/сonfig.js";
import {
useDeleteFileMutation,
useGetTaskFilesListQuery,
useUpdateTaskMutation,
useUploadFileMutation
} from "../../../../../Api/tasksApi.js";
import {setSelectedTaskToUpdate} from "../../../../../Redux/Slices/tasksSlice.js";
const useUpdateTaskModalForm = () => {
const dispatch = useDispatch();
const [form] = Form.useForm();
const editorRef = useRef(null);
const {selectedTaskToUpdate} = useSelector((state) => state.tasks);
const [updateTask, {isLoading: isLoadingUpdateTask}] = useUpdateTaskMutation();
const isModalOpen = selectedTaskToUpdate !== null;
const [draftFiles, setDraftFiles] = useState([]);
const [uploadFile] = useUploadFileMutation();
const [deleteFileMut] = useDeleteFileMutation();
const [downloadingFiles, setDownloadingFiles] = useState({});
const [deletingFiles, setDeletingFiles] = useState({});
const {
data: currentTaskFiles = [],
isLoading: isCurrentTaskFilesLoading,
isError: isCurrentTaskFilesError
} = useGetTaskFilesListQuery(selectedTaskToUpdate?.id, {
skip: !selectedTaskToUpdate?.id
});
const downloadFile = async (fileId, fileName) => {
try {
setDownloadingFiles((prev) => ({ ...prev, [fileId]: true }));
const token = localStorage.getItem('access_token');
if (!token) {
notification.error({
title: "Ошибка",
description: "Токен не найден",
placement: "topRight",
});
return;
}
const response = await fetch(`${CONFIG.BASE_URL}/tasks/file/${fileId}/`, {
method: 'GET',
headers: {
'Authorization': `Bearer ${token}`,
},
});
if (!response.ok) {
const errorText = await response.text();
notification.error({
title: "Ошибка",
description: errorText || "Не удалось скачать файл",
placement: "topRight",
});
return;
}
const contentType = response.headers.get('content-type');
if (!contentType || contentType.includes('text/html')) {
const errorText = await response.text();
notification.error({
title: "Ошибка",
description: errorText || "Не удалось скачать файл",
placement: "topRight",
});
return;
}
let safeFileName = fileName || "file";
if (!safeFileName.match(/\.[a-zA-Z0-9]+$/)) {
if (contentType.includes('application/pdf')) {
safeFileName += '.pdf';
} else if (contentType.includes('image/jpeg')) {
safeFileName += '.jpg';
} else if (contentType.includes('image/png')) {
safeFileName += '.png';
} else {
safeFileName += '.bin';
}
}
const blob = await response.blob();
const downloadUrl = window.URL.createObjectURL(blob);
const link = document.createElement("a");
link.href = downloadUrl;
link.setAttribute("download", safeFileName);
document.body.appendChild(link);
link.click();
link.remove();
window.URL.revokeObjectURL(downloadUrl);
} catch (error) {
notification.error({
title: "Ошибка",
description: error.message || "Не удалось скачать файл",
placement: "topRight",
});
} finally {
setDownloadingFiles((prev) => ({...prev, [fileId]: false}));
}
};
const deleteFile = async (fileId, fileName) => {
try {
setDeletingFiles((prev) => ({...prev, [fileId]: true}));
await deleteFileMut(fileId).unwrap();
notification.success({
title: "Файл удален",
description: `Файл ${fileName || "неизвестный"} успешно удален.`,
placement: "topRight",
});
} catch (error) {
console.error("Error deleting file:", error);
notification.error({
title: "Ошибка при удалении файла",
description: `Не удалось удалить файл ${fileName || "неизвестный"}: ${error.data?.detail || error.message}`,
placement: "topRight",
});
} finally {
setDeletingFiles((prev) => ({...prev, [fileId]: false}));
}
};
useEffect(() => {
if (selectedTaskToUpdate && isModalOpen) {
form.setFieldsValue({
title: selectedTaskToUpdate.title || "",
description: selectedTaskToUpdate.description || "",
number: selectedTaskToUpdate.number || 1,
});
setTimeout(() => {
if (editorRef.current) {
editorRef.current.value = selectedTaskToUpdate.text || "";
}
}, 0);
}
}, [selectedTaskToUpdate, isModalOpen, form]);
const handleCancel = () => {
form.resetFields();
if (editorRef.current) {
editorRef.current.value = "";
}
dispatch(setSelectedTaskToUpdate(null));
};
const handleOk = async () => {
try {
const values = await form.validateFields();
const content = editorRef.current?.value || "";
const taskData = {
title: values.title,
description: values.description || null,
text: content,
number: values.number,
};
await updateTask({
taskId: selectedTaskToUpdate.id,
taskData,
}).unwrap();
for (const file of draftFiles) {
try {
await uploadFile({
task_id: selectedTaskToUpdate.id,
fileData: file,
}).unwrap();
} catch (error) {
console.error(`Error uploading file ${file.name}:`, error);
const errorMessage = error.data?.detail
? JSON.stringify(error.data.detail, null, 2)
: JSON.stringify(error.data || error.message || "Неизвестная ошибка", null, 2);
notification.error({
title: "Ошибка загрузки файла",
description: `Не удалось загрузить файл ${file.name}: ${errorMessage}`,
placement: "topRight",
});
}
}
setDraftFiles([]);
notification.success({
title: "Успех",
description: "Лекция успешно обновлена!",
});
handleCancel();
} catch (error) {
notification.error({
title: "Ошибка",
description: error?.data?.detail || "Не удалось обновить лекцию",
});
}
};
const joditConfig = useMemo(
() => ({
readonly: false,
height: 150,
toolbarAdaptive: false,
buttons: [
"bold", "italic", "underline", "strikethrough", "|",
"superscript", "subscript", "|",
"ul", "ol", "outdent", "indent", "|",
"font", "fontsize", "brush", "paragraph", "|",
"align", "hr", "|",
"table", "link", "image", "video", "symbols", "|",
"undo", "redo", "cut", "copy", "paste", "selectall", "eraser", "|",
"find", "source", "fullsize", "print", "preview",
],
autofocus: false,
preserveSelection: true,
askBeforePasteHTML: false,
askBeforePasteFromWord: false,
defaultActionOnPaste: "insert_clear_html",
spellcheck: true,
placeholder: "Заполните содержимое лекционного материала",
showCharsCounter: true,
showWordsCounter: true,
showXPathInStatusbar: false,
toolbarSticky: true,
toolbarButtonSize: "middle",
cleanHTML: {
removeEmptyElements: true,
replaceNBSP: false,
},
hotkeys: {
"ctrl + shift + f": "find",
"ctrl + b": "bold",
"ctrl + i": "italic",
"ctrl + u": "underline",
},
image: {
editSrc: true,
editTitle: true,
editAlt: true,
openOnDblClick: false,
},
video: {
allowedSources: ["youtube", "vimeo"],
},
uploader: {
insertImageAsBase64URI: true,
},
paste: {
insertAsBase64: true,
mimeTypes: ["image/png", "image/jpeg", "image/gif"],
maxFileSize: 5 * 1024 * 1024,
error: () => {
notification.error({
title: "Ошибка вставки",
description: "Файл слишком большой или неподдерживаемый формат.",
placement: "topRight",
});
},
},
}),
[]
);
const handleAddFile = (file) => {
const maxSize = 50 * 1024 * 1024; // 50 мегабайт
if (file.size > maxSize) {
notification.error({
message: "Ошибка вставки",
description: "Файл слишком большой.",
placement: "topRight",
});
return false;
}
setDraftFiles((prev) => [...prev, file]);
return false;
};
const handleRemoveFile = (file) => {
setDraftFiles((prev) => prev.filter((f) => f.uid !== file.uid));
};
const initialContent = selectedTaskToUpdate?.text || "";
return {
isModalOpen,
handleCancel,
handleOk,
form,
joditConfig,
editorRef,
isLoading: isLoadingUpdateTask,
initialContent,
currenttask: selectedTaskToUpdate,
currentTaskFiles,
isFilesLoading: isCurrentTaskFilesLoading,
downloadFile,
files: currentTaskFiles,
downloadingFiles,
deletingFiles,
deleteFile,
draftFiles,
handleAddFile,
handleRemoveFile,
};
};
export default useUpdateTaskModalForm;

View File

@ -30,6 +30,7 @@ import CreateLessonModalForm from "./Components/CreateLessonModalForm/CreateLess
import ViewLessonModal from "./Components/ViewLessonModalForm/ViewLessonModal.jsx"; import ViewLessonModal from "./Components/ViewLessonModalForm/ViewLessonModal.jsx";
import UpdateLessonModalForm from "./Components/UpdateLessonModalForm/UpdateLessonModalForm.jsx"; import UpdateLessonModalForm from "./Components/UpdateLessonModalForm/UpdateLessonModalForm.jsx";
import CreateTaskModalForm from "./Components/CreateTaskModalForm/CreateTaskModalForm.jsx"; import CreateTaskModalForm from "./Components/CreateTaskModalForm/CreateTaskModalForm.jsx";
import UpdateTaskModalForm from "./Components/UpdateTaskModalForm/UpdateTaskModalForm.jsx";
const {Title, Text} = Typography; const {Title, Text} = Typography;
@ -217,6 +218,7 @@ const CourseDetailPage = () => {
<CreateTaskModalForm <CreateTaskModalForm
courseId={courseId} courseId={courseId}
/> />
<UpdateTaskModalForm/>
{[CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData.role.title) && ( {[CONFIG.ROOT_ROLE_NAME, ROLES.TEACHER].includes(userData.role.title) && (
<FloatButton.Group <FloatButton.Group
placement={"left"} placement={"left"}