From f4a49862a0349aedeef6683a8157319c2220aeb5 Mon Sep 17 00:00:00 2001 From: MHSanaei Date: Tue, 26 May 2026 13:14:03 +0200 Subject: [PATCH] =?UTF-8?q?feat(frontend):=20fallbacks=20polish=20?= =?UTF-8?q?=E2=80=94=20move=20up/down=20+=20Add=20all=20button?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two small UX wins on the InboundFormModal Fallbacks card: - Per-row Move up / Move down buttons (ArrowUp/Down icons) that swap adjacent indices. Order survives reloads via sortOrder (rebuilt from index on save). First row's Up button + last row's Down button are disabled. - 'Add all' button next to 'Add fallback' that one-shot inserts a fresh row for every eligible inbound (every option in fallbackChildOptions) not already wired up. Disabled when every eligible inbound is already covered. Convenient for operators running catch-all routing across every host on the panel. --- .../src/pages/inbounds/InboundFormModal.tsx | 75 ++++++++++++++++++- 1 file changed, 71 insertions(+), 4 deletions(-) diff --git a/frontend/src/pages/inbounds/InboundFormModal.tsx b/frontend/src/pages/inbounds/InboundFormModal.tsx index c6e3f8cc..8cb8978a 100644 --- a/frontend/src/pages/inbounds/InboundFormModal.tsx +++ b/frontend/src/pages/inbounds/InboundFormModal.tsx @@ -19,7 +19,14 @@ import { Typography, message, } from 'antd'; -import { DeleteOutlined, MinusOutlined, PlusOutlined, SyncOutlined } from '@ant-design/icons'; +import { + ArrowDownOutlined, + ArrowUpOutlined, + DeleteOutlined, + MinusOutlined, + PlusOutlined, + SyncOutlined, +} from '@ant-design/icons'; import { HttpUtil, NumberFormatter, RandomUtil, SizeFormatter, Wireguard } from '@/utils'; import { @@ -254,6 +261,41 @@ export default function InboundFormModal({ setFallbacks((prev) => prev.filter((_, i) => i !== idx)); }; + // Move a fallback row up/down by swapping adjacent indices. The order + // is persisted via the fallback row's sortOrder (rebuilt by index on + // save), so reordering survives reloads. + const moveFallback = (idx: number, direction: -1 | 1) => { + setFallbacks((prev) => { + const target = idx + direction; + if (target < 0 || target >= prev.length) return prev; + const next = prev.slice(); + [next[idx], next[target]] = [next[target], next[idx]]; + return next; + }); + }; + + // One-shot: add a fresh fallback row for every eligible inbound (i.e. + // every option in fallbackChildOptions) that is not already wired up. + // Convenient for operators who want catch-all routing to every host + // they manage on the panel. + const addAllFallbacks = () => { + setFallbacks((prev) => { + const alreadyHave = new Set(prev.map((r) => r.childId)); + const additions = fallbackChildOptions + .filter((opt) => !alreadyHave.has(opt.value)) + .map((opt) => ({ + rowKey: `fb-${++fallbackKeyRef.current}`, + childId: opt.value, + name: '', + alpn: '', + path: '', + xver: 0, + })); + if (additions.length === 0) return prev; + return [...prev, ...additions]; + }); + }; + const genRealityKeypair = async () => { setSaving(true); try { @@ -661,6 +703,20 @@ export default function InboundFormModal({ style={{ width: '100%' }} onChange={(v) => updateFallback(record.rowKey, { childId: v })} /> + + @@ -694,9 +750,20 @@ export default function InboundFormModal({ ))} - + + + + );