fix: Update map with tables fixes
This commit is contained in:
@ -3,21 +3,25 @@ import { Button } from "@mui/material";
|
||||
export const DeleteModal = ({
|
||||
onDelete,
|
||||
onCancel,
|
||||
edit = false,
|
||||
open,
|
||||
}: {
|
||||
onDelete: () => void;
|
||||
onCancel: () => void;
|
||||
edit?: boolean;
|
||||
open: boolean;
|
||||
}) => {
|
||||
return (
|
||||
<div
|
||||
className={`fixed top-0 left-0 w-screen h-screen flex justify-center items-center z-10000 bg-black/30 transition-all duration-300 ${
|
||||
className={`fixed top-0 left-0 w-screen h-screen flex justify-center items-center z-1000000000000 bg-black/30 transition-all duration-300 ${
|
||||
open ? "block" : "hidden"
|
||||
}`}
|
||||
>
|
||||
<div className="bg-white p-4 w-100 rounded-lg flex flex-col gap-4 items-center">
|
||||
<p className="text-black w-100 text-center">
|
||||
Вы уверены, что хотите удалить этот элемент?
|
||||
{`Вы уверены, что хотите ${
|
||||
edit ? "убрать" : "удалить"
|
||||
} этот элемент?`}
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center">
|
||||
<Button variant="contained" color="error" onClick={onDelete}>
|
||||
|
@ -18,6 +18,7 @@ import { observer } from "mobx-react-lite";
|
||||
import { Button, Checkbox, Typography } from "@mui/material";
|
||||
import { Vehicle } from "@shared";
|
||||
import { toast } from "react-toastify";
|
||||
import { useNavigate } from "react-router-dom";
|
||||
|
||||
export type ConnectedDevice = string;
|
||||
|
||||
@ -116,6 +117,7 @@ export const DevicesTable = observer(() => {
|
||||
const { snapshots, getSnapshots } = snapshotStore;
|
||||
const { getVehicles, vehicles } = vehicleStore; // Removed as devicesStore.devices should be the source of truth
|
||||
const { devices } = devicesStore;
|
||||
const navigate = useNavigate();
|
||||
const [selectedDeviceUuids, setSelectedDeviceUuids] = useState<string[]>([]);
|
||||
|
||||
// Transform the raw devices data into rows suitable for the table
|
||||
@ -182,13 +184,25 @@ export const DevicesTable = observer(() => {
|
||||
const handleSendSnapshotAction = async (snapshotId: string) => {
|
||||
if (selectedDeviceUuids.length === 0) return;
|
||||
|
||||
const send = async (deviceUuid: string) => {
|
||||
try {
|
||||
await authInstance.post(
|
||||
`/devices/${deviceUuid}/force-snapshot-update`,
|
||||
{
|
||||
snapshot_id: snapshotId,
|
||||
}
|
||||
);
|
||||
toast.success(`Снапшот отправлен на устройство `);
|
||||
} catch (error) {
|
||||
console.error(`Error sending snapshot to device ${deviceUuid}:`, error);
|
||||
toast.error(`Не удалось отправить снапшот на устройство`);
|
||||
}
|
||||
};
|
||||
try {
|
||||
// Create an array of promises for all snapshot requests
|
||||
const snapshotPromises = selectedDeviceUuids.map((deviceUuid) => {
|
||||
console.log(`Sending snapshot ${snapshotId} to device ${deviceUuid}`);
|
||||
return authInstance.post(`/devices/${deviceUuid}/force-snapshot`, {
|
||||
snapshot_id: snapshotId,
|
||||
});
|
||||
return send(deviceUuid);
|
||||
});
|
||||
|
||||
// Wait for all promises to settle (either resolve or reject)
|
||||
@ -209,6 +223,16 @@ export const DevicesTable = observer(() => {
|
||||
return (
|
||||
<>
|
||||
<TableContainer component={Paper} sx={{ mt: 2 }}>
|
||||
<div className="flex justify-end p-3 gap-2 ">
|
||||
<Button
|
||||
variant="contained"
|
||||
color="primary"
|
||||
size="small"
|
||||
onClick={() => navigate("/vehicle/create")}
|
||||
>
|
||||
Добавить устройство
|
||||
</Button>
|
||||
</div>
|
||||
<div className="flex justify-end p-3 gap-2">
|
||||
<Button
|
||||
variant="outlined" // Changed to outlined for distinction
|
||||
@ -269,16 +293,34 @@ export const DevicesTable = observer(() => {
|
||||
'input[type="checkbox"]'
|
||||
) === null
|
||||
) {
|
||||
handleSelectDevice(
|
||||
{
|
||||
target: {
|
||||
checked: !selectedDeviceUuids.includes(
|
||||
row.device_uuid ?? ""
|
||||
),
|
||||
},
|
||||
} as React.ChangeEvent<HTMLInputElement>, // Simulate event
|
||||
row.device_uuid ?? ""
|
||||
);
|
||||
if (event.shiftKey) {
|
||||
if (row.device_uuid) {
|
||||
navigator.clipboard
|
||||
.writeText(row.device_uuid)
|
||||
.then(() => {
|
||||
toast.success(`UUID скопирован`);
|
||||
})
|
||||
.catch(() => {
|
||||
toast.error("Не удалось скопировать UUID");
|
||||
});
|
||||
} else {
|
||||
toast.warning("Устройство не имеет UUID");
|
||||
}
|
||||
}
|
||||
|
||||
// Only toggle checkbox if Shift key is not pressed
|
||||
if (!event.shiftKey) {
|
||||
handleSelectDevice(
|
||||
{
|
||||
target: {
|
||||
checked: !selectedDeviceUuids.includes(
|
||||
row.device_uuid ?? ""
|
||||
),
|
||||
},
|
||||
} as React.ChangeEvent<HTMLInputElement>, // Simulate event
|
||||
row.device_uuid ?? ""
|
||||
);
|
||||
}
|
||||
}
|
||||
}}
|
||||
sx={{
|
||||
|
@ -46,10 +46,16 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
||||
) => {
|
||||
const file = event.target.files?.[0];
|
||||
if (file) {
|
||||
setFileToUpload(file);
|
||||
setUploadMediaOpen(true);
|
||||
if (imageKey && setHardcodeType) {
|
||||
setHardcodeType(imageKey);
|
||||
if (file.type.startsWith("image/") && file.type !== "image/gif") {
|
||||
setFileToUpload(file);
|
||||
setUploadMediaOpen(true);
|
||||
if (imageKey && setHardcodeType) {
|
||||
setHardcodeType(imageKey);
|
||||
}
|
||||
} else if (file.type === "image/gif") {
|
||||
toast.error("GIF файлы не поддерживаются");
|
||||
} else {
|
||||
toast.error("Пожалуйста, выберите изображение");
|
||||
}
|
||||
}
|
||||
// Reset the input value so selecting the same file again triggers change
|
||||
@ -78,9 +84,11 @@ export const ImageUploadCard: React.FC<ImageUploadCardProps> = ({
|
||||
const files = event.dataTransfer.files;
|
||||
if (files && files.length > 0) {
|
||||
const file = files[0];
|
||||
if (file.type.startsWith("image/")) {
|
||||
if (file.type.startsWith("image/") && file.type !== "image/gif") {
|
||||
setFileToUpload(file);
|
||||
setUploadMediaOpen(true);
|
||||
} else if (file.type === "image/gif") {
|
||||
toast.error("GIF файлы не поддерживаются");
|
||||
} else {
|
||||
toast.error("Пожалуйста, выберите изображение");
|
||||
}
|
||||
|
@ -44,14 +44,14 @@ export const LanguageSwitcher = observer(() => {
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="fixed bottom-0 left-1/2 -translate-x-1/2 flex gap-2 p-4 z-100000000">
|
||||
<div className="fixed top-0 left-1/2 -translate-x-1/2 flex gap-2 p-4 z-100000">
|
||||
{/* Added some styling for better visualization */}
|
||||
{LANGUAGES.map((lang) => (
|
||||
<Button
|
||||
key={lang}
|
||||
onClick={() => handleLanguageChange(lang)}
|
||||
variant={language === lang ? "contained" : "outlined"} // Highlight the active language
|
||||
color="primary"
|
||||
variant={"contained"} // Highlight the active language
|
||||
color={language === lang ? "primary" : "secondary"}
|
||||
sx={{ minWidth: "60px" }} // Give buttons a consistent width
|
||||
>
|
||||
{getLanguageLabel(lang)}
|
||||
|
@ -2,20 +2,32 @@ import * as React from "react";
|
||||
import Box from "@mui/material/Box";
|
||||
import Toolbar from "@mui/material/Toolbar";
|
||||
import IconButton from "@mui/material/IconButton";
|
||||
import { Menu, ChevronLeftIcon, ChevronRightIcon } from "lucide-react";
|
||||
import { Menu, ChevronLeftIcon, ChevronRightIcon, User } from "lucide-react";
|
||||
import { useTheme } from "@mui/material/styles";
|
||||
import { AppBar } from "./ui/AppBar";
|
||||
import { Drawer } from "./ui/Drawer";
|
||||
import { DrawerHeader } from "./ui/DrawerHeader";
|
||||
import { NavigationList } from "@features";
|
||||
import { authStore, userStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect } from "react";
|
||||
import { Typography } from "@mui/material";
|
||||
|
||||
interface LayoutProps {
|
||||
children: React.ReactNode;
|
||||
}
|
||||
|
||||
export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
export const Layout: React.FC<LayoutProps> = observer(({ children }) => {
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = React.useState(false);
|
||||
const [open, setOpen] = React.useState(true);
|
||||
const { getUsers, users } = userStore;
|
||||
|
||||
useEffect(() => {
|
||||
const fetchUsers = async () => {
|
||||
await getUsers();
|
||||
};
|
||||
fetchUsers();
|
||||
}, []);
|
||||
|
||||
const handleDrawerOpen = () => {
|
||||
setOpen(true);
|
||||
@ -28,7 +40,7 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
return (
|
||||
<Box sx={{ display: "flex" }}>
|
||||
<AppBar position="fixed" open={open}>
|
||||
<Toolbar>
|
||||
<Toolbar className="flex justify-between">
|
||||
<IconButton
|
||||
color="inherit"
|
||||
aria-label="open drawer"
|
||||
@ -43,17 +55,78 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
>
|
||||
<Menu />
|
||||
</IconButton>
|
||||
<div></div>
|
||||
<div className="flex gap-2 items-center">
|
||||
<div className="flex flex-col gap-1">
|
||||
{(() => {
|
||||
console.log(authStore.payload);
|
||||
return (
|
||||
<>
|
||||
<p className=" text-white">
|
||||
{
|
||||
users?.data?.find(
|
||||
// @ts-ignore
|
||||
(user) => user.id === authStore.payload?.user_id
|
||||
)?.name
|
||||
}
|
||||
</p>
|
||||
<div
|
||||
className="text-center text-xs"
|
||||
style={{
|
||||
backgroundColor: "#877361",
|
||||
borderRadius: "4px",
|
||||
color: "white",
|
||||
padding: "2px 10px",
|
||||
}}
|
||||
>
|
||||
{/* @ts-ignore */}
|
||||
{authStore.payload?.is_admin
|
||||
? "Администратор"
|
||||
: "Режим пользователя"}
|
||||
</div>
|
||||
</>
|
||||
);
|
||||
})()}
|
||||
</div>
|
||||
<div className="w-10 h-10 bg-gray-600 rounded-full flex items-center justify-center">
|
||||
<User />
|
||||
</div>
|
||||
</div>
|
||||
</Toolbar>
|
||||
</AppBar>
|
||||
<Drawer variant="permanent" open={open}>
|
||||
<DrawerHeader>
|
||||
<IconButton onClick={handleDrawerClose}>
|
||||
{theme.direction === "rtl" ? (
|
||||
<ChevronRightIcon />
|
||||
) : (
|
||||
<ChevronLeftIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
<Box
|
||||
sx={{
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
gap: 2,
|
||||
cursor: "pointer",
|
||||
}}
|
||||
onClick={() => {
|
||||
setOpen(!open);
|
||||
}}
|
||||
>
|
||||
<img
|
||||
src="/favicon_ship.png"
|
||||
alt="logo"
|
||||
width={40}
|
||||
height={40}
|
||||
style={{ filter: "brightness(0)", marginLeft: "-5px" }}
|
||||
/>
|
||||
<Typography variant="h6" component="h1">
|
||||
Белые ночи
|
||||
</Typography>
|
||||
</Box>
|
||||
{open && (
|
||||
<IconButton onClick={handleDrawerClose}>
|
||||
{theme.direction === "rtl" ? (
|
||||
<ChevronRightIcon />
|
||||
) : (
|
||||
<ChevronLeftIcon />
|
||||
)}
|
||||
</IconButton>
|
||||
)}
|
||||
</DrawerHeader>
|
||||
<NavigationList open={open} onDrawerOpen={handleDrawerOpen} />
|
||||
</Drawer>
|
||||
@ -67,10 +140,9 @@ export const Layout: React.FC<LayoutProps> = ({ children }) => {
|
||||
maxWidth: "100vw",
|
||||
}}
|
||||
>
|
||||
<DrawerHeader />
|
||||
|
||||
<div className="mt-16"></div>
|
||||
{children}
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
};
|
||||
});
|
||||
|
@ -24,4 +24,12 @@ export const AppBar = styled(MuiAppBar, {
|
||||
duration: theme.transitions.duration.enteringScreen,
|
||||
}),
|
||||
}),
|
||||
...(!open && {
|
||||
marginLeft: theme.spacing(7),
|
||||
width: `calc(100% - ${theme.spacing(7)})`,
|
||||
[theme.breakpoints.up("sm")]: {
|
||||
marginLeft: theme.spacing(8),
|
||||
width: `calc(100% - ${theme.spacing(8)})`,
|
||||
},
|
||||
}),
|
||||
}));
|
||||
|
@ -1,10 +1,12 @@
|
||||
import { styled } from "@mui/material/styles";
|
||||
import type { Theme } from "@mui/material/styles";
|
||||
import Box from "@mui/material/Box";
|
||||
|
||||
export const DrawerHeader = styled("div")(({ theme }: { theme: Theme }) => ({
|
||||
export const DrawerHeader = styled(Box)(({ theme }: { theme: Theme }) => ({
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "flex-end",
|
||||
padding: theme.spacing(0, 1),
|
||||
justifyContent: "space-between",
|
||||
padding: theme.spacing(2),
|
||||
...theme.mixins.toolbar,
|
||||
borderBottom: "1px solid rgba(0, 0, 0, 0.12)",
|
||||
}));
|
||||
|
@ -8,9 +8,23 @@ export const MediaAreaForSight = observer(
|
||||
({
|
||||
onFilesDrop, // 👈 Проп для обработки загруженных файлов
|
||||
onFinishUpload,
|
||||
contextObjectName,
|
||||
contextType,
|
||||
isArticle,
|
||||
articleName,
|
||||
}: {
|
||||
onFilesDrop?: (files: File[]) => void;
|
||||
onFinishUpload?: (mediaId: string) => void;
|
||||
contextObjectName?: string;
|
||||
contextType?:
|
||||
| "sight"
|
||||
| "city"
|
||||
| "carrier"
|
||||
| "country"
|
||||
| "vehicle"
|
||||
| "station";
|
||||
isArticle?: boolean;
|
||||
articleName?: string;
|
||||
}) => {
|
||||
const [selectMediaDialogOpen, setSelectMediaDialogOpen] = useState(false);
|
||||
const [uploadMediaDialogOpen, setUploadMediaDialogOpen] = useState(false);
|
||||
@ -94,6 +108,10 @@ export const MediaAreaForSight = observer(
|
||||
<UploadMediaDialog
|
||||
open={uploadMediaDialogOpen}
|
||||
onClose={() => setUploadMediaDialogOpen(false)}
|
||||
contextObjectName={contextObjectName}
|
||||
contextType={contextType}
|
||||
isArticle={isArticle}
|
||||
articleName={articleName}
|
||||
afterUploadSight={onFinishUpload}
|
||||
/>
|
||||
<SelectMediaDialog
|
||||
|
@ -36,6 +36,11 @@ export function MediaViewer({
|
||||
style={{
|
||||
height: fullHeight ? "100%" : "auto",
|
||||
width: fullWidth ? "100%" : "auto",
|
||||
...(media?.filename?.toLowerCase().endsWith(".webp") && {
|
||||
maxWidth: "300px",
|
||||
maxHeight: "300px",
|
||||
objectFit: "contain",
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
|
@ -5,7 +5,7 @@ import rehypeRaw from "rehype-raw";
|
||||
export const ReactMarkdownComponent = ({ value }: { value: string }) => {
|
||||
return (
|
||||
<Box
|
||||
className="prose prose-sm prose-invert"
|
||||
className="prose prose-sm prose-invert w-full"
|
||||
sx={{
|
||||
"& img": {
|
||||
maxWidth: "100%",
|
||||
|
@ -38,6 +38,12 @@ const StyledMarkdownEditor = styled("div")(({ theme }) => ({
|
||||
maxHeight: "500px",
|
||||
overflowY: "auto",
|
||||
overflowX: "hidden",
|
||||
"&::-webkit-scrollbar": {
|
||||
display: "none",
|
||||
},
|
||||
"&": {
|
||||
scrollbarWidth: "none",
|
||||
},
|
||||
wordBreak: "break-word", // ✅ добавлено
|
||||
},
|
||||
"& .CodeMirror-selected": {
|
||||
|
@ -31,7 +31,7 @@ import { toast } from "react-toastify";
|
||||
|
||||
export const CreateInformationTab = observer(
|
||||
({ value, index }: { value: number; index: number }) => {
|
||||
const { ruCities } = cityStore;
|
||||
const { cities } = cityStore;
|
||||
const [mediaId, setMediaId] = useState<string>("");
|
||||
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||
@ -175,10 +175,11 @@ export const CreateInformationTab = observer(
|
||||
/>
|
||||
|
||||
<Autocomplete
|
||||
options={ruCities.data ?? []}
|
||||
options={cities["ru"]?.data ?? []}
|
||||
value={
|
||||
ruCities.data.find((city) => city.id === sight.city_id) ??
|
||||
null
|
||||
cities["ru"]?.data?.find(
|
||||
(city) => city.id === sight.city_id
|
||||
) ?? null
|
||||
}
|
||||
getOptionLabel={(option) => option.name}
|
||||
onChange={(_, value) => {
|
||||
@ -246,7 +247,7 @@ export const CreateInformationTab = observer(
|
||||
}}
|
||||
>
|
||||
<ImageUploadCard
|
||||
title="Логотип"
|
||||
title="Иконка"
|
||||
imageKey="thumbnail"
|
||||
imageUrl={sight.thumbnail}
|
||||
onImageClick={() => {
|
||||
@ -266,11 +267,10 @@ export const CreateInformationTab = observer(
|
||||
setUploadMediaOpen={() => {
|
||||
setIsUploadMediaOpen(true);
|
||||
setActiveMenuType("thumbnail");
|
||||
setHardcodeType("thumbnail");
|
||||
}}
|
||||
setHardcodeType={(type) => {
|
||||
setHardcodeType(
|
||||
type as "thumbnail" | "watermark_lu" | "watermark_rd"
|
||||
);
|
||||
setHardcodeType={() => {
|
||||
setHardcodeType("thumbnail");
|
||||
}}
|
||||
/>
|
||||
|
||||
@ -295,16 +295,15 @@ export const CreateInformationTab = observer(
|
||||
setUploadMediaOpen={() => {
|
||||
setIsUploadMediaOpen(true);
|
||||
setActiveMenuType("watermark_lu");
|
||||
setHardcodeType("watermark_lu");
|
||||
}}
|
||||
setHardcodeType={(type) => {
|
||||
setHardcodeType(
|
||||
type as "thumbnail" | "watermark_lu" | "watermark_rd"
|
||||
);
|
||||
setHardcodeType={() => {
|
||||
setHardcodeType("watermark_lu");
|
||||
}}
|
||||
/>
|
||||
|
||||
<ImageUploadCard
|
||||
title="Водяной знак (правый нижний)"
|
||||
title="Водяной знак (правый верхний)"
|
||||
imageKey="watermark_rd"
|
||||
imageUrl={sight.watermark_rd}
|
||||
onImageClick={() => {
|
||||
@ -324,11 +323,10 @@ export const CreateInformationTab = observer(
|
||||
setUploadMediaOpen={() => {
|
||||
setIsUploadMediaOpen(true);
|
||||
setActiveMenuType("watermark_rd");
|
||||
setHardcodeType("watermark_rd");
|
||||
}}
|
||||
setHardcodeType={(type) => {
|
||||
setHardcodeType(
|
||||
type as "thumbnail" | "watermark_lu" | "watermark_rd"
|
||||
);
|
||||
setHardcodeType={() => {
|
||||
setHardcodeType("watermark_rd");
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@ -412,6 +410,8 @@ export const CreateInformationTab = observer(
|
||||
<UploadMediaDialog
|
||||
open={isUploadMediaOpen}
|
||||
onClose={() => setIsUploadMediaOpen(false)}
|
||||
contextObjectName={sight[language].name}
|
||||
contextType="sight"
|
||||
afterUpload={(media) => {
|
||||
handleChange({
|
||||
[activeMenuType ?? "thumbnail"]: media.id,
|
||||
|
@ -44,7 +44,7 @@ export const CreateLeftTab = observer(
|
||||
} = editSightStore;
|
||||
|
||||
const { language } = languageStore;
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
|
||||
useState(false);
|
||||
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
|
||||
@ -326,6 +326,7 @@ export const CreateLeftTab = observer(
|
||||
<Box
|
||||
sx={{
|
||||
overflow: "hidden",
|
||||
position: "relative",
|
||||
width: "100%",
|
||||
minHeight: 100,
|
||||
padding: "3px",
|
||||
@ -343,15 +344,45 @@ export const CreateLeftTab = observer(
|
||||
}}
|
||||
>
|
||||
{sight[language].left.media.length > 0 ? (
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: sight[language].left.media[0].id,
|
||||
media_type:
|
||||
sight[language].left.media[0].media_type,
|
||||
filename: sight[language].left.media[0].filename,
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<>
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: sight[language].left.media[0].id,
|
||||
media_type:
|
||||
sight[language].left.media[0].media_type,
|
||||
filename: sight[language].left.media[0].filename,
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
{sight.watermark_lu && (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
sight.watermark_lu
|
||||
}/download?token=${token}`}
|
||||
alt="preview"
|
||||
className="absolute top-4 left-4 z-10"
|
||||
style={{
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
{sight.watermark_rd && (
|
||||
<img
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
sight.watermark_rd
|
||||
}/download?token=${token}`}
|
||||
alt="preview"
|
||||
className="absolute bottom-4 right-4 z-10"
|
||||
style={{
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
) : (
|
||||
<ImagePlus size={48} color="white" />
|
||||
)}
|
||||
@ -400,7 +431,13 @@ export const CreateLeftTab = observer(
|
||||
sx={{
|
||||
padding: 1,
|
||||
maxHeight: "300px",
|
||||
overflowY: "scroll",
|
||||
overflowY: "auto",
|
||||
"&::-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,
|
||||
@ -451,6 +488,10 @@ export const CreateLeftTab = observer(
|
||||
<UploadMediaDialog
|
||||
open={uploadMediaOpen}
|
||||
onClose={() => setUploadMediaOpen(false)}
|
||||
contextObjectName={sight[language].name}
|
||||
contextType="sight"
|
||||
isArticle={true}
|
||||
articleName={sight[language].left.heading || "Левая статья"}
|
||||
afterUpload={async (media) => {
|
||||
setUploadMediaOpen(false);
|
||||
setFileToUpload(null);
|
||||
@ -466,6 +507,7 @@ export const CreateLeftTab = observer(
|
||||
open={isDeleteModalOpen}
|
||||
onDelete={() => {
|
||||
deleteLeftArticle(sight.left_article);
|
||||
setIsDeleteModalOpen(false);
|
||||
toast.success("Статья откреплена");
|
||||
}}
|
||||
onCancel={() => setIsDeleteModalOpen(false)}
|
||||
|
@ -153,10 +153,12 @@ export const CreateRightTab = observer(
|
||||
selectedArticleId: number
|
||||
) => {
|
||||
try {
|
||||
await linkExistingRightArticle(selectedArticleId);
|
||||
const linkedArticleId = await linkExistingRightArticle(
|
||||
selectedArticleId
|
||||
);
|
||||
setSelectArticleDialogOpen(false); // Close dialog
|
||||
const newIndex = sight[language].right.findIndex(
|
||||
(a) => a.id === selectedArticleId
|
||||
(a) => a.id === linkedArticleId
|
||||
);
|
||||
if (newIndex > -1) {
|
||||
setActiveArticleIndex(newIndex);
|
||||
@ -483,6 +485,9 @@ export const CreateRightTab = observer(
|
||||
linkPreviewMedia(mediaId);
|
||||
}}
|
||||
onFilesDrop={() => {}}
|
||||
contextObjectName={sight[language].name}
|
||||
contextType="sight"
|
||||
isArticle={false}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
@ -495,6 +500,9 @@ export const CreateRightTab = observer(
|
||||
linkPreviewMedia(mediaId);
|
||||
}}
|
||||
onFilesDrop={() => {}}
|
||||
contextObjectName={sight[language].name}
|
||||
contextType="sight"
|
||||
isArticle={false}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
@ -597,7 +605,13 @@ export const CreateRightTab = observer(
|
||||
padding: 1,
|
||||
minHeight: "200px",
|
||||
maxHeight: "300px",
|
||||
overflowY: "scroll",
|
||||
overflowY: "auto",
|
||||
"&::-webkit-scrollbar": {
|
||||
display: "none",
|
||||
},
|
||||
"&": {
|
||||
scrollbarWidth: "none",
|
||||
},
|
||||
background:
|
||||
"rgba(179, 165, 152, 0.4), linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 100%)",
|
||||
|
||||
@ -698,6 +712,14 @@ export const CreateRightTab = observer(
|
||||
setFileToUpload(null); // Clear file if dialog is closed without upload
|
||||
setMediaTarget(null);
|
||||
}}
|
||||
contextObjectName={sight[language].name}
|
||||
contextType="sight"
|
||||
isArticle={mediaTarget === "rightArticle"}
|
||||
articleName={
|
||||
mediaTarget === "rightArticle" && activeArticleIndex !== null
|
||||
? sight[language].right[activeArticleIndex].heading
|
||||
: undefined
|
||||
}
|
||||
afterUpload={handleMediaUploaded} // This will use the mediaTarget
|
||||
/>
|
||||
<SelectMediaDialog
|
||||
|
@ -32,8 +32,6 @@ import { toast } from "react-toastify";
|
||||
|
||||
export const InformationTab = observer(
|
||||
({ value, index }: { value: number; index: number }) => {
|
||||
const { ruCities } = cityStore;
|
||||
|
||||
const [mediaId, setMediaId] = useState<string>("");
|
||||
const [isPreviewMediaOpen, setIsPreviewMediaOpen] = useState(false);
|
||||
const [isUploadMediaOpen, setIsUploadMediaOpen] = useState(false);
|
||||
@ -53,6 +51,7 @@ export const InformationTab = observer(
|
||||
const [hardcodeType, setHardcodeType] = useState<
|
||||
"thumbnail" | "watermark_lu" | "watermark_rd" | null
|
||||
>(null);
|
||||
const { cities } = cityStore;
|
||||
useEffect(() => {
|
||||
// Показывать только при инициализации (не менять при ошибках пользователя)
|
||||
if (sight.common.latitude !== 0 || sight.common.longitude !== 0) {
|
||||
@ -89,7 +88,7 @@ export const InformationTab = observer(
|
||||
},
|
||||
true
|
||||
);
|
||||
setActiveMenuType(null);
|
||||
|
||||
setIsUploadMediaOpen(false);
|
||||
};
|
||||
|
||||
@ -163,9 +162,9 @@ export const InformationTab = observer(
|
||||
/>
|
||||
|
||||
<Autocomplete
|
||||
options={ruCities?.data ?? []}
|
||||
options={cities["ru"]?.data ?? []}
|
||||
value={
|
||||
ruCities?.data?.find(
|
||||
cities["ru"]?.data?.find(
|
||||
(city) => city.id === sight.common.city_id
|
||||
) ?? null
|
||||
}
|
||||
@ -246,7 +245,7 @@ export const InformationTab = observer(
|
||||
}}
|
||||
>
|
||||
<ImageUploadCard
|
||||
title="Логотип"
|
||||
title="Иконка"
|
||||
imageKey="thumbnail"
|
||||
imageUrl={sight.common.thumbnail}
|
||||
onImageClick={() => {
|
||||
@ -270,11 +269,10 @@ export const InformationTab = observer(
|
||||
setUploadMediaOpen={() => {
|
||||
setIsUploadMediaOpen(true);
|
||||
setActiveMenuType("thumbnail");
|
||||
setHardcodeType("thumbnail");
|
||||
}}
|
||||
setHardcodeType={(type) => {
|
||||
setHardcodeType(
|
||||
type as "thumbnail" | "watermark_lu" | "watermark_rd"
|
||||
);
|
||||
setHardcodeType={() => {
|
||||
setHardcodeType("thumbnail");
|
||||
}}
|
||||
/>
|
||||
<ImageUploadCard
|
||||
@ -302,15 +300,14 @@ export const InformationTab = observer(
|
||||
setUploadMediaOpen={() => {
|
||||
setIsUploadMediaOpen(true);
|
||||
setActiveMenuType("watermark_lu");
|
||||
setHardcodeType("watermark_lu");
|
||||
}}
|
||||
setHardcodeType={(type) => {
|
||||
setHardcodeType(
|
||||
type as "thumbnail" | "watermark_lu" | "watermark_rd"
|
||||
);
|
||||
setHardcodeType={() => {
|
||||
setHardcodeType("watermark_lu");
|
||||
}}
|
||||
/>
|
||||
<ImageUploadCard
|
||||
title="Водяной знак (правый нижний)"
|
||||
title="Водяной знак (правый верхний)"
|
||||
imageKey="watermark_rd"
|
||||
imageUrl={sight.common.watermark_rd}
|
||||
onImageClick={() => {
|
||||
@ -334,11 +331,10 @@ export const InformationTab = observer(
|
||||
setUploadMediaOpen={() => {
|
||||
setIsUploadMediaOpen(true);
|
||||
setActiveMenuType("watermark_rd");
|
||||
setHardcodeType("watermark_rd");
|
||||
}}
|
||||
setHardcodeType={(type) => {
|
||||
setHardcodeType(
|
||||
type as "thumbnail" | "watermark_lu" | "watermark_rd"
|
||||
);
|
||||
setHardcodeType={() => {
|
||||
setHardcodeType("watermark_rd");
|
||||
}}
|
||||
/>
|
||||
</Box>
|
||||
@ -399,7 +395,6 @@ export const InformationTab = observer(
|
||||
open={isAddMediaOpen}
|
||||
onClose={() => {
|
||||
setIsAddMediaOpen(false);
|
||||
setActiveMenuType(null);
|
||||
}}
|
||||
onSelectMedia={handleMediaSelect}
|
||||
mediaType={
|
||||
@ -414,6 +409,8 @@ export const InformationTab = observer(
|
||||
<UploadMediaDialog
|
||||
open={isUploadMediaOpen}
|
||||
onClose={() => setIsUploadMediaOpen(false)}
|
||||
contextObjectName={sight[language].name}
|
||||
contextType="sight"
|
||||
afterUpload={(media) => {
|
||||
handleChange(
|
||||
language as Language,
|
||||
|
@ -9,6 +9,7 @@ import {
|
||||
SelectArticleModal,
|
||||
UploadMediaDialog,
|
||||
Language,
|
||||
articlesStore,
|
||||
} from "@shared";
|
||||
import {
|
||||
LanguageSwitcher,
|
||||
@ -43,7 +44,7 @@ export const LeftWidgetTab = observer(
|
||||
const { language } = languageStore;
|
||||
const data = sight[language];
|
||||
const [isDeleteModalOpen, setIsDeleteModalOpen] = useState(false);
|
||||
|
||||
const token = localStorage.getItem("token");
|
||||
const [isSelectMediaDialogOpen, setIsSelectMediaDialogOpen] =
|
||||
useState(false);
|
||||
const [isSelectArticleDialogOpen, setIsSelectArticleDialogOpen] =
|
||||
@ -76,20 +77,40 @@ export const LeftWidgetTab = observer(
|
||||
}, []);
|
||||
|
||||
const handleSelectArticle = useCallback(
|
||||
(
|
||||
articleId: number,
|
||||
heading: string,
|
||||
body: string,
|
||||
media: { id: string; media_type: number; filename: string }[]
|
||||
async (
|
||||
articleId: number
|
||||
// heading: string,
|
||||
// body: string,
|
||||
// media: { id: string; media_type: number; filename: string }[]
|
||||
) => {
|
||||
setIsSelectArticleDialogOpen(false);
|
||||
updateSightInfo(languageStore.language, {
|
||||
|
||||
const ruArticle = await articlesStore.getArticle(articleId, "ru");
|
||||
const enArticle = await articlesStore.getArticle(articleId, "en");
|
||||
const zhArticle = await articlesStore.getArticle(articleId, "zh");
|
||||
|
||||
updateSightInfo("ru", {
|
||||
left: {
|
||||
heading,
|
||||
body,
|
||||
media,
|
||||
heading: ruArticle.data.heading,
|
||||
body: ruArticle.data.body,
|
||||
media: ruArticle.data.media || [],
|
||||
},
|
||||
});
|
||||
updateSightInfo("en", {
|
||||
left: {
|
||||
heading: enArticle.data.heading,
|
||||
body: enArticle.data.body,
|
||||
media: enArticle.data.media || [],
|
||||
},
|
||||
});
|
||||
updateSightInfo("zh", {
|
||||
left: {
|
||||
heading: zhArticle.data.heading,
|
||||
body: zhArticle.data.body,
|
||||
media: zhArticle.data.media || [],
|
||||
},
|
||||
});
|
||||
|
||||
updateSightInfo(
|
||||
languageStore.language,
|
||||
{
|
||||
@ -271,6 +292,7 @@ export const LeftWidgetTab = observer(
|
||||
width: "100%",
|
||||
minHeight: 100,
|
||||
padding: "3px",
|
||||
position: "relative",
|
||||
display: "flex",
|
||||
alignItems: "center",
|
||||
justifyContent: "center",
|
||||
@ -285,14 +307,41 @@ export const LeftWidgetTab = observer(
|
||||
}}
|
||||
>
|
||||
{data.left.media.length > 0 ? (
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: data.left.media[0].id,
|
||||
media_type: data.left.media[0].media_type,
|
||||
filename: data.left.media[0].filename,
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<>
|
||||
<MediaViewer
|
||||
media={{
|
||||
id: data.left.media[0].id,
|
||||
media_type: data.left.media[0].media_type,
|
||||
filename: data.left.media[0].filename,
|
||||
}}
|
||||
fullWidth
|
||||
/>
|
||||
<img
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
sight.common.watermark_lu
|
||||
}/download?token=${token}`}
|
||||
alt="preview"
|
||||
className="absolute top-4 left-4 z-10"
|
||||
style={{
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
|
||||
<img
|
||||
src={`${import.meta.env.VITE_KRBL_MEDIA}${
|
||||
sight.common.watermark_rd
|
||||
}/download?token=${token}`}
|
||||
alt="preview"
|
||||
className="absolute bottom-4 right-4 z-10"
|
||||
style={{
|
||||
width: "30px",
|
||||
height: "30px",
|
||||
objectFit: "contain",
|
||||
}}
|
||||
/>
|
||||
</>
|
||||
) : (
|
||||
<ImagePlus size={48} color="white" />
|
||||
)}
|
||||
@ -341,7 +390,14 @@ export const LeftWidgetTab = observer(
|
||||
sx={{
|
||||
padding: 1,
|
||||
maxHeight: "300px",
|
||||
overflowY: "scroll",
|
||||
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,
|
||||
@ -373,6 +429,12 @@ export const LeftWidgetTab = observer(
|
||||
<UploadMediaDialog
|
||||
open={uploadMediaOpen}
|
||||
onClose={() => setUploadMediaOpen(false)}
|
||||
contextObjectName={sight[languageStore.language].name}
|
||||
contextType="sight"
|
||||
isArticle={true}
|
||||
articleName={
|
||||
sight[languageStore.language].left.heading || "Левая статья"
|
||||
}
|
||||
afterUpload={async (media) => {
|
||||
setUploadMediaOpen(false);
|
||||
setFileToUpload(null);
|
||||
|
@ -115,9 +115,21 @@ export const RightWidgetTab = observer(
|
||||
setActiveArticleIndex(index);
|
||||
};
|
||||
|
||||
const handleCreateNew = () => {
|
||||
createNewRightArticle();
|
||||
handleClose();
|
||||
const handleCreateNew = async () => {
|
||||
try {
|
||||
const newArticleId = await createNewRightArticle();
|
||||
handleClose();
|
||||
// Automatically select the newly created article
|
||||
const newIndex = sight[language].right.findIndex(
|
||||
(article) => article.id === newArticleId
|
||||
);
|
||||
if (newIndex > -1) {
|
||||
setActiveArticleIndex(newIndex);
|
||||
setType("article");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error creating new article:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleSelectExisting = () => {
|
||||
@ -129,9 +141,21 @@ export const RightWidgetTab = observer(
|
||||
setIsSelectModalOpen(false);
|
||||
};
|
||||
|
||||
const handleArticleSelect = (id: number) => {
|
||||
linkArticle(id);
|
||||
handleCloseSelectModal();
|
||||
const handleArticleSelect = async (id: number) => {
|
||||
try {
|
||||
const linkedArticleId = await linkArticle(id);
|
||||
handleCloseSelectModal();
|
||||
// Automatically select the newly linked article
|
||||
const newIndex = sight[language].right.findIndex(
|
||||
(article) => article.id === linkedArticleId
|
||||
);
|
||||
if (newIndex > -1) {
|
||||
setActiveArticleIndex(newIndex);
|
||||
setType("article");
|
||||
}
|
||||
} catch (error) {
|
||||
console.error("Error linking article:", error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleMediaSelected = async (media: {
|
||||
@ -417,6 +441,9 @@ export const RightWidgetTab = observer(
|
||||
linkPreviewMedia(mediaId);
|
||||
}}
|
||||
onFilesDrop={() => {}}
|
||||
contextObjectName={sight[language].name}
|
||||
contextType="sight"
|
||||
isArticle={false}
|
||||
/>
|
||||
)}
|
||||
</Box>
|
||||
@ -441,6 +468,9 @@ export const RightWidgetTab = observer(
|
||||
linkPreviewMedia(mediaId);
|
||||
}}
|
||||
onFilesDrop={() => {}}
|
||||
contextObjectName={sight[language].name}
|
||||
contextType="sight"
|
||||
isArticle={false}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
@ -539,7 +569,15 @@ export const RightWidgetTab = observer(
|
||||
padding: 1,
|
||||
minHeight: "200px",
|
||||
maxHeight: "300px",
|
||||
overflowY: "scroll",
|
||||
overflowY: "auto",
|
||||
|
||||
width: "100%",
|
||||
"&::-webkit-scrollbar": {
|
||||
display: "none",
|
||||
},
|
||||
"&": {
|
||||
scrollbarWidth: "none",
|
||||
},
|
||||
background:
|
||||
"rgba(179, 165, 152, 0.4), linear-gradient(180deg, rgba(255, 255, 255, 0.2) 0%, rgba(255, 255, 255, 0.2) 100%)",
|
||||
|
||||
@ -565,13 +603,13 @@ export const RightWidgetTab = observer(
|
||||
sx={{
|
||||
p: 2,
|
||||
display: "flex",
|
||||
justifyContent: "space-between",
|
||||
justifyContent: "center",
|
||||
fontSize: "24px",
|
||||
fontWeight: 700,
|
||||
lineHeight: "120%",
|
||||
flexWrap: "wrap",
|
||||
|
||||
gap: 1,
|
||||
gap: "34px",
|
||||
backdropFilter: "blur(12px)",
|
||||
boxShadow:
|
||||
"inset 4px 4px 12px 0 rgba(255,255,255,0.12)",
|
||||
@ -627,6 +665,14 @@ export const RightWidgetTab = observer(
|
||||
<UploadMediaDialog
|
||||
open={uploadMediaOpen}
|
||||
onClose={() => setUploadMediaOpen(false)}
|
||||
contextObjectName={sight[language].name}
|
||||
contextType="sight"
|
||||
isArticle={true}
|
||||
articleName={
|
||||
activeArticleIndex !== null
|
||||
? sight[language].right[activeArticleIndex].heading
|
||||
: "Правая статья"
|
||||
}
|
||||
afterUpload={async (media) => {
|
||||
setUploadMediaOpen(false);
|
||||
setFileToUpload(null);
|
||||
|
@ -16,3 +16,4 @@ export * from "./LeaveAgree";
|
||||
export * from "./DeleteModal";
|
||||
export * from "./SnapshotRestore";
|
||||
export * from "./CreateButton";
|
||||
export * from "./modals";
|
||||
|
140
src/widgets/modals/EditStationModal.tsx
Normal file
140
src/widgets/modals/EditStationModal.tsx
Normal file
@ -0,0 +1,140 @@
|
||||
import {
|
||||
Button,
|
||||
Dialog,
|
||||
DialogActions,
|
||||
DialogContent,
|
||||
DialogTitle,
|
||||
TextField,
|
||||
Typography,
|
||||
IconButton,
|
||||
Box,
|
||||
} from "@mui/material";
|
||||
import { routeStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft } from "lucide-react";
|
||||
import { useParams } from "react-router-dom";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
interface EditStationModalProps {
|
||||
open: boolean;
|
||||
onClose: () => void;
|
||||
}
|
||||
|
||||
const transferFields = [
|
||||
{ key: "bus", label: "Автобус" },
|
||||
{ key: "metro_blue", label: "Метро (синяя)" },
|
||||
{ key: "metro_green", label: "Метро (зеленая)" },
|
||||
{ key: "metro_orange", label: "Метро (оранжевая)" },
|
||||
{ key: "metro_purple", label: "Метро (фиолетовая)" },
|
||||
{ key: "metro_red", label: "Метро (красная)" },
|
||||
{ key: "train", label: "Электричка" },
|
||||
{ key: "tram", label: "Трамвай" },
|
||||
{ key: "trolleybus", label: "Троллейбус" },
|
||||
];
|
||||
|
||||
export const EditStationModal = observer(
|
||||
({ open, onClose }: EditStationModalProps) => {
|
||||
const { id: routeId } = useParams<{ id: string }>();
|
||||
const {
|
||||
selectedStationId,
|
||||
setRouteStations,
|
||||
saveRouteStations,
|
||||
routeStations,
|
||||
} = routeStore;
|
||||
|
||||
const handleSave = async () => {
|
||||
console.log(routeId, selectedStationId);
|
||||
|
||||
await saveRouteStations(Number(routeId), selectedStationId);
|
||||
toast.success("Успешно сохранено");
|
||||
onClose();
|
||||
};
|
||||
|
||||
const station = routeStations[Number(routeId)]?.find(
|
||||
(station: any) => station.id === selectedStationId
|
||||
);
|
||||
|
||||
return (
|
||||
<Dialog open={open} onClose={onClose} maxWidth="md" fullWidth>
|
||||
<DialogTitle>
|
||||
<Box display="flex" alignItems="center" gap={2}>
|
||||
<IconButton onClick={onClose}>
|
||||
<ArrowLeft />
|
||||
</IconButton>
|
||||
<Box>
|
||||
<Typography variant="caption" color="text.secondary">
|
||||
Маршруты / Редактировать
|
||||
</Typography>
|
||||
<Typography variant="h6">Редактирование остановки</Typography>
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogTitle>
|
||||
<DialogContent>
|
||||
<Box sx={{ mt: 2, display: "flex", gap: 2, flexDirection: "column" }}>
|
||||
<TextField
|
||||
label="Смещение (X)"
|
||||
name="offset_x"
|
||||
type="number"
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
setRouteStations(Number(routeId), selectedStationId, {
|
||||
offset_x: Number(e.target.value),
|
||||
});
|
||||
}}
|
||||
defaultValue={station?.offset_x}
|
||||
/>
|
||||
|
||||
<TextField
|
||||
label="Смещение (Y)"
|
||||
name="offset_y"
|
||||
type="number"
|
||||
fullWidth
|
||||
onChange={(e) => {
|
||||
setRouteStations(Number(routeId), selectedStationId, {
|
||||
offset_y: Number(e.target.value),
|
||||
});
|
||||
}}
|
||||
defaultValue={station?.offset_y}
|
||||
/>
|
||||
</Box>
|
||||
<Box sx={{ mt: 4 }}>
|
||||
<Typography variant="h6" gutterBottom>
|
||||
Пересадки
|
||||
</Typography>
|
||||
<Box
|
||||
sx={{
|
||||
display: "grid",
|
||||
gridTemplateColumns: "repeat(3, 1fr)",
|
||||
gap: 2,
|
||||
}}
|
||||
>
|
||||
{transferFields.map(({ key, label }) => (
|
||||
<TextField
|
||||
key={key}
|
||||
label={label}
|
||||
name={key}
|
||||
fullWidth
|
||||
defaultValue={station?.transfers?.[key]}
|
||||
onChange={(e) => {
|
||||
setRouteStations(Number(routeId), selectedStationId, {
|
||||
...station,
|
||||
transfers: {
|
||||
...station?.transfers,
|
||||
[key]: e.target.value,
|
||||
},
|
||||
});
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</Box>
|
||||
</Box>
|
||||
</DialogContent>
|
||||
<DialogActions sx={{ p: 3, justifyContent: "flex-end" }}>
|
||||
<Button onClick={handleSave} variant="contained" color="primary">
|
||||
Сохранить
|
||||
</Button>
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
);
|
||||
}
|
||||
);
|
@ -1 +1,2 @@
|
||||
export * from "./SelectArticleDialog";
|
||||
export * from "./EditStationModal";
|
||||
|
Reference in New Issue
Block a user