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 = [ diff --git a/src/common/expandable_info.rs b/src/common/expandable_info.rs new file mode 100644 index 0000000..ebb0f8f --- /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, Serialize, Deserialize, Reactive)] +pub struct ExpandableInfo { + /// The main key member. Required and used to get/put the information in a database. + pub subject: String, + /// The main data store. Contains all information that this struct needs to hold. + #[serde(default, flatten)] + pub data: HashMap, +} + +impl ExpandableInfo { + pub fn new(key: String) -> Self { + Self { + subject: key, + data: HashMap::new(), + } + } +} 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..7e04a46 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -1,4 +1,7 @@ mod student_info; mod user; +mod form_schema; +mod expandable_info; +pub mod value_type; -pub use self::{student_info::*, user::*}; +pub use self::{expandable_info::*, form_schema::*, student_info::*, user::*}; diff --git a/src/common/student_info.rs b/src/common/student_info.rs index 1533782..6f7b792 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(None) +} \ No newline at end of file diff --git a/src/common/value_type.rs b/src/common/value_type.rs new file mode 100644 index 0000000..4572e1b --- /dev/null +++ b/src/common/value_type.rs @@ -0,0 +1,77 @@ +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(PartialEq, Clone, Serialize, Deserialize, Debug)] +pub enum ValueType { + /// Indicates a `String` value. + String(Option), + /// Indicates an `i32` value. + Number(Option), + /// Indicates a `Vec`, where `T` can be any other `ValueType`. + List(Option>), +} + +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) -> Result, Self> { + if let ValueType::String(v) = self { + Ok(v.clone()) + } else { + Err(self.clone()) + } + } + + /// 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) -> Result, Self> { + if let ValueType::Number(v) = self { + Ok(v.clone()) + } else { + Err(self.clone()) + } + } + + /// 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) -> Result>, Self> { + if let ValueType::List(v) = self { + Ok(v.clone()) + } else { + Err(self.clone()) + } + } +} + +impl Default for ValueType { + fn default() -> Self { + 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().unwrap_or("".to_owned()), + ValueType::Number(n) => n.clone().unwrap_or("".to_owned()), + ValueType::List(l) => format!("{l:?}"), + }; + write!(f, "{}", str) + } +} diff --git a/src/components/checkbox.rs b/src/components/checkbox.rs index 2a3bb53..47e5791 100644 --- a/src/components/checkbox.rs +++ b/src/components/checkbox.rs @@ -1,4 +1,6 @@ +use std::collections::HashMap; use leptos::prelude::*; +use crate::components::ValueType; #[component] pub fn Checkbox( @@ -42,7 +44,9 @@ pub fn Checkbox( #[component] pub fn CheckboxList( - #[prop(default = RwSignal::new(vec![]))] selected: RwSignal>, + #[prop(optional)] selected: RwSignal>, + #[prop(into)] data_member: String, + #[prop()] data_map: RwSignal>, #[prop()] items: Vec, #[prop(default = RwSignal::new(false))] disabled: RwSignal, #[prop(optional, into)] label: String, @@ -53,32 +57,56 @@ pub fn CheckboxList( {items .into_iter() .map(|item| { - let is_checked = { + let checked_signal = Signal::derive({ let item_name = item.clone(); - move || { selected.get().contains(&item_name) } + let data_member = data_member.clone(); + move || { + data_map.get() // HashMap + .get(&data_member) // Option + .unwrap_or(&ValueType::List(None)) // ValueType + .as_list() // Result>, ValueType> + .unwrap_or(Some(vec!())) // Option> + .unwrap_or(vec!()) // Vec + .contains(&ValueType::String(Some(item_name.clone()))) // bool + } + }); + + let on_change = { + let item_name = item.clone(); + let data_member = data_member.clone(); + move || { + let result = ValueType::String(Some(item_name.clone())); + data_map.update(|map| { + // Attempt to get the value type from the hash map. + match map.get_mut(&data_member) { + Some(value_type) => { + // Attempt to get the value as a list. + match value_type.as_list().unwrap_or(None) { + Some(mut list) => { + // Attempt to find the selected value in the selected list. + // If it's present, remove it, or vice versa. + match list.iter().position(|val| *val == result) { + Some(index) => { list.remove(index); }, + None => { list.push(result); } + }; + // Update the existing value_type entry in the hash map. + *value_type = ValueType::List(Some(list)); + }, + // Insert a new value_type entry. + None => { *value_type = ValueType::List(Some(vec![result])); } + }; + }, + // Insert a new value_type entry. + None => { map.insert(data_member.clone(), ValueType::List(Some(vec![result]))); } + }; + }); + } }; - let checked_signal = Signal::derive(is_checked); - // Check if this checkbox should be selected - + view! { , -} - -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().clone()); + let temp = either!(member_info.input_type, + InputType::Text => view! { + + }, + _ => view! {} + ); + + temp.into_view() } /// Represents a single entry in a MultiEntry component. @@ -86,44 +41,70 @@ 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 |_| { 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)); }; + + let rc_schema = Arc::new(schema); view! {
@@ -145,11 +126,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/radio.rs b/src/components/radio.rs index 0123571..561550a 100644 --- a/src/components/radio.rs +++ b/src/components/radio.rs @@ -1,4 +1,6 @@ +use std::collections::HashMap; use leptos::prelude::*; +use crate::components::ValueType; #[component] pub fn Radio( @@ -42,7 +44,9 @@ pub fn Radio( #[component] pub fn RadioList( - #[prop(default = RwSignal::new(String::new()))] selected: RwSignal, + // #[prop(default = RwSignal::new(String::new()))] selected: RwSignal, + #[prop(into)] data_member: String, + #[prop()] data_map: RwSignal>, #[prop()] items: Vec, #[prop(default = RwSignal::new(false))] disabled: RwSignal, #[prop(optional, into)] label: String, @@ -53,20 +57,43 @@ pub fn RadioList( {items .into_iter() .map(|item| { - let is_checked = { + let checked_signal = Signal::derive({ let item_name = item.clone(); - move || { selected.get() == item_name } + let data_member = data_member.clone(); + move || { + // Check if the value is contained in the data map's fields + let map = data_map.get(); + let value = map.get(&data_member); + let mut result = false; + if let Some(val) = value { + if val.is_string() { + result = val.as_string().unwrap().unwrap() == item_name + } + } + + result + } + }); + + let on_change = { + let item_name = item.clone(); + let data_member = data_member.clone(); + move || { + // Update the hash map + data_map.update(|map| { + if let Some(val) = map.get_mut(&data_member) { + *val = ValueType::String(Some(item_name.clone())); + } else { + map.insert(data_member.clone(), ValueType::String(Some(item_name.clone()))); + } + }); + } }; - let checked_signal = Signal::derive(is_checked); - // Check if this checkbox should be selected view! { , - #[prop(default = RwSignal::new("".into()))] value: RwSignal, + #[prop()] value_list: Vec, + #[prop(optional)] 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! {