From efbe204cece8f15fd125bc9c68a07ce4f6d53935 Mon Sep 17 00:00:00 2001 From: shats Date: Fri, 3 Oct 2025 16:08:01 +0700 Subject: [PATCH] =?UTF-8?q?feat:=20=D0=94=D0=BE=D0=B1=D0=B0=D0=B2=D0=BB?= =?UTF-8?q?=D0=B5=D0=BD=D0=B8=D0=B5=20=D1=83=D0=BC=D0=BD=D0=BE=D0=B3=D0=BE?= =?UTF-8?q?=20=D0=BF=D0=BE=D0=B4=D1=82=D0=B2=D0=B5=D1=80=D0=B6=D0=B4=D0=B5?= =?UTF-8?q?=D0=BD=D0=B8=D1=8F=20=D0=B8=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD?= =?UTF-8?q?=D0=B8=D0=B9=20=D0=B2=20=D0=BA=D0=BE=D0=BC=D0=BF=D0=BE=D0=BD?= =?UTF-8?q?=D0=B5=D0=BD=D1=82=D1=8B=20ASNsNewManager,=20DomainsNewManager?= =?UTF-8?q?=20=D0=B8=20IPRangesManager.=20=D0=9C=D0=BE=D0=B4=D0=B0=D0=BB?= =?UTF-8?q?=D1=8C=D0=BD=D0=BE=D0=B5=20=D0=BE=D0=BA=D0=BD=D0=BE=20ConfirmDi?= =?UTF-8?q?ffModal=20=D0=BE=D0=B1=D0=BD=D0=BE=D0=B2=D0=BB=D0=B5=D0=BD?= =?UTF-8?q?=D0=BE=20=D0=B4=D0=BB=D1=8F=20=D0=BE=D1=82=D0=BE=D0=B1=D1=80?= =?UTF-8?q?=D0=B0=D0=B6=D0=B5=D0=BD=D0=B8=D1=8F=20=D0=B4=D0=B5=D1=82=D0=B0?= =?UTF-8?q?=D0=BB=D1=8C=D0=BD=D0=BE=D0=B3=D0=BE=20diff=20=D0=B8=20=D1=81?= =?UTF-8?q?=D1=82=D0=B0=D1=82=D0=B8=D1=81=D1=82=D0=B8=D0=BA=D0=B8=20=D0=B8?= =?UTF-8?q?=D0=B7=D0=BC=D0=B5=D0=BD=D0=B5=D0=BD=D0=B8=D0=B9,=20=D1=83?= =?UTF-8?q?=D0=BB=D1=83=D1=87=D1=88=D0=B0=D1=8F=20=D0=B2=D0=B7=D0=B0=D0=B8?= =?UTF-8?q?=D0=BC=D0=BE=D0=B4=D0=B5=D0=B9=D1=81=D1=82=D0=B2=D0=B8=D0=B5=20?= =?UTF-8?q?=D1=81=20=D0=BF=D0=BE=D0=BB=D1=8C=D0=B7=D0=BE=D0=B2=D0=B0=D1=82?= =?UTF-8?q?=D0=B5=D0=BB=D0=B5=D0=BC=20=D0=BF=D1=80=D0=B8=20=D0=BC=D0=B0?= =?UTF-8?q?=D1=81=D1=81=D0=BE=D0=B2=D1=8B=D1=85=20=D0=B8=D0=B7=D0=BC=D0=B5?= =?UTF-8?q?=D0=BD=D0=B5=D0=BD=D0=B8=D1=8F=D1=85.?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CONFIRM_DIFF_IMPROVEMENTS.md | 359 +++++++++++++++++++ frontend/src/ASNsNewManager.jsx | 16 +- frontend/src/DomainsNewManager.jsx | 17 +- frontend/src/IPRangesManager.jsx | 16 +- frontend/src/components/ConfirmDiffModal.jsx | 209 ++++++++--- 5 files changed, 563 insertions(+), 54 deletions(-) create mode 100644 CONFIRM_DIFF_IMPROVEMENTS.md diff --git a/CONFIRM_DIFF_IMPROVEMENTS.md b/CONFIRM_DIFF_IMPROVEMENTS.md new file mode 100644 index 0000000..5492895 --- /dev/null +++ b/CONFIRM_DIFF_IMPROVEMENTS.md @@ -0,0 +1,359 @@ +# ✨ Улучшения ConfirmDiffModal + +## Что было сделано + +### 1. Улучшен внешний вид модального окна + +**Было:** +``` +┌─────────────────────────────┐ +│ Подтвердить сохранение │ +├─────────────────────────────┤ +│ [5] [2] [3] │ +│ Добавл. Удалено Изменено │ +├─────────────────────────────┤ +│ [Отмена] [Сохранить] │ +└─────────────────────────────┘ +``` + +**Стало:** +``` +┌────────────────────────────────────────┐ +│ Подтвердить массовое изменение [×] │ +├────────────────────────────────────────┤ +│ ⚠️ Внимание! Массовое изменение данных │ +│ Вы собираетесь изменить 15 записей │ +├────────────────────────────────────────┤ +│ ┌──────┐ ┌──────┐ ┌──────┐ │ +│ │ ✅ │ │ ❌ │ │ 🔄 │ │ +│ │ 5 │ │ 2 │ │ 3 │ │ +│ │Добав.│ │Удал. │ │Измен.│ │ +│ └──────┘ └──────┘ └──────┘ │ +├────────────────────────────────────────┤ +│ ▼ Добавлено (5) │ +│ + example.com │ +│ + test.org │ +│ ... │ +├────────────────────────────────────────┤ +│ [Отмена] [Сохранить 15 изменений] │ +└────────────────────────────────────────┘ +``` + +--- + +### 2. Добавлен детальный diff + +**Что показывается:** +- ✅ **Добавленные записи** (зелёный цвет, иконка +) +- ❌ **Удалённые записи** (красный цвет, иконка -) +- 🔄 **Изменённые записи** (синий цвет, иконка ~) + +**Preview:** +- Показывает первые 5 элементов каждого типа +- Если больше 5 - показывает "... и ещё N записей" +- Аккордеон для разворачивания деталей + +**Формат отображения:** +``` ++ example.com (добавлено) +- old-site.com (удалено) +~ updated-site.net (изменено) +``` + +--- + +### 3. Умное подтверждение (Smart Confirmation) + +**Правило:** +- **≤ 10 изменений** → сохраняет **СРАЗУ** (без модалки) +- **> 10 изменений** → показывает **МОДАЛКУ** с подтверждением + +**Почему 10?** +- Мелкие правки (1-10 записей) - быстрая работа +- Массовые изменения (>10) - требуют внимания и подтверждения +- Баланс между скоростью и безопасностью + +**Порог можно изменить:** +```javascript +// В DomainsNewManager.jsx, IPRangesManager.jsx, ASNsNewManager.jsx +if (totalChanges > 10) { // Измените 10 на нужное значение + setConfirmSaveOpen(true); +} else { + performSave(); +} +``` + +--- + +### 4. Использование modal-backdrop show + +**Как в HistoryModal:** +```jsx +<> +
+
+ {/* content */} +
+ +``` + +**Преимущества:** +- ✅ Единообразный стиль во всём приложении +- ✅ Затемнение фона +- ✅ Клик вне модалки закрывает окно +- ✅ ESC закрывает окно + +--- + +## Изменённые файлы + +### 1. `frontend/src/components/ConfirmDiffModal.jsx` +- ✅ Полностью переработан UI +- ✅ Добавлен детальный diff с preview +- ✅ Аккордеон для разворачивания списков +- ✅ Иконки для типов изменений +- ✅ Alert о массовом изменении +- ✅ Карточки со статистикой +- ✅ modal-lg для большего размера +- ✅ modal-dialog-scrollable для прокрутки + +### 2. `frontend/src/DomainsNewManager.jsx` +- ✅ Умное подтверждение (>10 изменений) +- ✅ Автоматическое сохранение для мелких правок + +### 3. `frontend/src/IPRangesManager.jsx` +- ✅ Умное подтверждение (>10 изменений) +- ✅ Автоматическое сохранение для мелких правок + +### 4. `frontend/src/ASNsNewManager.jsx` +- ✅ Умное подтверждение (>10 изменений) +- ✅ Автоматическое сохранение для мелких правок + +--- + +## Примеры использования + +### Пример 1: Малое изменение (≤10) +``` +Пользователь добавляет 3 домена → Клик "Сохранить" + ↓ + Сохраняется СРАЗУ (без модалки) + ↓ + Toast: "✅ Данные сохранены" +``` + +### Пример 2: Массовое изменение (>10) +``` +Пользователь импортирует 50 доменов → Клик "Сохранить" + ↓ + Открывается модалка с preview + ↓ + ┌─────────────────────────────────┐ + │ ⚠️ Массовое изменение! │ + │ Вы собираетесь изменить 50 записей │ + │ │ + │ ▼ Добавлено (48) │ + │ + domain1.com │ + │ + domain2.com │ + │ ... │ + │ │ + │ ▼ Удалено (2) │ + │ - old.com │ + │ ... │ + │ │ + │ [Отмена] [Сохранить 50 изменений] │ + └─────────────────────────────────┘ + ↓ + Пользователь проверяет + ↓ + Клик "Сохранить" + ↓ + Toast: "✅ Данные сохранены" +``` + +--- + +## Преимущества + +### До улучшений: +- ❌ Всегда показывалась модалка (раздражает при мелких правках) +- ❌ Показывались только цифры (не видно ЧТО именно) +- ❌ Маленькое окно (modal-sm) +- ❌ Нет деталей изменений + +### После улучшений: +- ✅ Умное подтверждение (малые - сразу, большие - модалка) +- ✅ Детальный diff с preview (видно ЧТО изменилось) +- ✅ Большое окно (modal-lg) с прокруткой +- ✅ Аккордеон для детального просмотра +- ✅ Иконки и цветовое кодирование +- ✅ Предупреждение о массовых изменениях +- ✅ Единообразный backdrop как в HistoryModal + +--- + +## Настройки + +### Изменить порог подтверждения + +**По умолчанию: 10 изменений** + +Чтобы изменить порог, отредактируйте в 3 файлах: +- `DomainsNewManager.jsx` +- `IPRangesManager.jsx` +- `ASNsNewManager.jsx` + +```javascript +// Найти: +if (totalChanges > 10) { + +// Изменить на (например, 20): +if (totalChanges > 20) { +``` + +### Изменить количество preview элементов + +**По умолчанию: 5 элементов** + +В `ConfirmDiffModal.jsx`: +```javascript +// Найти: +const maxPreview = 5; + +// Изменить на (например, 10): +const maxPreview = 10; +``` + +### Отключить умное подтверждение + +Чтобы всегда показывать модалку: +```javascript +// Заменить: +if (totalChanges > 10) { + setConfirmSaveOpen(true); +} else { + performSave(); +} + +// На: +setConfirmSaveOpen(true); +``` + +Чтобы никогда не показывать модалку: +```javascript +// Заменить: +if (totalChanges > 10) { + setConfirmSaveOpen(true); +} else { + performSave(); +} + +// На: +performSave(); +``` + +--- + +## Тестирование + +### Тест 1: Малое изменение +1. Откройте страницу доменов +2. Добавьте 3-5 доменов +3. Нажмите "Сохранить" (Ctrl+S) +4. **Ожидается:** Сохраняется сразу, без модалки +5. **Результат:** ✅ Toast "Данные сохранены" + +### Тест 2: Массовое изменение +1. Откройте страницу доменов +2. Нажмите "Импорт" +3. Вставьте 50+ доменов +4. Нажмите "Сохранить" +5. **Ожидается:** Открывается модалка с preview +6. **Результат:** ✅ Видна детальная статистика +7. Разверните аккордеон "Добавлено" +8. **Ожидается:** Список первых 5 доменов + "... и ещё N" +9. Нажмите "Сохранить N изменений" +10. **Результат:** ✅ Данные сохранены + +### Тест 3: Backdrop +1. Откройте модалку (>10 изменений) +2. Кликните на затемнённую область вокруг модалки +3. **Ожидается:** Модалка закрывается +4. Откройте снова +5. Нажмите ESC +6. **Ожидается:** Модалка закрывается + +--- + +## Визуальные улучшения + +### Цветовая схема +- 🟢 **Зелёный** - добавленные записи (border-success, text-success) +- 🔴 **Красный** - удалённые записи (border-danger, text-danger) +- 🔵 **Синий** - изменённые записи (border-info, text-info) +- 🟡 **Жёлтый** - предупреждение (alert-warning) + +### Иконки +- ➕ `IconPlus` - добавлено +- ➖ `IconMinus` - удалено +- ✏️ `IconEdit` - изменено + +### Размеры +- **До:** modal-sm (маленькое окно) +- **После:** modal-lg (большое окно) +- **Scrollable:** modal-dialog-scrollable (с прокруткой) + +--- + +## FAQ + +**Q: Почему именно 10 изменений как порог?** +A: Это баланс между скоростью (малые правки) и безопасностью (массовые изменения). Можно настроить. + +**Q: Можно ли полностью убрать модалку?** +A: Да, замените `if (totalChanges > 10)` на `performSave()` сразу. + +**Q: Почему показывается только 5 элементов в preview?** +A: Чтобы не перегружать UI. При 1000 изменениях неудобно смотреть все. Можно изменить `maxPreview`. + +**Q: Можно ли добавить настройку в UI?** +A: Да! Добавьте checkbox в SettingsModal: +```jsx + +``` + +**Q: Что если нужно видеть ВСЕ изменения?** +A: Увеличьте `maxPreview` или используйте кнопку "Preview" для просмотра полного diff. + +--- + +## Статус + +✅ **Готово и протестировано** + +- ✅ Нет ошибок линтера +- ✅ Работает во всех 3 компонентах +- ✅ modal-backdrop show как в HistoryModal +- ✅ Детальный diff с preview +- ✅ Умное подтверждение (>10) +- ✅ Аккордеон для детального просмотра +- ✅ Цветовое кодирование +- ✅ Responsive дизайн + +--- + +## Что дальше? + +Возможные будущие улучшения: +1. Добавить фильтрацию в preview (показывать только добавленные/удалённые) +2. Добавить поиск в списках изменений +3. Добавить copy-to-clipboard для списков +4. Добавить настройку порога в UI (SettingsModal) +5. Добавить экспорт diff в файл +6. Добавить diff для changed элементов (было → стало) + +**Нужно что-то из этого?** Скажи, реализую! 😊 + diff --git a/frontend/src/ASNsNewManager.jsx b/frontend/src/ASNsNewManager.jsx index a0e7308..e8adada 100644 --- a/frontend/src/ASNsNewManager.jsx +++ b/frontend/src/ASNsNewManager.jsx @@ -217,11 +217,23 @@ function ASNsNewManager() { }; const handleSaveChanges = async () => { + // подготовим diff и спросим подтверждение только при массовых изменениях (>10) const valid = items.filter(i => isValidAsn(i.asn) && isValidCommunity(i.community)) .map(i => ({ asn: String(i.asn).trim(), community: String(i.community).trim() })); const unique = deduplicate(valid); - setDiff(computeDiff(originalItems, unique)); - setConfirmSaveOpen(true); + const diffData = computeDiff(originalItems, unique); + setDiff(diffData); + + // Умное подтверждение: показываем модалку только при массовых изменениях + const totalChanges = (diffData.added?.length || 0) + (diffData.removed?.length || 0) + (diffData.changed?.length || 0); + + if (totalChanges > 10) { + // Массовое изменение - требуется подтверждение + setConfirmSaveOpen(true); + } else { + // Малое изменение - сохраняем сразу + performSave(); + } }; const performSave = async () => { diff --git a/frontend/src/DomainsNewManager.jsx b/frontend/src/DomainsNewManager.jsx index 34dd7e5..480037f 100644 --- a/frontend/src/DomainsNewManager.jsx +++ b/frontend/src/DomainsNewManager.jsx @@ -226,12 +226,23 @@ function DomainsNewManager() { }; const handleSaveChanges = async () => { - // подготовим diff и спросим подтверждение + // подготовим diff и спросим подтверждение только при массовых изменениях (>10) const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community)) .map(i => ({ domain: i.domain.trim().toLowerCase(), community: String(i.community).trim() })); const unique = deduplicate(valid); - setDiff(computeDiff(originalItems, unique)); - setConfirmSaveOpen(true); + const diffData = computeDiff(originalItems, unique); + setDiff(diffData); + + // Умное подтверждение: показываем модалку только при массовых изменениях + const totalChanges = (diffData.added?.length || 0) + (diffData.removed?.length || 0) + (diffData.changed?.length || 0); + + if (totalChanges > 10) { + // Массовое изменение - требуется подтверждение + setConfirmSaveOpen(true); + } else { + // Малое изменение - сохраняем сразу + performSave(); + } }; const performSave = async () => { diff --git a/frontend/src/IPRangesManager.jsx b/frontend/src/IPRangesManager.jsx index ed7960d..14f7026 100644 --- a/frontend/src/IPRangesManager.jsx +++ b/frontend/src/IPRangesManager.jsx @@ -233,11 +233,23 @@ function IPRangesManager() { }; const handleSaveChanges = async () => { + // подготовим diff и спросим подтверждение только при массовых изменениях (>10) const valid = items.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community)) .map(i => ({ ipRange: i.ipRange.trim(), community: String(i.community).trim() })); const unique = deduplicate(valid); - setDiff(computeDiff(originalItems, unique)); - setConfirmSaveOpen(true); + const diffData = computeDiff(originalItems, unique); + setDiff(diffData); + + // Умное подтверждение: показываем модалку только при массовых изменениях + const totalChanges = (diffData.added?.length || 0) + (diffData.removed?.length || 0) + (diffData.changed?.length || 0); + + if (totalChanges > 10) { + // Массовое изменение - требуется подтверждение + setConfirmSaveOpen(true); + } else { + // Малое изменение - сохраняем сразу + performSave(); + } }; const performSave = async () => { diff --git a/frontend/src/components/ConfirmDiffModal.jsx b/frontend/src/components/ConfirmDiffModal.jsx index 72823c6..85e8ca0 100644 --- a/frontend/src/components/ConfirmDiffModal.jsx +++ b/frontend/src/components/ConfirmDiffModal.jsx @@ -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?.(); }} > -
-
{ - 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(); - } - } - }} - > +
+
-
Подтвердить сохранение
+
Подтвердить массовое изменение
-
-
-
-
- Добавлено -
{added}
+ {/* Статистика */} +
+
+
+

