fix: Fix shit Andrey code
Some checks failed
release-tag / release-image (push) Failing after 1m53s

This commit is contained in:
Илья Куприец 2025-05-25 19:43:38 +03:00
parent 28826123ec
commit 16640cb116
9 changed files with 1388 additions and 1117 deletions

View File

@ -51,6 +51,7 @@
"react-simple-maps": "^3.0.0", "react-simple-maps": "^3.0.0",
"react-simplemde-editor": "^5.2.0", "react-simplemde-editor": "^5.2.0",
"react-swipeable": "^7.0.2", "react-swipeable": "^7.0.2",
"react-toastify": "^11.0.5",
"rehype-raw": "^7.0.0", "rehype-raw": "^7.0.0",
"three": "^0.175.0", "three": "^0.175.0",
"vite-plugin-svgr": "^4.3.0" "vite-plugin-svgr": "^4.3.0"

View File

@ -19,6 +19,7 @@ import {
ALLOWED_VIDEO_TYPES, ALLOWED_VIDEO_TYPES,
} from "../components/media/MediaFormUtils"; } from "../components/media/MediaFormUtils";
import { EVERY_LANGUAGE, Languages } from "@stores"; import { EVERY_LANGUAGE, Languages } from "@stores";
import { useNotification } from "@refinedev/core";
const MemoizedSimpleMDE = React.memo(MarkdownEditor); const MemoizedSimpleMDE = React.memo(MarkdownEditor);
@ -30,14 +31,16 @@ type MediaFile = {
}; };
type Props = { type Props = {
parentId: string | number; parentId?: string | number;
parentResource: string; parentResource: string;
childResource: string; childResource: string;
title: string; title: string;
left?: boolean; left?: boolean;
language: Languages, language: Languages;
setHeadingParent?: (heading: string) => void, setHeadingParent?: (heading: string) => void;
setBodyParent?: (body: string) => void, setBodyParent?: (body: string) => void;
onSave?: (something: any) => void;
noReset?: boolean;
}; };
export const CreateSightArticle = ({ export const CreateSightArticle = ({
@ -48,8 +51,11 @@ export const CreateSightArticle = ({
left, left,
language, language,
setHeadingParent, setHeadingParent,
setBodyParent setBodyParent,
onSave,
noReset,
}: Props) => { }: Props) => {
const notification = useNotification();
const theme = useTheme(); const theme = useTheme();
const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]); const [mediaFiles, setMediaFiles] = useState<MediaFile[]>([]);
const [workingLanguage, setWorkingLanguage] = useState<Languages>(language); const [workingLanguage, setWorkingLanguage] = useState<Languages>(language);
@ -69,10 +75,9 @@ export const CreateSightArticle = ({
}, },
}); });
const [articleData, setArticleData] = useState({ const [articleData, setArticleData] = useState({
heading: EVERY_LANGUAGE(""), heading: EVERY_LANGUAGE(""),
body: EVERY_LANGUAGE("") body: EVERY_LANGUAGE(""),
}); });
function updateTranslations() { function updateTranslations() {
@ -85,8 +90,8 @@ export const CreateSightArticle = ({
body: { body: {
...articleData.body, ...articleData.body,
[workingLanguage]: watch("body") ?? "", [workingLanguage]: watch("body") ?? "",
} },
} };
setArticleData(newArticleData); setArticleData(newArticleData);
return newArticleData; return newArticleData;
} }
@ -126,8 +131,12 @@ export const CreateSightArticle = ({
const { getRootProps, getInputProps, isDragActive } = useDropzone({ const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop, onDrop,
accept: { accept: {
"image/*": ALLOWED_IMAGE_TYPES, "image/jpeg": [".jpeg", ".jpg"],
"video/*": ALLOWED_VIDEO_TYPES, "image/png": [".png"],
"image/webp": [".webp"],
"video/mp4": [".mp4"],
"video/webm": [".webm"],
"video/ogg": [".ogg"],
}, },
multiple: true, multiple: true,
}); });
@ -153,24 +162,29 @@ export const CreateSightArticle = ({
try { try {
// Создаем статью // Создаем статью
const response = await axiosInstance.post( const response = await axiosInstance.post(
`${import.meta.env.VITE_KRBL_API}/${childResource}`, { `${import.meta.env.VITE_KRBL_API}/${childResource}`,
{
...data, ...data,
translations: updateTranslations() translations: updateTranslations(),
} }
); );
const itemId = response.data.id; const itemId = response.data.id;
if (parentId) {
// Получаем существующие статьи для определения порядкового номера // Получаем существующие статьи для определения порядкового номера
const existingItemsResponse = await axiosInstance.get( const existingItemsResponse = await axiosInstance.get(
`${import.meta.env.VITE_KRBL_API}/${parentResource}/${parentId}/${childResource}` `${
import.meta.env.VITE_KRBL_API
}/${parentResource}/${parentId}/${childResource}`
); );
const existingItems = existingItemsResponse.data ?? []; const existingItems = existingItemsResponse.data ?? [];
const nextPageNum = existingItems.length + 1; const nextPageNum = existingItems.length + 1;
if (!left) { if (!left) {
// Привязываем статью к достопримечательности если она не левая
await axiosInstance.post( await axiosInstance.post(
`${import.meta.env.VITE_KRBL_API}/${parentResource}/${parentId}/${childResource}/`, `${
import.meta.env.VITE_KRBL_API
}/${parentResource}/${parentId}/${childResource}/`,
{ {
[`${childResource}_id`]: itemId, [`${childResource}_id`]: itemId,
page_num: nextPageNum, page_num: nextPageNum,
@ -186,11 +200,12 @@ export const CreateSightArticle = ({
`${import.meta.env.VITE_KRBL_API}/${parentResource}/${parentId}/`, `${import.meta.env.VITE_KRBL_API}/${parentResource}/${parentId}/`,
{ {
...data, ...data,
left_article: itemId left_article: itemId,
} }
); );
} }
} }
}
// Загружаем все медиа файлы и получаем их ID // Загружаем все медиа файлы и получаем их ID
const mediaIds = await Promise.all( const mediaIds = await Promise.all(
@ -211,10 +226,21 @@ export const CreateSightArticle = ({
) )
) )
); );
if (noReset) {
setValue("heading", "");
setValue("body", "");
} else {
resetItem(); resetItem();
setMediaFiles([]); }
if (onSave) {
onSave(response.data);
notification.open({
message: "Статья успешно создана",
type: "success",
});
} else {
window.location.reload(); window.location.reload();
}
} catch (err: any) { } catch (err: any) {
console.error("Error creating item:", err); console.error("Error creating item:", err);
} }
@ -345,7 +371,12 @@ export const CreateSightArticle = ({
</Box> </Box>
<Box sx={{ mt: 2, display: "flex", gap: 2 }}> <Box sx={{ mt: 2, display: "flex", gap: 2 }}>
<Button variant="contained" color="primary" onClick={handleSubmitItem(handleCreate)}> <Button
variant="contained"
color="primary"
type="submit"
onClick={handleSubmitItem(handleCreate)}
>
Создать Создать
</Button> </Button>
<Button <Button

View File

@ -38,7 +38,7 @@ const style = {
bgcolor: "background.paper", bgcolor: "background.paper",
border: "2px solid #000", border: "2px solid #000",
boxShadow: 24, boxShadow: 24,
p: 4 p: 4,
}; };
export const ArticleEditModal = observer(() => { export const ArticleEditModal = observer(() => {
@ -62,13 +62,11 @@ export const ArticleEditModal = observer(() => {
// Load existing media files when editing an article // Load existing media files when editing an article
const loadExistingMedia = async () => { const loadExistingMedia = async () => {
console.log("Called loadExistingMedia") console.log("Called loadExistingMedia");
if (selectedArticleId) { if (selectedArticleId) {
try { try {
const response = await axiosInstance.get( const response = await axiosInstance.get(
`${ `${import.meta.env.VITE_KRBL_API}/article/${selectedArticleId}/media`
import.meta.env.VITE_KRBL_API
}/article/${selectedArticleId}/media`
); );
const existingMedia = response.data; const existingMedia = response.data;
@ -125,7 +123,9 @@ export const ArticleEditModal = observer(() => {
try { try {
// Upload new media files // Upload new media files
const newMediaFiles = mediaFiles.filter((file) => !file.media_id); const newMediaFiles = mediaFiles.filter((file) => !file.media_id);
const existingMediaAmount = mediaFiles.filter((file) => file.media_id).length; const existingMediaAmount = mediaFiles.filter(
(file) => file.media_id
).length;
const mediaIds = await Promise.all( const mediaIds = await Promise.all(
newMediaFiles.map(async (mediaFile) => { newMediaFiles.map(async (mediaFile) => {
return await uploadMedia(mediaFile); return await uploadMedia(mediaFile);
@ -164,10 +164,10 @@ export const ArticleEditModal = observer(() => {
useEffect(() => { useEffect(() => {
if (articleData.heading[language]) { if (articleData.heading[language]) {
setValue("heading", articleData.heading[language]) setValue("heading", articleData.heading[language]);
} }
if (articleData.body[language]) { if (articleData.body[language]) {
setValue("body", articleData.body[language]) setValue("body", articleData.body[language]);
} }
}, [language, articleData, setValue]); }, [language, articleData, setValue]);
@ -176,12 +176,12 @@ export const ArticleEditModal = observer(() => {
...prevData, ...prevData,
heading: { heading: {
...prevData.heading, ...prevData.heading,
[language]: watch("heading") ?? "" [language]: watch("heading") ?? "",
}, },
body: { body: {
...prevData.body, ...prevData.body,
[language]: watch("body") ?? "" [language]: watch("body") ?? "",
} },
})); }));
}; };
@ -205,8 +205,12 @@ export const ArticleEditModal = observer(() => {
const { getRootProps, getInputProps, isDragActive } = useDropzone({ const { getRootProps, getInputProps, isDragActive } = useDropzone({
onDrop, onDrop,
accept: { accept: {
"image/*": ALLOWED_IMAGE_TYPES, "image/jpeg": [".jpeg", ".jpg"],
"video/*": ALLOWED_VIDEO_TYPES, "image/png": [".png"],
"image/webp": [".webp"],
"video/mp4": [".mp4"],
"video/webm": [".webm"],
"video/ogg": [".ogg"],
}, },
multiple: true, multiple: true,
}); });

View File

@ -1,5 +1,5 @@
@import './stylesheets/hidden-functionality.css'; @import "./stylesheets/hidden-functionality.css";
@import './stylesheets/roles-functionality.css'; @import "./stylesheets/roles-functionality.css";
.limited-text { .limited-text {
overflow: hidden; overflow: hidden;
@ -7,3 +7,19 @@
-webkit-box-orient: vertical; -webkit-box-orient: vertical;
-webkit-line-clamp: 2; -webkit-line-clamp: 2;
} }
.backup-button {
background-color: transparent;
border: none;
cursor: pointer;
padding: 0;
margin: 0;
width: 35px;
height: 35px;
color: #544044;
border-radius: 10%;
transition: all 0.3s ease;
}
.backup-button:hover {
background-color: rgba(84, 64, 68, 0.5);
}

View File

@ -1,4 +1,13 @@
import { Autocomplete, Box, TextField, Typography, Paper } from "@mui/material"; import {
Autocomplete,
Box,
TextField,
Typography,
Paper,
Accordion,
AccordionSummary,
AccordionDetails,
} from "@mui/material";
import { Create, useAutocomplete } from "@refinedev/mui"; import { Create, useAutocomplete } from "@refinedev/mui";
import { useForm } from "@refinedev/react-hook-form"; import { useForm } from "@refinedev/react-hook-form";
import { Controller, FieldValues } from "react-hook-form"; import { Controller, FieldValues } from "react-hook-form";
@ -8,11 +17,14 @@ import { TOKEN_KEY } from "@providers";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { EVERY_LANGUAGE, Languages, languageStore, cityStore } from "@stores"; import { EVERY_LANGUAGE, Languages, languageStore, cityStore } from "@stores";
import { LanguageSelector } from "@ui"; import { LanguageSelector } from "@ui";
import { CreateSightArticle } from "@components";
import ExpandMoreIcon from "@mui/icons-material/ExpandMore";
export const SightCreate = observer(() => { export const SightCreate = observer(() => {
const { language, setLanguageAction } = languageStore; const { language, setLanguageAction } = languageStore;
const [sightData, setSightData] = useState({ const [sightData, setSightData] = useState({
name: EVERY_LANGUAGE(""), name: EVERY_LANGUAGE(""),
address: EVERY_LANGUAGE("") address: EVERY_LANGUAGE(""),
}); });
const { const {
@ -46,8 +58,8 @@ export const SightCreate = observer(() => {
address: { address: {
...sightData.address, ...sightData.address,
[language]: watch("address") ?? "", [language]: watch("address") ?? "",
} },
} };
if (update) setSightData(newSightData); if (update) setSightData(newSightData);
return newSightData; return newSightData;
} }
@ -62,7 +74,7 @@ export const SightCreate = observer(() => {
console.log(newTranslations); console.log(newTranslations);
return onFinish({ return onFinish({
...values, ...values,
translations: newTranslations translations: newTranslations,
}); });
}); });
@ -71,6 +83,11 @@ export const SightCreate = observer(() => {
latitude: "", latitude: "",
longitude: "", longitude: "",
}); });
const [creatingArticleHeading, setCreatingArticleHeading] =
useState<string>("");
const [creatingArticleBody, setCreatingArticleBody] = useState<string>("");
const [cityPreview, setCityPreview] = useState(""); const [cityPreview, setCityPreview] = useState("");
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null); const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null);
const [watermarkLUPreview, setWatermarkLUPreview] = useState<string | null>( const [watermarkLUPreview, setWatermarkLUPreview] = useState<string | null>(
@ -80,6 +97,8 @@ export const SightCreate = observer(() => {
null null
); );
const [leftArticlePreview, setLeftArticlePreview] = useState(""); const [leftArticlePreview, setLeftArticlePreview] = useState("");
const [customOptions, setCustomOptions] = useState<any[]>([]);
const [previewArticlePreview, setPreviewArticlePreview] = useState(""); const [previewArticlePreview, setPreviewArticlePreview] = useState("");
const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => { const handleCoordinatesChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@ -122,6 +141,7 @@ export const SightCreate = observer(() => {
const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({ const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({
resource: "article", resource: "article",
onSearch: (value) => [ onSearch: (value) => [
{ {
field: "heading", field: "heading",
@ -131,6 +151,8 @@ export const SightCreate = observer(() => {
], ],
}); });
const mergedOptions = [...articleAutocompleteProps.options, ...customOptions];
// Следим за изменениями во всех полях // Следим за изменениями во всех полях
const nameContent = watch("name"); const nameContent = watch("name");
const addressContent = watch("address"); const addressContent = watch("address");
@ -222,10 +244,13 @@ export const SightCreate = observer(() => {
}, [previewArticleContent, articleAutocompleteProps.options]); }, [previewArticleContent, articleAutocompleteProps.options]);
return ( return (
<Create isLoading={formLoading} saveButtonProps={{ <Create
isLoading={formLoading}
saveButtonProps={{
...saveButtonProps, ...saveButtonProps,
onClick: handleFormSubmit onClick: handleFormSubmit,
}}> }}
>
<Box sx={{ display: "flex", gap: 2 }}> <Box sx={{ display: "flex", gap: 2 }}>
<Box sx={{ display: "flex", flexDirection: "column", flex: 1, gap: 2 }}> <Box sx={{ display: "flex", flexDirection: "column", flex: 1, gap: 2 }}>
{/* Форма создания */} {/* Форма создания */}
@ -468,27 +493,25 @@ export const SightCreate = observer(() => {
render={({ field }) => ( render={({ field }) => (
<Autocomplete <Autocomplete
{...articleAutocompleteProps} {...articleAutocompleteProps}
options={mergedOptions} // ← use merged options
value={ value={
articleAutocompleteProps.options.find( mergedOptions.find((option) => option.id === field.value) ??
(option) => option.id === field.value null
) ?? null
} }
onChange={(_, value) => { onChange={(_, value) => {
field.onChange(value?.id ?? ""); field.onChange(value?.id ?? "");
}} }}
getOptionLabel={(item) => { getOptionLabel={(item) => (item ? item.heading : "")}
return item ? item.heading : ""; isOptionEqualToValue={(option, value) =>
}} option.id === value?.id
isOptionEqualToValue={(option, value) => { }
return option.id === value?.id; filterOptions={(options, { inputValue }) =>
}} options.filter((option) =>
filterOptions={(options, { inputValue }) => {
return options.filter((option) =>
option.heading option.heading
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) .includes(inputValue.toLowerCase())
); )
}} }
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
@ -503,6 +526,31 @@ export const SightCreate = observer(() => {
)} )}
/> />
{!leftArticleContent && (
<Accordion>
<AccordionSummary
expandIcon={<ExpandMoreIcon />}
aria-controls="create-article-content"
id="create-article-header"
>
<Typography>Создать новую статью</Typography>
</AccordionSummary>
<AccordionDetails>
<CreateSightArticle
language={language}
parentResource="sight"
childResource="article"
title="статью"
noReset
left
onSave={(something: any) => {
setCustomOptions((prev) => [...prev, something]);
}}
/>
</AccordionDetails>
</Accordion>
)}
<Controller <Controller
control={control} control={control}
name="preview_media" name="preview_media"
@ -516,7 +564,7 @@ export const SightCreate = observer(() => {
) || null ) || null
} }
onChange={(_, value) => { onChange={(_, value) => {
console.log(value, _) console.log(value, _);
field.onChange(value?.id || ""); field.onChange(value?.id || "");
}} }}
getOptionLabel={(item) => { getOptionLabel={(item) => {
@ -531,7 +579,7 @@ export const SightCreate = observer(() => {
option.media_name option.media_name
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) && .includes(inputValue.toLowerCase()) &&
[1,2,5,6].includes(option.media_type) [1, 2, 3, 4, 5, 6].includes(option.media_type)
); );
}} }}
renderInput={(params) => ( renderInput={(params) => (

View File

@ -19,10 +19,18 @@ import { ArticleItem, articleFields } from "./types";
import { axiosInstance, TOKEN_KEY } from "@providers"; import { axiosInstance, TOKEN_KEY } from "@providers";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { Languages, languageStore, articleStore, META_LANGUAGE, EVERY_LANGUAGE } from "@stores"; import {
Languages,
languageStore,
articleStore,
META_LANGUAGE,
EVERY_LANGUAGE,
} from "@stores";
import axios from "axios"; import axios from "axios";
import { LanguageSelector, MediaData, MediaView } from "@ui"; import { LanguageSelector, MediaData, MediaView } from "@ui";
import { ArticleEditModal } from "../../components/modals/ArticleEditModal/index"; import { ArticleEditModal } from "../../components/modals/ArticleEditModal/index";
import ReactMarkdown from "react-markdown";
import rehypeRaw from "rehype-raw";
function a11yProps(index: number) { function a11yProps(index: number) {
return { return {
@ -60,10 +68,9 @@ export const SightEdit = observer(() => {
const { setArticleModalOpenAction, setArticleIdAction } = articleStore; const { setArticleModalOpenAction, setArticleIdAction } = articleStore;
const [sightData, setSightData] = useState({ const [sightData, setSightData] = useState({
name: EVERY_LANGUAGE(""), name: EVERY_LANGUAGE(""),
address: EVERY_LANGUAGE("") address: EVERY_LANGUAGE(""),
}); });
const { const {
saveButtonProps, saveButtonProps,
register, register,
@ -76,8 +83,9 @@ export const SightEdit = observer(() => {
formState: { errors }, formState: { errors },
} = useForm({ } = useForm({
refineCoreProps: META_LANGUAGE(language), refineCoreProps: META_LANGUAGE(language),
warnWhenUnsavedChanges: false warnWhenUnsavedChanges: false,
}); });
const name = watch("name");
const getMedia = async (id?: string | number) => { const getMedia = async (id?: string | number) => {
if (!id) return; if (!id) return;
@ -110,7 +118,7 @@ export const SightEdit = observer(() => {
value, value,
}, },
], ],
...META_LANGUAGE("ru") ...META_LANGUAGE("ru"),
}); });
const [mediaFile, setMediaFile] = useState<MediaData>(); const [mediaFile, setMediaFile] = useState<MediaData>();
const [leftArticleData, setLeftArticleData] = useState<{ const [leftArticleData, setLeftArticleData] = useState<{
@ -129,7 +137,7 @@ export const SightEdit = observer(() => {
operator: "contains", operator: "contains",
value, value,
}, },
] ],
}); });
const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({ const { autocompleteProps: articleAutocompleteProps } = useAutocomplete({
@ -147,12 +155,10 @@ export const SightEdit = observer(() => {
value, value,
}, },
], ],
}); });
useEffect(() => { useEffect(() => {
if(sightData.name[language]) if (sightData.name[language]) setValue("name", sightData.name[language]);
setValue("name", sightData.name[language]);
if (sightData.address[language]) if (sightData.address[language])
setValue("address", sightData.address[language]); setValue("address", sightData.address[language]);
}, [language, sightData, setValue]); }, [language, sightData, setValue]);
@ -180,7 +186,8 @@ export const SightEdit = observer(() => {
// Состояния для предпросмотра // Состояния для предпросмотра
const [creatingArticleHeading, setCreatingArticleHeading] = useState<string>(""); const [creatingArticleHeading, setCreatingArticleHeading] =
useState<string>("");
const [creatingArticleBody, setCreatingArticleBody] = useState<string>(""); const [creatingArticleBody, setCreatingArticleBody] = useState<string>("");
const [coordinatesPreview, setCoordinatesPreview] = useState({ const [coordinatesPreview, setCoordinatesPreview] = useState({
@ -188,6 +195,7 @@ export const SightEdit = observer(() => {
longitude: "", longitude: "",
}); });
const [selectedArticleIndex, setSelectedArticleIndex] = useState(-1); const [selectedArticleIndex, setSelectedArticleIndex] = useState(-1);
const [previewMediaFile, setPreviewMediaFile] = useState<MediaData>();
const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null); const [thumbnailPreview, setThumbnailPreview] = useState<string | null>(null);
const [watermarkLUPreview, setWatermarkLUPreview] = useState<string | null>( const [watermarkLUPreview, setWatermarkLUPreview] = useState<string | null>(
null null
@ -202,14 +210,15 @@ export const SightEdit = observer(() => {
const previewMediaId = watch("preview_media"); const previewMediaId = watch("preview_media");
const leftArticleId = watch("left_article"); const leftArticleId = watch("left_article");
useEffect(() => { useEffect(() => {
if (previewMediaId) { if (previewMediaId) {
const selectedMedia = mediaAutocompleteProps.options.find( const selectedMedia = mediaAutocompleteProps.options.find(
(option) => option.id === previewMediaId (option) => option.id === previewMediaId
); );
console.log("Triggering", previewMediaId) console.log("Triggering", previewMediaId);
if (!selectedMedia) return; if (!selectedMedia) return;
setMediaFile(selectedMedia); setPreviewMediaFile(selectedMedia);
} }
}, [previewMediaId, mediaAutocompleteProps.options]); }, [previewMediaId, mediaAutocompleteProps.options]);
@ -245,10 +254,9 @@ export const SightEdit = observer(() => {
getMedia(linkedArticles[selectedArticleIndex].id).then((media) => { getMedia(linkedArticles[selectedArticleIndex].id).then((media) => {
setMediaFile(media); setMediaFile(media);
}); });
}; }
}, [selectedArticleIndex, linkedArticles]); }, [selectedArticleIndex, linkedArticles]);
useEffect(() => { useEffect(() => {
const selectedThumbnail = mediaAutocompleteProps.options.find( const selectedThumbnail = mediaAutocompleteProps.options.find(
(option) => option.id === thumbnailContent (option) => option.id === thumbnailContent
@ -312,8 +320,8 @@ export const SightEdit = observer(() => {
address: { address: {
...sightData.address, ...sightData.address,
[language]: watch("address") ?? "", [language]: watch("address") ?? "",
} },
} };
if (update) setSightData(newSightData); if (update) setSightData(newSightData);
return newSightData; return newSightData;
} }
@ -328,7 +336,7 @@ export const SightEdit = observer(() => {
console.log(newTranslations); console.log(newTranslations);
await onFinish({ await onFinish({
...values, ...values,
translations: newTranslations translations: newTranslations,
}); });
}); });
@ -338,21 +346,29 @@ export const SightEdit = observer(() => {
}; };
}, [setLanguageAction]); }, [setLanguageAction]);
const [articleAdditionMode, setArticleAdditionMode] = useState<'attaching' | 'creating'>('attaching'); const [articleAdditionMode, setArticleAdditionMode] = useState<
"attaching" | "creating"
>("attaching");
const [selectedItemId, setSelectedItemId] = useState<string>(); const [selectedItemId, setSelectedItemId] = useState<string>();
const [updatedLinkedArticles, setUpdatedLinkedArticles] = useState<ArticleItem[]>([]); const [updatedLinkedArticles, setUpdatedLinkedArticles] = useState<
ArticleItem[]
>([]);
const linkItem = () => { const linkItem = () => {
if (!selectedItemId) return; if (!selectedItemId) return;
const requestData = { const requestData = {
article_id: selectedItemId, article_id: selectedItemId,
page_num: linkedArticles.length + 1, page_num: linkedArticles.length + 1,
} };
axiosInstance axiosInstance
.post(`${import.meta.env.VITE_KRBL_API}/sight/${sightId}/article`, requestData) .post(
`${import.meta.env.VITE_KRBL_API}/sight/${sightId}/article`,
requestData
)
.then(() => { .then(() => {
axiosInstance.get(`${import.meta.env.VITE_KRBL_API}/sight/${sightId}/article`) axiosInstance
.get(`${import.meta.env.VITE_KRBL_API}/sight/${sightId}/article`)
.then((response) => { .then((response) => {
setUpdatedLinkedArticles(response?.data || []); setUpdatedLinkedArticles(response?.data || []);
setSelectedItemId(undefined); setSelectedItemId(undefined);
@ -381,7 +397,7 @@ export const SightEdit = observer(() => {
<Edit <Edit
saveButtonProps={{ saveButtonProps={{
...saveButtonProps, ...saveButtonProps,
onClick: handleFormSubmit onClick: handleFormSubmit,
}} }}
footerButtonProps={{ footerButtonProps={{
sx: { sx: {
@ -391,12 +407,26 @@ export const SightEdit = observer(() => {
}} }}
> >
<Box sx={{ display: "flex", flexDirection: "row", gap: 2 }}> <Box sx={{ display: "flex", flexDirection: "row", gap: 2 }}>
<Box sx={{ display: "flex", gap: 2, position: "relative", flex:1 }}> <Box
<Box sx={{display: "flex", flexDirection: "column", flex: 1, gap: 10, justifyContent: "space-between"}}> sx={{ display: "flex", gap: 2, position: "relative", flex: 1 }}
>
<Box
sx={{
display: "flex",
flexDirection: "column",
flex: 1,
gap: 10,
justifyContent: "space-between",
}}
>
<LanguageSelector action={handleLanguageChange} /> <LanguageSelector action={handleLanguageChange} />
{/* Форма редактирования */} {/* Форма редактирования */}
<Box component="form" sx={{ flex: 1, display: "flex", flexDirection: "column" }} autoComplete="off"> <Box
component="form"
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<TextField <TextField
{...register("name", { {...register("name", {
//required: "Это поле является обязательным", //required: "Это поле является обязательным",
@ -410,6 +440,19 @@ export const SightEdit = observer(() => {
label={"Название"} label={"Название"}
name="name" name="name"
/> />
<TextField
{...register("address", {
//required: "Это поле является обязательным",
})}
error={!!(errors as any)?.address}
helperText={(errors as any)?.address?.message}
margin="normal"
fullWidth
slotProps={{ inputLabel: { shrink: true } }}
type="text"
label={"Адрес"}
name="address"
/>
<Box sx={{ display: "none" }}> <Box sx={{ display: "none" }}>
<Controller <Controller
@ -433,14 +476,13 @@ export const SightEdit = observer(() => {
isOptionEqualToValue={(option, value) => { isOptionEqualToValue={(option, value) => {
return option.id === value?.id; return option.id === value?.id;
}} }}
filterOptions={(options, { inputValue }) => { // filterOptions={(options, { inputValue }) => {
return options.filter( // return options.filter((option) =>
(option) => // option.media_name
option.media_name // .toLowerCase()
.toLowerCase() // .includes(inputValue.toLowerCase())
.includes(inputValue.toLowerCase()) // );
); // }}
}}
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
@ -482,7 +524,7 @@ export const SightEdit = observer(() => {
name="city_id" name="city_id"
rules={{ required: "Это поле является обязательным" }} rules={{ required: "Это поле является обязательным" }}
defaultValue={null} defaultValue={null}
render={() => (<div/>)} render={() => <div />}
/> />
<Box sx={{ display: "none" }}> <Box sx={{ display: "none" }}>
@ -685,10 +727,9 @@ export const SightEdit = observer(() => {
variant="h6" variant="h6"
gutterBottom gutterBottom
px={2} px={2}
py={.5} py={0.5}
sx={{ sx={{
color: "text.primary" color: "text.primary",
}} }}
> >
Создать и прикрепить новую статью: Создать и прикрепить новую статью:
@ -767,7 +808,7 @@ export const SightEdit = observer(() => {
mb: 3, mb: 3,
}} }}
> >
{leftArticleId ? leftArticleData?.heading : creatingArticleHeading} {name}
</Typography> </Typography>
{/* Адрес */} {/* Адрес */}
@ -786,18 +827,50 @@ export const SightEdit = observer(() => {
</Box> </Box>
</Typography> </Typography>
{/* Текст статьи */}
<Typography variant="body1" sx={{ mb: 2 }}>
<Box <Box
component="span"
sx={{ sx={{
"& img": {
maxWidth: "100%",
height: "auto",
borderRadius: 1,
},
"& h1, & h2, & h3, & h4, & h5, & h6": {
color: "primary.main",
mt: 2,
mb: 1,
},
"& p": {
mb: 2,
color: (theme) => color: (theme) =>
theme.palette.mode === "dark" ? "grey.300" : "grey.800", theme.palette.mode === "dark" ? "grey.300" : "grey.800",
},
"& a": {
color: "primary.main",
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
},
},
"& blockquote": {
borderLeft: "4px solid",
borderColor: "primary.main",
pl: 2,
my: 2,
color: "text.secondary",
},
"& code": {
bgcolor: (theme) =>
theme.palette.mode === "dark" ? "grey.900" : "grey.100",
p: 0.5,
borderRadius: 0.5,
color: "primary.main",
},
}} }}
> >
<ReactMarkdown rehypePlugins={[rehypeRaw]}>
{leftArticleId ? leftArticleData?.body : creatingArticleBody} {leftArticleId ? leftArticleData?.body : creatingArticleBody}
</ReactMarkdown>
</Box> </Box>
</Typography>
</Paper> </Paper>
</Box> </Box>
</Edit> </Edit>
@ -807,15 +880,19 @@ export const SightEdit = observer(() => {
<Edit <Edit
saveButtonProps={{ saveButtonProps={{
...saveButtonProps, ...saveButtonProps,
onClick: handleFormSubmit onClick: handleFormSubmit,
}} }}
footerButtonProps={{ footerButtonProps={{
sx: { bottom: 0, left: 0 }, sx: { bottom: 0, left: 0 },
}}> }}
>
<Box sx={{ display: "flex", flexDirection: "row", gap: 2 }}> <Box sx={{ display: "flex", flexDirection: "row", gap: 2 }}>
<Box sx={{ flex: 1, gap: 2, position: "relative" }}> <Box sx={{ flex: 1, gap: 2, position: "relative" }}>
<Box component="form" sx={{ flex: 1, display: "flex", flexDirection: "column" }} autoComplete="off"> <Box
component="form"
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<Controller <Controller
control={control} control={control}
name="preview_media" name="preview_media"
@ -829,7 +906,7 @@ export const SightEdit = observer(() => {
) || null ) || null
} }
onChange={(_, value) => { onChange={(_, value) => {
console.log(value, _) console.log(value, _);
field.onChange(value?.id || ""); field.onChange(value?.id || "");
}} }}
getOptionLabel={(item) => { getOptionLabel={(item) => {
@ -839,19 +916,17 @@ export const SightEdit = observer(() => {
return option.id === value?.id; return option.id === value?.id;
}} }}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
return options.filter( return options.filter((option) =>
(option) =>
option.media_name option.media_name
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) && .includes(inputValue.toLowerCase())
[1,2,5,6].includes(option.media_type)
); );
}} }}
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
onClick={() => { onClick={() => {
//setPreviewSelected(true); setPreviewSelected(true);
//setSelectedMediaIndex(-1); //setSelectedMediaIndex(-1);
}} }}
label="Медиа-предпросмотр" label="Медиа-предпросмотр"
@ -875,8 +950,14 @@ export const SightEdit = observer(() => {
flex: 1, flex: 1,
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
bgcolor: articleAdditionMode === "attaching" ? "primary.main" : "transparent", bgcolor:
color: articleAdditionMode === "attaching" ? "white" : "inherit", articleAdditionMode === "attaching"
? "primary.main"
: "transparent",
color:
articleAdditionMode === "attaching"
? "white"
: "inherit",
borderRadius: 1, borderRadius: 1,
p: 1, p: 1,
}} }}
@ -890,8 +971,14 @@ export const SightEdit = observer(() => {
flex: 1, flex: 1,
display: "flex", display: "flex",
justifyContent: "center", justifyContent: "center",
bgcolor: articleAdditionMode === "creating" ? "primary.main" : "transparent", bgcolor:
color: articleAdditionMode === "creating" ? "white" : "inherit", articleAdditionMode === "creating"
? "primary.main"
: "transparent",
color:
articleAdditionMode === "creating"
? "white"
: "inherit",
borderRadius: 1, borderRadius: 1,
p: 1, p: 1,
}} }}
@ -947,7 +1034,10 @@ export const SightEdit = observer(() => {
> >
Добавить Добавить
</Button> </Button>
<Button variant="outlined" onClick={() => setSelectedItemId(undefined)}> <Button
variant="outlined"
onClick={() => setSelectedItemId(undefined)}
>
Очистить Очистить
</Button> </Button>
</Box> </Box>
@ -962,7 +1052,7 @@ export const SightEdit = observer(() => {
title="статью" title="статью"
//left //left
setHeadingParent={(heading) => { setHeadingParent={(heading) => {
console.log("Updating", heading) console.log("Updating", heading);
setCreatingArticleHeading(heading); setCreatingArticleHeading(heading);
}} }}
setBodyParent={(body) => { setBodyParent={(body) => {
@ -970,7 +1060,11 @@ export const SightEdit = observer(() => {
}} }}
/> />
)} )}
<Typography variant="subtitle1" fontWeight="bold" sx={{mt: 4}}> <Typography
variant="subtitle1"
fontWeight="bold"
sx={{ mt: 4 }}
>
Привязанные статьи Привязанные статьи
</Typography> </Typography>
@ -986,7 +1080,6 @@ export const SightEdit = observer(() => {
title="статьи" title="статьи"
updatedLinkedItems={updatedLinkedArticles} updatedLinkedItems={updatedLinkedArticles}
/> />
</Box> </Box>
</Box> </Box>
{/* Предпросмотр */} {/* Предпросмотр */}
@ -1016,7 +1109,6 @@ export const SightEdit = observer(() => {
flexGrow: 1, flexGrow: 1,
}} }}
> >
<Box <Box
sx={{ sx={{
mb: 2, mb: 2,
@ -1027,7 +1119,10 @@ export const SightEdit = observer(() => {
gap: 2, gap: 2,
}} }}
> >
{mediaFile && ( {previewSelected && previewMediaFile && (
<MediaView media={previewMediaFile} />
)}
{mediaFile && !previewSelected && (
<MediaView media={mediaFile} /> <MediaView media={mediaFile} />
)} )}
</Box> </Box>
@ -1045,7 +1140,6 @@ export const SightEdit = observer(() => {
overflowY: "auto", overflowY: "auto",
}} }}
> >
{!previewSelected && articleAdditionMode !== "creating" && ( {!previewSelected && articleAdditionMode !== "creating" && (
<Box <Box
sx={{ sx={{
@ -1060,14 +1154,13 @@ export const SightEdit = observer(() => {
variant="h4" variant="h4"
gutterBottom gutterBottom
px={2} px={2}
py={.5} py={0.5}
sx={{ sx={{
color: "text.primary", color: "text.primary",
background: background:
"linear-gradient(180deg, hsla(0,0%,100%,.2), hsla(0,0%,100%,0)), hsla(29,15%,65%,.4)", "linear-gradient(180deg, hsla(0,0%,100%,.2), hsla(0,0%,100%,0)), hsla(29,15%,65%,.4)",
boxShadow: "inset 4px 4px 12px hsla(0,0%,100%,.12)", boxShadow:
"inset 4px 4px 12px hsla(0,0%,100%,.12)",
}} }}
> >
{selectedArticle.heading} {selectedArticle.heading}
@ -1075,14 +1168,56 @@ export const SightEdit = observer(() => {
)} )}
{selectedArticle && ( {selectedArticle && (
<Typography <Box
variant="body1" sx={{
gutterBottom mt: -6,
px={2} p: 2,
sx={{ color: "text.primary" }} "& img": {
maxWidth: "100%",
height: "auto",
borderRadius: 1,
},
"& h1, & h2, & h3, & h4, & h5, & h6": {
color: "primary.main",
mt: 2,
mb: 1,
},
"& p": {
mb: 2,
color: (theme) =>
theme.palette.mode === "dark"
? "grey.300"
: "grey.800",
},
"& a": {
color: "primary.main",
textDecoration: "none",
"&:hover": {
textDecoration: "underline",
},
},
"& blockquote": {
borderLeft: "4px solid",
borderColor: "primary.main",
pl: 2,
my: 2,
color: "text.secondary",
},
"& code": {
bgcolor: (theme) =>
theme.palette.mode === "dark"
? "grey.900"
: "grey.100",
p: 0.5,
borderRadius: 0.5,
color: "primary.main",
},
}}
> >
<ReactMarkdown rehypePlugins={[rehypeRaw]}>
{selectedArticle.body} {selectedArticle.body}
</Typography> </ReactMarkdown>
</Box>
)} )}
</Box> </Box>
)} )}
@ -1100,14 +1235,12 @@ export const SightEdit = observer(() => {
variant="h4" variant="h4"
gutterBottom gutterBottom
px={2} px={2}
py={.5} py={0.5}
sx={{ sx={{
color: "text.primary", color: "text.primary",
background: background:
"linear-gradient(180deg, hsla(0,0%,100%,.2), hsla(0,0%,100%,0)), hsla(29,15%,65%,.4)", "linear-gradient(180deg, hsla(0,0%,100%,.2), hsla(0,0%,100%,0)), hsla(29,15%,65%,.4)",
boxShadow: "inset 4px 4px 12px hsla(0,0%,100%,.12)", boxShadow: "inset 4px 4px 12px hsla(0,0%,100%,.12)",
}} }}
> >
{creatingArticleHeading} {creatingArticleHeading}
@ -1133,7 +1266,6 @@ export const SightEdit = observer(() => {
background: background:
"linear-gradient(180deg, hsla(0,0%,100%,.2), hsla(0,0%,100%,0)), hsla(29,15%,65%,.4)", "linear-gradient(180deg, hsla(0,0%,100%,.2), hsla(0,0%,100%,0)), hsla(29,15%,65%,.4)",
boxShadow: "inset 4px 4px 12px hsla(0,0%,100%,.12)", boxShadow: "inset 4px 4px 12px hsla(0,0%,100%,.12)",
}} }}
> >
<Box <Box
@ -1156,8 +1288,9 @@ export const SightEdit = observer(() => {
bgcolor: "transparent", bgcolor: "transparent",
color: "inherit", color: "inherit",
textDecoration: textDecoration:
selectedArticleIndex === index ? selectedArticleIndex === index
"underline" : "none", ? "underline"
: "none",
p: 1, p: 1,
borderRadius: 1, borderRadius: 1,
}} }}
@ -1184,22 +1317,15 @@ export const SightEdit = observer(() => {
footerButtonProps={{ sx: { bottom: 0, left: 0 } }} footerButtonProps={{ sx: { bottom: 0, left: 0 } }}
> >
<Box sx={{ display: "flex", flexDirection: "row", gap: 2 }}> <Box sx={{ display: "flex", flexDirection: "row", gap: 2 }}>
<Box sx={{ display: "flex", flex: 1, gap: 2, position: "relative" }}> <Box
<Box component="form" sx={{ flex: 1, display: "flex", flexDirection: "column" }} autoComplete="off"> sx={{ display: "flex", flex: 1, gap: 2, position: "relative" }}
>
<Box
component="form"
sx={{ flex: 1, display: "flex", flexDirection: "column" }}
autoComplete="off"
>
<LanguageSelector action={handleLanguageChange} /> <LanguageSelector action={handleLanguageChange} />
<TextField
{...register("address", {
//required: "Это поле является обязательным",
})}
error={!!(errors as any)?.address}
helperText={(errors as any)?.address?.message}
margin="normal"
fullWidth
slotProps={{inputLabel: {shrink: true}}}
type="text"
label={"Адрес"}
name="address"
/>
<Controller <Controller
control={control} control={control}
@ -1221,7 +1347,7 @@ export const SightEdit = observer(() => {
return item ? item.name : ""; return item ? item.name : "";
}} }}
isOptionEqualToValue={(option, value) => { isOptionEqualToValue={(option, value) => {
console.log(cityAutocompleteProps.options) console.log(cityAutocompleteProps.options);
return option.id === value?.id; return option.id === value?.id;
}} }}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
@ -1425,7 +1551,9 @@ export const SightEdit = observer(() => {
component="span" component="span"
sx={{ sx={{
color: (theme) => color: (theme) =>
theme.palette.mode === "dark" ? "grey.300" : "grey.800", theme.palette.mode === "dark"
? "grey.300"
: "grey.800",
}} }}
> >
{`${addressContent}`} {`${addressContent}`}
@ -1523,7 +1651,9 @@ export const SightEdit = observer(() => {
component="span" component="span"
sx={{ sx={{
color: (theme) => color: (theme) =>
theme.palette.mode === "dark" ? "grey.300" : "grey.800", theme.palette.mode === "dark"
? "grey.300"
: "grey.800",
}} }}
> >
{`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`} {`${coordinatesPreview.latitude}, ${coordinatesPreview.longitude}`}

View File

@ -12,8 +12,14 @@ import { CustomDataGrid } from "@components";
import { localeText } from "../../locales/ru/localeText"; import { localeText } from "../../locales/ru/localeText";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { useMany } from "@refinedev/core"; import { useMany } from "@refinedev/core";
import { DatabaseBackup } from "lucide-react";
import axios from "axios";
import { TOKEN_KEY } from "../../providers/authProvider";
import { toast } from "react-toastify";
import { useNotification } from "@refinedev/core";
export const SnapshotList = observer(() => { export const SnapshotList = observer(() => {
const notification = useNotification();
const { dataGridProps } = useDataGrid({ const { dataGridProps } = useDataGrid({
resource: "snapshots", resource: "snapshots",
hasPagination: false, hasPagination: false,
@ -47,6 +53,30 @@ export const SnapshotList = observer(() => {
return map; return map;
}, [parentsData]); }, [parentsData]);
const handleBackup = async (id: number) => {
try {
const response = await axios.post(
`${import.meta.env.VITE_KRBL_API}/snapshots/${id}/restore`,
{},
{
headers: {
Authorization: `Bearer ${localStorage.getItem(TOKEN_KEY)}`,
},
}
);
notification?.open({
message: "Cнапшот восстановлен",
type: "success",
});
} catch (error) {
notification?.open({
message: "Ошибка при восстановлении снимка",
type: "error",
});
}
};
const columns = React.useMemo<GridColDef[]>( const columns = React.useMemo<GridColDef[]>(
() => [ () => [
{ {
@ -80,6 +110,12 @@ export const SnapshotList = observer(() => {
renderCell: function render({ row }) { renderCell: function render({ row }) {
return ( return (
<> <>
<button
className="backup-button"
onClick={() => handleBackup(row.ID)}
>
<DatabaseBackup />
</button>
<ShowButton hideText recordItemId={row.ID} /> <ShowButton hideText recordItemId={row.ID} />
<DeleteButton <DeleteButton
hideText hideText

View File

@ -19,8 +19,6 @@ axiosInstance.interceptors.request.use((config) => {
config.headers["X-Language"] = config.headers["Accept-Language"]; config.headers["X-Language"] = config.headers["Accept-Language"];
console.log("Request headers:", config.headers);
return config; return config;
}); });

View File

@ -5703,6 +5703,13 @@ react-swipeable@^7.0.2:
resolved "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.2.tgz" resolved "https://registry.npmjs.org/react-swipeable/-/react-swipeable-7.0.2.tgz"
integrity sha512-v1Qx1l+aC2fdxKa9aKJiaU/ZxmJ5o98RMoFwUqAAzVWUcxgfHFXDDruCKXhw6zIYXm6V64JiHgP9f6mlME5l8w== integrity sha512-v1Qx1l+aC2fdxKa9aKJiaU/ZxmJ5o98RMoFwUqAAzVWUcxgfHFXDDruCKXhw6zIYXm6V64JiHgP9f6mlME5l8w==
react-toastify@^11.0.5:
version "11.0.5"
resolved "https://registry.yarnpkg.com/react-toastify/-/react-toastify-11.0.5.tgz#ce4c42d10eeb433988ab2264d3e445c4e9d13313"
integrity sha512-EpqHBGvnSTtHYhCPLxML05NLY2ZX0JURbAdNYa6BUkk+amz4wbKBQvoKQAB0ardvSarUBuY4Q4s1sluAzZwkmA==
dependencies:
clsx "^2.1.1"
react-transition-group@^4.4.5: react-transition-group@^4.4.5:
version "4.4.5" version "4.4.5"
resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz" resolved "https://registry.npmjs.org/react-transition-group/-/react-transition-group-4.4.5.tgz"