From 7554f1804b62edaccd1d23d5c732a3e8f5083a22 Mon Sep 17 00:00:00 2001 From: Denozordec Date: Wed, 30 Sep 2026 11:35:36 +0700 Subject: [PATCH] =?UTF-8?q?fix(ui):=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2=D0=B5?= =?UTF-8?q?=D1=80=D0=B6=D0=B4=D0=B0=D1=82=D1=8C=20=D0=BF=D0=B5=D1=80=D0=B5?= =?UTF-8?q?=D0=BC=D0=B5=D1=89=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=BF=D1=80=D0=B0?= =?UTF-8?q?=D0=B2=D0=B8=D0=BB=20Filters?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- app/(main)/filters/page.tsx | 46 ++++++++----- components/data-grids/filters-data-grid.tsx | 27 ++++++-- docs/ux-ui-plan.md | 2 +- docs/ux-ui-progress.md | 10 ++- docs/ux-ui-stage-9.md | 4 ++ tests/e2e/filters-move.spec.ts | 73 +++++++++++++++++++++ 6 files changed, 138 insertions(+), 24 deletions(-) create mode 100644 tests/e2e/filters-move.spec.ts 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 (