diff --git a/client/src/modules/AutoTest/components/AutoTestForm/AutoTestForm.tsx b/client/src/modules/AutoTest/components/AutoTestForm/AutoTestForm.tsx new file mode 100644 index 000000000..c33f16789 --- /dev/null +++ b/client/src/modules/AutoTest/components/AutoTestForm/AutoTestForm.tsx @@ -0,0 +1,132 @@ +import { Button, Form, InputNumber, Card, Row, Col, Switch, Space } from 'antd'; +import { useMemo } from 'react'; +import { AutoTestTaskDto } from 'api'; +import { QuestionEditor } from './QuestionEditor'; +import { EditAutoTestFormData } from './data/types'; + +type EditTaskFormProps = { + selectedTask: AutoTestTaskDto; + onCancel: () => void; + onSave: (updatedTask: AutoTestTaskDto) => void; +}; + +export default function EditTaskForm({ selectedTask, onCancel, onSave }: EditTaskFormProps) { + const [form] = Form.useForm(); + const initialFormValues = useMemo(() => { + const pub = selectedTask.attributes.public; + const processedQuestions = pub.questions.map((q, idx) => { + if (q.multiple) { + return { + question: q.question, + questionImage: q.questionImage || '', + answersType: q.answersType || 'text', + multiple: q.multiple, + answers: q.answers.map((ans: string, aIdx: number) => ({ + text: ans, + correct: selectedTask.attributes.answers?.[idx]?.includes(aIdx) || false, + })), + }; + } else { + return { + question: q.question, + questionImage: q.questionImage || '', + answersType: q.answersType || 'text', + multiple: q.multiple, + answers: q.answers.map((ans: string) => ({ text: ans })), + correctIndex: selectedTask.attributes.answers?.[idx]?.[0], + }; + } + }); + return { ...pub, questions: processedQuestions }; + }, [selectedTask]); + + const handleSave = (values: any) => { + const { questions, ...rest } = values; + const updatedQuestions = questions.map((q: any) => ({ + question: q.question, + questionImage: q.questionImage, + answersType: q.answersType, + multiple: q.multiple, + answers: q.answers.map((a: any) => a.text), + })); + const updatedAnswers = questions.map((q: any) => { + if (q.multiple) { + return q.answers.reduce((acc: number[], a: any, idx: number) => (a.correct ? [...acc, idx] : acc), []); + } else { + return typeof q.correctIndex === 'number' ? [q.correctIndex] : []; + } + }); + onSave({ + ...selectedTask, + attributes: { public: { ...rest, questions: updatedQuestions }, answers: updatedAnswers }, + }); + }; + + return ( + +
+ + + + {[ + { + label: 'Max Attempts', + name: 'maxAttemptsNumber', + component: , + }, + { + label: 'Number of Questions', + name: 'numberOfQuestions', + component: , + }, + { + label: 'Threshold (%)', + name: 'tresholdPercentage', + component: , + }, + { label: 'Strict Mode', name: 'strictAttemptsMode', component: , valuePropName: 'checked' }, + ].map(({ label, name, component, valuePropName }) => ( + + + {component} + + + ))} + + + + + {(fields, { add, remove }) => ( + <> + {fields.map(field => ( + + ))} + + + + + )} + + + + + + + + + + + +
+ +
+ ); +} diff --git a/client/src/modules/AutoTest/components/AutoTestForm/QuestionEditor.tsx b/client/src/modules/AutoTest/components/AutoTestForm/QuestionEditor.tsx new file mode 100644 index 000000000..a4331e682 --- /dev/null +++ b/client/src/modules/AutoTest/components/AutoTestForm/QuestionEditor.tsx @@ -0,0 +1,100 @@ +import { Button, Card, Checkbox, Col, Form, FormInstance, Input, Radio, Row, Select } from 'antd'; +import DeleteOutlined from '@ant-design/icons/DeleteOutlined'; +import { FormListFieldData } from 'antd/lib'; +import { EditAutoTestFormData } from './data/types'; + +type QuestionEditorProps = { + field: FormListFieldData; + form: FormInstance; + removeQuestion: (name: number) => void; +}; + +const { Option } = Select; + +export function QuestionEditor({ field, form, removeQuestion }: QuestionEditorProps) { + const { key, name, ...restField } = field; + const isMultiple = Form.useWatch(['questions', name, 'multiple'], form); + const answersList = Form.useWatch(['questions', name, 'answers'], form) || []; + return ( + + + + + + + + + + Multiple Choice + + + + + + + + + + + + + + + + + {(subFields, { add: addAnswer, remove: removeAnswer }) => ( + <> + {subFields.map(({ key: subKey, name: subName, ...subRestField }) => ( + + {isMultiple && ( + + + + + + )} + + + + + + + + + + )} + + {!isMultiple && ( + + + {answersList.map((_: any, index: number) => ( + {`Answer ${index + 1}`} + ))} + + + )} + + + ); +} diff --git a/client/src/modules/AutoTest/components/AutoTestForm/data/types.ts b/client/src/modules/AutoTest/components/AutoTestForm/data/types.ts new file mode 100644 index 000000000..9c00cdb3b --- /dev/null +++ b/client/src/modules/AutoTest/components/AutoTestForm/data/types.ts @@ -0,0 +1,21 @@ +export interface AnswerFormData { + text: string; + correct?: boolean; +} + +export interface QuestionFormData { + question: string; + questionImage: string; + answersType: 'text' | 'image'; + multiple: boolean; + answers: AnswerFormData[]; + correctIndex?: number; +} + +export interface EditAutoTestFormData { + maxAttemptsNumber: number; + numberOfQuestions: number; + tresholdPercentage: number; + strictAttemptsMode: boolean; + questions: QuestionFormData[]; +} diff --git a/client/src/pages/admin/auto-test-task/[taskId].tsx b/client/src/pages/admin/auto-test-task/[taskId].tsx index 29bab9d9b..6ba1772dd 100644 --- a/client/src/pages/admin/auto-test-task/[taskId].tsx +++ b/client/src/pages/admin/auto-test-task/[taskId].tsx @@ -1,5 +1,6 @@ -import { Descriptions, Divider, Form, Space, Switch, Tag, Typography } from 'antd'; -import { AutoTestTaskDto, AutoTestsApi, SelfEducationQuestionSelectedAnswersDto } from 'api'; +import { useState } from 'react'; +import { Button, Descriptions, Divider, Form, message, Space, Switch, Tag, Typography } from 'antd'; +import { AutoTestTaskDto, AutoTestsApi, SelfEducationQuestionSelectedAnswersDto, TasksApi } from 'api'; import { AdminPageLayout } from 'components/PageLayout'; import { ActiveCourseProvider, SessionProvider, useActiveCourseContext } from 'modules/Course/contexts'; import { CourseRole } from 'services/models'; @@ -7,6 +8,7 @@ import { GetServerSideProps } from 'next'; import { getTokenFromContext } from 'utils/server'; import { getApiConfiguration } from 'utils/axios'; import { Question } from 'modules/AutoTest/components'; +import AutoTestForm from 'modules/AutoTest/components/AutoTestForm/AutoTestForm'; type PageProps = { selectedTask: AutoTestTaskDto; @@ -25,111 +27,134 @@ export const getServerSideProps: GetServerSideProps = async context => { const selectedTask = await api.getAutoTest(Number(taskId)); if (!selectedTask.data) { - return { - props: { - notFound: true, - }, - }; + return { props: { notFound: true } }; } - return { - props: { selectedTask: selectedTask.data }, - }; + return { props: { selectedTask: selectedTask.data } }; } catch { - return { - notFound: true, - }; + return { notFound: true }; } }; +const taskApi = new TasksApi(); + function Page({ selectedTask }: PageProps) { const { courses } = useActiveCourseContext(); + const [task, setTask] = useState(selectedTask); + const [isEditing, setIsEditing] = useState(false); - return ( - - - {selectedTask?.name} - {selectedTask?.descriptionUrl && ( - - - {selectedTask?.descriptionUrl} - - - )} - {selectedTask?.discipline?.name && ( - {selectedTask?.discipline?.name} - )} - {selectedTask?.courses?.length && selectedTask?.courses?.length > 0 && ( - - - {selectedTask.courses.map(course => ( - - {course.name} - - ))} - - - )} - - {selectedTask?.tags && ( - - {selectedTask?.tags.map(tag => ( - - {tag} - - ))} - - )} - + const handleSaveTask = async (updatedTask: AutoTestTaskDto) => { + try { + await taskApi.updateTask(updatedTask.id, { + name: selectedTask.name, + descriptionUrl: selectedTask.descriptionUrl, + disciplineId: selectedTask.discipline?.id, + tags: selectedTask.tags, + attributes: updatedTask.attributes, + description: selectedTask.description, + githubRepoName: selectedTask.githubRepoName, + sourceGithubRepoUrl: selectedTask.sourceGithubRepoUrl, + githubPrRequired: selectedTask.githubPrRequired, + type: selectedTask.type, + skills: selectedTask.skills, + }); + setTask(updatedTask); + setIsEditing(false); + } catch { + message.error('Failed to update auto test'); + } + }; - {selectedTask?.attributes?.public?.questions && ( + return ( + + {isEditing ? ( + setIsEditing(false)} onSave={handleSaveTask} /> + ) : ( <> - - - - {selectedTask?.attributes?.public?.maxAttemptsNumber} - - - {selectedTask?.attributes?.public?.numberOfQuestions} - - - {selectedTask?.attributes?.public?.questions?.length} - - - - - - {selectedTask?.attributes?.public?.tresholdPercentage} - + + + + + {task.name} + {task?.descriptionUrl && ( + + + {task?.descriptionUrl} + + + )} + {task?.discipline?.name && ( + {task?.discipline?.name} + )} + {task?.courses?.length && task?.courses?.length > 0 && ( + + + {task.courses.map(course => ( + + {course.name} + + ))} + + + )} + {task?.tags && ( + + {task?.tags.map(tag => ( + + {tag} + + ))} + + )} -
- {selectedTask?.attributes.public.questions.map((question, index) => ( - - ))} - + {task?.attributes?.public?.questions && ( + <> + + + + {task.attributes.public.maxAttemptsNumber} + + + {task.attributes.public.numberOfQuestions} + + {task.attributes.public.questions.length} + + + + + {task.attributes.public.tresholdPercentage} + + +
+ {task.attributes.public.questions.map((question, index) => ( + + ))} + + + )} )}