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

@@ -379,3 +379,27 @@ export const DELETE_CUSTOM_ROLE_MUTATION: string =
} }
} }
`.loc?.source.body || '' `.loc?.source.body || ''
export const ADMIN_UPDATE_USER_MUTATION = `
mutation UpdateUser(
$id: Int!
$email: String
$name: String
$slug: String
$roles: String!
) {
updateUser(
id: $id
email: $email
name: $name
slug: $slug
roles: $roles
) {
id
email
name
slug
roles
}
}
`

View File

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

View File

@@ -3,8 +3,7 @@ import type { AuthorsSortField } from '../context/sort'
import { AUTHORS_SORT_CONFIG } from '../context/sortConfig' import { AUTHORS_SORT_CONFIG } from '../context/sortConfig'
import { query } from '../graphql' import { query } from '../graphql'
import type { Query, AdminUserInfo as User } from '../graphql/generated/schema' import type { Query, AdminUserInfo as User } from '../graphql/generated/schema'
import { ADMIN_UPDATE_USER_MUTATION } from '../graphql/mutations' import { ADMIN_GET_USERS_QUERY, ADMIN_UPDATE_USER_MUTATION } from '../graphql/queries'
import { ADMIN_GET_USERS_QUERY } from '../graphql/queries'
import UserEditModal from '../modals/RolesModal' import UserEditModal from '../modals/RolesModal'
import styles from '../styles/Admin.module.css' import styles from '../styles/Admin.module.css'
import Pagination from '../ui/Pagination' import Pagination from '../ui/Pagination'
@@ -18,19 +17,13 @@ export interface AuthorsRouteProps {
} }
const AuthorsRoute: Component<AuthorsRouteProps> = (props) => { const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
console.log('[AuthorsRoute] Initializing...') const [users, setUsers] = createSignal<User[]>([])
const [authors, setUsers] = createSignal<User[]>([])
const [loading, setLoading] = createSignal(true) const [loading, setLoading] = createSignal(true)
const [selectedUser, setSelectedUser] = createSignal<User | null>(null) const [selectedUser, setSelectedUser] = createSignal<User | null>(null)
const [showEditModal, setShowEditModal] = createSignal(false) const [showEditModal, setShowEditModal] = createSignal(false)
// Pagination state // Pagination state
const [pagination, setPagination] = createSignal<{ const [pagination, setPagination] = createSignal({
page: number
limit: number
total: number
totalPages: number
}>({
page: 1, page: 1,
limit: 20, limit: 20,
total: 0, total: 0,
@@ -44,7 +37,6 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
* Загрузка списка пользователей с учетом пагинации и поиска * Загрузка списка пользователей с учетом пагинации и поиска
*/ */
async function loadUsers() { async function loadUsers() {
console.log('[AuthorsRoute] Loading authors...')
try { try {
setLoading(true) setLoading(true)
const data = await query<{ adminGetUsers: Query['adminGetUsers'] }>( const data = await query<{ adminGetUsers: Query['adminGetUsers'] }>(
@@ -57,7 +49,6 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
} }
) )
if (data?.adminGetUsers?.authors) { if (data?.adminGetUsers?.authors) {
console.log('[AuthorsRoute] Users loaded:', data.adminGetUsers.authors.length)
setUsers(data.adminGetUsers.authors) setUsers(data.adminGetUsers.authors)
setPagination((prev) => ({ setPagination((prev) => ({
...prev, ...prev,
@@ -76,53 +67,35 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
/** /**
* Обновляет данные пользователя (профиль и роли) * Обновляет данные пользователя (профиль и роли)
*/ */
async function updateUser(userData: { const updateUser = async (userData: {
id: number id: number
email?: string email?: string
name?: string name?: string
slug?: string slug?: string
roles: string[] roles: string
}) { }) => {
try { try {
await query(`${location.origin}/graphql`, ADMIN_UPDATE_USER_MUTATION, { const result = await query<{
user: userData updateUser: User
}>(`${location.origin}/graphql`, ADMIN_UPDATE_USER_MUTATION, {
...userData,
roles: userData.roles
}) })
setUsers((prev) => if (result.updateUser) {
prev.map((user) => { // Обновляем локальный список пользователей
if (user.id === userData.id) { setUsers((prevUsers) =>
return { prevUsers.map((user) => (user.id === result.updateUser.id ? result.updateUser : user))
...user, )
email: userData.email || user.email, // Закрываем модальное окно
name: userData.name || user.name, setShowEditModal(false)
slug: userData.slug || user.slug,
roles: userData.roles
}
}
return user
})
)
closeEditModal()
props.onSuccess?.('Данные пользователя успешно обновлены')
void loadUsers()
} catch (err) {
console.error('Ошибка обновления пользователя:', err)
let errorMessage = err instanceof Error ? err.message : 'Ошибка обновления данных пользователя'
if (errorMessage.includes('author_role.community')) {
errorMessage = 'Ошибка: для роли author требуется указать community. Обратитесь к администратору.'
} }
} catch (error) {
props.onError?.(errorMessage) console.error('Ошибка при обновлении пользователя:', error)
props.onError?.(error instanceof Error ? error.message : 'Не удалось обновить пользователя')
} }
} }
function closeEditModal() {
setShowEditModal(false)
setSelectedUser(null)
}
// Pagination handlers // Pagination handlers
function handlePageChange(page: number) { function handlePageChange(page: number) {
setPagination((prev) => ({ ...prev, page })) setPagination((prev) => ({ ...prev, page }))
@@ -134,11 +107,6 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
void loadUsers() void loadUsers()
} }
// Search handlers
function handleSearchChange(value: string) {
setSearchQuery(value)
}
function handleSearch() { function handleSearch() {
setPagination((prev) => ({ ...prev, page: 1 })) setPagination((prev) => ({ ...prev, page: 1 }))
void loadUsers() void loadUsers()
@@ -146,7 +114,6 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
// Load authors on mount // Load authors on mount
onMount(() => { onMount(() => {
console.log('[AuthorsRoute] Component mounted, loading authors...')
void loadUsers() void loadUsers()
}) })
@@ -156,35 +123,24 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
const RoleBadge: Component<{ role: string }> = (props) => { const RoleBadge: Component<{ role: string }> = (props) => {
const getRoleIcon = (role: string): string => { const getRoleIcon = (role: string): string => {
switch (role.toLowerCase().trim()) { switch (role.toLowerCase().trim()) {
case 'администратор':
case 'admin': case 'admin':
return '🪄' return '🔧'
case 'редактор':
case 'editor': case 'editor':
return '✒️' return '✒️'
case 'эксперт':
case 'expert': case 'expert':
return '🔬' return '🔬'
case 'автор':
case 'author': case 'author':
return '📝' return '📝'
case 'читатель':
case 'reader': case 'reader':
return '📖' return '📖'
case 'banned':
case 'заблокирован':
return '🚫'
case 'verified':
case 'проверен':
return '✓'
default: default:
return '👤' return '👤'
} }
} }
return ( return (
<span title={props.role} style={{ 'margin-right': '0.25rem' }}> <span title={props.role}>
{getRoleIcon(props.role)} {getRoleIcon(props.role)} {props.role}
</span> </span>
) )
} }
@@ -195,80 +151,74 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
<div class={styles['loading']}>Загрузка данных...</div> <div class={styles['loading']}>Загрузка данных...</div>
</Show> </Show>
<Show when={!loading() && authors().length === 0}> <Show when={!loading() && users().length === 0}>
<div class={styles['empty-state']}>Нет данных для отображения</div> <div class={styles['empty-state']}>Нет данных для отображения</div>
</Show> </Show>
<Show when={!loading() && authors().length > 0}> <Show when={!loading() && users().length > 0}>
<TableControls <TableControls
searchValue={searchQuery()} searchValue={searchQuery()}
onSearchChange={handleSearchChange} onSearchChange={setSearchQuery}
onSearch={handleSearch} onSearch={handleSearch}
searchPlaceholder="Поиск по email, имени или ID..." searchPlaceholder="Поиск по email, имени или ID..."
isLoading={loading()}
/> />
<div class={styles['authors-list']}> <table>
<table> <thead>
<thead> <tr>
<tr> <SortableHeader
<SortableHeader field={'id' as AuthorsSortField}
field={'id' as AuthorsSortField} allowedFields={AUTHORS_SORT_CONFIG.allowedFields}
allowedFields={AUTHORS_SORT_CONFIG.allowedFields} >
ID
</SortableHeader>
<SortableHeader
field={'email' as AuthorsSortField}
allowedFields={AUTHORS_SORT_CONFIG.allowedFields}
>
Email
</SortableHeader>
<SortableHeader
field={'name' as AuthorsSortField}
allowedFields={AUTHORS_SORT_CONFIG.allowedFields}
>
Имя
</SortableHeader>
<SortableHeader
field={'created_at' as AuthorsSortField}
allowedFields={AUTHORS_SORT_CONFIG.allowedFields}
>
Создан
</SortableHeader>
<th>Роли</th>
</tr>
</thead>
<tbody>
<For each={users()}>
{(user) => (
<tr
onClick={() => {
setSelectedUser(user)
setShowEditModal(true)
}}
> >
ID <td>{user.id}</td>
</SortableHeader> <td>{user.email}</td>
<SortableHeader <td>{user.name || '-'}</td>
field={'email' as AuthorsSortField} <td>{formatDateRelative(user.created_at || Date.now())()}</td>
allowedFields={AUTHORS_SORT_CONFIG.allowedFields} <td class={styles['roles-cell']}>
> <div class={styles['roles-container']}>
Email <For each={user.roles || []}>{(role) => <RoleBadge role={role.trim()} />}</For>
</SortableHeader> {(!user.roles || user.roles.length === 0) && (
<SortableHeader <span style="color: #999; font-size: 0.875rem;">Нет ролей</span>
field={'name' as AuthorsSortField} )}
allowedFields={AUTHORS_SORT_CONFIG.allowedFields} </div>
> </td>
Имя </tr>
</SortableHeader> )}
<SortableHeader </For>
field={'created_at' as AuthorsSortField} </tbody>
allowedFields={AUTHORS_SORT_CONFIG.allowedFields} </table>
>
Создан
</SortableHeader>
<th>Роли</th>
</tr>
</thead>
<tbody>
<For each={authors()}>
{(user) => (
<tr
onClick={() => {
setSelectedUser(user)
setShowEditModal(true)
}}
>
<td>{user.id}</td>
<td>{user.email}</td>
<td>{user.name || '-'}</td>
<td>{formatDateRelative(user.created_at || Date.now())()}</td>
<td class={styles['roles-cell']}>
<div class={styles['roles-container']}>
<For each={Array.from(user.roles || []).filter(Boolean)}>
{(role) => <RoleBadge role={role} />}
</For>
{/* Показываем сообщение если ролей нет */}
{(!user.roles || user.roles.length === 0) && (
<span style="color: #999; font-size: 0.875rem;">Нет ролей</span>
)}
</div>
</td>
</tr>
)}
</For>
</tbody>
</table>
</div>
<Pagination <Pagination
currentPage={pagination().page} currentPage={pagination().page}
@@ -284,7 +234,7 @@ const AuthorsRoute: Component<AuthorsRouteProps> = (props) => {
<UserEditModal <UserEditModal
user={selectedUser()!} user={selectedUser()!}
isOpen={showEditModal()} isOpen={showEditModal()}
onClose={closeEditModal} onClose={() => setShowEditModal(false)}
onSave={updateUser} onSave={updateUser}
/> />
</Show> </Show>