feat(communities): add icon support to community model and UI components for enhanced visual representation
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m58s

This commit is contained in:
2026-02-24 12:03:07 +07:00
parent 02a45fce71
commit 5e6435e2e1
7 changed files with 80 additions and 29 deletions
+39 -17
View File
@@ -11,7 +11,6 @@ import {
IconFilter,
IconChartBar,
IconList,
IconLink,
IconRefresh,
IconSortAscending,
IconSortDescending,
@@ -29,6 +28,8 @@ import ConfirmModal from './components/ConfirmModal.jsx';
import FormField from './components/FormField.jsx';
import ErrorAlert from './components/ErrorAlert.jsx';
import Pagination from './components/Pagination.jsx';
import IconPicker from './components/IconPicker.jsx';
import { getIconById } from './lib/brandIcons.js';
const API_URL = '/api';
@@ -53,9 +54,9 @@ function CommunitiesManager() {
// Состояние для переключения между списком и статистикой
const [activeTab, setActiveTab] = useState('list'); // 'list' | 'stats'
const [newItem, setNewItem] = useState({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '', category: '', priority: 0, enabled: true });
const [newItem, setNewItem] = useState({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '', icon: '', 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 [editingDraft, setEditingDraft] = useState({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '', icon: '', category: '', priority: 0, enabled: true });
const [filterUsage, setFilterUsage] = useState({});
const [filterUsageLoading, setFilterUsageLoading] = useState(false);
const [filterUsageError, setFilterUsageError] = useState('');
@@ -79,7 +80,7 @@ function CommunitiesManager() {
Object.keys(filterUsage).forEach((value) => {
if (!value) return;
if (!map.has(value)) {
map.set(value, { value, name: '', description: '', tags: [], gatewayDefault: '', color: '', _external: true });
map.set(value, { value, name: '', description: '', tags: [], gatewayDefault: '', color: '', icon: '', _external: true });
}
});
return Array.from(map.values());
@@ -130,13 +131,14 @@ function CommunitiesManager() {
tags: Array.isArray(d.tags) ? d.tags : [],
gatewayDefault: String(d.gatewayDefault || ''),
color: String(d.color || ''),
icon: String(d.icon || ''),
_external: false,
});
}
// add unknown ones from usage
for (const v of usedCommunities) {
if (!map.has(v)) {
map.set(v, { value: v, name: '', description: '', tags: [], gatewayDefault: '', color: '', _external: true });
map.set(v, { value: v, name: '', description: '', tags: [], gatewayDefault: '', color: '', icon: '', _external: true });
}
}
setItems(Array.from(map.values()));
@@ -217,8 +219,9 @@ function CommunitiesManager() {
tags: toTagsArray(newItem.tags),
gatewayDefault: String(newItem.gatewayDefault || ''),
color: String(newItem.color || ''),
icon: String(newItem.icon || ''),
}]);
setNewItem({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '' });
setNewItem({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '', icon: '' });
setAddModalOpen(false);
setSuccess('Community успешно добавлен!');
setTimeout(() => setSuccess(''), 3000);
@@ -233,6 +236,7 @@ function CommunitiesManager() {
tags: fromTagsArray(item.tags),
gatewayDefault: item.gatewayDefault || '',
color: item.color || '',
icon: item.icon || '',
});
setEditModalOpen(true);
};
@@ -249,6 +253,7 @@ function CommunitiesManager() {
tags: toTagsArray(editingDraft.tags),
gatewayDefault: String(editingDraft.gatewayDefault || ''),
color: String(editingDraft.color || ''),
icon: String(editingDraft.icon || ''),
} : i);
setItems(next);
setEditingValue(null);
@@ -278,7 +283,7 @@ function CommunitiesManager() {
};
const handleImport = () => {
const text = window.prompt('Вставьте JSON-массив community (value, name, description, tags, gatewayDefault, color)');
const text = window.prompt('Вставьте JSON-массив community (value, name, description, tags, gatewayDefault, color, icon)');
if (!text) return;
try {
const arr = JSON.parse(text);
@@ -297,6 +302,7 @@ function CommunitiesManager() {
tags: Array.isArray(x.tags) ? x.tags : [],
gatewayDefault: String(x.gatewayDefault || ''),
color: String(x.color || ''),
icon: String(x.icon || ''),
});
});
return Array.from(map.values());
@@ -553,6 +559,7 @@ function CommunitiesManager() {
<table className="table card-table table-vcenter table-nowrap mb-0">
<thead>
<tr>
<th style={{ width: '8%' }}>Иконка</th>
<th className="cursor-pointer user-select-none" onClick={()=>changeSort('value')}>
<div className="d-flex align-items-center">
<IconHash size={16} className="me-1 text-muted" />
@@ -584,9 +591,9 @@ function CommunitiesManager() {
</thead>
<tbody>
{loading ? (
<tr><td colSpan={7} className="text-center text-muted py-4">Загрузка...</td></tr>
<tr><td colSpan={8} className="text-center text-muted py-4">Загрузка...</td></tr>
) : paginated.length === 0 ? (
<tr><td colSpan={7} className="text-center text-muted py-4">
<tr><td colSpan={8} className="text-center text-muted py-4">
<EmptyState
title="Нет community"
description="Добавьте запись, чтобы начать."
@@ -600,8 +607,18 @@ function CommunitiesManager() {
</td></tr>
) : paginated.map((item) => {
const usageCount = filterUsage[item.value] || 0;
const iconInfo = item.icon ? getIconById(item.icon) : null;
return (
<tr key={item.value}>
<td>
{iconInfo ? (
<span className="avatar avatar-xs bg-blue-lt text-blue border">
<iconInfo.Icon size={14} stroke={1.5} />
</span>
) : (
<span className="text-muted"></span>
)}
</td>
<td>
<code className="text-blue">{item.value}</code>
{item._external && (
@@ -629,13 +646,6 @@ function CommunitiesManager() {
</td>
<td className="text-end">
<div className="btn-list">
<a
className="btn btn-outline-primary btn-icon btn-sm"
href={`/filters?community=${encodeURIComponent(item.value)}`}
title="Открыть в /filters с поиском по community"
>
<IconLink size={16} />
</a>
<button
className="btn btn-outline-primary btn-icon btn-sm"
onClick={()=>startEdit(item)}
@@ -681,7 +691,7 @@ function CommunitiesManager() {
show={addModalOpen}
onClose={() => {
setAddModalOpen(false);
setNewItem({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '' });
setNewItem({ value: '', name: '', description: '', tags: '', gatewayDefault: '', color: '', icon: '' });
setError('');
}}
onSubmit={addItem}
@@ -758,6 +768,12 @@ function CommunitiesManager() {
placeholder="blue, green, red ..."
helpText="Цвет для отображения бейджа в интерфейсе"
/>
<IconPicker
value={newItem.icon}
onChange={(id) => setNewItem({...newItem, icon: id})}
label="Иконка бренда (опционально)"
allowNone
/>
</FormModal>
{/* Модальное окно редактирования */}
@@ -840,6 +856,12 @@ function CommunitiesManager() {
placeholder="blue, green, red ..."
helpText="Цвет для отображения бейджа в интерфейсе"
/>
<IconPicker
value={editingDraft.icon}
onChange={(id) => setEditingDraft({...editingDraft, icon: id})}
label="Иконка бренда (опционально)"
allowNone
/>
</FormModal>
{/* Модальное окно подтверждения удаления */}