Дорабоки интерфейса системы тестирования. Раздел 1 Шапка+Верхний brick

This commit is contained in:
Константин Лебединский
2026-04-29 14:55:43 +05:00
parent 1c4dacbc85
commit eff3fda5b0
34 changed files with 3339 additions and 576 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 52 KiB

+129 -8
View File
@@ -1,4 +1,4 @@
import { useEffect, useState } from 'react';
import { useEffect, useMemo, useState } from 'react';
import { useNavigate, useParams, Link, useOutletContext } from 'react-router-dom';
import { api } from '../api';
import { formatTestAuthorLabel } from '../utils/formatUserName';
@@ -70,6 +70,41 @@ function mapEditorToDraftQuestions(ed) {
}));
}
function buildDraftSnapshot({ title, description, passing, questions }) {
return JSON.stringify({
title: title ?? '',
description: description ?? '',
passing: String(passing ?? ''),
questions: (questions || []).map((q) => ({
text: q.text ?? '',
hasMultipleAnswers: !!q.hasMultipleAnswers,
options: (q.options || []).map((o) => ({
text: o.text ?? '',
isCorrect: !!o.isCorrect,
})),
})),
});
}
function postDebugLog({ runId, hypothesisId, location, message, data }) {
fetch('http://127.0.0.1:7419/ingest/a86fc408-7178-4abe-8dd9-f3e6bfb05d76', {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Debug-Session-Id': '034e19',
},
body: JSON.stringify({
sessionId: '034e19',
runId,
hypothesisId,
location,
message,
data,
timestamp: Date.now(),
}),
}).catch(() => {});
}
export default function TestDetail() {
const { id } = useParams();
const nav = useNavigate();
@@ -83,6 +118,7 @@ export default function TestDetail() {
const [draftDescription, setDraftDescription] = useState('');
const [draftPassing, setDraftPassing] = useState('70');
const [draftQuestions, setDraftQuestions] = useState(() => [createEmptyQuestion()]);
const [draftSnapshotOnLoad, setDraftSnapshotOnLoad] = useState(null);
const [draftStatus, setDraftStatus] = useState('');
const [deactivateBusy, setDeactivateBusy] = useState(false);
const [importPreview, setImportPreview] = useState(null);
@@ -106,6 +142,16 @@ export default function TestDetail() {
const [assignLoadBusy, setAssignLoadBusy] = useState(false);
const [attemptsList, setAttemptsList] = useState(undefined);
const [attemptsErr, setAttemptsErr] = useState(null);
const debugRunId = 'pre-fix';
// #region agent log
postDebugLog({
runId: debugRunId,
hypothesisId: 'H1',
location: 'frontend/src/pages/TestDetail.jsx:component-start',
message: 'TestDetail render start',
data: { hasData: !!data, hasTaker: !!taker, hasErr: !!err, testId: id != null },
});
// #endregion
async function load() {
setErr(null);
@@ -127,14 +173,26 @@ export default function TestDetail() {
setData(v);
setChain(c);
if (ed?.test) {
setDraftTitle(ed.test.title || '');
setAiGenTopic((ed.test.title || '').trim());
setDraftDescription(ed.test.description || '');
const loadedTitle = ed.test.title || '';
const loadedDescription = ed.test.description || '';
const th = ed.test.passingThreshold;
setDraftPassing(
th !== undefined && th !== null && String(th) !== '' ? String(th) : '70'
const loadedPassing =
th !== undefined && th !== null && String(th) !== '' ? String(th) : '70';
const loadedQuestions = mapEditorToDraftQuestions(ed);
setDraftSnapshotOnLoad(
buildDraftSnapshot({
title: loadedTitle,
description: loadedDescription,
passing: loadedPassing,
questions: loadedQuestions,
})
);
setDraftQuestions(mapEditorToDraftQuestions(ed));
setDraftTitle(loadedTitle);
setAiGenTopic(loadedTitle.trim());
setDraftDescription(loadedDescription);
setDraftPassing(loadedPassing);
setDraftQuestions(loadedQuestions);
}
} catch (e) {
if (e.status === 401) {
@@ -590,13 +648,40 @@ export default function TestDetail() {
}
if (err) {
// #region agent log
postDebugLog({
runId: debugRunId,
hypothesisId: 'H3',
location: 'frontend/src/pages/TestDetail.jsx:return-err',
message: 'Returning error branch before memo hook',
data: { hasErr: true },
});
// #endregion
return <p className="error-text">{err}</p>;
}
if (!data && !taker) {
// #region agent log
postDebugLog({
runId: debugRunId,
hypothesisId: 'H1',
location: 'frontend/src/pages/TestDetail.jsx:return-loading',
message: 'Returning loading branch before memo hook',
data: { hasData: false, hasTaker: false },
});
// #endregion
return <p className="text-muted">Загрузка</p>;
}
if (taker) {
// #region agent log
postDebugLog({
runId: debugRunId,
hypothesisId: 'H1',
location: 'frontend/src/pages/TestDetail.jsx:return-taker',
message: 'Returning taker branch before memo hook',
data: { hasTaker: true },
});
// #endregion
const { test: t, hasActiveVersion } = taker.summary;
const title = t?.title || 'Тест';
return (
@@ -635,6 +720,42 @@ export default function TestDetail() {
const assignSelectedInList = assignPeople.filter((p) =>
assignSelected.has(assignPersonKey(p))
);
// #region agent log
postDebugLog({
runId: debugRunId,
hypothesisId: 'H1',
location: 'frontend/src/pages/TestDetail.jsx:before-useMemo',
message: 'Reached line right before hasDraftChanges useMemo',
data: { hasData: !!data, hasTaker: !!taker },
});
// #endregion
const hasDraftChanges = useMemo(() => {
// #region agent log
postDebugLog({
runId: debugRunId,
hypothesisId: 'H2',
location: 'frontend/src/pages/TestDetail.jsx:inside-useMemo',
message: 'Computing hasDraftChanges',
data: {
hasDraftSnapshotOnLoad: !!draftSnapshotOnLoad,
titleLen: (draftTitle || '').length,
descriptionLen: (draftDescription || '').length,
passing: String(draftPassing || ''),
questionsCount: Array.isArray(draftQuestions) ? draftQuestions.length : -1,
},
});
// #endregion
if (!draftSnapshotOnLoad) {
return false;
}
const currentSnapshot = buildDraftSnapshot({
title: draftTitle,
description: draftDescription,
passing: draftPassing,
questions: draftQuestions,
});
return currentSnapshot !== draftSnapshotOnLoad;
}, [draftDescription, draftPassing, draftQuestions, draftSnapshotOnLoad, draftTitle]);
return (
<div className="test-detail-page test-detail-page--with-fixed-actions">
@@ -667,7 +788,7 @@ export default function TestDetail() {
</div>
)}
{chain?.hasAnyAttempt && (
{chain?.hasAnyAttempt && hasDraftChanges && (
<div className="callout callout--warning" role="status" style={{ marginTop: '0.75rem' }}>
При сохранении будет создана новая версия теста.
</div>