From cd5e169bc64205fb159a1001bb7e25098ec97e69 Mon Sep 17 00:00:00 2001 From: shats Date: Thu, 2 Oct 2025 18:30:31 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=B8?= =?UTF-8?q?=D1=82=D1=8C=20=D1=84=D1=83=D0=BD=D0=BA=D1=86=D0=B8=D0=BE=D0=BD?= =?UTF-8?q?=D0=B0=D0=BB=D1=8C=D0=BD=D0=BE=D1=81=D1=82=D1=8C=20=D0=BC=D0=B0?= =?UTF-8?q?=D1=81=D1=81=D0=BE=D0=B2=D1=8B=D1=85=20=D0=BE=D0=BF=D0=B5=D1=80?= =?UTF-8?q?=D0=B0=D1=86=D0=B8=D0=B9=20=D0=B2=20=D0=BC=D0=B5=D0=BD=D0=B5?= =?UTF-8?q?=D0=B4=D0=B6=D0=B5=D1=80=20=D0=B4=D0=BE=D0=BC=D0=B5=D0=BD=D0=BE?= =?UTF-8?q?=D0=B2,=20=D0=B2=D0=BA=D0=BB=D1=8E=D1=87=D0=B0=D1=8F=20=D0=B2?= =?UTF-8?q?=D1=8B=D0=B1=D0=BE=D1=80=20=D0=B2=D1=81=D0=B5=D1=85,=20=D1=83?= =?UTF-8?q?=D0=B4=D0=B0=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D0=B8=20=D1=8D?= =?UTF-8?q?=D0=BA=D1=81=D0=BF=D0=BE=D1=80=D1=82=20=D0=B2=D1=8B=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D1=85=20=D0=B4=D0=BE=D0=BC=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE=D0=B2.=20=D0=9E=D0=B1=D0=BD=D0=BE=D0=B2=D0=B8=D1=82?= =?UTF-8?q?=D1=8C=20=D0=B8=D0=BD=D1=82=D0=B5=D1=80=D1=84=D0=B5=D0=B9=D1=81?= =?UTF-8?q?=20=D1=81=20=D0=BD=D0=BE=D0=B2=D1=8B=D0=BC=D0=B8=20=D0=BA=D0=BE?= =?UTF-8?q?=D0=BC=D0=BF=D0=BE=D0=BD=D0=B5=D0=BD=D1=82=D0=B0=D0=BC=D0=B8=20?= =?UTF-8?q?=D0=B4=D0=BB=D1=8F=20=D1=83=D0=BB=D1=83=D1=87=D1=88=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D1=8F=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0?= =?UTF-8?q?=D1=82=D0=B5=D0=BB=D1=8C=D1=81=D0=BA=D0=BE=D0=B3=D0=BE=20=D0=BE?= =?UTF-8?q?=D0=BF=D1=8B=D1=82=D0=B0=20=D0=B8=20=D0=B4=D0=BE=D0=B1=D0=B0?= =?UTF-8?q?=D0=B2=D0=BB=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=81=D0=BE=D1=85=D1=80?= =?UTF-8?q?=D0=B0=D0=BD=D1=91=D0=BD=D0=BD=D1=8B=D1=85=20=D1=84=D0=B8=D0=BB?= =?UTF-8?q?=D1=8C=D1=82=D1=80=D0=BE=D0=B2=20=D0=B4=D0=BB=D1=8F=20=D0=B1?= =?UTF-8?q?=D0=BE=D0=BB=D0=B5=D0=B5=20=D1=83=D0=B4=D0=BE=D0=B1=D0=BD=D0=BE?= =?UTF-8?q?=D0=B9=20=D1=80=D0=B0=D0=B1=D0=BE=D1=82=D1=8B=20=D1=81=20=D0=B4?= =?UTF-8?q?=D0=B0=D0=BD=D0=BD=D1=8B=D0=BC=D0=B8.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- frontend/src/App.jsx | 94 +++++++--- frontend/src/DomainsNewManager.jsx | 199 ++++++++++++++++----- frontend/src/components/CommandPalette.jsx | 28 ++- 3 files changed, 241 insertions(+), 80 deletions(-) diff --git a/frontend/src/App.jsx b/frontend/src/App.jsx index 04b4440..3492ce8 100644 --- a/frontend/src/App.jsx +++ b/frontend/src/App.jsx @@ -294,35 +294,79 @@ function MainLayout() { - {/* Правая часть навбара: язык и тема */} -
-
- + {lang.toUpperCase()} + +
+ { e.preventDefault(); setLang('ru'); }} + > + Русский + + { e.preventDefault(); setLang('en'); }} + > + English + +
+ + {/* Тема */} +
+ e.preventDefault()} + > + {theme === 'dark' ? '🌙' : '☀️'} + +
+ { e.preventDefault(); setTheme('light'); }} + > + ☀️ {t('light')} + + { e.preventDefault(); setTheme('dark'); }} + > + 🌙 {t('dark')} + +
+
+ + {/* Горячие клавиши */} +
+ +
+ + {/* Настройки */}
- -
-
- - + +
diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index 2c9b835..8c6ecfb 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -16,7 +16,8 @@ import { IconDeviceFloppy, IconHash, IconClock, - IconFileText + IconFileText, + IconFilter } from '@tabler/icons-react'; import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx'; import PageHeader from './components/PageHeader.jsx'; @@ -33,10 +34,15 @@ import { notifyMutationSuccess } from './components/NotifyProvider.jsx'; import ImportModal from './components/ImportModal.jsx'; import QuickAddBar from './components/QuickAddBar.jsx'; import AccordionCard from './components/AccordionCard.jsx'; +import SavedFilters from './components/SavedFilters.jsx'; +import BulkActionsBar from './components/BulkActionsBar.jsx'; +import ValidatedInput from './components/ValidatedInput.jsx'; +import { useToast } from './components/ToastContainer.jsx'; const API_URL = '/api'; function DomainsNewManager() { + const toast = useToast(); const [items, setItems] = useState([]); const [originalItems, setOriginalItems] = useState([]); const [etag, setEtag] = useState(''); @@ -62,6 +68,9 @@ function DomainsNewManager() { const editInputRef = useRef(null); const pageSize = 10; + // Bulk operations + const [selectedDomains, setSelectedDomains] = useState(new Set()); + // Справочник community для подсказок const [communities, setCommunities] = useState([]); useEffect(() => { @@ -399,6 +408,47 @@ function DomainsNewManager() { if (e.key === 'Escape') handleCancelEdit(); }; + // Bulk operations handlers + const handleSelectAll = () => { + setSelectedDomains(new Set(paginatedItems.map(i => i.domain))); + }; + + const handleDeselectAll = () => { + setSelectedDomains(new Set()); + }; + + const handleToggleSelect = (domain) => { + const newSet = new Set(selectedDomains); + if (newSet.has(domain)) { + newSet.delete(domain); + } else { + newSet.add(domain); + } + setSelectedDomains(newSet); + }; + + const handleBulkDelete = () => { + if (selectedDomains.size === 0) return; + const newItems = items.filter(i => !selectedDomains.has(i.domain)); + setItems(newItems); + setSelectedDomains(new Set()); + toast.success(`Удалено ${selectedDomains.size} доменов`); + }; + + const handleBulkExport = () => { + if (selectedDomains.size === 0) return; + const selectedItems = items.filter(i => selectedDomains.has(i.domain)); + const text = selectedItems.map(i => `${i.domain} ${i.community}`).join('\n'); + const blob = new Blob([text], { type: 'text/plain' }); + const url = URL.createObjectURL(blob); + const a = document.createElement('a'); + a.href = url; + a.download = `domains-selected-${selectedDomains.size}.txt`; + a.click(); + URL.revokeObjectURL(url); + toast.success(`Экспортировано ${selectedDomains.size} доменов`); + }; + return ( <> {error && ( @@ -422,11 +472,16 @@ function DomainsNewManager() { + )} actions={(
+ {/* Bulk Actions Bar */} + +
-
-

