roles-modal-fixes
Some checks failed
Deploy on push / deploy (push) Failing after 5s

This commit is contained in:
2025-07-25 10:50:03 +03:00
parent 5855412065
commit bceb311910
3 changed files with 209 additions and 314 deletions

View File

@@ -4,6 +4,9 @@ import formStyles from '../styles/Form.module.css'
import Button from '../ui/Button'
import Modal from '../ui/Modal'
// Список администраторских email
const ADMIN_EMAILS = ['welcome@discours.io']
export interface UserEditModalProps {
user: AdminUserInfo
isOpen: boolean
@@ -13,7 +16,7 @@ export interface UserEditModalProps {
email?: string
name?: string
slug?: string
roles: string[]
roles: string
}) => Promise<void>
}
@@ -65,7 +68,8 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
// Проверяем, является ли пользователь администратором по ролям, которые приходят с сервера
const isAdmin = () => {
return (props.user.roles || []).includes('admin')
const roles = formData().roles
return roles.includes('admin') || (props.user.email ? ADMIN_EMAILS.includes(props.user.email) : false)
}
// Получаем информацию о роли по ID
@@ -73,23 +77,16 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
return AVAILABLE_ROLES.find((role) => role.id === roleId) || { name: roleId, emoji: '👤' }
}
// Формируем строку с ролями и эмоджи
const getRolesDisplay = () => {
const roles = formData().roles
if (roles.length === 0) {
return isAdmin() ? '🪄 Администратор' : 'Роли не назначены'
// Обновляем поле формы
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
})
}
const roleTexts = roles.map((roleId) => {
const role = getRoleInfo(roleId)
return `${role.emoji} ${role.name}`
})
if (isAdmin()) {
return `🪄 Администратор, ${roleTexts.join(', ')}`
}
return roleTexts.join(', ')
}
// Обновляем форму при изменении пользователя
@@ -106,31 +103,25 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
}
})
const updateField = (field: string, value: string) => {
setFormData((prev) => ({ ...prev, [field]: value }))
// Очищаем ошибку при изменении поля
if (errors()[field]) {
setErrors((prev) => ({ ...prev, [field]: '' }))
}
}
const handleRoleToggle = (roleId: string) => {
// Роль администратора нельзя изменить вручную
if (roleId === 'admin') {
return
}
setFormData((prev) => {
const currentRoles = prev.roles
const currentRoles = prev.roles || []
const newRoles = currentRoles.includes(roleId)
? currentRoles.filter((r) => r !== roleId)
? currentRoles.filter((r: string) => r !== roleId)
: [...currentRoles, roleId]
return { ...prev, roles: newRoles }
})
// Очищаем ошибку ролей при изменении
if (errors().roles) {
setErrors((prev) => ({ ...prev, roles: '' }))
setErrors((prev) => {
const newErrors = { ...prev }
delete newErrors.roles
return newErrors
})
}
}
@@ -138,23 +129,19 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
const newErrors: Record<string, string> = {}
const data = formData()
// Email
if (!data.email.trim() || !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(data.email.trim())) {
newErrors.email = 'Неверный формат email'
}
// Имя
if (!data.name.trim() || data.name.trim().length < 2) {
newErrors.name = 'Имя должно содержать минимум 2 символа'
}
// Slug
if (!data.slug.trim() || !/^[a-z0-9_-]+$/.test(data.slug.trim())) {
newErrors.slug = 'Slug может содержать только латинские буквы, цифры, дефисы и подчеркивания'
}
// Роли (админы освобождаются от этого требования)
if (!isAdmin() && data.roles.filter((role) => role !== 'admin').length === 0) {
if (!isAdmin() && (data.roles || []).filter((role: string) => role !== 'admin').length === 0) {
newErrors.roles = 'Выберите хотя бы одну роль'
}
@@ -169,8 +156,10 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
setLoading(true)
try {
// Отправляем только обычные роли, админская роль определяется на сервере по email
await props.onSave(formData())
await props.onSave({
...formData(),
roles: (formData().roles || []).join(',')
})
props.onClose()
} catch (error) {
console.error('Ошибка при сохранении пользователя:', error)
@@ -185,148 +174,88 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
isOpen={props.isOpen}
onClose={props.onClose}
title={`Редактирование пользователя #${props.user.id}`}
size="large"
>
<div class={formStyles.form}>
{/* Компактная системная информация */}
{/* Основные данные */}
<div class={formStyles.fieldGroup}>
<div
style={{
display: 'grid',
'grid-template-columns': 'repeat(auto-fit, minmax(200px, 1fr))',
gap: '1rem',
padding: '1rem',
background: 'var(--form-bg-light)',
'font-size': '0.875rem',
color: 'var(--form-text-light)'
'grid-template-columns': 'repeat(auto-fit, minmax(250px, 1fr))',
gap: '1rem'
}}
>
<div>
<strong>ID:</strong> {props.user.id}
<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>
)}
</div>
<div>
<strong>Регистрация:</strong>{' '}
{props.user.created_at
? new Date(props.user.created_at * 1000).toLocaleDateString('ru-RU')
: '—'}
</div>
<div>
<strong>Активность:</strong>{' '}
{props.user.last_seen
? new Date(props.user.last_seen * 1000).toLocaleDateString('ru-RU')
: '—'}
</div>
</div>
</div>
{/* Текущие роли в строку */}
<div class={formStyles.fieldGroup} style={{ display: 'none' }}>
<label class={formStyles.label}>
<span class={formStyles.labelText}>
<span class={formStyles.labelIcon}>👤</span>
Текущие роли
</span>
</label>
<div
style={{
padding: '0.875rem 1rem',
background: isAdmin() ? 'rgba(245, 158, 11, 0.1)' : 'var(--form-bg-light)',
border: isAdmin() ? '1px solid rgba(245, 158, 11, 0.3)' : '1px solid var(--form-divider)',
'font-size': '0.95rem',
'font-weight': '500',
color: isAdmin() ? '#d97706' : 'var(--form-text)'
}}
>
{getRolesDisplay()}
</div>
</div>
{/* Основные данные в компактной сетке */}
<div
style={{
display: 'grid',
'grid-template-columns': 'repeat(auto-fit, minmax(250px, 1fr))',
gap: '1rem'
}}
>
<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>
)}
<div class={formStyles.hint}>
<span class={formStyles.hintIcon}>💡</span>
Администраторы определяются автоматически по настройкам сервера
<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>
)}
</div>
</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().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>
)}
</div>
<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"
/>
<div class={formStyles.hint}>
<span class={formStyles.hintIcon}>💡</span>
Только латинские буквы, цифры, дефисы и подчеркивания
<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>
)}
</div>
{errors().slug && (
<div class={formStyles.fieldError}>
<span class={formStyles.errorIcon}></span>
{errors().slug}
</div>
)}
</div>
</div>
@@ -346,8 +275,9 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
<For each={AVAILABLE_ROLES}>
{(role) => {
const isAdminRole = role.id === 'admin'
const isSelected = formData().roles.includes(role.id)
const isSelected = (formData().roles || []).includes(role.id)
const isDisabled = isAdminRole
const roleInfo = getRoleInfo(role.id)
return (
<label
@@ -369,7 +299,7 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
<div class={formStyles.roleHeader}>
<span class={formStyles.roleName}>
<span style={{ 'margin-right': '0.5rem', 'font-size': '1.1rem' }}>
{role.emoji}
{roleInfo.emoji}
</span>
{role.name}
{isAdminRole && (
@@ -418,20 +348,11 @@ const UserEditModal: Component<UserEditModalProps> = (props) => {
)}
{/* Компактные кнопки действий */}
<div
style={{
display: 'flex',
gap: '0.75rem',
'justify-content': 'flex-end',
'margin-top': '1.5rem',
'padding-top': '1rem',
'border-top': '1px solid var(--form-divider)'
}}
>
<Button variant="secondary" onClick={props.onClose} disabled={loading()}>
<div class={formStyles.actions}>
<Button type="button" onClick={props.onClose} disabled={loading()}>
Отмена
</Button>
<Button variant="primary" onClick={handleSave} loading={loading()}>
<Button type="button" onClick={handleSave} disabled={loading()}>
Сохранить
</Button>
</div>