feat: Добавление умного подтверждения изменений в компоненты ASNsNewManager, DomainsNewManager и IPRangesManager. Модальное окно ConfirmDiffModal обновлено для отображения детального diff и статистики изменений, улучшая взаимодействие с пользователем при массовых изменениях.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m49s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m49s
This commit is contained in:
@@ -1,13 +1,16 @@
|
||||
import { IconCheck, IconX, IconRefresh } from '@tabler/icons-react';
|
||||
import { IconPlus, IconMinus, IconEdit } from '@tabler/icons-react';
|
||||
|
||||
/**
|
||||
* ConfirmDiffModal - модальное окно для подтверждения изменений с отображением статистики
|
||||
* Простой подход со встроенным backdrop (как в HistoryModal)
|
||||
* ConfirmDiffModal - модальное окно для подтверждения изменений с отображением детального diff
|
||||
* Показывает ЧТО именно изменилось, не только цифры
|
||||
* Использует modal-backdrop show как HistoryModal
|
||||
*/
|
||||
function ConfirmDiffModal({ show, diff, onConfirm, onClose, loading = false }) {
|
||||
const added = diff?.added?.length || 0;
|
||||
const removed = diff?.removed?.length || 0;
|
||||
const changed = diff?.changed?.length || 0;
|
||||
const added = diff?.added || [];
|
||||
const removed = diff?.removed || [];
|
||||
const changed = diff?.changed || [];
|
||||
|
||||
const total = added.length + removed.length + changed.length;
|
||||
|
||||
if (!show) return null;
|
||||
|
||||
@@ -17,6 +20,12 @@ function ConfirmDiffModal({ show, diff, onConfirm, onClose, loading = false }) {
|
||||
}
|
||||
};
|
||||
|
||||
// Показываем только первые 5 элементов каждого типа для удобства
|
||||
const maxPreview = 5;
|
||||
const addedPreview = added.slice(0, maxPreview);
|
||||
const removedPreview = removed.slice(0, maxPreview);
|
||||
const changedPreview = changed.slice(0, maxPreview);
|
||||
|
||||
return (
|
||||
<>
|
||||
{/* Modal Backdrop */}
|
||||
@@ -30,58 +39,164 @@ function ConfirmDiffModal({ show, diff, onConfirm, onClose, loading = false }) {
|
||||
tabIndex={-1}
|
||||
onKeyDown={(e) => { if (e.key === 'Escape') onClose?.(); }}
|
||||
>
|
||||
<div className="modal-dialog modal-sm modal-dialog-centered" role="document">
|
||||
<div
|
||||
className="modal-content"
|
||||
tabIndex={-1}
|
||||
onKeyDown={(e) => {
|
||||
if (e.key === 'Tab') {
|
||||
const c = e.currentTarget;
|
||||
const focusable = c.querySelectorAll('button, [href], input, select, textarea, [tabindex]:not([tabindex="-1"])');
|
||||
if (!focusable || focusable.length === 0) return;
|
||||
const first = focusable[0];
|
||||
const last = focusable[focusable.length - 1];
|
||||
if (e.shiftKey && document.activeElement === first) {
|
||||
e.preventDefault();
|
||||
last.focus();
|
||||
} else if (!e.shiftKey && document.activeElement === last) {
|
||||
e.preventDefault();
|
||||
first.focus();
|
||||
}
|
||||
}
|
||||
}}
|
||||
>
|
||||
<div className="modal-dialog modal-lg modal-dialog-centered modal-dialog-scrollable" role="document">
|
||||
<div className="modal-content">
|
||||
<div className="modal-header">
|
||||
<h5 className="modal-title">Подтвердить сохранение</h5>
|
||||
<h5 className="modal-title">Подтвердить массовое изменение</h5>
|
||||
<button type="button" className="btn-close" onClick={onClose} aria-label="Закрыть" />
|
||||
</div>
|
||||
|
||||
<div className="modal-body">
|
||||
<div className="row g-2 text-center">
|
||||
<div className="col">
|
||||
<div className="card">
|
||||
<div className="card-body p-2">
|
||||
<strong>Добавлено</strong>
|
||||
<div className="text-muted">{added}</div>
|
||||
{/* Статистика */}
|
||||
<div className="alert alert-warning mb-3">
|
||||
<div className="d-flex">
|
||||
<div>
|
||||
<h4 className="alert-title">Внимание! Массовое изменение данных</h4>
|
||||
<div className="text-muted">
|
||||
Вы собираетесь изменить <strong>{total}</strong> {total === 1 ? 'запись' : total < 5 ? 'записи' : 'записей'}.
|
||||
Пожалуйста, проверьте изменения перед сохранением.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="card">
|
||||
<div className="card-body p-2">
|
||||
<strong>Удалено</strong>
|
||||
<div className="text-muted">{removed}</div>
|
||||
</div>
|
||||
|
||||
{/* Статистика в карточках */}
|
||||
<div className="row g-2 text-center mb-3">
|
||||
{added.length > 0 && (
|
||||
<div className="col">
|
||||
<div className="card border-success">
|
||||
<div className="card-body p-3">
|
||||
<IconPlus className="text-success mb-2" size={24} />
|
||||
<div className="h3 mb-0">{added.length}</div>
|
||||
<div className="text-muted small">Добавлено</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="col">
|
||||
<div className="card">
|
||||
<div className="card-body p-2">
|
||||
<strong>Изменено</strong>
|
||||
<div className="text-muted">{changed}</div>
|
||||
)}
|
||||
{removed.length > 0 && (
|
||||
<div className="col">
|
||||
<div className="card border-danger">
|
||||
<div className="card-body p-3">
|
||||
<IconMinus className="text-danger mb-2" size={24} />
|
||||
<div className="h3 mb-0">{removed.length}</div>
|
||||
<div className="text-muted small">Удалено</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{changed.length > 0 && (
|
||||
<div className="col">
|
||||
<div className="card border-info">
|
||||
<div className="card-body p-3">
|
||||
<IconEdit className="text-info mb-2" size={24} />
|
||||
<div className="h3 mb-0">{changed.length}</div>
|
||||
<div className="text-muted small">Изменено</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Детальный preview изменений */}
|
||||
<div className="accordion" id="diffAccordion">
|
||||
{/* Добавленные */}
|
||||
{added.length > 0 && (
|
||||
<div className="accordion-item">
|
||||
<h2 className="accordion-header">
|
||||
<button
|
||||
className="accordion-button collapsed"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#collapseAdded"
|
||||
>
|
||||
<IconPlus className="text-success me-2" size={20} />
|
||||
Добавлено ({added.length})
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseAdded" className="accordion-collapse collapse" data-bs-parent="#diffAccordion">
|
||||
<div className="accordion-body">
|
||||
<ul className="list-unstyled mb-0" style={{ maxHeight: '200px', overflowY: 'auto' }}>
|
||||
{addedPreview.map((item, idx) => (
|
||||
<li key={idx} className="mb-1">
|
||||
<code className="text-success">+ {typeof item === 'string' ? item : JSON.stringify(item)}</code>
|
||||
</li>
|
||||
))}
|
||||
{added.length > maxPreview && (
|
||||
<li className="text-muted small">
|
||||
... и ещё {added.length - maxPreview} {added.length - maxPreview === 1 ? 'запись' : 'записей'}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Удалённые */}
|
||||
{removed.length > 0 && (
|
||||
<div className="accordion-item">
|
||||
<h2 className="accordion-header">
|
||||
<button
|
||||
className="accordion-button collapsed"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#collapseRemoved"
|
||||
>
|
||||
<IconMinus className="text-danger me-2" size={20} />
|
||||
Удалено ({removed.length})
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseRemoved" className="accordion-collapse collapse" data-bs-parent="#diffAccordion">
|
||||
<div className="accordion-body">
|
||||
<ul className="list-unstyled mb-0" style={{ maxHeight: '200px', overflowY: 'auto' }}>
|
||||
{removedPreview.map((item, idx) => (
|
||||
<li key={idx} className="mb-1">
|
||||
<code className="text-danger">- {typeof item === 'string' ? item : JSON.stringify(item)}</code>
|
||||
</li>
|
||||
))}
|
||||
{removed.length > maxPreview && (
|
||||
<li className="text-muted small">
|
||||
... и ещё {removed.length - maxPreview} {removed.length - maxPreview === 1 ? 'запись' : 'записей'}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{/* Изменённые */}
|
||||
{changed.length > 0 && (
|
||||
<div className="accordion-item">
|
||||
<h2 className="accordion-header">
|
||||
<button
|
||||
className="accordion-button collapsed"
|
||||
type="button"
|
||||
data-bs-toggle="collapse"
|
||||
data-bs-target="#collapseChanged"
|
||||
>
|
||||
<IconEdit className="text-info me-2" size={20} />
|
||||
Изменено ({changed.length})
|
||||
</button>
|
||||
</h2>
|
||||
<div id="collapseChanged" className="accordion-collapse collapse" data-bs-parent="#diffAccordion">
|
||||
<div className="accordion-body">
|
||||
<ul className="list-unstyled mb-0" style={{ maxHeight: '200px', overflowY: 'auto' }}>
|
||||
{changedPreview.map((item, idx) => (
|
||||
<li key={idx} className="mb-1">
|
||||
<code className="text-info">~ {typeof item === 'string' ? item : JSON.stringify(item)}</code>
|
||||
</li>
|
||||
))}
|
||||
{changed.length > maxPreview && (
|
||||
<li className="text-muted small">
|
||||
... и ещё {changed.length - maxPreview} {changed.length - maxPreview === 1 ? 'запись' : 'записей'}
|
||||
</li>
|
||||
)}
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -103,7 +218,7 @@ function ConfirmDiffModal({ show, diff, onConfirm, onClose, loading = false }) {
|
||||
{loading && (
|
||||
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true" />
|
||||
)}
|
||||
Сохранить
|
||||
Сохранить {total} {total === 1 ? 'изменение' : total < 5 ? 'изменения' : 'изменений'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
Reference in New Issue
Block a user