From 24cce8ea7110c6679c527db5f870c3398947f6a2 Mon Sep 17 00:00:00 2001 From: danamir Date: Sun, 9 Aug 2026 19:03:40 +0300 Subject: [PATCH] Show the question bank as a table with a stable, right-aligned actions column The old list showed type, text, and points inline via flexbox, with no correct-answer info at all, and unstyled buttons that shifted position depending on question text length. A table with a shrink-to-fit actions column (text/kind/answer/points first, buttons last) fixes both: correct answers are now visible, and button position no longer depends on text length. --- src/Client/Features/Teacher/Questions/View.fs | 76 ++++++++++++++----- src/Client/style.css | 46 ++++++++--- 2 files changed, 91 insertions(+), 31 deletions(-) diff --git a/src/Client/Features/Teacher/Questions/View.fs b/src/Client/Features/Teacher/Questions/View.fs index 49e4dac..f9d6193 100644 --- a/src/Client/Features/Teacher/Questions/View.fs +++ b/src/Client/Features/Teacher/Questions/View.fs @@ -65,6 +65,25 @@ let private kindLabel (t: QuestionTypeView) = | ShortAnswerT _ -> "Короткий ответ" | NumericT _ -> "Числовой" +/// Read-only rendering of a question's correct answer(s) for the teacher's +/// question-bank table — distinct from `formFromSummary` in Types.fs, which +/// unpacks the same data to prefill the edit form instead of displaying it. +let private correctAnswerText (t: QuestionTypeView) = + match t with + | SingleChoiceT d -> + d.Options + |> List.tryFind (fun o -> o.Id = d.CorrectOptionId) + |> Option.map (fun o -> o.Text) + |> Option.defaultValue "—" + | MultipleChoiceT d -> + d.Options + |> List.filter (fun o -> List.contains o.Id d.CorrectOptionIds) + |> List.map (fun o -> o.Text) + |> String.concat ", " + | TrueFalseT b -> if b then "Верно" else "Неверно" + | ShortAnswerT d -> String.concat ", " d.AcceptedAnswers + | NumericT d -> sprintf "%s ± %s" (Format.points d.CorrectValue) (Format.points d.Tolerance) + let private questionsListView (model: Model) dispatch = Html.div [ prop.className "questions-list" @@ -78,30 +97,45 @@ let private questionsListView (model: Model) dispatch = elif model.Questions.IsEmpty then Html.p "В этой теме пока нет вопросов" else - Html.ul [ + Html.table [ + prop.className "questions-table" prop.children [ - for q in model.Questions -> - Html.li [ - prop.key (string q.Id) - prop.children [ - Html.span [ prop.className "question-kind"; prop.text (kindLabel q.Type) ] - Html.text (sprintf " %s " q.Text) - Html.span [ - prop.className "tag-mono" - prop.text (sprintf "%s б." (Format.points q.Points)) - ] - Html.button [ - prop.type'.button - prop.onClick (fun _ -> dispatch (StartEditQuestion q.Id)) - prop.text "Изменить" - ] - Html.button [ - prop.type'.button - prop.onClick (fun _ -> dispatch (RequestDeleteQuestion q.Id)) - prop.text "Удалить" + Html.thead [ + Html.tr [ + Html.th [ prop.text "Текст вопроса" ] + Html.th [ prop.text "Тип" ] + Html.th [ prop.text "Правильный ответ" ] + Html.th [ prop.text "Баллы" ] + Html.th [] + ] + ] + Html.tbody [ + for q in model.Questions -> + Html.tr [ + prop.key (string q.Id) + prop.children [ + Html.td [ prop.text q.Text ] + Html.td [ prop.className "question-kind"; prop.text (kindLabel q.Type) ] + Html.td [ prop.text (correctAnswerText q.Type) ] + Html.td [ prop.className "tag-mono"; prop.text (Format.points q.Points) ] + Html.td [ + prop.className "question-actions-cell" + prop.children [ + Html.button [ + prop.type'.button + prop.onClick (fun _ -> dispatch (StartEditQuestion q.Id)) + prop.text "Изменить" + ] + Html.button [ + prop.type'.button + prop.onClick (fun _ -> dispatch (RequestDeleteQuestion q.Id)) + prop.text "Удалить" + ] + ] + ] ] ] - ] + ] ] ] ] diff --git a/src/Client/style.css b/src/Client/style.css index e47d8eb..9a521e6 100644 --- a/src/Client/style.css +++ b/src/Client/style.css @@ -681,17 +681,44 @@ a { margin-bottom: 1.25rem; } -.questions-list li { - display: flex; - flex-wrap: wrap; - align-items: center; - gap: 0.5rem; - border-top: 1px solid var(--line); - padding: 0.6rem 0; +.questions-table { + width: 100%; + border-collapse: collapse; + font-size: 0.9rem; } -.questions-list li:first-child { - border-top: none; +.questions-table th, +.questions-table td { + text-align: left; + padding: 0.55rem 0.75rem; + border-bottom: 1px solid var(--line); + vertical-align: top; +} + +.questions-table th { + font-family: var(--font-mono); + font-size: 0.7rem; + letter-spacing: 0.04em; + text-transform: uppercase; + color: var(--ink-soft); +} + +.questions-table tbody tr:hover { + background: var(--brass-soft); +} + +/* `width: 1%` shrinks this column to just its buttons' content width — + since it's the only unconstrained column left, the question-text column + absorbs 100% of the remaining space instead, so the buttons stay pinned + flush right regardless of how long any question's text is. */ +.question-actions-cell { + white-space: nowrap; + width: 1%; + text-align: right; +} + +.question-actions-cell button { + margin-left: 0.5rem; } .question-kind { @@ -700,7 +727,6 @@ a { color: var(--brass); text-transform: uppercase; letter-spacing: 0.03em; - margin-right: 0.5rem; } .new-question-form {