fix: Обновление фильтрации и валидации в компонентах ASNsNewManager, AutoUrlManager, DomainsNewManager, IPRangesManager и FilterManager. Добавлена проверка на null для предотвращения ошибок и улучшения стабильности приложения.
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
Publish Fast Tabler Docker image / build-and-push-fast (push) Successful in 1m40s
This commit is contained in:
@@ -365,9 +365,10 @@ function ASNsNewManager() {
|
||||
|
||||
// Локальная фильтрация и пагинация
|
||||
const filtered = sortedItems.filter(i => {
|
||||
if (!i) return false;
|
||||
const byCommunity = !filterCommunity || i.community === filterCommunity;
|
||||
const term = String(searchTerm || '').toLowerCase();
|
||||
const bySearch = !term || String(i.asn).toLowerCase().includes(term);
|
||||
const bySearch = !term || String(i.asn || '').toLowerCase().includes(term);
|
||||
return byCommunity && bySearch;
|
||||
});
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
||||
@@ -375,7 +376,7 @@ function ASNsNewManager() {
|
||||
|
||||
// Подгружаем имена ASN из кэша/внешних API (после вычисления paginatedItems)
|
||||
useEffect(() => {
|
||||
const visible = paginatedItems.map(i => i.asn);
|
||||
const visible = paginatedItems.filter(i => i != null).map(i => i.asn).filter(asn => asn != null);
|
||||
visible.forEach(async (asn) => {
|
||||
const cached = getAsnNameSync(asn);
|
||||
if (cached !== null && typeof cached !== 'undefined') {
|
||||
@@ -388,7 +389,7 @@ function ASNsNewManager() {
|
||||
}, [JSON.stringify(paginatedItems)]);
|
||||
|
||||
// Для фильтра - список всех уникальных community
|
||||
const allCommunities = Array.from(new Set(items.map(i => String(i.community))));
|
||||
const allCommunities = Array.from(new Set(items.filter(i => i != null).map(i => String(i.community || '')).filter(c => c !== '')));
|
||||
|
||||
// Сортировка по клику
|
||||
const handleSort = (field) => {
|
||||
|
||||
@@ -83,7 +83,7 @@ function AutoUrlManager() {
|
||||
|
||||
// Validate URLs
|
||||
const validUrls = urls
|
||||
.filter(u => isValidHttpUrl(u.url) && isValidCommunity(u.community))
|
||||
.filter(u => u != null && isValidHttpUrl(u.url) && isValidCommunity(u.community))
|
||||
.map(u => ({ url: u.url.trim(), community: String(u.community).trim() }));
|
||||
if (validUrls.length === 0) {
|
||||
setMessage('Добавьте хотя бы одну корректную запись (валидный URL и числовой community)');
|
||||
@@ -302,7 +302,7 @@ function AutoUrlManager() {
|
||||
<div>
|
||||
<button
|
||||
className="btn btn-outline-secondary me-2"
|
||||
onClick={() => setUrls(urls.filter(u => u.url || u.community))}
|
||||
onClick={() => setUrls(urls.filter(u => u != null && (u.url || u.community)))}
|
||||
disabled={saving || processing || urls.length === 0}
|
||||
title="Удалить пустые строки"
|
||||
>
|
||||
|
||||
@@ -218,7 +218,7 @@ function DomainsNewManager() {
|
||||
const [clearCommunitiesOpen, setClearCommunitiesOpen] = useState(false);
|
||||
|
||||
const handlePreviewDiff = () => {
|
||||
const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community))
|
||||
const valid = items.filter(i => i != null && 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));
|
||||
@@ -227,7 +227,7 @@ function DomainsNewManager() {
|
||||
|
||||
const handleSaveChanges = async () => {
|
||||
// подготовим diff и спросим подтверждение только при массовых изменениях (>10)
|
||||
const valid = items.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community))
|
||||
const valid = items.filter(i => i != null && isValidDomain(i.domain) && isValidCommunity(i.community))
|
||||
.map(i => ({ domain: i.domain.trim().toLowerCase(), community: String(i.community).trim() }));
|
||||
const unique = deduplicate(valid);
|
||||
const diffData = computeDiff(originalItems, unique);
|
||||
@@ -256,7 +256,7 @@ function DomainsNewManager() {
|
||||
// Подгружаем весь список, применяем изменения текущей страницы и сохраняем полные данные
|
||||
const fullRes = await api.get(`/domains-new`, { params: { offset: 0, limit: 0, format: 'std' } });
|
||||
const full = Array.isArray(fullRes.data?.items) ? fullRes.data.items : (Array.isArray(fullRes.data) ? fullRes.data : []);
|
||||
const fullMap = new Map(full.map(i => [String(i.domain).trim().toLowerCase(), { domain: String(i.domain).trim().toLowerCase(), community: String(i.community || '').trim() }]));
|
||||
const fullMap = new Map(full.filter(i => i != null).map(i => [String(i.domain || '').trim().toLowerCase(), { domain: String(i.domain || '').trim().toLowerCase(), community: String(i.community || '').trim() }]));
|
||||
const originalPageMap = new Map(originalItems.map(i => [String(i.domain).trim().toLowerCase(), true]));
|
||||
const uniqueMap = new Map(unique.map(i => [String(i.domain).trim().toLowerCase(), { domain: String(i.domain).trim().toLowerCase(), community: String(i.community).trim() }]));
|
||||
// Удаления: всё, что было на странице, но отсутствует в изменённом наборе
|
||||
@@ -300,7 +300,7 @@ function DomainsNewManager() {
|
||||
const parsed = lines.map(l => {
|
||||
const [d, c] = l.split(/\s+/);
|
||||
return { domain: (d || '').toLowerCase(), community: (c || '').trim() };
|
||||
}).filter(i => isValidDomain(i.domain) && isValidCommunity(i.community));
|
||||
}).filter(i => i != null && isValidDomain(i.domain) && isValidCommunity(i.community));
|
||||
setItems(prev => {
|
||||
const merged = [...prev, ...parsed];
|
||||
const map = new Map();
|
||||
@@ -322,7 +322,7 @@ function DomainsNewManager() {
|
||||
const response = await api.get(`/domains-new`, { params: { q: effectiveQ, offset: 0, limit: 0, format: 'std' } });
|
||||
const all = Array.isArray(response.data?.items) ? response.data.items : (Array.isArray(response.data) ? response.data : []);
|
||||
const header = ['domain', 'community'];
|
||||
const csv = [header, ...all.map(i => [i.domain, i.community])]
|
||||
const csv = [header, ...all.filter(i => i != null).map(i => [i.domain || '', i.community || ''])]
|
||||
.map(r => r.map(x => `"${(x ?? '').toString().replace(/"/g, '""')}"`).join(','))
|
||||
.join('\n');
|
||||
const blob = new Blob([csv], { type: 'text/csv;charset=utf-8;' });
|
||||
@@ -338,7 +338,7 @@ function DomainsNewManager() {
|
||||
};
|
||||
|
||||
const clearInvalid = () => {
|
||||
setItems(prev => prev.filter(i => i.domain || i.community)
|
||||
setItems(prev => prev.filter(i => i != null && (i.domain || i.community))
|
||||
.filter(i => isValidDomain(i.domain) && isValidCommunity(i.community))
|
||||
);
|
||||
};
|
||||
@@ -376,16 +376,17 @@ function DomainsNewManager() {
|
||||
|
||||
// Локальная фильтрация и пагинация
|
||||
const filtered = sortedItems.filter(i => {
|
||||
if (!i) return false;
|
||||
const byCommunity = !filterCommunity || i.community === filterCommunity;
|
||||
const term = String(searchTerm || '').toLowerCase();
|
||||
const bySearch = !term || String(i.domain).toLowerCase().includes(term);
|
||||
const bySearch = !term || String(i.domain || '').toLowerCase().includes(term);
|
||||
return byCommunity && bySearch;
|
||||
});
|
||||
const totalPages = Math.max(1, Math.ceil(filtered.length / pageSize));
|
||||
const paginatedItems = filtered.slice((currentPage - 1) * pageSize, (currentPage) * pageSize);
|
||||
|
||||
// Для фильтра - список всех уникальных community
|
||||
const allCommunities = Array.from(new Set(items.map(i => i.community)));
|
||||
const allCommunities = Array.from(new Set(items.filter(i => i != null).map(i => i.community).filter(c => c != null)));
|
||||
|
||||
// Сортировка по клику
|
||||
const handleSort = (field) => {
|
||||
@@ -450,8 +451,8 @@ function DomainsNewManager() {
|
||||
|
||||
const handleBulkExport = () => {
|
||||
if (selectedDomains.size === 0) return;
|
||||
const selectedItems = items.filter(i => selectedDomains.has(i.domain));
|
||||
const text = selectedItems.map(i => `${i.domain} ${i.community}`).join('\n');
|
||||
const selectedItems = items.filter(i => i != null && selectedDomains.has(i.domain));
|
||||
const text = selectedItems.map(i => `${i.domain || ''} ${i.community || ''}`).join('\n');
|
||||
const blob = new Blob([text], { type: 'text/plain' });
|
||||
const url = URL.createObjectURL(blob);
|
||||
const a = document.createElement('a');
|
||||
@@ -895,7 +896,7 @@ function DomainsNewManager() {
|
||||
show={clearCommunitiesOpen}
|
||||
onClose={() => setClearCommunitiesOpen(false)}
|
||||
onConfirm={() => {
|
||||
setItems(prev => prev.filter(i => i.community !== filterCommunity));
|
||||
setItems(prev => prev.filter(i => i != null && i.community !== filterCommunity));
|
||||
setClearCommunitiesOpen(false);
|
||||
}}
|
||||
title="Удалить записи community?"
|
||||
|
||||
@@ -97,7 +97,10 @@ function FilterManager() {
|
||||
})();
|
||||
}, []);
|
||||
|
||||
const filterKey = (f) => `${String(f.community || '')}||${String(f.gateway || '')}`;
|
||||
const filterKey = (f) => {
|
||||
if (!f) return '';
|
||||
return `${String(f.community || '')}||${String(f.gateway || '')}`;
|
||||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchServers();
|
||||
@@ -460,8 +463,13 @@ function FilterManager() {
|
||||
}
|
||||
};
|
||||
|
||||
const handleDeleteFilter = async (filter) => {
|
||||
const updatedFilters = serverFilters.filter(f => f.community !== filter.community);
|
||||
const handleDeleteFilter = async () => {
|
||||
if (!filterToDelete || !selectedServer) return;
|
||||
|
||||
const updatedFilters = serverFilters.filter(f => {
|
||||
if (!f || !filterToDelete) return true;
|
||||
return f.community !== filterToDelete.community || f.gateway !== filterToDelete.gateway;
|
||||
});
|
||||
setServerFilters(updatedFilters);
|
||||
setDeleteFilterModalOpen(false);
|
||||
setFilterToDelete(null);
|
||||
@@ -617,13 +625,15 @@ function FilterManager() {
|
||||
const search = filterSearch.trim().toLowerCase();
|
||||
const filtered = search
|
||||
? serverFilters.filter((f) => {
|
||||
if (!f) return false;
|
||||
const c = String(f.community || '').toLowerCase();
|
||||
const g = String(f.gateway || '').toLowerCase();
|
||||
const d = String(f.description || '').toLowerCase();
|
||||
return c.includes(search) || g.includes(search) || d.includes(search);
|
||||
})
|
||||
: serverFilters.slice();
|
||||
: serverFilters.filter(f => f != null).slice();
|
||||
const sorted = filtered.sort((a, b) => {
|
||||
if (!a || !b) return 0;
|
||||
const av = String(a[sortBy] || '').toLowerCase();
|
||||
const bv = String(b[sortBy] || '').toLowerCase();
|
||||
if (av < bv) return sortDir === 'asc' ? -1 : 1;
|
||||
@@ -645,6 +655,7 @@ function FilterManager() {
|
||||
// Убрано сохранение режима в localStorage по просьбе пользователя
|
||||
|
||||
const startInlineEdit = (filter) => {
|
||||
if (!filter) return;
|
||||
setEditingKey(filterKey(filter));
|
||||
setEditingDraft({
|
||||
community: filter.community || '',
|
||||
@@ -1240,12 +1251,12 @@ function FilterManager() {
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{processedFilters.map((filter, index) => {
|
||||
{processedFilters.filter(f => f != null).map((filter, index) => {
|
||||
const key = filterKey(filter);
|
||||
const isEditing = editingKey === key;
|
||||
const isSelected = selectedFilterKeys.has(key);
|
||||
return (
|
||||
<tr key={`${filter.community}-${index}`} className={isEditing ? 'table-info' : ''}>
|
||||
<tr key={`${filter?.community || ''}-${index}`} className={isEditing ? 'table-info' : ''}>
|
||||
<td>
|
||||
<input
|
||||
className="form-check-input m-0 align-middle"
|
||||
@@ -1268,7 +1279,7 @@ function FilterManager() {
|
||||
<span className="badge bg-blue-lt text-blue me-2">
|
||||
<IconHash size={12} />
|
||||
</span>
|
||||
<code className="text-blue">{filter.community}</code>
|
||||
<code className="text-blue">{filter?.community || ''}</code>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
@@ -1284,7 +1295,7 @@ function FilterManager() {
|
||||
<span className="badge bg-green-lt text-green me-2">
|
||||
<IconServer size={12} />
|
||||
</span>
|
||||
<code className="text-green">{filter.gateway}</code>
|
||||
<code className="text-green">{filter?.gateway || ''}</code>
|
||||
</div>
|
||||
)}
|
||||
</td>
|
||||
@@ -1296,8 +1307,8 @@ function FilterManager() {
|
||||
onChange={(e) => setEditingDraft({ ...editingDraft, description: e.target.value })}
|
||||
/>
|
||||
) : (
|
||||
<div className="text-truncate" style={{ maxWidth: '300px' }} title={filter.description || ''}>
|
||||
{filter.description ? (
|
||||
<div className="text-truncate" style={{ maxWidth: '300px' }} title={filter?.description || ''}>
|
||||
{filter?.description ? (
|
||||
<span className="text-muted">{filter.description}</span>
|
||||
) : (
|
||||
<span className="text-muted fst-italic">Без описания</span>
|
||||
@@ -1712,26 +1723,26 @@ function FilterManager() {
|
||||
) : simpleFilters.length === 0 ? (
|
||||
<tr><td colSpan={4} className="text-center text-muted">Нет фильтров</td></tr>
|
||||
) : (
|
||||
simpleFilters.map((filter, idx) => (
|
||||
simpleFilters.filter(f => f != null).map((filter, idx) => (
|
||||
<tr key={idx}>
|
||||
<td>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
value={filter.community}
|
||||
value={filter?.community || ''}
|
||||
onChange={e => handleSimpleEditFilter(idx, 'community', e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
value={filter.gateway}
|
||||
value={filter?.gateway || ''}
|
||||
onChange={e => handleSimpleEditFilter(idx, 'gateway', e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
<td>
|
||||
<input
|
||||
className="form-control form-control-sm"
|
||||
value={filter.description || ''}
|
||||
value={filter?.description || ''}
|
||||
onChange={e => handleSimpleEditFilter(idx, 'description', e.target.value)}
|
||||
/>
|
||||
</td>
|
||||
@@ -2041,7 +2052,7 @@ function EditFilterModal({ show, filter, onChange, onSave, onClose }) {
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="65001:200"
|
||||
value={filter.community}
|
||||
value={filter?.community || ''}
|
||||
onChange={(e) => handleChange('community', e.target.value)}
|
||||
required
|
||||
/>
|
||||
@@ -2052,7 +2063,7 @@ function EditFilterModal({ show, filter, onChange, onSave, onClose }) {
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="SWE-HIPHOST"
|
||||
value={filter.gateway}
|
||||
value={filter?.gateway || ''}
|
||||
onChange={(e) => handleChange('gateway', e.target.value)}
|
||||
required
|
||||
/>
|
||||
@@ -2063,7 +2074,7 @@ function EditFilterModal({ show, filter, onChange, onSave, onClose }) {
|
||||
type="text"
|
||||
className="form-control"
|
||||
placeholder="Описание фильтра"
|
||||
value={filter.description}
|
||||
value={filter?.description || ''}
|
||||
onChange={(e) => handleChange('description', e.target.value)}
|
||||
/>
|
||||
</div>
|
||||
@@ -2099,9 +2110,9 @@ function DeleteFilterModal({ show, filter, onDelete, onClose }) {
|
||||
<div className="modal-body">
|
||||
<p>Вы уверены, что хотите удалить фильтр?</p>
|
||||
<div className="alert alert-warning">
|
||||
<strong>Community:</strong> {filter.community}<br />
|
||||
<strong>Gateway:</strong> {filter.gateway}<br />
|
||||
{filter.description && <><strong>Описание:</strong> {filter.description}</>}
|
||||
<strong>Community:</strong> {filter?.community || ''}<br />
|
||||
<strong>Gateway:</strong> {filter?.gateway || ''}<br />
|
||||
{filter?.description && <><strong>Описание:</strong> {filter.description}</>}
|
||||
</div>
|
||||
</div>
|
||||
<div className="modal-footer">
|
||||
|
||||
@@ -225,7 +225,7 @@ function IPRangesManager() {
|
||||
const [analyzeFilters, setAnalyzeFilters] = useState({ community: '', minMask: 0, maxMask: 32, type: 'any', supernet16: false });
|
||||
|
||||
const handlePreviewDiff = () => {
|
||||
const valid = items.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community))
|
||||
const valid = items.filter(i => i != null && 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));
|
||||
@@ -234,7 +234,7 @@ function IPRangesManager() {
|
||||
|
||||
const handleSaveChanges = async () => {
|
||||
// подготовим diff и спросим подтверждение только при массовых изменениях (>10)
|
||||
const valid = items.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community))
|
||||
const valid = items.filter(i => i != null && isValidCidr(i.ipRange) && isValidCommunity(i.community))
|
||||
.map(i => ({ ipRange: i.ipRange.trim(), community: String(i.community).trim() }));
|
||||
const unique = deduplicate(valid);
|
||||
const diffData = computeDiff(originalItems, unique);
|
||||
@@ -329,7 +329,7 @@ function IPRangesManager() {
|
||||
const parsed = lines.map(l => {
|
||||
const [cidr, c] = l.split(/\s+/);
|
||||
return { ipRange: (cidr || '').trim(), community: (c || '').trim() };
|
||||
}).filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community));
|
||||
}).filter(i => i != null && isValidCidr(i.ipRange) && isValidCommunity(i.community));
|
||||
setItems(prev => {
|
||||
const merged = [...prev, ...parsed];
|
||||
const map = new Map();
|
||||
@@ -342,7 +342,7 @@ function IPRangesManager() {
|
||||
};
|
||||
|
||||
const clearInvalid = () => {
|
||||
setItems(prev => prev.filter(i => i.ipRange || i.community)
|
||||
setItems(prev => prev.filter(i => i != null && (i.ipRange || i.community))
|
||||
.filter(i => isValidCidr(i.ipRange) && isValidCommunity(i.community))
|
||||
);
|
||||
};
|
||||
@@ -603,7 +603,7 @@ function IPRangesManager() {
|
||||
const community = String(analyzeFilters.community || '').trim();
|
||||
if (!community) { window.notify?.error?.('Выберите community'); return; }
|
||||
// исходные для community
|
||||
const allForCommunity = items.filter(i => i.community === community && isValidCidr(i.ipRange));
|
||||
const allForCommunity = items.filter(i => i != null && i.community === community && isValidCidr(i.ipRange));
|
||||
const totalBefore = allForCommunity.length;
|
||||
|
||||
const filtered = allForCommunity.filter(i => {
|
||||
@@ -641,7 +641,7 @@ function IPRangesManager() {
|
||||
const applyAnalysisToItems = () => {
|
||||
if (!analysisPreview) return;
|
||||
const { community, afterCidrs, unchangedCidrs } = analysisPreview;
|
||||
const keptOthers = items.filter(i => i.community !== community);
|
||||
const keptOthers = items.filter(i => i != null && i.community !== community);
|
||||
const newCommunityItems = [
|
||||
...unchangedCidrs.map(ipRange => ({ ipRange, community })),
|
||||
...afterCidrs.map(ipRange => ({ ipRange, community }))
|
||||
@@ -1054,7 +1054,7 @@ function IPRangesManager() {
|
||||
message={`Будут удалены все IP-диапазоны с community "${filterCommunity}". Действие НЕ сохраняет изменения автоматически.`}
|
||||
confirmText={'Удалить'}
|
||||
cancelText={'Отмена'}
|
||||
onConfirm={() => { setItems(prev => prev.filter(i => i.community !== filterCommunity)); setClearCommunitiesOpen(false); }}
|
||||
onConfirm={() => { setItems(prev => prev.filter(i => i != null && i.community !== filterCommunity)); setClearCommunitiesOpen(false); }}
|
||||
onCancel={() => setClearCommunitiesOpen(false)}
|
||||
/>
|
||||
{/* Analyze Settings Modal */}
|
||||
|
||||
@@ -129,7 +129,7 @@ export function useDataManager(config) {
|
||||
continue;
|
||||
}
|
||||
const prev = mapBefore.get(k);
|
||||
if (String(prev.community) !== String(v.community)) {
|
||||
if (prev && v && String(prev.community || '') !== String(v.community || '')) {
|
||||
changed.push({ from: prev, to: v });
|
||||
}
|
||||
}
|
||||
@@ -147,6 +147,7 @@ export function useDataManager(config) {
|
||||
try {
|
||||
// Валидируем и дедуплицируем
|
||||
const valid = items.filter(item => {
|
||||
if (!item) return false;
|
||||
const itemValid = validateItem ? validateItem(item[itemKey]) : true;
|
||||
const communityValid = validateCommunity ? validateCommunity(item.community) : true;
|
||||
return itemValid && communityValid;
|
||||
@@ -198,7 +199,8 @@ export function useDataManager(config) {
|
||||
};
|
||||
|
||||
// Сортировка
|
||||
const sortedItems = [...items].sort((a, b) => {
|
||||
const sortedItems = [...items].filter(item => item != null).sort((a, b) => {
|
||||
if (!a || !b) return 0;
|
||||
let valA = a[sortField] || '';
|
||||
let valB = b[sortField] || '';
|
||||
if (typeof valA === 'string') valA = valA.toLowerCase();
|
||||
@@ -210,9 +212,10 @@ export function useDataManager(config) {
|
||||
|
||||
// Фильтрация
|
||||
const filtered = sortedItems.filter(item => {
|
||||
if (!item) return false;
|
||||
const byCommunity = !filterCommunity || item.community === filterCommunity;
|
||||
const term = String(searchTerm || '').toLowerCase();
|
||||
const bySearch = !term || String(item[itemKey]).toLowerCase().includes(term);
|
||||
const bySearch = !term || String(item[itemKey] || '').toLowerCase().includes(term);
|
||||
return byCommunity && bySearch;
|
||||
});
|
||||
|
||||
@@ -221,7 +224,7 @@ export function useDataManager(config) {
|
||||
const paginatedItems = filtered.slice((currentPage - 1) * pageSize, currentPage * pageSize);
|
||||
|
||||
// Уникальные community для фильтра
|
||||
const allCommunities = Array.from(new Set(items.map(i => i.community)));
|
||||
const allCommunities = Array.from(new Set(items.filter(i => i != null).map(i => i.community).filter(c => c != null)));
|
||||
|
||||
// Управление сортировкой
|
||||
const handleSort = (field) => {
|
||||
|
||||
Reference in New Issue
Block a user