diff --git a/bro.config.js b/bro.config.js index 2690651..98fc6ca 100644 --- a/bro.config.js +++ b/bro.config.js @@ -18,7 +18,7 @@ module.exports = { /* use https://admin.bro-js.ru/ to create config, navigations and features */ navigations: { 'challenge-admin.main': '/challenge-admin', - 'link.challenge': '/challenge', + 'link.challenge.main': '/challenge', }, features: { 'challenge-admin': { diff --git a/locales/en.json b/locales/en.json index 81421b8..a1e1ebc 100644 --- a/locales/en.json +++ b/locales/en.json @@ -112,6 +112,21 @@ "challenge.admin.chains.delete.confirm.title": "Delete chain", "challenge.admin.chains.delete.confirm.message": "Are you sure you want to delete chain \"{name}\"? This action cannot be undone.", "challenge.admin.chains.delete.confirm.button": "Delete", + "challenge.admin.chains.duplicate.button": "Duplicate", + "challenge.admin.chains.duplicate.dialog.title": "Duplicate chain", + "challenge.admin.chains.duplicate.dialog.description": "Create a copy of chain \"{name}\" with the same tasks. The new chain will be created as inactive.", + "challenge.admin.chains.duplicate.dialog.field.name": "New chain name", + "challenge.admin.chains.duplicate.dialog.field.name.placeholder": "Copy - {name}", + "challenge.admin.chains.duplicate.dialog.field.name.helper": "Leave empty for auto-generated name", + "challenge.admin.chains.duplicate.dialog.button.confirm": "Create copy", + "challenge.admin.chains.duplicate.success": "Chain successfully duplicated", + "challenge.admin.chains.duplicate.error": "Failed to duplicate chain", + "challenge.admin.chains.clear.submissions.button": "Clear submissions", + "challenge.admin.chains.clear.submissions.dialog.title": "Clear chain submissions", + "challenge.admin.chains.clear.submissions.dialog.message": "Are you sure you want to delete all submissions for chain \"{name}\"? This action is irreversible. All deleted submissions cannot be restored.", + "challenge.admin.chains.clear.submissions.dialog.button.confirm": "Delete all submissions", + "challenge.admin.chains.clear.submissions.success": "Submissions successfully deleted", + "challenge.admin.chains.clear.submissions.error": "Failed to delete submissions", "challenge.admin.dashboard.title": "Dashboard", "challenge.admin.dashboard.loading": "Loading statistics...", "challenge.admin.dashboard.load.error": "Failed to load system statistics", diff --git a/locales/ru.json b/locales/ru.json index 50959d0..d3e9a39 100644 --- a/locales/ru.json +++ b/locales/ru.json @@ -111,6 +111,21 @@ "challenge.admin.chains.delete.confirm.title": "Удалить цепочку", "challenge.admin.chains.delete.confirm.message": "Вы уверены, что хотите удалить цепочку \"{name}\"? Это действие нельзя отменить.", "challenge.admin.chains.delete.confirm.button": "Удалить", + "challenge.admin.chains.duplicate.button": "Дублировать", + "challenge.admin.chains.duplicate.dialog.title": "Дублировать цепочку", + "challenge.admin.chains.duplicate.dialog.description": "Создать копию цепочки \"{name}\" с теми же заданиями. Новая цепочка будет создана неактивной.", + "challenge.admin.chains.duplicate.dialog.field.name": "Название новой цепочки", + "challenge.admin.chains.duplicate.dialog.field.name.placeholder": "Копия - {name}", + "challenge.admin.chains.duplicate.dialog.field.name.helper": "Оставьте пустым для автоматического названия", + "challenge.admin.chains.duplicate.dialog.button.confirm": "Создать копию", + "challenge.admin.chains.duplicate.success": "Цепочка успешно скопирована", + "challenge.admin.chains.duplicate.error": "Не удалось скопировать цепочку", + "challenge.admin.chains.clear.submissions.button": "Очистить попытки", + "challenge.admin.chains.clear.submissions.dialog.title": "Очистить попытки по цепочке", + "challenge.admin.chains.clear.submissions.dialog.message": "Вы уверены, что хотите удалить все попытки по цепочке \"{name}\"? Это действие необратимо. Все удаленные попытки невозможно восстановить.", + "challenge.admin.chains.clear.submissions.dialog.button.confirm": "Удалить все попытки", + "challenge.admin.chains.clear.submissions.success": "Попытки успешно удалены", + "challenge.admin.chains.clear.submissions.error": "Не удалось удалить попытки", "challenge.admin.dashboard.title": "Dashboard", "challenge.admin.dashboard.loading": "Загрузка статистики...", "challenge.admin.dashboard.load.error": "Не удалось загрузить статистику системы", diff --git a/src/__data__/api/api.ts b/src/__data__/api/api.ts index b03e42b..10a5ee3 100644 --- a/src/__data__/api/api.ts +++ b/src/__data__/api/api.ts @@ -13,6 +13,8 @@ import type { UpdateTaskRequest, CreateChainRequest, UpdateChainRequest, + DuplicateChainRequest, + ClearSubmissionsResponse, SubmitRequest, TestSubmissionResult, ChainSubmissionsResponse, @@ -115,6 +117,23 @@ export const api = createApi({ }), invalidatesTags: ['Chain'], }), + duplicateChain: builder.mutation({ + query: ({ chainId, name }) => ({ + url: `/challenge/chain/${chainId}/duplicate`, + method: 'POST', + body: name ? { name } : {}, + }), + transformResponse: (response: { body: ChallengeChain }) => response.body, + invalidatesTags: ['Chain'], + }), + clearChainSubmissions: builder.mutation({ + query: (chainId) => ({ + url: `/challenge/chain/${chainId}/submissions`, + method: 'DELETE', + }), + transformResponse: (response: { body: ClearSubmissionsResponse }) => response.body, + invalidatesTags: ['Chain', 'Submission'], + }), // Statistics getSystemStats: builder.query({ @@ -187,6 +206,8 @@ export const { useCreateChainMutation, useUpdateChainMutation, useDeleteChainMutation, + useDuplicateChainMutation, + useClearChainSubmissionsMutation, useGetSystemStatsQuery, useGetSystemStatsV2Query, useGetUserStatsQuery, diff --git a/src/components/ClearSubmissionsDialog.tsx b/src/components/ClearSubmissionsDialog.tsx new file mode 100644 index 0000000..354db5f --- /dev/null +++ b/src/components/ClearSubmissionsDialog.tsx @@ -0,0 +1,77 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import { + DialogRoot, + DialogContent, + DialogHeader, + DialogTitle, + DialogBody, + DialogFooter, + DialogActionTrigger, + Button, + Text, +} from '@chakra-ui/react' +import { useClearChainSubmissionsMutation } from '../__data__/api/api' +import { toaster } from './ui/toaster' +import type { ChallengeChain } from '../types/challenge' + +interface ClearSubmissionsDialogProps { + isOpen: boolean + onClose: () => void + chain: ChallengeChain | null +} + +export const ClearSubmissionsDialog: React.FC = ({ + isOpen, + onClose, + chain, +}) => { + const { t } = useTranslation() + const [clearSubmissions, { isLoading }] = useClearChainSubmissionsMutation() + + const handleConfirm = async () => { + if (!chain) return + + try { + await clearSubmissions(chain.id).unwrap() + toaster.create({ + title: t('challenge.admin.common.success'), + description: t('challenge.admin.chains.clear.submissions.success'), + type: 'success', + }) + onClose() + } catch (err) { + toaster.create({ + title: t('challenge.admin.common.error'), + description: t('challenge.admin.chains.clear.submissions.error'), + type: 'error', + }) + } + } + + if (!chain) return null + + return ( + !e.open && onClose()}> + + + {t('challenge.admin.chains.clear.submissions.dialog.title')} + + + {t('challenge.admin.chains.clear.submissions.dialog.message', { name: chain.name })} + + + + + + + + + + ) +} + diff --git a/src/components/DuplicateChainDialog.tsx b/src/components/DuplicateChainDialog.tsx new file mode 100644 index 0000000..bd970df --- /dev/null +++ b/src/components/DuplicateChainDialog.tsx @@ -0,0 +1,106 @@ +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + DialogRoot, + DialogContent, + DialogHeader, + DialogTitle, + DialogBody, + DialogFooter, + DialogActionTrigger, + Button, + Field, + Input, + Text, + VStack, +} from '@chakra-ui/react' +import { useDuplicateChainMutation } from '../__data__/api/api' +import { toaster } from './ui/toaster' +import type { ChallengeChain } from '../types/challenge' + +interface DuplicateChainDialogProps { + isOpen: boolean + onClose: () => void + chain: ChallengeChain | null +} + +export const DuplicateChainDialog: React.FC = ({ + isOpen, + onClose, + chain, +}) => { + const { t } = useTranslation() + const [name, setName] = useState('') + const [duplicateChain, { isLoading }] = useDuplicateChainMutation() + + const handleClose = () => { + setName('') + onClose() + } + + const handleConfirm = async () => { + if (!chain) return + + try { + await duplicateChain({ + chainId: chain.id, + name: name.trim() || undefined, + }).unwrap() + toaster.create({ + title: t('challenge.admin.common.success'), + description: t('challenge.admin.chains.duplicate.success'), + type: 'success', + }) + handleClose() + } catch (err) { + toaster.create({ + title: t('challenge.admin.common.error'), + description: t('challenge.admin.chains.duplicate.error'), + type: 'error', + }) + } + } + + if (!chain) return null + + const defaultPlaceholder = t('challenge.admin.chains.duplicate.dialog.field.name.placeholder', { + name: chain.name, + }) + + return ( + !e.open && handleClose()}> + + + {t('challenge.admin.chains.duplicate.dialog.title')} + + + + {t('challenge.admin.chains.duplicate.dialog.description', { name: chain.name })} + + {t('challenge.admin.chains.duplicate.dialog.field.name')} + setName(e.target.value)} + placeholder={defaultPlaceholder} + /> + + {t('challenge.admin.chains.duplicate.dialog.field.name.helper')} + + + + + + + + + + + + + ) +} + diff --git a/src/pages/chains/ChainsListPage.tsx b/src/pages/chains/ChainsListPage.tsx index 933f1fe..3b67881 100644 --- a/src/pages/chains/ChainsListPage.tsx +++ b/src/pages/chains/ChainsListPage.tsx @@ -18,6 +18,8 @@ import { LoadingSpinner } from '../../components/LoadingSpinner' import { ErrorAlert } from '../../components/ErrorAlert' import { EmptyState } from '../../components/EmptyState' import { ConfirmDialog } from '../../components/ConfirmDialog' +import { DuplicateChainDialog } from '../../components/DuplicateChainDialog' +import { ClearSubmissionsDialog } from '../../components/ClearSubmissionsDialog' import type { ChallengeChain } from '../../types/challenge' import { toaster } from '../../components/ui/toaster' @@ -29,6 +31,8 @@ export const ChainsListPage: React.FC = () => { const [searchQuery, setSearchQuery] = useState('') const [chainToDelete, setChainToDelete] = useState(null) + const [chainToDuplicate, setChainToDuplicate] = useState(null) + const [chainToClearSubmissions, setChainToClearSubmissions] = useState(null) const [updatingChainId, setUpdatingChainId] = useState(null) const [updateChain] = useUpdateChainMutation() @@ -182,6 +186,21 @@ export const ChainsListPage: React.FC = () => { > {t('challenge.admin.chains.list.button.edit')} + +