Список доменов {items.length}

-
- {/* Фильтр по community */} - - {/* Поиск */} -
- - - - { setSearchTerm(e.target.value); setCurrentPage(1); }} - aria-label="Поиск домена" - /> +
+
+
+

+ Список доменов + {filtered.length} +

+
+
+
+ {/* Сохранённые фильтры */} + { + if (filters.searchTerm !== undefined) setSearchTerm(filters.searchTerm); + if (filters.filterCommunity !== undefined) setFilterCommunity(filters.filterCommunity); + if (filters.sortField !== undefined) setSortField(filters.sortField); + if (filters.sortOrder !== undefined) setSortOrder(filters.sortOrder); + setCurrentPage(1); + }} + /> + + {/* Фильтр по community */} + + + {/* Поиск */} +
+ + + + { setSearchTerm(e.target.value); setCurrentPage(1); }} + aria-label="Поиск домена" + /> +
+
-
{/* Мета-информация будет показана внизу карточки */} {loading ? ( - + ) : paginatedItems.length === 0 ? ( + + 0} + onChange={(e) => e.target.checked ? handleSelectAll() : handleDeselectAll()} + /> + handleSort('domain')}> Домен {sortField === 'domain' && ( @@ -654,6 +749,14 @@ function DomainsNewManager() { {paginatedItems.map((item) => ( + + handleToggleSelect(item.domain)} + /> + {item.domain} {renderCommunityBadge(item.community)} diff --git a/frontend/src/components/CommandPalette.jsx b/frontend/src/components/CommandPalette.jsx index ed3b7cb..8ec0669 100644 --- a/frontend/src/components/CommandPalette.jsx +++ b/frontend/src/components/CommandPalette.jsx @@ -203,15 +203,29 @@ export function KeyboardShortcutsButton({ className = '' }) { { keys: ['Enter'], description: 'Подтвердить / Выбрать' }, ] + // Если className содержит nav-link, используем ссылку вместо кнопки + const isNavLink = className.includes('nav-link') + return ( <> - + {isNavLink ? ( + { e.preventDefault(); setShowModal(true); }} + title="Горячие клавиши (Ctrl+/)" + > + + + ) : ( + + )} {showModal && ( <>