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() {
- {/* Правая часть навбара: язык и тема */}
-
-
-
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}
+
+
+
-
{/* Мета-информация будет показана внизу карточки */}
{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 && (
<>
|