feat: update text field to use open-ended structures
This commit is contained in:
1 parent
6512f5fe13
commit
7a453ad367
8 files changed
+166
-47
No files matched your search
@@ -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<String, ValueType>,
|
||||
}
|
||||
|
||||
impl ExpandableInfo {
|
||||
pub fn new(key: String) -> Self {
|
||||
Self {
|
||||
key_member: key,
|
||||
data: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
+2
-1
@@ -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::*};
|
||||
@@ -46,5 +46,5 @@ pub struct StudentInfo {
|
||||
}
|
||||
|
||||
fn default_number() -> ValueType {
|
||||
ValueType::Number(0)
|
||||
ValueType::Number(None)
|
||||
}
|
||||
@@ -14,6 +14,7 @@ fn render_entry_component(entry_data: MultiEntryData, member_info: MultiEntryMem
|
||||
InputType::Text => view! {
|
||||
<OutlinedTextField
|
||||
label=member_info.display_name
|
||||
data_member=member_info.member_name
|
||||
placeholder="testing"
|
||||
value=reactive
|
||||
/>
|
||||
@@ -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));
|
||||
};
|
||||
|
||||
@@ -7,11 +7,11 @@ use std::fmt::Display;
|
||||
#[derive(Clone, Serialize, Deserialize, Debug)]
|
||||
pub enum ValueType {
|
||||
/// Indicates a `String` value.
|
||||
String(String),
|
||||
String(Option<String>),
|
||||
/// Indicates an `i32` value.
|
||||
Number(i32),
|
||||
Number(Option<i32>),
|
||||
/// Indicates a `Vec<T>`, where `T` can be any other `ValueType`.
|
||||
List(Vec<ValueType>),
|
||||
List(Option<Vec<ValueType>>),
|
||||
}
|
||||
|
||||
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<String> {
|
||||
pub fn as_string(&self) -> Result<Option<String>, 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<Option<i32>, 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<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>> {
|
||||
pub fn as_list(&self) -> Result<Option<Vec<ValueType>>, 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)
|
||||
|
||||
@@ -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<TStore, TInner, ValueType>,
|
||||
#[prop(into)] data_member: String, // should this be an RwSignal??
|
||||
#[prop(optional)] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
#[prop()] value: RwSignal<ValueType>,
|
||||
#[prop(default = "text".to_owned(), into)] input_type: String,
|
||||
#[prop(optional)] disabled: RwSignal<bool>,
|
||||
#[prop(optional)] error: RwSignal<bool>,
|
||||
#[prop(optional, into)] name: String,
|
||||
#[prop(optional, into)] label: String,
|
||||
#[prop(optional, into)] input_type: String,
|
||||
#[prop(optional, into)] error_text: RwSignal<String>
|
||||
) -> 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::<i32>().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::<i32>().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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
+54
-7
@@ -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<String, AttributeValue> = 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<StudentInfo> = 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::<CreateSampleSubmission>::new();
|
||||
let log_action = ServerAction::<LogExpandableInfo>::new();
|
||||
|
||||
view! {
|
||||
<AuthLoaded fallback=Loading>
|
||||
@@ -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 {
|
||||
</p>
|
||||
</Row>
|
||||
<Row>
|
||||
// Each OutlinedTextField could be simplified later into an enum:
|
||||
// Text { data_member, label, placeholder }, i.e.
|
||||
// Text { "first_name", "First Name:", "John" }
|
||||
<OutlinedTextField
|
||||
label="First Name:"
|
||||
placeholder="John"
|
||||
disabled=elements_disabled
|
||||
data_member = "first_name"
|
||||
data_map = expandable_react.data
|
||||
value=reactive_info.first_name
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Last Name:"
|
||||
placeholder="Smith"
|
||||
disabled=elements_disabled
|
||||
data_member = "last_name"
|
||||
data_map = expandable_react.data
|
||||
value=reactive_info.last_name
|
||||
/>
|
||||
</Row>
|
||||
@@ -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
|
||||
/>
|
||||
</Row>
|
||||
@@ -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
|
||||
/>
|
||||
</Row>
|
||||
@@ -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
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Highest Math SAT Score:"
|
||||
placeholder="600"
|
||||
disabled=elements_disabled
|
||||
data_member = "math_sat"
|
||||
data_map = expandable_react.data
|
||||
input_type = "number"
|
||||
value=reactive_info.math_sat
|
||||
/>
|
||||
</Row>
|
||||
// <Row>
|
||||
// Each RadioList could be simplified into an enum:
|
||||
// Radio { data_member, label, items }
|
||||
// <RadioList
|
||||
// label="Town:"
|
||||
// selected=reactive_info.town
|
||||
@@ -211,6 +251,7 @@ pub fn HomePage() -> impl IntoView {
|
||||
// />
|
||||
// </Row>
|
||||
// <Row>
|
||||
// Select { data_member, label, items }
|
||||
// <Select
|
||||
// label="Gender:"
|
||||
// value_list=vec!["Male", "Female", "Prefer not to answer"]
|
||||
@@ -225,11 +266,17 @@ pub fn HomePage() -> impl IntoView {
|
||||
<ActionButton
|
||||
on:click=move |_| {
|
||||
let captured = reactive_info.capture();
|
||||
let captured_map = expandable_react.capture();
|
||||
console_log(format!("Found values: {captured:?}").as_str());
|
||||
submit_action
|
||||
.dispatch(CreateSampleSubmission {
|
||||
student_info: captured,
|
||||
console_log(format!("Map values: {captured_map:?}").as_str());
|
||||
log_action
|
||||
.dispatch(LogExpandableInfo {
|
||||
info: captured_map
|
||||
});
|
||||
// submit_action
|
||||
// .dispatch(CreateSampleSubmission {
|
||||
// student_info: captured,
|
||||
// });
|
||||
}
|
||||
disabled=elements_disabled
|
||||
>"Submit"</ActionButton>
|
||||
|
||||
@@ -28,7 +28,8 @@ pub fn ProviderEntry() -> impl IntoView {
|
||||
placeholder = "Testing information..."
|
||||
name = "testing_input"
|
||||
label = "This is a testing question."
|
||||
value = RwSignal::new(ValueType::String("".to_owned()))
|
||||
data_member = "testing"
|
||||
value = RwSignal::new(ValueType::String(None))
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
|
||||
Reference in new issue
Block a user