From 51f7ba47f78d6c48d637dceb9b1bb7febe474cbe Mon Sep 17 00:00:00 2001 From: "Cheng Ho Ming, Eric" Date: Sat, 14 Nov 2026 00:25:38 +0800 Subject: [PATCH] feat(xray): drag to reorder DNS servers (#6959) Xray queries DNS servers in list order, but the DNS table offered only Edit and Delete, so changing a server's priority meant deleting and re-adding servers. Each row now has a grab handle that reorders by drag and drop, using the same pointer hook and drop markers as the Routing rules table, and the row menu gains Move up and Move down. Every path only reorders dns.servers; a server's own fields are left as they are. --- frontend/src/pages/xray/dns/DnsTab.tsx | 64 ++++++++++++++--- frontend/src/pages/xray/dns/useDnsColumns.tsx | 43 +++++++++++- frontend/src/test/dns-tab.test.tsx | 68 ++++++++++++++++++- 3 files changed, 161 insertions(+), 14 deletions(-) diff --git a/frontend/src/pages/xray/dns/DnsTab.tsx b/frontend/src/pages/xray/dns/DnsTab.tsx index b52193936..003fbfdf3 100644 --- a/frontend/src/pages/xray/dns/DnsTab.tsx +++ b/frontend/src/pages/xray/dns/DnsTab.tsx @@ -27,6 +27,7 @@ import { selectTagRender } from '@/components/form'; import { onNumber } from '@/utils/onNumber'; import { SettingListItem } from '@/components/ui'; import { useMediaQuery } from '@/hooks/useMediaQuery'; +import { usePointerRowReorder } from '@/hooks/usePointerRowReorder'; import { catTabLabel } from '@/pages/settings/catTabLabel'; import DnsServerModal from './DnsServerModal'; import type { DnsServerValue } from './DnsServerModal'; @@ -52,6 +53,7 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab const [editingServer, setEditingServer] = useState(null); const [editingIndex, setEditingIndex] = useState(null); const [presetsModalOpen, setPresetsModalOpen] = useState(false); + const serversContainerRef = useRef(null); const dns = (templateSettings?.dns as DnsConfig | undefined) ?? null; const dnsEnabled = !!dns; @@ -166,8 +168,33 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab }, [mutate], ); + const moveServer = useCallback( + (from: number, to: number) => { + mutate((tt) => { + const servers = (tt.dns as DnsConfig | undefined)?.servers; + if (!servers) return; + const [moved] = servers.splice(from, 1); + servers.splice(to, 0, moved); + }); + }, + [mutate], + ); - const dnsColumns = useDnsServerColumns({ openEditServer, deleteServer }); + const { drag, onPointerDown: onHandlePointerDown } = usePointerRowReorder({ + containerRef: serversContainerRef, + getRowKey: (row) => Number(row.dataset.rowKey), + onDrop: ({ from, to }) => moveServer(from, to), + }); + const draggedIndex = drag?.from ?? null; + const dropTargetIndex = drag?.to ?? null; + + const dnsColumns = useDnsServerColumns({ + rowsLength: dnsServers.length, + onHandlePointerDown, + openEditServer, + moveServer, + deleteServer, + }); function openAddServer() { setEditingServer(null); @@ -475,14 +502,21 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab {t('pages.xray.dns.clearAll')} - r.key} - pagination={false} - size="small" - bordered - /> +
+
r.key} + rowClassName={(_r, i) => { + if (i === draggedIndex) return 'row-dragging'; + if (i !== dropTargetIndex || draggedIndex == null) return ''; + return i > draggedIndex ? 'drop-after' : 'drop-before'; + }} + pagination={false} + size="small" + bordered + /> + ), }); @@ -517,7 +551,17 @@ export default function DnsTab({ templateSettings, setTemplateSettings }: DnsTab return out; // eslint-disable-next-line react-hooks/exhaustive-deps - }, [t, isMobile, dnsEnabled, dns, hostsList, dnsServers, fakeDnsList]); + }, [ + t, + isMobile, + dnsEnabled, + dns, + hostsList, + dnsServers, + fakeDnsList, + draggedIndex, + dropTargetIndex, + ]); return ( <> diff --git a/frontend/src/pages/xray/dns/useDnsColumns.tsx b/frontend/src/pages/xray/dns/useDnsColumns.tsx index 70f12f25b..1c7ec608a 100644 --- a/frontend/src/pages/xray/dns/useDnsColumns.tsx +++ b/frontend/src/pages/xray/dns/useDnsColumns.tsx @@ -1,7 +1,14 @@ import { useMemo } from 'react'; import { useTranslation } from 'react-i18next'; import { Button, Dropdown, Input, InputNumber, Space } from 'antd'; -import { MoreOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons'; +import { + MoreOutlined, + EditOutlined, + DeleteOutlined, + ArrowUpOutlined, + ArrowDownOutlined, + HolderOutlined, +} from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { onNumber } from '@/utils/onNumber'; @@ -13,10 +20,16 @@ export type DnsServerRow = { key: number; server: DnsServerValue }; export type FakednsTableRow = { key: number; ipPool: string; poolSize: number }; export function useDnsServerColumns({ + rowsLength, + onHandlePointerDown, openEditServer, + moveServer, deleteServer, }: { + rowsLength: number; + onHandlePointerDown: (idx: number, ev: React.PointerEvent) => void; openEditServer: (idx: number) => void; + moveServer: (from: number, to: number) => void; deleteServer: (idx: number) => void; }): ColumnsType { const { t } = useTranslation(); @@ -29,6 +42,12 @@ export function useDnsServerColumns({ width: 60, render: (_v, _record, index) => ( +