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.
This commit is contained in:
alphafetus committed 2025-09-16 19:08:13 -04:00
1 parent 4fc01acd50
commit 0d4bbc9e70
9 files changed
+241 -75

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 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 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<String, ValueType>,
}
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! {
<OutlinedTextField
label=member_info.display_name
placeholder="testing"
value=reactive
/>
},
_ => 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<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 {
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
<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">{info_data.as_string()}</span>
<span class="flex-1">{name_data.to_string()}</span>
<span class="flex-1">{info_data.to_string()}</span>
</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]
pub fn MultiEntry(
/// The list of entries to use.
#[prop()]
entries: RwSignal<Vec<MultiEntryData>>,
/// the main display member for each entry object.
/// The main display member for each entry object.
#[prop()]
name_member: Signal<MultiEntryMember>,
/// The secondary display member for each entry object.
#[prop()]
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 {
// 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! {
<div class="flex-auto p-2 m-5">
@@ -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! {
<Entry
entry_data=entry.clone()
name_member=name_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::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<StudentInfo, ServerFnError> {
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")]
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
+1
View File
@@ -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")]
/>
</div>
</Authenticated>