fix(ui): улучшить клавиатурный ввод Community в Filters

This commit is contained in:
Denozordec committed 2026-09-30 12:39:45 +07:00
1 parent 4ae5f2edcd
commit 29a4ea2e5b
10 files changed
+130 -37

No files matched your search

+22 -36
View File
@@ -1,6 +1,6 @@
"use client"
import { useMemo, useState, useCallback, useEffect, useRef } from "react"
import { useMemo, useState, useCallback, useEffect, useRef, useId } from "react"
import { PageHeader } from "@/components/page-header"
import { EmptyState } from "@/components/empty-state"
import { StatusDot } from "@/components/status-dot"
@@ -12,6 +12,7 @@ import {
import { DataPageCard } from "@/components/data-page-card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Combobox, ComboboxInput, ComboboxContent, ComboboxList, ComboboxItem, ComboboxEmpty } from "@/components/ui/combobox"
import { Separator } from "@/components/ui/separator"
import { cn } from "@/lib/utils"
import { filtersResponseSchema, filtersMutationSchema, filtersGreResponseSchema, filtersRecursiveResponseSchema } from "@/lib/filters-response"
@@ -407,40 +408,21 @@ function RecursiveGatewayCards({
// ── community autocomplete ─────────────────────────────────────────────────────
function CommunityInput({
value,
onChange,
suggestions,
nameMap,
}: {
value: string
onChange: (v: string) => void
suggestions: string[]
nameMap: Record<string, string>
function CommunityInput({ value, onChange, suggestions, nameMap, inputId, disabled }: {
value: string; onChange: (v: string) => void; suggestions: string[]; nameMap: Record<string, string>
inputId: string; disabled: boolean
}) {
const [open, setOpen] = useState(false)
const filtered = suggestions.filter(c =>
c.toLowerCase().includes(value.toLowerCase()) ||
(nameMap[c] ?? "").toLowerCase().includes(value.toLowerCase())
)
return (
<div className="relative">
<Input value={value} onChange={e => { onChange(e.target.value); setOpen(true) }}
onFocus={() => setOpen(true)} onBlur={() => setTimeout(() => setOpen(false), 150)}
placeholder="65001:100" className="font-mono h-9" />
{open && filtered.length > 0 && (
<div className="absolute z-50 w-full mt-1 rounded-md border bg-popover shadow-md overflow-hidden max-h-56 overflow-y-auto">
{filtered.map(c => (
<button key={c} type="button" onMouseDown={() => { onChange(c); setOpen(false) }}
className="w-full flex items-center gap-2 px-3 py-2 hover:bg-accent text-left">
<span className="font-mono text-xs font-medium">{c}</span>
<span className="text-xs text-muted-foreground truncate">{nameMap[c] ?? ""}</span>
</button>
))}
</div>
)}
</div>
)
return <Combobox items={suggestions} value={value} inputValue={value} disabled={disabled}
onInputValueChange={onChange} onValueChange={v => { if (v !== null) onChange(v) }}
filter={(item, query) => item.toLowerCase().includes(query.toLowerCase()) || (nameMap[item] ?? "").toLowerCase().includes(query.toLowerCase())}>
<ComboboxInput id={inputId} disabled={disabled} showTrigger={false} placeholder="65001:100" className="h-9 font-mono" />
<ComboboxContent>
<ComboboxEmpty>Нет подсказок — можно ввести Community вручную</ComboboxEmpty>
<ComboboxList>{(community: string) => <ComboboxItem key={community} value={community}>
<span className="font-mono text-xs">{community}</span><span className="truncate text-xs text-muted-foreground">{nameMap[community]}</span>
</ComboboxItem>}</ComboboxList>
</ComboboxContent>
</Combobox>
}
// ── rule sheet ─────────────────────────────────────────────────────────────────
@@ -480,6 +462,7 @@ function RuleSheet({
evoHint?: string | null
recursiveRoutes: RecursiveRouteLite[]
}) {
const communityInputId = useId()
const [form, setForm] = useState<RuleForm>(initial)
const [error, setError] = useState<string | null>(null)
const [touched, setTouched] = useState(false)
@@ -566,11 +549,13 @@ function RuleSheet({
{/* ── BGP Community ── */}
<div className="flex flex-col gap-1.5">
<label className="text-xs font-medium flex items-center gap-1">
<label htmlFor={communityInputId} className="text-xs font-medium flex items-center gap-1">
<TagIcon className="size-3 text-muted-foreground" />
BGP Community <span className="text-destructive ml-0.5">*</span>
</label>
<CommunityInput
inputId={communityInputId}
disabled={saving}
value={form.community}
onChange={handleCommunityChange}
suggestions={communitySuggestions}
@@ -603,6 +588,7 @@ function RuleSheet({
{(["route", "blackhole"] as const).map(a => (
<button
key={a}
aria-pressed={form.action === a}
type="button"
onClick={() => set("action", a)}
className={cn(
@@ -743,7 +729,7 @@ function RuleSheet({
</div>
{error && (
<div className="flex items-center gap-2 text-sm text-destructive bg-destructive/10 border border-destructive/20 px-3 py-2 rounded-md">
<div role="alert" className="flex items-center gap-2 text-sm text-destructive bg-destructive/10 border border-destructive/20 px-3 py-2 rounded-md">
<AlertCircleIcon className="size-4 shrink-0" />{error}
</div>
)}