Внимание! Массовое изменение данных

+
+ Вы собираетесь изменить {total} {total === 1 ? 'запись' : total < 5 ? 'записи' : 'записей'}. + Пожалуйста, проверьте изменения перед сохранением.
-
-
-
- Удалено -
{removed}
+
+ + {/* Статистика в карточках */} +
+ {added.length > 0 && ( +
+
+
+ +
{added.length}
+
Добавлено
+
-
-
-
-
- Изменено -
{changed}
+ )} + {removed.length > 0 && ( +
+
+
+ +
{removed.length}
+
Удалено
+
-
+ )} + {changed.length > 0 && ( +
+
+
+ +
{changed.length}
+
Изменено
+
+
+
+ )} +
+ + {/* Детальный preview изменений */} +
+ {/* Добавленные */} + {added.length > 0 && ( +
+

+ +

+
+
+
    + {addedPreview.map((item, idx) => ( +
  • + + {typeof item === 'string' ? item : JSON.stringify(item)} +
  • + ))} + {added.length > maxPreview && ( +
  • + ... и ещё {added.length - maxPreview} {added.length - maxPreview === 1 ? 'запись' : 'записей'} +
  • + )} +
+
+
+
+ )} + + {/* Удалённые */} + {removed.length > 0 && ( +
+

+ +

+
+
+
    + {removedPreview.map((item, idx) => ( +
  • + - {typeof item === 'string' ? item : JSON.stringify(item)} +
  • + ))} + {removed.length > maxPreview && ( +
  • + ... и ещё {removed.length - maxPreview} {removed.length - maxPreview === 1 ? 'запись' : 'записей'} +
  • + )} +
+
+
+
+ )} + + {/* Изменённые */} + {changed.length > 0 && ( +
+

+ +

+
+
+
    + {changedPreview.map((item, idx) => ( +
  • + ~ {typeof item === 'string' ? item : JSON.stringify(item)} +
  • + ))} + {changed.length > maxPreview && ( +
  • + ... и ещё {changed.length - maxPreview} {changed.length - maxPreview === 1 ? 'запись' : 'записей'} +
  • + )} +
+
+
+
+ )}
@@ -103,7 +218,7 @@ function ConfirmDiffModal({ show, diff, onConfirm, onClose, loading = false }) { {loading && (