hotfix admin panel
This commit is contained in:
@@ -73,7 +73,8 @@ if (typeof document !== "undefined") {
|
||||
import { languageInstance } from "@shared";
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
import { stationsStore, routeStore, sightsStore } from "@shared";
|
||||
import { stationsStore, routeStore, sightsStore, menuStore } from "@shared";
|
||||
import { Token } from "@mui/icons-material";
|
||||
|
||||
// Функция для сброса кешей карты
|
||||
export const clearMapCaches = () => {
|
||||
@@ -101,6 +102,7 @@ interface ApiStation {
|
||||
name: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
interface ApiSight {
|
||||
@@ -109,8 +111,12 @@ interface ApiSight {
|
||||
description: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
created_at?: string;
|
||||
}
|
||||
|
||||
export type SortType = "name_asc" | "name_desc" | "date_asc" | "date_desc";
|
||||
|
||||
|
||||
class MapStore {
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
@@ -120,6 +126,61 @@ class MapStore {
|
||||
stations: ApiStation[] = [];
|
||||
sights: ApiSight[] = [];
|
||||
|
||||
// НОВЫЕ ПОЛЯ ДЛЯ СОРТИРОВКИ
|
||||
stationSort: SortType = "name_asc";
|
||||
sightSort: SortType = "name_asc";
|
||||
|
||||
// НОВЫЕ МЕТОДЫ-СЕТТЕРЫ
|
||||
setStationSort = (sortType: SortType) => {
|
||||
this.stationSort = sortType;
|
||||
};
|
||||
|
||||
setSightSort = (sortType: SortType) => {
|
||||
this.sightSort = sortType;
|
||||
};
|
||||
|
||||
// ПРИВАТНЫЙ МЕТОД ДЛЯ ОБЩЕЙ ЛОГИКИ СОРТИРОВКИ
|
||||
private sortFeatures<T extends ApiStation | ApiSight>(
|
||||
features: T[],
|
||||
sortType: SortType
|
||||
): T[] {
|
||||
const sorted = [...features];
|
||||
switch (sortType) {
|
||||
case "name_asc":
|
||||
return sorted.sort((a, b) => a.name.localeCompare(b.name));
|
||||
case "name_desc":
|
||||
return sorted.sort((a, b) => b.name.localeCompare(a.name));
|
||||
case "date_asc":
|
||||
return sorted.sort((a, b) => {
|
||||
if ('created_at' in a && 'created_at' in b && a.created_at && b.created_at) {
|
||||
return new Date(a.created_at).getTime() - new Date(b.created_at).getTime();
|
||||
}
|
||||
// Фоллбэк: сортировка по ID, если дата недоступна
|
||||
return a.id - b.id;
|
||||
});
|
||||
case "date_desc":
|
||||
return sorted.sort((a, b) => {
|
||||
if ('created_at' in a && 'created_at' in b && a.created_at && b.created_at) {
|
||||
return new Date(b.created_at).getTime() - new Date(a.created_at).getTime();
|
||||
}
|
||||
// Фоллбэк: сортировка по ID, если дата недоступна
|
||||
return b.id - a.id;
|
||||
});
|
||||
default:
|
||||
return sorted;
|
||||
}
|
||||
}
|
||||
|
||||
// НОВЫЕ ГЕТТЕРЫ, ВОЗВРАЩАЮЩИЕ ОТСОРТИРОВАННЫЕ СПИСКИ
|
||||
get sortedStations(): ApiStation[] {
|
||||
return this.sortFeatures(this.stations, this.stationSort);
|
||||
}
|
||||
|
||||
get sortedSights(): ApiSight[] {
|
||||
return this.sortFeatures(this.sights, this.sightSort);
|
||||
}
|
||||
|
||||
|
||||
getRoutes = async () => {
|
||||
const response = await languageInstance("ru").get("/route");
|
||||
const routesIds = response.data.map((route: any) => route.id);
|
||||
@@ -2116,6 +2177,8 @@ const MapControls: React.FC<MapControlsProps> = ({
|
||||
);
|
||||
};
|
||||
|
||||
import {observer} from "mobx-react-lite";
|
||||
|
||||
// --- MAP SIGHTBAR COMPONENT ---
|
||||
interface MapSightbarProps {
|
||||
mapService: MapService | null;
|
||||
@@ -2126,8 +2189,7 @@ interface MapSightbarProps {
|
||||
activeSection: string | null;
|
||||
setActiveSection: (section: string | null) => void;
|
||||
}
|
||||
|
||||
const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
const MapSightbar: React.FC<MapSightbarProps> = observer(({
|
||||
mapService,
|
||||
mapFeatures,
|
||||
selectedFeature,
|
||||
@@ -2138,50 +2200,50 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
}) => {
|
||||
const navigate = useNavigate();
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [stationSort, setStationSort] = useState<SortType>("name_asc");
|
||||
const [sightSort, setSightSort] = useState<SortType>("name_asc");
|
||||
|
||||
const actualFeatures = useMemo(() => {
|
||||
return mapFeatures.filter((feature) => !feature.get("isProxy"));
|
||||
}, [mapFeatures]);
|
||||
const { isOpen } = menuStore;
|
||||
|
||||
|
||||
const actualFeatures = useMemo(
|
||||
() => mapFeatures.filter((f) => !f.get("isProxy")),
|
||||
[mapFeatures]
|
||||
);
|
||||
|
||||
const filteredFeatures = useMemo(() => {
|
||||
if (!searchQuery.trim()) return actualFeatures;
|
||||
return actualFeatures.filter((feature) =>
|
||||
((feature.get("name") as string) || "")
|
||||
return actualFeatures.filter((f) =>
|
||||
((f.get("name") as string) || "")
|
||||
.toLowerCase()
|
||||
.includes(searchQuery.toLowerCase())
|
||||
);
|
||||
}, [actualFeatures, searchQuery]);
|
||||
|
||||
const handleFeatureClick = useCallback(
|
||||
(id: string | number | undefined) => {
|
||||
if (!id || !mapService) return;
|
||||
(id: string | number) => {
|
||||
if (!mapService) return;
|
||||
mapService.selectFeature(id);
|
||||
},
|
||||
[mapService]
|
||||
);
|
||||
|
||||
const handleDeleteFeature = useCallback(
|
||||
// @ts-ignore
|
||||
(id, recourse) => {
|
||||
if (
|
||||
mapService &&
|
||||
window.confirm("Вы действительно хотите удалить этот объект?")
|
||||
) {
|
||||
mapService.deleteFeature(id, recourse);
|
||||
(id: string | number, resource: string) => {
|
||||
if (!mapService) return;
|
||||
if (window.confirm("Вы действительно хотите удалить этот объект?")) {
|
||||
mapService.deleteFeature(id, resource);
|
||||
}
|
||||
},
|
||||
[mapService]
|
||||
);
|
||||
|
||||
const handleCheckboxChange = useCallback(
|
||||
(id: string | number | undefined) => {
|
||||
if (!id || !mapService) return;
|
||||
(id: string | number) => {
|
||||
if (!mapService) return;
|
||||
const newSet = new Set(selectedIds);
|
||||
if (newSet.has(id)) {
|
||||
newSet.delete(id);
|
||||
} else {
|
||||
newSet.add(id);
|
||||
}
|
||||
if (newSet.has(id)) newSet.delete(id);
|
||||
else newSet.add(id);
|
||||
setSelectedIds(newSet);
|
||||
mapService.setSelectedIds(newSet);
|
||||
},
|
||||
@@ -2200,69 +2262,68 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
}
|
||||
}, [mapService, selectedIds, setSelectedIds]);
|
||||
|
||||
// @ts-ignore
|
||||
const handleEditFeature = useCallback(
|
||||
// @ts-ignore
|
||||
(featureType, fullId) => {
|
||||
if (!featureType || !fullId) return;
|
||||
(featureType: string, fullId: string | number) => {
|
||||
const numericId = String(fullId).split("-")[1];
|
||||
if (numericId) navigate(`/${featureType}/${numericId}/edit`);
|
||||
if (!featureType || !numericId) return;
|
||||
navigate(`/${featureType}/${numericId}/edit`);
|
||||
},
|
||||
[navigate]
|
||||
);
|
||||
|
||||
const sortFeatures = (
|
||||
features: Feature<Geometry>[],
|
||||
currentSelectedIds: Set<string | number>,
|
||||
currentSelectedFeature: Feature<Geometry> | null
|
||||
) => {
|
||||
const selectedId = currentSelectedFeature?.getId();
|
||||
return [...features].sort((a, b) => {
|
||||
const aId = a.getId();
|
||||
const bId = b.getId();
|
||||
|
||||
if (selectedId) {
|
||||
if (aId === selectedId) return -1;
|
||||
if (bId === selectedId) return 1;
|
||||
}
|
||||
|
||||
const aIsChecked = aId !== undefined && currentSelectedIds.has(aId);
|
||||
const bIsChecked = bId !== undefined && currentSelectedIds.has(bId);
|
||||
if (aIsChecked && !bIsChecked) return -1;
|
||||
if (!aIsChecked && bIsChecked) return 1;
|
||||
|
||||
const aNumericId = aId ? parseInt(String(aId).split("-")[1], 10) : 0;
|
||||
const bNumericId = bId ? parseInt(String(bId).split("-")[1], 10) : 0;
|
||||
if (
|
||||
!isNaN(aNumericId) &&
|
||||
!isNaN(bNumericId) &&
|
||||
aNumericId !== bNumericId
|
||||
) {
|
||||
return aNumericId - bNumericId;
|
||||
}
|
||||
|
||||
const aName = (a.get("name") as string) || "";
|
||||
const bName = (b.get("name") as string) || "";
|
||||
return aName.localeCompare(bName, "ru");
|
||||
});
|
||||
const sortFeaturesByType = <T extends Feature<Geometry>>(
|
||||
features: T[],
|
||||
sortType: SortType
|
||||
): T[] => {
|
||||
const sorted = [...features];
|
||||
switch (sortType) {
|
||||
case "name_asc":
|
||||
return sorted.sort((a, b) =>
|
||||
((a.get("name") as string) || "").localeCompare(
|
||||
((b.get("name") as string) || "")
|
||||
)
|
||||
);
|
||||
case "name_desc":
|
||||
return sorted.sort((a, b) =>
|
||||
((b.get("name") as string) || "").localeCompare(
|
||||
((a.get("name") as string) || "")
|
||||
)
|
||||
);
|
||||
case "date_asc":
|
||||
return sorted.sort((a, b) => {
|
||||
const aDate = a.get("created_at")
|
||||
? new Date(a.get("created_at"))
|
||||
: new Date(0);
|
||||
const bDate = b.get("created_at")
|
||||
? new Date(b.get("created_at"))
|
||||
: new Date(0);
|
||||
return aDate.getTime() - bDate.getTime();
|
||||
});
|
||||
case "date_desc":
|
||||
return sorted.sort((a, b) => {
|
||||
const aDate = a.get("created_at")
|
||||
? new Date(a.get("created_at"))
|
||||
: new Date(0);
|
||||
const bDate = b.get("created_at")
|
||||
? new Date(b.get("created_at"))
|
||||
: new Date(0);
|
||||
return bDate.getTime() - aDate.getTime();
|
||||
});
|
||||
default:
|
||||
return sorted;
|
||||
}
|
||||
};
|
||||
|
||||
const toggleSection = (id: string) =>
|
||||
setActiveSection(activeSection === id ? null : id);
|
||||
|
||||
const stations = filteredFeatures.filter(
|
||||
(f) => f.get("featureType") === "station"
|
||||
);
|
||||
const lines = filteredFeatures.filter(
|
||||
(f) => f.get("featureType") === "route"
|
||||
);
|
||||
const lines = filteredFeatures.filter((f) => f.get("featureType") === "route");
|
||||
const sights = filteredFeatures.filter(
|
||||
(f) => f.get("featureType") === "sight"
|
||||
);
|
||||
|
||||
const sortedStations = sortFeatures(stations, selectedIds, selectedFeature);
|
||||
const sortedLines = sortFeatures(lines, selectedIds, selectedFeature);
|
||||
const sortedSights = sortFeatures(sights, selectedIds, selectedFeature);
|
||||
const sortedStations = sortFeaturesByType(stations, stationSort);
|
||||
const sortedSights = sortFeaturesByType(sights, sightSort);
|
||||
|
||||
const renderFeatureList = (
|
||||
features: Feature<Geometry>[],
|
||||
@@ -2273,9 +2334,11 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
{features.length > 0 ? (
|
||||
features.map((feature) => {
|
||||
const fId = feature.getId();
|
||||
if (fId === undefined) return null; // TypeScript-safe
|
||||
const fName = (feature.get("name") as string) || "Без названия";
|
||||
const isSelected = selectedFeature?.getId() === fId;
|
||||
const isChecked = fId !== undefined && selectedIds.has(fId);
|
||||
const isChecked = selectedIds.has(fId);
|
||||
|
||||
return (
|
||||
<div
|
||||
key={String(fId)}
|
||||
@@ -2316,11 +2379,8 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
size={16}
|
||||
/>
|
||||
<span
|
||||
className={`font-medium truncate ${
|
||||
isSelected
|
||||
? "text-orange-600"
|
||||
: "group-hover:text-blue-600"
|
||||
}`}
|
||||
className={`font-medium whitespace-nowrap overflow-x-auto block
|
||||
scrollbar-thin scrollbar-thumb-gray-200 scrollbar-track-transparent`}
|
||||
title={fName}
|
||||
>
|
||||
{fName}
|
||||
@@ -2331,7 +2391,8 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
<button
|
||||
onClick={(e) => {
|
||||
e.stopPropagation();
|
||||
handleEditFeature(feature.get("featureType"), fId);
|
||||
const featureTypeVal = feature.get("featureType");
|
||||
if (featureTypeVal) handleEditFeature(featureTypeVal, fId);
|
||||
}}
|
||||
className="p-1 rounded-full text-gray-400 hover:text-blue-600 hover:bg-blue-100 transition-colors"
|
||||
title="Редактировать детали"
|
||||
@@ -2358,32 +2419,69 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
</div>
|
||||
);
|
||||
|
||||
const toggleSection = (id: string) =>
|
||||
setActiveSection(activeSection === id ? null : id);
|
||||
|
||||
const sections = [
|
||||
{
|
||||
id: "layers",
|
||||
title: `Остановки (${sortedStations.length})`,
|
||||
icon: <Bus size={20} />,
|
||||
count: sortedStations.length,
|
||||
content: renderFeatureList(sortedStations, "station", MapPin),
|
||||
content: (
|
||||
<>
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<label>Сортировка:</label>
|
||||
<select
|
||||
value={stationSort}
|
||||
onChange={(e) => setStationSort(e.target.value as SortType)}
|
||||
className="border rounded px-2 py-1 text-sm"
|
||||
>
|
||||
<option value="name_asc">Имя ↑</option>
|
||||
<option value="name_desc">Имя ↓</option>
|
||||
</select>
|
||||
</div>
|
||||
{renderFeatureList(sortedStations, "station", MapPin)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
{
|
||||
id: "lines",
|
||||
title: `Маршруты (${sortedLines.length})`,
|
||||
title: `Маршруты (${lines.length})`,
|
||||
icon: <RouteIcon size={20} />,
|
||||
count: sortedLines.length,
|
||||
content: renderFeatureList(sortedLines, "route", ArrowRightLeft),
|
||||
count: lines.length,
|
||||
content: renderFeatureList(lines, "route", ArrowRightLeft),
|
||||
},
|
||||
{
|
||||
id: "sights",
|
||||
title: `Достопримечательности (${sortedSights.length})`,
|
||||
icon: <Landmark size={20} />,
|
||||
count: sortedSights.length,
|
||||
content: renderFeatureList(sortedSights, "sight", Landmark),
|
||||
content: (
|
||||
<>
|
||||
<div className="flex items-center space-x-2 mb-2">
|
||||
<label>Сортировка:</label>
|
||||
<select
|
||||
value={sightSort}
|
||||
onChange={(e) => setSightSort(e.target.value as SortType)}
|
||||
className="border rounded px-2 py-1 text-sm"
|
||||
>
|
||||
<option value="name_asc">Имя ↑</option>
|
||||
<option value="name_desc">Имя ↓</option>
|
||||
</select>
|
||||
</div>
|
||||
{renderFeatureList(sortedSights, "sight", Landmark)}
|
||||
</>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
React.useEffect(() => {
|
||||
console.log("isOpen changed:", isOpen);
|
||||
}, [isOpen]);
|
||||
|
||||
return (
|
||||
<div className="w-[360px] relative bg-gray-50 shadow-lg flex flex-col border-l border-gray-200 h-[90vh]">
|
||||
<div className={`${isOpen ? "w-[360px]" : "w-[590px]"} transition-all duration-300 ease-in-out relative bg-gray-50 shadow-lg flex flex-col border-l border-gray-200 h-[90vh]`}>
|
||||
<div className="p-4 bg-gray-700 text-white">
|
||||
<h2 className="text-lg font-semibold">Панель управления</h2>
|
||||
</div>
|
||||
@@ -2396,8 +2494,7 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
className="w-full px-3 py-2 text-sm border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 flex flex-col min-h-0">
|
||||
<div className="flex-1 flex flex-col min-h-0 overflow-y-auto">
|
||||
{filteredFeatures.length === 0 && searchQuery ? (
|
||||
<div className="p-4 text-center text-gray-500">
|
||||
Ничего не найдено.
|
||||
@@ -2452,7 +2549,6 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
)
|
||||
)}
|
||||
</div>
|
||||
|
||||
{selectedIds.size > 0 && (
|
||||
<div className="p-3 border-t border-gray-200 bg-white flex-shrink-0">
|
||||
<button
|
||||
@@ -2466,7 +2562,7 @@ const MapSightbar: React.FC<MapSightbarProps> = ({
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
});
|
||||
// --- MAP PAGE COMPONENT ---
|
||||
export const MapPage: React.FC = () => {
|
||||
const mapRef = useRef<HTMLDivElement | null>(null);
|
||||
|
||||
@@ -301,10 +301,10 @@ export const RouteCreatePage = observer(() => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Селектор видео превью */}
|
||||
{/* Селектор видеозаставки */}
|
||||
<Box className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Видео превью
|
||||
Видеозаставка
|
||||
</label>
|
||||
<Box className="flex gap-2">
|
||||
<Box
|
||||
|
||||
@@ -307,10 +307,10 @@ export const RouteEditPage = observer(() => {
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* Селектор видео превью */}
|
||||
{/* Селектор видеозаставки */}
|
||||
<Box className="flex flex-col gap-2">
|
||||
<label className="text-sm font-medium text-gray-700">
|
||||
Видео превью
|
||||
Видеозаставка
|
||||
</label>
|
||||
<Box className="flex gap-2">
|
||||
<Box
|
||||
|
||||
@@ -6,7 +6,8 @@ import {
|
||||
MenuItem,
|
||||
FormControl,
|
||||
InputLabel,
|
||||
} from "@mui/material";
|
||||
}
|
||||
from "@mui/material";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { ArrowLeft, Save } from "lucide-react";
|
||||
import { Loader2 } from "lucide-react";
|
||||
@@ -15,6 +16,7 @@ import { toast } from "react-toastify";
|
||||
import { stationsStore, languageStore, cityStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LanguageSwitcher } from "@widgets";
|
||||
import { SaveWithoutCityAgree } from "@widgets";
|
||||
|
||||
export const StationCreatePage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
@@ -28,6 +30,8 @@ export const StationCreatePage = observer(() => {
|
||||
} = stationsStore;
|
||||
const { cities, getCities } = cityStore;
|
||||
const [coordinates, setCoordinates] = useState<string>("");
|
||||
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
|
||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
if (
|
||||
@@ -40,7 +44,8 @@ export const StationCreatePage = observer(() => {
|
||||
}
|
||||
}, [createStationData.common.latitude, createStationData.common.longitude]);
|
||||
|
||||
const handleCreate = async () => {
|
||||
// НОВАЯ ФУНКЦИЯ: Фактическое создание (вызывается после подтверждения)
|
||||
const executeCreate = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await createStation();
|
||||
@@ -54,6 +59,30 @@ export const StationCreatePage = observer(() => {
|
||||
}
|
||||
};
|
||||
|
||||
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или создания
|
||||
const handleCreate = async () => {
|
||||
const isCityMissing = !createStationData.common.city_id;
|
||||
const isNameMissing = !createStationData[language].name;
|
||||
|
||||
if (isCityMissing || isNameMissing) {
|
||||
setIsSaveWarningOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await executeCreate();
|
||||
};
|
||||
|
||||
// Обработчик "Да" в предупреждающем окне
|
||||
const handleConfirmCreate = async () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
await executeCreate();
|
||||
};
|
||||
|
||||
// Обработчик "Нет" в предупреждающем окне
|
||||
const handleCancelCreate = () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchCities = async () => {
|
||||
await getCities("ru");
|
||||
@@ -192,7 +221,7 @@ export const StationCreatePage = observer(() => {
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleCreate}
|
||||
disabled={isLoading || !createStationData[language]?.name}
|
||||
disabled={isLoading} // Убрал проверку name/city_id отсюда, чтобы ее обрабатывал handleCreate
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
@@ -201,6 +230,16 @@ export const StationCreatePage = observer(() => {
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */}
|
||||
{isSaveWarningOpen && (
|
||||
<SaveWithoutCityAgree
|
||||
blocker={{
|
||||
proceed: handleConfirmCreate,
|
||||
reset: handleCancelCreate,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
});
|
||||
});
|
||||
@@ -16,6 +16,7 @@ import { stationsStore, languageStore, cityStore } from "@shared";
|
||||
import { useEffect, useState } from "react";
|
||||
import { LanguageSwitcher } from "@widgets";
|
||||
import { LinkedSights } from "../LinkedSights";
|
||||
import { SaveWithoutCityAgree } from "@widgets";
|
||||
|
||||
export const StationEditPage = observer(() => {
|
||||
const navigate = useNavigate();
|
||||
@@ -31,9 +32,10 @@ export const StationEditPage = observer(() => {
|
||||
} = stationsStore;
|
||||
const { cities, getCities } = cityStore;
|
||||
const [coordinates, setCoordinates] = useState<string>("");
|
||||
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
|
||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||
|
||||
useEffect(() => {
|
||||
// Устанавливаем русский язык при загрузке страницы
|
||||
languageStore.setLanguage("ru");
|
||||
}, []);
|
||||
|
||||
@@ -48,7 +50,8 @@ export const StationEditPage = observer(() => {
|
||||
}
|
||||
}, [editStationData.common.latitude, editStationData.common.longitude]);
|
||||
|
||||
const handleEdit = async () => {
|
||||
// НОВАЯ ФУНКЦИЯ: Фактическое редактирование (вызывается после подтверждения)
|
||||
const executeEdit = async () => {
|
||||
try {
|
||||
setIsLoading(true);
|
||||
await editStation(Number(id));
|
||||
@@ -61,6 +64,30 @@ export const StationEditPage = observer(() => {
|
||||
}
|
||||
};
|
||||
|
||||
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или редактирования
|
||||
const handleEdit = async () => {
|
||||
const isCityMissing = !editStationData.common.city_id;
|
||||
const isNameMissing = !editStationData[language].name;
|
||||
|
||||
if (isCityMissing || isNameMissing) {
|
||||
setIsSaveWarningOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await executeEdit();
|
||||
};
|
||||
|
||||
// Обработчик "Да" в предупреждающем окне
|
||||
const handleConfirmEdit = async () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
await executeEdit();
|
||||
};
|
||||
|
||||
// Обработчик "Нет" в предупреждающем окне
|
||||
const handleCancelEdit = () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
const fetchAndSetStationData = async () => {
|
||||
if (!id) return;
|
||||
@@ -211,7 +238,7 @@ export const StationEditPage = observer(() => {
|
||||
className="w-min flex gap-2 items-center"
|
||||
startIcon={<Save size={20} />}
|
||||
onClick={handleEdit}
|
||||
disabled={isLoading || !editStationData[language]?.name}
|
||||
disabled={isLoading} // Убрал проверку name/city_id отсюда, чтобы ее обрабатывал handleEdit
|
||||
>
|
||||
{isLoading ? (
|
||||
<Loader2 size={20} className="animate-spin" />
|
||||
@@ -220,6 +247,16 @@ export const StationEditPage = observer(() => {
|
||||
)}
|
||||
</Button>
|
||||
</div>
|
||||
|
||||
{/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */}
|
||||
{isSaveWarningOpen && (
|
||||
<SaveWithoutCityAgree
|
||||
blocker={{
|
||||
proceed: handleConfirmEdit,
|
||||
reset: handleCancelEdit,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</Paper>
|
||||
);
|
||||
});
|
||||
});
|
||||
15
src/shared/store/MenuStore/index.ts
Normal file
15
src/shared/store/MenuStore/index.ts
Normal file
@@ -0,0 +1,15 @@
|
||||
import { makeAutoObservable } from "mobx";
|
||||
|
||||
class MenuStore {
|
||||
isOpen: boolean = true;
|
||||
|
||||
constructor() {
|
||||
makeAutoObservable(this);
|
||||
}
|
||||
|
||||
setIsMenuOpen = (isOpen: boolean) => {
|
||||
this.isOpen = isOpen;
|
||||
};
|
||||
}
|
||||
|
||||
export const menuStore = new MenuStore();
|
||||
@@ -14,3 +14,4 @@ export * from "./RouteStore";
|
||||
export * from "./UserStore";
|
||||
export * from "./CarrierStore";
|
||||
export * from "./StationsStore";
|
||||
export * from "./MenuStore"
|
||||
|
||||
@@ -8,7 +8,7 @@ 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 { authStore, userStore, menuStore } from "@shared";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect } from "react";
|
||||
import { Typography } from "@mui/material";
|
||||
@@ -20,6 +20,14 @@ interface LayoutProps {
|
||||
export const Layout: React.FC<LayoutProps> = observer(({ children }) => {
|
||||
const theme = useTheme();
|
||||
const [open, setOpen] = React.useState(true);
|
||||
const { setIsMenuOpen } = menuStore;
|
||||
|
||||
React.useEffect(() => {
|
||||
setIsMenuOpen(open);
|
||||
}, [open]);
|
||||
|
||||
|
||||
|
||||
const { getUsers, users } = userStore;
|
||||
|
||||
useEffect(() => {
|
||||
|
||||
23
src/widgets/SaveWithoutCityAgree/index.tsx
Normal file
23
src/widgets/SaveWithoutCityAgree/index.tsx
Normal file
@@ -0,0 +1,23 @@
|
||||
import { Button } from "@mui/material";
|
||||
|
||||
export const SaveWithoutCityAgree = ({ blocker }: { blocker: any }) => {
|
||||
return (
|
||||
<div className="fixed top-0 left-0 w-screen h-screen flex justify-center items-center z-10000 bg-black/30">
|
||||
<div className="bg-white p-4 w-140 rounded-lg flex flex-col gap-4 items-center">
|
||||
<p className="text-black w-140 text-center">
|
||||
Вы не указали город и/или не заполнили названия на всех языках.
|
||||
<br />
|
||||
Сохранить достопримечательность без этой информации?
|
||||
</p>
|
||||
<div className="flex gap-4 justify-center">
|
||||
<Button variant="contained" onClick={() => blocker.proceed()}>
|
||||
Да
|
||||
</Button>
|
||||
<Button variant="outlined" onClick={() => blocker.reset()}>
|
||||
Нет
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -35,8 +35,7 @@ import { Save } from "lucide-react";
|
||||
import { observer } from "mobx-react-lite";
|
||||
import { useEffect, useState } from "react";
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
// Мокап для всплывающей подсказки
|
||||
import { SaveWithoutCityAgree } from "@widgets";
|
||||
|
||||
export const CreateInformationTab = observer(
|
||||
({ value, index }: { value: number; index: number }) => {
|
||||
@@ -51,7 +50,6 @@ export const CreateInformationTab = observer(
|
||||
const [, setCity] = useState<number>(sight.city_id ?? 0);
|
||||
const [coordinates, setCoordinates] = useState<string>(`0, 0`);
|
||||
|
||||
// Menu state for each media button
|
||||
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [activeMenuType, setActiveMenuType] = useState<
|
||||
"thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null
|
||||
@@ -62,21 +60,15 @@ export const CreateInformationTab = observer(
|
||||
"thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null
|
||||
>(null);
|
||||
|
||||
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
|
||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||
|
||||
useEffect(() => {}, [hardcodeType]);
|
||||
// const handleMenuOpen = (
|
||||
// event: React.MouseEvent<HTMLElement>,
|
||||
// type: "thumbnail" | "watermark_lu" | "watermark_rd"
|
||||
// ) => {
|
||||
// setMenuAnchorEl(event.currentTarget);
|
||||
// setActiveMenuType(type);
|
||||
// };
|
||||
|
||||
useEffect(() => {
|
||||
// Показывать только при инициализации (не менять при ошибках пользователя)
|
||||
if (sight.latitude !== 0 || sight.longitude !== 0) {
|
||||
setCoordinates(`${sight.latitude}, ${sight.longitude}`);
|
||||
}
|
||||
// если координаты обнулились — оставить поле как есть
|
||||
}, [sight.latitude, sight.longitude]);
|
||||
|
||||
const handleMenuClose = () => {
|
||||
@@ -125,6 +117,29 @@ export const CreateInformationTab = observer(
|
||||
}
|
||||
};
|
||||
|
||||
const handleSave = async () => {
|
||||
const isCityMissing = !sight.city_id;
|
||||
const isNameMissing = !sight[language].name;
|
||||
|
||||
if (isCityMissing || isNameMissing) {
|
||||
setIsSaveWarningOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await createSight(language);
|
||||
toast.success("Достопримечательность создана");
|
||||
};
|
||||
|
||||
const handleConfirmSave = async () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
await createSight(language);
|
||||
toast.success("Достопримечательность создана");
|
||||
};
|
||||
|
||||
const handleCancelSave = () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabPanel value={value} index={index}>
|
||||
@@ -134,7 +149,7 @@ export const CreateInformationTab = observer(
|
||||
flexDirection: "column",
|
||||
gap: 3,
|
||||
position: "relative",
|
||||
paddingBottom: "70px" /* Space for save button */,
|
||||
paddingBottom: "70px",
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
|
||||
@@ -146,12 +161,11 @@ export const CreateInformationTab = observer(
|
||||
sx={{
|
||||
display: "flex",
|
||||
|
||||
gap: 4, // Added gap between the two main columns
|
||||
gap: 4,
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* Left column with main fields */}
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
@@ -215,14 +229,13 @@ export const CreateInformationTab = observer(
|
||||
value={coordinates}
|
||||
onChange={(e) => {
|
||||
const input = e.target.value;
|
||||
setCoordinates(input); // показываем как есть
|
||||
setCoordinates(input);
|
||||
|
||||
const [latStr, lonStr] = input.split(/\s+/); // учитываем любые пробелы
|
||||
const [latStr, lonStr] = input.split(/\s+/);
|
||||
|
||||
const lat = parseFloat(latStr);
|
||||
const lon = parseFloat(lonStr);
|
||||
|
||||
// Проверка, что обе координаты валидные числа
|
||||
const isValidLat = !isNaN(lat);
|
||||
const isValidLon = !isNaN(lon);
|
||||
|
||||
@@ -260,7 +273,7 @@ export const CreateInformationTab = observer(
|
||||
justifyContent: "space-around",
|
||||
width: "80%",
|
||||
gap: 2,
|
||||
flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
}}
|
||||
>
|
||||
<ImageUploadCard
|
||||
@@ -348,7 +361,7 @@ export const CreateInformationTab = observer(
|
||||
/>
|
||||
|
||||
<VideoPreviewCard
|
||||
title="Видео превью"
|
||||
title="Видеозаставка"
|
||||
videoId={sight.video_preview}
|
||||
onVideoClick={handleVideoPreviewClick}
|
||||
onDeleteVideoClick={() => {
|
||||
@@ -358,12 +371,10 @@ export const CreateInformationTab = observer(
|
||||
}}
|
||||
onSelectVideoClick={(file) => {
|
||||
if (file) {
|
||||
// Если передан файл, открываем диалог загрузки медиа
|
||||
createSightStore.setFileToUpload(file);
|
||||
setActiveMenuType("video_preview");
|
||||
setIsUploadMediaOpen(true);
|
||||
} else {
|
||||
// Если файл не передан, открываем диалог выбора существующих медиа
|
||||
setActiveMenuType("video_preview");
|
||||
setIsAddMediaOpen(true);
|
||||
}
|
||||
@@ -373,31 +384,25 @@ export const CreateInformationTab = observer(
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* LanguageSwitcher positioned at the top right */}
|
||||
|
||||
<LanguageSwitcher />
|
||||
|
||||
{/* Save Button fixed at the bottom right */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
padding: 2,
|
||||
backgroundColor: "background.paper", // To ensure it stands out over content
|
||||
width: "100%", // Take full width to cover content below it
|
||||
backgroundColor: "background.paper",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end", // Align to the right
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
startIcon={<Save color="white" size={18} />}
|
||||
onClick={async () => {
|
||||
await createSight(language);
|
||||
toast.success("Достопримечательность создана");
|
||||
}}
|
||||
onClick={handleSave}
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
@@ -405,7 +410,6 @@ export const CreateInformationTab = observer(
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
{/* Media Menu */}
|
||||
<MuiMenu
|
||||
anchorEl={menuAnchorEl}
|
||||
open={Boolean(menuAnchorEl)}
|
||||
@@ -471,7 +475,6 @@ export const CreateInformationTab = observer(
|
||||
initialFile={createSightStore.fileToUpload || undefined}
|
||||
/>
|
||||
|
||||
{/* Модальное окно предпросмотра видео */}
|
||||
{sight.video_preview && sight.video_preview !== "" && (
|
||||
<Dialog
|
||||
open={isVideoPreviewOpen}
|
||||
@@ -498,7 +501,17 @@ export const CreateInformationTab = observer(
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */}
|
||||
{isSaveWarningOpen && (
|
||||
<SaveWithoutCityAgree
|
||||
blocker={{
|
||||
proceed: handleConfirmSave,
|
||||
reset: handleCancelSave,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
);
|
||||
@@ -37,7 +37,8 @@ import { useEffect, useState } from "react";
|
||||
|
||||
import { toast } from "react-toastify";
|
||||
|
||||
// Мокап для всплывающей подсказки
|
||||
// Компонент предупреждающего окна (перенесен сюда)
|
||||
import { SaveWithoutCityAgree } from "@widgets";
|
||||
|
||||
export const InformationTab = observer(
|
||||
({ value, index }: { value: number; index: number }) => {
|
||||
@@ -51,7 +52,6 @@ export const InformationTab = observer(
|
||||
const [, setCity] = useState<number>(sight.common.city_id ?? 0);
|
||||
const [coordinates, setCoordinates] = useState<string>(`0, 0`);
|
||||
|
||||
// Menu state for each media button
|
||||
const [menuAnchorEl, setMenuAnchorEl] = useState<null | HTMLElement>(null);
|
||||
const [activeMenuType, setActiveMenuType] = useState<
|
||||
"thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null
|
||||
@@ -62,15 +62,15 @@ export const InformationTab = observer(
|
||||
"thumbnail" | "watermark_lu" | "watermark_rd" | "video_preview" | null
|
||||
>(null);
|
||||
const { cities } = cityStore;
|
||||
// НОВОЕ СОСТОЯНИЕ ДЛЯ ПРЕДУПРЕЖДАЮЩЕГО ОКНА
|
||||
const [isSaveWarningOpen, setIsSaveWarningOpen] = useState(false);
|
||||
|
||||
useEffect(() => {}, [hardcodeType]);
|
||||
|
||||
useEffect(() => {
|
||||
// Показывать только при инициализации (не менять при ошибках пользователя)
|
||||
if (sight.common.latitude !== 0 || sight.common.longitude !== 0) {
|
||||
setCoordinates(`${sight.common.latitude}, ${sight.common.longitude}`);
|
||||
}
|
||||
// если координаты обнулились — оставить поле как есть
|
||||
}, [sight.common.latitude, sight.common.longitude]);
|
||||
|
||||
const handleMenuClose = () => {
|
||||
@@ -119,6 +119,36 @@ export const InformationTab = observer(
|
||||
updateSightInfo(language, content, common);
|
||||
};
|
||||
|
||||
// НОВАЯ ФУНКЦИЯ: Фактическое сохранение (вызывается после подтверждения)
|
||||
const executeSave = async () => {
|
||||
await updateSight();
|
||||
toast.success("Достопримечательность сохранена");
|
||||
};
|
||||
|
||||
// ОБНОВЛЕННАЯ ФУНКЦИЯ: Проверка и вызов окна или сохранения
|
||||
const handleSave = async () => {
|
||||
const isCityMissing = !sight.common.city_id;
|
||||
const isNameMissing = !sight[language].name;
|
||||
|
||||
if (isCityMissing || isNameMissing) {
|
||||
setIsSaveWarningOpen(true);
|
||||
return;
|
||||
}
|
||||
|
||||
await executeSave();
|
||||
};
|
||||
|
||||
// Обработчик "Да" в предупреждающем окне
|
||||
const handleConfirmSave = async () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
await executeSave();
|
||||
};
|
||||
|
||||
// Обработчик "Нет" в предупреждающем окне
|
||||
const handleCancelSave = () => {
|
||||
setIsSaveWarningOpen(false);
|
||||
};
|
||||
|
||||
return (
|
||||
<>
|
||||
<TabPanel value={value} index={index}>
|
||||
@@ -128,7 +158,7 @@ export const InformationTab = observer(
|
||||
flexDirection: "column",
|
||||
gap: 3,
|
||||
position: "relative",
|
||||
paddingBottom: "70px" /* Space for save button */,
|
||||
paddingBottom: "70px",
|
||||
}}
|
||||
>
|
||||
<div className="flex gap-10 items-center mb-5 max-w-[80%]">
|
||||
@@ -141,12 +171,11 @@ export const InformationTab = observer(
|
||||
sx={{
|
||||
display: "flex",
|
||||
|
||||
gap: 4, // Added gap between the two main columns
|
||||
gap: 4,
|
||||
width: "100%",
|
||||
flexDirection: "column",
|
||||
}}
|
||||
>
|
||||
{/* Left column with main fields */}
|
||||
<Box
|
||||
sx={{
|
||||
flexGrow: 1,
|
||||
@@ -208,16 +237,14 @@ export const InformationTab = observer(
|
||||
value={coordinates}
|
||||
onChange={(e) => {
|
||||
const newValue = e.target.value;
|
||||
setCoordinates(newValue); // сохраняем ввод пользователя как есть
|
||||
setCoordinates(newValue);
|
||||
|
||||
// Обрабатываем значение для сохранения
|
||||
const input = newValue.replace(/,/g, " ").trim();
|
||||
const [latStr, lonStr] = input.split(/\s+/);
|
||||
|
||||
const lat = parseFloat(latStr);
|
||||
const lon = parseFloat(lonStr);
|
||||
|
||||
// Проверка, что обе координаты валидные числа
|
||||
const isValidLat = !isNaN(lat);
|
||||
const isValidLon = !isNaN(lon);
|
||||
|
||||
@@ -260,7 +287,7 @@ export const InformationTab = observer(
|
||||
justifyContent: "space-around",
|
||||
width: "80%",
|
||||
gap: 2,
|
||||
flexDirection: { xs: "column", sm: "row" }, // Stack on extra small, side-by-side on small and up
|
||||
flexDirection: { xs: "column", sm: "row" },
|
||||
}}
|
||||
>
|
||||
<ImageUploadCard
|
||||
@@ -358,7 +385,7 @@ export const InformationTab = observer(
|
||||
/>
|
||||
|
||||
<VideoPreviewCard
|
||||
title="Видео превью"
|
||||
title="Видеозаставка"
|
||||
videoId={sight.common.video_preview}
|
||||
onVideoClick={handleVideoPreviewClick}
|
||||
onDeleteVideoClick={() => {
|
||||
@@ -372,12 +399,10 @@ export const InformationTab = observer(
|
||||
}}
|
||||
onSelectVideoClick={(file) => {
|
||||
if (file) {
|
||||
// Если передан файл, открываем диалог загрузки медиа
|
||||
editSightStore.setFileToUpload(file);
|
||||
setActiveMenuType("video_preview");
|
||||
setIsUploadMediaOpen(true);
|
||||
} else {
|
||||
// Если файл не передан, открываем диалог выбора существующих медиа
|
||||
setActiveMenuType("video_preview");
|
||||
setIsAddMediaOpen(true);
|
||||
}
|
||||
@@ -387,31 +412,25 @@ export const InformationTab = observer(
|
||||
</Box>
|
||||
</Box>
|
||||
|
||||
{/* LanguageSwitcher positioned at the top right */}
|
||||
|
||||
<LanguageSwitcher />
|
||||
|
||||
{/* Save Button fixed at the bottom right */}
|
||||
<Box
|
||||
sx={{
|
||||
position: "absolute",
|
||||
bottom: 0,
|
||||
right: 0,
|
||||
padding: 2,
|
||||
backgroundColor: "background.paper", // To ensure it stands out over content
|
||||
width: "100%", // Take full width to cover content below it
|
||||
backgroundColor: "background.paper",
|
||||
width: "100%",
|
||||
display: "flex",
|
||||
justifyContent: "flex-end", // Align to the right
|
||||
justifyContent: "flex-end",
|
||||
}}
|
||||
>
|
||||
<Button
|
||||
variant="contained"
|
||||
color="success"
|
||||
startIcon={<Save color="white" size={18} />}
|
||||
onClick={async () => {
|
||||
await updateSight();
|
||||
toast.success("Достопримечательность сохранена");
|
||||
}}
|
||||
onClick={handleSave} // Используем новую функцию-обработчик
|
||||
>
|
||||
Сохранить
|
||||
</Button>
|
||||
@@ -419,7 +438,6 @@ export const InformationTab = observer(
|
||||
</Box>
|
||||
</TabPanel>
|
||||
|
||||
{/* Media Menu */}
|
||||
<MuiMenu
|
||||
anchorEl={menuAnchorEl}
|
||||
open={Boolean(menuAnchorEl)}
|
||||
@@ -492,7 +510,6 @@ export const InformationTab = observer(
|
||||
mediaId={mediaId}
|
||||
/>
|
||||
|
||||
{/* Модальное окно предпросмотра видео */}
|
||||
{sight.common.video_preview && sight.common.video_preview !== "" && (
|
||||
<Dialog
|
||||
open={isVideoPreviewOpen}
|
||||
@@ -519,7 +536,17 @@ export const InformationTab = observer(
|
||||
</DialogActions>
|
||||
</Dialog>
|
||||
)}
|
||||
|
||||
{/* ИНТЕГРИРОВАННОЕ ПРЕДУПРЕЖДАЮЩЕЕ ОКНО */}
|
||||
{isSaveWarningOpen && (
|
||||
<SaveWithoutCityAgree
|
||||
blocker={{
|
||||
proceed: handleConfirmSave,
|
||||
reset: handleCancelSave,
|
||||
}}
|
||||
/>
|
||||
)}
|
||||
</>
|
||||
);
|
||||
}
|
||||
);
|
||||
);
|
||||
@@ -17,4 +17,5 @@ export * from "./LeaveAgree";
|
||||
export * from "./DeleteModal";
|
||||
export * from "./SnapshotRestore";
|
||||
export * from "./CreateButton";
|
||||
export * from "./SaveWithoutCityAgree"
|
||||
export * from "./modals";
|
||||
|
||||
Reference in New Issue
Block a user