feat(frontend): fallbacks polish — move up/down + Add all button

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.
This commit is contained in:
MHSanaei
2026-05-26 13:14:03 +02:00
parent 19204f9e04
commit f4a49862a0

View File

@@ -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<FallbackRow>((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 })}
/>
<Button
disabled={idx === 0}
onClick={() => moveFallback(idx, -1)}
title="Move up"
>
<ArrowUpOutlined />
</Button>
<Button
disabled={idx === fallbacks.length - 1}
onClick={() => moveFallback(idx, 1)}
title="Move down"
>
<ArrowDownOutlined />
</Button>
<Button danger onClick={() => removeFallback(idx)}>
<DeleteOutlined />
</Button>
@@ -694,9 +750,20 @@ export default function InboundFormModal({
</Space.Compact>
</div>
))}
<Button size="small" onClick={addFallback}>
<PlusOutlined /> {t('pages.inbounds.fallbacks.add') || 'Add fallback'}
</Button>
<Space>
<Button size="small" onClick={addFallback}>
<PlusOutlined /> {t('pages.inbounds.fallbacks.add') || 'Add fallback'}
</Button>
<Button
size="small"
onClick={addAllFallbacks}
disabled={fallbackChildOptions.length === 0
|| fallbacks.length >= fallbackChildOptions.length}
title="Add a fallback row for every eligible inbound not yet wired up"
>
Add all
</Button>
</Space>
</Card>
);