feat: Sprint 3 — test editing with versioning
Backend:
- migration 003: add parent_id to tests table
- PUT /api/tests/{id}: edit in place if no attempts, create new version otherwise
- GET /api/tests: show only latest versions (no successor)
Frontend:
- TestForm: extracted reusable form component
- TestCreate: refactored to use TestForm
- TestEdit: full edit mode with pre-populated form, version redirect on new version
Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -5,29 +5,91 @@ import {
|
||||
EditOutlined,
|
||||
PlayCircleOutlined,
|
||||
} from '@ant-design/icons'
|
||||
import { useQuery } from '@tanstack/react-query'
|
||||
import { Alert, Button, Card, Descriptions, List, Space, Spin, Tag, Typography } from 'antd'
|
||||
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
|
||||
import { Alert, Button, Card, Descriptions, List, Space, Spin, Tag, Typography, message } from 'antd'
|
||||
import { useState } from 'react'
|
||||
import { useNavigate, useParams } from 'react-router-dom'
|
||||
|
||||
import { Answer, testsApi } from '../../api/tests'
|
||||
import { Answer, CreateTestDto, testsApi } from '../../api/tests'
|
||||
import TestForm, { TestFormValues } from '../../components/TestForm'
|
||||
|
||||
const { Title, Text } = Typography
|
||||
|
||||
export default function TestEdit() {
|
||||
const { id } = useParams<{ id: string }>()
|
||||
const navigate = useNavigate()
|
||||
const queryClient = useQueryClient()
|
||||
const [editMode, setEditMode] = useState(false)
|
||||
|
||||
const { data: test, isLoading } = useQuery({
|
||||
queryKey: ['tests', id],
|
||||
queryFn: () => testsApi.get(Number(id)).then((r) => r.data),
|
||||
})
|
||||
|
||||
const { mutate: updateTest, isPending } = useMutation({
|
||||
mutationFn: (data: CreateTestDto) => testsApi.update(Number(id), data).then((r) => r.data),
|
||||
onSuccess: (result) => {
|
||||
queryClient.invalidateQueries({ queryKey: ['tests'] })
|
||||
if (result.is_new_version) {
|
||||
message.success(`Создана новая версия теста (v${result.test.version})`)
|
||||
navigate(`/tests/${result.test.id}/edit`)
|
||||
} else {
|
||||
message.success('Тест обновлён')
|
||||
queryClient.invalidateQueries({ queryKey: ['tests', id] })
|
||||
setEditMode(false)
|
||||
}
|
||||
},
|
||||
onError: (error: unknown) => {
|
||||
const err = error as { response?: { data?: { detail?: string } } }
|
||||
message.error(err.response?.data?.detail || 'Ошибка при сохранении теста')
|
||||
},
|
||||
})
|
||||
|
||||
if (isLoading) {
|
||||
return <Spin size="large" style={{ display: 'block', margin: '48px auto' }} />
|
||||
}
|
||||
|
||||
if (!test) return null
|
||||
|
||||
// Режим редактирования — показываем форму с предзаполненными данными
|
||||
if (editMode) {
|
||||
const initialValues: TestFormValues = {
|
||||
title: test.title,
|
||||
description: test.description ?? undefined,
|
||||
passing_score: test.passing_score,
|
||||
has_timer: test.time_limit !== null,
|
||||
time_limit: test.time_limit ?? undefined,
|
||||
allow_navigation_back: test.allow_navigation_back,
|
||||
questions: test.questions.map((q) => ({
|
||||
text: q.text,
|
||||
answers: q.answers.map((a) => ({ text: a.text, is_correct: a.is_correct })),
|
||||
})),
|
||||
}
|
||||
|
||||
const onSubmit = (values: TestFormValues) => {
|
||||
updateTest({
|
||||
title: values.title,
|
||||
description: values.description,
|
||||
passing_score: values.passing_score,
|
||||
time_limit: values.has_timer ? values.time_limit : undefined,
|
||||
allow_navigation_back: values.allow_navigation_back ?? true,
|
||||
questions: values.questions,
|
||||
})
|
||||
}
|
||||
|
||||
return (
|
||||
<TestForm
|
||||
heading={`Редактирование теста — v${test.version}`}
|
||||
initialValues={initialValues}
|
||||
onSubmit={onSubmit}
|
||||
isPending={isPending}
|
||||
submitLabel="Сохранить"
|
||||
onCancel={() => setEditMode(false)}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
// Режим просмотра (вид автора)
|
||||
return (
|
||||
<div style={{ maxWidth: 820, margin: '0 auto', padding: 24 }}>
|
||||
<Space style={{ width: '100%', justifyContent: 'space-between', marginBottom: 16 }}>
|
||||
@@ -37,8 +99,7 @@ export default function TestEdit() {
|
||||
<Space>
|
||||
<Button
|
||||
icon={<EditOutlined />}
|
||||
disabled
|
||||
title="Редактирование будет доступно в следующем спринте"
|
||||
onClick={() => setEditMode(true)}
|
||||
>
|
||||
Редактировать
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user