feat: Добавить уведомления об успешных мутациях и глобальные горячие клавиши для сохранения изменений в менеджерах ASNs, Domains и IPRanges
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m43s

This commit is contained in:
2025-08-27 16:57:40 +07:00
parent 8d0c28de92
commit 3358b8daed
5 changed files with 101 additions and 42 deletions
+20 -14
View File
@@ -17,7 +17,6 @@ import {
IconClock,
IconFileText
} from '@tabler/icons-react';
import LockBanner from './components/LockBanner.jsx';
import TableSkeleton from './components/TableSkeleton.jsx';
import S3MetaBar from './components/S3MetaBar.jsx';
import PageHeaderActions from './components/PageHeaderActions.jsx';
@@ -25,6 +24,8 @@ import ConfirmDialog from './components/ConfirmDialog.jsx';
import WsUpdateModal from './components/WsUpdateModal.jsx';
import ConfirmDiffModal from './components/ConfirmDiffModal.jsx';
import HistoryModal from './components/HistoryModal.jsx';
import ErrorAlert from './components/ErrorAlert.jsx';
import { notifyMutationSuccess } from './components/NotifyProvider.jsx';
const API_URL = '/api';
@@ -265,6 +266,7 @@ function DomainsNewManager() {
setItems(unique);
setSuccess('Изменения успешно сохранены!');
setTimeout(() => setSuccess(''), 3000);
notifyMutationSuccess('Изменения сохранены');
} catch (error) {
console.error('Error saving changes:', error);
setError('Не удалось сохранить изменения.');
@@ -375,6 +377,18 @@ function DomainsNewManager() {
}
}, [editingDomain]);
// Глобальные хоткеи: Ctrl+S — сохранить изменения
useEffect(() => {
const onKey = (e) => {
if ((e.ctrlKey || e.metaKey) && (e.key === 's' || e.key === 'S')) {
e.preventDefault();
if (!loading) handleSaveChanges();
}
};
window.addEventListener('keydown', onKey);
return () => window.removeEventListener('keydown', onKey);
}, [loading, items, originalItems, etag, sortField, sortOrder, filterCommunity]);
const handleEditKeyDown = (e, domainName) => {
if (e.key === 'Enter') handleSaveEdit(domainName);
if (e.key === 'Escape') handleCancelEdit();
@@ -383,17 +397,7 @@ function DomainsNewManager() {
return (
<>
{error && (
<div className="alert alert-danger alert-dismissible" role="alert">
<div className="d-flex">
<div>
<IconX className="icon alert-icon" />
</div>
<div>
{error}
</div>
</div>
<button type="button" className="btn-close" onClick={() => setError('')}></button>
</div>
<ErrorAlert message={error} onClose={() => setError('')} />
)}
{success && (
@@ -439,7 +443,7 @@ function DomainsNewManager() {
if (!res.ok || data.ok === false) {
throw new Error(data?.message || `HTTP ${res.status}`);
}
window.notify?.success('Фоновое обновление запущено', data);
notifyMutationSuccess('Фоновое обновление запущено', data);
} catch (e) {
window.notify?.error('Не удалось запустить фоновое обновление', String(e));
}
@@ -450,7 +454,6 @@ function DomainsNewManager() {
</div>
<div className="row">
<div className="col-lg-3">
<LockBanner resource="domains-new" className="mb-3" />
{/* Add New Item Card */}
<div className="card card-md">
<div className="card-header">
@@ -472,6 +475,7 @@ function DomainsNewManager() {
/>
{newInvalid.domain && <div className="invalid-feedback">Неверный формат домена</div>}
<div className="form-text">Без http/https, только доменное имя</div>
<div className="form-text">Формат: sub.domain.tld (например foo.example.org)</div>
</div>
<div className="mb-3">
<label className="form-label">Community</label>
@@ -483,6 +487,7 @@ function DomainsNewManager() {
className={`form-control${newInvalid.community ? ' is-invalid' : ''}`}
/>
{newInvalid.community && <div className="invalid-feedback">Только цифры</div>}
<div className="form-text">Формат: N или N:N (например 65000:100)</div>
{(() => {
const match = communities.find(c => c.value === String(newItem.community).trim());
if (!match) return null;
@@ -588,6 +593,7 @@ function DomainsNewManager() {
value={editingValue}
onChange={setEditingValue}
communities={communities}
onKeyDown={(e) => handleEditKeyDown(e, item.domain)}
className={`form-control d-inline-block w-auto me-2${isValidCommunity(editingValue) ? '' : ' is-invalid'}`}
/>
<button className="btn btn-success btn-icon btn-sm me-1" onClick={() => handleSaveEdit(item.domain)} disabled={!isValidCommunity(editingValue)}><IconCheck size={16} /></button>