diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx
index 02784eb..b42a900 100644
--- a/frontend/src/ASNsNewManager.jsx
+++ b/frontend/src/ASNsNewManager.jsx
@@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
+import CommunityAutocompleteInput from './components/CommunityAutocompleteInput.jsx';
import {
IconPlus,
IconSearch,
@@ -286,13 +287,12 @@ function ASNsNewManager() {
-
setNewItem({ ...newItem, community: e.target.value })}
+ onChange={(v) => setNewItem({ ...newItem, community: v })}
+ communities={communities}
+ placeholder="Начните вводить номер или имя"
+ className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
/>
{newInvalid.community &&
Только цифры
}
{(() => {
@@ -439,15 +439,11 @@ function ASNsNewManager() {
{editingAsn === item.asn ? (
<>
- setEditingValue(e.target.value)}
- onKeyDown={e => handleEditKeyDown(e, item.asn)}
- style={{maxWidth: 120}}
+ onChange={setEditingValue}
+ communities={communities}
+ className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
/>
@@ -553,15 +549,7 @@ function ASNsNewManager() {
)}
- {/* Общий список подсказок community */}
-
+ {/* datalist больше не нужен, т.к. используем кастомный автокомплит */}
>
);
}
diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx
index 3f96ef7..4885f7b 100644
--- a/frontend/src/DomainsNewManager.jsx
+++ b/frontend/src/DomainsNewManager.jsx
@@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
+import CommunityAutocompleteInput from './components/CommunityAutocompleteInput.jsx';
import {
IconPlus,
IconSearch,
@@ -292,13 +293,12 @@ function DomainsNewManager() {
- setNewItem({ ...newItem, community: e.target.value })}
+ onChange={(v) => setNewItem({ ...newItem, community: v })}
+ communities={communities}
+ placeholder="Начните вводить номер или имя"
+ className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
/>
{newInvalid.community && Только цифры }
{(() => {
@@ -445,15 +445,11 @@ function DomainsNewManager() {
{editingDomain === item.domain ? (
<>
- setEditingValue(e.target.value)}
- onKeyDown={e => handleEditKeyDown(e, item.domain)}
- style={{maxWidth: 120}}
+ onChange={setEditingValue}
+ communities={communities}
+ className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
/>
@@ -559,15 +555,7 @@ function DomainsNewManager() {
)}
- {/* Общий список подсказок community */}
-
+ {/* datalist больше не нужен, т.к. используем кастомный автокомплит */}
>
);
}
diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx
index f96748e..146461a 100644
--- a/frontend/src/IPRangesManager.jsx
+++ b/frontend/src/IPRangesManager.jsx
@@ -1,5 +1,6 @@
import { useState, useEffect, useRef } from 'react';
import axios from 'axios';
+import CommunityAutocompleteInput from './components/CommunityAutocompleteInput.jsx';
import {
IconPlus,
IconSearch,
@@ -301,13 +302,12 @@ function IPRangesManager() {
- setNewItem({ ...newItem, community: e.target.value })}
+ onChange={(v) => setNewItem({ ...newItem, community: v })}
+ communities={communities}
+ placeholder="Начните вводить номер или имя"
+ className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
/>
{newInvalid.community && Только цифры }
{(() => {
@@ -454,15 +454,11 @@ function IPRangesManager() {
{editingIp === item.ipRange ? (
<>
- setEditingValue(e.target.value)}
- onKeyDown={e => handleEditKeyDown(e, item.ipRange)}
- style={{maxWidth: 120}}
+ onChange={setEditingValue}
+ communities={communities}
+ className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
/>
@@ -573,15 +569,7 @@ function IPRangesManager() {
)}
- {/* Общий список подсказок community */}
-
+ {/* datalist больше не нужен, т.к. используем кастомный автокомплит */}
>
);
}
diff --git a/frontend/src/components/CommunityAutocompleteInput.jsx b/frontend/src/components/CommunityAutocompleteInput.jsx
new file mode 100644
index 0000000..ab90412
--- /dev/null
+++ b/frontend/src/components/CommunityAutocompleteInput.jsx
@@ -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 (
+
+ {
+ onChange(e.target.value);
+ setOpen(true);
+ }}
+ onFocus={() => setOpen(true)}
+ onKeyDown={handleKeyDown}
+ autoComplete="off"
+ />
+
+ {open && suggestions.length > 0 && (
+
+ {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 (
+
+ );
+ })}
+
+ )}
+
+ );
+}
+
+export default CommunityAutocompleteInput;
+
+
| | |