refactor(Managers): integrate AddItemModal into ASNs, Domains, and IPRanges managers for improved item addition functionality and UI consistency
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m57s

This commit is contained in:
2026-02-16 16:09:05 +07:00
parent 3a7578ce26
commit 94efee905a
4 changed files with 375 additions and 381 deletions
+35 -134
View File
@@ -47,6 +47,7 @@ import BulkActionsBar from './components/BulkActionsBar.jsx';
import FormField from './components/FormField.jsx';
import Tooltip from './components/Tooltip.jsx';
import Pagination from './components/Pagination.jsx';
import AddItemModal from './components/AddItemModal.jsx';
import { useToast } from './components/ToastContainer.jsx';
const API_URL = '/api';
@@ -70,6 +71,7 @@ function DomainsNewManager() {
const [loading, setLoading] = useState(false);
const [showDeleteModal, setShowDeleteModal] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null);
const [addModalOpen, setAddModalOpen] = useState(false);
const [currentPage, setCurrentPage] = useState(1);
const [totalItems, setTotalItems] = useState(0);
const [sortField, setSortField] = useState('domain');
@@ -137,17 +139,23 @@ function DomainsNewManager() {
// Подгрузка при изменении страницы/поиска/фильтра
useEffect(() => { if (!didInit.current) { didInit.current = true; fetchItems(); } }, []);
const handleAddItem = () => {
const domainOk = isValidDomain(newItem.domain);
const communityOk = isValidCommunity(newItem.community);
setNewInvalid({ domain: !domainOk, community: !communityOk });
const handleAddItem = (item) => {
// Если item передан (из модалки), используем его, иначе из state (старый способ)
const domain = item?.domain || newItem.domain;
const community = item?.community || newItem.community;
const domainOk = isValidDomain(domain);
const communityOk = isValidCommunity(community);
if (!domainOk || !communityOk) {
setError('Заполните корректный домен (example.com) и числовой community.');
return;
}
setError('');
setItems([...items, { domain: newItem.domain.trim().toLowerCase(), community: String(newItem.community).trim() }]);
setNewItem({ domain: '', community: '' });
const newDomain = { domain: domain.trim().toLowerCase(), community: String(community).trim() };
setItems([...items, newDomain]);
if (!item) setNewItem({ domain: '', community: '' }); // Сброс только если не из модалки
setNewInvalid({ domain: false, community: false });
};
@@ -520,7 +528,16 @@ function DomainsNewManager() {
/>
)}
actions={(
<PageHeaderActions
<div className="d-flex align-items-center gap-2">
<button
className="btn btn-primary"
onClick={() => setAddModalOpen(true)}
title="Добавить новый домен"
>
<IconPlus className="me-1" size={18} />
Добавить
</button>
<PageHeaderActions
loading={loading}
onPreview={handlePreviewDiff}
disablePreview={loading || items.length === 0}
@@ -550,137 +567,21 @@ function DomainsNewManager() {
}
}}
/>
</div>
)}
/>
<AddItemModal
open={addModalOpen}
onClose={() => setAddModalOpen(false)}
onSubmit={handleAddItem}
type="domain"
communities={communities}
loading={loading}
/>
<div className="row">
<div className="col-lg-3">
{/* Classic Add New Domain Card (improved) */}
<div className="card card-md animate-in">
<div className="card-header bg-primary-lt">
<h3 className="card-title text-primary">
<IconPlus className="icon me-2" />
Добавить новый домен
</h3>
</div>
<div className="card-body">
<form onSubmit={(e) => { e.preventDefault(); handleAddItem(); }}>
<div className="mb-3">
<label className="form-label required">Домен</label>
<div className="input-icon">
<span className="input-icon-addon">
<IconWorld size={18} />
</span>
<input
type="text"
className={`form-control${newInvalid.domain ? ' is-invalid' : isValidDomain(newItem.domain) ? ' is-valid' : ''}`}
placeholder="example.com"
value={newItem.domain}
onChange={(e) => setNewItem({ ...newItem, domain: e.target.value })}
/>
</div>
{newInvalid.domain && <div className="invalid-feedback">Неверный формат домена</div>}
{isValidDomain(newItem.domain) && !newInvalid.domain && (
<div className="valid-feedback">Корректный формат домена</div>
)}
<div className="form-text text-muted small">
Формат: sub.domain.tld (например foo.example.org)
</div>
</div>
<div className="mb-3">
<label className="form-label required">Community</label>
<div className="input-icon">
<span className="input-icon-addon">
<IconHash size={18} />
</span>
<CommunityAutocompleteInput
value={newItem.community}
onChange={(v) => setNewItem({ ...newItem, community: v })}
communities={communities}
placeholder="Начните вводить номер или имя"
className={`form-control${newInvalid.community ? ' is-invalid' : isValidCommunity(newItem.community) ? ' is-valid' : ''}`}
useShortValue={true}
/>
</div>
{newInvalid.community && <div className="invalid-feedback">Неверный формат community</div>}
{isValidCommunity(newItem.community) && !newInvalid.community && (
<div className="valid-feedback">Корректный формат community</div>
)}
<div className="form-text text-muted small">
Формат: N или N:N (например 65000:100)
</div>
{(() => {
const match = communities.find(c => c.value === String(newItem.community).trim());
if (!match) return null;
return (
<div className="alert alert-info alert-dismissible mt-2 mb-0" role="alert">
<div className="d-flex">
<div>
<strong>{match.name || match.value}</strong>
{match.description && <div className="text-muted small">{match.description}</div>}
{match.tags && match.tags.length > 0 && (
<div className="mt-1">
{match.tags.map((tag, idx) => (
<span key={idx} className="badge bg-blue-lt text-blue me-1">{tag}</span>
))}
</div>
)}
</div>
</div>
</div>
);
})()}
</div>
<div className="form-footer">
<button
type="submit"
className="btn btn-primary w-100"
disabled={!isValidDomain(newItem.domain) || !isValidCommunity(newItem.community)}
>
<IconPlus className="icon me-2" />
Добавить домен
</button>
</div>
</form>
</div>
</div>
{/* Дополнительные варианты */}
<AccordionCard title={'Дополнительные варианты'} defaultOpen={false} className="animate-in" style={{animationDelay: '0.1s'}}>
<QuickAddBar
placeholder={'example.com 65000:100'}
help={'Вставьте строки: DOMAIN ПРОБЕЛ COMMUNITY'}
parseLine={(line) => { const [d, c] = String(line).split(/\s+/); return { domain: (d||'').toLowerCase(), community: (c||'').trim() }; }}
validateItem={(obj) => isValidDomain(obj.domain) && isValidCommunity(obj.community)}
onApply={(list) => {
setItems(prev => {
const merged = [...prev, ...list];
const map = new Map();
for (const it of merged) {
const key = String(it.domain).trim().toLowerCase();
if (!map.has(key)) map.set(key, { domain: key, community: String(it.community).trim() });
}
return Array.from(map.values());
});
}}
/>
<div
className="card card-md border-dashed mt-3 animate-in"
style={{animationDelay: '0.15s'}}
onDragOver={(e) => { e.preventDefault(); e.currentTarget.style.borderColor = 'var(--tblr-primary)'; }}
onDragLeave={(e) => { e.currentTarget.style.borderColor = ''; }}
onDrop={(e) => { e.currentTarget.style.borderColor = ''; onDropImport(e); }}
>
<div className="card-body text-center text-muted py-4">
<IconUpload size={32} className="mb-2 text-muted" />
<div className="fw-medium">Перетащите файл TXT/CSV сюда</div>
<div className="small">Формат: domain community</div>
</div>
</div>
</AccordionCard>
</div>
<div className="col-lg-9">
<div className="col-12">
{/* Bulk Actions Bar */}
<BulkActionsBar
selectedCount={selectedDomains.size}