diff --git a/app/(main)/filters/page.tsx b/app/(main)/filters/page.tsx index 69f945bc..3e0acca3 100644 --- a/app/(main)/filters/page.tsx +++ b/app/(main)/filters/page.tsx @@ -1361,6 +1361,9 @@ export default function FiltersPage() { const [deletePending, setDeletePending] = useState(false) const [deleteError, setDeleteError] = useState(null) const deleteLock = useRef(false) + const [movePending, setMovePending] = useState(false) + const [moveError, setMoveError] = useState(null) + const moveLock = useRef(false) const [liveStale, setLiveStale] = useState(false) const [historyOpen, setHistoryOpen] = useState(false) const [historyLoading, setHistoryLoading] = useState(false) @@ -1458,7 +1461,7 @@ export default function FiltersPage() { const allTunnels = !isLive || liveLoadState === "error" ? greTunnels : (greByServer[selectedServerId] ?? []) const selectedServer = allServers.find(s => s.id === selectedServerId) ?? allServers[0] const totalRules = rulesets.reduce((s, r) => s + r.rules.length, 0) - const mutationsLocked = isLive && (applyBusy || historyRestoring || liveStale || liveLoadState !== "idle") + const mutationsLocked = movePending || (isLive && (applyBusy || historyRestoring || liveStale || liveLoadState !== "idle")) const filterRailItems = useMemo(() => ( allServers.map((s) => ({ @@ -1479,6 +1482,7 @@ export default function FiltersPage() { setSheetOpen(false) setPendingDelete(null) setDeleteError(null) + setMoveError(null) setSelectedServerId(id) setSearch("") }, []) @@ -1677,18 +1681,25 @@ export default function FiltersPage() { const result = await applyRules(targetServerId, next, "copy") return result.ok ? null : result.error ?? "Не удалось скопировать правила" }, [rulesets, applyRules]) - const handleMoveUp = (index: number) => { - if (mutationsLocked || index === 0) return - updateRules(selectedServerId, rules => { - const n = [...rules]; [n[index - 1], n[index]] = [n[index], n[index - 1]]; return n - }) - } - const handleMoveDown = (index: number) => { - if (mutationsLocked) return - updateRules(selectedServerId, rules => { - if (index >= rules.length - 1) return rules - const n = [...rules]; [n[index], n[index + 1]] = [n[index + 1], n[index]]; return n - }) + const handleMove = async (id: string, direction: -1 | 1) => { + if (mutationsLocked || applyLock.current || moveLock.current) return + const index = currentRules.findIndex(r => r.id === id) + const destination = index + direction + if (index < 0 || destination < 0 || destination >= currentRules.length) return + moveLock.current = true + setMovePending(true) + setMoveError(null) + const next = [...currentRules] + ;[next[index], next[destination]] = [next[destination], next[index]] + try { + const result = await applyRules(selectedServerId, next) + if (!result.ok) setMoveError(result.error ?? "Не удалось переместить правило") + } catch (error) { + setMoveError(error instanceof Error ? error.message : "Не удалось переместить правило") + } finally { + moveLock.current = false + setMovePending(false) + } } if (isLive && liveLoadState === "loading") { @@ -1868,6 +1879,9 @@ export default function FiltersPage() { + {movePending &&

Ожидание подтверждения порядка…

} + {moveError &&

{moveError}

} + {currentRules.length === 0 ? ( /* empty state */ handleMoveUp(currentRules.findIndex((r) => r.id === id))} - onMoveDown={(id) => handleMoveDown(currentRules.findIndex((r) => r.id === id))} + onMoveUp={(id) => { void handleMove(id, -1) }} + onMoveDown={(id) => { void handleMove(id, 1) }} /> )} diff --git a/components/data-grids/filters-data-grid.tsx b/components/data-grids/filters-data-grid.tsx index 27e1a74d..09e28bd4 100644 --- a/components/data-grids/filters-data-grid.tsx +++ b/components/data-grids/filters-data-grid.tsx @@ -145,9 +145,11 @@ function FilterGatewayCell({ } function FilterRowActions({ + disabled, onEdit, onDelete, }: { + disabled: boolean onEdit: () => void onDelete: () => void }) { @@ -158,6 +160,7 @@ function FilterRowActions({ variant="ghost" className="size-7 p-0 text-muted-foreground hover:text-foreground" onClick={onEdit} + disabled={disabled} aria-label="Редактировать правило" > @@ -167,6 +170,7 @@ function FilterRowActions({ variant="ghost" className="size-7 p-0 text-muted-foreground hover:text-destructive" onClick={onDelete} + disabled={disabled} aria-label="Удалить правило" > @@ -177,6 +181,8 @@ function FilterRowActions({ interface FiltersDataGridProps { rules: FilterRule[] + orderedRules?: FilterRule[] + mutationsLocked?: boolean tunnelsList: GreTunnel[] serversList: Server[] communityNameMap: Record @@ -190,6 +196,8 @@ interface FiltersDataGridProps { function FiltersDataGrid({ rules, + orderedRules = rules, + mutationsLocked = false, tunnelsList, serversList, communityNameMap, @@ -206,14 +214,15 @@ function FiltersDataGrid({ id: "priority", header: () => #, enableSorting: false, - cell: ({ row, table }) => { - const isLast = row.index === table.getRowModel().rows.length - 1 + cell: ({ row }) => { + const index = orderedRules.findIndex(r => r.id === row.original.id) + const isLast = index === orderedRules.length - 1 return (
{isLast ? ( ) : ( - {row.index + 1} + {index + 1} )}
) @@ -225,15 +234,16 @@ function FiltersDataGrid({ id: "reorder", header: () => Порядок, enableSorting: false, - cell: ({ row, table }) => { - const isLast = row.index === table.getRowModel().rows.length - 1 + cell: ({ row }) => { + const index = orderedRules.findIndex(r => r.id === row.original.id) + const isLast = index === orderedRules.length - 1 return (