2025-07-02 19:30:21 +00:00
|
|
|
|
import { createEffect, createSignal, Show } from 'solid-js'
|
|
|
|
|
import { useData } from '../context/data'
|
|
|
|
|
import type { Role } from '../graphql/generated/schema'
|
|
|
|
|
import {
|
|
|
|
|
GET_COMMUNITY_ROLE_SETTINGS_QUERY,
|
|
|
|
|
GET_COMMUNITY_ROLES_QUERY,
|
|
|
|
|
UPDATE_COMMUNITY_ROLE_SETTINGS_MUTATION
|
|
|
|
|
} from '../graphql/queries'
|
2025-06-30 19:19:46 +00:00
|
|
|
|
import formStyles from '../styles/Form.module.css'
|
|
|
|
|
import styles from '../styles/Modal.module.css'
|
|
|
|
|
import Button from '../ui/Button'
|
|
|
|
|
import Modal from '../ui/Modal'
|
2025-07-02 19:30:21 +00:00
|
|
|
|
import RoleManager from '../ui/RoleManager'
|
2025-07-03 09:15:10 +00:00
|
|
|
|
import HTMLEditor from '../ui/HTMLEditor'
|
2025-06-30 19:19:46 +00:00
|
|
|
|
|
|
|
|
|
interface Community {
|
|
|
|
|
id: number
|
|
|
|
|
name: string
|
2025-07-02 19:30:21 +00:00
|
|
|
|
slug: string
|
2025-06-30 19:19:46 +00:00
|
|
|
|
desc?: string
|
2025-07-02 19:30:21 +00:00
|
|
|
|
pic?: string
|
2025-06-30 19:19:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface CommunityEditModalProps {
|
|
|
|
|
isOpen: boolean
|
2025-07-02 19:30:21 +00:00
|
|
|
|
community: Community | null
|
2025-06-30 19:19:46 +00:00
|
|
|
|
onClose: () => void
|
2025-07-02 19:30:21 +00:00
|
|
|
|
onSave: (communityData: Partial<Community>) => Promise<void>
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
interface RoleSettings {
|
|
|
|
|
default_roles: string[]
|
|
|
|
|
available_roles: string[]
|
2025-06-30 19:19:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
interface CustomRole {
|
|
|
|
|
id: string
|
|
|
|
|
name: string
|
|
|
|
|
description: string
|
|
|
|
|
icon: string
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const STANDARD_ROLES = [
|
|
|
|
|
{ id: 'reader', name: 'Читатель', description: 'Может читать и комментировать', icon: '👁️' },
|
|
|
|
|
{ id: 'author', name: 'Автор', description: 'Может создавать публикации', icon: '✍️' },
|
|
|
|
|
{ id: 'artist', name: 'Художник', description: 'Может быть credited artist', icon: '🎨' },
|
|
|
|
|
{ id: 'expert', name: 'Эксперт', description: 'Может добавлять доказательства', icon: '🧠' },
|
|
|
|
|
{ id: 'editor', name: 'Редактор', description: 'Может модерировать контент', icon: '📝' },
|
|
|
|
|
{ id: 'admin', name: 'Администратор', description: 'Полные права', icon: '👑' }
|
|
|
|
|
]
|
|
|
|
|
|
|
|
|
|
const CommunityEditModal = (props: CommunityEditModalProps) => {
|
|
|
|
|
const { queryGraphQL } = useData()
|
|
|
|
|
const [formData, setFormData] = createSignal<Partial<Community>>({})
|
|
|
|
|
const [roleSettings, setRoleSettings] = createSignal<RoleSettings>({
|
|
|
|
|
default_roles: ['reader'],
|
|
|
|
|
available_roles: ['reader', 'author', 'artist', 'expert', 'editor', 'admin']
|
2025-06-30 19:19:46 +00:00
|
|
|
|
})
|
2025-07-02 19:30:21 +00:00
|
|
|
|
const [customRoles, setCustomRoles] = createSignal<CustomRole[]>([])
|
2025-06-30 19:19:46 +00:00
|
|
|
|
const [errors, setErrors] = createSignal<Record<string, string>>({})
|
2025-07-02 19:30:21 +00:00
|
|
|
|
const [activeTab, setActiveTab] = createSignal<'basic' | 'roles'>('basic')
|
|
|
|
|
const [loading, setLoading] = createSignal(false)
|
2025-06-30 19:19:46 +00:00
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
// Инициализация формы при открытии
|
2025-06-30 19:19:46 +00:00
|
|
|
|
createEffect(() => {
|
|
|
|
|
if (props.isOpen) {
|
|
|
|
|
if (props.community) {
|
|
|
|
|
setFormData({
|
2025-07-02 19:30:21 +00:00
|
|
|
|
name: props.community.name || '',
|
|
|
|
|
slug: props.community.slug || '',
|
2025-06-30 19:19:46 +00:00
|
|
|
|
desc: props.community.desc || '',
|
2025-07-02 19:30:21 +00:00
|
|
|
|
pic: props.community.pic || ''
|
2025-06-30 19:19:46 +00:00
|
|
|
|
})
|
2025-07-02 19:30:21 +00:00
|
|
|
|
void loadRoleSettings()
|
2025-06-30 19:19:46 +00:00
|
|
|
|
} else {
|
2025-07-02 19:30:21 +00:00
|
|
|
|
setFormData({ name: '', slug: '', desc: '', pic: '' })
|
|
|
|
|
setRoleSettings({
|
|
|
|
|
default_roles: ['reader'],
|
|
|
|
|
available_roles: ['reader', 'author', 'artist', 'expert', 'editor', 'admin']
|
2025-06-30 19:19:46 +00:00
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
setErrors({})
|
2025-07-02 19:30:21 +00:00
|
|
|
|
setActiveTab('basic')
|
|
|
|
|
setCustomRoles([])
|
2025-06-30 19:19:46 +00:00
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
const loadRoleSettings = async () => {
|
|
|
|
|
if (!props.community?.id) return
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const data = await queryGraphQL(GET_COMMUNITY_ROLE_SETTINGS_QUERY, {
|
|
|
|
|
community_id: props.community.id
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (data?.adminGetCommunityRoleSettings && !data.adminGetCommunityRoleSettings.error) {
|
|
|
|
|
setRoleSettings({
|
|
|
|
|
default_roles: data.adminGetCommunityRoleSettings.default_roles,
|
|
|
|
|
available_roles: data.adminGetCommunityRoleSettings.available_roles
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Загружаем все роли сообщества для получения произвольных
|
|
|
|
|
const rolesData = await queryGraphQL(GET_COMMUNITY_ROLES_QUERY, {
|
|
|
|
|
community: props.community.id
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (rolesData?.adminGetRoles) {
|
|
|
|
|
// Фильтруем только произвольные роли (не стандартные)
|
|
|
|
|
const standardRoleIds = STANDARD_ROLES.map((r) => r.id)
|
|
|
|
|
const customRolesList = rolesData.adminGetRoles
|
|
|
|
|
.filter((role: Role) => !standardRoleIds.includes(role.id))
|
|
|
|
|
.map((role: Role) => ({
|
|
|
|
|
id: role.id,
|
|
|
|
|
name: role.name,
|
|
|
|
|
description: role.description || '',
|
|
|
|
|
icon: '🔖' // Пока иконки не хранятся в БД
|
|
|
|
|
}))
|
|
|
|
|
|
|
|
|
|
setCustomRoles(customRolesList)
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Ошибка загрузки настроек ролей:', error)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validateForm = (): boolean => {
|
2025-06-30 19:19:46 +00:00
|
|
|
|
const newErrors: Record<string, string> = {}
|
|
|
|
|
const data = formData()
|
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
if (!data.name?.trim()) {
|
|
|
|
|
newErrors.name = 'Название обязательно'
|
2025-06-30 19:19:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
if (!data.slug?.trim()) {
|
|
|
|
|
newErrors.slug = 'Слаг обязательный'
|
|
|
|
|
} else if (!/^[a-z0-9-]+$/.test(data.slug)) {
|
|
|
|
|
newErrors.slug = 'Слаг может содержать только латинские буквы, цифры и дефисы'
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Валидация ролей
|
|
|
|
|
const roleSet = roleSettings()
|
|
|
|
|
if (roleSet.default_roles.length === 0) {
|
|
|
|
|
newErrors.roles = 'Должна быть хотя бы одна дефолтная роль'
|
2025-06-30 19:19:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
const invalidDefaults = roleSet.default_roles.filter((role) => !roleSet.available_roles.includes(role))
|
|
|
|
|
if (invalidDefaults.length > 0) {
|
|
|
|
|
newErrors.roles = 'Дефолтные роли должны быть из списка доступных'
|
2025-06-30 19:19:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
setErrors(newErrors)
|
|
|
|
|
return Object.keys(newErrors).length === 0
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const updateField = (field: string, value: string) => {
|
|
|
|
|
setFormData((prev) => ({ ...prev, [field]: value }))
|
|
|
|
|
setErrors((prev) => ({ ...prev, [field]: '' }))
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
const handleSave = async () => {
|
2025-06-30 19:19:46 +00:00
|
|
|
|
if (!validateForm()) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
2025-07-02 19:30:21 +00:00
|
|
|
|
setLoading(true)
|
|
|
|
|
try {
|
|
|
|
|
// Сохраняем основные данные сообщества
|
|
|
|
|
await props.onSave(formData())
|
|
|
|
|
|
|
|
|
|
// Если редактируем существующее сообщество, сохраняем настройки ролей
|
|
|
|
|
if (props.community?.id) {
|
|
|
|
|
const roleData = await queryGraphQL(UPDATE_COMMUNITY_ROLE_SETTINGS_MUTATION, {
|
|
|
|
|
community_id: props.community.id,
|
|
|
|
|
default_roles: roleSettings().default_roles,
|
|
|
|
|
available_roles: roleSettings().available_roles
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
if (!roleData?.adminUpdateCommunityRoleSettings?.success) {
|
|
|
|
|
console.error(
|
|
|
|
|
'Ошибка сохранения настроек ролей:',
|
|
|
|
|
roleData?.adminUpdateCommunityRoleSettings?.error
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('Ошибка сохранения:', error)
|
|
|
|
|
} finally {
|
|
|
|
|
setLoading(false)
|
|
|
|
|
}
|
2025-06-30 19:19:46 +00:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const isCreating = () => props.community === null
|
|
|
|
|
const modalTitle = () =>
|
|
|
|
|
isCreating()
|
|
|
|
|
? 'Создание нового сообщества'
|
|
|
|
|
: `Редактирование сообщества: ${props.community?.name || ''}`
|
|
|
|
|
|
|
|
|
|
return (
|
2025-07-02 19:30:21 +00:00
|
|
|
|
<Modal isOpen={props.isOpen} onClose={props.onClose} title={modalTitle()} size="large">
|
|
|
|
|
<div class={styles.content}>
|
|
|
|
|
{/* Табы */}
|
|
|
|
|
<div class={formStyles.tabs}>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class={`${formStyles.tab} ${activeTab() === 'basic' ? formStyles.active : ''}`}
|
|
|
|
|
onClick={() => setActiveTab('basic')}
|
|
|
|
|
>
|
|
|
|
|
<span class={formStyles.tabIcon}>⚙️</span>
|
|
|
|
|
Основные настройки
|
|
|
|
|
</button>
|
|
|
|
|
<Show when={!isCreating()}>
|
|
|
|
|
<button
|
|
|
|
|
type="button"
|
|
|
|
|
class={`${formStyles.tab} ${activeTab() === 'roles' ? formStyles.active : ''}`}
|
|
|
|
|
onClick={() => setActiveTab('roles')}
|
|
|
|
|
>
|
|
|
|
|
<span class={formStyles.tabIcon}>👥</span>
|
|
|
|
|
Роли и права
|
|
|
|
|
</button>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
{/* Контент табов */}
|
|
|
|
|
<div class={formStyles.content}>
|
|
|
|
|
<Show when={activeTab() === 'basic'}>
|
|
|
|
|
<div class={formStyles.form}>
|
|
|
|
|
<div class={formStyles.fieldGroup}>
|
|
|
|
|
<label class={formStyles.label}>
|
|
|
|
|
<span class={formStyles.labelText}>
|
|
|
|
|
<span class={formStyles.labelIcon}>🏷️</span>
|
|
|
|
|
Название сообщества
|
|
|
|
|
<span class={formStyles.required}>*</span>
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
class={`${formStyles.input} ${errors().name ? formStyles.error : ''}`}
|
|
|
|
|
value={formData().name || ''}
|
|
|
|
|
onInput={(e) => updateField('name', e.currentTarget.value)}
|
|
|
|
|
placeholder="Введите название сообщества"
|
|
|
|
|
/>
|
|
|
|
|
<Show when={errors().name}>
|
|
|
|
|
<span class={formStyles.fieldError}>
|
|
|
|
|
<span class={formStyles.errorIcon}>⚠️</span>
|
|
|
|
|
{errors().name}
|
|
|
|
|
</span>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class={formStyles.fieldGroup}>
|
|
|
|
|
<label class={formStyles.label}>
|
|
|
|
|
<span class={formStyles.labelText}>
|
|
|
|
|
<span class={formStyles.labelIcon}>🔗</span>
|
|
|
|
|
Слаг
|
|
|
|
|
<span class={formStyles.required}>*</span>
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="text"
|
|
|
|
|
class={`${formStyles.input} ${errors().slug ? formStyles.error : ''} ${!isCreating() ? formStyles.disabled : ''}`}
|
|
|
|
|
value={formData().slug || ''}
|
|
|
|
|
onInput={(e) => updateField('slug', e.currentTarget.value)}
|
|
|
|
|
placeholder="community-slug"
|
|
|
|
|
disabled={!isCreating()}
|
|
|
|
|
/>
|
|
|
|
|
<Show when={errors().slug}>
|
|
|
|
|
<span class={formStyles.fieldError}>
|
|
|
|
|
<span class={formStyles.errorIcon}>⚠️</span>
|
|
|
|
|
{errors().slug}
|
|
|
|
|
</span>
|
|
|
|
|
</Show>
|
|
|
|
|
<Show when={!isCreating()}>
|
|
|
|
|
<span class={formStyles.hint}>
|
|
|
|
|
<span class={formStyles.hintIcon}>💡</span>
|
|
|
|
|
Слаг нельзя изменить после создания
|
|
|
|
|
</span>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class={formStyles.fieldGroup}>
|
|
|
|
|
<label class={formStyles.label}>
|
|
|
|
|
<span class={formStyles.labelText}>
|
|
|
|
|
<span class={formStyles.labelIcon}>📝</span>
|
|
|
|
|
Описание
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
2025-07-03 09:15:10 +00:00
|
|
|
|
<HTMLEditor
|
2025-07-02 19:30:21 +00:00
|
|
|
|
value={formData().desc || ''}
|
2025-07-03 09:15:10 +00:00
|
|
|
|
onInput={(value) => updateField('desc', value)}
|
2025-07-02 19:30:21 +00:00
|
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class={formStyles.fieldGroup}>
|
|
|
|
|
<label class={formStyles.label}>
|
|
|
|
|
<span class={formStyles.labelText}>
|
|
|
|
|
<span class={formStyles.labelIcon}>🖼️</span>
|
|
|
|
|
Изображение (URL)
|
|
|
|
|
</span>
|
|
|
|
|
</label>
|
|
|
|
|
<input
|
|
|
|
|
type="url"
|
|
|
|
|
class={formStyles.input}
|
|
|
|
|
value={formData().pic || ''}
|
|
|
|
|
onInput={(e) => updateField('pic', e.currentTarget.value)}
|
|
|
|
|
placeholder="https://example.com/image.jpg"
|
|
|
|
|
/>
|
|
|
|
|
</div>
|
2025-06-30 19:19:46 +00:00
|
|
|
|
</div>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
</Show>
|
|
|
|
|
|
|
|
|
|
<Show when={activeTab() === 'roles' && !isCreating()}>
|
|
|
|
|
<RoleManager
|
|
|
|
|
communityId={props.community?.id}
|
|
|
|
|
roleSettings={roleSettings()}
|
|
|
|
|
onRoleSettingsChange={setRoleSettings}
|
|
|
|
|
customRoles={customRoles()}
|
|
|
|
|
onCustomRolesChange={setCustomRoles}
|
2025-06-30 19:19:46 +00:00
|
|
|
|
/>
|
2025-07-02 19:30:21 +00:00
|
|
|
|
|
|
|
|
|
<Show when={errors().roles}>
|
|
|
|
|
<span class={formStyles.fieldError}>
|
|
|
|
|
<span class={formStyles.errorIcon}>⚠️</span>
|
|
|
|
|
{errors().roles}
|
|
|
|
|
</span>
|
|
|
|
|
</Show>
|
|
|
|
|
</Show>
|
|
|
|
|
</div>
|
|
|
|
|
|
|
|
|
|
<div class={styles.footer}>
|
|
|
|
|
<Button variant="secondary" onClick={props.onClose}>
|
|
|
|
|
Отмена
|
|
|
|
|
</Button>
|
|
|
|
|
<Button variant="primary" onClick={handleSave} disabled={loading()}>
|
|
|
|
|
<Show when={loading()}>
|
|
|
|
|
<span class={formStyles.spinner} />
|
|
|
|
|
</Show>
|
|
|
|
|
{loading() ? 'Сохранение...' : isCreating() ? 'Создать' : 'Сохранить'}
|
|
|
|
|
</Button>
|
2025-06-30 19:19:46 +00:00
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
</Modal>
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
export default CommunityEditModal
|