This commit is contained in:
parent
5cfde98c22
commit
6c95b0575a
16
CHANGELOG.md
16
CHANGELOG.md
|
@ -6,6 +6,22 @@
|
||||||
- Исправлена ошибка в функции `authenticate` в файле `auth/internal.py` - неправильное создание объекта `AuthState` и использование `TokenManager` вместо прямого создания `SessionTokenManager`
|
- Исправлена ошибка в функции `authenticate` в файле `auth/internal.py` - неправильное создание объекта `AuthState` и использование `TokenManager` вместо прямого создания `SessionTokenManager`
|
||||||
- Исправлена ошибка в функции `admin_get_invites` в файле `resolvers/admin.py` - добавлено значение по умолчанию для поля `slug` в объектах `Author`, чтобы избежать ошибки "Cannot return null for non-nullable field Author.slug"
|
- Исправлена ошибка в функции `admin_get_invites` в файле `resolvers/admin.py` - добавлено значение по умолчанию для поля `slug` в объектах `Author`, чтобы избежать ошибки "Cannot return null for non-nullable field Author.slug"
|
||||||
|
|
||||||
|
### Улучшения админ-панели для приглашений
|
||||||
|
|
||||||
|
- **ОБНОВЛЕНО**: Управление приглашениями в админ-панели:
|
||||||
|
- **Удалена возможность создания приглашений**: Приглашения теперь создаются только через основной интерфейс пользователями
|
||||||
|
- **Удалена возможность редактирования приглашений**: Статусы приглашений изменяются автоматически при принятии/отклонении
|
||||||
|
- **Добавлено пакетное удаление**: Возможность выбрать несколько приглашений с помощью чекбоксов и удалить их одним действием
|
||||||
|
- **Чекбоксы для выбора**: Добавлены чекбоксы для каждого приглашения и опция "Выбрать все"
|
||||||
|
- **Кнопка пакетного удаления**: Появляется только когда выбрано хотя бы одно приглашение
|
||||||
|
- **Счетчик выбранных**: Отображает количество выбранных для удаления приглашений
|
||||||
|
- **Подтверждение удаления**: Модальное окно с запросом подтверждения перед пакетным удалением
|
||||||
|
|
||||||
|
- **Серверная часть**:
|
||||||
|
- **Новая GraphQL мутация**: `adminDeleteInvitesBatch` для пакетного удаления приглашений
|
||||||
|
- **Оптимизированная обработка**: Удаление нескольких приглашений в рамках одной транзакции
|
||||||
|
- **Обработка ошибок**: Детальное логирование и возврат информации о количестве успешно удаленных приглашений
|
||||||
|
|
||||||
### Новая функциональность CRUD приглашений
|
### Новая функциональность CRUD приглашений
|
||||||
|
|
||||||
- **НОВОЕ**: Полноценное управление приглашениями в админ-панели:
|
- **НОВОЕ**: Полноценное управление приглашениями в админ-панели:
|
||||||
|
|
|
@ -128,3 +128,12 @@ export const ADMIN_DELETE_INVITE_MUTATION = `
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
`
|
`
|
||||||
|
|
||||||
|
export const ADMIN_DELETE_INVITES_BATCH_MUTATION = `
|
||||||
|
mutation AdminDeleteInvitesBatch($invites: [AdminInviteIdInput!]!) {
|
||||||
|
adminDeleteInvitesBatch(invites: $invites) {
|
||||||
|
success
|
||||||
|
error
|
||||||
|
}
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
|
@ -1,11 +1,9 @@
|
||||||
import { Component, createSignal, For, onMount, Show } from 'solid-js'
|
import { Component, createSignal, For, onMount, Show } from 'solid-js'
|
||||||
import {
|
import {
|
||||||
ADMIN_CREATE_INVITE_MUTATION,
|
|
||||||
ADMIN_DELETE_INVITE_MUTATION,
|
ADMIN_DELETE_INVITE_MUTATION,
|
||||||
ADMIN_UPDATE_INVITE_MUTATION
|
ADMIN_DELETE_INVITES_BATCH_MUTATION
|
||||||
} from '../graphql/mutations'
|
} from '../graphql/mutations'
|
||||||
import { ADMIN_GET_INVITES_QUERY } from '../graphql/queries'
|
import { ADMIN_GET_INVITES_QUERY } from '../graphql/queries'
|
||||||
import InviteEditModal from '../modals/InviteEditModal'
|
|
||||||
import styles from '../styles/Table.module.css'
|
import styles from '../styles/Table.module.css'
|
||||||
import Button from '../ui/Button'
|
import Button from '../ui/Button'
|
||||||
import Modal from '../ui/Modal'
|
import Modal from '../ui/Modal'
|
||||||
|
@ -60,15 +58,18 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
||||||
totalPages: 1
|
totalPages: 1
|
||||||
})
|
})
|
||||||
|
|
||||||
const [editModal, setEditModal] = createSignal<{ show: boolean; invite: Invite | null }>({
|
// Состояние для выбранных приглашений
|
||||||
show: false,
|
const [selectedInvites, setSelectedInvites] = createSignal<Record<string, boolean>>({})
|
||||||
invite: null
|
const [selectAll, setSelectAll] = createSignal(false)
|
||||||
})
|
|
||||||
|
// Состояние для модального окна подтверждения удаления
|
||||||
const [deleteModal, setDeleteModal] = createSignal<{ show: boolean; invite: Invite | null }>({
|
const [deleteModal, setDeleteModal] = createSignal<{ show: boolean; invite: Invite | null }>({
|
||||||
show: false,
|
show: false,
|
||||||
invite: null
|
invite: null
|
||||||
})
|
})
|
||||||
const [createModal, setCreateModal] = createSignal<{ show: boolean }>({
|
|
||||||
|
// Состояние для модального окна подтверждения пакетного удаления
|
||||||
|
const [batchDeleteModal, setBatchDeleteModal] = createSignal<{ show: boolean }>({
|
||||||
show: false
|
show: false
|
||||||
})
|
})
|
||||||
|
|
||||||
|
@ -116,6 +117,10 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
||||||
total: data.total || 0,
|
total: data.total || 0,
|
||||||
totalPages: data.totalPages || 1
|
totalPages: data.totalPages || 1
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Сбрасываем выбранные приглашения при загрузке новых данных
|
||||||
|
setSelectedInvites({})
|
||||||
|
setSelectAll(false)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
props.onError(`Ошибка загрузки приглашений: ${(error as Error).message}`)
|
props.onError(`Ошибка загрузки приглашений: ${(error as Error).message}`)
|
||||||
} finally {
|
} finally {
|
||||||
|
@ -161,66 +166,6 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
|
||||||
* Открывает модалку создания
|
|
||||||
*/
|
|
||||||
const openCreateModal = () => {
|
|
||||||
setCreateModal({ show: true })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Открывает модалку редактирования
|
|
||||||
*/
|
|
||||||
const openEditModal = (invite: Invite) => {
|
|
||||||
setEditModal({ show: true, invite })
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Обрабатывает сохранение приглашения (создание или обновление)
|
|
||||||
*/
|
|
||||||
const handleSaveInvite = async (inviteData: Partial<Invite>) => {
|
|
||||||
try {
|
|
||||||
const isCreating = !editModal().invite && createModal().show
|
|
||||||
const mutation = isCreating ? ADMIN_CREATE_INVITE_MUTATION : ADMIN_UPDATE_INVITE_MUTATION
|
|
||||||
|
|
||||||
// Получаем токен авторизации из localStorage или cookie
|
|
||||||
const authToken = localStorage.getItem('auth_token') || getAuthTokenFromCookie()
|
|
||||||
console.log(`[InvitesRoute] Сохранение приглашения, токен: ${authToken ? 'найден' : 'не найден'}`)
|
|
||||||
|
|
||||||
const response = await fetch('/graphql', {
|
|
||||||
method: 'POST',
|
|
||||||
headers: {
|
|
||||||
'Content-Type': 'application/json',
|
|
||||||
Authorization: authToken ? `Bearer ${authToken}` : ''
|
|
||||||
},
|
|
||||||
body: JSON.stringify({
|
|
||||||
query: mutation,
|
|
||||||
variables: { invite: inviteData }
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
const result = await response.json()
|
|
||||||
|
|
||||||
if (result.errors) {
|
|
||||||
throw new Error(result.errors[0].message)
|
|
||||||
}
|
|
||||||
|
|
||||||
const resultData = isCreating ? result.data.adminCreateInvite : result.data.adminUpdateInvite
|
|
||||||
if (!resultData.success) {
|
|
||||||
throw new Error(resultData.error || 'Неизвестная ошибка')
|
|
||||||
}
|
|
||||||
|
|
||||||
props.onSuccess(isCreating ? 'Приглашение успешно создано' : 'Приглашение успешно обновлено')
|
|
||||||
setCreateModal({ show: false })
|
|
||||||
setEditModal({ show: false, invite: null })
|
|
||||||
await loadInvites(pagination().page)
|
|
||||||
} catch (error) {
|
|
||||||
props.onError(
|
|
||||||
`Ошибка ${createModal().show ? 'создания' : 'обновления'} приглашения: ${(error as Error).message}`
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Удаляет приглашение
|
* Удаляет приглашение
|
||||||
*/
|
*/
|
||||||
|
@ -264,6 +209,104 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Пакетное удаление выбранных приглашений
|
||||||
|
*/
|
||||||
|
const deleteSelectedInvites = async () => {
|
||||||
|
try {
|
||||||
|
const selected = selectedInvites()
|
||||||
|
const invitesToDelete = invites().filter(invite => {
|
||||||
|
const key = `${invite.inviter_id}-${invite.author_id}-${invite.shout_id}`
|
||||||
|
return selected[key]
|
||||||
|
})
|
||||||
|
|
||||||
|
if (invitesToDelete.length === 0) {
|
||||||
|
props.onError('Не выбрано ни одного приглашения для удаления')
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Получаем токен авторизации из localStorage или cookie
|
||||||
|
const authToken = localStorage.getItem('auth_token') || getAuthTokenFromCookie()
|
||||||
|
console.log(`[InvitesRoute] Пакетное удаление приглашений, токен: ${authToken ? 'найден' : 'не найден'}`)
|
||||||
|
|
||||||
|
const response = await fetch('/graphql', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: {
|
||||||
|
'Content-Type': 'application/json',
|
||||||
|
Authorization: authToken ? `Bearer ${authToken}` : ''
|
||||||
|
},
|
||||||
|
body: JSON.stringify({
|
||||||
|
query: ADMIN_DELETE_INVITES_BATCH_MUTATION,
|
||||||
|
variables: {
|
||||||
|
invites: invitesToDelete.map(invite => ({
|
||||||
|
inviter_id: invite.inviter_id,
|
||||||
|
author_id: invite.author_id,
|
||||||
|
shout_id: invite.shout_id
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
})
|
||||||
|
|
||||||
|
const result = await response.json()
|
||||||
|
|
||||||
|
if (result.errors) {
|
||||||
|
throw new Error(result.errors[0].message)
|
||||||
|
}
|
||||||
|
|
||||||
|
const deleteResult = result.data.adminDeleteInvitesBatch
|
||||||
|
|
||||||
|
if (!deleteResult.success) {
|
||||||
|
throw new Error(deleteResult.error || 'Неизвестная ошибка')
|
||||||
|
}
|
||||||
|
|
||||||
|
props.onSuccess(`Успешно удалено ${invitesToDelete.length} приглашений`)
|
||||||
|
setBatchDeleteModal({ show: false })
|
||||||
|
setSelectedInvites({})
|
||||||
|
setSelectAll(false)
|
||||||
|
await loadInvites(pagination().page)
|
||||||
|
} catch (error) {
|
||||||
|
props.onError(`Ошибка пакетного удаления приглашений: ${(error as Error).message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработчик выбора/снятия выбора с приглашения
|
||||||
|
*/
|
||||||
|
const handleSelectInvite = (invite: Invite, checked: boolean) => {
|
||||||
|
const key = `${invite.inviter_id}-${invite.author_id}-${invite.shout_id}`
|
||||||
|
setSelectedInvites(prev => ({ ...prev, [key]: checked }))
|
||||||
|
|
||||||
|
// Если снимаем выбор с элемента, то снимаем и "выбрать все"
|
||||||
|
if (!checked && selectAll()) {
|
||||||
|
setSelectAll(false)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Обработчик выбора/снятия выбора со всех приглашений
|
||||||
|
*/
|
||||||
|
const handleSelectAll = (checked: boolean) => {
|
||||||
|
setSelectAll(checked)
|
||||||
|
|
||||||
|
const newSelected: Record<string, boolean> = {}
|
||||||
|
if (checked) {
|
||||||
|
// Выбираем все приглашения на текущей странице
|
||||||
|
invites().forEach(invite => {
|
||||||
|
const key = `${invite.inviter_id}-${invite.author_id}-${invite.shout_id}`
|
||||||
|
newSelected[key] = true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
setSelectedInvites(newSelected)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Получает количество выбранных приглашений
|
||||||
|
*/
|
||||||
|
const getSelectedCount = () => {
|
||||||
|
return Object.values(selectedInvites()).filter(Boolean).length
|
||||||
|
}
|
||||||
|
|
||||||
// Загружаем приглашения при монтировании компонента
|
// Загружаем приглашения при монтировании компонента
|
||||||
onMount(() => {
|
onMount(() => {
|
||||||
void loadInvites()
|
void loadInvites()
|
||||||
|
@ -300,12 +343,40 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
||||||
{loading() ? 'Загрузка...' : 'Обновить'}
|
{loading() ? 'Загрузка...' : 'Обновить'}
|
||||||
</Button>
|
</Button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<Button variant="primary" onClick={openCreateModal}>
|
|
||||||
Создать приглашение
|
|
||||||
</Button>
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* Панель пакетных действий */}
|
||||||
|
<Show when={!loading() && invites().length > 0}>
|
||||||
|
<div class={styles['batch-actions']}>
|
||||||
|
<div class={styles['select-all-container']}>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
id="select-all"
|
||||||
|
checked={selectAll()}
|
||||||
|
onChange={(e) => handleSelectAll(e.target.checked)}
|
||||||
|
class={styles.checkbox}
|
||||||
|
/>
|
||||||
|
<label for="select-all" class={styles['select-all-label']}>
|
||||||
|
Выбрать все
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Show when={getSelectedCount() > 0}>
|
||||||
|
<div class={styles['selected-count']}>
|
||||||
|
Выбрано: {getSelectedCount()}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<button
|
||||||
|
class={styles['batch-delete-button']}
|
||||||
|
onClick={() => setBatchDeleteModal({ show: true })}
|
||||||
|
title="Удалить выбранные приглашения"
|
||||||
|
>
|
||||||
|
Удалить выбранные
|
||||||
|
</button>
|
||||||
|
</Show>
|
||||||
|
</div>
|
||||||
|
</Show>
|
||||||
|
|
||||||
<Show when={loading()}>
|
<Show when={loading()}>
|
||||||
<div class={styles.loading}>Загрузка приглашений...</div>
|
<div class={styles.loading}>Загрузка приглашений...</div>
|
||||||
</Show>
|
</Show>
|
||||||
|
@ -319,6 +390,7 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
||||||
<table class={styles.table}>
|
<table class={styles.table}>
|
||||||
<thead>
|
<thead>
|
||||||
<tr>
|
<tr>
|
||||||
|
<th class={styles['checkbox-column']}></th>
|
||||||
<th>Приглашающий</th>
|
<th>Приглашающий</th>
|
||||||
<th>Приглашаемый</th>
|
<th>Приглашаемый</th>
|
||||||
<th>Публикация</th>
|
<th>Публикация</th>
|
||||||
|
@ -330,12 +402,20 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
||||||
<For each={invites()}>
|
<For each={invites()}>
|
||||||
{(invite) => {
|
{(invite) => {
|
||||||
const statusDisplay = getStatusDisplay(invite.status)
|
const statusDisplay = getStatusDisplay(invite.status)
|
||||||
|
const inviteKey = `${invite.inviter_id}-${invite.author_id}-${invite.shout_id}`
|
||||||
|
const isSelected = selectedInvites()[inviteKey] || false
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<tr
|
<tr>
|
||||||
class={styles.clickableRow}
|
<td class={styles['checkbox-column']}>
|
||||||
onClick={() => openEditModal(invite)}
|
<input
|
||||||
title="Нажмите для редактирования"
|
type="checkbox"
|
||||||
>
|
checked={isSelected}
|
||||||
|
onChange={(e) => handleSelectInvite(invite, e.target.checked)}
|
||||||
|
class={styles.checkbox}
|
||||||
|
onClick={(e) => e.stopPropagation()}
|
||||||
|
/>
|
||||||
|
</td>
|
||||||
<td>
|
<td>
|
||||||
<div>
|
<div>
|
||||||
<strong>{invite.inviter.name || 'Без имени'}</strong>
|
<strong>{invite.inviter.name || 'Без имени'}</strong>
|
||||||
|
@ -365,10 +445,7 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
||||||
<td>
|
<td>
|
||||||
<button
|
<button
|
||||||
class={styles.deleteButton}
|
class={styles.deleteButton}
|
||||||
onClick={(e) => {
|
onClick={() => setDeleteModal({ show: true, invite })}
|
||||||
e.stopPropagation()
|
|
||||||
setDeleteModal({ show: true, invite })
|
|
||||||
}}
|
|
||||||
title="Удалить приглашение"
|
title="Удалить приглашение"
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
|
@ -391,21 +468,6 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
||||||
/>
|
/>
|
||||||
</Show>
|
</Show>
|
||||||
|
|
||||||
{/* Модальные окна */}
|
|
||||||
<InviteEditModal
|
|
||||||
isOpen={createModal().show}
|
|
||||||
invite={null}
|
|
||||||
onClose={() => setCreateModal({ show: false })}
|
|
||||||
onSave={handleSaveInvite}
|
|
||||||
/>
|
|
||||||
|
|
||||||
<InviteEditModal
|
|
||||||
isOpen={editModal().show}
|
|
||||||
invite={editModal().invite}
|
|
||||||
onClose={() => setEditModal({ show: false, invite: null })}
|
|
||||||
onSave={handleSaveInvite}
|
|
||||||
/>
|
|
||||||
|
|
||||||
{/* Модальное окно подтверждения удаления */}
|
{/* Модальное окно подтверждения удаления */}
|
||||||
<Modal
|
<Modal
|
||||||
isOpen={deleteModal().show}
|
isOpen={deleteModal().show}
|
||||||
|
@ -433,6 +495,33 @@ const InvitesRoute: Component<InvitesRouteProps> = (props) => {
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</Modal>
|
</Modal>
|
||||||
|
|
||||||
|
{/* Модальное окно подтверждения пакетного удаления */}
|
||||||
|
<Modal
|
||||||
|
isOpen={batchDeleteModal().show}
|
||||||
|
onClose={() => setBatchDeleteModal({ show: false })}
|
||||||
|
title="Подтверждение пакетного удаления"
|
||||||
|
size="small"
|
||||||
|
>
|
||||||
|
<div class={styles.deleteConfirmation}>
|
||||||
|
<p>
|
||||||
|
Вы действительно хотите удалить <strong>{getSelectedCount()}</strong> выбранных приглашений?
|
||||||
|
<br />
|
||||||
|
Это действие нельзя отменить.
|
||||||
|
</p>
|
||||||
|
<div class={styles.modalActions}>
|
||||||
|
<Button variant="secondary" onClick={() => setBatchDeleteModal({ show: false })}>
|
||||||
|
Отмена
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
variant="danger"
|
||||||
|
onClick={deleteSelectedInvites}
|
||||||
|
>
|
||||||
|
Удалить выбранные
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Modal>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
@ -207,3 +207,65 @@
|
||||||
.delete-button:active {
|
.delete-button:active {
|
||||||
transform: scale(0.95);
|
transform: scale(0.95);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* Стили для чекбоксов и пакетного удаления */
|
||||||
|
.checkbox-column {
|
||||||
|
width: 40px;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.checkbox {
|
||||||
|
cursor: pointer;
|
||||||
|
width: 18px;
|
||||||
|
height: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-actions {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.selected-count {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
font-size: 14px;
|
||||||
|
color: #666;
|
||||||
|
margin-right: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-all-container {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
margin-right: 15px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.select-all-label {
|
||||||
|
margin-left: 5px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Кнопка пакетного удаления */
|
||||||
|
.batch-delete-button {
|
||||||
|
background-color: #dc3545;
|
||||||
|
color: white;
|
||||||
|
border: none;
|
||||||
|
padding: 6px 12px;
|
||||||
|
border-radius: 4px;
|
||||||
|
font-size: 14px;
|
||||||
|
cursor: pointer;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 5px;
|
||||||
|
transition: background-color 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-delete-button:hover {
|
||||||
|
background-color: #c82333;
|
||||||
|
}
|
||||||
|
|
||||||
|
.batch-delete-button:disabled {
|
||||||
|
background-color: #e9a8ae;
|
||||||
|
cursor: not-allowed;
|
||||||
|
}
|
||||||
|
|
|
@ -991,3 +991,71 @@ async def admin_delete_invite(
|
||||||
logger.error(f"Ошибка при удалении приглашения: {e!s}")
|
logger.error(f"Ошибка при удалении приглашения: {e!s}")
|
||||||
msg = f"Не удалось удалить приглашение: {e!s}"
|
msg = f"Не удалось удалить приглашение: {e!s}"
|
||||||
raise GraphQLError(msg) from e
|
raise GraphQLError(msg) from e
|
||||||
|
|
||||||
|
|
||||||
|
@mutation.field("adminDeleteInvitesBatch")
|
||||||
|
@admin_auth_required
|
||||||
|
async def admin_delete_invites_batch(
|
||||||
|
_: None, _info: GraphQLResolveInfo, invites: list[dict[str, Any]]
|
||||||
|
) -> dict[str, Any]:
|
||||||
|
"""
|
||||||
|
Пакетное удаление приглашений
|
||||||
|
|
||||||
|
Args:
|
||||||
|
_info: Контекст GraphQL запроса
|
||||||
|
invites: Список приглашений для удаления (каждое содержит inviter_id, author_id, shout_id)
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Результат операции
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
if not invites:
|
||||||
|
return {"success": False, "error": "Список приглашений для удаления пуст"}
|
||||||
|
|
||||||
|
deleted_count = 0
|
||||||
|
errors = []
|
||||||
|
|
||||||
|
with local_session() as session:
|
||||||
|
for invite_data in invites:
|
||||||
|
inviter_id = invite_data.get("inviter_id")
|
||||||
|
author_id = invite_data.get("author_id")
|
||||||
|
shout_id = invite_data.get("shout_id")
|
||||||
|
|
||||||
|
if not all([inviter_id, author_id, shout_id]):
|
||||||
|
errors.append(f"Неполные данные для приглашения: {invite_data}")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Находим приглашение для удаления
|
||||||
|
invite = (
|
||||||
|
session.query(Invite)
|
||||||
|
.filter(
|
||||||
|
Invite.inviter_id == inviter_id,
|
||||||
|
Invite.author_id == author_id,
|
||||||
|
Invite.shout_id == shout_id,
|
||||||
|
)
|
||||||
|
.first()
|
||||||
|
)
|
||||||
|
|
||||||
|
if not invite:
|
||||||
|
errors.append(f"Приглашение с ID {inviter_id}-{author_id}-{shout_id} не найдено")
|
||||||
|
continue
|
||||||
|
|
||||||
|
# Удаляем приглашение
|
||||||
|
session.delete(invite)
|
||||||
|
deleted_count += 1
|
||||||
|
|
||||||
|
# Сохраняем все изменения за раз
|
||||||
|
if deleted_count > 0:
|
||||||
|
session.commit()
|
||||||
|
logger.info(f"Пакетное удаление: удалено {deleted_count} приглашений")
|
||||||
|
|
||||||
|
if errors:
|
||||||
|
error_message = f"Удалено {deleted_count} из {len(invites)} приглашений. Ошибки: {', '.join(errors)}"
|
||||||
|
return {"success": deleted_count > 0, "error": error_message}
|
||||||
|
|
||||||
|
return {"success": True, "error": None}
|
||||||
|
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Ошибка при пакетном удалении приглашений: {e!s}")
|
||||||
|
msg = f"Не удалось удалить приглашения: {e!s}"
|
||||||
|
raise GraphQLError(msg) from e
|
||||||
|
|
|
@ -141,6 +141,13 @@ input AdminInviteUpdateInput {
|
||||||
status: InviteStatus!
|
status: InviteStatus!
|
||||||
}
|
}
|
||||||
|
|
||||||
|
# Входной тип для идентификации приглашения при пакетном удалении
|
||||||
|
input AdminInviteIdInput {
|
||||||
|
inviter_id: Int!
|
||||||
|
author_id: Int!
|
||||||
|
shout_id: Int!
|
||||||
|
}
|
||||||
|
|
||||||
extend type Query {
|
extend type Query {
|
||||||
getEnvVariables: [EnvSection!]!
|
getEnvVariables: [EnvSection!]!
|
||||||
# Запросы для управления пользователями
|
# Запросы для управления пользователями
|
||||||
|
@ -166,4 +173,5 @@ extend type Mutation {
|
||||||
adminCreateInvite(invite: AdminInviteUpdateInput!): OperationResult!
|
adminCreateInvite(invite: AdminInviteUpdateInput!): OperationResult!
|
||||||
adminUpdateInvite(invite: AdminInviteUpdateInput!): OperationResult!
|
adminUpdateInvite(invite: AdminInviteUpdateInput!): OperationResult!
|
||||||
adminDeleteInvite(inviter_id: Int!, author_id: Int!, shout_id: Int!): OperationResult!
|
adminDeleteInvite(inviter_id: Int!, author_id: Int!, shout_id: Int!): OperationResult!
|
||||||
|
adminDeleteInvitesBatch(invites: [AdminInviteIdInput!]!): OperationResult!
|
||||||
}
|
}
|
||||||
|
|
Loading…
Reference in New Issue
Block a user