fix(ui): улучшить клавиатурный ввод Community в Filters
This commit is contained in:
1 parent
4ae5f2edcd
commit
29a4ea2e5b
10 files changed
+130
-37
No files matched your search
+22
-36
@@ -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>
|
||||
)}
|
||||
|
||||
Reference in new issue
Block a user