feat: Replace input fields with CommunityAutocompleteInput in ASNs, Domains, and IPRanges managers for enhanced user experience and streamlined community selection
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 5m37s

This commit is contained in:
2025-08-10 21:15:01 +07:00
parent 92474cc49b
commit d3490c438b
4 changed files with 170 additions and 69 deletions
@@ -0,0 +1,137 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { IconHash } from '@tabler/icons-react';
function CommunityAutocompleteInput({
value,
onChange,
communities = [],
placeholder = '',
className = 'form-control',
onSelectMeta,
maxSuggestions = 8,
}) {
const containerRef = useRef(null);
const inputRef = useRef(null);
const [open, setOpen] = useState(false);
const [activeIndex, setActiveIndex] = useState(-1);
const suggestions = useMemo(() => {
const q = String(value || '').toLowerCase();
if (!q) return communities.slice(0, maxSuggestions);
const filtered = communities.filter((c) => {
const v = (c.value || '').toLowerCase();
const n = (c.name || '').toLowerCase();
const d = (c.description || '').toLowerCase();
const t = Array.isArray(c.tags) ? c.tags.join(' ').toLowerCase() : '';
return v.includes(q) || n.includes(q) || d.includes(q) || t.includes(q);
});
return filtered.slice(0, maxSuggestions);
}, [value, communities, maxSuggestions]);
useEffect(() => {
const handleOutside = (e) => {
if (!containerRef.current) return;
if (!containerRef.current.contains(e.target)) setOpen(false);
};
document.addEventListener('click', handleOutside);
return () => document.removeEventListener('click', handleOutside);
}, []);
const selectItem = (item) => {
onChange(item.value);
if (onSelectMeta) onSelectMeta(item);
setOpen(false);
setActiveIndex(-1);
if (inputRef.current) inputRef.current.focus();
};
const handleKeyDown = (e) => {
if (!open && (e.key === 'ArrowDown' || e.key === 'ArrowUp')) {
setOpen(true);
return;
}
if (!open) return;
if (e.key === 'ArrowDown') {
e.preventDefault();
setActiveIndex((prev) => Math.min(prev + 1, suggestions.length - 1));
} else if (e.key === 'ArrowUp') {
e.preventDefault();
setActiveIndex((prev) => Math.max(prev - 1, 0));
} else if (e.key === 'Enter') {
if (activeIndex >= 0 && activeIndex < suggestions.length) {
e.preventDefault();
selectItem(suggestions[activeIndex]);
}
} else if (e.key === 'Escape') {
setOpen(false);
setActiveIndex(-1);
}
};
return (
<div ref={containerRef} className="position-relative" style={{ width: '100%' }}>
<input
ref={inputRef}
type="text"
className={className}
placeholder={placeholder}
value={value}
onChange={(e) => {
onChange(e.target.value);
setOpen(true);
}}
onFocus={() => setOpen(true)}
onKeyDown={handleKeyDown}
autoComplete="off"
/>
{open && suggestions.length > 0 && (
<div
className="dropdown-menu show"
style={{
display: 'block',
width: '100%',
maxHeight: 280,
overflowY: 'auto',
}}
>
{suggestions.map((s, idx) => {
const label = s.name ? `${s.value} - ${s.name}` : `${s.value}`;
const description = s.description || '';
const tags = Array.isArray(s.tags) ? s.tags : [];
return (
<button
type="button"
key={`${s.value}-${idx}`}
className={`dropdown-item${idx === activeIndex ? ' active' : ''}`}
onMouseDown={(e) => { e.preventDefault(); selectItem(s); }}
onMouseEnter={() => setActiveIndex(idx)}
>
<div className="d-flex align-items-start">
<span className="avatar me-2 bg-blue-lt text-blue border-0" style={{ width: 22, height: 22 }}>
<IconHash size={14} />
</span>
<div className="flex-fill text-start">
<div className="fw-medium">{label}</div>
{(description || tags.length > 0) && (
<div className="text-muted small text-truncate" style={{ maxWidth: '100%' }}>
{description}
{tags.length > 0 && (
<> ({tags.join(', ')})</>
)}
</div>
)}
</div>
</div>
</button>
);
})}
</div>
)}
</div>
);
}
export default CommunityAutocompleteInput;