feat(test detail): regroup sections, copy, and editor affordances
- AccSection: optional subtitle; disclosure body top padding - Renames: О тесте, Вопросы, История (Версии + Прохождения), Показ в каталоге - Move import into Вопросы as «Документ в вопросы»; remove duplicate import section - Merge publication + assignment with subheads Видимость / Кому выдать; non-full-width actions - Assign: «Выбрать всех (N)»; search row height tweaks - Editor: крестик удаления варианта; единый футер + вариант/удалить вопрос; + вопрос в отдельной полосе - AI block: test-detail-ai-panel (lighter, aligned with rest) - History: empty state for runs; attempts error only in section (remove hero duplicate) Made-with: Cursor
This commit is contained in:
+393
-323
@@ -34,13 +34,18 @@ function createEmptyQuestion() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* @param {{ title: string, defaultOpen?: boolean, id?: string, children: import('react').ReactNode }} p
|
* @param {{ title: string, subtitle?: string, defaultOpen?: boolean, id?: string, children: import('react').ReactNode }} p
|
||||||
*/
|
*/
|
||||||
function AccSection({ title, defaultOpen, id, children }) {
|
function AccSection({ title, subtitle, defaultOpen, id, children }) {
|
||||||
return (
|
return (
|
||||||
<div className="cabinet-brick">
|
<div className="cabinet-brick">
|
||||||
<details className="cabinet-disclosure" defaultOpen={defaultOpen} id={id || undefined}>
|
<details className="cabinet-disclosure" defaultOpen={defaultOpen} id={id || undefined}>
|
||||||
<summary className="cabinet-disclosure__summary font-headline">{title}</summary>
|
<summary className="cabinet-disclosure__summary">
|
||||||
|
<span className="cabinet-disclosure__summary-text">
|
||||||
|
<span className="cabinet-disclosure__summary-title font-headline">{title}</span>
|
||||||
|
{subtitle ? <span className="cabinet-disclosure__summary-sub">{subtitle}</span> : null}
|
||||||
|
</span>
|
||||||
|
</summary>
|
||||||
<div className="cabinet-disclosure__body">{children}</div>
|
<div className="cabinet-disclosure__body">{children}</div>
|
||||||
</details>
|
</details>
|
||||||
</div>
|
</div>
|
||||||
@@ -235,6 +240,16 @@ export default function TestDetail() {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function selectAllVisible() {
|
||||||
|
setAssignSelected((prev) => {
|
||||||
|
const next = new Set(prev);
|
||||||
|
for (const p of assignPeople) {
|
||||||
|
next.add(assignPersonKey(p));
|
||||||
|
}
|
||||||
|
return next;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
async function postAssign() {
|
async function postAssign() {
|
||||||
const selectedRows = assignPeople.filter((p) =>
|
const selectedRows = assignPeople.filter((p) =>
|
||||||
assignSelected.has(assignPersonKey(p))
|
assignSelected.has(assignPersonKey(p))
|
||||||
@@ -657,14 +672,13 @@ export default function TestDetail() {
|
|||||||
При сохранении будет создана новая версия теста.
|
При сохранении будет создана новая версия теста.
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{data && attemptsErr && (
|
|
||||||
<p className="error-text" style={{ marginTop: '0.75rem', marginBottom: 0 }} role="alert">
|
|
||||||
{attemptsErr}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<AccSection title="Метаинформация" defaultOpen>
|
<AccSection
|
||||||
|
title="О тесте"
|
||||||
|
subtitle="Название, описание и порог зачёта"
|
||||||
|
defaultOpen
|
||||||
|
>
|
||||||
<div className="draft-block">
|
<div className="draft-block">
|
||||||
<label className="form-label" htmlFor="draft-title">
|
<label className="form-label" htmlFor="draft-title">
|
||||||
Название
|
Название
|
||||||
@@ -702,17 +716,13 @@ export default function TestDetail() {
|
|||||||
</div>
|
</div>
|
||||||
</AccSection>
|
</AccSection>
|
||||||
|
|
||||||
<AccSection title="Содержание" defaultOpen>
|
<AccSection
|
||||||
|
title="Вопросы"
|
||||||
|
subtitle="Тексты, варианты и при необходимости загрузка из файла"
|
||||||
|
defaultOpen
|
||||||
|
>
|
||||||
<div className="draft-block">
|
<div className="draft-block">
|
||||||
<div
|
<div className="test-detail-ai-panel" aria-label="Параметры генерации теста с ИИ">
|
||||||
className="surface-card"
|
|
||||||
style={{
|
|
||||||
padding: '1rem 1.1rem',
|
|
||||||
marginBottom: '1.25rem',
|
|
||||||
borderLeft: '3px solid color-mix(in srgb, var(--primary) 45%, transparent)',
|
|
||||||
}}
|
|
||||||
aria-label="Параметры генерации теста с ИИ"
|
|
||||||
>
|
|
||||||
<p
|
<p
|
||||||
className="font-headline"
|
className="font-headline"
|
||||||
style={{ fontSize: '0.95rem', marginTop: 0, marginBottom: '0.65rem' }}
|
style={{ fontSize: '0.95rem', marginTop: 0, marginBottom: '0.65rem' }}
|
||||||
@@ -792,12 +802,7 @@ export default function TestDetail() {
|
|||||||
{draftQuestions.map((q, qi) => (
|
{draftQuestions.map((q, qi) => (
|
||||||
<div
|
<div
|
||||||
key={q.key}
|
key={q.key}
|
||||||
className="question-editor-block"
|
className={`question-editor-block${qi === 0 ? ' question-editor-block--first' : ''}`}
|
||||||
style={{
|
|
||||||
borderTop: '1px solid var(--border-subtle, #2a2a2a)',
|
|
||||||
marginTop: '1rem',
|
|
||||||
paddingTop: '1rem',
|
|
||||||
}}
|
|
||||||
>
|
>
|
||||||
<div className="question-editor-block__header">
|
<div className="question-editor-block__header">
|
||||||
<label
|
<label
|
||||||
@@ -848,11 +853,7 @@ export default function TestDetail() {
|
|||||||
</p>
|
</p>
|
||||||
<ul className="question-options-list" style={{ listStyle: 'none', padding: 0, margin: 0 }}>
|
<ul className="question-options-list" style={{ listStyle: 'none', padding: 0, margin: 0 }}>
|
||||||
{q.options.map((o, oi) => (
|
{q.options.map((o, oi) => (
|
||||||
<li
|
<li key={o.key} className="question-option-row">
|
||||||
key={o.key}
|
|
||||||
className="question-option-row"
|
|
||||||
style={{ display: 'flex', flexWrap: 'wrap', alignItems: 'center', gap: 8, marginBottom: 8 }}
|
|
||||||
>
|
|
||||||
<input
|
<input
|
||||||
key={`${o.key}-${q.hasMultipleAnswers ? 'm' : 's'}`}
|
key={`${o.key}-${q.hasMultipleAnswers ? 'm' : 's'}`}
|
||||||
className="question-option-row__mark"
|
className="question-option-row__mark"
|
||||||
@@ -886,8 +887,7 @@ export default function TestDetail() {
|
|||||||
aria-label="Пометить как верный"
|
aria-label="Пометить как верный"
|
||||||
/>
|
/>
|
||||||
<input
|
<input
|
||||||
className="form-input"
|
className="form-input question-option-row__text"
|
||||||
style={{ flex: '1 1 200px' }}
|
|
||||||
value={o.text}
|
value={o.text}
|
||||||
onChange={(e) => {
|
onChange={(e) => {
|
||||||
const v = e.target.value;
|
const v = e.target.value;
|
||||||
@@ -908,17 +908,25 @@ export default function TestDetail() {
|
|||||||
{q.options.length > 1 && (
|
{q.options.length > 1 && (
|
||||||
<button
|
<button
|
||||||
type="button"
|
type="button"
|
||||||
className="btn btn-ghost btn--sm"
|
className="question-option-remove"
|
||||||
onClick={() => removeOption(qi, oi)}
|
onClick={() => removeOption(qi, oi)}
|
||||||
|
title="Удалить вариант"
|
||||||
|
aria-label="Удалить вариант"
|
||||||
>
|
>
|
||||||
убрать
|
<span className="material-symbols-outlined" aria-hidden>
|
||||||
|
close
|
||||||
|
</span>
|
||||||
</button>
|
</button>
|
||||||
)}
|
)}
|
||||||
</li>
|
</li>
|
||||||
))}
|
))}
|
||||||
</ul>
|
</ul>
|
||||||
<div className="inline-actions" style={{ marginTop: 4 }}>
|
<div className="question-editor__footer">
|
||||||
<button type="button" className="btn btn-ghost btn--sm" onClick={() => addOption(qi)}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost btn--sm"
|
||||||
|
onClick={() => addOption(qi)}
|
||||||
|
>
|
||||||
+ вариант
|
+ вариант
|
||||||
</button>
|
</button>
|
||||||
{draftQuestions.length > 1 && (
|
{draftQuestions.length > 1 && (
|
||||||
@@ -933,11 +941,92 @@ export default function TestDetail() {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
))}
|
))}
|
||||||
<div className="inline-actions" style={{ marginTop: '0.75rem' }}>
|
<div className="test-detail-add-question">
|
||||||
<button type="button" className="btn btn-ghost" onClick={addQuestion}>
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost question-editor__add-question"
|
||||||
|
onClick={addQuestion}
|
||||||
|
>
|
||||||
+ вопрос
|
+ вопрос
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<div className="test-detail-subsection test-detail-subsection--import">
|
||||||
|
<h3 className="test-detail-subsection__title">Документ в вопросы</h3>
|
||||||
|
<p className="muted test-detail-hint" style={{ marginTop: 0 }}>
|
||||||
|
PDF, Word или текст — вставьте в черновик вопросов.
|
||||||
|
</p>
|
||||||
|
<div
|
||||||
|
className="import-file-row import-file-row--block"
|
||||||
|
style={{ marginBottom: importPreview || importErr ? '0.5rem' : 0, marginTop: 0 }}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
id="import-test-file"
|
||||||
|
className="import-file-input"
|
||||||
|
type="file"
|
||||||
|
accept=".pdf,.docx,.txt,.md,application/pdf"
|
||||||
|
onChange={onImportFile}
|
||||||
|
disabled={importBusy}
|
||||||
|
aria-label="Выбрать файл для импорта"
|
||||||
|
/>
|
||||||
|
<label
|
||||||
|
htmlFor="import-test-file"
|
||||||
|
className={`btn btn-ghost import-file-label${importBusy ? ' is-busy' : ''}`}
|
||||||
|
style={importBusy ? { pointerEvents: 'none' } : undefined}
|
||||||
|
>
|
||||||
|
{importBusy ? 'Обработка…' : 'Выбрать файл'}
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
{importErr && (
|
||||||
|
<p className="error-text" role="alert">
|
||||||
|
{importErr}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
{importPreview && (
|
||||||
|
<div className="draft-block" style={{ marginTop: '0.65rem', marginBottom: 0 }}>
|
||||||
|
<p className="muted" style={{ marginTop: 0, marginBottom: 0 }}>
|
||||||
|
{importPreview.generation?.message}
|
||||||
|
</p>
|
||||||
|
{importPreview.generation?.textPreview && !importPreview.generation?.available && (
|
||||||
|
<pre
|
||||||
|
className="form-input"
|
||||||
|
style={{
|
||||||
|
maxHeight: 180,
|
||||||
|
overflow: 'auto',
|
||||||
|
fontSize: 13,
|
||||||
|
marginTop: 8,
|
||||||
|
whiteSpace: 'pre-wrap',
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{importPreview.generation.textPreview}
|
||||||
|
{importPreview.textLength > 4000 ? '…' : ''}
|
||||||
|
</pre>
|
||||||
|
)}
|
||||||
|
{importPreview.generation?.draft && (
|
||||||
|
<div className="inline-actions" style={{ marginTop: 8, marginBottom: 0 }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost"
|
||||||
|
onClick={applyGeneratedDraft}
|
||||||
|
>
|
||||||
|
Применить сгенерированный черновик
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{importPreview.extractedText && (
|
||||||
|
<div className="inline-actions" style={{ marginTop: 8, marginBottom: 0 }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost"
|
||||||
|
onClick={applyExtractedToQuestion}
|
||||||
|
>
|
||||||
|
Вставить в первый вопрос (до 2000 симв.)
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</AccSection>
|
</AccSection>
|
||||||
|
|
||||||
@@ -959,295 +1048,276 @@ export default function TestDetail() {
|
|||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
<AccSection title="История версий" defaultOpen={false}>
|
<AccSection
|
||||||
<ul className="version-card-list" aria-label="Список версий теста">
|
title="История"
|
||||||
{versions.map((r) => (
|
subtitle="Версии теста и кто проходил"
|
||||||
<li key={r.id} className="surface-card version-card-list__item">
|
defaultOpen={false}
|
||||||
<div className="version-card-list__row">
|
|
||||||
<div className="version-card-list__main">
|
|
||||||
<div className="version-card-list__title-line">
|
|
||||||
<span className="font-headline" style={{ fontSize: '1rem' }}>
|
|
||||||
v{r.version}
|
|
||||||
</span>
|
|
||||||
{r.is_active && (
|
|
||||||
<span className="code-inline" style={{ fontSize: '0.7rem' }}>
|
|
||||||
текущая
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
<p className="muted mono" style={{ margin: '0.4rem 0 0', fontSize: '0.8rem' }}>
|
|
||||||
{fmtDt(r.created_at)}
|
|
||||||
</p>
|
|
||||||
<p className="muted" style={{ margin: '0.2rem 0 0', fontSize: '0.8rem' }}>
|
|
||||||
Активна: {r.is_active ? 'да' : 'нет'}
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{!r.is_active && (
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost btn--sm version-card-list__action"
|
|
||||||
onClick={() => activateVersion(r.id)}
|
|
||||||
>
|
|
||||||
Сделать активной
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
))}
|
|
||||||
</ul>
|
|
||||||
</AccSection>
|
|
||||||
|
|
||||||
{attemptsList != null && attemptsList.length > 0 && (
|
|
||||||
<AccSection title="Прогоны и разбор" defaultOpen={false}>
|
|
||||||
{attemptsErr && (
|
|
||||||
<p className="error-text" role="alert">
|
|
||||||
{attemptsErr}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<ul className="attempts-card-list" aria-label="Список прогонов">
|
|
||||||
{attemptsList.map((a) => {
|
|
||||||
const when = a.completedAt
|
|
||||||
? fmtDt(a.completedAt)
|
|
||||||
: a.startedAt
|
|
||||||
? fmtDt(a.startedAt)
|
|
||||||
: '—';
|
|
||||||
const result =
|
|
||||||
a.status === 'completed' && a.totalQuestions != null ? (
|
|
||||||
<>
|
|
||||||
{a.correctCount}/{a.totalQuestions}
|
|
||||||
{a.passed ? ' · зачёт' : ' · незачёт'}
|
|
||||||
</>
|
|
||||||
) : (
|
|
||||||
a.status
|
|
||||||
);
|
|
||||||
return (
|
|
||||||
<li key={a.id} className="surface-card attempts-card-list__item">
|
|
||||||
<div className="attempts-card-list__row">
|
|
||||||
<div className="attempts-card-list__main">
|
|
||||||
<p className="muted attempts-card-list__when mono" style={{ margin: 0, fontSize: '0.8rem' }}>
|
|
||||||
{when}
|
|
||||||
</p>
|
|
||||||
<p className="attempts-card-list__who" style={{ margin: '0.35rem 0 0', fontWeight: 600 }}>
|
|
||||||
{a.attempterName || '—'}
|
|
||||||
{a.attempterLogin && (
|
|
||||||
<span className="code-inline" style={{ fontSize: 11, marginLeft: 6 }}>
|
|
||||||
{a.attempterLogin}
|
|
||||||
</span>
|
|
||||||
)}
|
|
||||||
</p>
|
|
||||||
<p className="muted" style={{ margin: '0.25rem 0 0', fontSize: '0.85rem' }}>
|
|
||||||
v{a.testVersion} ·{' '}
|
|
||||||
<span className="attempts-card-list__result">{result}</span>
|
|
||||||
</p>
|
|
||||||
</div>
|
|
||||||
{a.status === 'completed' && (
|
|
||||||
<Link
|
|
||||||
to={`/tests/${id}/attempts/${a.id}/review`}
|
|
||||||
className="btn btn-ghost btn--sm attempts-card-list__action"
|
|
||||||
>
|
|
||||||
Разбор
|
|
||||||
</Link>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</li>
|
|
||||||
);
|
|
||||||
})}
|
|
||||||
</ul>
|
|
||||||
</AccSection>
|
|
||||||
)}
|
|
||||||
|
|
||||||
<AccSection title="Публикация (видимость в списке)" defaultOpen={false}>
|
|
||||||
<div
|
|
||||||
className="inline-actions inline-actions--block-mobile"
|
|
||||||
style={{ marginTop: '0.5rem' }}
|
|
||||||
>
|
>
|
||||||
{test?.chainActive !== false ? (
|
<div className="test-detail-subsection test-detail-subsection--tight">
|
||||||
<button
|
<h3 className="test-detail-subsection__title">Версии</h3>
|
||||||
type="button"
|
<ul className="version-card-list" aria-label="Список версий теста">
|
||||||
className="btn btn-ghost"
|
{versions.map((r) => (
|
||||||
disabled={deactivateBusy}
|
<li key={r.id} className="surface-card version-card-list__item">
|
||||||
onClick={() => setChainVisible(false)}
|
<div className="version-card-list__row">
|
||||||
>
|
<div className="version-card-list__main">
|
||||||
Скрыть из списка
|
<div className="version-card-list__title-line">
|
||||||
</button>
|
<span className="font-headline" style={{ fontSize: '1rem' }}>
|
||||||
) : (
|
v{r.version}
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost"
|
|
||||||
disabled={deactivateBusy}
|
|
||||||
onClick={() => setChainVisible(true)}
|
|
||||||
>
|
|
||||||
Снова показать в списке
|
|
||||||
</button>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
</AccSection>
|
|
||||||
|
|
||||||
<AccSection title="Импорт из файла" defaultOpen={false}>
|
|
||||||
<div className="import-file-row" style={{ marginBottom: '0.5rem' }}>
|
|
||||||
<input
|
|
||||||
id="import-test-file"
|
|
||||||
className="import-file-input"
|
|
||||||
type="file"
|
|
||||||
accept=".pdf,.docx,.txt,.md,application/pdf"
|
|
||||||
onChange={onImportFile}
|
|
||||||
disabled={importBusy}
|
|
||||||
aria-label="Выбрать файл для импорта"
|
|
||||||
/>
|
|
||||||
<label
|
|
||||||
htmlFor="import-test-file"
|
|
||||||
className={`btn btn-ghost import-file-label${importBusy ? ' is-busy' : ''}`}
|
|
||||||
style={importBusy ? { pointerEvents: 'none' } : undefined}
|
|
||||||
>
|
|
||||||
{importBusy ? 'Обработка…' : 'Выбрать файл'}
|
|
||||||
</label>
|
|
||||||
</div>
|
|
||||||
{importErr && (
|
|
||||||
<p className="error-text" role="alert">
|
|
||||||
{importErr}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
{importPreview && (
|
|
||||||
<div className="draft-block" style={{ marginBottom: '1rem' }}>
|
|
||||||
<p className="muted" style={{ marginTop: 0 }}>
|
|
||||||
{importPreview.generation?.message}
|
|
||||||
</p>
|
|
||||||
{importPreview.generation?.textPreview && !importPreview.generation?.available && (
|
|
||||||
<pre
|
|
||||||
className="form-input"
|
|
||||||
style={{
|
|
||||||
maxHeight: 180,
|
|
||||||
overflow: 'auto',
|
|
||||||
fontSize: 13,
|
|
||||||
marginTop: 8,
|
|
||||||
whiteSpace: 'pre-wrap',
|
|
||||||
}}
|
|
||||||
>
|
|
||||||
{importPreview.generation.textPreview}
|
|
||||||
{importPreview.textLength > 4000 ? '…' : ''}
|
|
||||||
</pre>
|
|
||||||
)}
|
|
||||||
{importPreview.generation?.draft && (
|
|
||||||
<div className="inline-actions" style={{ marginTop: 8 }}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost"
|
|
||||||
onClick={applyGeneratedDraft}
|
|
||||||
>
|
|
||||||
Применить сгенерированный черновик
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{importPreview.extractedText && (
|
|
||||||
<div className="inline-actions" style={{ marginTop: 8 }}>
|
|
||||||
<button
|
|
||||||
type="button"
|
|
||||||
className="btn btn-ghost"
|
|
||||||
onClick={applyExtractedToQuestion}
|
|
||||||
>
|
|
||||||
Вставить в первый вопрос (до 2000 симв.)
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
</AccSection>
|
|
||||||
|
|
||||||
{assignmentUi && data && (
|
|
||||||
<AccSection title="Назначение сотрудникам" defaultOpen={false}>
|
|
||||||
{assignErr && (
|
|
||||||
<p className="error-text" role="alert">
|
|
||||||
{assignErr}
|
|
||||||
</p>
|
|
||||||
)}
|
|
||||||
<div className="assign-toolbar">
|
|
||||||
<input
|
|
||||||
type="search"
|
|
||||||
className="form-input assign-toolbar__search"
|
|
||||||
placeholder="Поиск: ФИО, логин"
|
|
||||||
value={assignSearch}
|
|
||||||
onChange={(e) => setAssignSearch(e.target.value)}
|
|
||||||
aria-label="Поиск сотрудника"
|
|
||||||
/>
|
|
||||||
<select
|
|
||||||
className="form-input"
|
|
||||||
value={assignDept}
|
|
||||||
onChange={(e) => setAssignDept(e.target.value)}
|
|
||||||
aria-label="Фильтр по отделу"
|
|
||||||
>
|
|
||||||
<option value="__all__">Все отделы</option>
|
|
||||||
{assignDepts.map((d) => (
|
|
||||||
<option key={d} value={d}>
|
|
||||||
{d}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<select
|
|
||||||
className="form-input"
|
|
||||||
value={assignClinic}
|
|
||||||
onChange={(e) => setAssignClinic(e.target.value)}
|
|
||||||
aria-label="Кто в модуле"
|
|
||||||
>
|
|
||||||
<option value="all">Все</option>
|
|
||||||
<option value="with">С учёткой в модуле</option>
|
|
||||||
<option value="without">Без учётки (создать)</option>
|
|
||||||
</select>
|
|
||||||
{assignLoadBusy && <span className="text-muted">Загрузка…</span>}
|
|
||||||
</div>
|
|
||||||
{assignPeople.length > 0 ? (
|
|
||||||
<div className="assign-list" role="group" aria-label="Список сотрудников">
|
|
||||||
{assignPeople.map((p) => {
|
|
||||||
const k = assignPersonKey(p);
|
|
||||||
const picked = assignSelected.has(k);
|
|
||||||
return (
|
|
||||||
<label
|
|
||||||
key={k}
|
|
||||||
className={`assign-row${picked ? ' assign-row--selected' : ''}`}
|
|
||||||
>
|
|
||||||
<input
|
|
||||||
type="checkbox"
|
|
||||||
checked={picked}
|
|
||||||
onChange={() => toggleAssignPerson(p)}
|
|
||||||
/>
|
|
||||||
<span className="assign-row__text">
|
|
||||||
<span className="assign-row__fio">{p.fio}</span>
|
|
||||||
{p.webLogin && (
|
|
||||||
<span className="assign-row__login">{p.webLogin}</span>
|
|
||||||
)}
|
|
||||||
<span className="assign-row__meta">
|
|
||||||
{p.departments || '—'}
|
|
||||||
{p.clinicUserId
|
|
||||||
? ' · есть учётка в модуле'
|
|
||||||
: ' · нет учётки (создадим при назначении)'}
|
|
||||||
</span>
|
</span>
|
||||||
</span>
|
{r.is_active && (
|
||||||
</label>
|
<span className="code-inline" style={{ fontSize: '0.7rem' }}>
|
||||||
);
|
текущая
|
||||||
})}
|
</span>
|
||||||
</div>
|
)}
|
||||||
) : (
|
</div>
|
||||||
!assignLoadBusy && (
|
<p className="muted mono" style={{ margin: '0.4rem 0 0', fontSize: '0.8rem' }}>
|
||||||
<p className="text-muted" style={{ margin: '0.5rem 0' }}>
|
{fmtDt(r.created_at)}
|
||||||
Нет подходящих записей. Смените фильтры или поиск.
|
</p>
|
||||||
|
<p className="muted" style={{ margin: '0.2rem 0 0', fontSize: '0.8rem' }}>
|
||||||
|
Активна: {r.is_active ? 'да' : 'нет'}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{!r.is_active && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost btn--sm version-card-list__action"
|
||||||
|
onClick={() => activateVersion(r.id)}
|
||||||
|
>
|
||||||
|
Сделать активной
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
))}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
{attemptsList !== undefined && (
|
||||||
|
<div className="test-detail-subsection">
|
||||||
|
<h3 className="test-detail-subsection__title">Прохождения</h3>
|
||||||
|
{attemptsList == null && (
|
||||||
|
<p
|
||||||
|
className={attemptsErr ? 'error-text' : 'text-muted'}
|
||||||
|
style={{ margin: 0 }}
|
||||||
|
role={attemptsErr ? 'alert' : undefined}
|
||||||
|
>
|
||||||
|
{attemptsErr || 'Список прогонов сейчас недоступен.'}
|
||||||
</p>
|
</p>
|
||||||
)
|
)}
|
||||||
)}
|
{attemptsList && attemptsList.length > 0 && (
|
||||||
<div className="inline-actions" style={{ marginTop: '0.75rem' }}>
|
<ul className="attempts-card-list" aria-label="Список прогонов">
|
||||||
<button
|
{attemptsList.map((a) => {
|
||||||
type="button"
|
const when = a.completedAt
|
||||||
className="btn btn-ghost"
|
? fmtDt(a.completedAt)
|
||||||
disabled={assignSelectedInList.length === 0}
|
: a.startedAt
|
||||||
onClick={postAssign}
|
? fmtDt(a.startedAt)
|
||||||
>
|
: '—';
|
||||||
Назначить выбранных
|
const result =
|
||||||
{assignSelectedInList.length > 0
|
a.status === 'completed' && a.totalQuestions != null ? (
|
||||||
? ` (${assignSelectedInList.length})`
|
<>
|
||||||
: ''}
|
{a.correctCount}/{a.totalQuestions}
|
||||||
</button>
|
{a.passed ? ' · зачёт' : ' · незачёт'}
|
||||||
|
</>
|
||||||
|
) : (
|
||||||
|
a.status
|
||||||
|
);
|
||||||
|
return (
|
||||||
|
<li key={a.id} className="surface-card attempts-card-list__item">
|
||||||
|
<div className="attempts-card-list__row">
|
||||||
|
<div className="attempts-card-list__main">
|
||||||
|
<p
|
||||||
|
className="muted attempts-card-list__when mono"
|
||||||
|
style={{ margin: 0, fontSize: '0.8rem' }}
|
||||||
|
>
|
||||||
|
{when}
|
||||||
|
</p>
|
||||||
|
<p
|
||||||
|
className="attempts-card-list__who"
|
||||||
|
style={{ margin: '0.35rem 0 0', fontWeight: 600 }}
|
||||||
|
>
|
||||||
|
{a.attempterName || '—'}
|
||||||
|
{a.attempterLogin && (
|
||||||
|
<span className="code-inline" style={{ fontSize: 11, marginLeft: 6 }}>
|
||||||
|
{a.attempterLogin}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</p>
|
||||||
|
<p className="muted" style={{ margin: '0.25rem 0 0', fontSize: '0.85rem' }}>
|
||||||
|
v{a.testVersion} ·{' '}
|
||||||
|
<span className="attempts-card-list__result">{result}</span>
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
{a.status === 'completed' && (
|
||||||
|
<Link
|
||||||
|
to={`/tests/${id}/attempts/${a.id}/review`}
|
||||||
|
className="btn btn-ghost btn--sm attempts-card-list__action"
|
||||||
|
>
|
||||||
|
Разбор
|
||||||
|
</Link>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</ul>
|
||||||
|
)}
|
||||||
|
{attemptsList && attemptsList.length === 0 && (
|
||||||
|
<p className="text-muted" style={{ margin: 0 }}>
|
||||||
|
Пока нет зарегистрированных прогонов.
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
{assignMsg && <p className="text-muted">{assignMsg}</p>}
|
)}
|
||||||
</AccSection>
|
</AccSection>
|
||||||
)}
|
|
||||||
|
<AccSection
|
||||||
|
title="Показ в каталоге"
|
||||||
|
subtitle={
|
||||||
|
assignmentUi && data
|
||||||
|
? 'Видимость в списке и выдача сотрудникам'
|
||||||
|
: 'Показать или скрыть в общем списке'
|
||||||
|
}
|
||||||
|
defaultOpen={false}
|
||||||
|
>
|
||||||
|
<div className="test-detail-subsection test-detail-subsection--tight">
|
||||||
|
<h3 className="test-detail-subsection__title">Видимость</h3>
|
||||||
|
<p className="test-detail-hint" style={{ marginTop: 0 }}>
|
||||||
|
Скрытые тесты в общем списке не показываются; ссылку на тест по-прежнему можно открыть.
|
||||||
|
</p>
|
||||||
|
<div className="publication-visibility__actions" style={{ marginTop: 4 }}>
|
||||||
|
{test?.chainActive !== false ? (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost"
|
||||||
|
disabled={deactivateBusy}
|
||||||
|
onClick={() => setChainVisible(false)}
|
||||||
|
>
|
||||||
|
Скрыть из списка
|
||||||
|
</button>
|
||||||
|
) : (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost"
|
||||||
|
disabled={deactivateBusy}
|
||||||
|
onClick={() => setChainVisible(true)}
|
||||||
|
>
|
||||||
|
Снова показать в списке
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{assignmentUi && data && (
|
||||||
|
<div className="test-detail-subsection">
|
||||||
|
<h3 className="test-detail-subsection__title">Кому выдать</h3>
|
||||||
|
<p className="test-detail-hint" style={{ marginTop: 0 }}>
|
||||||
|
Список с учётом поиска и фильтров; можно отметить всех на экране.
|
||||||
|
</p>
|
||||||
|
{assignErr && (
|
||||||
|
<p className="error-text" role="alert" style={{ marginTop: 0, marginBottom: 8 }}>
|
||||||
|
{assignErr}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
<div className="assign-toolbar">
|
||||||
|
<input
|
||||||
|
type="search"
|
||||||
|
className="form-input assign-toolbar__search"
|
||||||
|
placeholder="Поиск: ФИО, логин"
|
||||||
|
value={assignSearch}
|
||||||
|
onChange={(e) => setAssignSearch(e.target.value)}
|
||||||
|
aria-label="Поиск сотрудника"
|
||||||
|
/>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={assignDept}
|
||||||
|
onChange={(e) => setAssignDept(e.target.value)}
|
||||||
|
aria-label="Фильтр по отделу"
|
||||||
|
>
|
||||||
|
<option value="__all__">Все отделы</option>
|
||||||
|
{assignDepts.map((d) => (
|
||||||
|
<option key={d} value={d}>
|
||||||
|
{d}
|
||||||
|
</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
<select
|
||||||
|
className="form-input"
|
||||||
|
value={assignClinic}
|
||||||
|
onChange={(e) => setAssignClinic(e.target.value)}
|
||||||
|
aria-label="Кто в модуле"
|
||||||
|
>
|
||||||
|
<option value="all">Все</option>
|
||||||
|
<option value="with">С учёткой в модуле</option>
|
||||||
|
<option value="without">Без учётки (создать)</option>
|
||||||
|
</select>
|
||||||
|
{assignLoadBusy && <span className="text-muted">Загрузка…</span>}
|
||||||
|
</div>
|
||||||
|
{assignPeople.length > 0 && (
|
||||||
|
<div
|
||||||
|
className="publication-visibility__actions"
|
||||||
|
style={{ marginTop: '0.45rem', marginBottom: '0.25rem' }}
|
||||||
|
>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost btn--sm"
|
||||||
|
onClick={selectAllVisible}
|
||||||
|
disabled={assignLoadBusy}
|
||||||
|
>
|
||||||
|
Выбрать всех ({assignPeople.length})
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{assignPeople.length > 0 ? (
|
||||||
|
<div className="assign-list" role="group" aria-label="Список сотрудников">
|
||||||
|
{assignPeople.map((p) => {
|
||||||
|
const k = assignPersonKey(p);
|
||||||
|
const picked = assignSelected.has(k);
|
||||||
|
return (
|
||||||
|
<label
|
||||||
|
key={k}
|
||||||
|
className={`assign-row${picked ? ' assign-row--selected' : ''}`}
|
||||||
|
>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={picked}
|
||||||
|
onChange={() => toggleAssignPerson(p)}
|
||||||
|
/>
|
||||||
|
<span className="assign-row__text">
|
||||||
|
<span className="assign-row__fio">{p.fio}</span>
|
||||||
|
{p.webLogin && <span className="assign-row__login">{p.webLogin}</span>}
|
||||||
|
<span className="assign-row__meta">
|
||||||
|
{p.departments || '—'}
|
||||||
|
{p.clinicUserId
|
||||||
|
? ' · есть учётка в модуле'
|
||||||
|
: ' · нет учётки (создадим при назначении)'}
|
||||||
|
</span>
|
||||||
|
</span>
|
||||||
|
</label>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
) : (
|
||||||
|
!assignLoadBusy && (
|
||||||
|
<p className="text-muted" style={{ margin: '0.5rem 0' }}>
|
||||||
|
Нет подходящих записей. Смените фильтры или поиск.
|
||||||
|
</p>
|
||||||
|
)
|
||||||
|
)}
|
||||||
|
<div className="publication-visibility__actions" style={{ marginTop: '0.75rem' }}>
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-ghost"
|
||||||
|
disabled={assignSelectedInList.length === 0}
|
||||||
|
onClick={postAssign}
|
||||||
|
>
|
||||||
|
Назначить выбранных
|
||||||
|
{assignSelectedInList.length > 0
|
||||||
|
? ` (${assignSelectedInList.length})`
|
||||||
|
: ''}
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
{assignMsg && <p className="text-muted" style={{ marginTop: 8, marginBottom: 0 }}>{assignMsg}</p>}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</AccSection>
|
||||||
|
|
||||||
<div
|
<div
|
||||||
className="test-detail-fixed-actions"
|
className="test-detail-fixed-actions"
|
||||||
|
|||||||
@@ -605,6 +605,29 @@ code,
|
|||||||
min-height: 2.75rem;
|
min-height: 2.75rem;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabinet-disclosure__summary-text {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
gap: 0.15rem;
|
||||||
|
min-width: 0;
|
||||||
|
text-align: left;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabinet-disclosure__summary-title {
|
||||||
|
font-size: 1.05rem;
|
||||||
|
line-height: 1.25;
|
||||||
|
}
|
||||||
|
|
||||||
|
.cabinet-disclosure__summary-sub {
|
||||||
|
display: block;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
font-weight: 400;
|
||||||
|
line-height: 1.3;
|
||||||
|
color: var(--secondary);
|
||||||
}
|
}
|
||||||
|
|
||||||
.cabinet-disclosure__summary::-webkit-details-marker {
|
.cabinet-disclosure__summary::-webkit-details-marker {
|
||||||
@@ -626,7 +649,7 @@ code,
|
|||||||
}
|
}
|
||||||
|
|
||||||
.cabinet-disclosure__body {
|
.cabinet-disclosure__body {
|
||||||
padding: 0 1rem 1.05rem;
|
padding: 0.7rem 1rem 1.05rem;
|
||||||
border-top: 1px solid color-mix(in srgb, var(--outline-variant) 35%, transparent);
|
border-top: 1px solid color-mix(in srgb, var(--outline-variant) 35%, transparent);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -657,19 +680,160 @@ code,
|
|||||||
|
|
||||||
.assign-toolbar__search {
|
.assign-toolbar__search {
|
||||||
flex: 1 1 200px;
|
flex: 1 1 200px;
|
||||||
|
min-height: 2.5rem;
|
||||||
|
max-height: 2.75rem;
|
||||||
|
padding-top: 6px;
|
||||||
|
padding-bottom: 6px;
|
||||||
|
line-height: 1.25;
|
||||||
|
box-sizing: border-box;
|
||||||
}
|
}
|
||||||
|
|
||||||
/* Назначение: компактнее поля на мобилке; 16px — без авто-зума iOS в поле */
|
/* Назначение: селекты — как раньше; поиск — одна «строка», не «плитка» */
|
||||||
@media (max-width: 640px) {
|
@media (max-width: 640px) {
|
||||||
.assign-toolbar {
|
.assign-toolbar {
|
||||||
gap: 0.4rem;
|
gap: 0.4rem;
|
||||||
}
|
}
|
||||||
|
|
||||||
.assign-toolbar .form-input {
|
.assign-toolbar .form-input {
|
||||||
padding: 8px 12px;
|
|
||||||
font-size: 16px;
|
font-size: 16px;
|
||||||
line-height: 1.3;
|
line-height: 1.3;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.assign-toolbar__search {
|
||||||
|
font-size: 16px;
|
||||||
|
line-height: 1.3;
|
||||||
|
}
|
||||||
|
|
||||||
|
.assign-toolbar .form-input:not(.assign-toolbar__search) {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Подсекции и подсказки (карточка теста) */
|
||||||
|
.test-detail-subsection {
|
||||||
|
margin-top: 1.25rem;
|
||||||
|
padding-top: 1.15rem;
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--outline-variant) 32%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-detail-subsection:first-of-type,
|
||||||
|
.test-detail-subsection--tight {
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
border-top: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-detail-subsection--import {
|
||||||
|
margin-top: 1.35rem;
|
||||||
|
padding-top: 1.2rem;
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--outline-variant) 32%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-detail-subsection__title {
|
||||||
|
margin: 0 0 0.35rem;
|
||||||
|
font-size: 0.95rem;
|
||||||
|
font-weight: 600;
|
||||||
|
font-family: inherit;
|
||||||
|
color: var(--on-surface);
|
||||||
|
letter-spacing: -0.01em;
|
||||||
|
}
|
||||||
|
|
||||||
|
.test-detail-hint {
|
||||||
|
margin: 0 0 0.6rem;
|
||||||
|
font-size: 0.8rem;
|
||||||
|
line-height: 1.4;
|
||||||
|
color: var(--secondary);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Панель «ИИ» — в тон остальным disclosure, без лишнего «модальности» */
|
||||||
|
.test-detail-ai-panel {
|
||||||
|
padding: 0.9rem 1rem;
|
||||||
|
margin-bottom: 1.15rem;
|
||||||
|
background: var(--surface-container-low);
|
||||||
|
border: 1px solid color-mix(in srgb, var(--outline-variant) 32%, transparent);
|
||||||
|
border-radius: 0.85rem;
|
||||||
|
box-shadow: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Кнопка публикации — не тянем на 100% ширины, если одна */
|
||||||
|
.publication-visibility__actions {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.publication-visibility__actions .btn {
|
||||||
|
width: auto;
|
||||||
|
min-width: min(100%, 11rem);
|
||||||
|
max-width: 100%;
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Убрать вариант: иконка */
|
||||||
|
.question-option-remove {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
width: 2.25rem;
|
||||||
|
min-width: 2.25rem;
|
||||||
|
height: 2.25rem;
|
||||||
|
margin: 0;
|
||||||
|
padding: 0;
|
||||||
|
border: none;
|
||||||
|
border-radius: 0.65rem;
|
||||||
|
background: transparent;
|
||||||
|
color: var(--secondary);
|
||||||
|
cursor: pointer;
|
||||||
|
transition: background 0.12s, color 0.12s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-option-remove:hover {
|
||||||
|
background: color-mix(in srgb, var(--error) 8%, var(--surface));
|
||||||
|
color: var(--error, #b91c1c);
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-option-remove .material-symbols-outlined {
|
||||||
|
font-size: 1.2rem;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Футер вопроса: + вариант, удалить вопрос */
|
||||||
|
.question-editor__footer {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.4rem 0.65rem;
|
||||||
|
margin-top: 0.4rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-editor__footer .btn--sm,
|
||||||
|
.question-editor__add-question {
|
||||||
|
font-size: 0.8rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Добавить вопрос — в конец блока «Вопросы» */
|
||||||
|
.test-detail-add-question {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-start;
|
||||||
|
margin-top: 1.1rem;
|
||||||
|
padding-top: 0.85rem;
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--outline-variant) 28%, transparent);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* Импорт: кнопка на всю ширину в колонку; ритм отступов */
|
||||||
|
@media (max-width: 640px) {
|
||||||
|
.import-file-row--block {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: stretch;
|
||||||
|
}
|
||||||
|
|
||||||
|
.import-file-label {
|
||||||
|
display: block;
|
||||||
|
width: 100%;
|
||||||
|
text-align: center;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
.assign-list {
|
.assign-list {
|
||||||
@@ -986,6 +1150,31 @@ code,
|
|||||||
|
|
||||||
/* --- Спринт 2: редактор вопроса, прогоны, импорт, фикс. футер --- */
|
/* --- Спринт 2: редактор вопроса, прогоны, импорт, фикс. футер --- */
|
||||||
|
|
||||||
|
.question-editor-block {
|
||||||
|
border-top: 1px solid color-mix(in srgb, var(--outline-variant) 32%, transparent);
|
||||||
|
margin-top: 1.05rem;
|
||||||
|
padding-top: 1.05rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-editor-block--first {
|
||||||
|
border-top: none;
|
||||||
|
margin-top: 0;
|
||||||
|
padding-top: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-option-row {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: nowrap;
|
||||||
|
align-items: center;
|
||||||
|
gap: 0.5rem;
|
||||||
|
margin-bottom: 0.5rem;
|
||||||
|
}
|
||||||
|
|
||||||
|
.question-option-row__text {
|
||||||
|
flex: 1 1 10rem;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
.question-editor-block__header {
|
.question-editor-block__header {
|
||||||
display: flex;
|
display: flex;
|
||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
|
|||||||
Reference in New Issue
Block a user