Refactor all components to use expandable data #10

Merged
BobdaFett merged 11 commits from feature/multi-entry-data into main 2025-09-26 16:13:29 +00:00
9 changed files with 241 additions and 75 deletions
Showing only changes of commit 0d4bbc9e70 - Show all commits

No files matched your search

+18
View File
@@ -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<String>),
/// Represents a checkbox input. Requires a `String` key and a series of options. The selected
/// value will be returned as a `Vec<String>`.
Checkbox(Vec<String>)
}
+2 -1
View File
@@ -1,4 +1,5 @@
mod student_info; mod student_info;
mod user; mod user;
mod form_schema;
pub use self::{student_info::*, user::*}; pub use self::{student_info::*, user::*, form_schema::*};
+11
View File
@@ -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::*,
};
@@ -1,75 +1,27 @@
use std::collections::HashMap; use std::sync::Arc;
use leptos::either::either;
use crate::components::ActionButton; use crate::components::{ActionButton, OutlinedTextField};
use leptos::prelude::*; use leptos::prelude::*;
use macros::Reactive; use crate::common::InputType;
use serde::{Deserialize, Serialize}; use crate::components::ValueType;
use uuid::Uuid; use super::{multi_entry_data::*, multi_entry_member::*};
#[derive(Clone, Serialize, Deserialize, Debug, Reactive, Default)] fn render_entry_component(entry_data: MultiEntryData, member_info: MultiEntryMember, label: String) -> impl IntoView {
pub struct MultiEntryData { // Parse the multientry members into the correct components
pub id: Uuid, let data = entry_data.data.get(&member_info.member_name);
pub data: HashMap<String, ValueType>, let reactive = RwSignal::new(data.unwrap().as_string().unwrap());
} let temp = either!(member_info.input_type,
InputType::Text => view! {
impl MultiEntryData { <OutlinedTextField
/// Creates a new MultiEntryData struct. label=member_info.display_name
pub fn new() -> Self { placeholder="testing"
Self { value=reactive
id: Uuid::new_v4(), />
data: HashMap::new(), },
} _ => view! {}
} );
}
temp.into_view()
/// 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(),
}
}
} }
/// Represents a single entry in a MultiEntry component. /// Represents a single entry in a MultiEntry component.
@@ -86,30 +38,54 @@ pub fn Entry(
/// The secondary display member for this entry. /// The secondary display member for this entry.
#[prop()] #[prop()]
info_member: Signal<MultiEntryMember>, info_member: Signal<MultiEntryMember>,
/// The schema for all inputs that should be used, and what `MultiEntryMember` info is associated
/// with them.
#[prop()]
schema: Arc<Vec<MultiEntryMember>>
) -> impl IntoView { ) -> impl IntoView {
let name_data = entry_data.data.get(&name_member.get().member_name).unwrap(); 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 info_data = entry_data.data.get(&info_member.get().member_name).unwrap();
let schema = Arc::clone(&schema);
view! { view! {
// This div MUST have the same spacing CSS as the second-level header div // This div MUST have the same spacing CSS as the second-level header div
<div class="flex rounded-sm transition-shadow shadow-sm hover:shadow-lg/30 p-3 m-1"> <div class="flex rounded-sm transition-shadow shadow-sm hover:shadow-lg/30 p-3 m-1">
<span class="flex-1">{name_data.as_string()}</span> <span class="flex-1">{name_data.to_string()}</span>
<span class="flex-1">{info_data.as_string()}</span> <span class="flex-1">{info_data.to_string()}</span>
</div> </div>
{schema
.iter()
.map(|input_type| {
let data = entry_data.clone();
let input = input_type.clone();
view! {
<div>{render_entry_component(data, input, "test".to_owned())}</div>
}
}).collect::<Vec<_>>()
}
} }
} }
/// Allows users to enter more open-ended information, and generates a `Vec<MultiDataEntry>` 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] #[component]
pub fn MultiEntry( pub fn MultiEntry(
/// The list of entries to use. /// The list of entries to use.
#[prop()] #[prop()]
entries: RwSignal<Vec<MultiEntryData>>, entries: RwSignal<Vec<MultiEntryData>>,
/// the main display member for each entry object. /// The main display member for each entry object.
#[prop()] #[prop()]
name_member: Signal<MultiEntryMember>, name_member: Signal<MultiEntryMember>,
/// The secondary display member for each entry object. /// The secondary display member for each entry object.
#[prop()] #[prop()]
info_member: Signal<MultiEntryMember>, info_member: Signal<MultiEntryMember>,
/// 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<MultiEntryMember>
) -> impl IntoView { ) -> impl IntoView {
// Creates an entry in the given entries list. // Creates an entry in the given entries list.
let create_entry = move |_| { let create_entry = move |_| {
@@ -124,6 +100,8 @@ pub fn MultiEntry(
); );
entries.update(|entry_vec| entry_vec.push(new_entry)); entries.update(|entry_vec| entry_vec.push(new_entry));
}; };
let rc_schema = Arc::new(schema);
view! { view! {
<div class="flex-auto p-2 m-5"> <div class="flex-auto p-2 m-5">
@@ -145,11 +123,13 @@ pub fn MultiEntry(
each=move || entries.get() each=move || entries.get()
key=|entry: &MultiEntryData| entry.id key=|entry: &MultiEntryData| entry.id
children=move |entry| { children=move |entry| {
let schema = Arc::clone(&rc_schema);
view! { view! {
<Entry <Entry
entry_data=entry.clone() entry_data=entry.clone()
name_member=name_member name_member=name_member
info_member=info_member info_member=info_member
schema=schema
/> />
} }
} }
@@ -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<String, ValueType>,
}
impl MultiEntryData {
/// Creates a new MultiEntryData struct.
pub fn new() -> Self {
Self {
id: Uuid::new_v4(),
data: HashMap::new(),
}
}
}
@@ -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
}
}
}
+71
View File
@@ -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<T>`, where `T` can be any other `ValueType`.
List(Vec<ValueType>),
}
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<String> {
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<ValueType>`. 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<ValueType>> {
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)
}
}
+10 -1
View File
@@ -7,12 +7,16 @@ use serde_dynamo::{from_item, to_item};
use crate::app::Unauthenticated; use crate::app::Unauthenticated;
use crate::common::{StudentInfo, UserClaims}; 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::leptos_dom::logging::console_log;
use leptos::prelude::*; use leptos::prelude::*;
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated}; use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated};
use traits::{AsReactive, ReactiveCapture}; 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")] #[server(GetSubmission, endpoint = "/get-submission")]
pub async fn get_submission(id: String) -> Result<StudentInfo, ServerFnError> { pub async fn get_submission(id: String) -> Result<StudentInfo, ServerFnError> {
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
@@ -47,6 +51,10 @@ pub async fn get_submission(id: String) -> Result<StudentInfo, ServerFnError> {
} }
} }
/// # 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")] #[server(CreateSampleSubmission, endpoint = "/create-sample-submission")]
pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), ServerFnError> { pub async fn create_sample_submission(student_info: StudentInfo) -> Result<(), ServerFnError> {
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await; 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] #[component]
pub fn HomePage() -> impl IntoView { pub fn HomePage() -> impl IntoView {
// Creates a reactive value to update the button // Creates a reactive value to update the button
+1
View File
@@ -37,6 +37,7 @@ pub fn ProviderEntry() -> impl IntoView {
entries=multi_entries entries=multi_entries
name_member=Signal::from(MultiEntryMember::from_str("Entry Name", "name")) name_member=Signal::from(MultiEntryMember::from_str("Entry Name", "name"))
info_member=Signal::from(MultiEntryMember::from_str("Entry Info", "info")) info_member=Signal::from(MultiEntryMember::from_str("Entry Info", "info"))
schema=vec![MultiEntryMember::from_str("Entry Name", "name"), MultiEntryMember::from_str("Entry Info", "info")]
/> />
</div> </div>
</Authenticated> </Authenticated>