Add "Other" option to radio buttons #47
No files matched your search
@@ -2,6 +2,7 @@ use crate::common::ValueType;
|
|||||||
use crate::components::utils::create_unique_id;
|
use crate::components::utils::create_unique_id;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use leptos::html::Input;
|
||||||
use crate::components::{use_validation_context, InputState, ValidationState};
|
use crate::components::{use_validation_context, InputState, ValidationState};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
@@ -20,9 +21,7 @@ pub fn Radio(
|
|||||||
// Using the "peer" class links this input with the div "peer" at the same level.
|
// Using the "peer" class links this input with the div "peer" at the same level.
|
||||||
// By using the "peer-checked" selector this div can toggle states based on this
|
// By using the "peer-checked" selector this div can toggle states based on this
|
||||||
// input's state (checked/unchecked)
|
// input's state (checked/unchecked)
|
||||||
class="
|
class="relative peer shrink-0 hidden"
|
||||||
relative peer shrink-0
|
|
||||||
hidden"
|
|
||||||
type="radio"
|
type="radio"
|
||||||
id=id.clone()
|
id=id.clone()
|
||||||
name=name
|
name=name
|
||||||
@@ -43,7 +42,95 @@ pub fn Radio(
|
|||||||
rotate-45 transform border-3 border-3 rounded-full bg-white
|
rotate-45 transform border-3 border-3 rounded-full bg-white
|
||||||
border-white transition-opacity duration-150"></span>
|
border-white transition-opacity duration-150"></span>
|
||||||
</div>
|
</div>
|
||||||
<span>{value.clone()}</span>
|
<span class="px-0.5 py-0.5 flex-1">{value.clone()}</span>
|
||||||
|
</label>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Ratio Other Component
|
||||||
|
/// This is a simple "other" field that contains a text input component. It can be selected using the
|
||||||
|
/// same logic as the [`Radio`] component, and also has a secondary `RwSignal` that contains the
|
||||||
|
/// current value of the embedded text input.
|
||||||
|
///
|
||||||
|
/// Example usage: <still working on this>
|
||||||
|
#[component]
|
||||||
|
fn RadioOther(
|
||||||
|
#[prop(into)] name: Signal<String>,
|
||||||
|
#[prop(into)] checked: Signal<bool>,
|
||||||
|
#[prop(into)] on_change: Callback<()>,
|
||||||
|
#[prop(into)] disabled: Signal<bool>,
|
||||||
|
#[prop(optional, into)] value: RwSignal<String>,
|
||||||
|
#[prop(into)] dirty: RwSignal<bool>,
|
||||||
|
#[prop(into)] placeholder: Signal<String>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let id = create_unique_id(&name.get_untracked(), &"other".to_string());
|
||||||
|
let input_ref = NodeRef::<Input>::new();
|
||||||
|
|
||||||
|
let input_disabled = Signal::derive(move || {
|
||||||
|
!checked.get() || disabled.get()
|
||||||
|
});
|
||||||
|
|
||||||
|
Effect::new(move || {
|
||||||
|
checked.get()
|
||||||
|
.then(|| {
|
||||||
|
if let Some(node) = input_ref.get() {
|
||||||
|
node.focus().expect("Couldn't focus input element");
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let on_change_text = move |ev| {
|
||||||
|
let val = event_target_value(&ev);
|
||||||
|
|
||||||
|
value.set(val);
|
||||||
|
|
||||||
|
on_change.run(());
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_blur_text = move |_| {
|
||||||
|
dirty.set(true);
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<label for=id class="flex items_center">
|
||||||
|
<input
|
||||||
|
// Using the "peer" class links this input with the div "peer" at the same level.
|
||||||
|
// By using the "peer-checked" selector this div can toggle states based on this
|
||||||
|
// input's state (checked/unchecked)
|
||||||
|
class="relative peer shrink-0 hidden"
|
||||||
|
type="radio"
|
||||||
|
id=id.clone()
|
||||||
|
name=name
|
||||||
|
on:change=move |_| on_change.run(())
|
||||||
|
prop:checked=checked
|
||||||
|
disabled=disabled
|
||||||
|
/>
|
||||||
|
// This div displays the box for the checkbox, and applies some styling to the child
|
||||||
|
// span element.
|
||||||
|
<div class="relative inline-block min-h-5 min-w-5 h-5 w-5 m-1 mr-2 rounded-full border border-gray-300 bg-white
|
||||||
|
transition-bg duration-200 peer-checked:bg-red-700 peer-checked:border-red-700
|
||||||
|
[&>span]:opacity-0 peer-checked:[&>span]:opacity-100
|
||||||
|
peer-disabled:bg-gray-600/33 peer-disabled:border-gray-600
|
||||||
|
peer-disabled:peer-checked:bg-gray-600">
|
||||||
|
|
||||||
|
// This span is the check icon. It is centered within the div and has a border on the bottom
|
||||||
|
// and right sides, which is then rotated to appear like a checkmark.
|
||||||
|
<span
|
||||||
|
class="absolute left-1/2 top-1/2 block h-2.5 w-2.5 -translate-x-1/2 -translate-y-1/2
|
||||||
|
rotate-45 transform border-3 border-3 rounded-full bg-white
|
||||||
|
border-white transition-opacity duration-150"></span>
|
||||||
|
</div>
|
||||||
|
<input
|
||||||
|
node_ref=input_ref
|
||||||
|
class="border-b-2 mb-1.5 px-1.5 py-0.5 rounded-t-xs bg-transparent relative flex-1 transition-all duration-150
|
||||||
|
border-red-700 disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
|
||||||
|
r#type="text"
|
||||||
|
disabled=input_disabled
|
||||||
|
placeholder=placeholder
|
||||||
|
prop:value=value
|
||||||
|
on:input=on_change_text
|
||||||
|
on:blur=on_blur_text
|
||||||
|
/>
|
||||||
</label>
|
</label>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -64,24 +151,47 @@ pub fn RadioList(
|
|||||||
#[prop(optional, into)] disabled: Signal<bool>,
|
#[prop(optional, into)] disabled: Signal<bool>,
|
||||||
#[prop(optional, into)] label: String,
|
#[prop(optional, into)] label: String,
|
||||||
#[prop(optional, into)] required: Signal<bool>,
|
#[prop(optional, into)] required: Signal<bool>,
|
||||||
|
#[prop(optional, into)] other_prompt: Signal<String>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
//#region Value Logic
|
//#region Value Logic
|
||||||
|
|
||||||
// Store a single signal that is the currently selected value. We can only have one selected
|
// Store a single signal that is the currently selected value. We can only have one selected
|
||||||
// value, so there's no list needed. The default selected value is whatever the map has selected
|
// value, so there's no list needed. The default selected value is whatever the map has selected
|
||||||
// already, or nothing.
|
// already, or nothing.
|
||||||
let default_value = match data_map.get_untracked().get(&data_member.get_untracked()) {
|
|
||||||
Some(ValueType::String(Some(value))) => value.clone(),
|
|
||||||
_ => String::new()
|
|
||||||
};
|
|
||||||
|
|
||||||
let selected_value = RwSignal::new(default_value);
|
let hydrated = RwSignal::new(false);
|
||||||
|
let selected_value = RwSignal::new(String::new());
|
||||||
|
let other_checked = RwSignal::new(false);
|
||||||
|
let other_value = RwSignal::new(String::new());
|
||||||
|
|
||||||
// When the selected value changes, update the data map at the correct location
|
// Hydration effect that resolves data. Sets up all data properly.
|
||||||
|
Effect::new({
|
||||||
|
let items = items.clone();
|
||||||
|
move || {
|
||||||
|
if hydrated.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let default_value = match data_map.get_untracked().get(&data_member.get_untracked()) {
|
||||||
|
Some(ValueType::String(Some(value))) => value.clone(),
|
||||||
|
_ => String::new()
|
||||||
|
};
|
||||||
|
|
||||||
|
if !items.contains(&default_value) && !default_value.is_empty() {
|
||||||
|
other_checked.set(true);
|
||||||
|
other_value.set(default_value.clone());
|
||||||
|
}
|
||||||
|
|
||||||
|
selected_value.set(default_value);
|
||||||
|
|
||||||
|
hydrated.set(true);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// When the other option is selected and the other value changes, update the selected value.
|
||||||
Effect::new(move || {
|
Effect::new(move || {
|
||||||
data_map.update(|map| {
|
other_checked.get()
|
||||||
map.insert(data_member.get(), ValueType::String(Some(selected_value.get())));
|
.then(|| selected_value.set(other_value.get()));
|
||||||
});
|
|
||||||
});
|
});
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
@@ -109,7 +219,15 @@ pub fn RadioList(
|
|||||||
list.retain(|v| *v.get_untracked().input_name != *input_state.get_untracked().input_name)
|
list.retain(|v| *v.get_untracked().input_name != *input_state.get_untracked().input_name)
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// When the selected value changes, update the data map at the correct location and validate.
|
||||||
|
Effect::new(move || {
|
||||||
|
error.set(validate(selected_value.get(), required.get()));
|
||||||
|
data_map.update(|map| {
|
||||||
|
map.insert(data_member.get(), ValueType::String(Some(selected_value.get())));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
//#endregion
|
//#endregion
|
||||||
//#region Render Logic
|
//#region Render Logic
|
||||||
|
|
||||||
@@ -123,12 +241,13 @@ pub fn RadioList(
|
|||||||
let item = RwSignal::new(item);
|
let item = RwSignal::new(item);
|
||||||
|
|
||||||
let checked_signal = Signal::derive(move || {
|
let checked_signal = Signal::derive(move || {
|
||||||
selected_value.get() == item.get()
|
selected_value.get() == item.get() && !other_checked.get()
|
||||||
});
|
});
|
||||||
|
|
||||||
let on_change = move || {
|
let on_change = move || {
|
||||||
// on_change only runs when the radio button is checked, not when unchecked.
|
// on_change only runs when the radio button is checked, not when unchecked.
|
||||||
// Change the selected value to the new one.
|
// Change the selected value to the new one.
|
||||||
|
other_checked.set(false);
|
||||||
selected_value.set(item.get());
|
selected_value.set(item.get());
|
||||||
error.set(validate(selected_value.get_untracked(), required.get_untracked()));
|
error.set(validate(selected_value.get_untracked(), required.get_untracked()));
|
||||||
dirty.set(true);
|
dirty.set(true);
|
||||||
@@ -146,6 +265,18 @@ pub fn RadioList(
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
.collect::<Vec<_>>()}
|
.collect::<Vec<_>>()}
|
||||||
|
|
||||||
|
<Show when=move || !other_prompt.get().is_empty()>
|
||||||
|
<RadioOther
|
||||||
|
checked=other_checked
|
||||||
|
name=data_member
|
||||||
|
disabled=disabled
|
||||||
|
value=other_value
|
||||||
|
on_change=move |_| other_checked.set(true)
|
||||||
|
placeholder=other_prompt
|
||||||
|
dirty=dirty
|
||||||
|
/>
|
||||||
|
</Show>
|
||||||
</div>
|
</div>
|
||||||
<Show when=move || show_errors.get()>
|
<Show when=move || show_errors.get()>
|
||||||
<div class="text-red-600 text-sm mr-1.5 ml-1.5">
|
<div class="text-red-600 text-sm mr-1.5 ml-1.5">
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ use crate::common::{
|
|||||||
use leptos::prelude::ServerFnError;
|
use leptos::prelude::ServerFnError;
|
||||||
use leptos::server;
|
use leptos::server;
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
static COMPARISONS_TABLE: &str = "leptos-comparison-test";
|
static COMPARISONS_TABLE: &str = "leptos-comparison-test";
|
||||||
|
|
||||||
/// Helper function to create all sports comparisons. Only used to create initial
|
/// Helper function to create all sports comparisons. Only used to create initial
|
||||||
|
|||||||
@@ -17,6 +17,7 @@ use crate::common::ExpandableInfo;
|
|||||||
use leptos::prelude::ServerFnError;
|
use leptos::prelude::ServerFnError;
|
||||||
use leptos::server;
|
use leptos::server;
|
||||||
|
|
||||||
|
#[cfg(feature = "ssr")]
|
||||||
static SCHOLARSHIPS_TABLE: &str = "leptos-scholarship-test";
|
static SCHOLARSHIPS_TABLE: &str = "leptos-scholarship-test";
|
||||||
|
|
||||||
#[server(GetScholarshipInfo, endpoint = "/scholarship/info/get")]
|
#[server(GetScholarshipInfo, endpoint = "/scholarship/info/get")]
|
||||||
|
|||||||
+2
-24
@@ -8,7 +8,7 @@ use std::process::{Command, Stdio};
|
|||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use crate::common::{ComparisonData, ExpandableInfo};
|
use crate::common::ExpandableInfo;
|
||||||
use crate::pages::UnauthenticatedPage;
|
use crate::pages::UnauthenticatedPage;
|
||||||
use crate::pages::utils::get_user_claims;
|
use crate::pages::utils::get_user_claims;
|
||||||
use crate::components::{ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row, Select, TextFieldType, ValidatedForm};
|
use crate::components::{ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row, Select, TextFieldType, ValidatedForm};
|
||||||
@@ -22,7 +22,6 @@ use leptos::task::spawn_local;
|
|||||||
use leptos::web_sys::HtmlAnchorElement;
|
use leptos::web_sys::HtmlAnchorElement;
|
||||||
use leptos::wasm_bindgen::JsCast;
|
use leptos::wasm_bindgen::JsCast;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use crate::pages::api::get_comparisons_categorized;
|
|
||||||
|
|
||||||
/// # Get Student Info
|
/// # Get Student Info
|
||||||
/// Gets a student's information given their `subject`.
|
/// Gets a student's information given their `subject`.
|
||||||
@@ -210,28 +209,6 @@ pub fn HomePage() -> impl IntoView {
|
|||||||
};
|
};
|
||||||
//#endregion
|
//#endregion
|
||||||
|
|
||||||
let comparison_resource = Resource::new(
|
|
||||||
move || user_subject.get(),
|
|
||||||
async move |_| get_comparisons_categorized().await
|
|
||||||
);
|
|
||||||
|
|
||||||
let comparison_info = RwSignal::new(HashMap::<String, Vec<ComparisonData>>::new());
|
|
||||||
|
|
||||||
Effect::new(move || {
|
|
||||||
match comparison_resource.get() {
|
|
||||||
Some(Ok(comparisons)) => {
|
|
||||||
log!("Found comparison info: {:?}", comparisons);
|
|
||||||
comparison_info.set(comparisons);
|
|
||||||
}
|
|
||||||
Some(Err(err)) => {
|
|
||||||
log!("Error while getting comparisons: {:?}", err);
|
|
||||||
}
|
|
||||||
_ => {
|
|
||||||
log!("An unknown error occurred.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<AuthLoaded fallback=Loading>
|
<AuthLoaded fallback=Loading>
|
||||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||||
@@ -331,6 +308,7 @@ pub fn HomePage() -> impl IntoView {
|
|||||||
data_member="town"
|
data_member="town"
|
||||||
data_map=form_data
|
data_map=form_data
|
||||||
required=true
|
required=true
|
||||||
|
other_prompt="Other..."
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
|
|||||||
@@ -361,42 +361,7 @@ fn ScholarshipForm(
|
|||||||
/// submission fails, this function is not run.
|
/// submission fails, this function is not run.
|
||||||
#[prop(into)] on_submit: Callback<()>
|
#[prop(into)] on_submit: Callback<()>
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let submit_action = ServerAction::<CreateScholarshipInfo>::new();
|
//#region Form Data Setup
|
||||||
|
|
||||||
let submit_status = RwSignal::new(SubmitStatus::Idle);
|
|
||||||
let elements_disabled = Signal::derive(move || {
|
|
||||||
matches!(submit_action.pending().get(), true)
|
|
||||||
});
|
|
||||||
|
|
||||||
Effect::new(move || {
|
|
||||||
if submit_action.pending().get() {
|
|
||||||
submit_status.set(SubmitStatus::Sending);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
if let Some(result) = submit_action.value().get() {
|
|
||||||
match result {
|
|
||||||
Ok(()) => {
|
|
||||||
submit_status.set(SubmitStatus::Success);
|
|
||||||
on_submit.run(());
|
|
||||||
},
|
|
||||||
Err(_err) => submit_status.set(SubmitStatus::Error(()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let comparison_lists = Resource::new(
|
|
||||||
move || scholarship_id.get().is_some(),
|
|
||||||
async move |_| {
|
|
||||||
get_comparisons_categorized().await
|
|
||||||
}
|
|
||||||
);
|
|
||||||
|
|
||||||
Effect::new(move || {
|
|
||||||
if let Some(Ok(map)) = comparison_lists.get() {
|
|
||||||
log!("Available categories: {:?}", map.keys());
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
let scholarship_info = Resource::new(
|
let scholarship_info = Resource::new(
|
||||||
move || scholarship_id.get(),
|
move || scholarship_id.get(),
|
||||||
@@ -428,6 +393,22 @@ fn ScholarshipForm(
|
|||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
//#region Comparison Logic
|
||||||
|
|
||||||
|
let comparison_lists = Resource::new(
|
||||||
|
move || scholarship_id.get().is_some(),
|
||||||
|
async move |_| {
|
||||||
|
get_comparisons_categorized().await
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
Effect::new(move || {
|
||||||
|
if let Some(Ok(map)) = comparison_lists.get() {
|
||||||
|
log!("Available categories: {:?}", map.keys());
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let get_comp_category = move |category: &str| {
|
let get_comp_category = move |category: &str| {
|
||||||
if let Some(Ok(map)) = comparison_lists.get() {
|
if let Some(Ok(map)) = comparison_lists.get() {
|
||||||
map.get(category).unwrap_or(&Vec::new()).clone()
|
map.get(category).unwrap_or(&Vec::new()).clone()
|
||||||
@@ -469,6 +450,33 @@ fn ScholarshipForm(
|
|||||||
let major_ids = Signal::derive(move || get_comp_ids("Majors"));
|
let major_ids = Signal::derive(move || get_comp_ids("Majors"));
|
||||||
let major_text = Signal::derive(move || get_comp_text("Majors"));
|
let major_text = Signal::derive(move || get_comp_text("Majors"));
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
//#region Submission Logic
|
||||||
|
|
||||||
|
let submit_action = ServerAction::<CreateScholarshipInfo>::new();
|
||||||
|
|
||||||
|
let submit_status = RwSignal::new(SubmitStatus::Idle);
|
||||||
|
let elements_disabled = Signal::derive(move || {
|
||||||
|
matches!(submit_action.pending().get(), true)
|
||||||
|
});
|
||||||
|
|
||||||
|
Effect::new(move || {
|
||||||
|
if submit_action.pending().get() {
|
||||||
|
submit_status.set(SubmitStatus::Sending);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
if let Some(result) = submit_action.value().get() {
|
||||||
|
match result {
|
||||||
|
Ok(()) => {
|
||||||
|
submit_status.set(SubmitStatus::Success);
|
||||||
|
on_submit.run(());
|
||||||
|
},
|
||||||
|
Err(_err) => submit_status.set(SubmitStatus::Error(()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
let mut toasts = expect_context::<ToastContext>();
|
let mut toasts = expect_context::<ToastContext>();
|
||||||
|
|
||||||
let on_submit = move |_| {
|
let on_submit = move |_| {
|
||||||
@@ -481,7 +489,7 @@ fn ScholarshipForm(
|
|||||||
info
|
info
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
Effect::new(move || {
|
Effect::new(move || {
|
||||||
submit_action.value().get().is_some()
|
submit_action.value().get().is_some()
|
||||||
.then(|| {
|
.then(|| {
|
||||||
@@ -496,6 +504,8 @@ fn ScholarshipForm(
|
|||||||
});
|
});
|
||||||
});
|
});
|
||||||
|
|
||||||
|
//#endregion
|
||||||
|
|
||||||
let create_comps = ServerAction::<CreateTestComparisons>::new();
|
let create_comps = ServerAction::<CreateTestComparisons>::new();
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
@@ -586,25 +596,21 @@ fn ScholarshipForm(
|
|||||||
label="Will the award be made to the school or the student?"
|
label="Will the award be made to the school or the student?"
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
|
||||||
<RadioList
|
|
||||||
data_member="essay_required"
|
|
||||||
data_map=form_data
|
|
||||||
items=vec!["Yes".to_string(), "No".to_string()]
|
|
||||||
disabled=elements_disabled
|
|
||||||
label="Do you require a student essay?"
|
|
||||||
/>
|
|
||||||
</Row>
|
|
||||||
<Row>
|
<Row>
|
||||||
<RadioList
|
<RadioList
|
||||||
data_member="essay_prompt"
|
data_member="essay_prompt"
|
||||||
data_map=form_data
|
data_map=form_data
|
||||||
items=vec!["Test 1", "Test 2", "Test 3"]
|
items=vec![
|
||||||
|
"Write an essay describing your major/career direction. Include any abilities, interests, experiences, employment or clubs you have had that have affected your direction. If you are undecided, indicate why.",
|
||||||
|
"Why is community service important? How has it personally affected you and your goals and what role do you hope to play in it in the future?",
|
||||||
|
"How has participation in sports been beneficial to your personal, academic, and emotional growth?"
|
||||||
|
]
|
||||||
.iter()
|
.iter()
|
||||||
.map(|s| s.to_string())
|
.map(|prompt| prompt.to_string())
|
||||||
.collect()
|
.collect()
|
||||||
disabled=elements_disabled
|
disabled=elements_disabled
|
||||||
label="If so, select a prompt from the list below."
|
label="Select an essay prompt, or leave blank if none is required:"
|
||||||
|
other_prompt="Custom prompt..."
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
|
|||||||
Reference in new issue
Block a user