From a58f438dce04bea3679c1db76ac0e6c972d5f0b3 Mon Sep 17 00:00:00 2001 From: itoshi Date: Sun, 5 Apr 2026 15:31:42 +0300 Subject: [PATCH] feat: add search for list pages --- src/pages/Article/ArticleListPage/index.tsx | 22 +++++--- src/pages/Carrier/CarrierListPage/index.tsx | 34 ++++++++----- src/pages/City/CityListPage/index.tsx | 21 ++++++-- src/pages/Country/CountryListPage/index.tsx | 24 ++++++--- src/pages/Media/MediaListPage/index.tsx | 22 +++++--- src/pages/Route/RouteListPage/index.tsx | 50 +++++++++++-------- src/pages/Sight/SightListPage/index.tsx | 19 ++++--- src/pages/Snapshot/SnapshotListPage/index.tsx | 30 ++++++++--- src/pages/Station/StationListPage/index.tsx | 36 +++++++------ src/pages/User/UserListPage/index.tsx | 28 ++++++++--- src/pages/Vehicle/VehicleListPage/index.tsx | 36 +++++++++---- src/shared/ui/SearchInput/index.tsx | 37 ++++++++++++++ src/shared/ui/index.ts | 1 + 13 files changed, 255 insertions(+), 105 deletions(-) create mode 100644 src/shared/ui/SearchInput/index.tsx diff --git a/src/pages/Article/ArticleListPage/index.tsx b/src/pages/Article/ArticleListPage/index.tsx index 118ecbc..46bda00 100644 --- a/src/pages/Article/ArticleListPage/index.tsx +++ b/src/pages/Article/ArticleListPage/index.tsx @@ -1,7 +1,7 @@ import { DataGrid, GridColDef, GridRenderCellParams } from "@mui/x-data-grid"; import { ruRU } from "@mui/x-data-grid/locales"; -import { authStore, articlesStore, languageStore } from "@shared"; -import { useEffect, useState } from "react"; +import { authStore, articlesStore, languageStore, SearchInput } from "@shared"; +import { useEffect, useState, useMemo } from "react"; import { observer } from "mobx-react-lite"; import { Trash2, Eye, Minus } from "lucide-react"; import { useNavigate } from "react-router-dom"; @@ -22,6 +22,7 @@ export const ArticleListPage = observer(() => { pageSize: 50, }); const canWriteArticles = authStore.canWrite("sights"); + const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { const fetchArticles = async () => { @@ -72,11 +73,16 @@ export const ArticleListPage = observer(() => { }, ]; - const rows = articleList[language].data.map((article) => ({ - id: article.id, - heading: article.heading, - body: article.body, - })); + const rows = useMemo(() => { + const query = searchQuery.toLowerCase(); + return articleList[language].data + .filter((article) => !query || (article.heading ?? "").toLowerCase().includes(query)) + .map((article) => ({ + id: article.id, + heading: article.heading, + body: article.body, + })); + }, [articleList[language].data, searchQuery]); return ( <> @@ -99,6 +105,8 @@ export const ArticleListPage = observer(() => { )} + +
{ }); const { language } = languageStore; const canReadCities = authStore.canRead("cities"); + const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { const fetchData = async () => { @@ -119,16 +120,23 @@ export const CarrierListPage = observer(() => { const canWriteCarriers = authStore.canWrite("carriers"); - const rows = carriers[language].data - ?.filter((carrier) => - !allowedCityIds || allowedCityIds.includes(carrier.city_id), - ) - .map((carrier) => ({ - id: carrier.id, - full_name: carrier.full_name, - short_name: carrier.short_name, - city_id: carrier.city_id, - })); + const rows = useMemo(() => { + const query = searchQuery.toLowerCase(); + return (carriers[language].data ?? []) + .filter((carrier) => !allowedCityIds || allowedCityIds.includes(carrier.city_id)) + .filter( + (carrier) => + !query || + (carrier.full_name ?? "").toLowerCase().includes(query) || + (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 ( <> @@ -153,6 +161,8 @@ export const CarrierListPage = observer(() => {
)} + + { }); const { language } = languageStore; const canWriteCities = authStore.canWrite("cities"); + const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { const fetchData = async () => { @@ -57,6 +58,18 @@ export const CityListPage = observer(() => { setRows(newRows2 || []); }, [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[] = [ { field: "country", @@ -142,8 +155,10 @@ export const CityListPage = observer(() => { )} + + { }); const { language } = languageStore; const canWriteCountries = authStore.canWrite("countries"); + const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { const fetchCountries = async () => { @@ -72,11 +73,16 @@ export const CountryListPage = observer(() => { }] : []), ]; - const rows = countries[language]?.data.map((country) => ({ - id: country.code, - code: country.code, - name: country.name, - })); + const rows = useMemo(() => { + const query = searchQuery.toLowerCase(); + return (countries[language]?.data ?? []) + .filter((country) => !query || (country.name ?? "").toLowerCase().includes(query)) + .map((country) => ({ + id: country.code, + code: country.code, + name: country.name, + })); + }, [countries[language]?.data, searchQuery]); return ( <> @@ -102,8 +108,10 @@ export const CountryListPage = observer(() => { )} + + { }); const { language } = languageStore; const canWriteMedia = authStore.canWrite("sights"); + const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { const fetchMedia = async () => { @@ -95,15 +96,22 @@ export const MediaListPage = observer(() => { }, ]; - const rows = media.map((media) => ({ - id: media.id, - media_name: media.media_name, - media_type: media.media_type, - })); + const rows = useMemo(() => { + const query = searchQuery.toLowerCase(); + return media + .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 ( <>
+ + {canWriteMedia && ids.length > 0 && (
)} + + { }); const { language } = languageStore; const canReadCities = authStore.canRead("cities"); + const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { const fetchSights = async () => { @@ -120,13 +122,16 @@ export const SightListPage = observer(() => { }); }, [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) => ({ - id: sight.id, - name: sight.name, - city_id: sight.city_id, - })); + const canWriteSights = authStore.canWrite("sights"); return ( <> @@ -155,6 +160,8 @@ export const SightListPage = observer(() => {
)} + + { const { language } = languageStore; const [isRestoreModalOpen, setIsRestoreModalOpen] = useState(false); const [isLoading, setIsLoading] = useState(false); + const [searchQuery, setSearchQuery] = useState(""); const [paginationModel, setPaginationModel] = useState({ page: 0, pageSize: 50, @@ -89,22 +90,35 @@ export const SnapshotListPage = observer(() => { }] : []), ]; - const rows = snapshots.map((snapshot) => ({ - id: snapshot.ID, - name: snapshot.Name, - parent: snapshots.find((s) => s.ID === snapshot.ParentID)?.Name, - created_at: formatCreationTime(snapshot.CreationTime), - })); + const rows = useMemo(() => { + const query = searchQuery.toLowerCase(); + return snapshots + .filter( + (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 ( <>

Экспорт Медиа

+ {canCreateSnapshot && ( )}
+ + { }); const { language } = languageStore; const canWriteStations = authStore.canWrite("stations"); + const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { const fetchStations = async () => { @@ -121,21 +123,23 @@ export const StationListPage = observer(() => { }, ]; - const filteredStations = () => { + const rows = useMemo(() => { const { selectedCityId } = selectedCityStore; - if (!selectedCityId) { - return stationLists[language].data; - } - return stationLists[language].data.filter( - (station: any) => station.city_id === selectedCityId - ); - }; - - const rows = filteredStations().map((station: any) => ({ - id: station.id, - name: station.name, - description: station.description, - })); + const query = searchQuery.toLowerCase(); + return stationLists[language].data + .filter((station: any) => !selectedCityId || station.city_id === selectedCityId) + .filter( + (station: any) => + !query || + (station.name ?? "").toLowerCase().includes(query) || + (station.description ?? "").toLowerCase().includes(query) + ) + .map((station: any) => ({ + id: station.id, + name: station.name, + description: station.description, + })); + }, [stationLists[language].data, selectedCityStore.selectedCityId, searchQuery]); return ( <> @@ -161,6 +165,8 @@ export const StationListPage = observer(() => {
)} + + { pageSize: 50, }); const canWriteUsers = authStore.canWrite("users"); + const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { const fetchUsers = async () => { @@ -107,12 +108,21 @@ export const UserListPage = observer(() => { }] : []), ]; - const rows = users.data?.map((user) => ({ - id: user.id, - email: user.email, - is_admin: user.is_admin || (user.roles ?? []).includes("admin"), - name: user.name, - })); + const rows = useMemo(() => { + const query = searchQuery.toLowerCase(); + return (users.data ?? []) + .filter((user) => + !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 ( <> @@ -136,6 +146,8 @@ export const UserListPage = observer(() => { )} + + { }); const { language } = languageStore; const canWriteVehicles = authStore.canWrite("devices"); + const [searchQuery, setSearchQuery] = useState(""); useEffect(() => { const fetchData = async () => { @@ -136,27 +137,40 @@ export const VehicleListPage = observer(() => { }, ]; - const rows = vehicles.data?.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, - })); + const rows = useMemo(() => { + const query = searchQuery.toLowerCase(); + return (vehicles.data ?? []) + .filter( + (vehicle) => + !query || + (vehicle.vehicle.tail_number ?? "").toLowerCase().includes(query) || + (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 ( <>

Транспортные средства

+
+ + {canWriteVehicles && ids.length > 0 && (
+ )} +
+ ); +}; diff --git a/src/shared/ui/index.ts b/src/shared/ui/index.ts index e0e28d8..ce77238 100644 --- a/src/shared/ui/index.ts +++ b/src/shared/ui/index.ts @@ -5,3 +5,4 @@ export * from "./CoordinatesInput"; export * from "./AnimatedCircleButton"; export * from "./LoadingSpinner"; export * from "./MultiSelect"; +export * from "./SearchInput";