feat: add search for list pages
This commit is contained in:
@@ -1,7 +1,7 @@
|
|||||||
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, articlesStore, languageStore } from "@shared";
|
import { authStore, articlesStore, languageStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Trash2, Eye, Minus } from "lucide-react";
|
import { Trash2, Eye, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -22,6 +22,7 @@ export const ArticleListPage = observer(() => {
|
|||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
});
|
});
|
||||||
const canWriteArticles = authStore.canWrite("sights");
|
const canWriteArticles = authStore.canWrite("sights");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchArticles = async () => {
|
const fetchArticles = async () => {
|
||||||
@@ -72,11 +73,16 @@ export const ArticleListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = articleList[language].data.map((article) => ({
|
const rows = useMemo(() => {
|
||||||
id: article.id,
|
const query = searchQuery.toLowerCase();
|
||||||
heading: article.heading,
|
return articleList[language].data
|
||||||
body: article.body,
|
.filter((article) => !query || (article.heading ?? "").toLowerCase().includes(query))
|
||||||
}));
|
.map((article) => ({
|
||||||
|
id: article.id,
|
||||||
|
heading: article.heading,
|
||||||
|
body: article.body,
|
||||||
|
}));
|
||||||
|
}, [articleList[language].data, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -99,6 +105,8 @@ export const ArticleListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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, carrierStore, cityStore, languageStore } from "@shared";
|
import { authStore, carrierStore, cityStore, languageStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Pencil, Trash2, Minus } from "lucide-react";
|
import { Pencil, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -22,6 +22,7 @@ export const CarrierListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const canReadCities = authStore.canRead("cities");
|
const canReadCities = authStore.canRead("cities");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -119,16 +120,23 @@ export const CarrierListPage = observer(() => {
|
|||||||
|
|
||||||
const canWriteCarriers = authStore.canWrite("carriers");
|
const canWriteCarriers = authStore.canWrite("carriers");
|
||||||
|
|
||||||
const rows = carriers[language].data
|
const rows = useMemo(() => {
|
||||||
?.filter((carrier) =>
|
const query = searchQuery.toLowerCase();
|
||||||
!allowedCityIds || allowedCityIds.includes(carrier.city_id),
|
return (carriers[language].data ?? [])
|
||||||
)
|
.filter((carrier) => !allowedCityIds || allowedCityIds.includes(carrier.city_id))
|
||||||
.map((carrier) => ({
|
.filter(
|
||||||
id: carrier.id,
|
(carrier) =>
|
||||||
full_name: carrier.full_name,
|
!query ||
|
||||||
short_name: carrier.short_name,
|
(carrier.full_name ?? "").toLowerCase().includes(query) ||
|
||||||
city_id: carrier.city_id,
|
(carrier.short_name ?? "").toLowerCase().includes(query)
|
||||||
}));
|
)
|
||||||
|
.map((carrier) => ({
|
||||||
|
id: carrier.id,
|
||||||
|
full_name: carrier.full_name,
|
||||||
|
short_name: carrier.short_name,
|
||||||
|
city_id: carrier.city_id,
|
||||||
|
}));
|
||||||
|
}, [carriers[language].data, searchQuery, allowedCityIds]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -153,6 +161,8 @@ export const CarrierListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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, cityStore, countryStore } from "@shared";
|
import { authStore, languageStore, cityStore, countryStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Pencil, Trash2, Minus } from "lucide-react";
|
import { Pencil, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -24,6 +24,7 @@ export const CityListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const canWriteCities = authStore.canWrite("cities");
|
const canWriteCities = authStore.canWrite("cities");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -57,6 +58,18 @@ export const CityListPage = observer(() => {
|
|||||||
setRows(newRows2 || []);
|
setRows(newRows2 || []);
|
||||||
}, [cities, countryStore.countries, language, isLoading]);
|
}, [cities, countryStore.countries, language, isLoading]);
|
||||||
|
|
||||||
|
const filteredRows = useMemo(() => {
|
||||||
|
const query = searchQuery.toLowerCase();
|
||||||
|
if (!query) return rows;
|
||||||
|
return rows.filter((row) => {
|
||||||
|
const cityName = (row.name ?? "").toLowerCase();
|
||||||
|
const countryName = (
|
||||||
|
countryStore.countries[language]?.data?.find((c) => c.code === row.country)?.name ?? ""
|
||||||
|
).toLowerCase();
|
||||||
|
return cityName.includes(query) || countryName.includes(query);
|
||||||
|
});
|
||||||
|
}, [rows, searchQuery, countryStore.countries, language]);
|
||||||
|
|
||||||
const columns: GridColDef[] = [
|
const columns: GridColDef[] = [
|
||||||
{
|
{
|
||||||
field: "country",
|
field: "country",
|
||||||
@@ -142,8 +155,10 @@ export const CityListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={filteredRows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
checkboxSelection={canWriteCities}
|
checkboxSelection={canWriteCities}
|
||||||
disableRowSelectionExcludeModel
|
disableRowSelectionExcludeModel
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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, countryStore, languageStore } from "@shared";
|
import { authStore, countryStore, languageStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Trash2, Minus } from "lucide-react";
|
import { Trash2, Minus } from "lucide-react";
|
||||||
|
|
||||||
@@ -22,6 +22,7 @@ export const CountryListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const canWriteCountries = authStore.canWrite("countries");
|
const canWriteCountries = authStore.canWrite("countries");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchCountries = async () => {
|
const fetchCountries = async () => {
|
||||||
@@ -72,11 +73,16 @@ export const CountryListPage = observer(() => {
|
|||||||
}] : []),
|
}] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = countries[language]?.data.map((country) => ({
|
const rows = useMemo(() => {
|
||||||
id: country.code,
|
const query = searchQuery.toLowerCase();
|
||||||
code: country.code,
|
return (countries[language]?.data ?? [])
|
||||||
name: country.name,
|
.filter((country) => !query || (country.name ?? "").toLowerCase().includes(query))
|
||||||
}));
|
.map((country) => ({
|
||||||
|
id: country.code,
|
||||||
|
code: country.code,
|
||||||
|
name: country.name,
|
||||||
|
}));
|
||||||
|
}, [countries[language]?.data, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -102,8 +108,10 @@ export const CountryListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows || []}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
checkboxSelection={canWriteCountries}
|
checkboxSelection={canWriteCountries}
|
||||||
disableRowSelectionExcludeModel
|
disableRowSelectionExcludeModel
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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, MEDIA_TYPE_LABELS, mediaStore } from "@shared";
|
import { authStore, languageStore, MEDIA_TYPE_LABELS, mediaStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Trash2, Minus } from "lucide-react";
|
import { Eye, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -22,6 +22,7 @@ export const MediaListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const canWriteMedia = authStore.canWrite("sights");
|
const canWriteMedia = authStore.canWrite("sights");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchMedia = async () => {
|
const fetchMedia = async () => {
|
||||||
@@ -95,15 +96,22 @@ export const MediaListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = media.map((media) => ({
|
const rows = useMemo(() => {
|
||||||
id: media.id,
|
const query = searchQuery.toLowerCase();
|
||||||
media_name: media.media_name,
|
return media
|
||||||
media_type: media.media_type,
|
.filter((item) => !query || (item.media_name ?? "").toLowerCase().includes(query))
|
||||||
}));
|
.map((item) => ({
|
||||||
|
id: item.id,
|
||||||
|
media_name: item.media_name,
|
||||||
|
media_type: item.media_type,
|
||||||
|
}));
|
||||||
|
}, [media, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div className="w-full">
|
<div className="w-full">
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
{canWriteMedia && ids.length > 0 && (
|
{canWriteMedia && ids.length > 0 && (
|
||||||
<div className="flex justify-end mb-5 duration-300">
|
<div className="flex justify-end mb-5 duration-300">
|
||||||
<button
|
<button
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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, carrierStore, languageStore, routeStore, selectedCityStore } from "@shared";
|
import { authStore, carrierStore, languageStore, routeStore, selectedCityStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Map, Pencil, Trash2, Minus } from "lucide-react";
|
import { Map, Pencil, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -28,6 +28,7 @@ export const RouteListPage = observer(() => {
|
|||||||
authStore.canWrite("sights") &&
|
authStore.canWrite("sights") &&
|
||||||
authStore.canWrite("routes");
|
authStore.canWrite("routes");
|
||||||
const canShowActionsColumn = canWriteRoutes || canShowRoutePreview;
|
const canShowActionsColumn = canWriteRoutes || canShowRoutePreview;
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -146,26 +147,33 @@ export const RouteListPage = observer(() => {
|
|||||||
}] : []),
|
}] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const filteredRoutes = () => {
|
const rows = useMemo(() => {
|
||||||
const { selectedCityId } = selectedCityStore;
|
const { selectedCityId } = selectedCityStore;
|
||||||
if (!selectedCityId) {
|
const query = searchQuery.toLowerCase();
|
||||||
return routes.data;
|
let filtered = routes.data;
|
||||||
|
if (selectedCityId) {
|
||||||
|
const cityCarrierIds = new Set(
|
||||||
|
carriers["ru"].data
|
||||||
|
.filter((c) => c.city_id === selectedCityId)
|
||||||
|
.map((c) => c.id)
|
||||||
|
);
|
||||||
|
filtered = filtered.filter((route) => cityCarrierIds.has(route.carrier_id));
|
||||||
}
|
}
|
||||||
const cityCarrierIds = new Set(
|
return filtered
|
||||||
carriers["ru"].data
|
.filter(
|
||||||
.filter((c) => c.city_id === selectedCityId)
|
(route) =>
|
||||||
.map((c) => c.id)
|
!query ||
|
||||||
);
|
(route.route_name ?? "").toLowerCase().includes(query) ||
|
||||||
return routes.data.filter((route) => cityCarrierIds.has(route.carrier_id));
|
String(route.route_number ?? "").toLowerCase().includes(query)
|
||||||
};
|
)
|
||||||
|
.map((route) => ({
|
||||||
const rows = filteredRoutes().map((route) => ({
|
id: route.id,
|
||||||
id: route.id,
|
carrier_id: route.carrier_id,
|
||||||
carrier_id: route.carrier_id,
|
route_number: route.route_number,
|
||||||
route_number: route.route_number,
|
route_direction: route.route_direction ? "Прямой" : "Обратный",
|
||||||
route_direction: route.route_direction ? "Прямой" : "Обратный",
|
route_name: route.route_name,
|
||||||
route_name: route.route_name,
|
}));
|
||||||
}));
|
}, [routes.data, carriers["ru"].data, selectedCityStore.selectedCityId, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -191,6 +199,8 @@ export const RouteListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import {
|
|||||||
languageStore,
|
languageStore,
|
||||||
sightsStore,
|
sightsStore,
|
||||||
selectedCityStore,
|
selectedCityStore,
|
||||||
|
SearchInput,
|
||||||
} from "@shared";
|
} 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";
|
||||||
@@ -28,6 +29,7 @@ export const SightListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const canReadCities = authStore.canRead("cities");
|
const canReadCities = authStore.canRead("cities");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchSights = async () => {
|
const fetchSights = async () => {
|
||||||
@@ -120,13 +122,16 @@ export const SightListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
}, [sights, selectedCityStore.selectedCityId, canReadCities, authStore.meCities]);
|
}, [sights, selectedCityStore.selectedCityId, canReadCities, authStore.meCities]);
|
||||||
|
|
||||||
const canWriteSights = authStore.canWrite("sights");
|
const query = searchQuery.toLowerCase();
|
||||||
|
const rows = filteredSights
|
||||||
|
.filter((sight: any) => !query || (sight.name ?? "").toLowerCase().includes(query))
|
||||||
|
.map((sight) => ({
|
||||||
|
id: sight.id,
|
||||||
|
name: sight.name,
|
||||||
|
city_id: sight.city_id,
|
||||||
|
}));
|
||||||
|
|
||||||
const rows = filteredSights.map((sight) => ({
|
const canWriteSights = authStore.canWrite("sights");
|
||||||
id: sight.id,
|
|
||||||
name: sight.name,
|
|
||||||
city_id: sight.city_id,
|
|
||||||
}));
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -155,6 +160,8 @@ export const SightListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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 } from "@shared";
|
import { authStore, languageStore, snapshotStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } 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";
|
||||||
import { CreateButton, DeleteModal, SnapshotRestore } from "@widgets";
|
import { CreateButton, DeleteModal, SnapshotRestore } from "@widgets";
|
||||||
@@ -19,6 +19,7 @@ export const SnapshotListPage = observer(() => {
|
|||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
|
const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false);
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
const [paginationModel, setPaginationModel] = useState({
|
const [paginationModel, setPaginationModel] = useState({
|
||||||
page: 0,
|
page: 0,
|
||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
@@ -89,22 +90,35 @@ export const SnapshotListPage = observer(() => {
|
|||||||
}] : []),
|
}] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = snapshots.map((snapshot) => ({
|
const rows = useMemo(() => {
|
||||||
id: snapshot.ID,
|
const query = searchQuery.toLowerCase();
|
||||||
name: snapshot.Name,
|
return snapshots
|
||||||
parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name,
|
.filter(
|
||||||
created_at: formatCreationTime(snapshot.CreationTime),
|
(snapshot) =>
|
||||||
}));
|
!query ||
|
||||||
|
(snapshot.Name ?? "").toLowerCase().includes(query) ||
|
||||||
|
(snapshots.find((s) => s.ID === snapshot.ParentID)?.Name ?? "").toLowerCase().includes(query)
|
||||||
|
)
|
||||||
|
.map((snapshot) => ({
|
||||||
|
id: snapshot.ID,
|
||||||
|
name: snapshot.Name,
|
||||||
|
parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name,
|
||||||
|
created_at: formatCreationTime(snapshot.CreationTime),
|
||||||
|
}));
|
||||||
|
}, [snapshots, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl ">Экспорт Медиа</h1>
|
<h1 className="text-2xl ">Экспорт Медиа</h1>
|
||||||
|
|
||||||
{canCreateSnapshot && (
|
{canCreateSnapshot && (
|
||||||
<CreateButton label="Создать экспорт медиа" path="/snapshot/create" />
|
<CreateButton label="Создать экспорт медиа" path="/snapshot/create" />
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -5,8 +5,9 @@ import {
|
|||||||
languageStore,
|
languageStore,
|
||||||
stationsStore,
|
stationsStore,
|
||||||
selectedCityStore,
|
selectedCityStore,
|
||||||
|
SearchInput,
|
||||||
} from "@shared";
|
} from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Pencil, Trash2, Minus, Route } from "lucide-react";
|
import { Pencil, Trash2, Minus, Route } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -36,6 +37,7 @@ export const StationListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const canWriteStations = authStore.canWrite("stations");
|
const canWriteStations = authStore.canWrite("stations");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchStations = async () => {
|
const fetchStations = async () => {
|
||||||
@@ -121,21 +123,23 @@ export const StationListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const filteredStations = () => {
|
const rows = useMemo(() => {
|
||||||
const { selectedCityId } = selectedCityStore;
|
const { selectedCityId } = selectedCityStore;
|
||||||
if (!selectedCityId) {
|
const query = searchQuery.toLowerCase();
|
||||||
return stationLists[language].data;
|
return stationLists[language].data
|
||||||
}
|
.filter((station: any) => !selectedCityId || station.city_id === selectedCityId)
|
||||||
return stationLists[language].data.filter(
|
.filter(
|
||||||
(station: any) => station.city_id === selectedCityId
|
(station: any) =>
|
||||||
);
|
!query ||
|
||||||
};
|
(station.name ?? "").toLowerCase().includes(query) ||
|
||||||
|
(station.description ?? "").toLowerCase().includes(query)
|
||||||
const rows = filteredStations().map((station: any) => ({
|
)
|
||||||
id: station.id,
|
.map((station: any) => ({
|
||||||
name: station.name,
|
id: station.id,
|
||||||
description: station.description,
|
name: station.name,
|
||||||
}));
|
description: station.description,
|
||||||
|
}));
|
||||||
|
}, [stationLists[language].data, selectedCityStore.selectedCityId, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -161,6 +165,8 @@ export const StationListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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, userStore } from "@shared";
|
import { authStore, userStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Pencil, Trash2, Minus } from "lucide-react";
|
import { Pencil, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -21,6 +21,7 @@ export const UserListPage = observer(() => {
|
|||||||
pageSize: 50,
|
pageSize: 50,
|
||||||
});
|
});
|
||||||
const canWriteUsers = authStore.canWrite("users");
|
const canWriteUsers = authStore.canWrite("users");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchUsers = async () => {
|
const fetchUsers = async () => {
|
||||||
@@ -107,12 +108,21 @@ export const UserListPage = observer(() => {
|
|||||||
}] : []),
|
}] : []),
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = users.data?.map((user) => ({
|
const rows = useMemo(() => {
|
||||||
id: user.id,
|
const query = searchQuery.toLowerCase();
|
||||||
email: user.email,
|
return (users.data ?? [])
|
||||||
is_admin: user.is_admin || (user.roles ?? []).includes("admin"),
|
.filter((user) =>
|
||||||
name: user.name,
|
!query ||
|
||||||
}));
|
(user.name ?? "").toLowerCase().includes(query) ||
|
||||||
|
(user.email ?? "").toLowerCase().includes(query)
|
||||||
|
)
|
||||||
|
.map((user) => ({
|
||||||
|
id: user.id,
|
||||||
|
email: user.email,
|
||||||
|
is_admin: user.is_admin || (user.roles ?? []).includes("admin"),
|
||||||
|
name: user.name,
|
||||||
|
}));
|
||||||
|
}, [users.data, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
@@ -136,6 +146,8 @@ export const UserListPage = observer(() => {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
<DataGrid
|
<DataGrid
|
||||||
rows={rows}
|
rows={rows}
|
||||||
columns={columns}
|
columns={columns}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
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, carrierStore, languageStore, vehicleStore } from "@shared";
|
import { authStore, carrierStore, languageStore, vehicleStore, SearchInput } from "@shared";
|
||||||
import { useEffect, useState } from "react";
|
import { useEffect, useState, useMemo } from "react";
|
||||||
import { observer } from "mobx-react-lite";
|
import { observer } from "mobx-react-lite";
|
||||||
import { Eye, Pencil, Trash2, Minus } from "lucide-react";
|
import { Eye, Pencil, Trash2, Minus } from "lucide-react";
|
||||||
import { useNavigate } from "react-router-dom";
|
import { useNavigate } from "react-router-dom";
|
||||||
@@ -24,6 +24,7 @@ export const VehicleListPage = observer(() => {
|
|||||||
});
|
});
|
||||||
const { language } = languageStore;
|
const { language } = languageStore;
|
||||||
const canWriteVehicles = authStore.canWrite("devices");
|
const canWriteVehicles = authStore.canWrite("devices");
|
||||||
|
const [searchQuery, setSearchQuery] = useState("");
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const fetchData = async () => {
|
const fetchData = async () => {
|
||||||
@@ -136,27 +137,40 @@ export const VehicleListPage = observer(() => {
|
|||||||
},
|
},
|
||||||
];
|
];
|
||||||
|
|
||||||
const rows = vehicles.data?.map((vehicle) => ({
|
const rows = useMemo(() => {
|
||||||
id: vehicle.vehicle.id,
|
const query = searchQuery.toLowerCase();
|
||||||
tail_number: vehicle.vehicle.tail_number,
|
return (vehicles.data ?? [])
|
||||||
type: vehicle.vehicle.type,
|
.filter(
|
||||||
carrier: vehicle.vehicle.carrier,
|
(vehicle) =>
|
||||||
city: carriers[language].data?.find(
|
!query ||
|
||||||
(carrier) => carrier.id === vehicle.vehicle.carrier_id
|
(vehicle.vehicle.tail_number ?? "").toLowerCase().includes(query) ||
|
||||||
)?.city,
|
(vehicle.vehicle.carrier ?? "").toLowerCase().includes(query)
|
||||||
}));
|
)
|
||||||
|
.map((vehicle) => ({
|
||||||
|
id: vehicle.vehicle.id,
|
||||||
|
tail_number: vehicle.vehicle.tail_number,
|
||||||
|
type: vehicle.vehicle.type,
|
||||||
|
carrier: vehicle.vehicle.carrier,
|
||||||
|
city: carriers[language].data?.find(
|
||||||
|
(carrier) => carrier.id === vehicle.vehicle.carrier_id
|
||||||
|
)?.city,
|
||||||
|
}));
|
||||||
|
}, [vehicles.data, carriers[language].data, searchQuery]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<div style={{ width: "100%" }}>
|
<div style={{ width: "100%" }}>
|
||||||
<div className="flex justify-between items-center mb-10">
|
<div className="flex justify-between items-center mb-10">
|
||||||
<h1 className="text-2xl">Транспортные средства</h1>
|
<h1 className="text-2xl">Транспортные средства</h1>
|
||||||
|
|
||||||
<CreateButton
|
<CreateButton
|
||||||
label="Создать транспортное средство"
|
label="Создать транспортное средство"
|
||||||
path="/vehicle/create"
|
path="/vehicle/create"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<SearchInput value={searchQuery} onChange={setSearchQuery} />
|
||||||
|
|
||||||
{canWriteVehicles && ids.length > 0 && (
|
{canWriteVehicles && ids.length > 0 && (
|
||||||
<div className="flex justify-end mb-5 duration-300">
|
<div className="flex justify-end mb-5 duration-300">
|
||||||
<button
|
<button
|
||||||
|
|||||||
37
src/shared/ui/SearchInput/index.tsx
Normal file
37
src/shared/ui/SearchInput/index.tsx
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
import { Search, X } from "lucide-react";
|
||||||
|
|
||||||
|
interface SearchInputProps {
|
||||||
|
value: string;
|
||||||
|
onChange: (value: string) => void;
|
||||||
|
placeholder?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const SearchInput = ({
|
||||||
|
value,
|
||||||
|
onChange,
|
||||||
|
placeholder = "Поиск...",
|
||||||
|
}: SearchInputProps) => {
|
||||||
|
return (
|
||||||
|
<div className="relative mb-4 w-full max-w-sm">
|
||||||
|
<Search
|
||||||
|
size={16}
|
||||||
|
className="absolute left-3 top-1/2 -translate-y-1/2 text-gray-400 pointer-events-none"
|
||||||
|
/>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={value}
|
||||||
|
onChange={(e) => onChange(e.target.value)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
className="w-full pl-9 pr-8 py-2 border border-gray-300 rounded-md text-sm outline-none focus:border-blue-500 focus:ring-1 focus:ring-blue-500 bg-white"
|
||||||
|
/>
|
||||||
|
{value && (
|
||||||
|
<button
|
||||||
|
onClick={() => onChange("")}
|
||||||
|
className="absolute right-2 top-1/2 -translate-y-1/2 text-gray-400 hover:text-gray-600"
|
||||||
|
>
|
||||||
|
<X size={14} />
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -5,3 +5,4 @@ export * from "./CoordinatesInput";
|
|||||||
export * from "./AnimatedCircleButton";
|
export * from "./AnimatedCircleButton";
|
||||||
export * from "./LoadingSpinner";
|
export * from "./LoadingSpinner";
|
||||||
export * from "./MultiSelect";
|
export * from "./MultiSelect";
|
||||||
|
export * from "./SearchInput";
|
||||||
|
|||||||
Reference in New Issue
Block a user