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("ru"); const [searchQuery, setSearchQuery] = useState(""); const [tabValue, setTabValue] = useState(0); const [isCreating, setIsCreating] = useState(false); const [isSaving, setIsSaving] = useState(false); const [selectedArticleId, setSelectedArticleId] = useState( 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(null); const [isUploadMediaDialogOpen, setIsUploadMediaDialogOpen] = useState(false); const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] = useState(false); const [fileToUpload, setFileToUpload] = useState(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(null); const clickTimerRef = (typeof window !== "undefined" ? (window as any) : {}) as { current?: any; } as React.MutableRefObject; 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 ( {tabValue === 0 && !isEditMode ? "Выберите существующую статью" : selectedArticleId ? "Редактирование статьи" : "Создать новую статью"} {tabValue === 0 && !isEditMode ? ( <> setTabValue(newValue)} > setSearchQuery(e.target.value)} sx={{ mb: 2 }} InputProps={{ startAdornment: ( ), }} /> {filteredArticles.length === 0 ? ( {searchQuery ? "Статьи не найдены" : "Нет доступных статей"} ) : ( filteredArticles.map((article) => ( handleListItemClick(article.id)} onDoubleClick={() => handleListItemDoubleClick(article.id) } sx={{ borderRadius: 1, mb: 0.5, "&:hover": { backgroundColor: "action.hover", }, }} > )) )} {tabValue === 0 && !isEditMode ? ( articlesStore.articleMedia ? ( ) : ( ) ) : previewMedia ? ( ) : ( )} {tabValue === 0 && !isEditMode ? selectionPreviewHeading || "Название информации" : previewData.heading || "Название информации"} {(tabValue === 0 && !isEditMode ? selectionPreviewBody : previewData.body) && ( )} ) : ( setModalLanguage(newValue)} > { if (selectedArticleId) { setEditedArticleData({ ...editedArticleData, [modalLanguage]: { ...editedArticleData[modalLanguage], heading: e.target.value, }, }); } else { setNewArticleData({ ...newArticleData, [modalLanguage]: { ...newArticleData[modalLanguage], heading: e.target.value, }, }); } }} variant="outlined" fullWidth /> { if (selectedArticleId) { setEditedArticleData({ ...editedArticleData, [modalLanguage]: { ...editedArticleData[modalLanguage], body: value, }, }); } else { setNewArticleData({ ...newArticleData, [modalLanguage]: { ...newArticleData[modalLanguage], body: value, }, }); } }} /> handleDeleteMedia(mediaId) } setSelectMediaDialogOpen={setIsSelectMediaDialogOpen} onFilesDrop={handleMediaFilesDrop} /> {previewMedia ? ( ) : ( )} {previewData.heading || "Название информации"} {previewData.body && ( )} )} {!(tabValue === 0 && !isEditMode) && ( )} {tabValue === 1 && !tempArticleId && !selectedArticleId && ( )} {(tempArticleId || selectedArticleId) && ( <> {selectedArticleId && ( )} )} setIsSelectMediaDialogOpen(false)} onSelectMedia={handleMediaSelect} /> { setIsUploadMediaDialogOpen(false); setFileToUpload(null); }} contextObjectName="Статья" contextType="sight" isArticle={true} articleName={currentArticleData[modalLanguage].heading || "Статья"} initialFile={fileToUpload || undefined} afterUpload={handleMediaUpload} /> ); } );