fix(ui): подтверждать копирование правил Filters

This commit is contained in:
Denozordec committed 2026-09-30 11:24:48 +07:00
1 parent 1cf7297d64
commit 8412ba9bbe
5 files changed
+141 -33

No files matched your search

+39 -31
View File
@@ -877,30 +877,23 @@ function CopyRulesSheet({
open: boolean; onClose: () => void
sourceServerId: string; sourceRules: FilterRule[]
rulesets: ServerFilterRuleset[]
onCopy: (targetServerId: string, rules: FilterRule[], mode: CopyMode) => void
onCopy: (targetServerId: string, rules: FilterRule[], mode: CopyMode) => Promise<string | null>
serversList: Server[]
greByServer: Record<string, GreTunnel[]>
ensureGreFor?: (serverId: string) => void
recRoutesByServer: Record<string, RecursiveRouteLite[]>
ensureRecursiveFor?: (serverId: string) => void
}) {
const [selected, setSelected] = useState<Set<string>>(new Set())
const [selected, setSelected] = useState<Set<string>>(() => new Set(sourceRules.map(r => r.id)))
const [targetId, setTargetId] = useState<string | null>(null)
const [mode, setMode] = useState<CopyMode>("append")
const [done, setDone] = useState(false)
const [copying, setCopying] = useState(false)
const [copyError, setCopyError] = useState<string | null>(null)
const copyLock = useRef(false)
const sourceRecursiveRoutes = recRoutesByServer[sourceServerId] ?? []
const targetRecursiveRoutes = targetId ? (recRoutesByServer[targetId] ?? []) : []
// Reset each time the sheet opens
useEffect(() => {
queueMicrotask(() => {
if (!open) { setDone(false); return }
setSelected(new Set(sourceRules.map(r => r.id)))
setTargetId(null); setMode("append"); setDone(false)
})
}, [open, sourceRules])
useEffect(() => {
if (!open || !ensureGreFor) return
ensureGreFor(sourceServerId)
@@ -975,8 +968,11 @@ function CopyRulesSheet({
)
}
function handleCopy() {
if (!targetId || selectedRules.length === 0) return
async function handleCopy() {
if (copyLock.current || !targetId || selectedRules.length === 0) return
copyLock.current = true
setCopying(true)
setCopyError(null)
const copied: FilterRule[] = selectedRules.map(r => {
if ((r.action ?? "route") === "blackhole") {
return { ...r, id: newId() }
@@ -998,15 +994,23 @@ function CopyRulesSheet({
gatewayTunnelId: m?.tunnelId ?? "",
}
})
onCopy(targetId, copied, mode)
setDone(true)
setTimeout(() => onClose(), 900)
try {
const error = await onCopy(targetId, copied, mode)
if (error) setCopyError(error)
else onClose()
} catch (error) {
setCopyError(error instanceof Error ? error.message : "Не удалось скопировать правила")
} finally {
copyLock.current = false
setCopying(false)
}
}
const canCopy = targetId !== null && selectedRules.length > 0 && !done
const close = () => { if (!copyLock.current) onClose() }
const canCopy = targetId !== null && selectedRules.length > 0 && !copying
return (
<Sheet open={open} onOpenChange={v => { if (!v) onClose() }}>
<Sheet open={open} onOpenChange={v => { if (!v) close() }}>
<SheetContent side="right" className="sm:max-w-[500px] p-0 flex flex-col" showCloseButton={false}>
{/* header */}
@@ -1024,13 +1028,13 @@ function CopyRulesSheet({
<span className="text-muted-foreground">целевой сервер</span>
</SheetDescription>
</div>
<Button variant="ghost" size="icon-sm" onClick={onClose} className="shrink-0 mt-0.5">
<Button variant="ghost" size="icon-sm" onClick={close} disabled={copying} aria-label="Закрыть копирование" className="shrink-0 mt-0.5">
<XIcon className="size-4" />
</Button>
</div>
</SheetHeader>
<div className="flex-1 overflow-y-auto flex flex-col divide-y divide-border">
<fieldset disabled={copying} aria-busy={copying} className="flex-1 min-h-0 overflow-y-auto flex flex-col divide-y divide-border">
{/* ── rule selection ── */}
<div className="px-5 py-4 flex flex-col gap-2">
@@ -1053,7 +1057,7 @@ function CopyRulesSheet({
: undefined
const isSelected = selected.has(r.id)
return (
<button key={r.id} type="button" onClick={() => toggleRule(r.id)}
<button key={r.id} type="button" aria-pressed={isSelected} onClick={() => toggleRule(r.id)}
className={cn(
"flex items-center gap-2.5 px-3 py-2 rounded-lg border text-left transition-all",
isSelected
@@ -1106,7 +1110,7 @@ function CopyRulesSheet({
const ruleCount = rulesets.find(r => r.serverId === s.id)?.rules.length ?? 0
const isTarget = targetId === s.id
return (
<button key={s.id} type="button" onClick={() => setTargetId(s.id)}
<button key={s.id} type="button" aria-pressed={isTarget} onClick={() => setTargetId(s.id)}
className={cn(
"flex items-center gap-2.5 px-3 py-2.5 rounded-lg border text-left transition-all",
isTarget
@@ -1136,7 +1140,7 @@ function CopyRulesSheet({
{ v: "append" as CopyMode, label: "Добавить", sub: `+${selectedRules.length} к ${targetRules.length} существующим` },
{ v: "replace" as CopyMode, label: "Заменить", sub: `${targetRules.length} правил → ${selectedRules.length} новых` },
] as const).map(opt => (
<button key={opt.v} type="button" onClick={() => setMode(opt.v)}
<button key={opt.v} type="button" aria-pressed={mode === opt.v} onClick={() => setMode(opt.v)}
className={cn(
"flex flex-col gap-1 text-left px-3 py-2.5 rounded-lg border transition-all",
mode === opt.v
@@ -1218,18 +1222,20 @@ function CopyRulesSheet({
</div>
)}
</div>
</fieldset>
{copyError && <p role="alert" className="px-5 py-3 text-sm text-destructive">{copyError}</p>}
{copying && <p role="status" className="px-5 py-2 text-sm text-muted-foreground">Ожидание подтверждения копирования…</p>}
{/* footer */}
<SheetFooter className="px-5 py-4 border-t shrink-0 gap-2">
<Button variant="outline" onClick={onClose} className="flex-1">Отмена</Button>
<Button variant="outline" onClick={close} disabled={copying} className="flex-1">Отмена</Button>
<Button
onClick={handleCopy}
onClick={() => void handleCopy()}
disabled={!canCopy}
className="flex-1"
>
{done
? <><CheckIcon className="size-4" />Скопировано!</>
{copying
? <><LoaderCircleIcon className="size-4 animate-spin" />Копирование…</>
: <><CopyIcon className="size-4" />Копировать {selectedRules.length} правил</>
}
</Button>
@@ -1630,10 +1636,11 @@ export default function FiltersPage() {
updateRules(selectedServerId, rules => rules.filter(r => r.id !== id))
}
const handleCopyRules = useCallback((targetServerId: string, rules: FilterRule[], mode: CopyMode) => {
const handleCopyRules = useCallback(async (targetServerId: string, rules: FilterRule[], mode: CopyMode) => {
const existing = rulesets.find((rs) => rs.serverId === targetServerId)?.rules ?? []
const next = mode === "replace" ? rules : [...existing, ...rules]
void applyRules(targetServerId, next, "copy")
const result = await applyRules(targetServerId, next, "copy")
return result.ok ? null : result.error ?? "Не удалось скопировать правила"
}, [rulesets, applyRules])
const handleMoveUp = (index: number) => {
if (mutationsLocked || index === 0) return
@@ -1904,6 +1911,7 @@ export default function FiltersPage() {
/>
<CopyRulesSheet
key={`${copyOpen}-${selectedServerId}`}
open={copyOpen}
onClose={() => setCopyOpen(false)}
sourceServerId={selectedServerId}