From 7a453ad3677ff8e262240592db8b2ce2d43db1c7 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Thu, 25 Sep 2025 09:29:04 -0400 Subject: [PATCH] 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 } //