All checks were successful
CI/CD / build-test-deploy (pull_request) Successful in 1m39s
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.
327 lines
14 KiB
Forth
327 lines
14 KiB
Forth
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
|
||
]
|
||
]
|
||
]
|
||
]
|
||
]
|