feat: role system fix

This commit is contained in:
2026-03-18 21:38:50 +03:00
parent c3127b8d47
commit 591ca8104d
14 changed files with 267 additions and 183 deletions

View File

@@ -21,6 +21,7 @@ export const ArticleListPage = observer(() => {
page: 0, page: 0,
pageSize: 50, pageSize: 50,
}); });
const canWriteArticles = authStore.canWrite("sights");
useEffect(() => { useEffect(() => {
const fetchArticles = async () => { const fetchArticles = async () => {
@@ -56,7 +57,7 @@ export const ArticleListPage = observer(() => {
<button onClick={() => navigate(`/article/${params.row.id}`)}> <button onClick={() => navigate(`/article/${params.row.id}`)}>
<Eye size={20} className="text-green-500" /> <Eye size={20} className="text-green-500" />
</button> </button>
{authStore.canWrite("sights") && ( {canWriteArticles && (
<button <button
onClick={() => { onClick={() => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
@@ -86,7 +87,7 @@ export const ArticleListPage = observer(() => {
<h1 className="text-2xl">Статьи</h1> <h1 className="text-2xl">Статьи</h1>
</div> </div>
{ids.length > 0 && ( {canWriteArticles && ids.length > 0 && (
<div className="flex justify-end mb-5 duration-300"> <div className="flex justify-end mb-5 duration-300">
<button <button
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center" className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
@@ -102,25 +103,37 @@ export const ArticleListPage = observer(() => {
<DataGrid <DataGrid
rows={rows} rows={rows}
columns={columns} columns={columns}
checkboxSelection checkboxSelection={canWriteArticles}
disableRowSelectionExcludeModel disableRowSelectionExcludeModel
loading={isLoading} loading={isLoading}
paginationModel={paginationModel} paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel} onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]} pageSizeOptions={[50]}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText} localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
onRowSelectionModelChange={(newSelection: any) => { onRowSelectionModelChange={
if (Array.isArray(newSelection)) { canWriteArticles
const selectedIds = newSelection.map((id: string | number) => Number(id)); ? (newSelection: any) => {
setIds(selectedIds); if (Array.isArray(newSelection)) {
} else if (newSelection && typeof newSelection === 'object' && 'ids' in newSelection) { const selectedIds = newSelection.map(
const idsSet = newSelection.ids as Set<string | number>; (id: string | number) => Number(id)
const selectedIds = Array.from(idsSet).map((id: string | number) => Number(id)); );
setIds(selectedIds); setIds(selectedIds);
} else { } else if (
setIds([]); newSelection &&
} typeof newSelection === "object" &&
}} "ids" in newSelection
) {
const idsSet = newSelection.ids as Set<string | number>;
const selectedIds = Array.from(idsSet).map(
(id: string | number) => Number(id)
);
setIds(selectedIds);
} else {
setIds([]);
}
}
: undefined
}
slots={{ slots={{
noRowsOverlay: () => ( noRowsOverlay: () => (
<Box <Box

View File

@@ -130,7 +130,7 @@ export const CityListPage = observer(() => {
)} )}
</div> </div>
{ids.length > 0 && ( {canWriteCities && ids.length > 0 && (
<div className="flex justify-end mb-5 duration-300"> <div className="flex justify-end mb-5 duration-300">
<button <button
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center" className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
@@ -145,25 +145,37 @@ export const CityListPage = observer(() => {
<DataGrid <DataGrid
rows={rows} rows={rows}
columns={columns} columns={columns}
checkboxSelection={authStore.canWrite("cities")} checkboxSelection={canWriteCities}
disableRowSelectionExcludeModel disableRowSelectionExcludeModel
loading={isLoading} loading={isLoading}
paginationModel={paginationModel} paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel} onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]} pageSizeOptions={[50]}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText} localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
onRowSelectionModelChange={(newSelection: any) => { onRowSelectionModelChange={
if (Array.isArray(newSelection)) { canWriteCities
const selectedIds = newSelection.map((id: string | number) => Number(id)); ? (newSelection: any) => {
setIds(selectedIds); if (Array.isArray(newSelection)) {
} else if (newSelection && typeof newSelection === 'object' && 'ids' in newSelection) { const selectedIds = newSelection.map(
const idsSet = newSelection.ids as Set<string | number>; (id: string | number) => Number(id)
const selectedIds = Array.from(idsSet).map((id: string | number) => Number(id)); );
setIds(selectedIds); setIds(selectedIds);
} else { } else if (
setIds([]); newSelection &&
} typeof newSelection === "object" &&
}} "ids" in newSelection
) {
const idsSet = newSelection.ids as Set<string | number>;
const selectedIds = Array.from(idsSet).map(
(id: string | number) => Number(id)
);
setIds(selectedIds);
} else {
setIds([]);
}
}
: undefined
}
slots={{ slots={{
noRowsOverlay: () => ( noRowsOverlay: () => (
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}> <Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>

View File

@@ -90,7 +90,7 @@ export const CountryListPage = observer(() => {
)} )}
</div> </div>
{ids.length > 0 && ( {canWriteCountries && ids.length > 0 && (
<div className="flex justify-end mb-5 duration-300"> <div className="flex justify-end mb-5 duration-300">
<button <button
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center" className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
@@ -105,25 +105,37 @@ export const CountryListPage = observer(() => {
<DataGrid <DataGrid
rows={rows || []} rows={rows || []}
columns={columns} columns={columns}
checkboxSelection={authStore.canWrite("countries")} checkboxSelection={canWriteCountries}
disableRowSelectionExcludeModel disableRowSelectionExcludeModel
loading={isLoading} loading={isLoading}
paginationModel={paginationModel} paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel} onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]} pageSizeOptions={[50]}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText} localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
onRowSelectionModelChange={(newSelection: any) => { onRowSelectionModelChange={
if (Array.isArray(newSelection)) { canWriteCountries
const selectedIds = newSelection.map((id: string | number) => Number(id)); ? (newSelection: any) => {
setIds(selectedIds); if (Array.isArray(newSelection)) {
} else if (newSelection && typeof newSelection === 'object' && 'ids' in newSelection) { const selectedIds = newSelection.map(
const idsSet = newSelection.ids as Set<string | number>; (id: string | number) => Number(id)
const selectedIds = Array.from(idsSet).map((id: string | number) => Number(id)); );
setIds(selectedIds); setIds(selectedIds);
} else { } else if (
setIds([]); newSelection &&
} typeof newSelection === "object" &&
}} "ids" in newSelection
) {
const idsSet = newSelection.ids as Set<string | number>;
const selectedIds = Array.from(idsSet).map(
(id: string | number) => Number(id)
);
setIds(selectedIds);
} else {
setIds([]);
}
}
: undefined
}
slots={{ slots={{
noRowsOverlay: () => ( noRowsOverlay: () => (
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}> <Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>

View File

@@ -21,6 +21,7 @@ export const MediaListPage = observer(() => {
pageSize: 50, pageSize: 50,
}); });
const { language } = languageStore; const { language } = languageStore;
const canWriteMedia = authStore.canWrite("sights");
useEffect(() => { useEffect(() => {
const fetchMedia = async () => { const fetchMedia = async () => {
@@ -79,7 +80,7 @@ export const MediaListPage = observer(() => {
<button onClick={() => navigate(`/media/${params.row.id}`)}> <button onClick={() => navigate(`/media/${params.row.id}`)}>
<Eye size={20} className="text-green-500" /> <Eye size={20} className="text-green-500" />
</button> </button>
{authStore.canWrite("sights") && ( {canWriteMedia && (
<button <button
onClick={() => { onClick={() => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
@@ -103,7 +104,7 @@ export const MediaListPage = observer(() => {
return ( return (
<> <>
<div className="w-full"> <div className="w-full">
{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
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center" className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
@@ -118,24 +119,36 @@ export const MediaListPage = observer(() => {
<DataGrid <DataGrid
rows={rows} rows={rows}
columns={columns} columns={columns}
checkboxSelection={authStore.canWrite("sights")} checkboxSelection={canWriteMedia}
disableRowSelectionExcludeModel disableRowSelectionExcludeModel
loading={isLoading} loading={isLoading}
paginationModel={paginationModel} paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel} onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]} pageSizeOptions={[50]}
onRowSelectionModelChange={(newSelection: any) => { onRowSelectionModelChange={
if (Array.isArray(newSelection)) { canWriteMedia
const selectedIds = newSelection.map((id: string | number) => String(id)); ? (newSelection: any) => {
setIds(selectedIds); if (Array.isArray(newSelection)) {
} else if (newSelection && typeof newSelection === 'object' && 'ids' in newSelection) { const selectedIds = newSelection.map(
const idsSet = newSelection.ids as Set<string | number>; (id: string | number) => String(id)
const selectedIds = Array.from(idsSet).map((id: string | number) => String(id)); );
setIds(selectedIds); setIds(selectedIds);
} else { } else if (
setIds([]); newSelection &&
} typeof newSelection === "object" &&
}} "ids" in newSelection
) {
const idsSet = newSelection.ids as Set<string | number>;
const selectedIds = Array.from(idsSet).map(
(id: string | number) => String(id)
);
setIds(selectedIds);
} else {
setIds([]);
}
}
: undefined
}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText} localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
slots={{ slots={{
noRowsOverlay: () => ( noRowsOverlay: () => (

View File

@@ -252,7 +252,7 @@ const LinkedItemsContentsInner = <
}) })
.catch((error) => { .catch((error) => {
console.error("Error fetching all items:", error); console.error("Error fetching all items:", error);
setError("Failed to load available stations"); setError(null);
setAllItems([]); setAllItems([]);
}); });
} }

View File

@@ -22,6 +22,12 @@ export const RouteListPage = observer(() => {
pageSize: 50, pageSize: 50,
}); });
const { language } = languageStore; const { language } = languageStore;
const canWriteRoutes = authStore.canWrite("routes");
const canShowRoutePreview =
authStore.canWrite("stations") &&
authStore.canWrite("sights") &&
authStore.canWrite("routes");
const canShowActionsColumn = canWriteRoutes || canShowRoutePreview;
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@@ -104,7 +110,7 @@ export const RouteListPage = observer(() => {
); );
}, },
}, },
{ ...(canShowActionsColumn ? [{
field: "actions", field: "actions",
headerName: "Действия", headerName: "Действия",
width: 250, width: 250,
@@ -112,14 +118,9 @@ export const RouteListPage = observer(() => {
headerAlign: "center" as const, headerAlign: "center" as const,
sortable: false, sortable: false,
renderCell: (params: GridRenderCellParams) => { renderCell: (params: GridRenderCellParams) => {
const canWrite = authStore.canWrite("routes");
const canShowRoutePreview =
authStore.canRead("stations") &&
authStore.canRead("sights") &&
authStore.canRead("routes");
return ( return (
<div className="flex h-full gap-7 justify-center items-center"> <div className="flex h-full gap-7 justify-center items-center">
{canWrite && ( {canWriteRoutes && (
<button onClick={() => navigate(`/route/${params.row.id}/edit`)}> <button onClick={() => navigate(`/route/${params.row.id}/edit`)}>
<Pencil size={20} className="text-blue-500" /> <Pencil size={20} className="text-blue-500" />
</button> </button>
@@ -129,7 +130,7 @@ export const RouteListPage = observer(() => {
<Map size={20} className="text-purple-500" /> <Map size={20} className="text-purple-500" />
</button> </button>
)} )}
{canWrite && ( {canWriteRoutes && (
<button <button
onClick={() => { onClick={() => {
setIsDeleteModalOpen(true); setIsDeleteModalOpen(true);
@@ -142,7 +143,7 @@ export const RouteListPage = observer(() => {
</div> </div>
); );
}, },
}, }] : []),
]; ];
const rows = routes.data.map((route) => ({ const rows = routes.data.map((route) => ({
@@ -160,10 +161,12 @@ export const RouteListPage = observer(() => {
<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 label="Создать маршрут" path="/route/create" /> {canWriteRoutes && (
<CreateButton label="Создать маршрут" path="/route/create" />
)}
</div> </div>
{ids.length > 0 && ( {canWriteRoutes && ids.length > 0 && (
<div className="flex justify-end mb-5 duration-300"> <div className="flex justify-end mb-5 duration-300">
<button <button
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center" className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
@@ -178,25 +181,37 @@ export const RouteListPage = observer(() => {
<DataGrid <DataGrid
rows={rows} rows={rows}
columns={columns} columns={columns}
checkboxSelection={authStore.canWrite("routes")} checkboxSelection={canWriteRoutes}
disableRowSelectionExcludeModel disableRowSelectionExcludeModel
loading={isLoading} loading={isLoading}
paginationModel={paginationModel} paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel} onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]} pageSizeOptions={[50]}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText} localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
onRowSelectionModelChange={(newSelection: any) => { onRowSelectionModelChange={
if (Array.isArray(newSelection)) { canWriteRoutes
const selectedIds = newSelection.map((id: string | number) => Number(id)); ? (newSelection: any) => {
setIds(selectedIds); if (Array.isArray(newSelection)) {
} else if (newSelection && typeof newSelection === 'object' && 'ids' in newSelection) { const selectedIds = newSelection.map((id: string | number) =>
const idsSet = newSelection.ids as Set<string | number>; Number(id)
const selectedIds = Array.from(idsSet).map((id: string | number) => Number(id)); );
setIds(selectedIds); setIds(selectedIds);
} else { } else if (
setIds([]); newSelection &&
} typeof newSelection === "object" &&
}} "ids" in newSelection
) {
const idsSet = newSelection.ids as Set<string | number>;
const selectedIds = Array.from(idsSet).map(
(id: string | number) => Number(id)
);
setIds(selectedIds);
} else {
setIds([]);
}
}
: undefined
}
slots={{ slots={{
noRowsOverlay: () => ( noRowsOverlay: () => (
<Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}> <Box sx={{ mt: 5, textAlign: "center", color: "text.secondary" }}>

View File

@@ -356,7 +356,7 @@ const LinkedStationsContentsInner = <
}) })
.catch((error) => { .catch((error) => {
console.error("Error fetching all stations:", error); console.error("Error fetching all stations:", error);
setError("Failed to load available stations"); setError(null);
setAllItems([]); setAllItems([]);
}); });
} }

View File

@@ -354,7 +354,7 @@ const LinkedSightsContentsInner = <
}) })
.catch((error) => { .catch((error) => {
console.error("Error fetching all sights:", error); console.error("Error fetching all sights:", error);
setError("Failed to load available sights"); setError(null);
setAllItems([]); setAllItems([]);
}); });
} }

View File

@@ -149,75 +149,69 @@ export const StationListPage = observer(() => {
)} )}
</div> </div>
<div className="flex justify-end mb-5 duration-300"> {canWriteStations && ids.length > 0 && (
<button <div className="flex justify-end mb-5 duration-300">
className={`px-4 py-2 rounded flex gap-2 items-center transition-all ${ <button
ids.length > 0 className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
? "bg-red-500 text-white cursor-pointer opacity-100" onClick={() => setIsBulkDeleteModalOpen(true)}
: "bg-gray-300 text-gray-500 cursor-not-allowed opacity-0 pointer-events-none" >
}`} <Trash2 size={20} className="text-white" /> Удалить выбранные (
onClick={() => { {ids.length})
if (ids.length > 0) { </button>
setIsBulkDeleteModalOpen(true); </div>
} )}
}}
disabled={ids.length === 0}
>
<Trash2
size={20}
className={ids.length > 0 ? "text-white" : "text-gray-500"}
/>
Удалить выбранные ({ids.length})
</button>
</div>
<DataGrid <DataGrid
rows={rows} rows={rows}
columns={columns} columns={columns}
checkboxSelection checkboxSelection={canWriteStations}
disableRowSelectionExcludeModel disableRowSelectionExcludeModel
loading={isLoading} loading={isLoading}
paginationModel={paginationModel} paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel} onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]} pageSizeOptions={[50]}
onRowSelectionModelChange={(newSelection: any) => { onRowSelectionModelChange={
if (Array.isArray(newSelection)) { canWriteStations
const selectedIds = newSelection ? (newSelection: any) => {
.map((id: string | number) => { if (Array.isArray(newSelection)) {
const numId = const selectedIds = newSelection
typeof id === "string" .map((id: string | number) => {
? Number.parseInt(id, 10) const numId =
: Number(id); typeof id === "string"
return numId; ? Number.parseInt(id, 10)
}) : Number(id);
.filter( return numId;
(id: number) => })
!Number.isNaN(id) && id !== null && id !== undefined .filter(
); (id: number) =>
setIds(selectedIds); !Number.isNaN(id) && id !== null && id !== undefined
} else if ( );
newSelection && setIds(selectedIds);
typeof newSelection === "object" && } else if (
"ids" in newSelection newSelection &&
) { typeof newSelection === "object" &&
const idsSet = newSelection.ids as Set<string | number>; "ids" in newSelection
const selectedIds = Array.from(idsSet) ) {
.map((id: string | number) => { const idsSet = newSelection.ids as Set<string | number>;
const numId = const selectedIds = Array.from(idsSet)
typeof id === "string" .map((id: string | number) => {
? Number.parseInt(id, 10) const numId =
: Number(id); typeof id === "string"
return numId; ? Number.parseInt(id, 10)
}) : Number(id);
.filter( return numId;
(id: number) => })
!Number.isNaN(id) && id !== null && id !== undefined .filter(
); (id: number) =>
setIds(selectedIds); !Number.isNaN(id) && id !== null && id !== undefined
} else { );
setIds([]); setIds(selectedIds);
} } else {
}} setIds([]);
}
}
: undefined
}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText} localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
slots={{ slots={{
noRowsOverlay: () => ( noRowsOverlay: () => (

View File

@@ -124,7 +124,7 @@ export const UserListPage = observer(() => {
)} )}
</div> </div>
{ids.length > 0 && ( {canWriteUsers && ids.length > 0 && (
<div className="flex justify-end mb-5 duration-300"> <div className="flex justify-end mb-5 duration-300">
<button <button
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center" className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
@@ -145,18 +145,30 @@ export const UserListPage = observer(() => {
paginationModel={paginationModel} paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel} onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]} pageSizeOptions={[50]}
onRowSelectionModelChange={(newSelection: any) => { onRowSelectionModelChange={
if (Array.isArray(newSelection)) { canWriteUsers
const selectedIds = newSelection.map((id: string | number) => Number(id)); ? (newSelection: any) => {
setIds(selectedIds); if (Array.isArray(newSelection)) {
} else if (newSelection && typeof newSelection === 'object' && 'ids' in newSelection) { const selectedIds = newSelection.map(
const idsSet = newSelection.ids as Set<string | number>; (id: string | number) => Number(id)
const selectedIds = Array.from(idsSet).map((id: string | number) => Number(id)); );
setIds(selectedIds); setIds(selectedIds);
} else { } else if (
setIds([]); newSelection &&
} typeof newSelection === "object" &&
}} "ids" in newSelection
) {
const idsSet = newSelection.ids as Set<string | number>;
const selectedIds = Array.from(idsSet).map(
(id: string | number) => Number(id)
);
setIds(selectedIds);
} else {
setIds([]);
}
}
: undefined
}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText} localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
slots={{ slots={{
noRowsOverlay: () => ( noRowsOverlay: () => (

View File

@@ -23,6 +23,7 @@ export const VehicleListPage = observer(() => {
pageSize: 50, pageSize: 50,
}); });
const { language } = languageStore; const { language } = languageStore;
const canWriteVehicles = authStore.canWrite("devices");
useEffect(() => { useEffect(() => {
const fetchData = async () => { const fetchData = async () => {
@@ -156,7 +157,7 @@ export const VehicleListPage = observer(() => {
/> />
</div> </div>
{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
className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center" className="px-4 py-2 bg-red-500 text-white rounded flex gap-2 items-center"
@@ -171,24 +172,36 @@ export const VehicleListPage = observer(() => {
<DataGrid <DataGrid
rows={rows} rows={rows}
columns={columns} columns={columns}
checkboxSelection={authStore.canWrite("devices")} checkboxSelection={canWriteVehicles}
disableRowSelectionExcludeModel disableRowSelectionExcludeModel
loading={isLoading} loading={isLoading}
paginationModel={paginationModel} paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel} onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]} pageSizeOptions={[50]}
onRowSelectionModelChange={(newSelection: any) => { onRowSelectionModelChange={
if (Array.isArray(newSelection)) { canWriteVehicles
const selectedIds = newSelection.map((id: string | number) => Number(id)); ? (newSelection: any) => {
setIds(selectedIds); if (Array.isArray(newSelection)) {
} else if (newSelection && typeof newSelection === 'object' && 'ids' in newSelection) { const selectedIds = newSelection.map(
const idsSet = newSelection.ids as Set<string | number>; (id: string | number) => Number(id)
const selectedIds = Array.from(idsSet).map((id: string | number) => Number(id)); );
setIds(selectedIds); setIds(selectedIds);
} else { } else if (
setIds([]); newSelection &&
} typeof newSelection === "object" &&
}} "ids" in newSelection
) {
const idsSet = newSelection.ids as Set<string | number>;
const selectedIds = Array.from(idsSet).map(
(id: string | number) => Number(id)
);
setIds(selectedIds);
} else {
setIds([]);
}
}
: undefined
}
localeText={ruRU.components.MuiDataGrid.defaultProps.localeText} localeText={ruRU.components.MuiDataGrid.defaultProps.localeText}
slots={{ slots={{
noRowsOverlay: () => ( noRowsOverlay: () => (

View File

@@ -152,8 +152,6 @@ class AuthStore {
}; };
canAccess = (permission: string): boolean => { canAccess = (permission: string): boolean => {
// If permission looks like a concrete role (e.g. snapshot_create/snapshot_rw),
// check it as-is; otherwise treat it as a resource name.
if (permission.includes("_")) { if (permission.includes("_")) {
return this.hasRole(permission); return this.hasRole(permission);
} }

View File

@@ -13,17 +13,17 @@ import { MapPin } from "lucide-react";
export const CitySelector: React.FC = observer(() => { export const CitySelector: React.FC = observer(() => {
const { selectedCity, setSelectedCity } = selectedCityStore; const { selectedCity, setSelectedCity } = selectedCityStore;
const canReadCities = authStore.canRead("cities"); const canLoadAllCities = authStore.isAdmin && authStore.canRead("cities");
useEffect(() => { useEffect(() => {
if (canReadCities) { if (canLoadAllCities) {
cityStore.getCities("ru"); cityStore.getCities("ru");
return; return;
} }
authStore.fetchMeCities().catch(() => undefined); authStore.fetchMeCities().catch(() => undefined);
}, [canReadCities]); }, [canLoadAllCities]);
const baseCities: City[] = canReadCities const baseCities: City[] = canLoadAllCities
? cityStore.cities["ru"].data ? cityStore.cities["ru"].data
: authStore.meCities["ru"].map((uc) => ({ : authStore.meCities["ru"].map((uc) => ({
id: uc.city_id, id: uc.city_id,

View File

@@ -781,7 +781,7 @@ export const DevicesTable = observer(() => {
Добавить устройство Добавить устройство
</Button> </Button>
)} )}
{selectedIds.length > 0 && ( {canWriteDevices && selectedIds.length > 0 && (
<Button <Button
variant="contained" variant="contained"
color="error" color="error"
@@ -862,16 +862,18 @@ export const DevicesTable = observer(() => {
<DataGrid <DataGrid
rows={groupRows} rows={groupRows}
columns={columns} columns={columns}
checkboxSelection checkboxSelection={canWriteDevices}
disableRowSelectionExcludeModel disableRowSelectionExcludeModel
loading={isLoading} loading={isLoading}
paginationModel={paginationModel} paginationModel={paginationModel}
onPaginationModelChange={setPaginationModel} onPaginationModelChange={setPaginationModel}
pageSizeOptions={[50]} pageSizeOptions={[50]}
onRowSelectionModelChange={ onRowSelectionModelChange={
createSelectionHandler(groupRowIds) as ( canWriteDevices
ids: unknown, ? (createSelectionHandler(groupRowIds) as (
) => void ids: unknown,
) => void)
: undefined
} }
rowSelectionModel={{ rowSelectionModel={{
type: "include", type: "include",