Files
RuvdsTest/src/Client/Features/Teacher/Questions/View.fs
danamir 24cce8ea71
All checks were successful
CI/CD / build-test-deploy (pull_request) Successful in 1m39s
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.
2026-08-09 19:03:40 +03:00

327 lines
14 KiB
Forth
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

module Client.Features.Teacher.Questions.View
open Feliz
open Domain
open Domain.Contracts
open Client.Shared
open Client.Features.Teacher.Questions.Types
let private topicsView (model: Model) dispatch =
Html.div [
prop.className "topics-panel"
prop.children [
Html.h2 "Темы"
match model.TopicsError with
| Some err -> Html.p [ prop.className "error"; prop.text err ]
| None -> Html.none
if model.TopicsLoading then
Html.p "Загрузка…"
else
Html.ul [
prop.children [
for topic in model.Topics ->
Html.li [
prop.key (string topic.Id)
prop.className (
if model.SelectedTopicId = Some topic.Id then
"topic-item selected"
else
"topic-item"
)
prop.onClick (fun _ -> dispatch (SelectTopic topic.Id))
prop.text topic.Name
]
]
]
Html.form [
prop.onSubmit (fun e ->
e.preventDefault ()
dispatch SubmitNewTopic)
prop.children [
Html.input [
prop.type'.text
prop.placeholder "Название новой темы"
prop.value model.NewTopicForm.Name
prop.onChange (SetNewTopicName >> dispatch)
]
Html.button [
prop.type'.submit
prop.disabled model.NewTopicForm.IsSubmitting
prop.text "Добавить тему"
]
match model.NewTopicForm.Error with
| Some err -> Html.p [ prop.className "error"; prop.text err ]
| None -> Html.none
]
]
]
]
let private kindLabel (t: QuestionTypeView) =
match t with
| SingleChoiceT _ -> "Один вариант"
| MultipleChoiceT _ -> "Несколько вариантов"
| TrueFalseT _ -> "Верно/неверно"
| 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"
prop.children [
Html.h3 "Вопросы в теме"
match model.QuestionsError with
| Some err -> Html.p [ prop.className "error"; prop.text err ]
| None -> Html.none
if model.QuestionsLoading then
Html.p "Загрузка…"
elif model.Questions.IsEmpty then
Html.p "В этой теме пока нет вопросов"
else
Html.table [
prop.className "questions-table"
prop.children [
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 "Удалить"
]
]
]
]
]
]
]
]
]
]
let private optionsEditor (form: NewQuestionForm) dispatch (multi: bool) =
Html.div [
prop.className "options-editor"
prop.children [
for opt in form.Options do
Html.div [
prop.key (string opt.Id)
prop.className "option-row"
prop.children [
Html.input [
prop.type'.radio
prop.name "correct-option"
prop.isChecked (not multi && form.CorrectSingle = Some opt.Id)
prop.style [ if multi then style.display.none ]
prop.onChange (fun (_: bool) -> dispatch (SetCorrectSingle opt.Id))
]
Html.input [
prop.type'.checkbox
prop.isChecked (form.CorrectMultiple.Contains opt.Id)
prop.style [ if not multi then style.display.none ]
prop.onChange (fun (_: bool) -> dispatch (ToggleCorrectMultiple opt.Id))
]
Html.input [
prop.type'.text
prop.placeholder "Текст варианта"
prop.value opt.Text
prop.onChange (fun v -> dispatch (SetOptionText(opt.Id, v)))
]
Html.button [
prop.type'.button
prop.disabled (form.Options.Length <= 2)
prop.onClick (fun _ -> dispatch (RemoveOption opt.Id))
prop.text "Удалить"
]
]
]
Html.button [ prop.type'.button; prop.onClick (fun _ -> dispatch AddOption); prop.text "Добавить вариант" ]
]
]
let private newQuestionFormView (isEditing: bool) (form: NewQuestionForm) dispatch =
Html.form [
prop.className "new-question-form"
prop.onSubmit (fun e ->
e.preventDefault ()
dispatch SubmitNewQuestion)
prop.children [
Html.h3 (if isEditing then "Редактирование вопроса" else "Новый вопрос")
Html.label [ prop.text "Текст вопроса" ]
Html.input [
prop.type'.text
prop.value form.Text
prop.onChange (SetQuestionText >> dispatch)
]
Html.label [ prop.text "Баллы" ]
Html.input [
prop.type'.number
prop.value form.Points
prop.onChange (SetQuestionPoints >> dispatch)
]
Html.label [ prop.text "Тип вопроса" ]
Html.select [
prop.value (
match form.Kind with
| KSingleChoice -> "single"
| KMultipleChoice -> "multiple"
| KTrueFalse -> "truefalse"
| KShortAnswer -> "short"
| KNumeric -> "numeric"
)
prop.onChange (fun (v: string) ->
let kind =
match v with
| "single" -> KSingleChoice
| "multiple" -> KMultipleChoice
| "truefalse" -> KTrueFalse
| "short" -> KShortAnswer
| _ -> KNumeric
dispatch (SetQuestionKind kind))
prop.children [
Html.option [ prop.value "single"; prop.text "Один вариант" ]
Html.option [ prop.value "multiple"; prop.text "Несколько вариантов" ]
Html.option [ prop.value "truefalse"; prop.text "Верно/неверно" ]
Html.option [ prop.value "short"; prop.text "Короткий ответ" ]
Html.option [ prop.value "numeric"; prop.text "Числовой" ]
]
]
match form.Kind with
| KSingleChoice -> optionsEditor form dispatch false
| KMultipleChoice -> optionsEditor form dispatch true
| KTrueFalse ->
Html.div [
for value, text in [ true, "Верно"; false, "Неверно" ] ->
Html.label [
prop.children [
Html.input [
prop.type'.radio
prop.name "correct-bool"
prop.isChecked (form.CorrectBool = value)
prop.onChange (fun (_: bool) -> dispatch (SetCorrectBool value))
]
Html.text text
]
]
]
| KShortAnswer ->
Html.div [
Html.label [ prop.text "Допустимые ответы (через запятую)" ]
Html.input [
prop.type'.text
prop.value form.AcceptedAnswersText
prop.onChange (SetAcceptedAnswersText >> dispatch)
]
Html.label [
prop.children [
Html.input [
prop.type'.checkbox
prop.isChecked form.CaseSensitive
prop.onChange (SetCaseSensitive >> dispatch)
]
Html.text "Учитывать регистр"
]
]
]
| KNumeric ->
Html.div [
Html.label [ prop.text "Правильное значение" ]
Html.input [
prop.type'.text
prop.value form.CorrectValueText
prop.onChange (SetCorrectValueText >> dispatch)
]
Html.label [ prop.text "Допустимая погрешность" ]
Html.input [
prop.type'.text
prop.value form.ToleranceText
prop.onChange (SetToleranceText >> dispatch)
]
]
match form.Error with
| Some err -> Html.p [ prop.className "error"; prop.text err ]
| None -> Html.none
Html.button [
prop.type'.submit
prop.disabled form.IsSubmitting
prop.text (
if form.IsSubmitting then "Сохранение…"
elif isEditing then "Сохранить изменения"
else "Добавить вопрос"
)
]
if isEditing then
Html.button [
prop.type'.button
prop.onClick (fun _ -> dispatch CancelEditQuestion)
prop.text "Отмена"
]
]
]
let view (model: Model) (dispatch: Msg -> unit) =
Html.div [
prop.className "teacher-questions-page"
prop.children [
Html.h1 "Банк вопросов"
Html.div [
prop.className "teacher-layout"
prop.children [
topicsView model dispatch
match model.SelectedTopicId with
| None -> Html.p "Выберите или создайте тему слева"
| Some _ ->
Html.div [
questionsListView model dispatch
newQuestionFormView model.EditingQuestionId.IsSome model.NewQuestionForm dispatch
]
]
]
]
]