feat: add city name in snaphost name

This commit is contained in:
2026-05-23 10:30:36 +03:00
parent 1bb3f43979
commit 2659c6a5b8
3 changed files with 77 additions and 7 deletions

View File

@@ -17,6 +17,7 @@ import {
countryStore, countryStore,
languageStore, languageStore,
mediaStore, mediaStore,
snapshotStore,
isMediaIdEmpty, isMediaIdEmpty,
SelectMediaDialog, SelectMediaDialog,
UploadMediaDialog, UploadMediaDialog,
@@ -60,7 +61,13 @@ export const CityCreatePage = observer(() => {
const handleCreate = async () => { const handleCreate = async () => {
try { try {
setIsLoading(true); setIsLoading(true);
const ruCityName = createCityData.ru.name.trim();
await cityStore.createCity(); await cityStore.createCity();
try {
await snapshotStore.createEmptySnapshot(`${ruCityName}устой_Экспорт`);
} catch (e) {
console.warn("Failed to create empty snapshot for city:", e);
}
toast.success("Город успешно создан"); toast.success("Город успешно создан");
navigate("/city"); navigate("/city");
} catch (error) { } catch (error) {

View File

@@ -6,14 +6,24 @@ import {
DialogContent, DialogContent,
DialogActions, DialogActions,
} from "@mui/material"; } from "@mui/material";
import { snapshotStore, authStore, routeStore, selectedCityStore } from "@shared"; import { snapshotStore, authStore, routeStore, selectedCityStore, cityStore } from "@shared";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { ArrowLeft, Loader2, Save } from "lucide-react"; import { ArrowLeft, Loader2, Save } from "lucide-react";
import { useState, useEffect } from "react"; import { useState, useEffect, useMemo } from "react";
import { useNavigate } from "react-router-dom"; import { useNavigate } from "react-router-dom";
import { toast } from "react-toastify"; import { toast } from "react-toastify";
import { runInAction } from "mobx"; import { runInAction } from "mobx";
function escapeRegex(s: string) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function buildExportNameRegex(cityNames: string[]): RegExp {
if (!cityNames.length) return /.+/;
const pattern = cityNames.map(escapeRegex).join("|");
return new RegExp(`^(${pattern})_.+$`);
}
export const SnapshotCreatePage = observer(() => { export const SnapshotCreatePage = observer(() => {
const { createSnapshot, getSnapshotStatus, getStorageInfo, snapshotStatus } = snapshotStore; const { createSnapshot, getSnapshotStatus, getStorageInfo, snapshotStatus } = snapshotStore;
const navigate = useNavigate(); const navigate = useNavigate();
@@ -24,10 +34,22 @@ export const SnapshotCreatePage = observer(() => {
}, []); }, []);
const [name, setName] = useState(""); const [name, setName] = useState("");
const [nameError, setNameError] = useState<string | null>(null);
const [isLoading, setIsLoading] = useState(false); const [isLoading, setIsLoading] = useState(false);
const [duplicateWarningOpen, setDuplicateWarningOpen] = useState(false); const [duplicateWarningOpen, setDuplicateWarningOpen] = useState(false);
const [duplicateRouteNumbers, setDuplicateRouteNumbers] = useState<string[]>([]); const [duplicateRouteNumbers, setDuplicateRouteNumbers] = useState<string[]>([]);
const exportNameRegex = useMemo(() => {
const names = cityStore.cities["ru"].data.map((c) => c.name.trim());
return buildExportNameRegex(names);
}, [cityStore.cities["ru"].data.length]);
useEffect(() => {
if (!cityStore.cities["ru"].loaded) {
cityStore.getCities("ru");
}
}, []);
const canReadRoutes = authStore.canRead("routes"); const canReadRoutes = authStore.canRead("routes");
const startExport = async () => { const startExport = async () => {
@@ -115,7 +137,19 @@ export const SnapshotCreatePage = observer(() => {
label="Название" label="Название"
required required
value={name} value={name}
onChange={(e) => setName(e.target.value)} error={!!nameError}
helperText={nameError ?? " "}
onChange={(e) => {
const val = e.target.value;
setName(val);
const trimmed = val.trim();
const hasFullFormat = trimmed.includes("_") && trimmed.split("_").slice(1).join("_").length > 0;
if (hasFullFormat && !exportNameRegex.test(trimmed)) {
setNameError("Название должно начинаться с названия существующего города");
} else {
setNameError(null);
}
}}
/> />
<Button <Button
@@ -124,7 +158,7 @@ export const SnapshotCreatePage = observer(() => {
className="w-min flex gap-2 items-center" className="w-min flex gap-2 items-center"
startIcon={<Save size={20} />} startIcon={<Save size={20} />}
onClick={handleSave} onClick={handleSave}
disabled={isLoading || !name.trim()} disabled={isLoading || !exportNameRegex.test(name.trim())}
> >
{isLoading ? ( {isLoading ? (
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">

View File

@@ -1,6 +1,6 @@
import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid";
import { ruRU } from "@mui/x-data-grid/locales"; import { ruRU } from "@mui/x-data-grid/locales";
import { authStore, languageStore, snapshotStore, SearchInput } from "@shared"; import { authStore, languageStore, snapshotStore, cityStore, SearchInput } from "@shared";
import { useEffect, useState, useMemo } from "react"; import { useEffect, useState, useMemo } from "react";
import { observer } from "mobx-react-lite"; import { observer } from "mobx-react-lite";
import { DatabaseBackup, Trash2 } from "lucide-react"; import { DatabaseBackup, Trash2 } from "lucide-react";
@@ -9,6 +9,16 @@ import { Alert, Box, Button, CircularProgress, Dialog, DialogActions, DialogCont
const LOW_STORAGE_THRESHOLD_GB = 10; const LOW_STORAGE_THRESHOLD_GB = 10;
function escapeRegex(s: string) {
return s.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
}
function buildExportNameRegex(cityNames: string[]): RegExp {
if (!cityNames.length) return /.+/;
const pattern = cityNames.map(escapeRegex).join("|");
return new RegExp(`^(${pattern})_.+$`);
}
const SEGMENT_COLORS = [ const SEGMENT_COLORS = [
"#FF3B30", "#FF3B30",
"#FF9500", "#FF9500",
@@ -45,6 +55,12 @@ export const SnapshotListPage = observer(() => {
const [searchQuery, setSearchQuery] = useState(""); const [searchQuery, setSearchQuery] = useState("");
const [isEmptySnapshotModalOpen, setIsEmptySnapshotModalOpen] = useState(false); const [isEmptySnapshotModalOpen, setIsEmptySnapshotModalOpen] = useState(false);
const [emptySnapshotName, setEmptySnapshotName] = useState(""); const [emptySnapshotName, setEmptySnapshotName] = useState("");
const [emptySnapshotNameError, setEmptySnapshotNameError] = useState<string | null>(null);
const exportNameRegex = useMemo(() => {
const names = cityStore.cities["ru"].data.map((c) => c.name.trim());
return buildExportNameRegex(names);
}, [cityStore.cities["ru"].data.length]);
const [isCreatingEmpty, setIsCreatingEmpty] = useState(false); const [isCreatingEmpty, setIsCreatingEmpty] = useState(false);
const [paginationModel, setPaginationModel] = useState({ const [paginationModel, setPaginationModel] = useState({
page: 0, page: 0,
@@ -201,6 +217,7 @@ export const SnapshotListPage = observer(() => {
disabled={isLowStorage} disabled={isLowStorage}
onClick={() => { onClick={() => {
setEmptySnapshotName(""); setEmptySnapshotName("");
setEmptySnapshotNameError(null);
setIsEmptySnapshotModalOpen(true); setIsEmptySnapshotModalOpen(true);
}} }}
> >
@@ -353,7 +370,19 @@ export const SnapshotListPage = observer(() => {
fullWidth fullWidth
label="Название" label="Название"
value={emptySnapshotName} value={emptySnapshotName}
onChange={(e) => setEmptySnapshotName(e.target.value)} error={!!emptySnapshotNameError}
helperText={emptySnapshotNameError ?? " "}
onChange={(e) => {
const val = e.target.value;
setEmptySnapshotName(val);
const trimmed = val.trim();
const hasFullFormat = trimmed.includes("_") && trimmed.split("_").slice(1).join("_").length > 0;
if (hasFullFormat && !exportNameRegex.test(trimmed)) {
setEmptySnapshotNameError("Название должно начинаться с названия существующего города");
} else {
setEmptySnapshotNameError(null);
}
}}
margin="normal" margin="normal"
/> />
</DialogContent> </DialogContent>
@@ -363,7 +392,7 @@ export const SnapshotListPage = observer(() => {
</Button> </Button>
<Button <Button
variant="contained" variant="contained"
disabled={!emptySnapshotName.trim() || isCreatingEmpty} disabled={!exportNameRegex.test(emptySnapshotName.trim()) || isCreatingEmpty}
onClick={async () => { onClick={async () => {
setIsCreatingEmpty(true); setIsCreatingEmpty(true);
try { try {