Implement error handling #41
No files matched your search
Generated
+1
@@ -2947,6 +2947,7 @@ dependencies = [
|
|||||||
"leptos_oidc",
|
"leptos_oidc",
|
||||||
"leptos_router",
|
"leptos_router",
|
||||||
"macros",
|
"macros",
|
||||||
|
"regex",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_dynamo",
|
"serde_dynamo",
|
||||||
"tokio",
|
"tokio",
|
||||||
|
|||||||
@@ -23,6 +23,7 @@ uuid = { version = "1.17.0", features = ["serde", "v4", "js"] }
|
|||||||
leptos_oidc = "0.9.0"
|
leptos_oidc = "0.9.0"
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
chrono = "0.4.42"
|
chrono = "0.4.42"
|
||||||
|
regex = "1.11.2"
|
||||||
|
|
||||||
macros = { path = "./macros" }
|
macros = { path = "./macros" }
|
||||||
traits = { path = "./traits" }
|
traits = { path = "./traits" }
|
||||||
|
|||||||
@@ -1,5 +1,5 @@
|
|||||||
use crate::common::ValueType;
|
use crate::common::ValueType;
|
||||||
use crate::components::{CheckboxList, OutlinedTextField, RadioList, Row, Select};
|
use crate::components::{CheckboxList, OutlinedTextField, RadioList, Row, Select, TextFieldType};
|
||||||
use leptos::either::EitherOf5;
|
use leptos::either::EitherOf5;
|
||||||
use leptos::prelude::RwSignal;
|
use leptos::prelude::RwSignal;
|
||||||
use leptos::{IntoView, view};
|
use leptos::{IntoView, view};
|
||||||
@@ -49,7 +49,7 @@ impl InputType {
|
|||||||
placeholder = placeholder.clone()
|
placeholder = placeholder.clone()
|
||||||
data_member = member.clone()
|
data_member = member.clone()
|
||||||
data_map = data_map
|
data_map = data_map
|
||||||
input_type = "number"
|
input_type = TextFieldType::Number
|
||||||
/>
|
/>
|
||||||
}),
|
}),
|
||||||
InputType::Checkbox(member, label, options) =>
|
InputType::Checkbox(member, label, options) =>
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ use leptos::prelude::*;
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ActionButton(
|
pub fn ActionButton(
|
||||||
#[prop(default = String::from("button"))] button_type: String,
|
#[prop(default = String::from("button"), into)] button_type: String,
|
||||||
#[prop(optional, into)] disabled: Signal<bool>,
|
#[prop(optional, into)] disabled: Signal<bool>,
|
||||||
children: Children,
|
children: Children,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
|||||||
@@ -11,8 +11,9 @@ mod row;
|
|||||||
mod select;
|
mod select;
|
||||||
mod text_field;
|
mod text_field;
|
||||||
mod utils;
|
mod utils;
|
||||||
|
mod validated_form;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
action_button::*, banner::*, checkbox::*, chips::*, dashboard_button::*, loading::*,
|
action_button::*, banner::*, checkbox::*, chips::*, dashboard_button::*, loading::*,
|
||||||
multi_entry::*, panel::*, radio::*, row::*, select::*, text_field::*,
|
multi_entry::*, panel::*, radio::*, row::*, select::*, text_field::*, validated_form::*
|
||||||
};
|
};
|
||||||
+119
-51
@@ -1,69 +1,130 @@
|
|||||||
use crate::common::ValueType;
|
use crate::common::ValueType;
|
||||||
use leptos::leptos_dom::logging::console_log;
|
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use leptos::html::Input;
|
||||||
|
use leptos::logging::log;
|
||||||
|
use crate::components::InputState;
|
||||||
|
use crate::components::validated_form::{FormValidationRegistry, ValidationState};
|
||||||
|
|
||||||
|
/// Determines the available types of text fields.
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub enum TextFieldType {
|
||||||
|
/// A text field that accepts any string.
|
||||||
|
#[default]
|
||||||
|
Text,
|
||||||
|
/// A text field that accepts only numbers.
|
||||||
|
Number,
|
||||||
|
/// A text field that accepts only Region 15 email addresses.
|
||||||
|
/// TODO Accept more than just Region 15 addresses.
|
||||||
|
Email
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Entry point for validating a text field.
|
||||||
|
fn validate(required: bool, value: &str, input_type: &TextFieldType) -> ValidationState {
|
||||||
|
log!("Input information: required: {}, value: \"{}\", input_type: {:?}", required, value, input_type);
|
||||||
|
if required && value.is_empty() {
|
||||||
|
ValidationState::Invalid("This field is required.".to_string())
|
||||||
|
} else {
|
||||||
|
match input_type {
|
||||||
|
TextFieldType::Email => validate_email(value),
|
||||||
|
TextFieldType::Number => validate_number(value),
|
||||||
|
TextFieldType::Text => ValidationState::Valid
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates an email address.
|
||||||
|
fn validate_email(input: &str) -> ValidationState {
|
||||||
|
// TODO Accept more than just Region 15 addresses.
|
||||||
|
log!("Validating email: {}", input);
|
||||||
|
let pattern = regex::Regex::new(r"^[a-zA-Z0-9._%+-]+@region15\.org$").expect("Invalid regex pattern");
|
||||||
|
|
||||||
|
if pattern.is_match(input) {
|
||||||
|
ValidationState::Valid
|
||||||
|
} else {
|
||||||
|
ValidationState::Invalid("Please provide a valid Region 15 email address.".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Validates a number.
|
||||||
|
fn validate_number(input: &str) -> ValidationState {
|
||||||
|
log!("Validating number: {}", input);
|
||||||
|
if input.parse::<i32>().is_ok() {
|
||||||
|
ValidationState::Valid
|
||||||
|
} else {
|
||||||
|
ValidationState::Invalid("Value is not a valid number.".to_string())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn OutlinedTextField(
|
pub fn OutlinedTextField(
|
||||||
#[prop(optional, into)] placeholder: String,
|
#[prop(optional, into)] placeholder: String,
|
||||||
// #[prop()] value: Subfield<TStore, TInner, ValueType>,
|
|
||||||
#[prop(into)] data_member: String, // should this be an RwSignal??
|
#[prop(into)] data_member: String, // should this be an RwSignal??
|
||||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||||
#[prop(optional)] value: RwSignal<ValueType>,
|
#[prop(optional)] input_type: TextFieldType,
|
||||||
#[prop(default = "text".to_owned(), into)] input_type: String,
|
|
||||||
#[prop(into, optional)] disabled: Signal<bool>,
|
#[prop(into, optional)] disabled: Signal<bool>,
|
||||||
#[prop(optional)] error: RwSignal<bool>,
|
|
||||||
#[prop(optional, into)] name: String,
|
#[prop(optional, into)] name: String,
|
||||||
#[prop(optional, into)] label: String,
|
#[prop(optional, into)] label: String,
|
||||||
#[prop(optional, into)] error_text: RwSignal<String>,
|
#[prop(optional, into)] required: Signal<bool>
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
|
let valid = RwSignal::new(true);
|
||||||
|
let input_ref = NodeRef::<Input>::new();
|
||||||
|
|
||||||
|
// Register this input's validation signal.
|
||||||
|
let validator_context = use_context::<FormValidationRegistry>()
|
||||||
|
.expect("FormValidSignal was not found");
|
||||||
|
|
||||||
|
let raw_value = RwSignal::new(data_map.get_untracked()
|
||||||
|
.get(&data_member)
|
||||||
|
.unwrap_or(&ValueType::String(None))
|
||||||
|
.to_string());
|
||||||
|
|
||||||
|
let dirty = RwSignal::new(false);
|
||||||
|
let error = RwSignal::new(validate(required.get_untracked(), &raw_value.get_untracked(), &input_type));
|
||||||
|
let show_errors = Signal::derive(move || {
|
||||||
|
dirty.get() && matches!(error.get(), ValidationState::Invalid(_))
|
||||||
|
});
|
||||||
|
|
||||||
|
let validator = RwSignal::new(InputState::new(data_member.clone(), error.clone(), dirty.clone()));
|
||||||
|
|
||||||
|
validator_context.validators.update(|list| list.push(validator));
|
||||||
|
|
||||||
|
on_cleanup(move || {
|
||||||
|
validator_context.validators.update(|list| {
|
||||||
|
list.retain(|v| *v.get_untracked().input_name != *validator.get_untracked().input_name)
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let str_type = match input_type {
|
||||||
|
TextFieldType::Number => "number",
|
||||||
|
_ => "text" // This is fine even for emails, as we're doing our own validation.
|
||||||
|
};
|
||||||
|
|
||||||
|
let on_blur = move |_| {
|
||||||
|
// Check validity of input's current value. The value will be updated by on_input,
|
||||||
|
// and it should be a raw input (not parsed into a ValueType).
|
||||||
|
dirty.set(true);
|
||||||
|
};
|
||||||
|
|
||||||
// This function parses the input into the correct type. It only accepts numbers or strings as
|
// This function parses the input into the correct type. It only accepts numbers or strings as
|
||||||
// valid types, and parses them accordingly.
|
// valid types, and parses them accordingly.
|
||||||
let on_input = {
|
let on_input = {
|
||||||
let input_type = input_type.clone();
|
|
||||||
let data_member = data_member.clone();
|
let data_member = data_member.clone();
|
||||||
move |e| {
|
move |e| {
|
||||||
let to_parse = event_target_value(&e);
|
let to_parse = event_target_value(&e);
|
||||||
match input_type.as_str() {
|
raw_value.set(to_parse.clone());
|
||||||
"text" => {
|
error.set(validate(required.get(), &to_parse, &input_type));
|
||||||
// String parse function. It will always parse, but make sure the error
|
let into_map = match input_type {
|
||||||
// state is set properly anyway.
|
TextFieldType::Number => ValueType::Number(Some(to_parse)),
|
||||||
error.set(false);
|
_ => ValueType::String(Some(to_parse))
|
||||||
value.set(ValueType::String(Some(to_parse.clone())));
|
|
||||||
data_map.update(|map| {
|
|
||||||
map.insert(data_member.clone(), ValueType::String(Some(to_parse)));
|
|
||||||
});
|
|
||||||
}
|
|
||||||
"number" => {
|
|
||||||
// Define number parse function.
|
|
||||||
// If the number parses, Some(num), else None, but we want to keep the value that's
|
|
||||||
// in the text field at this point, just set the error.
|
|
||||||
console_log(format!("Parsing {to_parse} to a number").as_str());
|
|
||||||
match to_parse
|
|
||||||
.parse::<i32>()
|
|
||||||
.map(|n| ValueType::Number(Some(n.to_string())))
|
|
||||||
{
|
|
||||||
Ok(value_type) => {
|
|
||||||
error.set(false);
|
|
||||||
value.set(value_type.clone());
|
|
||||||
data_map.update(|map| {
|
|
||||||
map.insert(data_member.clone(), value_type);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
Err(_) => {
|
|
||||||
error.set(true);
|
|
||||||
error_text.set("Value is not a valid number.".to_owned());
|
|
||||||
}
|
|
||||||
};
|
};
|
||||||
}
|
|
||||||
_ => {
|
// Edit the data map. Note that this should be uncoupled in the future - we don't want
|
||||||
// Create some custom function that reports failure.
|
// to update the map inside the component definition, but inside the page/form that
|
||||||
let msg = format!("Input {input_type:?} is not a valid type.");
|
// created this input.
|
||||||
console_log(msg.as_str());
|
data_map.update(|map| {
|
||||||
error.set(true);
|
map.insert(data_member.clone(), into_map);
|
||||||
error_text.set(msg);
|
});
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -72,11 +133,12 @@ pub fn OutlinedTextField(
|
|||||||
<label class="flex flex-col flex-1">
|
<label class="flex flex-col flex-1">
|
||||||
<span class="block ml-1.5 mb-0 font-bold">{label}</span>
|
<span class="block ml-1.5 mb-0 font-bold">{label}</span>
|
||||||
<input
|
<input
|
||||||
|
node_ref=input_ref
|
||||||
class="border-2 m-1.5 p-1.5 mt-0 rounded-md bg-transparent relative flex-1
|
class="border-2 m-1.5 p-1.5 mt-0 rounded-md bg-transparent relative flex-1
|
||||||
transition-all duration-150
|
transition-all duration-150
|
||||||
border-red-700 bg-transparent
|
border-red-700 bg-transparent
|
||||||
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
|
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
|
||||||
r#type={input_type}
|
r#type={str_type}
|
||||||
disabled={disabled}
|
disabled={disabled}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
prop:name=name
|
prop:name=name
|
||||||
@@ -86,12 +148,18 @@ pub fn OutlinedTextField(
|
|||||||
.unwrap_or(&ValueType::String(None)) // ValueType
|
.unwrap_or(&ValueType::String(None)) // ValueType
|
||||||
.to_string()
|
.to_string()
|
||||||
on:input=on_input
|
on:input=on_input
|
||||||
|
on:blur=on_blur
|
||||||
/>
|
/>
|
||||||
<div
|
<Show when=move || show_errors.get()>
|
||||||
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">
|
||||||
class=(["hidden"], move || !error.get())>
|
{move || {
|
||||||
{error_text}
|
match error.get() {
|
||||||
|
ValidationState::Invalid(msg) => msg,
|
||||||
|
_ => "There is no error - should not see this message.".to_string()
|
||||||
|
}
|
||||||
|
}}
|
||||||
</div>
|
</div>
|
||||||
|
</Show>
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,147 @@
|
|||||||
|
use leptos::logging::log;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use leptos::web_sys::SubmitEvent;
|
||||||
|
use crate::components::ActionButton;
|
||||||
|
|
||||||
|
/// Indicates the current state of an input.
|
||||||
|
#[derive(Default, Debug, Clone)]
|
||||||
|
pub enum ValidationState {
|
||||||
|
/// A valid input.
|
||||||
|
#[default]
|
||||||
|
Valid,
|
||||||
|
/// An invalid input with an error message.
|
||||||
|
Invalid(String)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A struct that contains all information required for a form to make real-time validation
|
||||||
|
/// decisions, including display of error messages.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct InputState {
|
||||||
|
/// The name of the input. This is used to identify the input in the form.
|
||||||
|
pub input_name: String,
|
||||||
|
/// Indicates the current state of the input.
|
||||||
|
error: Signal<ValidationState>,
|
||||||
|
/// Indicates whether the form has requested the error state. This means that the input
|
||||||
|
/// should show their error appearances immediately.
|
||||||
|
form_requested_validation: RwSignal<bool>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputState {
|
||||||
|
/// Creates a new [`InputState`] with the given error and form requested validation signals.
|
||||||
|
pub fn new(
|
||||||
|
name: impl Into<String>,
|
||||||
|
error: impl Into<Signal<ValidationState>>,
|
||||||
|
form_requested_errors: RwSignal<bool>
|
||||||
|
) -> Self {
|
||||||
|
Self {
|
||||||
|
input_name: name.into(),
|
||||||
|
error: error.into(),
|
||||||
|
form_requested_validation: form_requested_errors
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn error(&self) -> Signal<ValidationState> {
|
||||||
|
self.error.clone()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// A list of [`InputState`]s.
|
||||||
|
pub type ValidatorList = Vec<RwSignal<InputState>>;
|
||||||
|
|
||||||
|
/// A struct that keeps track of all components that must be validated.
|
||||||
|
/// All of our custom-made components must be placed within an area that contains context with
|
||||||
|
/// this struct.
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct FormValidationRegistry {
|
||||||
|
pub validators: RwSignal<ValidatorList>
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Validated Form Component
|
||||||
|
///
|
||||||
|
/// A simple HTML `<form>` wrapper that executes validation logic using a [`FormValidationRegistry`].
|
||||||
|
/// It will ensure that any invalid inputs are caught before submission. All input components that
|
||||||
|
/// are used within this form should call `use_context::<FormValidationRegistry>()` to register
|
||||||
|
/// themselves with the form.
|
||||||
|
///
|
||||||
|
/// Example usage in the view macro:
|
||||||
|
/// ```
|
||||||
|
/// view! {
|
||||||
|
/// <ValidatedForm on_submit=Callback::new(move || {/* something here */})>
|
||||||
|
/// {/* As many input components as you want */}
|
||||||
|
/// </ValidatedForm>
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// Example input component implementation:
|
||||||
|
/// ```
|
||||||
|
/// fn SomeComponent() -> impl IntoView {
|
||||||
|
/// // Get FormValidationContext
|
||||||
|
/// let context = use_context::<FormValidationRegistry>()
|
||||||
|
/// .expect("Couldn't find FormValidationRegsitry context");
|
||||||
|
///
|
||||||
|
/// // Register with the form
|
||||||
|
/// let error = RwSignal::new(ValidationState::Valid);
|
||||||
|
/// let form_requested_errors = RwSignal::new(false);
|
||||||
|
/// let state = InputState::new(
|
||||||
|
/// "the_input_name",
|
||||||
|
/// error.clone(),
|
||||||
|
/// form_requested_errors.clone()
|
||||||
|
/// );
|
||||||
|
/// context.validators.update(|list| list.push(RwSignal::new(state)));
|
||||||
|
///
|
||||||
|
/// /* Other input view logic */
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
#[component]
|
||||||
|
pub fn ValidatedForm(
|
||||||
|
children: Children,
|
||||||
|
/// A callback that runs when the form successfully validates.
|
||||||
|
#[prop(into)] on_submit: Callback<()>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
let validators = RwSignal::new(vec![]);
|
||||||
|
|
||||||
|
// Provide context to all children nested within this component. They will be able to access
|
||||||
|
// this no matter how nested they are.
|
||||||
|
provide_context(FormValidationRegistry {validators});
|
||||||
|
|
||||||
|
let can_submit = Memo::new(move |_| {
|
||||||
|
log!("Checking {} inputs...", validators.get().len());
|
||||||
|
validators
|
||||||
|
.with(|list| {
|
||||||
|
list.iter()
|
||||||
|
// TODO all() short circuits on the first false
|
||||||
|
.all(|v| {
|
||||||
|
// Not sure how this will work reactively.
|
||||||
|
let result = matches!(v.get().error().get(), ValidationState::Valid);
|
||||||
|
v.update(|validator| validator.form_requested_validation.set(true));
|
||||||
|
result
|
||||||
|
})
|
||||||
|
})
|
||||||
|
});
|
||||||
|
|
||||||
|
let submit_success_event = move |e: SubmitEvent| {
|
||||||
|
e.prevent_default();
|
||||||
|
|
||||||
|
if !can_submit.get() {
|
||||||
|
log!("Form failed to submit, there are errors in form inputs.");
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log!("Form validation successful.");
|
||||||
|
on_submit.run(());
|
||||||
|
};
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<form novalidate on:submit=submit_success_event>
|
||||||
|
{children()}
|
||||||
|
<ActionButton
|
||||||
|
on:click=move |_| {
|
||||||
|
log!("{:?}", can_submit.get());
|
||||||
|
}
|
||||||
|
button_type="submit"
|
||||||
|
>
|
||||||
|
"Submit form (but actually)"
|
||||||
|
</ActionButton>
|
||||||
|
</form>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -9,12 +9,9 @@ use std::process::{Command, Stdio};
|
|||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
|
|
||||||
use crate::common::{ExpandableInfo, UserClaims};
|
use crate::common::ExpandableInfo;
|
||||||
use crate::pages::UnauthenticatedPage;
|
use crate::pages::UnauthenticatedPage;
|
||||||
use crate::components::{
|
use crate::components::{ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row, Select, TextFieldType, ValidatedForm};
|
||||||
ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row,
|
|
||||||
Select,
|
|
||||||
};
|
|
||||||
use crate::input;
|
use crate::input;
|
||||||
use leptos::leptos_dom::logging::console_log;
|
use leptos::leptos_dom::logging::console_log;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
@@ -167,6 +164,7 @@ pub fn HomePage() -> impl IntoView {
|
|||||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||||
// Replace this fallback with a real loading screen.
|
// Replace this fallback with a real loading screen.
|
||||||
<Suspense fallback=Loading>
|
<Suspense fallback=Loading>
|
||||||
|
<ValidatedForm on_submit=Callback::new(move |_| {})>
|
||||||
{move || {
|
{move || {
|
||||||
server_resource
|
server_resource
|
||||||
.get()
|
.get()
|
||||||
@@ -227,6 +225,7 @@ pub fn HomePage() -> impl IntoView {
|
|||||||
disabled=elements_disabled
|
disabled=elements_disabled
|
||||||
data_member="contact_email"
|
data_member="contact_email"
|
||||||
data_map=expandable_react.data
|
data_map=expandable_react.data
|
||||||
|
input_type=TextFieldType::Email
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
@@ -254,7 +253,7 @@ pub fn HomePage() -> impl IntoView {
|
|||||||
disabled=elements_disabled
|
disabled=elements_disabled
|
||||||
data_member="math_sat"
|
data_member="math_sat"
|
||||||
data_map=expandable_react.data
|
data_map=expandable_react.data
|
||||||
input_type="number"
|
input_type=TextFieldType::Number
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
@@ -357,6 +356,7 @@ pub fn HomePage() -> impl IntoView {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
</ValidatedForm>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
</Authenticated>
|
</Authenticated>
|
||||||
</AuthLoaded>
|
</AuthLoaded>
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ use leptos::prelude::*;
|
|||||||
use leptos_oidc::{AuthLoaded, Authenticated};
|
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||||
use leptos_router::hooks::{use_navigate, use_params};
|
use leptos_router::hooks::{use_navigate, use_params};
|
||||||
use crate::common::{ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType};
|
use crate::common::{ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType};
|
||||||
use crate::components::{ActionButton, Banner, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row};
|
use crate::components::{ActionButton, Banner, ChipsList, Loading, OutlinedTextField, Panel, RadioList, Row, TextFieldType};
|
||||||
use super::UnauthenticatedPage;
|
use super::UnauthenticatedPage;
|
||||||
use traits::{AsReactive, ReactiveCapture};
|
use traits::{AsReactive, ReactiveCapture};
|
||||||
use crate::common::ComparisonData;
|
use crate::common::ComparisonData;
|
||||||
@@ -461,7 +461,7 @@ fn ScholarshipForm(
|
|||||||
disabled=elements_disabled
|
disabled=elements_disabled
|
||||||
data_member="amount_per_award"
|
data_member="amount_per_award"
|
||||||
data_map=reactive_info.data
|
data_map=reactive_info.data
|
||||||
input_type="number"
|
input_type=TextFieldType::Number
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
@@ -471,7 +471,7 @@ fn ScholarshipForm(
|
|||||||
disabled=elements_disabled
|
disabled=elements_disabled
|
||||||
data_member="num_awards"
|
data_member="num_awards"
|
||||||
data_map=reactive_info.data
|
data_map=reactive_info.data
|
||||||
input_type="number"
|
input_type=TextFieldType::Number
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
@@ -481,7 +481,7 @@ fn ScholarshipForm(
|
|||||||
disabled=elements_disabled
|
disabled=elements_disabled
|
||||||
data_member="total_awards"
|
data_member="total_awards"
|
||||||
data_map=reactive_info.data
|
data_map=reactive_info.data
|
||||||
input_type="number"
|
input_type=TextFieldType::Number
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
|
|||||||
Reference in new issue
Block a user