feat: Добавление функциональности массового выбора и действий в компоненты ASNsNewManager, DomainsNewManager и IPRangesManager. Реализованы функции для выбора всех элементов, удаления и экспорта выбранных записей, а также добавлены новые иконки и улучшены стили для повышения удобства использования.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m45s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m45s
This commit is contained in:
@@ -23,7 +23,8 @@ import {
|
||||
IconTrendingUp,
|
||||
IconUsers,
|
||||
IconArrowUp,
|
||||
IconArrowDown
|
||||
IconArrowDown,
|
||||
IconCopy
|
||||
} from '@tabler/icons-react';
|
||||
import TableSkeleton, { TableEmpty } from './components/TableSkeleton.jsx';
|
||||
import PageHeader from './components/PageHeader.jsx';
|
||||
@@ -44,6 +45,7 @@ import AccordionCard from './components/AccordionCard.jsx';
|
||||
import SavedFilters from './components/SavedFilters.jsx';
|
||||
import BulkActionsBar from './components/BulkActionsBar.jsx';
|
||||
import FormField from './components/FormField.jsx';
|
||||
import Tooltip from './components/Tooltip.jsx';
|
||||
import { useToast } from './components/ToastContainer.jsx';
|
||||
|
||||
const API_URL = '/api';
|
||||
@@ -171,6 +173,19 @@ function DomainsNewManager() {
|
||||
|
||||
const handleDeleteItem = (domainNameToDelete) => {
|
||||
setItems(items.filter(i => i.domain !== domainNameToDelete));
|
||||
setSelectedDomains(prev => {
|
||||
const next = new Set(prev);
|
||||
next.delete(domainNameToDelete);
|
||||
return next;
|
||||
});
|
||||
};
|
||||
|
||||
const copyToClipboard = (text) => {
|
||||
navigator.clipboard.writeText(text).then(() => {
|
||||
toast.success('Скопировано в буфер обмена');
|
||||
}).catch(() => {
|
||||
toast.error('Не удалось скопировать');
|
||||
});
|
||||
};
|
||||
|
||||
const confirmDelete = (item) => {
|
||||
@@ -448,10 +463,11 @@ function DomainsNewManager() {
|
||||
|
||||
const handleBulkDelete = () => {
|
||||
if (selectedDomains.size === 0) return;
|
||||
const count = selectedDomains.size;
|
||||
const newItems = items.filter(i => !selectedDomains.has(i.domain));
|
||||
setItems(newItems);
|
||||
setSelectedDomains(new Set());
|
||||
toast.success(`Удалено ${selectedDomains.size} доменов`);
|
||||
toast.success(`Удалено ${count} доменов`);
|
||||
};
|
||||
|
||||
const handleBulkExport = () => {
|
||||
@@ -536,96 +552,6 @@ function DomainsNewManager() {
|
||||
)}
|
||||
/>
|
||||
|
||||
{/* Статистические карточки */}
|
||||
<div className="row g-3 mb-4">
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<div className="card card-sm animate-in">
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="bg-blue text-white avatar">
|
||||
<IconWorld size={24} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="font-weight-medium">Всего доменов</div>
|
||||
<div className="text-muted">
|
||||
<span className="h2 mb-0">{items.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<div className="card card-sm animate-in" style={{animationDelay: '0.1s'}}>
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="bg-green text-white avatar">
|
||||
<IconFilter size={24} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="font-weight-medium">Отфильтровано</div>
|
||||
<div className="text-muted">
|
||||
<span className="h2 mb-0">{filtered.length}</span>
|
||||
{filtered.length !== items.length && (
|
||||
<span className="text-muted small ms-1">
|
||||
({items.length - filtered.length} скрыто)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<div className="card card-sm animate-in" style={{animationDelay: '0.2s'}}>
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="bg-purple text-white avatar">
|
||||
<IconUsers size={24} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="font-weight-medium">Уникальных Community</div>
|
||||
<div className="text-muted">
|
||||
<span className="h2 mb-0">{allCommunities.length}</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col-sm-6 col-lg-3">
|
||||
<div className="card card-sm animate-in" style={{animationDelay: '0.3s'}}>
|
||||
<div className="card-body">
|
||||
<div className="row align-items-center">
|
||||
<div className="col-auto">
|
||||
<span className="bg-orange text-white avatar">
|
||||
<IconTrendingUp size={24} />
|
||||
</span>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="font-weight-medium">Выбрано</div>
|
||||
<div className="text-muted">
|
||||
<span className="h2 mb-0">{selectedDomains.size}</span>
|
||||
{selectedDomains.size > 0 && (
|
||||
<span className="text-muted small ms-1">
|
||||
({Math.round((selectedDomains.size / filtered.length) * 100)}%)
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="row">
|
||||
<div className="col-lg-3">
|
||||
{/* Classic Add New Domain Card (improved) */}
|
||||
@@ -908,14 +834,14 @@ function DomainsNewManager() {
|
||||
)}
|
||||
</div>
|
||||
</th>
|
||||
<th className="text-end"> </th>
|
||||
<th className="text-end">Действия</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{paginatedItems.map((item, idx) => (
|
||||
<tr
|
||||
key={item.domain}
|
||||
className={editingDomain === item.domain ? 'table-active' : ''}
|
||||
className={editingDomain === item.domain ? 'table-active' : (selectedDomains.has(item.domain) ? 'table-selected' : '')}
|
||||
style={{
|
||||
animation: `fadeIn 0.3s ease ${idx * 0.02}s both`
|
||||
}}
|
||||
@@ -927,12 +853,22 @@ function DomainsNewManager() {
|
||||
checked={selectedDomains.has(item.domain)}
|
||||
onChange={() => handleToggleSelect(item.domain)}
|
||||
aria-label={`Выбрать ${item.domain}`}
|
||||
onClick={(e) => e.stopPropagation()}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<div className="d-flex align-items-center">
|
||||
<IconWorld size={16} className="me-2 text-muted" />
|
||||
<span className="font-monospace">{item.domain}</span>
|
||||
<Tooltip content="Копировать домен">
|
||||
<button
|
||||
className="btn btn-ghost-secondary btn-icon btn-sm ms-2"
|
||||
onClick={() => copyToClipboard(item.domain)}
|
||||
aria-label="Копировать"
|
||||
>
|
||||
<IconCopy size={14} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
@@ -952,42 +888,55 @@ function DomainsNewManager() {
|
||||
className={`form-control d-inline-block me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
|
||||
style={{ width: '150px' }}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-success btn-icon btn-sm"
|
||||
onClick={() => handleSaveEdit(item.domain)}
|
||||
disabled={!isValidCommunity(editingValue)}
|
||||
aria-label="Сохранить"
|
||||
title="Сохранить (Enter)"
|
||||
>
|
||||
<IconCheck size={16} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-secondary btn-icon btn-sm"
|
||||
onClick={handleCancelEdit}
|
||||
aria-label="Отмена"
|
||||
title="Отмена (Esc)"
|
||||
>
|
||||
<IconX size={16} />
|
||||
</button>
|
||||
<Tooltip content="Сохранить (Enter)">
|
||||
<button
|
||||
className="btn btn-success btn-icon btn-sm"
|
||||
onClick={() => handleSaveEdit(item.domain)}
|
||||
disabled={!isValidCommunity(editingValue)}
|
||||
aria-label="Сохранить"
|
||||
>
|
||||
<IconCheck size={16} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Отмена (Esc)">
|
||||
<button
|
||||
className="btn btn-secondary btn-icon btn-sm"
|
||||
onClick={handleCancelEdit}
|
||||
aria-label="Отмена"
|
||||
>
|
||||
<IconX size={16} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</>
|
||||
) : (
|
||||
<>
|
||||
<button
|
||||
className="btn btn-outline-primary btn-icon btn-sm"
|
||||
aria-label="Редактировать"
|
||||
onClick={() => handleEdit(item)}
|
||||
title="Редактировать community"
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</button>
|
||||
<button
|
||||
className="btn btn-outline-danger btn-icon btn-sm"
|
||||
aria-label="Удалить"
|
||||
onClick={() => confirmDelete(item)}
|
||||
title="Удалить домен"
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</button>
|
||||
<Tooltip content="Редактировать">
|
||||
<button
|
||||
className="btn btn-outline-primary btn-icon btn-sm"
|
||||
aria-label="Редактировать"
|
||||
onClick={() => handleEdit(item)}
|
||||
>
|
||||
<IconEdit size={16} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Копировать">
|
||||
<button
|
||||
className="btn btn-outline-secondary btn-icon btn-sm"
|
||||
aria-label="Копировать"
|
||||
onClick={() => copyToClipboard(`${item.domain} ${item.community}`)}
|
||||
>
|
||||
<IconCopy size={16} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
<Tooltip content="Удалить">
|
||||
<button
|
||||
className="btn btn-outline-danger btn-icon btn-sm"
|
||||
aria-label="Удалить"
|
||||
onClick={() => confirmDelete(item)}
|
||||
>
|
||||
<IconTrash size={16} />
|
||||
</button>
|
||||
</Tooltip>
|
||||
</>
|
||||
)}
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user