feat: Refactor CommunitiesManager component to enhance UI and functionality with modals for adding, editing, and deleting communities. Improved validation and error handling, and updated layout for better user experience.
Publish Fast Tabler Docker image / build-and-push-fast (push) Failing after 57s

This commit is contained in:
2025-11-30 22:36:15 +07:00
parent 2d87ca1632
commit aa882bad20
+375 -170
View File
@@ -1,4 +1,4 @@
import { useEffect, useMemo, useRef, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import api from './lib/api.js';
import {
IconPlus,
@@ -7,25 +7,23 @@ import {
IconTrash,
IconCheck,
IconX,
IconDatabase,
IconRefresh,
IconUpload,
IconDownload,
IconDeviceFloppy,
IconHash,
IconFilter,
IconChartBar,
IconList,
IconArrowUp,
IconArrowDown
IconSortAscending,
IconSortDescending
} from '@tabler/icons-react';
import PageHeaderActions from './components/PageHeaderActions.jsx';
import PageHeader from './components/PageHeader.jsx';
import EmptyState from './components/EmptyState.jsx';
import { TableEmpty } from './components/TableSkeleton.jsx';
import Breadcrumbs from './components/Breadcrumbs.jsx';
import CommunityStats from './components/CommunityStats.jsx';
import SavedFilters from './components/SavedFilters.jsx';
import FormModal from './components/FormModal.jsx';
import ConfirmModal from './components/ConfirmModal.jsx';
import FormField from './components/FormField.jsx';
import ErrorAlert from './components/ErrorAlert.jsx';
import Pagination from './components/Pagination.jsx';
const API_URL = '/api';
@@ -46,7 +44,12 @@ function CommunitiesManager() {
const [newItem, setNewItem] = useState({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '', category: '', priority: 0, enabled: true });
const [editingValue, setEditingValue] = useState(null);
const [editingDraft, setEditingDraft] = useState({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '', category: '', priority: 0, enabled: true });
const editRef = useRef(null);
// Модальные окна
const [addModalOpen, setAddModalOpen] = useState(false);
const [editModalOpen, setEditModalOpen] = useState(false);
const [deleteModalOpen, setDeleteModalOpen] = useState(false);
const [itemToDelete, setItemToDelete] = useState(null);
useEffect(() => { fetchItems(); }, []);
@@ -135,6 +138,7 @@ function CommunitiesManager() {
const value = String(newItem.value || '').trim();
if (!value) { setError('Поле value обязательно.'); return; }
if (items.some(i => i.value === value)) { setError('Такое value уже существует.'); return; }
if (!/^(\d+|\d+:\d+)$/.test(value)) { setError('Формат: число или N:N'); return; }
setError('');
setItems([...items, {
value,
@@ -145,6 +149,9 @@ function CommunitiesManager() {
color: String(newItem.color || ''),
}]);
setNewItem({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '' });
setAddModalOpen(false);
setSuccess('Community успешно добавлен!');
setTimeout(() => setSuccess(''), 3000);
};
const startEdit = (item) => {
@@ -157,11 +164,13 @@ function CommunitiesManager() {
gatewayDefault: item.gatewayDefault || '',
color: item.color || '',
});
setEditModalOpen(true);
};
const saveEdit = () => {
const value = String(editingDraft.value || '').trim();
if (!value) { setError('Поле value обязательно.'); return; }
if (!/^(\d+|\d+:\d+)$/.test(value)) { setError('Формат: число или N:N'); return; }
if (value !== editingValue && items.some(i => i.value === value)) { setError('Такое value уже существует.'); return; }
const next = items.map(i => i.value === editingValue ? {
value,
@@ -173,11 +182,30 @@ function CommunitiesManager() {
} : i);
setItems(next);
setEditingValue(null);
setEditModalOpen(false);
setSuccess('Community успешно обновлён!');
setTimeout(() => setSuccess(''), 3000);
};
const cancelEdit = () => setEditingValue(null);
const cancelEdit = () => {
setEditingValue(null);
setEditModalOpen(false);
};
const deleteItem = (value) => setItems(items.filter(i => i.value !== value));
const handleDeleteClick = (item) => {
setItemToDelete(item);
setDeleteModalOpen(true);
};
const confirmDelete = () => {
if (itemToDelete) {
setItems(items.filter(i => i.value !== itemToDelete.value));
setDeleteModalOpen(false);
setItemToDelete(null);
setSuccess('Community успешно удалён!');
setTimeout(() => setSuccess(''), 3000);
}
};
const handleImport = () => {
const text = window.prompt('Вставьте JSON-массив community (value, name, description, tags, gatewayDefault, color)');
@@ -236,17 +264,24 @@ function CommunitiesManager() {
};
return (
<div>
<PageHeader
title={(<><IconFilter className="me-2" />Справочник Community</>)}
pretitle={(
<div className="page-wrapper">
<div className="page-header d-print-none">
<div className="container-xl">
<div className="row g-2 align-items-center">
<div className="col">
<h2 className="page-title">
<IconFilter className="icon me-2" />
Справочник Community
</h2>
<div className="page-pretitle">
<Breadcrumbs items={[
{ label: 'Главная', to: '/dashboard' },
{ label: 'Данные' },
{ label: 'Community' },
]} />
)}
actions={(
</div>
</div>
<div className="col-auto ms-auto d-print-none">
<PageHeaderActions
loading={loading}
onRefresh={fetchItems}
@@ -257,101 +292,90 @@ function CommunitiesManager() {
onSave={handleSave}
disableSave={loading}
/>
)}
/>
{/* Вкладки: Список / Статистика */}
<div className="card mb-3">
<div className="card-body p-2">
<div className="btn-group w-100" role="group">
<button
type="button"
className={`btn ${activeTab === 'list' ? 'btn-primary' : 'btn-outline-primary'}`}
onClick={() => setActiveTab('list')}
>
<IconList className="me-2" size={18} />
Список communities
</button>
<button
type="button"
className={`btn ${activeTab === 'stats' ? 'btn-primary' : 'btn-outline-primary'}`}
onClick={() => setActiveTab('stats')}
>
<IconChartBar className="me-2" size={18} />
Статистика использования
</button>
</div>
</div>
</div>
</div>
<div className="page-body">
<div className="container-xl">
{/* Уведомления */}
{error && (
<div className="alert alert-danger alert-dismissible" role="alert">
<IconX className="me-2" />{error}
<button type="button" className="btn-close" onClick={() => setError('')}></button>
</div>
<ErrorAlert message={error} onClose={() => setError('')} onRetry={fetchItems} />
)}
{success && (
<div className="alert alert-success alert-dismissible" role="alert">
<IconCheck className="me-2" />{success}
<div className="d-flex">
<div>
<IconCheck className="icon alert-icon" />
</div>
<div>
{success}
</div>
</div>
<button type="button" className="btn-close" onClick={() => setSuccess('')}></button>
</div>
)}
{/* Вкладки: Список / Статистика */}
<div className="card mb-3">
<div className="card-body p-2">
<ul className="nav nav-tabs nav-tabs-bg" role="tablist">
<li className="nav-item" role="presentation">
<button
className={`nav-link ${activeTab === 'list' ? 'active' : ''}`}
onClick={() => setActiveTab('list')}
type="button"
role="tab"
aria-selected={activeTab === 'list'}
>
<IconList className="me-2" size={18} />
Список communities
</button>
</li>
<li className="nav-item" role="presentation">
<button
className={`nav-link ${activeTab === 'stats' ? 'active' : ''}`}
onClick={() => setActiveTab('stats')}
type="button"
role="tab"
aria-selected={activeTab === 'stats'}
>
<IconChartBar className="me-2" size={18} />
Статистика использования
</button>
</li>
</ul>
</div>
</div>
{/* Отображение списка communities */}
{activeTab === 'list' && (
<div className="row g-3">
<div className="col-lg-3">
<div className="card card-md animate-in">
<div className="card-header bg-primary-lt"><h3 className="card-title text-primary"><IconPlus className="me-2" />Добавить</h3></div>
<div className="card-body">
<div className="mb-2">
<label className="form-label required">Value</label>
<div className="input-icon">
<span className="input-icon-addon"><IconHash size={18} /></span>
<input className={`form-control${newItem.value && !/^(\d+|\d+:\d+)$/.test(String(newItem.value).trim()) ? ' is-invalid' : (newItem.value ? ' is-valid' : '')}`} value={newItem.value} onChange={(e)=>setNewItem({...newItem, value:e.target.value})} placeholder="65001:200 или 100" />
</div>
{newItem.value && !/^(\d+|\d+:\d+)$/.test(String(newItem.value).trim()) && (<div className="invalid-feedback">Формат: число или N:N</div>)}
{newItem.value && /^(\d+|\d+:\d+)$/.test(String(newItem.value).trim()) && (<div className="valid-feedback">Корректно</div>)}
</div>
<div className="mb-2">
<label className="form-label">Название</label>
<div className="input-icon">
<span className="input-icon-addon"><IconFilter size={18} /></span>
<input className="form-control" value={newItem.name} onChange={(e)=>setNewItem({...newItem, name:e.target.value})} placeholder="Напр. Social" />
</div>
</div>
<div className="mb-2">
<label className="form-label">Описание</label>
<textarea className="form-control" rows={2} value={newItem.description} onChange={(e)=>setNewItem({...newItem, description:e.target.value})} />
</div>
<div className="mb-2">
<label className="form-label">Теги (через запятую)</label>
<input className="form-control" value={newItem.tags} onChange={(e)=>setNewItem({...newItem, tags:e.target.value})} placeholder="video,bank" />
</div>
<div className="mb-2">
<label className="form-label">Gateway по умолчанию</label>
<input className="form-control" value={newItem.gatewayDefault} onChange={(e)=>setNewItem({...newItem, gatewayDefault:e.target.value})} placeholder="SWE-HIPHOST" />
</div>
<div className="mb-3">
<label className="form-label">Цвет бейджа (опц.)</label>
<input className="form-control" value={newItem.color} onChange={(e)=>setNewItem({...newItem, color:e.target.value})} placeholder="blue, green, red ..." />
</div>
<button className="btn btn-primary w-100" onClick={addItem} disabled={!/^(\d+|\d+:\d+)$/.test(String(newItem.value || '').trim())}><IconPlus className="me-2" />Добавить</button>
</div>
</div>
{/* Карточка действий не нужна — действия перенесены в page-header */}
</div>
<div className="col-lg-9">
<div className="card animate-in" style={{animationDelay: '0.2s'}}>
<div className="col-12">
<div className="card">
<div className="card-header">
<div className="row align-items-center w-100 g-2">
<div className="col-auto">
<h3 className="card-title mb-0"><IconFilter className="icon me-2" size={20} />Список community <span className="badge bg-blue-lt text-blue ms-2">{filtered.length}</span></h3>
</div>
<div className="col">
<div className="d-flex gap-2 flex-wrap">
<h3 className="card-title mb-0">
<IconFilter className="icon me-2" size={20} />
Список community
<span className="badge bg-blue-lt text-blue ms-2">{filtered.length}</span>
</h3>
</div>
<div className="col-auto">
<button
className="btn btn-primary"
onClick={() => setAddModalOpen(true)}
>
<IconPlus className="icon me-1" />
Добавить community
</button>
</div>
</div>
</div>
<div className="card-body">
<div className="d-flex gap-2 flex-wrap mb-3">
<SavedFilters
pageKey="communities"
currentFilters={{ searchTerm, sortField, sortOrder }}
@@ -364,17 +388,26 @@ function CommunitiesManager() {
/>
<div className="input-icon flex-grow-1" style={{ minWidth: '200px', position: 'relative' }}>
<span className="input-icon-addon"><IconSearch size={18} /></span>
<input className="form-control" placeholder="Поиск по value, названию, описанию и тегам" value={searchTerm} onChange={(e)=>{setSearchTerm(e.target.value); setCurrentPage(1);}} style={{ paddingRight: searchTerm ? '2.5rem' : undefined }} />
<input
className="form-control"
placeholder="Поиск по value, названию, описанию и тегам"
value={searchTerm}
onChange={(e)=>{setSearchTerm(e.target.value); setCurrentPage(1);}}
style={{ paddingRight: searchTerm ? '2.5rem' : undefined }}
/>
{!!searchTerm && (
<button type="button" className="btn btn-ghost-secondary btn-icon btn-sm" style={{ position: 'absolute', right: '0.25rem', top: '50%', transform: 'translateY(-50%)', zIndex: 2 }} aria-label="Очистить поиск" onClick={()=>{ setSearchTerm(''); setCurrentPage(1); }}>
<button
type="button"
className="btn btn-ghost-secondary btn-icon btn-sm"
style={{ position: 'absolute', right: '0.25rem', top: '50%', transform: 'translateY(-50%)', zIndex: 2 }}
aria-label="Очистить поиск"
onClick={()=>{ setSearchTerm(''); setCurrentPage(1); }}
>
<IconX size={16} />
</button>
)}
</div>
</div>
</div>
</div>
</div>
<div className="table-responsive">
<table className="table card-table table-vcenter table-nowrap mb-0">
<thead>
@@ -383,14 +416,22 @@ function CommunitiesManager() {
<div className="d-flex align-items-center">
<IconHash size={16} className="me-1 text-muted" />
Value
{sortField==='value' && <span className="ms-1">{sortOrder==='asc'? <IconArrowUp size={14} /> : <IconArrowDown size={14} />}</span>}
{sortField==='value' && (
<span className="ms-1">
{sortOrder==='asc' ? <IconSortAscending size={14} /> : <IconSortDescending size={14} />}
</span>
)}
</div>
</th>
<th className="cursor-pointer user-select-none" onClick={()=>changeSort('name')}>
<div className="d-flex align-items-center">
<IconFilter size={16} className="me-1 text-muted" />
Название
{sortField==='name' && <span className="ms-1">{sortOrder==='asc'? <IconArrowUp size={14} /> : <IconArrowDown size={14} />}</span>}
{sortField==='name' && (
<span className="ms-1">
{sortOrder==='asc' ? <IconSortAscending size={14} /> : <IconSortDescending size={14} />}
</span>
)}
</div>
</th>
<th>Описание</th>
@@ -404,96 +445,69 @@ function CommunitiesManager() {
<tr><td colSpan={6} className="text-center text-muted py-4">Загрузка...</td></tr>
) : paginated.length === 0 ? (
<tr><td colSpan={6} className="text-center text-muted py-4">
<EmptyState title="Нет community" description="Добавьте запись, чтобы начать." action={<button className="btn btn-primary" onClick={addItem}>Добавить</button>} />
<EmptyState
title="Нет community"
description="Добавьте запись, чтобы начать."
action={
<button className="btn btn-primary" onClick={() => setAddModalOpen(true)}>
<IconPlus className="me-2" />
Добавить
</button>
}
/>
</td></tr>
) : paginated.map((item, idx) => (
<tr key={item.value} className={editingValue === item.value ? 'table-active' : ''} style={{ animation: `fadeIn 0.3s ease ${idx * 0.02}s both` }}>
) : paginated.map((item) => (
<tr key={item.value}>
<td>
{editingValue === item.value ? (
<input className="form-control form-control-sm" value={editingDraft.value} onChange={(e)=>setEditingDraft({...editingDraft, value:e.target.value})} ref={editRef} />
) : (
<>
<code className="text-blue">{item.value}</code>
{item._external && (
<span className="badge bg-yellow-lt text-yellow ms-2" title="Не внесён в справочник">не в справ.</span>
)}
</>
)}
</td>
<td>{item.name || <span className="text-muted">—</span>}</td>
<td>
{editingValue === item.value ? (
<input className="form-control form-control-sm" value={editingDraft.name} onChange={(e)=>setEditingDraft({...editingDraft, name:e.target.value})} />
) : item.name || <span className="text-muted">—</span>}
</td>
<td>
{editingValue === item.value ? (
<input className="form-control form-control-sm" value={editingDraft.description} onChange={(e)=>setEditingDraft({...editingDraft, description:e.target.value})} />
) : (
<span className="text-muted" title={item.description}>{item.description || '—'}</span>
)}
</td>
<td>
{editingValue === item.value ? (
<input className="form-control form-control-sm" value={editingDraft.tags} onChange={(e)=>setEditingDraft({...editingDraft, tags:e.target.value})} />
) : (
(item.tags || []).map(t => <span key={t} className="badge bg-blue-lt text-blue me-1">{t}</span>)
)}
{(item.tags || []).map(t => (
<span key={t} className="badge bg-blue-lt text-blue me-1">{t}</span>
))}
</td>
<td>
{editingValue === item.value ? (
<input className="form-control form-control-sm" value={editingDraft.gatewayDefault} onChange={(e)=>setEditingDraft({...editingDraft, gatewayDefault:e.target.value})} />
) : (
<span className="text-muted">{item.gatewayDefault || '—'}</span>
)}
</td>
<td className="text-end">
{editingValue === item.value ? (
<>
<button className="btn btn-success btn-icon btn-sm me-1" onClick={saveEdit}><IconCheck size={16} /></button>
<button className="btn btn-secondary btn-icon btn-sm" onClick={cancelEdit}><IconX size={16} /></button>
</>
) : (
<>
<button className="btn btn-outline-primary btn-icon btn-sm me-1" onClick={()=>startEdit(item)}><IconEdit size={16} /></button>
<button className="btn btn-outline-danger btn-icon btn-sm" onClick={()=>deleteItem(item.value)}><IconTrash size={16} /></button>
</>
)}
<div className="btn-list">
<button
className="btn btn-outline-primary btn-icon btn-sm"
onClick={()=>startEdit(item)}
title="Редактировать"
>
<IconEdit size={16} />
</button>
<button
className="btn btn-outline-danger btn-icon btn-sm"
onClick={()=>handleDeleteClick(item)}
title="Удалить"
>
<IconTrash size={16} />
</button>
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
{totalPages > 1 && (
<div className="card-footer">
<div className="d-flex align-items-center justify-content-between flex-wrap gap-2">
<div className="text-muted small">Показано <strong>{((page - 1) * pageSize) + 1}</strong> - <strong>{Math.min(page * pageSize, filtered.length)}</strong> из <strong>{filtered.length}</strong></div>
<ul className="pagination m-0">
<li className={`page-item${page === 1 ? ' disabled' : ''}`}>
<button className="page-link" onClick={()=>setCurrentPage(1)} disabled={page===1} aria-label="Первая страница">
<IconArrowUp size={14} style={{ transform: 'rotate(-90deg)' }} />
</button>
</li>
<li className={`page-item${page === 1 ? ' disabled' : ''}`}>
<button className="page-link" onClick={()=>setCurrentPage(page-1)} disabled={page===1} aria-label="Предыдущая страница">
<IconArrowUp size={14} style={{ transform: 'rotate(-90deg)' }} />
<span className="d-none d-sm-inline ms-1">Назад</span>
</button>
</li>
<li className={`page-item${page === totalPages ? ' disabled' : ''}`}>
<button className="page-link" onClick={()=>setCurrentPage(page+1)} disabled={page===totalPages} aria-label="Следующая страница">
<span className="d-none d-sm-inline me-1">Вперед</span>
<IconArrowDown size={14} style={{ transform: 'rotate(-90deg)' }} />
</button>
</li>
<li className={`page-item${page === totalPages ? ' disabled' : ''}`}>
<button className="page-link" onClick={()=>setCurrentPage(totalPages)} disabled={page===totalPages} aria-label="Последняя страница">
<IconArrowDown size={14} style={{ transform: 'rotate(-90deg)' }} />
</button>
</li>
</ul>
</div>
</div>
<Pagination
currentPage={page}
totalPages={totalPages}
totalItems={filtered.length}
pageSize={pageSize}
onPageChange={setCurrentPage}
/>
)}
</div>
</div>
@@ -505,6 +519,197 @@ function CommunitiesManager() {
<CommunityStats communities={items} />
)}
</div>
</div>
{/* Модальное окно добавления */}
<FormModal
show={addModalOpen}
onClose={() => {
setAddModalOpen(false);
setNewItem({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '' });
setError('');
}}
onSubmit={addItem}
title="Добавить community"
submitLabel="Добавить"
submitIcon={IconPlus}
size="md"
>
{error && (
<div className="alert alert-danger mb-3" role="alert">
{error}
</div>
)}
<FormField
label="Value"
name="value"
value={newItem.value}
onChange={(val) => setNewItem({...newItem, value: val})}
placeholder="65001:200 или 100"
required
icon={IconHash}
helpText="Формат: число или N:N"
onValidate={(val) => {
const trimmed = String(val || '').trim();
if (!trimmed) return { valid: false, message: 'Поле обязательно' };
if (!/^(\d+|\d+:\d+)$/.test(trimmed)) return { valid: false, message: 'Формат: число или N:N' };
if (items.some(i => i.value === trimmed)) return { valid: false, message: 'Такое value уже существует' };
return { valid: true, message: 'Корректно' };
}}
/>
<FormField
label="Название"
name="name"
value={newItem.name}
onChange={(val) => setNewItem({...newItem, name: val})}
placeholder="Напр. Social"
icon={IconFilter}
/>
<FormField
label="Описание"
name="description"
type="textarea"
value={newItem.description}
onChange={(val) => setNewItem({...newItem, description: val})}
rows={2}
/>
<FormField
label="Теги (через запятую)"
name="tags"
value={newItem.tags}
onChange={(val) => setNewItem({...newItem, tags: val})}
placeholder="video,bank"
helpText="Разделяйте теги запятыми"
/>
<FormField
label="Gateway по умолчанию"
name="gatewayDefault"
value={newItem.gatewayDefault}
onChange={(val) => setNewItem({...newItem, gatewayDefault: val})}
placeholder="SWE-HIPHOST"
/>
<FormField
label="Цвет бейджа (опционально)"
name="color"
value={newItem.color}
onChange={(val) => setNewItem({...newItem, color: val})}
placeholder="blue, green, red ..."
helpText="Цвет для отображения бейджа в интерфейсе"
/>
</FormModal>
{/* Модальное окно редактирования */}
<FormModal
show={editModalOpen}
onClose={cancelEdit}
onSubmit={saveEdit}
title="Редактировать community"
submitLabel="Сохранить"
submitIcon={IconCheck}
size="md"
>
{error && (
<div className="alert alert-danger mb-3" role="alert">
{error}
</div>
)}
<FormField
label="Value"
name="value"
value={editingDraft.value}
onChange={(val) => setEditingDraft({...editingDraft, value: val})}
placeholder="65001:200 или 100"
required
icon={IconHash}
helpText="Формат: число или N:N"
onValidate={(val) => {
const trimmed = String(val || '').trim();
if (!trimmed) return { valid: false, message: 'Поле обязательно' };
if (!/^(\d+|\d+:\d+)$/.test(trimmed)) return { valid: false, message: 'Формат: число или N:N' };
if (trimmed !== editingValue && items.some(i => i.value === trimmed)) {
return { valid: false, message: 'Такое value уже существует' };
}
return { valid: true, message: 'Корректно' };
}}
/>
<FormField
label="Название"
name="name"
value={editingDraft.name}
onChange={(val) => setEditingDraft({...editingDraft, name: val})}
placeholder="Напр. Social"
icon={IconFilter}
/>
<FormField
label="Описание"
name="description"
type="textarea"
value={editingDraft.description}
onChange={(val) => setEditingDraft({...editingDraft, description: val})}
rows={2}
/>
<FormField
label="Теги (через запятую)"
name="tags"
value={editingDraft.tags}
onChange={(val) => setEditingDraft({...editingDraft, tags: val})}
placeholder="video,bank"
helpText="Разделяйте теги запятыми"
/>
<FormField
label="Gateway по умолчанию"
name="gatewayDefault"
value={editingDraft.gatewayDefault}
onChange={(val) => setEditingDraft({...editingDraft, gatewayDefault: val})}
placeholder="SWE-HIPHOST"
/>
<FormField
label="Цвет бейджа (опционально)"
name="color"
value={editingDraft.color}
onChange={(val) => setEditingDraft({...editingDraft, color: val})}
placeholder="blue, green, red ..."
helpText="Цвет для отображения бейджа в интерфейсе"
/>
</FormModal>
{/* Модальное окно подтверждения удаления */}
<ConfirmModal
show={deleteModalOpen}
onClose={() => {
setDeleteModalOpen(false);
setItemToDelete(null);
}}
onConfirm={confirmDelete}
title="Удалить community?"
message={
itemToDelete ? (
<>
<p>Вы уверены, что хотите удалить community <strong><code>{itemToDelete.value}</code></strong>?</p>
{itemToDelete.name && (
<p className="text-muted mb-0">Название: {itemToDelete.name}</p>
)}
<p className="text-muted mb-0 mt-2">Это действие необратимо.</p>
</>
) : null
}
confirmLabel="Удалить"
variant="danger"
icon={IconTrash}
/>
</div>
);
}