Allow the quiz-builder picker to expand multiple topics at once
Sources could already draw from any number of topics, but the picker UI only ever showed one topic's questions at a time, forcing a teacher to lose their place switching between topics while assembling a quiz.
This commit is contained in:
@@ -175,34 +175,74 @@ let update (token: string option) (msg: Msg) (model: Model) : Model * Cmd<Msg> =
|
|||||||
{ model with QuizForm = { model.QuizForm with ShuffleQuestions = value } }, Cmd.none
|
{ model with QuizForm = { model.QuizForm with ShuffleQuestions = value } }, Cmd.none
|
||||||
| SetQuizShuffleAnswers value ->
|
| SetQuizShuffleAnswers value ->
|
||||||
{ model with QuizForm = { model.QuizForm with ShuffleAnswers = value } }, Cmd.none
|
{ model with QuizForm = { model.QuizForm with ShuffleAnswers = value } }, Cmd.none
|
||||||
| SelectPickerTopic topicId ->
|
| ToggleTopicInPicker topicId ->
|
||||||
|
let form = model.QuizForm
|
||||||
|
|
||||||
|
if form.PickerTopicIds.Contains topicId then
|
||||||
|
// Deselecting a topic drops it from the quiz entirely — its
|
||||||
|
// fixed picks and pool rule no longer apply.
|
||||||
|
let belongsToTopic qid =
|
||||||
|
form.PickerQuestions
|
||||||
|
|> List.tryFind (fun q -> q.Id = qid)
|
||||||
|
|> Option.map (fun q -> q.TopicId = topicId)
|
||||||
|
|> Option.defaultValue false
|
||||||
|
|
||||||
|
let sourcesWithoutTopic =
|
||||||
|
form.Sources
|
||||||
|
|> List.filter (function
|
||||||
|
| PoolDraft(tid, _) -> tid <> topicId
|
||||||
|
| FixedDraft qid -> not (belongsToTopic qid))
|
||||||
|
|
||||||
|
{ model with
|
||||||
|
QuizForm =
|
||||||
|
{ form with
|
||||||
|
PickerTopicIds = form.PickerTopicIds.Remove topicId
|
||||||
|
PickerQuestions = form.PickerQuestions |> List.filter (fun q -> q.TopicId <> topicId)
|
||||||
|
PoolCountTexts = form.PoolCountTexts.Remove topicId
|
||||||
|
Sources = sourcesWithoutTopic } },
|
||||||
|
Cmd.none
|
||||||
|
else
|
||||||
let cmd =
|
let cmd =
|
||||||
Cmd.OfAsync.either
|
Cmd.OfAsync.either
|
||||||
(Api.listQuestionsInTopic token)
|
(Api.listQuestionsInTopic token)
|
||||||
topicId
|
topicId
|
||||||
(function
|
(function
|
||||||
| Ok questions -> PickerQuestionsLoaded questions
|
| Ok questions -> PickerQuestionsLoaded(topicId, questions)
|
||||||
| Error err -> PickerQuestionsLoadFailed err)
|
| Error err -> PickerQuestionsLoadFailed(topicId, err))
|
||||||
(fun ex -> PickerQuestionsLoadFailed ex.Message)
|
(fun ex -> PickerQuestionsLoadFailed(topicId, ex.Message))
|
||||||
|
|
||||||
let existingPoolCount =
|
let existingPoolCount =
|
||||||
model.QuizForm.Sources
|
form.Sources
|
||||||
|> List.tryPick (function
|
|> List.tryPick (function
|
||||||
| PoolDraft(tid, count) when tid = topicId -> Some count
|
| PoolDraft(tid, count) when tid = topicId -> Some count
|
||||||
| _ -> None)
|
| _ -> None)
|
||||||
|
|
||||||
|
let poolCountTexts =
|
||||||
|
match existingPoolCount with
|
||||||
|
| Some count -> form.PoolCountTexts |> Map.add topicId (string count)
|
||||||
|
| None -> form.PoolCountTexts
|
||||||
|
|
||||||
|
{ model with
|
||||||
|
QuizForm =
|
||||||
|
{ form with
|
||||||
|
PickerTopicIds = form.PickerTopicIds.Add topicId
|
||||||
|
PickerLoadingTopics = form.PickerLoadingTopics.Add topicId
|
||||||
|
PoolCountTexts = poolCountTexts } },
|
||||||
|
cmd
|
||||||
|
| PickerQuestionsLoaded(topicId, questions) ->
|
||||||
{ model with
|
{ model with
|
||||||
QuizForm =
|
QuizForm =
|
||||||
{ model.QuizForm with
|
{ model.QuizForm with
|
||||||
PickerTopicId = Some topicId
|
PickerQuestions = model.QuizForm.PickerQuestions @ questions
|
||||||
PickerLoading = true
|
PickerLoadingTopics = model.QuizForm.PickerLoadingTopics.Remove topicId } },
|
||||||
PickerQuestions = []
|
Cmd.none
|
||||||
PoolCountText = existingPoolCount |> Option.map string |> Option.defaultValue "" } },
|
| PickerQuestionsLoadFailed(topicId, err) ->
|
||||||
cmd
|
{ model with
|
||||||
| PickerQuestionsLoaded questions ->
|
QuizForm =
|
||||||
{ model with QuizForm = { model.QuizForm with PickerQuestions = questions; PickerLoading = false } }, Cmd.none
|
{ model.QuizForm with
|
||||||
| PickerQuestionsLoadFailed err ->
|
PickerLoadingTopics = model.QuizForm.PickerLoadingTopics.Remove topicId
|
||||||
{ model with QuizForm = { model.QuizForm with PickerLoading = false; Error = Some err } }, Cmd.none
|
Error = Some err } },
|
||||||
|
Cmd.none
|
||||||
| ToggleQuestionPick questionId ->
|
| ToggleQuestionPick questionId ->
|
||||||
let form = model.QuizForm
|
let form = model.QuizForm
|
||||||
|
|
||||||
@@ -216,7 +256,7 @@ let update (token: string option) (msg: Msg) (model: Model) : Model * Cmd<Msg> =
|
|||||||
form.Sources @ [ FixedDraft questionId ]
|
form.Sources @ [ FixedDraft questionId ]
|
||||||
|
|
||||||
{ model with QuizForm = { form with Sources = next } }, Cmd.none
|
{ model with QuizForm = { form with Sources = next } }, Cmd.none
|
||||||
| SelectAllInTopic ->
|
| SelectAllInTopic topicId ->
|
||||||
let form = model.QuizForm
|
let form = model.QuizForm
|
||||||
|
|
||||||
let alreadySelected =
|
let alreadySelected =
|
||||||
@@ -228,19 +268,20 @@ let update (token: string option) (msg: Msg) (model: Model) : Model * Cmd<Msg> =
|
|||||||
|
|
||||||
let toAdd =
|
let toAdd =
|
||||||
form.PickerQuestions
|
form.PickerQuestions
|
||||||
|
|> List.filter (fun q -> q.TopicId = topicId)
|
||||||
|> List.map (fun q -> q.Id)
|
|> List.map (fun q -> q.Id)
|
||||||
|> List.filter (alreadySelected.Contains >> not)
|
|> List.filter (alreadySelected.Contains >> not)
|
||||||
|> List.map FixedDraft
|
|> List.map FixedDraft
|
||||||
|
|
||||||
{ model with QuizForm = { form with Sources = form.Sources @ toAdd } }, Cmd.none
|
{ model with QuizForm = { form with Sources = form.Sources @ toAdd } }, Cmd.none
|
||||||
| SetPoolCountText text -> { model with QuizForm = { model.QuizForm with PoolCountText = text } }, Cmd.none
|
| SetPoolCountText(topicId, text) ->
|
||||||
| AddRandomPool ->
|
{ model with QuizForm = { model.QuizForm with PoolCountTexts = model.QuizForm.PoolCountTexts |> Map.add topicId text } },
|
||||||
|
Cmd.none
|
||||||
|
| AddRandomPool topicId ->
|
||||||
let form = model.QuizForm
|
let form = model.QuizForm
|
||||||
|
let text = form.PoolCountTexts |> Map.tryFind topicId |> Option.defaultValue ""
|
||||||
|
|
||||||
match form.PickerTopicId with
|
match System.Int32.TryParse text with
|
||||||
| None -> model, Cmd.none
|
|
||||||
| Some topicId ->
|
|
||||||
match System.Int32.TryParse form.PoolCountText with
|
|
||||||
| true, count when count > 0 ->
|
| true, count when count > 0 ->
|
||||||
let withoutOldPool =
|
let withoutOldPool =
|
||||||
form.Sources
|
form.Sources
|
||||||
@@ -262,9 +303,7 @@ let update (token: string option) (msg: Msg) (model: Model) : Model * Cmd<Msg> =
|
|||||||
| PoolDraft(tid, _) -> tid <> topicId
|
| PoolDraft(tid, _) -> tid <> topicId
|
||||||
| _ -> true)
|
| _ -> true)
|
||||||
|
|
||||||
let poolCountText = if form.PickerTopicId = Some topicId then "" else form.PoolCountText
|
{ model with QuizForm = { form with Sources = next; PoolCountTexts = form.PoolCountTexts.Remove topicId } }, Cmd.none
|
||||||
|
|
||||||
{ model with QuizForm = { form with Sources = next; PoolCountText = poolCountText } }, Cmd.none
|
|
||||||
| SubmitQuizForm ->
|
| SubmitQuizForm ->
|
||||||
match buildQuizFields model.QuizForm with
|
match buildQuizFields model.QuizForm with
|
||||||
| Error err -> { model with QuizForm = { model.QuizForm with Error = Some err } }, Cmd.none
|
| Error err -> { model with QuizForm = { model.QuizForm with Error = Some err } }, Cmd.none
|
||||||
|
|||||||
@@ -21,16 +21,19 @@ type QuizForm =
|
|||||||
PassingScoreText: string // empty = None
|
PassingScoreText: string // empty = None
|
||||||
ShuffleQuestions: bool
|
ShuffleQuestions: bool
|
||||||
ShuffleAnswers: bool
|
ShuffleAnswers: bool
|
||||||
/// Accumulates across topic switches in the picker below — membership
|
/// Accumulates across topics selected in the picker below — membership
|
||||||
/// alone decides the checkbox state, regardless of which topic's
|
/// alone decides the checkbox state, regardless of which topics'
|
||||||
/// questions are currently displayed.
|
/// questions are currently displayed.
|
||||||
Sources: QuizSourceDraft list
|
Sources: QuizSourceDraft list
|
||||||
PickerTopicId: TopicId option
|
/// Topics currently expanded in the picker — a test can draw questions
|
||||||
|
/// from any number of them at once.
|
||||||
|
PickerTopicIds: Set<TopicId>
|
||||||
|
/// Union of questions loaded for every topic in `PickerTopicIds`.
|
||||||
PickerQuestions: QuestionSummary list
|
PickerQuestions: QuestionSummary list
|
||||||
PickerLoading: bool
|
/// Topics whose questions are still being fetched.
|
||||||
/// Draft text for "N случайных вопросов" of the topic currently open
|
PickerLoadingTopics: Set<TopicId>
|
||||||
/// in the picker — reset whenever the picker's topic changes.
|
/// Draft text for "N случайных вопросов", one per expanded topic.
|
||||||
PoolCountText: string
|
PoolCountTexts: Map<TopicId, string>
|
||||||
Error: string option
|
Error: string option
|
||||||
IsSubmitting: bool }
|
IsSubmitting: bool }
|
||||||
|
|
||||||
@@ -43,30 +46,27 @@ let emptyQuizForm =
|
|||||||
ShuffleQuestions = false
|
ShuffleQuestions = false
|
||||||
ShuffleAnswers = false
|
ShuffleAnswers = false
|
||||||
Sources = []
|
Sources = []
|
||||||
PickerTopicId = None
|
PickerTopicIds = Set.empty
|
||||||
PickerQuestions = []
|
PickerQuestions = []
|
||||||
PickerLoading = false
|
PickerLoadingTopics = Set.empty
|
||||||
PoolCountText = ""
|
PoolCountTexts = Map.empty
|
||||||
Error = None
|
Error = None
|
||||||
IsSubmitting = false }
|
IsSubmitting = false }
|
||||||
|
|
||||||
/// Ids of `form.PickerQuestions` (the topic currently open in the picker)
|
/// Loaded questions belonging to one topic.
|
||||||
/// that are individually fixed-selected — used to enforce "fixed selection
|
let questionsInTopic (form: QuizForm) (topicId: TopicId) : QuestionSummary list =
|
||||||
/// XOR random pool" per topic, one topic at a time.
|
form.PickerQuestions |> List.filter (fun q -> q.TopicId = topicId)
|
||||||
let fixedIdsInCurrentTopic (form: QuizForm) : QuestionId list =
|
|
||||||
let topicIds = form.PickerQuestions |> List.map (fun q -> q.Id) |> Set.ofList
|
|
||||||
|
|
||||||
|
/// Whether a question is individually fixed-selected — used to enforce
|
||||||
|
/// "fixed selection XOR random pool" per topic.
|
||||||
|
let isFixedSelected (form: QuizForm) (questionId: QuestionId) : bool =
|
||||||
form.Sources
|
form.Sources
|
||||||
|> List.choose (function
|
|> List.exists (function
|
||||||
| FixedDraft qid when topicIds.Contains qid -> Some qid
|
| FixedDraft qid -> qid = questionId
|
||||||
| _ -> None)
|
| _ -> false)
|
||||||
|
|
||||||
/// The random-pool count configured for the topic currently open in the
|
/// The random-pool count configured for a given topic, if any.
|
||||||
/// picker, if any.
|
let poolCountForTopic (form: QuizForm) (topicId: TopicId) : int option =
|
||||||
let poolCountForCurrentTopic (form: QuizForm) : int option =
|
|
||||||
match form.PickerTopicId with
|
|
||||||
| None -> None
|
|
||||||
| Some topicId ->
|
|
||||||
form.Sources
|
form.Sources
|
||||||
|> List.tryPick (function
|
|> List.tryPick (function
|
||||||
| PoolDraft(tid, count) when tid = topicId -> Some count
|
| PoolDraft(tid, count) when tid = topicId -> Some count
|
||||||
@@ -162,13 +162,13 @@ type Msg =
|
|||||||
| SetQuizPassingScoreText of string
|
| SetQuizPassingScoreText of string
|
||||||
| SetQuizShuffleQuestions of bool
|
| SetQuizShuffleQuestions of bool
|
||||||
| SetQuizShuffleAnswers of bool
|
| SetQuizShuffleAnswers of bool
|
||||||
| SelectPickerTopic of TopicId
|
| ToggleTopicInPicker of TopicId
|
||||||
| PickerQuestionsLoaded of QuestionSummary list
|
| PickerQuestionsLoaded of TopicId * QuestionSummary list
|
||||||
| PickerQuestionsLoadFailed of string
|
| PickerQuestionsLoadFailed of TopicId * string
|
||||||
| ToggleQuestionPick of QuestionId
|
| ToggleQuestionPick of QuestionId
|
||||||
| SelectAllInTopic
|
| SelectAllInTopic of TopicId
|
||||||
| SetPoolCountText of string
|
| SetPoolCountText of TopicId * string
|
||||||
| AddRandomPool
|
| AddRandomPool of TopicId
|
||||||
| RemoveRandomPool of TopicId
|
| RemoveRandomPool of TopicId
|
||||||
| SubmitQuizForm
|
| SubmitQuizForm
|
||||||
| QuizSaved of QuizAdminSummary
|
| QuizSaved of QuizAdminSummary
|
||||||
|
|||||||
@@ -203,16 +203,16 @@ let private resultsView (model: Model) dispatch =
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
/// Summary of every configured random-pool rule, across all topics — needed
|
/// Summary of configured random-pool rules whose topic isn't currently
|
||||||
/// because the picker below only shows one topic's questions at a time, so
|
/// expanded in the picker below (e.g. right after opening an existing quiz
|
||||||
/// without this list, switching the topic dropdown away would make an
|
/// for editing) — without this, such a pool rule would be invisible until
|
||||||
/// already-configured pool rule invisible.
|
/// its topic checkbox is ticked again.
|
||||||
let private poolRulesSummary (model: Model) dispatch =
|
let private poolRulesSummary (model: Model) dispatch =
|
||||||
let pools =
|
let pools =
|
||||||
model.QuizForm.Sources
|
model.QuizForm.Sources
|
||||||
|> List.choose (function
|
|> List.choose (function
|
||||||
| PoolDraft(topicId, count) -> Some(topicId, count)
|
| PoolDraft(topicId, count) when not (model.QuizForm.PickerTopicIds.Contains topicId) -> Some(topicId, count)
|
||||||
| FixedDraft _ -> None)
|
| _ -> None)
|
||||||
|
|
||||||
if pools.IsEmpty then
|
if pools.IsEmpty then
|
||||||
Html.none
|
Html.none
|
||||||
@@ -241,33 +241,33 @@ let private poolRulesSummary (model: Model) dispatch =
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
|
||||||
let private questionPicker (model: Model) dispatch =
|
/// One expanded topic's slice of the picker: either its fixed-question
|
||||||
|
/// checkboxes, or its random-pool config, plus the "N нет вопросов"/loading
|
||||||
|
/// states.
|
||||||
|
let private topicPickerSection (model: Model) dispatch topicId =
|
||||||
let form = model.QuizForm
|
let form = model.QuizForm
|
||||||
let poolForTopic = poolCountForCurrentTopic form
|
let topicName =
|
||||||
let fixedInTopic = fixedIdsInCurrentTopic form
|
model.Topics
|
||||||
|
|> List.tryFind (fun t -> t.Id = topicId)
|
||||||
|
|> Option.map (fun t -> t.Name)
|
||||||
|
|> Option.defaultValue "?"
|
||||||
|
|
||||||
|
let topicQuestions = questionsInTopic form topicId
|
||||||
|
let poolCount = poolCountForTopic form topicId
|
||||||
|
let fixedInTopic = topicQuestions |> List.map (fun q -> q.Id) |> List.filter (isFixedSelected form)
|
||||||
|
let poolCountText = form.PoolCountTexts |> Map.tryFind topicId |> Option.defaultValue ""
|
||||||
|
|
||||||
Html.div [
|
Html.div [
|
||||||
prop.className "question-picker"
|
prop.key (string topicId)
|
||||||
|
prop.className "topic-picker-section"
|
||||||
prop.children [
|
prop.children [
|
||||||
Html.label [ prop.text "Тема" ]
|
Html.h4 topicName
|
||||||
Html.select [
|
if form.PickerLoadingTopics.Contains topicId then
|
||||||
prop.value (form.PickerTopicId |> Option.map string |> Option.defaultValue "")
|
|
||||||
prop.onChange (fun (v: string) ->
|
|
||||||
model.Topics
|
|
||||||
|> List.tryFind (fun t -> string t.Id = v)
|
|
||||||
|> Option.iter (fun t -> dispatch (SelectPickerTopic t.Id)))
|
|
||||||
prop.children [
|
|
||||||
Html.option [ prop.value ""; prop.text "— выберите тему —" ]
|
|
||||||
for topic in model.Topics do
|
|
||||||
Html.option [ prop.key (string topic.Id); prop.value (string topic.Id); prop.text topic.Name ]
|
|
||||||
]
|
|
||||||
]
|
|
||||||
if form.PickerLoading then
|
|
||||||
Html.p "Загрузка вопросов…"
|
Html.p "Загрузка вопросов…"
|
||||||
elif form.PickerTopicId.IsSome && form.PickerQuestions.IsEmpty then
|
elif topicQuestions.IsEmpty then
|
||||||
Html.p "В этой теме нет вопросов"
|
Html.p "В этой теме нет вопросов"
|
||||||
elif form.PickerTopicId.IsSome then
|
else
|
||||||
match poolForTopic with
|
match poolCount with
|
||||||
| Some count ->
|
| Some count ->
|
||||||
Html.div [
|
Html.div [
|
||||||
prop.className "pool-picker"
|
prop.className "pool-picker"
|
||||||
@@ -280,18 +280,17 @@ let private questionPicker (model: Model) dispatch =
|
|||||||
Html.input [
|
Html.input [
|
||||||
prop.type'.text
|
prop.type'.text
|
||||||
prop.className "pool-count-input"
|
prop.className "pool-count-input"
|
||||||
prop.value form.PoolCountText
|
prop.value poolCountText
|
||||||
prop.onChange (SetPoolCountText >> dispatch)
|
prop.onChange (fun v -> dispatch (SetPoolCountText(topicId, v)))
|
||||||
]
|
]
|
||||||
Html.button [
|
Html.button [
|
||||||
prop.type'.button
|
prop.type'.button
|
||||||
prop.onClick (fun _ -> dispatch AddRandomPool)
|
prop.onClick (fun _ -> dispatch (AddRandomPool topicId))
|
||||||
prop.text "Обновить количество"
|
prop.text "Обновить количество"
|
||||||
]
|
]
|
||||||
Html.button [
|
Html.button [
|
||||||
prop.type'.button
|
prop.type'.button
|
||||||
prop.onClick (fun _ ->
|
prop.onClick (fun _ -> dispatch (RemoveRandomPool topicId))
|
||||||
form.PickerTopicId |> Option.iter (RemoveRandomPool >> dispatch))
|
|
||||||
prop.text "Убрать случайный набор, выбирать вручную"
|
prop.text "Убрать случайный набор, выбирать вручную"
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
@@ -301,7 +300,7 @@ let private questionPicker (model: Model) dispatch =
|
|||||||
prop.children [
|
prop.children [
|
||||||
Html.div [
|
Html.div [
|
||||||
prop.children [
|
prop.children [
|
||||||
for q in form.PickerQuestions ->
|
for q in topicQuestions ->
|
||||||
Html.label [
|
Html.label [
|
||||||
prop.key (string q.Id)
|
prop.key (string q.Id)
|
||||||
prop.className "student-row"
|
prop.className "student-row"
|
||||||
@@ -318,7 +317,7 @@ let private questionPicker (model: Model) dispatch =
|
|||||||
]
|
]
|
||||||
Html.button [
|
Html.button [
|
||||||
prop.type'.button
|
prop.type'.button
|
||||||
prop.onClick (fun _ -> dispatch SelectAllInTopic)
|
prop.onClick (fun _ -> dispatch (SelectAllInTopic topicId))
|
||||||
prop.text "Выбрать все вопросы темы"
|
prop.text "Выбрать все вопросы темы"
|
||||||
]
|
]
|
||||||
Html.p "или задать случайный набор вместо ручного выбора:"
|
Html.p "или задать случайный набор вместо ручного выбора:"
|
||||||
@@ -326,13 +325,13 @@ let private questionPicker (model: Model) dispatch =
|
|||||||
prop.type'.text
|
prop.type'.text
|
||||||
prop.className "pool-count-input"
|
prop.className "pool-count-input"
|
||||||
prop.placeholder "Число вопросов"
|
prop.placeholder "Число вопросов"
|
||||||
prop.value form.PoolCountText
|
prop.value poolCountText
|
||||||
prop.onChange (SetPoolCountText >> dispatch)
|
prop.onChange (fun v -> dispatch (SetPoolCountText(topicId, v)))
|
||||||
]
|
]
|
||||||
Html.button [
|
Html.button [
|
||||||
prop.type'.button
|
prop.type'.button
|
||||||
prop.disabled (not fixedInTopic.IsEmpty)
|
prop.disabled (not fixedInTopic.IsEmpty)
|
||||||
prop.onClick (fun _ -> dispatch AddRandomPool)
|
prop.onClick (fun _ -> dispatch (AddRandomPool topicId))
|
||||||
prop.text "Добавить случайный набор из этой темы"
|
prop.text "Добавить случайный набор из этой темы"
|
||||||
]
|
]
|
||||||
if not fixedInTopic.IsEmpty then
|
if not fixedInTopic.IsEmpty then
|
||||||
@@ -342,8 +341,42 @@ let private questionPicker (model: Model) dispatch =
|
|||||||
]
|
]
|
||||||
]
|
]
|
||||||
]
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
|
||||||
|
let private questionPicker (model: Model) dispatch =
|
||||||
|
let form = model.QuizForm
|
||||||
|
|
||||||
|
Html.div [
|
||||||
|
prop.className "question-picker"
|
||||||
|
prop.children [
|
||||||
|
Html.label [ prop.text "Темы (можно выбрать несколько)" ]
|
||||||
|
Html.div [
|
||||||
|
prop.className "topic-checkbox-list"
|
||||||
|
prop.children [
|
||||||
|
for topic in model.Topics ->
|
||||||
|
Html.label [
|
||||||
|
prop.key (string topic.Id)
|
||||||
|
prop.className "student-row"
|
||||||
|
prop.children [
|
||||||
|
Html.input [
|
||||||
|
prop.type'.checkbox
|
||||||
|
prop.isChecked (form.PickerTopicIds.Contains topic.Id)
|
||||||
|
prop.onChange (fun (_: bool) -> dispatch (ToggleTopicInPicker topic.Id))
|
||||||
|
]
|
||||||
|
Html.text topic.Name
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
]
|
||||||
|
if form.PickerTopicIds.IsEmpty then
|
||||||
|
Html.p [
|
||||||
|
prop.className "hint"
|
||||||
|
prop.text "Выберите одну или несколько тем, из которых будут вопросы теста"
|
||||||
|
]
|
||||||
else
|
else
|
||||||
Html.none
|
for topicId in form.PickerTopicIds |> Set.toList do
|
||||||
|
topicPickerSection model dispatch topicId
|
||||||
poolRulesSummary model dispatch
|
poolRulesSummary model dispatch
|
||||||
Html.span [
|
Html.span [
|
||||||
prop.className "tag-mono"
|
prop.className "tag-mono"
|
||||||
|
|||||||
Reference in New Issue
Block a user