Files
core/panel/modals/RolesModal.tsx

387 lines
14 KiB
TypeScript
Raw Normal View History

2025-07-25 11:12:19 +03:00
import { Component, createEffect, createSignal, For, Show } from 'solid-js'
2025-06-30 21:25:26 +03:00
import type { AdminUserInfo } from '../graphql/generated/schema'
2025-07-02 22:30:21 +03:00
import formStyles from '../styles/Form.module.css'
2025-06-30 21:25:26 +03:00
import Button from '../ui/Button'
import Modal from '../ui/Modal'
2025-07-25 10:50:03 +03:00
// Список администраторских email
const ADMIN_EMAILS = ['welcome@discours.io']
2025-06-30 21:25:26 +03:00
export interface UserEditModalProps {
user: AdminUserInfo
isOpen: boolean
onClose: () => void
onSave: (userData: {
id: number
email?: string
name?: string
slug?: string
2025-07-25 10:50:03 +03:00
roles: string
2025-06-30 21:25:26 +03:00
}) => Promise<void>
}
2025-07-25 09:58:34 +03:00
// Доступные роли в системе
2025-06-30 21:25:26 +03:00
const AVAILABLE_ROLES = [
2025-07-25 09:58:34 +03:00
{
2025-07-25 10:09:01 +03:00
id: 'admin',
2025-07-25 09:58:34 +03:00
name: 'Системный администратор',
description: 'Администраторы определяются автоматически по настройкам сервера',
emoji: '🪄'
},
2025-06-30 21:25:26 +03:00
{
2025-07-25 10:09:01 +03:00
id: 'editor',
2025-07-02 22:30:21 +03:00
name: 'Редактор',
description: 'Редактирование публикаций и управление сообществом',
emoji: '✒️'
},
{
2025-07-25 10:09:01 +03:00
id: 'expert',
2025-06-30 21:25:26 +03:00
name: 'Эксперт',
2025-07-02 22:30:21 +03:00
description: 'Добавление доказательств и опровержений, управление темами',
emoji: '🔬'
2025-06-30 21:25:26 +03:00
},
2025-07-02 22:30:21 +03:00
{
2025-07-25 10:09:01 +03:00
id: 'author',
2025-07-02 22:30:21 +03:00
name: 'Автор',
description: 'Создание и редактирование своих публикаций',
emoji: '📝'
},
{
2025-07-25 10:09:01 +03:00
id: 'reader',
2025-07-02 22:30:21 +03:00
name: 'Читатель',
description: 'Чтение и комментирование',
emoji: '📖'
}
2025-06-30 21:25:26 +03:00
]
const UserEditModal: Component<UserEditModalProps> = (props) => {
const [formData, setFormData] = createSignal({
2025-07-02 22:30:21 +03:00
id: props.user.id,
2025-06-30 21:25:26 +03:00
email: props.user.email || '',
name: props.user.name || '',
slug: props.user.slug || '',
2025-07-25 10:09:01 +03:00
roles: props.user.roles || []
2025-06-30 21:25:26 +03:00
})
2025-07-02 22:30:21 +03:00
2025-06-30 21:25:26 +03:00
const [errors, setErrors] = createSignal<Record<string, string>>({})
2025-07-02 22:30:21 +03:00
const [loading, setLoading] = createSignal(false)
// Проверяем, является ли пользователь администратором по ролям, которые приходят с сервера
const isAdmin = () => {
2025-07-25 10:50:03 +03:00
const roles = formData().roles
return roles.includes('admin') || (props.user.email ? ADMIN_EMAILS.includes(props.user.email) : false)
2025-07-02 22:30:21 +03:00
}
// Получаем информацию о роли по ID
const getRoleInfo = (roleId: string) => {
2025-07-03 00:20:10 +03:00
return AVAILABLE_ROLES.find((role) => role.id === roleId) || { name: roleId, emoji: '👤' }
2025-07-02 22:30:21 +03:00
}
2025-07-25 10:50:03 +03:00
// Обновляем поле формы
const updateField = (field: keyof ReturnType<typeof formData>, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }))
if (errors()[field]) {
setErrors((prev) => {
const newErrors = { ...prev }
delete newErrors[field]
return newErrors
})
2025-07-02 22:30:21 +03:00
}
}
2025-06-30 21:25:26 +03:00
2025-07-02 22:30:21 +03:00
// Обновляем форму при изменении пользователя
2025-06-30 21:25:26 +03:00
createEffect(() => {
2025-07-02 22:30:21 +03:00
if (props.user) {
2025-06-30 21:25:26 +03:00
setFormData({
2025-07-02 22:30:21 +03:00
id: props.user.id,
2025-06-30 21:25:26 +03:00
email: props.user.email || '',
name: props.user.name || '',
slug: props.user.slug || '',
2025-07-25 10:09:01 +03:00
roles: props.user.roles || []
2025-06-30 21:25:26 +03:00
})
setErrors({})
}
})
2025-07-02 22:30:21 +03:00
const handleRoleToggle = (roleId: string) => {
2025-07-25 10:09:01 +03:00
if (roleId === 'admin') {
2025-07-25 09:58:34 +03:00
return
}
2025-07-02 22:30:21 +03:00
setFormData((prev) => {
2025-07-25 10:50:03 +03:00
const currentRoles = prev.roles || []
2025-07-02 22:30:21 +03:00
const newRoles = currentRoles.includes(roleId)
2025-07-25 10:50:03 +03:00
? currentRoles.filter((r: string) => r !== roleId)
2025-07-02 22:30:21 +03:00
: [...currentRoles, roleId]
return { ...prev, roles: newRoles }
})
if (errors().roles) {
2025-07-25 10:50:03 +03:00
setErrors((prev) => {
const newErrors = { ...prev }
delete newErrors.roles
return newErrors
})
2025-07-02 22:30:21 +03:00
}
}
const validateForm = (): boolean => {
2025-06-30 21:25:26 +03:00
const newErrors: Record<string, string> = {}
const data = formData()
2025-07-25 10:09:01 +03:00
if (!data.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email.trim())) {
2025-07-02 22:30:21 +03:00
newErrors.email = 'Неверный формат email'
2025-06-30 21:25:26 +03:00
}
2025-07-25 10:09:01 +03:00
if (!data.name.trim() || data.name.trim().length < 2) {
2025-07-02 22:30:21 +03:00
newErrors.name = 'Имя должно содержать минимум 2 символа'
2025-06-30 21:25:26 +03:00
}
2025-07-25 10:09:01 +03:00
if (!data.slug.trim() || !/^[a-z0-9_-]+$/.test(data.slug.trim())) {
2025-06-30 21:25:26 +03:00
newErrors.slug = 'Slug может содержать только латинские буквы, цифры, дефисы и подчеркивания'
}
2025-07-25 10:50:03 +03:00
if (!isAdmin() && (data.roles || []).filter((role: string) => role !== 'admin').length === 0) {
2025-07-25 10:09:01 +03:00
newErrors.roles = 'Выберите хотя бы одну роль'
2025-06-30 21:25:26 +03:00
}
setErrors(newErrors)
return Object.keys(newErrors).length === 0
}
const handleSave = async () => {
if (!validateForm()) {
return
}
setLoading(true)
try {
2025-07-25 10:50:03 +03:00
await props.onSave({
...formData(),
roles: (formData().roles || []).join(',')
})
2025-06-30 21:25:26 +03:00
props.onClose()
} catch (error) {
2025-07-02 22:30:21 +03:00
console.error('Ошибка при сохранении пользователя:', error)
setErrors({ general: 'Ошибка при сохранении пользователя' })
2025-06-30 21:25:26 +03:00
} finally {
setLoading(false)
}
}
return (
<Modal
isOpen={props.isOpen}
onClose={props.onClose}
2025-07-02 22:30:21 +03:00
title={`Редактирование пользователя #${props.user.id}`}
2025-06-30 21:25:26 +03:00
>
2025-07-02 22:30:21 +03:00
<div class={formStyles.form}>
2025-07-25 10:50:03 +03:00
{/* Основные данные */}
2025-07-02 22:30:21 +03:00
<div class={formStyles.fieldGroup}>
<div
style={{
display: 'grid',
2025-07-25 10:50:03 +03:00
'grid-template-columns': 'repeat(auto-fit, minmax(250px, 1fr))',
gap: '1rem'
2025-07-02 22:30:21 +03:00
}}
>
2025-07-25 10:50:03 +03:00
<div class={formStyles.fieldGroup}>
<label class={formStyles.label}>
<span class={formStyles.labelText}>
<span class={formStyles.labelIcon}>📧</span>
Email
<span class={formStyles.required}>*</span>
</span>
</label>
<input
type="email"
class={`${formStyles.input} ${errors().email ? formStyles.error : ''}`}
value={formData().email}
onInput={(e) => updateField('email', e.currentTarget.value)}
disabled={loading()}
placeholder="user@example.com"
/>
{errors().email && (
<div class={formStyles.fieldError}>
<span class={formStyles.errorIcon}></span>
{errors().email}
</div>
)}
2025-06-30 21:25:26 +03:00
</div>
2025-07-25 10:50:03 +03:00
<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)}
disabled={loading()}
placeholder="Иван Иванов"
/>
{errors().name && (
<div class={formStyles.fieldError}>
<span class={formStyles.errorIcon}></span>
{errors().name}
</div>
)}
2025-07-02 22:30:21 +03:00
</div>
2025-06-30 21:25:26 +03:00
2025-07-25 10:50:03 +03:00
<div class={formStyles.fieldGroup}>
<label class={formStyles.label}>
<span class={formStyles.labelText}>
<span class={formStyles.labelIcon}>🔗</span>
Slug (URL)
<span class={formStyles.required}>*</span>
</span>
</label>
<input
type="text"
class={`${formStyles.input} ${errors().slug ? formStyles.error : ''}`}
value={formData().slug}
onInput={(e) => updateField('slug', e.currentTarget.value.toLowerCase())}
disabled={loading()}
placeholder="ivan-ivanov"
/>
{errors().slug && (
<div class={formStyles.fieldError}>
<span class={formStyles.errorIcon}></span>
{errors().slug}
</div>
)}
2025-06-30 21:25:26 +03:00
</div>
</div>
</div>
{/* Роли */}
2025-07-02 22:30:21 +03:00
<div class={formStyles.fieldGroup}>
<label class={formStyles.label}>
<span class={formStyles.labelText}>
<span class={formStyles.labelIcon}></span>
Управление ролями
<span class={formStyles.required} style={{ display: isAdmin() ? 'none' : 'inline' }}>
*
</span>
</span>
</label>
2025-06-30 21:25:26 +03:00
2025-07-02 22:30:21 +03:00
<div class={formStyles.rolesGrid}>
2025-06-30 21:25:26 +03:00
<For each={AVAILABLE_ROLES}>
2025-07-25 09:58:34 +03:00
{(role) => {
2025-07-25 10:09:01 +03:00
const isAdminRole = role.id === 'admin'
2025-07-25 10:50:03 +03:00
const isSelected = (formData().roles || []).includes(role.id)
2025-07-25 10:09:01 +03:00
const isDisabled = isAdminRole
2025-07-25 10:50:03 +03:00
const roleInfo = getRoleInfo(role.id)
2025-07-25 09:58:34 +03:00
return (
<label
2025-07-25 10:09:01 +03:00
class={`${formStyles.roleCard} ${isSelected ? formStyles.roleCardSelected : ''} ${isDisabled ? formStyles.roleCardDisabled : ''}`}
2025-07-25 09:58:34 +03:00
style={{
opacity: isDisabled ? 0.7 : 1,
cursor: isDisabled ? 'not-allowed' : 'pointer',
background: isAdminRole && isSelected ? 'rgba(245, 158, 11, 0.1)' : undefined,
border: isAdminRole && isSelected ? '1px solid rgba(245, 158, 11, 0.3)' : undefined
}}
2025-07-25 11:18:12 +03:00
onClick={() => !isDisabled && handleRoleToggle(role.id)}
2025-07-25 09:58:34 +03:00
>
<div class={formStyles.roleHeader}>
<span class={formStyles.roleName}>
<span style={{ 'margin-right': '0.5rem', 'font-size': '1.1rem' }}>
2025-07-25 10:50:03 +03:00
{roleInfo.emoji}
2025-07-25 09:58:34 +03:00
</span>
{role.name}
{isAdminRole && (
<span
style={{
'margin-left': '0.5rem',
'font-size': '0.75rem',
color: '#d97706',
'font-weight': 'normal'
}}
>
(системная)
</span>
)}
</span>
2025-07-25 11:12:19 +03:00
<div
style={{
width: '20px',
height: '20px',
'border-radius': '50%',
border: `2px solid ${isSelected ? '#3b82f6' : '#a1a1aa'}`,
'background-color': isSelected ? '#3b82f6' : 'transparent',
display: 'flex',
'align-items': 'center',
'justify-content': 'center',
cursor: isDisabled ? 'not-allowed' : 'pointer'
}}
>
2025-07-25 11:18:12 +03:00
<Show when={isSelected}>
2025-07-25 11:12:19 +03:00
<svg
xmlns="http://www.w3.org/2000/svg"
width="12"
height="12"
viewBox="0 0 24 24"
fill="none"
stroke="white"
stroke-width="3"
stroke-linecap="round"
stroke-linejoin="round"
>
<polyline points="20 6 9 17 4 12"></polyline>
</svg>
2025-07-25 11:18:12 +03:00
</Show>
2025-07-25 11:12:19 +03:00
</div>
2025-07-25 09:58:34 +03:00
</div>
<div class={formStyles.roleDescription}>{role.description}</div>
</label>
)
}}
2025-06-30 21:25:26 +03:00
</For>
</div>
2025-07-02 22:30:21 +03:00
{!isAdmin() && errors().roles && (
<div class={formStyles.fieldError}>
<span class={formStyles.errorIcon}></span>
{errors().roles}
</div>
)}
<div class={formStyles.hint}>
<span class={formStyles.hintIcon}>💡</span>
2025-07-25 09:58:34 +03:00
Системные роли (администратор) назначаются автоматически и не могут быть изменены вручную.
{!isAdmin() &&
' Выберите дополнительные роли для пользователя - минимум одна роль обязательна.'}
2025-07-02 22:30:21 +03:00
</div>
</div>
{/* Общая ошибка */}
{errors().general && (
<div class={formStyles.fieldError}>
<span class={formStyles.errorIcon}></span>
{errors().general}
</div>
)}
{/* Компактные кнопки действий */}
2025-07-25 10:50:03 +03:00
<div class={formStyles.actions}>
<Button type="button" onClick={props.onClose} disabled={loading()}>
2025-07-02 22:30:21 +03:00
Отмена
</Button>
2025-07-25 10:50:03 +03:00
<Button type="button" onClick={handleSave} disabled={loading()}>
2025-07-02 22:30:21 +03:00
Сохранить
</Button>
2025-06-30 21:25:26 +03:00
</div>
</div>
</Modal>
)
}
export default UserEditModal