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")] />