From 584fd78f23c63d93f3870c099cd5cd55eb77986a Mon Sep 17 00:00:00 2001 From: shats Date: Sun, 7 Dec 2025 02:44:51 +0700 Subject: [PATCH] feat: Add CommunityAutocomplete component to FilterManager for enhanced community selection, featuring dynamic filtering, grouping by category, and improved user experience. --- frontend/src/FilterManager.jsx | 229 +++++++++++++++++++++++++++++++-- 1 file changed, 220 insertions(+), 9 deletions(-) diff --git a/frontend/src/FilterManager.jsx b/frontend/src/FilterManager.jsx index fe5aad9..1f14936 100644 --- a/frontend/src/FilterManager.jsx +++ b/frontend/src/FilterManager.jsx @@ -70,6 +70,222 @@ const countryToFlag = (code) => { return String.fromCodePoint(...codePoints); }; +// Компонент автокомплита для выбора Community +function CommunityAutocomplete({ label, value, onChange, communities = [], required = false, placeholder = 'Введите или выберите community...' }) { + const [inputValue, setInputValue] = useState(value || ''); + const [isOpen, setIsOpen] = useState(false); + const [highlightedIndex, setHighlightedIndex] = useState(-1); + const containerRef = useRef(null); + const inputRef = useRef(null); + + useEffect(() => { + setInputValue(value || ''); + }, [value]); + + // Фильтруем communities по введённому тексту + const filteredCommunities = communities.filter(c => { + const search = inputValue.toLowerCase(); + return ( + (c.value && c.value.toLowerCase().includes(search)) || + (c.name && c.name.toLowerCase().includes(search)) || + (c.description && c.description.toLowerCase().includes(search)) + ); + }); + + useEffect(() => { + const handleClickOutside = (e) => { + if (containerRef.current && !containerRef.current.contains(e.target)) { + setIsOpen(false); + } + }; + document.addEventListener('mousedown', handleClickOutside); + return () => document.removeEventListener('mousedown', handleClickOutside); + }, []); + + const handleInputChange = (e) => { + const val = e.target.value; + setInputValue(val); + onChange(val); + setIsOpen(true); + setHighlightedIndex(-1); + }; + + const handleSelect = (c) => { + setInputValue(c.value); + onChange(c.value); + setIsOpen(false); + setHighlightedIndex(-1); + }; + + const handleKeyDown = (e) => { + if (!isOpen) { + if (e.key === 'ArrowDown' || e.key === 'Enter') { + setIsOpen(true); + e.preventDefault(); + } + return; + } + + switch (e.key) { + case 'ArrowDown': + e.preventDefault(); + setHighlightedIndex(prev => Math.min(prev + 1, filteredCommunities.length - 1)); + break; + case 'ArrowUp': + e.preventDefault(); + setHighlightedIndex(prev => Math.max(prev - 1, 0)); + break; + case 'Enter': + e.preventDefault(); + if (highlightedIndex >= 0 && filteredCommunities[highlightedIndex]) { + handleSelect(filteredCommunities[highlightedIndex]); + } + break; + case 'Escape': + setIsOpen(false); + setHighlightedIndex(-1); + break; + } + }; + + const selectedCommunity = communities.find(c => c.value === value); + + // Группируем по категориям + const groupedCommunities = filteredCommunities.reduce((acc, c) => { + const cat = c.category || 'Другое'; + if (!acc[cat]) acc[cat] = []; + acc[cat].push(c); + return acc; + }, {}); + + return ( +
+ +
+
+ + + + setIsOpen(true)} + onKeyDown={handleKeyDown} + placeholder={placeholder} + required={required} + autoComplete="off" + /> + setIsOpen(!isOpen)} + > + + +
+ + {/* Dropdown */} + {isOpen && ( +
+ {communities.length === 0 ? ( +
+ + Справочник community пуст +
+ ) : filteredCommunities.length === 0 ? ( +
+ Ничего не найдено. Нажмите Enter чтобы использовать "{inputValue}" +
+ ) : ( + <> + {inputValue && !communities.some(c => c.value === inputValue) && ( +
Свободный ввод: {inputValue}
+ )} + {Object.entries(groupedCommunities).map(([category, items]) => ( +
+
{category}
+ {items.map((c, idx) => { + const globalIdx = filteredCommunities.indexOf(c); + return ( +
handleSelect(c)} + onMouseEnter={() => setHighlightedIndex(globalIdx)} + > +
+ + + +
+
+ {c.value} + {c.name && — {c.name}} +
+ {c.description && ( +
{c.description}
+ )} +
+ {c.enabled === false && Выкл} +
+
+ ); + })} +
+ ))} + + )} +
+ )} +
+ + {/* Показываем выбранный community */} + {value && selectedCommunity && !isOpen && ( +
+
+ + + +
+
+ {selectedCommunity.value} + {selectedCommunity.name && — {selectedCommunity.name}} +
+ {selectedCommunity.description && ( +
{selectedCommunity.description}
+ )} +
+ {selectedCommunity.category && {selectedCommunity.category}} +
+
+ )} + + {/* Индикация свободного ввода */} + {value && !selectedCommunity && !isOpen && ( +
+
+ +
Community "{value}" — свободный ввод (не из справочника)
+
+
+ )} +
+ ); +} + // Компонент автокомплита для выбора Gateway function GatewayAutocomplete({ label, value, onChange, gateways = [], required = false, placeholder = 'Введите или выберите gateway...' }) { const [inputValue, setInputValue] = useState(value || ''); @@ -1571,15 +1787,12 @@ function FilterManager() { )} - setNewFilter({ ...newFilter, community: val })} - placeholder="65000:100" + communities={communities} required - icon={IconHash} - helpText="Введите community в формате AS:VALUE" /> {editingFilter && ( <> - setEditingFilter({ ...editingFilter, community: val })} - placeholder="65000:100" + communities={communities} required - icon={IconHash} />