316 lines
9.6 KiB
TypeScript
316 lines
9.6 KiB
TypeScript
import { articlesStore, authInstance, languageStore } from "@shared";
|
||
import { observer } from "mobx-react-lite";
|
||
import { useEffect, useState } from "react";
|
||
import {
|
||
Dialog,
|
||
DialogTitle,
|
||
DialogContent,
|
||
DialogActions,
|
||
Button,
|
||
TextField,
|
||
List,
|
||
ListItemButton,
|
||
ListItemText,
|
||
Paper,
|
||
Box,
|
||
Typography,
|
||
InputAdornment,
|
||
} from "@mui/material";
|
||
import { ImagePlus, Search } from "lucide-react";
|
||
import { MediaViewer, ReactMarkdownComponent } from "@widgets";
|
||
|
||
interface SelectArticleModalProps {
|
||
open: boolean;
|
||
onClose: () => void;
|
||
onSelectArticle: (
|
||
articleId: number,
|
||
heading: string,
|
||
body: string,
|
||
media: { id: string; media_type: number; filename: string }[]
|
||
) => void;
|
||
linkedArticleIds?: number[]; // Add optional prop for linked articles
|
||
}
|
||
|
||
export const SelectArticleModal = observer(
|
||
({
|
||
open,
|
||
onClose,
|
||
onSelectArticle,
|
||
linkedArticleIds = [],
|
||
}: SelectArticleModalProps) => {
|
||
const { articles, getArticle, getArticleMedia } = articlesStore;
|
||
const [searchQuery, setSearchQuery] = useState("");
|
||
const [selectedArticleId, setSelectedArticleId] = useState<number | null>(
|
||
null
|
||
);
|
||
const [isLoading, setIsLoading] = useState(false);
|
||
|
||
// Reset selection when modal opens/closes
|
||
useEffect(() => {
|
||
if (open) {
|
||
setSelectedArticleId(null);
|
||
articlesStore.articleData = null;
|
||
articlesStore.articleMedia = null;
|
||
}
|
||
}, [open]);
|
||
|
||
useEffect(() => {
|
||
const handleKeyPress = async (event: KeyboardEvent) => {
|
||
if (event.key.toLowerCase() === "enter") {
|
||
event.preventDefault();
|
||
if (selectedArticleId) {
|
||
const media = await authInstance.get(
|
||
`/article/${selectedArticleId}/media`
|
||
);
|
||
onSelectArticle(
|
||
selectedArticleId,
|
||
articlesStore.articleData?.heading || "",
|
||
articlesStore.articleData?.body || "",
|
||
media.data || []
|
||
);
|
||
onClose();
|
||
setSelectedArticleId(null);
|
||
}
|
||
}
|
||
};
|
||
|
||
window.addEventListener("keydown", handleKeyPress);
|
||
return () => {
|
||
window.removeEventListener("keydown", handleKeyPress);
|
||
};
|
||
}, [selectedArticleId, onSelectArticle, onClose]);
|
||
|
||
const handleArticleClick = async (articleId: number) => {
|
||
setSelectedArticleId(articleId);
|
||
setIsLoading(true);
|
||
|
||
try {
|
||
await Promise.all([
|
||
getArticle(Number(articleId)),
|
||
getArticleMedia(Number(articleId)),
|
||
]);
|
||
} catch (error) {
|
||
console.error("Failed to fetch article data:", error);
|
||
// Reset article data on error
|
||
articlesStore.articleData = null;
|
||
articlesStore.articleMedia = null;
|
||
} finally {
|
||
setIsLoading(false);
|
||
}
|
||
};
|
||
|
||
const filteredArticles = articles[languageStore.language].filter(
|
||
(article) => !linkedArticleIds.includes(article.id)
|
||
);
|
||
// .filter((article) =>
|
||
// article.service_name.toLowerCase().includes(searchQuery.toLowerCase())
|
||
// );
|
||
|
||
return (
|
||
<Dialog
|
||
open={open}
|
||
onClose={onClose}
|
||
maxWidth="lg"
|
||
fullWidth
|
||
PaperProps={{
|
||
sx: {
|
||
minHeight: "80vh",
|
||
maxHeight: "90vh",
|
||
},
|
||
}}
|
||
>
|
||
<DialogTitle>Выберите существующую статью</DialogTitle>
|
||
<DialogContent
|
||
className="flex gap-4"
|
||
dividers
|
||
sx={{
|
||
height: "600px",
|
||
display: "flex",
|
||
flexDirection: "row",
|
||
alignItems: "center",
|
||
|
||
p: 2,
|
||
}}
|
||
>
|
||
<Paper className="w-[66%] flex flex-col h-full" elevation={2}>
|
||
<TextField
|
||
fullWidth
|
||
placeholder="Поиск статей..."
|
||
value={searchQuery}
|
||
onChange={(e) => setSearchQuery(e.target.value)}
|
||
sx={{ mb: 2, mt: 1, px: 2 }}
|
||
InputProps={{
|
||
startAdornment: (
|
||
<InputAdornment position="start">
|
||
<Search size={20} />
|
||
</InputAdornment>
|
||
),
|
||
}}
|
||
/>
|
||
<List sx={{ flexGrow: 1, overflowY: "auto", px: 2 }}>
|
||
{filteredArticles.length === 0 ? (
|
||
<Typography
|
||
variant="body2"
|
||
color="text.secondary"
|
||
sx={{ p: 2, textAlign: "center" }}
|
||
>
|
||
{searchQuery ? "Статьи не найдены" : "Нет доступных статей"}
|
||
</Typography>
|
||
) : (
|
||
// @ts-ignore
|
||
filteredArticles.map((article) => (
|
||
<ListItemButton
|
||
key={article.id}
|
||
onClick={() => handleArticleClick(article.id)}
|
||
onDoubleClick={async () => {
|
||
const media = await authInstance.get(
|
||
`/article/${article.id}/media`
|
||
);
|
||
onSelectArticle(
|
||
article.id,
|
||
article.heading,
|
||
article.body,
|
||
media.data
|
||
);
|
||
}}
|
||
selected={selectedArticleId === article.id}
|
||
disabled={isLoading}
|
||
sx={{
|
||
borderRadius: 1,
|
||
mb: 0.5,
|
||
"&:hover": {
|
||
backgroundColor: "action.hover",
|
||
},
|
||
"&.Mui-selected": {
|
||
backgroundColor: "primary.main",
|
||
color: "primary.contrastText",
|
||
"&:hover": {
|
||
backgroundColor: "primary.dark",
|
||
},
|
||
},
|
||
}}
|
||
>
|
||
<ListItemText
|
||
primary={article.service_name}
|
||
primaryTypographyProps={{
|
||
fontWeight:
|
||
selectedArticleId === article.id ? "bold" : "normal",
|
||
}}
|
||
/>
|
||
</ListItemButton>
|
||
))
|
||
)}
|
||
</List>
|
||
</Paper>
|
||
<Paper
|
||
elevation={3}
|
||
sx={{
|
||
width: "100%",
|
||
minWidth: 320,
|
||
maxWidth: 310,
|
||
|
||
background:
|
||
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
|
||
|
||
padding: 0,
|
||
margin: "0px auto",
|
||
display: "flex",
|
||
flexDirection: "column",
|
||
}}
|
||
>
|
||
<Box
|
||
sx={{
|
||
width: "100%",
|
||
height: 175,
|
||
display: "flex",
|
||
alignItems: "center",
|
||
justifyContent: "center",
|
||
padding: "3px",
|
||
}}
|
||
>
|
||
{articlesStore.articleMedia ? (
|
||
<MediaViewer
|
||
media={{
|
||
id: articlesStore.articleMedia.id,
|
||
media_type: articlesStore.articleMedia.media_type,
|
||
filename: articlesStore.articleMedia.filename,
|
||
}}
|
||
/>
|
||
) : (
|
||
<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%",
|
||
}}
|
||
>
|
||
{articlesStore.articleData?.heading || "Название cтатьи"}
|
||
</Typography>
|
||
</Box>
|
||
|
||
{articlesStore.articleData?.body && (
|
||
<Box
|
||
sx={{
|
||
padding: 1,
|
||
maxHeight: "200px",
|
||
overflowY: "scroll",
|
||
background:
|
||
"#806c59 linear-gradient(90deg, rgba(255, 255, 255, 0.2) 12.5%, rgba(255, 255, 255, 0.2) 100%)",
|
||
}}
|
||
>
|
||
<ReactMarkdownComponent
|
||
value={articlesStore.articleData?.body || "Описание"}
|
||
/>
|
||
</Box>
|
||
)}
|
||
</Paper>
|
||
</DialogContent>
|
||
<DialogActions sx={{ p: 2 }}>
|
||
<Button onClick={onClose}>Отмена</Button>
|
||
<Button
|
||
variant="contained"
|
||
onClick={async () => {
|
||
if (selectedArticleId) {
|
||
const media = await authInstance.get(
|
||
`/article/${selectedArticleId}/media`
|
||
);
|
||
|
||
onSelectArticle(
|
||
selectedArticleId,
|
||
articlesStore.articleData?.heading || "",
|
||
articlesStore.articleData?.body || "",
|
||
media.data
|
||
);
|
||
onClose();
|
||
setSelectedArticleId(null);
|
||
}
|
||
}}
|
||
disabled={!selectedArticleId || isLoading}
|
||
>
|
||
Выбрать
|
||
</Button>
|
||
</DialogActions>
|
||
</Dialog>
|
||
);
|
||
}
|
||
);
|