fix(ui): подтверждать копирование правил Filters
This commit is contained in:
1 parent
1cf7297d64
commit
8412ba9bbe
5 files changed
+141
-33
No files matched your search
+39
-31
@@ -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}
|
||||
|
||||
Reference in new issue
Block a user