feat: Redesign route direction with media_order input

This commit is contained in:
Илья Куприец 2025-05-20 16:59:57 +03:00
parent 7c363f1730
commit 8a443882b5
4 changed files with 244 additions and 184 deletions

View File

@ -106,17 +106,19 @@ export const LinkedItems = <T extends { id: number; [key: string]: any }>(
</AccordionDetails> </AccordionDetails>
</Accordion> </Accordion>
{!props.dontRecurse && {!props.dontRecurse && (
<> <>
<ArticleEditModal /> <ArticleEditModal />
<StationEditModal /> <StationEditModal />
</> </>
} )}
</> </>
); );
} };
export const LinkedItemsContents = <T extends { id: number; [key: string]: any }>({ export const LinkedItemsContents = <
T extends { id: number; [key: string]: any }
>({
parentId, parentId,
parentResource, parentResource,
childResource, childResource,
@ -128,7 +130,7 @@ export const LinkedItemsContents = <T extends { id: number; [key: string]: any }
onUpdate, onUpdate,
disableCreation = false, disableCreation = false,
updatedLinkedItems, updatedLinkedItems,
refresh refresh,
}: LinkedItemsProps<T>) => { }: LinkedItemsProps<T>) => {
const { language } = languageStore; const { language } = languageStore;
const { setArticleModalOpenAction, setArticleIdAction } = articleStore; const { setArticleModalOpenAction, setArticleIdAction } = articleStore;
@ -154,7 +156,7 @@ export const LinkedItemsContents = <T extends { id: number; [key: string]: any }
}, [childResource, availableItems]); }, [childResource, availableItems]);
useEffect(() => { useEffect(() => {
if(!updatedLinkedItems?.length) return; if (!updatedLinkedItems?.length) return;
setLinkedItems(updatedLinkedItems); setLinkedItems(updatedLinkedItems);
}, [updatedLinkedItems]); }, [updatedLinkedItems]);
@ -173,7 +175,7 @@ export const LinkedItemsContents = <T extends { id: number; [key: string]: any }
setLinkedItems(reorderedItems); setLinkedItems(reorderedItems);
if(parentResource === "sight" && childResource === "article") { if (parentResource === "sight" && childResource === "article") {
axiosInstance.post( axiosInstance.post(
`${import.meta.env.VITE_KRBL_API}/sight/${parentId}/article/order`, `${import.meta.env.VITE_KRBL_API}/sight/${parentId}/article/order`,
{ {
@ -358,11 +360,17 @@ export const LinkedItemsContents = <T extends { id: number; [key: string]: any }
: "default", : "default",
}} }}
onClick={() => { onClick={() => {
if (childResource === "article" && type==="edit") { if (
childResource === "article" &&
type === "edit"
) {
setArticleModalOpenAction(true); setArticleModalOpenAction(true);
setArticleIdAction(item.id); setArticleIdAction(item.id);
} }
if (childResource === "station" && type==="edit") { if (
childResource === "station" &&
type === "edit"
) {
setStationModalOpenAction(true); setStationModalOpenAction(true);
setStationIdAction(item.id); setStationIdAction(item.id);
setRouteIdAction(Number(parentId)); setRouteIdAction(Number(parentId));
@ -434,25 +442,15 @@ export const LinkedItemsContents = <T extends { id: number; [key: string]: any }
<Autocomplete <Autocomplete
fullWidth fullWidth
value={ value={
availableItems?.find( availableItems?.find((item) => item.id === selectedItemId) || null
(item) => item.id === selectedItemId
) || null
}
onChange={(_, newValue) =>
setSelectedItemId(newValue?.id || null)
} }
onChange={(_, newValue) => setSelectedItemId(newValue?.id || null)}
options={availableItems} options={availableItems}
getOptionLabel={(item) => String(item[fields[0].data])} getOptionLabel={(item) => String(item[fields[0].data])}
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField {...params} label={`Выберите ${title}`} fullWidth />
{...params}
label={`Выберите ${title}`}
fullWidth
/>
)} )}
isOptionEqualToValue={(option, value) => isOptionEqualToValue={(option, value) => option.id === value?.id}
option.id === value?.id
}
filterOptions={(options, { inputValue }) => { filterOptions={(options, { inputValue }) => {
const searchWords = inputValue const searchWords = inputValue
.toLowerCase() .toLowerCase()
@ -495,17 +493,32 @@ export const LinkedItemsContents = <T extends { id: number; [key: string]: any }
{childResource === "media" && ( {childResource === "media" && (
<FormControl fullWidth> <FormControl fullWidth>
<TextField <TextField
type="number" type="text"
label="Порядок отображения медиа" label="Порядок отображения медиа"
value={mediaOrder} value={mediaOrder}
onChange={(e) => { onChange={(e) => {
const newValue = Number(e.target.value); const rawValue = e.target.value;
const numericValue = Number(rawValue);
const maxValue = linkedItems.length + 1; const maxValue = linkedItems.length + 1;
const value = Math.max(1, Math.min(newValue, maxValue));
setMediaOrder(value); if (isNaN(numericValue)) {
return;
} else {
let newValue = numericValue;
if (newValue < 10 && newValue > 0) {
setMediaOrder(numericValue);
}
if (newValue > maxValue) {
newValue = maxValue;
}
setMediaOrder(newValue);
}
}} }}
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} InputLabelProps={{ shrink: true }}
/> />
</FormControl> </FormControl>
)} )}
@ -513,7 +526,9 @@ export const LinkedItemsContents = <T extends { id: number; [key: string]: any }
<Button <Button
variant="contained" variant="contained"
onClick={linkItem} onClick={linkItem}
disabled={!selectedItemId} disabled={
!selectedItemId || (childResource == "media" && mediaOrder == 0)
}
sx={{ alignSelf: "flex-start" }} sx={{ alignSelf: "flex-start" }}
> >
Добавить Добавить

View File

@ -8,6 +8,7 @@ import {
} from "@mui/material"; } 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 { useState } from "react";
import { Controller } from "react-hook-form"; import { Controller } from "react-hook-form";
export const RouteCreate = () => { export const RouteCreate = () => {
@ -16,6 +17,7 @@ export const RouteCreate = () => {
refineCore: { formLoading }, refineCore: { formLoading },
register, register,
control, control,
setValue,
formState: { errors }, formState: { errors },
} = useForm({ } = useForm({
refineCoreProps: { refineCoreProps: {
@ -23,6 +25,16 @@ export const RouteCreate = () => {
}, },
}); });
const directions = [
{
label: "Прямой",
value: true,
},
{
label: "Обратный",
value: false,
},
];
const { autocompleteProps: carrierAutocompleteProps } = useAutocomplete({ const { autocompleteProps: carrierAutocompleteProps } = useAutocomplete({
resource: "carrier", resource: "carrier",
onSearch: (value) => [ onSearch: (value) => [
@ -34,22 +46,25 @@ export const RouteCreate = () => {
], ],
}); });
const { autocompleteProps: governorAppealAutocompleteProps } = useAutocomplete({ const { autocompleteProps: governorAppealAutocompleteProps } =
resource: "article", useAutocomplete({
resource: "article",
onSearch: (value) => [ onSearch: (value) => [
{ {
field: "heading", field: "heading",
operator: "contains", operator: "contains",
value, value,
}, },
{ {
field: "media_type", field: "media_type",
operator: "contains", operator: "contains",
value, value,
}, },
] ],
}); });
const [routeDirection, setRouteDirection] = useState(false);
return ( return (
<Create isLoading={formLoading} saveButtonProps={saveButtonProps}> <Create isLoading={formLoading} saveButtonProps={saveButtonProps}>
@ -111,37 +126,12 @@ export const RouteCreate = () => {
helperText={(errors as any)?.route_number?.message} helperText={(errors as any)?.route_number?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="text" type="text"
label={"Номер маршрута *"} label={"Номер маршрута *"}
name="route_number" name="route_number"
/> />
<Controller
name="route_direction" // boolean
control={control}
defaultValue={false}
render={({ field }: { field: any }) => (
<FormControlLabel
label="Прямой маршрут? *"
control={
<Checkbox
{...field}
checked={field.value}
onChange={(e) => field.onChange(e.target.checked)}
/>
}
/>
)}
/>
<Typography
variant="caption"
color="textSecondary"
sx={{ mt: 0, mb: 1 }}
>
(Прямой / Обратный)
</Typography>
<TextField <TextField
{...register("path", { {...register("path", {
required: "Это поле является обязательным", required: "Это поле является обязательным",
@ -191,7 +181,7 @@ export const RouteCreate = () => {
helperText={(errors as any)?.path?.message} helperText={(errors as any)?.path?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="text" type="text"
label={"Координаты маршрута *"} label={"Координаты маршрута *"}
name="path" name="path"
@ -209,9 +199,9 @@ export const RouteCreate = () => {
helperText={(errors as any)?.route_sys_number?.message} helperText={(errors as any)?.route_sys_number?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="number" type="number"
label={"Системный номер маршрута *"} label={"Номер маршрута в Говорящем Городе *"}
name="route_sys_number" name="route_sys_number"
/> />
@ -256,7 +246,44 @@ export const RouteCreate = () => {
)} )}
/> />
)} )}
/> />
<input
type="hidden"
{...register("route_direction", {
value: routeDirection,
})}
/>
<Autocomplete
options={directions}
defaultValue={directions.find((el) => el.value == false)}
onChange={(_, element) => {
if (element) {
setValue("route_direction", element.value);
setRouteDirection(element.value);
}
}}
renderInput={(params) => (
<TextField
{...params}
label="Прямой/обратный маршрут"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
required
/>
)}
/>
<Typography
variant="caption"
color="textSecondary"
sx={{ mt: 0, mb: 1 }}
>
{routeDirection ? "Прямой" : "Обратный"}
</Typography>
<TextField <TextField
{...register("scale_min", { {...register("scale_min", {
@ -267,7 +294,7 @@ export const RouteCreate = () => {
helperText={(errors as any)?.scale_min?.message} helperText={(errors as any)?.scale_min?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="number" type="number"
label={"Масштаб (мин)"} label={"Масштаб (мин)"}
name="scale_min" name="scale_min"
@ -282,7 +309,7 @@ export const RouteCreate = () => {
helperText={(errors as any)?.scale_max?.message} helperText={(errors as any)?.scale_max?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="number" type="number"
label={"Масштаб (макс)"} label={"Масштаб (макс)"}
name="scale_max" name="scale_max"
@ -297,7 +324,7 @@ export const RouteCreate = () => {
helperText={(errors as any)?.rotate?.message} helperText={(errors as any)?.rotate?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="number" type="number"
label={"Поворот"} label={"Поворот"}
name="rotate" name="rotate"
@ -312,7 +339,7 @@ export const RouteCreate = () => {
helperText={(errors as any)?.center_latitude?.message} helperText={(errors as any)?.center_latitude?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="number" type="number"
label={"Центр. широта"} label={"Центр. широта"}
name="center_latitude" name="center_latitude"
@ -327,7 +354,7 @@ export const RouteCreate = () => {
helperText={(errors as any)?.center_longitude?.message} helperText={(errors as any)?.center_longitude?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="number" type="number"
label={"Центр. долгота"} label={"Центр. долгота"}
name="center_longitude" name="center_longitude"

View File

@ -18,7 +18,7 @@ import {
stationFields, stationFields,
vehicleFields, vehicleFields,
} from "./types"; } from "./types";
import { useEffect } from "react"; import { useEffect, useState } from "react";
import { META_LANGUAGE, languageStore } from "@stores"; import { META_LANGUAGE, languageStore } from "@stores";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { LanguageSelector } from "@ui"; import { LanguageSelector } from "@ui";
@ -32,11 +32,25 @@ export const RouteEdit = observer(() => {
formState: { errors }, formState: { errors },
refineCore: { queryResult }, refineCore: { queryResult },
setValue, setValue,
getValues,
watch, watch,
} = useForm({ } = useForm({
refineCoreProps: META_LANGUAGE(language) refineCoreProps: META_LANGUAGE(language),
}); });
const routeDirectionFromServer = watch("route_direction");
const [routeDirection, setRouteDirection] = useState(false);
const navigate = useNavigate(); const navigate = useNavigate();
const directions = [
{
label: "Прямой",
value: true,
},
{
label: "Обратный",
value: false,
},
];
const { id: routeId } = useParams<{ id: string }>(); const { id: routeId } = useParams<{ id: string }>();
@ -59,35 +73,40 @@ export const RouteEdit = observer(() => {
value, value,
}, },
], ],
...META_LANGUAGE(language) ...META_LANGUAGE(language),
}); });
const { autocompleteProps: governorAppealAutocompleteProps } = useAutocomplete({ const { autocompleteProps: governorAppealAutocompleteProps } =
resource: "article", useAutocomplete({
resource: "article",
onSearch: (value) => [ onSearch: (value) => [
{ {
field: "heading", field: "heading",
operator: "contains", operator: "contains",
value, value,
}, },
{ {
field: "media_type", field: "media_type",
operator: "contains", operator: "contains",
value, value,
}, },
], ],
...META_LANGUAGE(language) ...META_LANGUAGE(language),
}); });
useEffect(() => {
if (routeDirectionFromServer) {
setRouteDirection(routeDirectionFromServer);
}
}, [routeDirectionFromServer]);
return ( return (
<Edit saveButtonProps={saveButtonProps}> <Edit saveButtonProps={saveButtonProps}>
<Box <Box sx={{ display: "flex", flexDirection: "column", gap: 1 }}>
sx={{display: "flex", flexDirection: "column", gap:1}}
>
<Box <Box
component="form" component="form"
sx={{ display: "flex", flexDirection: "column"}} sx={{ display: "flex", flexDirection: "column" }}
autoComplete="off" autoComplete="off"
> >
<LanguageSelector /> <LanguageSelector />
@ -144,37 +163,36 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.route_number?.message} helperText={(errors as any)?.route_number?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="text" type="text"
label={"Номер маршрута"} label={"Номер маршрута"}
name="route_number" name="route_number"
/> />
<Controller
name="route_direction" // boolean <input type="hidden" {...register("route_direction")} />
control={control}
defaultValue={false} <Autocomplete
render={({ field }: { field: any }) => ( options={directions}
<FormControlLabel value={directions.find((el) => el.value == routeDirection)}
label="Прямой маршрут?" onChange={(_, element) => {
control={ if (element) {
<Checkbox setValue("route_direction", element.value);
{...field} setRouteDirection(element.value);
checked={field.value} }
onChange={(e) => field.onChange(e.target.checked)} }}
/> renderInput={(params) => (
} <TextField
{...params}
label="Прямой/обратный маршрут"
margin="normal"
variant="outlined"
error={!!errors.arms}
helperText={(errors as any)?.arms?.message}
required
/> />
)} )}
/> />
<Typography
variant="caption"
color="textSecondary"
sx={{ mt: 0, mb: 1 }}
>
(Прямой / Обратный)
</Typography>
<TextField <TextField
{...register("path", { {...register("path", {
required: "Это поле является обязательным", required: "Это поле является обязательным",
@ -198,7 +216,8 @@ export const RouteEdit = observer(() => {
return "Введите хотя бы одну пару координат"; return "Введите хотя бы одну пару координат";
if ( if (
!value.every( !value.every(
(point: unknown) => Array.isArray(point) && point.length === 2 (point: unknown) =>
Array.isArray(point) && point.length === 2
) )
) { ) {
return "Каждая строка должна содержать две координаты"; return "Каждая строка должна содержать две координаты";
@ -220,12 +239,12 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.path?.message} helperText={(errors as any)?.path?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="text" type="text"
label={"Координаты маршрута *"} label={"Координаты маршрута *"}
name="path" name="path"
placeholder="55.7558 37.6173 placeholder="55.7558 37.6173
55.7539 37.6208" 55.7539 37.6208"
multiline multiline
rows={4} rows={4}
sx={{ sx={{
@ -241,53 +260,53 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.route_sys_number?.message} helperText={(errors as any)?.route_sys_number?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="number" type="number"
label={"Системный номер маршрута *"} label={"Номер маршрута в Говорящем Городе *"}
name="route_sys_number" name="route_sys_number"
/> />
<Controller <Controller
control={control} control={control}
name="governor_appeal" name="governor_appeal"
defaultValue={null} defaultValue={null}
render={({ field }) => ( render={({ field }) => (
<Autocomplete <Autocomplete
{...governorAppealAutocompleteProps} {...governorAppealAutocompleteProps}
value={ value={
governorAppealAutocompleteProps.options.find( governorAppealAutocompleteProps.options.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) => {
return item ? item.heading : ""; return item ? item.heading : "";
}} }}
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((option) => return options.filter((option) =>
option.heading option.heading
.toLowerCase() .toLowerCase()
.includes(inputValue.toLowerCase()) .includes(inputValue.toLowerCase())
); );
}} }}
renderInput={(params) => ( renderInput={(params) => (
<TextField <TextField
{...params} {...params}
label="Обращение губернатора" label="Обращение губернатора"
margin="normal" margin="normal"
variant="outlined" variant="outlined"
error={!!errors.arms} error={!!errors.arms}
helperText={(errors as any)?.arms?.message} helperText={(errors as any)?.arms?.message}
required required
/> />
)} )}
/> />
)} )}
/> />
<TextField <TextField
@ -299,7 +318,7 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.scale_min?.message} helperText={(errors as any)?.scale_min?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="number" type="number"
label={"Масштаб (мин)"} label={"Масштаб (мин)"}
name="scale_min" name="scale_min"
@ -314,7 +333,7 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.scale_max?.message} helperText={(errors as any)?.scale_max?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="number" type="number"
label={"Масштаб (макс)"} label={"Масштаб (макс)"}
name="scale_max" name="scale_max"
@ -329,7 +348,7 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.rotate?.message} helperText={(errors as any)?.rotate?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="number" type="number"
label={"Поворот"} label={"Поворот"}
name="rotate" name="rotate"
@ -344,7 +363,7 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.center_latitude?.message} helperText={(errors as any)?.center_latitude?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="number" type="number"
label={"Центр. широта"} label={"Центр. широта"}
name="center_latitude" name="center_latitude"
@ -359,12 +378,11 @@ export const RouteEdit = observer(() => {
helperText={(errors as any)?.center_longitude?.message} helperText={(errors as any)?.center_longitude?.message}
margin="normal" margin="normal"
fullWidth fullWidth
slotProps={{inputLabel: {shrink: true}}} slotProps={{ inputLabel: { shrink: true } }}
type="number" type="number"
label={"Центр. долгота"} label={"Центр. долгота"}
name="center_longitude" name="center_longitude"
/> />
</Box> </Box>
{routeId && ( {routeId && (
@ -390,9 +408,9 @@ export const RouteEdit = observer(() => {
</> </>
)} )}
<Box sx={{ display: 'flex', justifyContent: 'flex-start' }}> <Box sx={{ display: "flex", justifyContent: "flex-start" }}>
<Button <Button
variant="contained" variant="contained"
color="primary" color="primary"
onClick={() => navigate(`/route-preview/${routeId}`)} onClick={() => navigate(`/route-preview/${routeId}`)}
> >

View File

@ -9,7 +9,7 @@ import {
} from "@refinedev/mui"; } from "@refinedev/mui";
import { Button, Typography } from "@mui/material"; import { Button, Typography } from "@mui/material";
import React from "react"; import React from "react";
import MapIcon from '@mui/icons-material/Map'; import MapIcon from "@mui/icons-material/Map";
import { localeText } from "../../locales/ru/localeText"; import { localeText } from "../../locales/ru/localeText";
import { useLink } from "@refinedev/core"; import { useLink } from "@refinedev/core";
@ -18,10 +18,10 @@ import { languageStore, META_LANGUAGE } from "@stores";
export const RouteList = observer(() => { export const RouteList = observer(() => {
const Link = useLink(); const Link = useLink();
const { language } = languageStore; const { language } = languageStore;
const { dataGridProps } = useDataGrid({ const { dataGridProps } = useDataGrid({
resource: "route/", resource: "route/",
meta: META_LANGUAGE(language) meta: META_LANGUAGE(language),
}); });
const columns = React.useMemo<GridColDef[]>( const columns = React.useMemo<GridColDef[]>(
@ -64,7 +64,7 @@ export const RouteList = observer(() => {
}, },
{ {
field: "route_sys_number", field: "route_sys_number",
headerName: "Системный номер маршрута", headerName: "Номер маршрута в Говорящем Городе",
type: "string", type: "string",
minWidth: 120, minWidth: 120,
display: "flex", display: "flex",
@ -156,7 +156,7 @@ export const RouteList = observer(() => {
<> <>
<EditButton hideText recordItemId={row.id} /> <EditButton hideText recordItemId={row.id} />
<Link to={`/route-preview/${row.id}`}> <Link to={`/route-preview/${row.id}`}>
<Button sx={{minWidth: 0}} > <Button sx={{ minWidth: 0 }}>
<MapIcon fontSize="small" /> <MapIcon fontSize="small" />
</Button> </Button>
</Link> </Link>