Files
WhiteNightsAdminPanel/src/shared/modals/ArticleSelectOrCreateDialog/index.tsx
fisenko 90f3d66b22 #14 Перепись редактирования и создания маршрута (#16)
Добавлено новое поле route_name:

Текстовые поля на двух страницах
Поле в списке маршрутов

Добавлено выбор видео на двух страниц вместе с редактором статей в виде модального окна

Модальное окно позволяет создать статью, выбрать готовую, отредактировать выбранную сразу на трех языках

Микаэл:

Пожалуйста, перепроверь код, вдруг чего найдешь как улучшить

+

захости локально и потыкай пж:

создай с 0 маршрут и прикрепи к нему созданную / какую-нибудь статью с видео, можешь попробовать загрузить либо взять готовое

после того как создашь, попробуй потыкать и поменять чего-нибудь

(проще обьясню: представь, что ты Руслан)

Reviewed-on: #16
Reviewed-by: Микаэл Оганесян <15lu.akari@unprism.ru>
Co-authored-by: fisenko <kkzemeow@gmail.com>
Co-committed-by: fisenko <kkzemeow@gmail.com>
2025-10-31 11:13:08 +00:00

1071 lines
35 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

import {
articlesStore,
languageStore,
authInstance,
SelectMediaDialog,
UploadMediaDialog,
Language,
} from "@shared";
import { observer } from "mobx-react-lite";
import { useEffect, useState } from "react";
import { runInAction } from "mobx";
import {
Dialog,
DialogTitle,
DialogContent,
DialogActions,
Button,
TextField,
List,
ListItemButton,
ListItemText,
Box,
Tabs,
Tab,
Typography,
InputAdornment,
Paper,
} from "@mui/material";
import { Search, Plus, ImagePlus, Save } from "lucide-react";
import {
ReactMarkdownEditor,
ReactMarkdownComponent,
MediaViewer,
MediaArea,
} from "@widgets";
import { toast } from "react-toastify";
interface ArticleSelectOrCreateDialogProps {
open: boolean;
onClose: () => void;
onSelectArticle: (articleId: number) => void;
}
export const ArticleSelectOrCreateDialog = observer(
({ open, onClose, onSelectArticle }: ArticleSelectOrCreateDialogProps) => {
const { articles, getArticles, getArticle, getArticleMedia } =
articlesStore;
const [modalLanguage, setModalLanguage] = useState<Language>("ru");
const [searchQuery, setSearchQuery] = useState("");
const [tabValue, setTabValue] = useState(0);
const [isCreating, setIsCreating] = useState(false);
const [isSaving, setIsSaving] = useState(false);
const [selectedArticleId, setSelectedArticleId] = useState<number | null>(
null
);
const [editedArticleData, setEditedArticleData] = useState({
ru: { heading: "", body: "" },
en: { heading: "", body: "" },
zh: { heading: "", body: "" },
});
const [editedArticleMedia, setEditedArticleMedia] = useState<
{
id: string;
filename: string;
media_type: number;
}[]
>([]);
const [newArticleData, setNewArticleData] = useState({
ru: { heading: "", body: "" },
en: { heading: "", body: "" },
zh: { heading: "", body: "" },
});
const [createdArticleMedia, setCreatedArticleMedia] = useState<
{
id: string;
filename: string;
media_name?: string;
media_type: number;
}[]
>([]);
const [tempArticleId, setTempArticleId] = useState<number | null>(null);
const [isUploadMediaDialogOpen, setIsUploadMediaDialogOpen] =
useState(false);
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
useState(false);
const [fileToUpload, setFileToUpload] = useState<File | null>(null);
const currentArticleId = selectedArticleId || tempArticleId;
const currentArticleData = selectedArticleId
? editedArticleData
: newArticleData;
const currentMedia = selectedArticleId
? editedArticleMedia
: createdArticleMedia;
const isEditMode =
selectedArticleId !== null || tempArticleId !== null || tabValue === 1;
useEffect(() => {
if (open) {
setModalLanguage("ru");
(async () => {
await Promise.all([
getArticles("ru"),
getArticles("en"),
getArticles("zh"),
]);
})();
setSearchQuery("");
setTabValue(0);
setIsCreating(false);
setIsSaving(false);
setSelectedArticleId(null);
setTempArticleId(null);
setEditedArticleData({
ru: { heading: "", body: "" },
en: { heading: "", body: "" },
zh: { heading: "", body: "" },
});
setNewArticleData({
ru: { heading: "", body: "" },
en: { heading: "", body: "" },
zh: { heading: "", body: "" },
});
setEditedArticleMedia([]);
setCreatedArticleMedia([]);
}
}, [open, getArticles]);
useEffect(() => {
if (!open) {
languageStore.setLanguage("ru");
}
}, [open]);
const loadArticleForEdit = async (articleId: number) => {
try {
const [ruArticle, enArticle, zhArticle, mediaResponse] =
await Promise.all([
articlesStore.getArticle(articleId, "ru"),
articlesStore.getArticle(articleId, "en"),
articlesStore.getArticle(articleId, "zh"),
authInstance.get(`/article/${articleId}/media`),
]);
setEditedArticleData({
ru: {
heading: ruArticle.data.heading,
body: ruArticle.data.body,
},
en: {
heading: enArticle.data.heading,
body: enArticle.data.body,
},
zh: {
heading: zhArticle.data.heading,
body: zhArticle.data.body,
},
});
setEditedArticleMedia(
(mediaResponse.data || []).map((m: any) => ({
id: m.id,
filename: m.filename,
media_type: m.media_type,
}))
);
setSelectedArticleId(articleId);
await getArticleMedia(articleId);
} catch (error) {
console.error("Error loading article:", error);
toast.error("Ошибка при загрузке статьи");
}
};
const handleArticleSelect = async (articleId: number) => {
await loadArticleForEdit(articleId);
};
const handleSaveArticle = async () => {
if (!currentArticleId) return;
try {
setIsSaving(true);
await authInstance.patch(`/article/${currentArticleId}`, {
translations: {
heading: {
ru: currentArticleData.ru.heading,
en: currentArticleData.en.heading,
zh: currentArticleData.zh.heading,
},
body: {
ru: currentArticleData.ru.body,
en: currentArticleData.en.body,
zh: currentArticleData.zh.body,
},
},
});
await loadArticleForEdit(currentArticleId);
toast.success("Статья успешно сохранена");
} catch (error) {
console.error("Error saving article:", error);
toast.error("Ошибка при сохранении статьи");
} finally {
setIsSaving(false);
}
};
const handleSelectAndClose = () => {
if (currentArticleId) {
onSelectArticle(currentArticleId);
onClose();
}
};
const handleCreateArticle = async () => {
try {
setIsCreating(true);
const hasData = Object.values(newArticleData).some(
(langData) => langData.heading.trim() || langData.body.trim()
);
if (!hasData) {
toast.error("Заполните хотя бы одно поле для любого языка");
setIsCreating(false);
return;
}
const response = await authInstance.post("/article", {
translations: {
heading: {
ru: newArticleData.ru.heading || "Новый заголовок (RU)",
en: newArticleData.en.heading || "New Heading (EN)",
zh: newArticleData.zh.heading || "Новый заголовок (ZH)",
},
body: {
ru: newArticleData.ru.body || "Новый текст (RU)",
en: newArticleData.en.body || "New Text (EN)",
zh: newArticleData.zh.body || "Новый текст (ZH)",
},
},
});
const { id } = response.data;
setTempArticleId(id);
const ruHeading = newArticleData.ru.heading || "Новый заголовок (RU)";
const enHeading = newArticleData.en.heading || "New Heading (EN)";
const zhHeading = newArticleData.zh.heading || "Новый заголовок (ZH)";
const ruBody = newArticleData.ru.body || "Новый текст (RU)";
const enBody = newArticleData.en.body || "New Text (EN)";
const zhBody = newArticleData.zh.body || "Новый текст (ZH)";
runInAction(() => {
articlesStore.articleList.ru.data.unshift({
id,
heading: ruHeading,
body: ruBody,
service_name: ruHeading,
} as any);
articlesStore.articleList.en.data.unshift({
id,
heading: enHeading,
body: enBody,
service_name: enHeading,
} as any);
articlesStore.articleList.zh.data.unshift({
id,
heading: zhHeading,
body: zhBody,
service_name: zhHeading,
} as any);
articlesStore.articleList.ru.loaded = true;
articlesStore.articleList.en.loaded = true;
articlesStore.articleList.zh.loaded = true;
if (articlesStore.articles) {
articlesStore.articles.ru.unshift({
id,
heading: ruHeading,
body: ruBody,
service_name: ruHeading,
} as any);
articlesStore.articles.en.unshift({
id,
heading: enHeading,
body: enBody,
service_name: enHeading,
} as any);
articlesStore.articles.zh.unshift({
id,
heading: zhHeading,
body: zhBody,
service_name: zhHeading,
} as any);
}
});
if (createdArticleMedia.length > 0) {
try {
for (let i = 0; i < createdArticleMedia.length; i++) {
await authInstance.post(`/article/${id}/media`, {
media_id: createdArticleMedia[i].id,
media_order: i + 1,
});
}
} catch (error) {
console.error("Error linking media:", error);
toast.warning(
"Статья создана, но не удалось добавить некоторые медиа"
);
}
}
const mediaResponse = await authInstance.get(`/article/${id}/media`);
if (mediaResponse.data && mediaResponse.data.length > 0) {
setCreatedArticleMedia(
mediaResponse.data.map((m: any) => ({
id: m.id,
filename: m.filename,
media_name: m.media_name,
media_type: m.media_type,
}))
);
}
await getArticle(id);
await getArticleMedia(id);
toast.success("Статья успешно создана");
onSelectArticle(id);
onClose();
} catch (error) {
console.error("Error creating article:", error);
toast.error("Ошибка при создании статьи");
} finally {
setIsCreating(false);
}
};
const handleMediaSelect = async (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
if (!currentArticleId) {
if (tabValue === 1) {
setCreatedArticleMedia((prev) => [...prev, media]);
}
setIsSelectMediaDialogOpen(false);
return;
}
try {
const currentMediaCount = currentMedia.length;
await authInstance.post(`/article/${currentArticleId}/media`, {
media_id: media.id,
media_order: currentMediaCount + 1,
});
const mediaResponse = await authInstance.get(
`/article/${currentArticleId}/media`
);
const mediaList = mediaResponse.data.map((m: any) => ({
id: m.id,
filename: m.filename,
media_type: m.media_type,
}));
if (selectedArticleId) {
setEditedArticleMedia(mediaList);
} else {
setCreatedArticleMedia(
mediaResponse.data.map((m: any) => ({
id: m.id,
filename: m.filename,
media_name: m.media_name,
media_type: m.media_type,
}))
);
}
} catch (error) {
console.error("Error linking media:", error);
toast.error("Ошибка при добавлении медиа");
}
setIsSelectMediaDialogOpen(false);
};
const handleDeleteMedia = async (mediaId: string) => {
if (!currentArticleId) {
if (tabValue === 1) {
setCreatedArticleMedia((prev) =>
prev.filter((m) => m.id !== mediaId)
);
}
return;
}
try {
await authInstance.delete(
`/article/${currentArticleId}/media/${mediaId}`
);
const response = await authInstance.get(
`/article/${currentArticleId}/media`
);
const mediaList = response.data.map((m: any) => ({
id: m.id,
filename: m.filename,
media_type: m.media_type,
}));
if (selectedArticleId) {
setEditedArticleMedia(mediaList);
} else {
setCreatedArticleMedia(
response.data.map((m: any) => ({
id: m.id,
filename: m.filename,
media_name: m.media_name,
media_type: m.media_type,
}))
);
}
} catch (error) {
console.error("Error deleting media:", error);
toast.error("Ошибка при удалении медиа");
}
};
const handleMediaFilesDrop = (files: File[]) => {
if (files.length > 0) {
setFileToUpload(files[0]);
setIsUploadMediaDialogOpen(true);
}
};
const handleMediaUpload = async (media: {
id: string;
filename: string;
media_name?: string;
media_type: number;
}) => {
if (!currentArticleId) {
await handleMediaSelect(media);
return;
}
try {
const currentMediaCount = currentMedia.length;
await authInstance.post(`/article/${currentArticleId}/media`, {
media_id: media.id,
media_order: currentMediaCount + 1,
});
const mediaResponse = await authInstance.get(
`/article/${currentArticleId}/media`
);
const mediaList = mediaResponse.data.map((m: any) => ({
id: m.id,
filename: m.filename,
media_type: m.media_type,
}));
if (selectedArticleId) {
setEditedArticleMedia(mediaList);
} else {
setCreatedArticleMedia(
mediaResponse.data.map((m: any) => ({
id: m.id,
filename: m.filename,
media_name: m.media_name,
media_type: m.media_type,
}))
);
}
} catch (error) {
console.error("Error linking media:", error);
toast.error("Ошибка при добавлении медиа");
}
setIsUploadMediaDialogOpen(false);
setFileToUpload(null);
};
const handleBack = () => {
if (selectedArticleId) {
setSelectedArticleId(null);
setEditedArticleData({
ru: { heading: "", body: "" },
en: { heading: "", body: "" },
zh: { heading: "", body: "" },
});
setEditedArticleMedia([]);
} else if (tempArticleId) {
setTempArticleId(null);
setNewArticleData({
ru: { heading: "", body: "" },
en: { heading: "", body: "" },
zh: { heading: "", body: "" },
});
setCreatedArticleMedia([]);
setTabValue(0);
} else if (tabValue === 1) {
setTabValue(0);
setNewArticleData({
ru: { heading: "", body: "" },
en: { heading: "", body: "" },
zh: { heading: "", body: "" },
});
setCreatedArticleMedia([]);
}
setModalLanguage("ru");
languageStore.setLanguage("ru");
};
const filteredArticles = articles[modalLanguage].filter((article) =>
article?.service_name?.toLowerCase().includes(searchQuery.toLowerCase())
);
// Preview-by-click logic with request serialization to avoid concurrent requests
const [isPreviewLoading, setIsPreviewLoading] = useState(false);
const [queuedPreviewId, setQueuedPreviewId] = useState<number | null>(null);
const clickTimerRef = (typeof window !== "undefined"
? (window as any)
: {}) as {
current?: any;
} as React.MutableRefObject<NodeJS.Timeout | null>;
if (clickTimerRef.current === undefined) {
(clickTimerRef as any).current = null;
}
const runPreviewFetch = async (articleId: number) => {
if (isPreviewLoading) {
setQueuedPreviewId(articleId);
return;
}
setIsPreviewLoading(true);
try {
await Promise.all([
getArticle(articleId, modalLanguage),
getArticleMedia(articleId),
]);
} finally {
setIsPreviewLoading(false);
if (queuedPreviewId && queuedPreviewId !== articleId) {
const nextId = queuedPreviewId;
setQueuedPreviewId(null);
// Run the next queued preview
runPreviewFetch(nextId);
} else {
setQueuedPreviewId(null);
}
}
};
const handleListItemClick = (articleId: number) => {
// Delay to allow double-click to cancel preview
if (clickTimerRef.current) clearTimeout(clickTimerRef.current);
clickTimerRef.current = setTimeout(() => {
if (tabValue === 0 && !selectedArticleId && !tempArticleId) {
runPreviewFetch(articleId);
}
}, 200);
};
const handleListItemDoubleClick = (articleId: number) => {
// Cancel pending single-click preview and proceed to select
if (clickTimerRef.current) {
clearTimeout(clickTimerRef.current);
(clickTimerRef as any).current = null;
}
handleArticleSelect(articleId);
};
const previewData = {
heading: currentArticleData[modalLanguage].heading || "",
body: currentArticleData[modalLanguage].body || "",
};
const previewMedia =
currentMedia.length > 0
? {
id: currentMedia[0].id,
media_type: currentMedia[0].media_type,
filename: currentMedia[0].filename,
}
: null;
const selectionPreviewHeading =
(articlesStore.articleData as any)?.[modalLanguage]?.heading ||
(articlesStore.articleData as any)?.heading ||
"";
const selectionPreviewBody =
(articlesStore.articleData as any)?.[modalLanguage]?.body ||
(articlesStore.articleData as any)?.body ||
"";
return (
<Dialog open={open} onClose={onClose} maxWidth="lg" fullWidth>
<DialogTitle>
{tabValue === 0 && !isEditMode
? "Выберите существующую статью"
: selectedArticleId
? "Редактирование статьи"
: "Создать новую статью"}
</DialogTitle>
<DialogContent
dividers
sx={{ height: "600px", display: "flex", flexDirection: "column" }}
>
{tabValue === 0 && !isEditMode ? (
<>
<Box sx={{ borderBottom: 1, borderColor: "divider", mb: 2 }}>
<Tabs
value={tabValue}
onChange={(_e, newValue) => setTabValue(newValue)}
>
<Tab label="Выбрать существующую" />
<Tab label="Создать новую" />
</Tabs>
</Box>
<Box
sx={{ display: "flex", gap: 2, flex: 1, overflow: "hidden" }}
>
<Paper className="w-[66%] flex flex-col">
<Box
sx={{
p: 2,
display: "flex",
flexDirection: "column",
flex: 1,
overflow: "hidden",
}}
>
<TextField
fullWidth
placeholder="Поиск статей..."
value={searchQuery}
onChange={(e) => setSearchQuery(e.target.value)}
sx={{ mb: 2 }}
InputProps={{
startAdornment: (
<InputAdornment position="start">
<Search size={20} />
</InputAdornment>
),
}}
/>
<List
sx={{
flexGrow: 1,
overflowY: "auto",
border: "1px solid",
borderColor: "divider",
borderRadius: 1,
}}
>
{filteredArticles.length === 0 ? (
<Box sx={{ p: 2, textAlign: "center" }}>
<Typography variant="body2" color="text.secondary">
{searchQuery
? "Статьи не найдены"
: "Нет доступных статей"}
</Typography>
</Box>
) : (
filteredArticles.map((article) => (
<ListItemButton
key={article.id}
onClick={() => handleListItemClick(article.id)}
onDoubleClick={() =>
handleListItemDoubleClick(article.id)
}
sx={{
borderRadius: 1,
mb: 0.5,
"&:hover": {
backgroundColor: "action.hover",
},
}}
>
<ListItemText primary={article.service_name} />
</ListItemButton>
))
)}
</List>
</Box>
</Paper>
<Paper
elevation={3}
sx={{
flex: 1,
maxWidth: "320px",
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
overflowY: "auto",
display: "flex",
flexDirection: "column",
borderRadius: "10px",
}}
>
<Box
sx={{
overflow: "hidden",
width: "100%",
minHeight: 100,
padding: "3px",
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
"& img": {
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
width: "100%",
height: "auto",
objectFit: "contain",
},
}}
>
{tabValue === 0 && !isEditMode ? (
articlesStore.articleMedia ? (
<MediaViewer
media={articlesStore.articleMedia}
fullWidth
/>
) : (
<ImagePlus size={48} color="white" />
)
) : previewMedia ? (
<MediaViewer media={previewMedia} fullWidth />
) : (
<ImagePlus size={48} color="white" />
)}
</Box>
<Box
sx={{
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
color: "white",
margin: "5px 0px 5px 0px",
display: "flex",
flexDirection: "column",
gap: 1,
padding: 1,
}}
>
<Typography
variant="h5"
component="h2"
sx={{
wordBreak: "break-word",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
}}
>
{tabValue === 0 && !isEditMode
? selectionPreviewHeading || "Название информации"
: previewData.heading || "Название информации"}
</Typography>
</Box>
{(tabValue === 0 && !isEditMode
? selectionPreviewBody
: previewData.body) && (
<Box
sx={{
padding: 1,
maxHeight: "300px",
overflowY: "auto",
width: "100%",
"&::-webkit-scrollbar": {
display: "none",
},
"&": {
scrollbarWidth: "none",
},
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
flexGrow: 1,
}}
>
<ReactMarkdownComponent
value={
tabValue === 0 && !isEditMode
? selectionPreviewBody
: previewData.body
}
/>
</Box>
)}
</Paper>
</Box>
</>
) : (
<Box
sx={{
display: "flex",
gap: 3,
flex: 1,
overflow: "hidden",
position: "relative",
}}
>
<Box
sx={{
flex: 2,
display: "flex",
flexDirection: "column",
gap: 2,
overflow: "auto",
position: "relative",
paddingBottom: "60px",
}}
>
<Box sx={{ borderBottom: 1, borderColor: "divider" }}>
<Tabs
value={modalLanguage}
onChange={(_e, newValue) => setModalLanguage(newValue)}
>
<Tab label="Русский" value="ru" />
<Tab label="English" value="en" />
<Tab label="中文" value="zh" />
</Tabs>
</Box>
<TextField
label="Название информации"
value={currentArticleData[modalLanguage].heading}
onChange={(e) => {
if (selectedArticleId) {
setEditedArticleData({
...editedArticleData,
[modalLanguage]: {
...editedArticleData[modalLanguage],
heading: e.target.value,
},
});
} else {
setNewArticleData({
...newArticleData,
[modalLanguage]: {
...newArticleData[modalLanguage],
heading: e.target.value,
},
});
}
}}
variant="outlined"
fullWidth
/>
<ReactMarkdownEditor
value={currentArticleData[modalLanguage].body}
onChange={(value: any) => {
if (selectedArticleId) {
setEditedArticleData({
...editedArticleData,
[modalLanguage]: {
...editedArticleData[modalLanguage],
body: value,
},
});
} else {
setNewArticleData({
...newArticleData,
[modalLanguage]: {
...newArticleData[modalLanguage],
body: value,
},
});
}
}}
/>
<MediaArea
articleId={currentArticleId || 0}
mediaIds={currentMedia}
deleteMedia={(_articleId, mediaId) =>
handleDeleteMedia(mediaId)
}
setSelectMediaDialogOpen={setIsSelectMediaDialogOpen}
onFilesDrop={handleMediaFilesDrop}
/>
</Box>
<Box
sx={{
flex: 1,
display: "flex",
flexDirection: "column",
maxWidth: "320px",
gap: 0.5,
}}
>
<Paper
elevation={3}
sx={{
width: "100%",
minWidth: 320,
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
overflowY: "auto",
display: "flex",
flexDirection: "column",
borderRadius: "10px",
}}
>
<Box
sx={{
overflow: "hidden",
width: "100%",
minHeight: 100,
padding: "3px",
position: "relative",
display: "flex",
alignItems: "center",
justifyContent: "center",
"& img": {
borderTopLeftRadius: "10px",
borderTopRightRadius: "10px",
width: "100%",
height: "auto",
objectFit: "contain",
},
}}
>
{previewMedia ? (
<MediaViewer media={previewMedia} fullWidth />
) : (
<ImagePlus size={48} color="white" />
)}
</Box>
<Box
sx={{
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
color: "white",
margin: "5px 0px 5px 0px",
display: "flex",
flexDirection: "column",
gap: 1,
padding: 1,
}}
>
<Typography
variant="h5"
component="h2"
sx={{
wordBreak: "break-word",
fontSize: "24px",
fontWeight: 700,
lineHeight: "120%",
}}
>
{previewData.heading || "Название информации"}
</Typography>
</Box>
{previewData.body && (
<Box
sx={{
padding: 1,
maxHeight: "300px",
overflowY: "auto",
width: "100%",
"&::-webkit-scrollbar": {
display: "none",
},
"&": {
scrollbarWidth: "none",
},
background:
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
flexGrow: 1,
}}
>
<ReactMarkdownComponent value={previewData.body} />
</Box>
)}
</Paper>
</Box>
</Box>
)}
</DialogContent>
<DialogActions>
{!(tabValue === 0 && !isEditMode) && (
<Button onClick={handleBack} sx={{ mr: "auto" }}>
Назад
</Button>
)}
<Button
onClick={() => {
languageStore.setLanguage("ru");
onClose();
}}
>
Отмена
</Button>
{tabValue === 1 && !tempArticleId && !selectedArticleId && (
<Button
variant="contained"
onClick={handleCreateArticle}
disabled={isCreating}
startIcon={<Plus size={16} />}
>
{isCreating ? "Создание..." : "Создать статью"}
</Button>
)}
{(tempArticleId || selectedArticleId) && (
<>
{selectedArticleId && (
<Button
variant="outlined"
onClick={handleSaveArticle}
disabled={isSaving}
startIcon={<Save size={16} />}
>
Сохранить
</Button>
)}
<Button
variant="contained"
onClick={() => {
handleSelectAndClose();
languageStore.setLanguage("ru");
}}
startIcon={<Plus size={16} />}
>
Выбрать и закрыть
</Button>
</>
)}
</DialogActions>
<SelectMediaDialog
open={isSelectMediaDialogOpen}
onClose={() => setIsSelectMediaDialogOpen(false)}
onSelectMedia={handleMediaSelect}
/>
<UploadMediaDialog
open={isUploadMediaDialogOpen}
onClose={() => {
setIsUploadMediaDialogOpen(false);
setFileToUpload(null);
}}
contextObjectName="Статья"
contextType="sight"
isArticle={true}
articleName={currentArticleData[modalLanguage].heading || "Статья"}
initialFile={fileToUpload || undefined}
afterUpload={handleMediaUpload}
/>
</Dialog>
);
}
);