Files
router-lists-ui/frontend/src/components/ConfirmDiffModal.jsx
T
denozord 58ee09b70e
Publish Docker image / build-and-push (push) Successful in 2m24s
Update frontend dependencies, enhance Vite configuration, and refactor components
- Added new dependencies including @base-ui/react, @fontsource-variable/geist, and tailwindcss for improved UI and styling.
- Updated Vite configuration to include path aliasing for easier imports.
- Refactored App.jsx to implement a theme context and improved routing structure.
- Enhanced various components (e.g., ConfirmDialog, DataTable) to utilize new UI components and improve user experience.
- Cleaned up CSS imports and ensured proper styling integration.

Made-with: Cursor
2026-04-26 13:48:59 +07:00

229 lines
10 KiB
React

import { IconPlus, IconMinus, IconEdit } from '@tabler/icons-react';
/**
* ConfirmDiffModal - модальное окно для подтверждения изменений с отображением детального diff
* Показывает ЧТО именно изменилось, не только цифры
* Использует modal-backdrop show как HistoryModal
*/
function ConfirmDiffModal({ show, diff, onConfirm, onClose, loading = false }) {
const added = diff?.added || [];
const removed = diff?.removed || [];
const changed = diff?.changed || [];
const total = added.length + removed.length + changed.length;
if (!show) return null;
const handleBackdropClick = (e) => {
if (e.target === e.currentTarget) {
onClose?.();
}
};
// Показываем только первые 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 */}
<div className="modal-backdrop show" onClick={handleBackdropClick} />
{/* Modal */}
<div
className="modal show d-block"
role="dialog"
aria-modal="true"
tabIndex={-1}
onKeyDown={(e) => { if (e.key === 'Escape') onClose?.(); }}
>
<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>
<button type="button" className="btn-close" onClick={onClose} aria-label="Закрыть" />
</div>
<div className="modal-body">
{/* Статистика */}
<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>
{/* Статистика в карточках */}
<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>
)}
{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>
)}
{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"
onClick={(e) => e.preventDefault()}
>
<IconPlus className="text-success me-2" size={20} />
Добавлено ({added.length})
</button>
</h2>
<div id="collapseAdded" className="accordion-collapse collapse show" 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"
onClick={(e) => e.preventDefault()}
>
<IconMinus className="text-danger me-2" size={20} />
Удалено ({removed.length})
</button>
</h2>
<div id="collapseRemoved" className="accordion-collapse collapse show" 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"
onClick={(e) => e.preventDefault()}
>
<IconEdit className="text-info me-2" size={20} />
Изменено ({changed.length})
</button>
</h2>
<div id="collapseChanged" className="accordion-collapse collapse show" 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>
<div className="modal-footer">
<button
type="button"
className="btn btn-secondary"
onClick={onClose}
disabled={loading}
>
Отмена
</button>
<button
type="button"
className="btn btn-primary"
onClick={onConfirm}
disabled={loading}
>
{loading && (
<span className="spinner-border spinner-border-sm me-2" role="status" aria-hidden="true" />
)}
Сохранить {total} {total === 1 ? 'изменение' : total < 5 ? 'изменения' : 'изменений'}
</button>
</div>
</div>
</div>
</div>
</>
);
}
export default ConfirmDiffModal;