сделал редактирование задачи
This commit is contained in:
parent
ca6727d9bf
commit
23ba0edf17
@ -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;
|
||||||
@ -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;
|
||||||
@ -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"}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user