From 0d4bbc9e70b1b900e904c46bdb03758e33e10ea8 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Tue, 16 Sep 2025 19:08:13 -0400 Subject: [PATCH 01/11] chore: implement basic logic to show a text field in the MultiEntry component This logic does not support any info types other than a String, represented by an OutlinedTextField. To support more, this will likely need to completely overhaul every input component so that they accept the ValueType as a primary argument. --- src/common/form_schema.rs | 18 +++ src/common/mod.rs | 3 +- src/components/multi_entry/mod.rs | 11 ++ .../{ => multi_entry}/multi_entry.rs | 126 ++++++++---------- .../multi_entry/multi_entry_data.rs | 30 +++++ .../multi_entry/multi_entry_member.rs | 45 +++++++ src/components/multi_entry/value_type.rs | 71 ++++++++++ src/pages/home_page.rs | 11 +- src/pages/provider_entry.rs | 1 + 9 files changed, 241 insertions(+), 75 deletions(-) create mode 100644 src/common/form_schema.rs create mode 100644 src/components/multi_entry/mod.rs rename src/components/{ => multi_entry}/multi_entry.rs (57%) create mode 100644 src/components/multi_entry/multi_entry_data.rs create mode 100644 src/components/multi_entry/multi_entry_member.rs create mode 100644 src/components/multi_entry/value_type.rs diff --git a/src/common/form_schema.rs b/src/common/form_schema.rs new file mode 100644 index 0000000..5ebd963 --- /dev/null +++ b/src/common/form_schema.rs @@ -0,0 +1,18 @@ +use serde::{Serialize, Deserialize}; + +/// Defines a series of options that allow a user to define a form. +#[derive(Serialize, Deserialize, Clone, Debug)] +pub enum InputType{ + /// Represents a text input. Requires a `String` key. The entered value will be returned as a + /// single `String` value. + Text, + /// Represents a number input. Requires a `String` key. The entered value will be returned as a + /// single `i32` value. + Number, + /// Represents a radio input. Requires a `String` key and a series of options. The selected + /// value will be returned as a single `String` value. + Radio(Vec), + /// Represents a checkbox input. Requires a `String` key and a series of options. The selected + /// value will be returned as a `Vec`. + Checkbox(Vec) +} diff --git a/src/common/mod.rs b/src/common/mod.rs index 33ed212..8ca998f 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,4 +1,5 @@ mod student_info; mod user; +mod form_schema; -pub use self::{student_info::*, user::*}; +pub use self::{student_info::*, user::*, form_schema::*}; diff --git a/src/components/multi_entry/mod.rs b/src/components/multi_entry/mod.rs new file mode 100644 index 0000000..7123667 --- /dev/null +++ b/src/components/multi_entry/mod.rs @@ -0,0 +1,11 @@ +pub mod multi_entry_data; +pub mod value_type; +pub mod multi_entry; +mod multi_entry_member; + +pub use self::{ + value_type::*, + multi_entry_data::*, + multi_entry::*, + multi_entry_member::*, +}; diff --git a/src/components/multi_entry.rs b/src/components/multi_entry/multi_entry.rs similarity index 57% rename from src/components/multi_entry.rs rename to src/components/multi_entry/multi_entry.rs index ec8476a..536f3c9 100644 --- a/src/components/multi_entry.rs +++ b/src/components/multi_entry/multi_entry.rs @@ -1,75 +1,27 @@ -use std::collections::HashMap; - -use crate::components::ActionButton; +use std::sync::Arc; +use leptos::either::either; +use crate::components::{ActionButton, OutlinedTextField}; use leptos::prelude::*; -use macros::Reactive; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; +use crate::common::InputType; +use crate::components::ValueType; +use super::{multi_entry_data::*, multi_entry_member::*}; -#[derive(Clone, Serialize, Deserialize, Debug, Reactive, Default)] -pub struct MultiEntryData { - pub id: Uuid, - pub data: HashMap, -} - -impl MultiEntryData { - /// Creates a new MultiEntryData struct. - pub fn new() -> Self { - Self { - id: Uuid::new_v4(), - data: HashMap::new(), - } - } -} - -/// Contains all information required to display an entry in the MultiEntry component. -#[derive(Clone, Serialize, Deserialize, Debug)] -pub struct MultiEntryMember { - /// The name of the member, used for display only. - pub display_name: String, - /// The actual name of the member, used to get the data from an Entry struct. - pub member_name: String, -} - -impl MultiEntryMember { - /// Creates a new MultiEntryMember struct. - pub fn _new() -> Self { - Self { - display_name: String::new(), - member_name: String::new(), - } - } - - /// Creates a new MultiEntryMember struct with the specified information. - pub fn _new_with_data(display_name: String, member_name: String) -> Self { - Self { - display_name, - member_name, - } - } - - /// Creates a new MultiEntryMember struct with the specified information. Uses string slices. - pub fn from_str(display_name: &str, member_name: &str) -> Self { - Self { - display_name: display_name.into(), - member_name: member_name.into(), - } - } -} - -#[derive(Clone, Serialize, Deserialize, Debug)] -pub enum ValueType { - String(String), - Number(i32), -} - -impl ValueType { - pub fn as_string(&self) -> String { - match self { - ValueType::String(s) => s.clone(), - ValueType::Number(n) => n.to_string(), - } - } +fn render_entry_component(entry_data: MultiEntryData, member_info: MultiEntryMember, label: String) -> impl IntoView { + // Parse the multientry members into the correct components + let data = entry_data.data.get(&member_info.member_name); + let reactive = RwSignal::new(data.unwrap().as_string().unwrap()); + let temp = either!(member_info.input_type, + InputType::Text => view! { + + }, + _ => view! {} + ); + + temp.into_view() } /// Represents a single entry in a MultiEntry component. @@ -86,30 +38,54 @@ pub fn Entry( /// The secondary display member for this entry. #[prop()] info_member: Signal, + /// The schema for all inputs that should be used, and what `MultiEntryMember` info is associated + /// with them. + #[prop()] + schema: Arc> ) -> impl IntoView { let name_data = entry_data.data.get(&name_member.get().member_name).unwrap(); let info_data = entry_data.data.get(&info_member.get().member_name).unwrap(); + let schema = Arc::clone(&schema); + view! { // This div MUST have the same spacing CSS as the second-level header div
- {name_data.as_string()} - {info_data.as_string()} + {name_data.to_string()} + {info_data.to_string()}
+ {schema + .iter() + .map(|input_type| { + let data = entry_data.clone(); + let input = input_type.clone(); + view! { +
{render_entry_component(data, input, "test".to_owned())}
+ } + }).collect::>() + } } } +/// Allows users to enter more open-ended information, and generates a `Vec` filled +/// with the user's entered information. The schema for this data must be defined by this component +/// such that it is constructable using a series of `String` keys, where each key stores a single +/// `ValueType` enum. #[component] pub fn MultiEntry( /// The list of entries to use. #[prop()] entries: RwSignal>, - /// the main display member for each entry object. + /// The main display member for each entry object. #[prop()] name_member: Signal, /// The secondary display member for each entry object. #[prop()] info_member: Signal, + /// The schema of the components. Determines what kind of data is stored and where. + /// The `name_member` and `info_member` should be included in this list. + #[prop(optional)] + schema: Vec ) -> impl IntoView { // Creates an entry in the given entries list. let create_entry = move |_| { @@ -124,6 +100,8 @@ pub fn MultiEntry( ); entries.update(|entry_vec| entry_vec.push(new_entry)); }; + + let rc_schema = Arc::new(schema); view! {
@@ -145,11 +123,13 @@ pub fn MultiEntry( each=move || entries.get() key=|entry: &MultiEntryData| entry.id children=move |entry| { + let schema = Arc::clone(&rc_schema); view! { } } diff --git a/src/components/multi_entry/multi_entry_data.rs b/src/components/multi_entry/multi_entry_data.rs new file mode 100644 index 0000000..5bc6ede --- /dev/null +++ b/src/components/multi_entry/multi_entry_data.rs @@ -0,0 +1,30 @@ +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; +use macros::Reactive; +use crate::components::ValueType; + +/// Stores all data for the `MultiEntry` component. +/// +/// All values are stored within the `HashMap`. These entries only contain the value and +/// the type of the value itself. Each piece of data also has a unique ID, which allows for simpler +/// dynamic rendering.`data` +#[derive(Clone, Serialize, Deserialize, Debug, Reactive, Default)] +pub struct MultiEntryData { + /// The data's unique ID. + pub id: Uuid, + /// The pertinent data for this struct. This may be variable - each `MultiEntry` component + /// may specify its own schema and way of storing this data. The only limitation will be the + /// types of data that can be stored - they must be easily parsable by a standard HTML input. + pub data: HashMap, +} + +impl MultiEntryData { + /// Creates a new MultiEntryData struct. + pub fn new() -> Self { + Self { + id: Uuid::new_v4(), + data: HashMap::new(), + } + } +} diff --git a/src/components/multi_entry/multi_entry_member.rs b/src/components/multi_entry/multi_entry_member.rs new file mode 100644 index 0000000..efc658e --- /dev/null +++ b/src/components/multi_entry/multi_entry_member.rs @@ -0,0 +1,45 @@ +use serde::{Deserialize, Serialize}; +use crate::common::InputType; + +/// Contains all information required to display an entry in the MultiEntry component. +/// +/// NOTE: This does not define a schema for the data stored by the component, only the data that the +/// component displays as "identifying information." +#[derive(Clone, Serialize, Deserialize, Debug)] +pub struct MultiEntryMember { + /// The name of the member, used for display only. + pub display_name: String, + /// The actual name of the member, used to get the data from an Entry struct. + pub member_name: String, + /// The type of input that should be used to display this member. + pub input_type: InputType +} + +impl MultiEntryMember { + /// Creates a new MultiEntryMember struct. + pub fn _new() -> Self { + Self { + display_name: String::new(), + member_name: String::new(), + input_type: InputType::Text, + } + } + + /// Creates a new MultiEntryMember struct with the specified information. + pub fn _new_with_data(display_name: String, member_name: String, input_type: InputType) -> Self { + Self { + display_name, + member_name, + input_type + } + } + + /// Creates a new MultiEntryMember struct with the specified information. Uses string slices. + pub fn from_str(display_name: &str, member_name: &str) -> Self { + Self { + display_name: display_name.into(), + member_name: member_name.into(), + input_type: InputType::Text + } + } +} diff --git a/src/components/multi_entry/value_type.rs b/src/components/multi_entry/value_type.rs new file mode 100644 index 0000000..fbe78d2 --- /dev/null +++ b/src/components/multi_entry/value_type.rs @@ -0,0 +1,71 @@ +use serde::{Serialize, Deserialize}; +use std::fmt::Display; + +/// Defines a set of possible input types. They allow for the type and value of every data member +/// to be stored within a single `HashMap` or `Vec`. This means that even though all the data is +/// created as a single type, the values are still able to be type safe. +#[derive(Clone, Serialize, Deserialize, Debug)] +pub enum ValueType { + /// Indicates a `String` value. + String(String), + /// Indicates an `i32` value. + Number(i32), + /// Indicates a `Vec`, where `T` can be any other `ValueType`. + List(Vec), +} + +impl ValueType { + /// Returns true if this is a `String`. + pub fn is_string(&self) -> bool { + if let ValueType::String(_) = self { true } else { false } + } + + /// Attempts to get this value as a `String` type. This is different from the `to_string` method. + pub fn as_string(&self) -> Option { + if let ValueType::String(v) = self { + Some(v.clone()) + } else { + None + } + } + + /// Returns true if this is a `Number`. + pub fn is_number(&self) -> bool { + if let ValueType::Number(_) = self { true } else { false } + } + + /// Attempts to get this value as an `i32`. + pub fn as_number(&self) -> Option<&i32> { + if let ValueType::Number(v) = self { + Some(v) + } else { + None + } + } + + /// Returns true if this is a `List`. + pub fn is_list(&self) -> bool { + if let ValueType::List(_) = self { true } else { false } + } + + /// Attempts to get this value as a `Vec`. Each of the values inside the list must + /// also be converted later, as these values are any of the possible types in the `ValueType`. + pub fn as_list(&self) -> Option<&Vec> { + if let ValueType::List(v) = self { + Some(v) + } else { + None + } + } +} + +impl Display for ValueType { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + let str = match self { + ValueType::String(s) => s.clone(), + ValueType::Number(n) => n.to_string(), + ValueType::List(l) => format!("{l:?}"), + }; + write!(f, "{}", str) + } +} diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index ef4ffeb..5e36a68 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -7,12 +7,16 @@ use serde_dynamo::{from_item, to_item}; use crate::app::Unauthenticated; use crate::common::{StudentInfo, UserClaims}; -use crate::components::{ActionButton, CheckboxList, Loading, MultiEntry, MultiEntryMember, OutlinedTextField, OutlinedTextFieldPropsBuilder_Error_Repeated_field_disabled, Panel, RadioList, Row, Select}; +use crate::components::{ActionButton, Loading, OutlinedTextField, Panel, RadioList, Row, Select}; use leptos::leptos_dom::logging::console_log; use leptos::prelude::*; use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated}; use traits::{AsReactive, ReactiveCapture}; +/// # Get Student Info +/// Gets a student's information given their `subject`. +/// +/// All information is found by using a `GetItemCommand` in the student application DynamoDB table. #[server(GetSubmission, endpoint = "/get-submission")] pub async fn get_submission(id: String) -> Result { let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; @@ -47,6 +51,10 @@ pub async fn get_submission(id: String) -> Result { } } +/// # Create Sample Submission +/// Creates a submission given a full `StudentInfo` struct. +/// +/// All information is stored using a `PutItemCommand` in the student application DynamoDB table. #[server(CreateSampleSubmission, endpoint = "/create-sample-submission")] pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), ServerFnError> { let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; @@ -78,6 +86,7 @@ pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), S } } +/// The main home page component. Contains a simple contact form. #[component] pub fn HomePage() -> impl IntoView { // Creates a reactive value to update the button diff --git a/src/pages/provider_entry.rs b/src/pages/provider_entry.rs index 7bbdf16..a9cf089 100644 --- a/src/pages/provider_entry.rs +++ b/src/pages/provider_entry.rs @@ -37,6 +37,7 @@ pub fn ProviderEntry() -> impl IntoView { entries=multi_entries name_member=Signal::from(MultiEntryMember::from_str("Entry Name", "name")) info_member=Signal::from(MultiEntryMember::from_str("Entry Info", "info")) + schema=vec![MultiEntryMember::from_str("Entry Name", "name"), MultiEntryMember::from_str("Entry Info", "info")] />
From 39996319ba38b447fe85d25ca3e81d0b46276b73 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Fri, 19 Sep 2025 12:30:27 -0400 Subject: [PATCH 02/11] feat: alter OutlinedTextField to use ValueType enum Also implemented extra error checking when the values are passed as the incorrect type. For example, the only accepted types are strings and numbers, so that will be checked and handled accordingly. --- src/common/student_info.rs | 27 ++++++----- src/components/multi_entry/multi_entry.rs | 4 +- src/components/multi_entry/value_type.rs | 6 +++ src/components/text_field.rs | 55 ++++++++++++++--------- src/pages/home_page.rs | 52 +++++++++++---------- src/pages/provider_entry.rs | 4 +- 6 files changed, 89 insertions(+), 59 deletions(-) diff --git a/src/common/student_info.rs b/src/common/student_info.rs index 1533782..ca617a4 100644 --- a/src/common/student_info.rs +++ b/src/common/student_info.rs @@ -1,11 +1,12 @@ -use crate::components::MultiEntryData; +use crate::components::{MultiEntryData, ValueType}; use macros::Reactive; use serde::{Deserialize, Serialize}; use std::default::Default; use chrono::{DateTime, Utc}; +use reactive_stores::Store; /// Represents information about a student. -#[derive(Serialize, Deserialize, Debug, Clone, Reactive, Default)] +#[derive(Serialize, Deserialize, Debug, Clone, Default, Store, Reactive)] pub struct StudentInfo { /// The email address of the student. This was used in the previous version of the database, /// and should now be deprecated. @@ -14,32 +15,36 @@ pub struct StudentInfo { pub Email: String, /// The student's first name. #[serde(default)] - pub first_name: String, + pub first_name: ValueType, // String /// The student's last name. #[serde(default)] - pub last_name: String, + pub last_name: ValueType, // String /// The student's highest math score on the SAT. - #[serde(default)] - pub math_sat: i32, + #[serde(default = "default_number")] + pub math_sat: ValueType, // Number /// The student's desired contact email. #[serde(default)] - pub contact_email: String, + pub contact_email: ValueType, // String /// The student's address. #[serde(default)] - pub address: String, + pub address: ValueType, // String /// The student's town of residence. #[serde(default)] - pub town: String, + pub town: ValueType, // String /// The student's entered phone number. #[serde(default)] - pub phone_number: String, // May be stored as a straight number in the future. + pub phone_number: ValueType, // May be stored as a straight number in the future, but String /// The student's birthdate. #[serde(default)] pub birth_date: DateTime, /// The student's gender. #[serde(default)] - pub gender: String, + pub gender: ValueType, // String /// The student's athletic information. #[serde(default)] pub athletic_info: Vec, } + +fn default_number() -> ValueType { + ValueType::Number(0) +} \ No newline at end of file diff --git a/src/components/multi_entry/multi_entry.rs b/src/components/multi_entry/multi_entry.rs index 536f3c9..1e85385 100644 --- a/src/components/multi_entry/multi_entry.rs +++ b/src/components/multi_entry/multi_entry.rs @@ -9,7 +9,7 @@ use super::{multi_entry_data::*, multi_entry_member::*}; fn render_entry_component(entry_data: MultiEntryData, member_info: MultiEntryMember, label: String) -> impl IntoView { // Parse the multientry members into the correct components let data = entry_data.data.get(&member_info.member_name); - let reactive = RwSignal::new(data.unwrap().as_string().unwrap()); + let reactive = RwSignal::new(data.unwrap().clone()); let temp = either!(member_info.input_type, InputType::Text => view! { {render_entry_component(data, input, "test".to_owned())} + //
{render_entry_component(data, input, "test".to_owned())}
} }).collect::>() } diff --git a/src/components/multi_entry/value_type.rs b/src/components/multi_entry/value_type.rs index fbe78d2..8b8d7fb 100644 --- a/src/components/multi_entry/value_type.rs +++ b/src/components/multi_entry/value_type.rs @@ -59,6 +59,12 @@ impl ValueType { } } +impl Default for ValueType { + fn default() -> Self { + ValueType::String(String::new()) + } +} + impl Display for ValueType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let str = match self { diff --git a/src/components/text_field.rs b/src/components/text_field.rs index 46f8c4e..51d2dba 100644 --- a/src/components/text_field.rs +++ b/src/components/text_field.rs @@ -1,32 +1,42 @@ use leptos::prelude::*; -use std::str::FromStr; +use crate::components::ValueType; #[component] -pub fn OutlinedTextField( - #[prop(optional, into)] placeholder: T, - #[prop(optional)] value: RwSignal, +pub fn OutlinedTextField( + #[prop(optional, into)] placeholder: String, + // #[prop()] value: Subfield, + #[prop()] value: RwSignal, #[prop(optional)] disabled: RwSignal, #[prop(optional)] error: RwSignal, #[prop(optional, into)] name: String, #[prop(optional, into)] label: String, #[prop(optional, into)] input_type: String, -) -> impl IntoView -where - T: Default + 'static + FromStr + ToString + PartialEq + Send + Sync + Clone, -{ - // Parsing logic for non-string types which happens on each input. + #[prop(optional, into)] error_text: RwSignal +) -> impl IntoView { + // Check if the type is one of the supported formats. This input will only accept a string + // or a number value. let on_input = move |e| { - let input_value = event_target_value(&e); - let can_parse = T::from_str(&input_value); - if let Ok(parsed_value) = can_parse { - value.set(parsed_value); - } else { - error.set(true); - value.set(T::default()); + match value.get() { + ValueType::String(_) => { + // Define string parse function. + value.set(ValueType::String(event_target_value(&e))); + }, + ValueType::Number(_) => { + // Define number parse function. + let temp = event_target_value(&e); + value.set(ValueType::Number(temp.parse::().unwrap_or(0))); + }, + _ => { + // Create some custom function that reports failure. + let value_type = value.get(); + println!("Input {value_type:?} is not a valid type."); + error.set(true); + error_text.set(format!("InputType {value_type:?} is not a valid type.")); + } } }; - - view! ( + + view! {
- ) + } } diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index 5e36a68..4b08df1 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -7,10 +7,11 @@ use serde_dynamo::{from_item, to_item}; use crate::app::Unauthenticated; use crate::common::{StudentInfo, UserClaims}; -use crate::components::{ActionButton, Loading, OutlinedTextField, Panel, RadioList, Row, Select}; +use crate::components::{ActionButton, Loading, OutlinedTextField, Panel, Row}; use leptos::leptos_dom::logging::console_log; use leptos::prelude::*; use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated}; +use reactive_stores::Store; use traits::{AsReactive, ReactiveCapture}; /// # Get Student Info @@ -124,6 +125,7 @@ pub fn HomePage() -> impl IntoView { server_resource .get() .map(|submission: StudentInfo| { + let store_info = Store::new(submission.clone()); let reactive_info = submission.as_reactive(); let elements_disabled = RwSignal::new(false); let result_msg = Signal::derive(move || { @@ -197,34 +199,36 @@ pub fn HomePage() -> impl IntoView { value=reactive_info.address /> - - - - - + // impl IntoView { placeholder = "Testing information..." name = "testing_input" label = "This is a testing question." - value = RwSignal::new("".to_owned()) + value = RwSignal::new(ValueType::String("".to_owned())) />
From 6512f5fe1338a7697d782a697f4221789417c7f5 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Fri, 19 Sep 2025 12:31:04 -0400 Subject: [PATCH 03/11] chore: add missing dependencies for Store macro --- Cargo.lock | 1 + Cargo.toml | 1 + 2 files changed, 2 insertions(+) diff --git a/Cargo.lock b/Cargo.lock index 7b7a06a..aa67db7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -2946,6 +2946,7 @@ dependencies = [ "leptos_oidc", "leptos_router", "macros", + "reactive_stores", "serde", "serde_dynamo", "tokio", diff --git a/Cargo.toml b/Cargo.toml index 017d911..ed0fae9 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -26,6 +26,7 @@ url = "2.5.4" macros = { path = "./macros" } traits = { path = "./traits" } chrono = { version = "0.4.42", features = ["serde"] } +reactive_stores = "0.2.5" [features] hydrate = [ From 7a453ad3677ff8e262240592db8b2ce2d43db1c7 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 25 Sep 2025 09:29:04 -0400 Subject: [PATCH 04/11] feat: update text field to use open-ended structures --- src/common/expandable_info.rs | 34 ++++++++++ src/common/mod.rs | 3 +- src/common/student_info.rs | 2 +- src/components/multi_entry/multi_entry.rs | 5 +- src/components/multi_entry/value_type.rs | 30 ++++----- src/components/text_field.rs | 75 +++++++++++++++++------ src/pages/home_page.rs | 61 +++++++++++++++--- src/pages/provider_entry.rs | 3 +- 8 files changed, 166 insertions(+), 47 deletions(-) create mode 100644 src/common/expandable_info.rs diff --git a/src/common/expandable_info.rs b/src/common/expandable_info.rs new file mode 100644 index 0000000..e7bafca --- /dev/null +++ b/src/common/expandable_info.rs @@ -0,0 +1,34 @@ +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; +use macros::Reactive; +use crate::components::ValueType; + +/// This struct allows for completely open-ended data storage. +/// +/// The `HashMap` allows the application to store an arbitrary number of values which are accessible +/// using a `String` as a key, allowing for simple shifting of stored values. All information is +/// stored using the `ValueType` enum, which provides some simple methods to verify the type of the +/// stored value. It *must* have a main key member name so that a database will know how to store +/// the data. +/// +/// In the future, this should also be optimized to store properly within a DynamoDB table. As of +/// now, the table will simply create a single massive `Map` field that contains all the stored +/// information in the `HashMap`. We'd prefer to be able to get single values if possible, +/// especially for interoperability with other applications, should they be created. +#[derive(Debug, Clone, Default, Serialize, Deserialize, Reactive)] +pub struct ExpandableInfo { + /// The main key member. Required and used to get/put the information in a database. + key_member: String, + /// The main data store. Contains all information that this struct needs to hold. + #[serde(default)] + data: HashMap, +} + +impl ExpandableInfo { + pub fn new(key: String) -> Self { + Self { + key_member: key, + data: HashMap::new(), + } + } +} diff --git a/src/common/mod.rs b/src/common/mod.rs index 8ca998f..0729aa3 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,5 +1,6 @@ mod student_info; mod user; mod form_schema; +mod expandable_info; -pub use self::{student_info::*, user::*, form_schema::*}; +pub use self::{student_info::*, user::*, form_schema::*, expandable_info::*}; diff --git a/src/common/student_info.rs b/src/common/student_info.rs index ca617a4..6f7b792 100644 --- a/src/common/student_info.rs +++ b/src/common/student_info.rs @@ -46,5 +46,5 @@ pub struct StudentInfo { } fn default_number() -> ValueType { - ValueType::Number(0) + ValueType::Number(None) } \ No newline at end of file diff --git a/src/components/multi_entry/multi_entry.rs b/src/components/multi_entry/multi_entry.rs index 1e85385..a1a49ff 100644 --- a/src/components/multi_entry/multi_entry.rs +++ b/src/components/multi_entry/multi_entry.rs @@ -14,6 +14,7 @@ fn render_entry_component(entry_data: MultiEntryData, member_info: MultiEntryMem InputType::Text => view! { @@ -92,11 +93,11 @@ pub fn MultiEntry( let mut new_entry = MultiEntryData::new(); new_entry.data.insert( name_member.get().member_name, - ValueType::String(String::from("Testing")), + ValueType::String(Some(String::from("Testing"))), ); new_entry.data.insert( info_member.get().member_name, - ValueType::String(new_entry.id.to_string()), + ValueType::String(Some(new_entry.id.to_string())), ); entries.update(|entry_vec| entry_vec.push(new_entry)); }; diff --git a/src/components/multi_entry/value_type.rs b/src/components/multi_entry/value_type.rs index 8b8d7fb..c482032 100644 --- a/src/components/multi_entry/value_type.rs +++ b/src/components/multi_entry/value_type.rs @@ -7,11 +7,11 @@ use std::fmt::Display; #[derive(Clone, Serialize, Deserialize, Debug)] pub enum ValueType { /// Indicates a `String` value. - String(String), + String(Option), /// Indicates an `i32` value. - Number(i32), + Number(Option), /// Indicates a `Vec`, where `T` can be any other `ValueType`. - List(Vec), + List(Option>), } impl ValueType { @@ -21,11 +21,11 @@ impl ValueType { } /// Attempts to get this value as a `String` type. This is different from the `to_string` method. - pub fn as_string(&self) -> Option { + pub fn as_string(&self) -> Result, Self> { if let ValueType::String(v) = self { - Some(v.clone()) + Ok(v.clone()) } else { - None + Err(self.clone()) } } @@ -35,11 +35,11 @@ impl ValueType { } /// Attempts to get this value as an `i32`. - pub fn as_number(&self) -> Option<&i32> { + pub fn as_number(&self) -> Result, Self> { if let ValueType::Number(v) = self { - Some(v) + Ok(v.clone()) } else { - None + Err(self.clone()) } } @@ -50,26 +50,26 @@ impl ValueType { /// Attempts to get this value as a `Vec`. Each of the values inside the list must /// also be converted later, as these values are any of the possible types in the `ValueType`. - pub fn as_list(&self) -> Option<&Vec> { + pub fn as_list(&self) -> Result>, Self> { if let ValueType::List(v) = self { - Some(v) + Ok(v.clone()) } else { - None + Err(self.clone()) } } } impl Default for ValueType { fn default() -> Self { - ValueType::String(String::new()) + ValueType::String(Some(String::new())) } } impl Display for ValueType { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { let str = match self { - ValueType::String(s) => s.clone(), - ValueType::Number(n) => n.to_string(), + ValueType::String(s) => s.clone().unwrap_or("".to_owned()), + ValueType::Number(n) => n.unwrap_or(0).to_string(), ValueType::List(l) => format!("{l:?}"), }; write!(f, "{}", str) diff --git a/src/components/text_field.rs b/src/components/text_field.rs index 51d2dba..7577503 100644 --- a/src/components/text_field.rs +++ b/src/components/text_field.rs @@ -1,3 +1,5 @@ +use std::collections::HashMap; +use leptos::leptos_dom::logging::console_log; use leptos::prelude::*; use crate::components::ValueType; @@ -5,33 +7,66 @@ use crate::components::ValueType; pub fn OutlinedTextField( #[prop(optional, into)] placeholder: String, // #[prop()] value: Subfield, + #[prop(into)] data_member: String, // should this be an RwSignal?? + #[prop(optional)] data_map: RwSignal>, #[prop()] value: RwSignal, + #[prop(default = "text".to_owned(), into)] input_type: String, #[prop(optional)] disabled: RwSignal, #[prop(optional)] error: RwSignal, #[prop(optional, into)] name: String, #[prop(optional, into)] label: String, - #[prop(optional, into)] input_type: String, #[prop(optional, into)] error_text: RwSignal ) -> impl IntoView { - // Check if the type is one of the supported formats. This input will only accept a string - // or a number value. - let on_input = move |e| { - match value.get() { - ValueType::String(_) => { - // Define string parse function. - value.set(ValueType::String(event_target_value(&e))); - }, - ValueType::Number(_) => { - // Define number parse function. - let temp = event_target_value(&e); - value.set(ValueType::Number(temp.parse::().unwrap_or(0))); - }, - _ => { - // Create some custom function that reports failure. - let value_type = value.get(); - println!("Input {value_type:?} is not a valid type."); - error.set(true); - error_text.set(format!("InputType {value_type:?} is not a valid type.")); + // This function parses the input into the correct type. It only accepts numbers or strings as + // valid types, and parses them accordingly. + let on_input = { + let input_type = input_type.clone(); + move |e| { + let to_parse = event_target_value(&e); + match input_type.as_str() { + "text" => { + // String parse function. It will always parse, but make sure the error + // state is set properly anyway. + error.set(false); + value.set(ValueType::String(Some(to_parse.clone()))); + data_map.update(|map| { + if let Some(val) = map.get_mut(&data_member) { + *val = ValueType::String(Some(to_parse)); + } else { + 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::().map(|n| ValueType::Number(Some(n))) { + Ok(value_type) => { + error.set(false); + value.set(value_type.clone()); + data_map.update(|map| { + if let Some(val) = map.get_mut(&data_member) { + *val = value_type; + } else { + map.insert(data_member.clone(), value_type); + } + }); + }, + Err(_) => { + error.set(true); + error_text.set("Value is not a valid number.".to_owned()); + } + }; + }, + _ => { + // Create some custom function that reports failure. + let msg = format!("Input {input_type:?} is not a valid type."); + console_log(msg.as_str()); + error.set(true); + error_text.set(msg); + } } } }; diff --git a/src/pages/home_page.rs b/src/pages/home_page.rs index 4b08df1..9b2f2f5 100644 --- a/src/pages/home_page.rs +++ b/src/pages/home_page.rs @@ -1,3 +1,4 @@ +use std::collections::HashMap; // Server dependencies #[cfg(feature = "ssr")] use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValue}; @@ -6,7 +7,7 @@ use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValu use serde_dynamo::{from_item, to_item}; use crate::app::Unauthenticated; -use crate::common::{StudentInfo, UserClaims}; +use crate::common::{ExpandableInfo, StudentInfo, UserClaims}; use crate::components::{ActionButton, Loading, OutlinedTextField, Panel, Row}; use leptos::leptos_dom::logging::console_log; use leptos::prelude::*; @@ -64,7 +65,7 @@ pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), S // The put_item action can create or update an item in a DynamoDB table. // It will completely replace any existing item with the same primary key, // which is the desired behavior. - let item = to_item(student_info.clone()).unwrap(); + let item = to_item(student_info.clone())?; console_log(format!("Arguments for sample submission: {:?}", item).as_str()); match dbclient .put_item() @@ -87,6 +88,16 @@ pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), S } } +#[server(LogExpandableInfo, endpoint = "/log-expandable")] +pub async fn log_expandable(info: ExpandableInfo) -> Result<(), ServerFnError> { + console_log(format!("Was given info: {:?}", info).as_str()); + // Manually specify the type here since we're not able to infer the type later. + let item: HashMap = to_item(info.clone())?; + console_log(format!("Converted into a DynamoDB item: {:?}", item).as_str()); + + Ok(()) +} + /// The main home page component. Contains a simple contact form. #[component] pub fn HomePage() -> impl IntoView { @@ -104,7 +115,7 @@ pub fn HomePage() -> impl IntoView { // Note that the value passed in MUST be equatable. // We get/unwrap the value repeatedly until we get a simple string value, then clone it so that // we don't lose access to it in the future, should we need it again. - let server_resource = Resource::new( + let server_resource: Resource = Resource::new( move || user_claims.get().map(|claim| claim.claims.subject.clone()), async |opt_username| match opt_username { Some(username) => get_submission(username).await.unwrap_or_else(|e| { @@ -115,6 +126,7 @@ pub fn HomePage() -> impl IntoView { }, ); let submit_action = ServerAction::::new(); + let log_action = ServerAction::::new(); view! { @@ -124,8 +136,10 @@ pub fn HomePage() -> impl IntoView { {move || { server_resource .get() - .map(|submission: StudentInfo| { + .map(|submission| { let store_info = Store::new(submission.clone()); + let expandable = ExpandableInfo::new("email".into()); + let expandable_react = expandable.as_reactive(); let reactive_info = submission.as_reactive(); let elements_disabled = RwSignal::new(false); let result_msg = Signal::derive(move || { @@ -162,16 +176,23 @@ pub fn HomePage() -> impl IntoView {

+ // Each OutlinedTextField could be simplified later into an enum: + // Text { data_member, label, placeholder }, i.e. + // Text { "first_name", "First Name:", "John" } @@ -180,6 +201,8 @@ pub fn HomePage() -> impl IntoView { label="Contact Email:" placeholder="student@region15.org" disabled=elements_disabled + data_member = "contact_email" + data_map = expandable_react.data value=reactive_info.contact_email /> @@ -188,6 +211,8 @@ pub fn HomePage() -> impl IntoView { label="Phone Number:" placeholder="123-456-7890" disabled=elements_disabled + data_member = "phone_number" + data_map = expandable_react.data value=reactive_info.phone_number /> @@ -196,10 +221,25 @@ pub fn HomePage() -> impl IntoView { label="Street Address:" placeholder="123 Fake Street" disabled=elements_disabled + data_member = "address" + data_map = expandable_react.data value=reactive_info.address /> + + + // + // Each RadioList could be simplified into an enum: + // Radio { data_member, label, items } // impl IntoView { // /> // // + // Select { data_member, label, items } // Date: Thu, 25 Sep 2025 10:38:42 -0400 Subject: [PATCH 06/11] feat: update select component to use open-ended structures (for real this time) A previous commit incorrectly called out the select component, except it really updated the radio component. --- src/components/select.rs | 25 ++++++++++++++++++++++--- src/pages/home_page.rs | 34 ++++++++++++++-------------------- 2 files changed, 36 insertions(+), 23 deletions(-) diff --git a/src/components/select.rs b/src/components/select.rs index 20c6f60..edb26ed 100644 --- a/src/components/select.rs +++ b/src/components/select.rs @@ -1,4 +1,8 @@ +use std::collections::HashMap; +use leptos::ev::{Event, Targeted}; use leptos::prelude::*; +use leptos::web_sys::HtmlSelectElement; +use crate::components::ValueType; /// A custom-styled select input. /// @@ -8,9 +12,26 @@ use leptos::prelude::*; pub fn Select( #[prop(default = vec!["Test Value".into()])] value_list: Vec, #[prop(default = RwSignal::new("".into()))] value: RwSignal, + #[prop(into)] data_member: String, + #[prop()] data_map: RwSignal>, #[prop(optional, into)] label: String, #[prop(default = RwSignal::new(false))] disabled: RwSignal, ) -> impl IntoView { + + let on_change = { + let data_member = data_member.clone(); + move |e: Targeted| { + let target_value = e.target().value(); + data_map.update(|map| { + if let Some(val) = map.get_mut(&data_member) { + *val = ValueType::String(Some(target_value)); + } else { + map.insert(data_member.clone(), ValueType::String(Some(target_value))); + } + }); + } + }; + view! {