Files
core/panel/routes/communities.tsx
Untone e13267a868
Some checks failed
Deploy on push / deploy (push) Failing after 2m15s
panel-linter-fix
2025-08-18 20:23:25 +03:00

428 lines
14 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { Component, createEffect, createSignal, For, on, onMount, Show, untrack } from 'solid-js'
import { useTableSort } from '../context/sort'
import { COMMUNITIES_SORT_CONFIG } from '../context/sortConfig'
import { query } from '../graphql'
import {
CREATE_COMMUNITY_MUTATION,
DELETE_COMMUNITY_MUTATION,
UPDATE_COMMUNITY_MUTATION
} from '../graphql/mutations'
import { GET_COMMUNITIES_QUERY } from '../graphql/queries'
import CommunityEditModal from '../modals/CommunityEditModal'
import styles from '../styles/Table.module.css'
import Button from '../ui/Button'
import Modal from '../ui/Modal'
import SortableHeader from '../ui/SortableHeader'
import TableControls from '../ui/TableControls'
/**
* Интерфейс для сообщества (используем локальный интерфейс для совместимости)
*/
interface Community {
id: number
slug: string
name: string
description: string
created_at: string
updated_at: string
creator_id: number
creator_name: string
followers_count: number
shouts_count: number
}
interface CommunitiesRouteProps {
onError: (error: string) => void
onSuccess: (message: string) => void
}
// Types for GraphQL responses
interface CommunitiesResponse {
get_communities_all: Array<{
id: number
name: string
slug: string
description: string
created_at: string
updated_at: string
creator_id: number
creator_name: string
followers_count: number
shouts_count: number
}>
}
interface CreateCommunityResponse {
create_community: {
success: boolean
error?: string
community?: {
id: number
name: string
slug: string
}
}
}
interface UpdateCommunityResponse {
update_community: {
success: boolean
error?: string
community?: {
id: number
name: string
slug: string
}
}
}
interface DeleteCommunityResponse {
delete_community: {
success: boolean
error?: string
}
}
/**
* Компонент для управления сообществами
*/
const CommunitiesRoute: Component<CommunitiesRouteProps> = (props) => {
const [communities, setCommunities] = createSignal<Community[]>([])
const [loading, setLoading] = createSignal(false)
const { sortState } = useTableSort()
const [editModal, setEditModal] = createSignal<{
show: boolean
community: Community | null
}>({
show: false,
community: null
})
const [deleteModal, setDeleteModal] = createSignal<{
show: boolean
community: Community | null
}>({
show: false,
community: null
})
const [createModal, setCreateModal] = createSignal<{ show: boolean }>({
show: false
})
/**
* Загружает список всех сообществ
*/
const loadCommunities = async () => {
setLoading(true)
try {
// Загружаем все сообщества без параметров сортировки
// Сортировка будет выполнена на клиенте
const result = await query('/graphql', GET_COMMUNITIES_QUERY)
// Получаем данные и сортируем их на клиенте
const communitiesData = (result as CommunitiesResponse)?.get_communities_all || []
const sortedCommunities = sortCommunities(communitiesData)
setCommunities(sortedCommunities)
} catch (error) {
props.onError(`Ошибка загрузки сообществ: ${(error as Error).message}`)
} finally {
setLoading(false)
}
}
/**
* Форматирует дату
*/
const formatDate = (dateString: string): string => {
return new Date(dateString).toLocaleDateString('ru-RU')
}
/**
* Сортирует сообщества на клиенте в соответствии с текущим состоянием сортировки
*/
const sortCommunities = (communities: Community[]): Community[] => {
const { field, direction } = sortState()
return [...communities].sort((a, b) => {
let comparison = 0
switch (field) {
case 'id':
comparison = a.id - b.id
break
case 'name':
comparison = (a.name || '').localeCompare(b.name || '', 'ru')
break
case 'slug':
comparison = (a.slug || '').localeCompare(b.slug || '', 'ru')
break
case 'created_at':
comparison = a.created_at.localeCompare(b.created_at, 'ru')
break
case 'created_by': {
const aName = a.creator_name || ''
const bName = b.creator_name || ''
comparison = aName.localeCompare(bName, 'ru')
break
}
case 'shouts':
comparison = (a.shouts_count || 0) - (b.shouts_count || 0)
break
case 'followers':
comparison = (a.followers_count || 0) - (b.followers_count || 0)
break
case 'authors':
comparison = (a.creator_id || 0) - (b.creator_id || 0)
break
default:
comparison = a.id - b.id
}
return direction === 'desc' ? -comparison : comparison
})
}
/**
* Открывает модалку создания
*/
const openCreateModal = () => {
setCreateModal({ show: true })
}
/**
* Открывает модалку редактирования
*/
const openEditModal = (community: Community) => {
setEditModal({ show: true, community })
}
/**
* Обрабатывает сохранение сообщества (создание или обновление)
*/
const handleSaveCommunity = async (communityData: Partial<Community>) => {
try {
const isCreating = !editModal().community && createModal().show
const mutation = isCreating ? CREATE_COMMUNITY_MUTATION : UPDATE_COMMUNITY_MUTATION
// Удаляем created_by, если он null или undefined
if (communityData.creator_id === null || communityData.creator_id === undefined) {
delete communityData.creator_id
}
const result = await query('/graphql', mutation, { community_input: communityData })
const resultData = isCreating
? (result as CreateCommunityResponse).create_community
: (result as UpdateCommunityResponse).update_community
if (resultData.error) {
throw new Error(resultData.error)
}
props.onSuccess(isCreating ? 'Сообщество успешно создано' : 'Сообщество успешно обновлено')
setCreateModal({ show: false })
setEditModal({ show: false, community: null })
await loadCommunities()
} catch (error) {
props.onError(
`Ошибка ${createModal().show ? 'создания' : 'обновления'} сообщества: ${(error as Error).message}`
)
}
}
/**
* Удаляет сообщество
*/
const deleteCommunity = async (slug: string) => {
try {
const result = await query('/graphql', DELETE_COMMUNITY_MUTATION, { slug })
const deleteResult = (result as DeleteCommunityResponse).delete_community
if (deleteResult.error) {
throw new Error(deleteResult.error)
}
if (!deleteResult.success) {
throw new Error('Не удалось удалить сообщество')
}
props.onSuccess('Сообщество успешно удалено')
setDeleteModal({ show: false, community: null })
await loadCommunities()
} catch (error) {
props.onError(`Ошибка удаления сообщества: ${(error as Error).message}`)
}
}
// Пересортировка при изменении состояния сортировки
createEffect(
on([sortState], () => {
if (communities().length > 0) {
// Используем untrack для предотвращения бесконечной рекурсии
const currentCommunities = untrack(() => communities())
const sortedCommunities = sortCommunities(currentCommunities)
// Сравниваем текущий порядок с отсортированным, чтобы избежать лишних обновлений
const needsUpdate =
JSON.stringify(currentCommunities.map((c: Community) => c.id)) !==
JSON.stringify(sortedCommunities.map((c: Community) => c.id))
if (needsUpdate) {
setCommunities(sortedCommunities)
}
}
})
)
// Загружаем сообщества при монтировании компонента
onMount(() => {
void loadCommunities()
})
return (
<div class={styles.container}>
<TableControls
onRefresh={loadCommunities}
isLoading={loading()}
actions={
<Button variant="primary" onClick={openCreateModal}>
Создать сообщество
</Button>
}
/>
<Show
when={!loading()}
fallback={
<div class="loading-screen">
<div class="loading-spinner" />
<div>Загрузка сообществ...</div>
</div>
}
>
<table class={styles.table}>
<thead>
<tr>
<SortableHeader field="id" allowedFields={COMMUNITIES_SORT_CONFIG.allowedFields}>
ID
</SortableHeader>
<SortableHeader field="name" allowedFields={COMMUNITIES_SORT_CONFIG.allowedFields}>
Название
</SortableHeader>
<SortableHeader field="slug" allowedFields={COMMUNITIES_SORT_CONFIG.allowedFields}>
Slug
</SortableHeader>
<th>Описание</th>
<SortableHeader field="created_by" allowedFields={COMMUNITIES_SORT_CONFIG.allowedFields}>
Создатель
</SortableHeader>
<SortableHeader field="shouts" allowedFields={COMMUNITIES_SORT_CONFIG.allowedFields}>
Публикации
</SortableHeader>
<SortableHeader field="followers" allowedFields={COMMUNITIES_SORT_CONFIG.allowedFields}>
Подписчики
</SortableHeader>
<th>Авторы</th>
<SortableHeader field="created_at" allowedFields={COMMUNITIES_SORT_CONFIG.allowedFields}>
Создано
</SortableHeader>
<th>Действия</th>
</tr>
</thead>
<tbody>
<For each={communities()}>
{(community) => (
<tr
onClick={() => openEditModal(community)}
style={{ cursor: 'pointer' }}
class={styles['clickable-row']}
>
<td>{community.id}</td>
<td>{community.name}</td>
<td>{community.slug}</td>
<td>
<div
style={{
'max-width': '200px',
overflow: 'hidden',
'text-overflow': 'ellipsis',
'white-space': 'nowrap'
}}
title={community.description}
>
{community.description || '—'}
</div>
</td>
<td>
<span>{community.creator_name || ''}</span>
</td>
<td>{community.shouts_count}</td>
<td>{community.followers_count}</td>
<td>{community.creator_id}</td>
<td>{formatDate(community.created_at)}</td>
<td onClick={(e) => e.stopPropagation()}>
<button
onClick={(e) => {
e.stopPropagation()
setDeleteModal({ show: true, community })
}}
class={styles['delete-button']}
title="Удалить сообщество"
aria-label="Удалить сообщество"
>
×
</button>
</td>
</tr>
)}
</For>
</tbody>
</table>
</Show>
{/* Модальное окно создания */}
<CommunityEditModal
isOpen={createModal().show}
community={null}
onClose={() => setCreateModal({ show: false })}
onSave={handleSaveCommunity}
/>
{/* Модальное окно редактирования */}
<CommunityEditModal
isOpen={editModal().show}
community={editModal().community}
onClose={() => setEditModal({ show: false, community: null })}
onSave={handleSaveCommunity}
/>
{/* Модальное окно подтверждения удаления */}
<Modal
isOpen={deleteModal().show}
onClose={() => setDeleteModal({ show: false, community: null })}
title="Подтверждение удаления"
>
<div>
<p>
Вы уверены, что хотите удалить сообщество "<strong>{deleteModal().community?.name}</strong>"?
</p>
<p class={styles['warning-text']}>
Это действие нельзя отменить. Все публикации и темы сообщества могут быть затронуты.
</p>
<div class={styles['modal-actions']}>
<Button variant="secondary" onClick={() => setDeleteModal({ show: false, community: null })}>
Отмена
</Button>
<Button
variant="danger"
onClick={() => deleteModal().community && deleteCommunity(deleteModal().community!.slug)}
>
Удалить
</Button>
</div>
</div>
</Modal>
</div>
)
}
export default CommunitiesRoute