Merge pull request #10 from R15-Software-Dev/feature/multi-entry-data
Refactor all components to use expandable data
This commit was merged in pull request #10.
This commit is contained in:
17 files changed
+568
-173
No files matched your search
Generated
+1
@@ -2946,6 +2946,7 @@ dependencies = [
|
||||
"leptos_oidc",
|
||||
"leptos_router",
|
||||
"macros",
|
||||
"reactive_stores",
|
||||
"serde",
|
||||
"serde_dynamo",
|
||||
"tokio",
|
||||
|
||||
@@ -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 = [
|
||||
|
||||
@@ -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<String, ValueType>,
|
||||
}
|
||||
|
||||
impl ExpandableInfo {
|
||||
pub fn new(key: String) -> Self {
|
||||
Self {
|
||||
subject: key,
|
||||
data: HashMap::new(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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>)
|
||||
}
|
||||
+4
-1
@@ -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::*};
|
||||
+16
-11
@@ -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<Utc>,
|
||||
/// The student's gender.
|
||||
#[serde(default)]
|
||||
pub gender: String,
|
||||
pub gender: ValueType, // String
|
||||
/// The student's athletic information.
|
||||
#[serde(default)]
|
||||
pub athletic_info: Vec<MultiEntryData>,
|
||||
}
|
||||
|
||||
fn default_number() -> ValueType {
|
||||
ValueType::Number(None)
|
||||
}
|
||||
@@ -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<String>),
|
||||
/// Indicates an `i32` value.
|
||||
Number(Option<String>),
|
||||
/// Indicates a `Vec<T>`, where `T` can be any other `ValueType`.
|
||||
List(Option<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) -> Result<Option<String>, 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<Option<String>, 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<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) -> Result<Option<Vec<ValueType>>, 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)
|
||||
}
|
||||
}
|
||||
+49
-21
@@ -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<Vec<String>>,
|
||||
#[prop(optional)] selected: RwSignal<Vec<String>>,
|
||||
#[prop(into)] data_member: String,
|
||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
#[prop()] items: Vec<String>,
|
||||
#[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>,
|
||||
#[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<ValueType>
|
||||
.unwrap_or(&ValueType::List(None)) // ValueType
|
||||
.as_list() // Result<Option<Vec<ValueType>>, ValueType>
|
||||
.unwrap_or(Some(vec!())) // Option<Vec<ValueType>>
|
||||
.unwrap_or(vec!()) // Vec<ValueType>
|
||||
.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! {
|
||||
<Checkbox
|
||||
checked=checked_signal
|
||||
on_change={
|
||||
let item_name = item.clone();
|
||||
move || {
|
||||
selected
|
||||
.update(|list| {
|
||||
if let Some(index) = list
|
||||
.iter()
|
||||
.position(|val| *val == item_name)
|
||||
{
|
||||
list.remove(index);
|
||||
} else {
|
||||
list.push(item_name.clone());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
on_change=on_change
|
||||
// The actual selected values are tracked by this element, not by the checkboxes themselves.
|
||||
value=item.clone()
|
||||
disabled=disabled
|
||||
|
||||
@@ -0,0 +1,10 @@
|
||||
pub mod multi_entry_data;
|
||||
pub mod multi_entry;
|
||||
mod multi_entry_member;
|
||||
|
||||
pub use crate::common::value_type::*;
|
||||
pub use self::{
|
||||
multi_entry::*,
|
||||
multi_entry_data::*,
|
||||
multi_entry_member::*,
|
||||
};
|
||||
@@ -1,75 +1,30 @@
|
||||
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>,
|
||||
}
|
||||
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! {
|
||||
<OutlinedTextField
|
||||
label=member_info.display_name
|
||||
data_member=member_info.member_name
|
||||
data_map=RwSignal::new(HashMap::default())
|
||||
placeholder="testing"
|
||||
value=reactive
|
||||
/>
|
||||
},
|
||||
_ => 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(),
|
||||
}
|
||||
}
|
||||
temp.into_view()
|
||||
}
|
||||
|
||||
/// Represents a single entry in a MultiEntry component.
|
||||
@@ -86,45 +41,71 @@ 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 |_| {
|
||||
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! {
|
||||
<div class="flex-auto p-2 m-5">
|
||||
// Header section
|
||||
@@ -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! {
|
||||
<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
|
||||
}
|
||||
}
|
||||
}
|
||||
+36
-9
@@ -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<String>,
|
||||
// #[prop(default = RwSignal::new(String::new()))] selected: RwSignal<ValueType>,
|
||||
#[prop(into)] data_member: String,
|
||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
#[prop()] items: Vec<String>,
|
||||
#[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>,
|
||||
#[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! {
|
||||
<Radio
|
||||
checked=checked_signal
|
||||
on_change={
|
||||
let item_name = item.clone();
|
||||
move || selected.set(item_name.clone())
|
||||
}
|
||||
on_change=on_change
|
||||
// The actual selected values are tracked by this element, not by the checkboxes themselves.
|
||||
value=item.clone()
|
||||
disabled=disabled
|
||||
|
||||
@@ -1,4 +1,8 @@
|
||||
use std::collections::HashMap;
|
||||
use leptos::ev::{Event, Targeted};
|
||||
use leptos::prelude::*;
|
||||
use leptos::web_sys::HtmlSelectElement;
|
||||
use crate::components::ValueType;
|
||||
|
||||
/// A custom-styled select input.
|
||||
///
|
||||
@@ -6,11 +10,27 @@ use leptos::prelude::*;
|
||||
/// by the `value` prop of the component.
|
||||
#[component]
|
||||
pub fn Select(
|
||||
#[prop(default = vec!["Test Value".into()])] value_list: Vec<String>,
|
||||
#[prop(default = RwSignal::new("".into()))] value: RwSignal<String>,
|
||||
#[prop()] value_list: Vec<String>,
|
||||
#[prop(optional)] value: RwSignal<String>,
|
||||
#[prop(into)] data_member: String,
|
||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
#[prop(optional, into)] label: String,
|
||||
#[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>,
|
||||
) -> impl IntoView {
|
||||
let on_change = {
|
||||
let data_member = data_member.clone();
|
||||
move |e: Targeted<Event, HtmlSelectElement>| {
|
||||
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! {
|
||||
<div class="flex flex-1">
|
||||
<label class="flex flex-col flex-1">
|
||||
@@ -20,9 +40,7 @@ pub fn Select(
|
||||
transition-border duration-150
|
||||
border-red-700 bg-transparent
|
||||
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
|
||||
on:change:target=move |e| {
|
||||
value.set(e.target().value());
|
||||
}
|
||||
on:change:target=on_change
|
||||
disabled = disabled >
|
||||
// This closure handles the display of the options.
|
||||
{move || {
|
||||
|
||||
@@ -1,32 +1,79 @@
|
||||
use std::collections::HashMap;
|
||||
use leptos::leptos_dom::logging::console_log;
|
||||
use leptos::prelude::*;
|
||||
use std::str::FromStr;
|
||||
use crate::components::ValueType;
|
||||
|
||||
#[component]
|
||||
pub fn OutlinedTextField<T>(
|
||||
#[prop(optional, into)] placeholder: T,
|
||||
#[prop(optional)] value: RwSignal<T>,
|
||||
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()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
#[prop(optional)] 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,
|
||||
) -> impl IntoView
|
||||
where
|
||||
T: Default + 'static + FromStr + ToString + PartialEq + Send + Sync + Clone,
|
||||
{
|
||||
// Parsing logic for non-string types which happens on each input.
|
||||
let on_input = move |e| {
|
||||
let input_value = event_target_value(&e);
|
||||
let can_parse = T::from_str(&input_value);
|
||||
if let Ok(parsed_value) = can_parse {
|
||||
value.set(parsed_value);
|
||||
} else {
|
||||
error.set(true);
|
||||
value.set(T::default());
|
||||
#[prop(optional, into)] error_text: RwSignal<String>
|
||||
) -> impl IntoView {
|
||||
// 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();
|
||||
let data_member = data_member.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.to_string()))) {
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
view! (
|
||||
view! {
|
||||
<div class="flex flex-1">
|
||||
<label class="flex flex-col flex-1">
|
||||
<span class="block ml-1.5 mb-0 font-bold">{label}</span>
|
||||
@@ -37,12 +84,21 @@ where
|
||||
disabled:border-gray-600 disabled:pointer-events-none disabled:bg-gray-600/33"
|
||||
r#type={input_type}
|
||||
disabled={disabled}
|
||||
placeholder={placeholder.to_string()}
|
||||
placeholder={placeholder}
|
||||
prop:name=name
|
||||
prop:value={move || value.get().to_string()} // Sets the initial value.
|
||||
prop:value=move || data_map
|
||||
.get() // HashMap
|
||||
.get(&data_member) // Option<ValueType>
|
||||
.unwrap_or(&ValueType::String(None)) // ValueType
|
||||
.to_string()
|
||||
on:input=on_input
|
||||
/>
|
||||
<div
|
||||
class="text-red-600 text-sm mr-1.5 ml-1.5"
|
||||
class=(["hidden"], move || !error.get())>
|
||||
{error_text}
|
||||
</div>
|
||||
</label>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
}
|
||||
+83
-29
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
// Server dependencies
|
||||
#[cfg(feature = "ssr")]
|
||||
use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValue};
|
||||
@@ -6,26 +7,30 @@ 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::components::{ActionButton, CheckboxList, Loading, MultiEntry, MultiEntryMember, OutlinedTextField, OutlinedTextFieldPropsBuilder_Error_Repeated_field_disabled, Panel, RadioList, Row, Select};
|
||||
use crate::common::{ExpandableInfo, UserClaims};
|
||||
use crate::components::{ActionButton, CheckboxList, 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> {
|
||||
pub async fn get_submission(subject: String) -> Result<ExpandableInfo, ServerFnError> {
|
||||
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
|
||||
let dbclient = Client::new(&config);
|
||||
|
||||
console_log(format!("Getting values from API using username {}", id).as_str());
|
||||
console_log(format!("Getting values from API using subject {}", subject).as_str());
|
||||
|
||||
// Gets the item from the database. It only returns an error if the database
|
||||
// experiences an error - not if the item is not found.
|
||||
match dbclient
|
||||
.get_item()
|
||||
.table_name("student-applications")
|
||||
.key("Email", AttributeValue::S(id.clone()))
|
||||
.table_name("leptos-test")
|
||||
.key("subject", AttributeValue::S(subject.clone()))
|
||||
.send()
|
||||
.await
|
||||
{
|
||||
@@ -35,8 +40,7 @@ pub async fn get_submission(id: String) -> Result<StudentInfo, ServerFnError> {
|
||||
Ok(from_item(item)?)
|
||||
} else {
|
||||
console_log("Couldn't find entry, returning default.");
|
||||
let mut info = StudentInfo::default();
|
||||
info.Email = id; // Set the correct subject
|
||||
let info = ExpandableInfo::new(subject);
|
||||
Ok(info)
|
||||
}
|
||||
}
|
||||
@@ -47,19 +51,23 @@ 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> {
|
||||
pub async fn create_sample_submission(student_info: ExpandableInfo) -> Result<(), ServerFnError> {
|
||||
let config = aws_config::load_defaults(aws_config::BehaviorVersion::latest()).await;
|
||||
let dbclient = Client::new(&config);
|
||||
|
||||
// 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()
|
||||
.table_name("student-applications")
|
||||
.table_name("leptos-test")
|
||||
.set_item(Some(item))
|
||||
.send()
|
||||
.await
|
||||
@@ -78,6 +86,17 @@ 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)?;
|
||||
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 {
|
||||
// Creates a reactive value to update the button
|
||||
@@ -94,17 +113,18 @@ 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<ExpandableInfo> = 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| {
|
||||
Some(subject) => get_submission(subject.clone()).await.unwrap_or_else(|e| {
|
||||
console_log(e.to_string().as_str());
|
||||
StudentInfo::default()
|
||||
ExpandableInfo::new(subject)
|
||||
}),
|
||||
None => StudentInfo::default(),
|
||||
None => ExpandableInfo::new("".to_owned()),
|
||||
},
|
||||
);
|
||||
let submit_action = ServerAction::<CreateSampleSubmission>::new();
|
||||
let log_action = ServerAction::<LogExpandableInfo>::new();
|
||||
|
||||
view! {
|
||||
<AuthLoaded fallback=Loading>
|
||||
@@ -114,8 +134,8 @@ pub fn HomePage() -> impl IntoView {
|
||||
{move || {
|
||||
server_resource
|
||||
.get()
|
||||
.map(|submission: StudentInfo| {
|
||||
let reactive_info = submission.as_reactive();
|
||||
.map(|submission| {
|
||||
let expandable_react = submission.as_reactive();
|
||||
let elements_disabled = RwSignal::new(false);
|
||||
let result_msg = Signal::derive(move || {
|
||||
// Eventually, this function will show and hide a loading symbol
|
||||
@@ -151,17 +171,20 @@ pub fn HomePage() -> impl IntoView {
|
||||
</p>
|
||||
</Row>
|
||||
<Row>
|
||||
// Text { data_member, label, placeholder }
|
||||
<OutlinedTextField
|
||||
label="First Name:"
|
||||
placeholder="John"
|
||||
disabled=elements_disabled
|
||||
value=reactive_info.first_name
|
||||
data_member = "first_name"
|
||||
data_map = expandable_react.data
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Last Name:"
|
||||
placeholder="Smith"
|
||||
disabled=elements_disabled
|
||||
value=reactive_info.last_name
|
||||
data_member = "last_name"
|
||||
data_map = expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
@@ -169,7 +192,8 @@ pub fn HomePage() -> impl IntoView {
|
||||
label="Contact Email:"
|
||||
placeholder="student@region15.org"
|
||||
disabled=elements_disabled
|
||||
value=reactive_info.contact_email
|
||||
data_member = "contact_email"
|
||||
data_map = expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
@@ -177,7 +201,8 @@ pub fn HomePage() -> impl IntoView {
|
||||
label="Phone Number:"
|
||||
placeholder="123-456-7890"
|
||||
disabled=elements_disabled
|
||||
value=reactive_info.phone_number
|
||||
data_member = "phone_number"
|
||||
data_map = expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
@@ -185,38 +210,67 @@ pub fn HomePage() -> impl IntoView {
|
||||
label="Street Address:"
|
||||
placeholder="123 Fake Street"
|
||||
disabled=elements_disabled
|
||||
value=reactive_info.address
|
||||
data_member = "address"
|
||||
data_map = expandable_react.data
|
||||
/>
|
||||
</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"
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
// Radio { data_member, label, items }
|
||||
<RadioList
|
||||
label="Town:"
|
||||
selected=reactive_info.town
|
||||
items=vec!["Southbury", "Middlebury"]
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="town"
|
||||
data_map = expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
// Checkbox { data_member, label, items }
|
||||
<CheckboxList
|
||||
label="Favorite Candies:"
|
||||
items=vec!["Twizzlers", "Reese's", "Starburst"]
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="favorite_candies"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
// Select { data_member, label, items }
|
||||
<Select
|
||||
label="Gender:"
|
||||
value_list=vec!["Male", "Female", "Prefer not to answer"]
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.map(|s| s.to_owned())
|
||||
.collect()
|
||||
value=reactive_info.gender
|
||||
disabled=elements_disabled
|
||||
data_member="gender"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ActionButton
|
||||
on:click=move |_| {
|
||||
submit_action
|
||||
.dispatch(CreateSampleSubmission {
|
||||
student_info: reactive_info.capture(),
|
||||
});
|
||||
let captured_map = expandable_react.capture();
|
||||
console_log(format!("Map values: {:?}", captured_map).as_str());
|
||||
submit_action.dispatch(CreateSampleSubmission {
|
||||
student_info: captured_map,
|
||||
});
|
||||
}
|
||||
disabled=elements_disabled
|
||||
>"Submit"</ActionButton>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use std::collections::HashMap;
|
||||
use crate::app::Unauthenticated;
|
||||
use crate::common::UserClaims;
|
||||
use crate::components::{Loading, MultiEntry, MultiEntryData, MultiEntryMember, OutlinedTextField};
|
||||
use crate::components::{Loading, MultiEntry, MultiEntryData, MultiEntryMember, OutlinedTextField, ValueType};
|
||||
use leptos::leptos_dom::logging::console_log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated, LogoutLink};
|
||||
@@ -24,11 +25,14 @@ pub fn ProviderEntry() -> impl IntoView {
|
||||
<h1>This is the provider entry page</h1>
|
||||
<h2>It would usually have a series of questions that we ask the providers</h2>
|
||||
<div>
|
||||
// Much of this is just a placeholder.
|
||||
<OutlinedTextField
|
||||
placeholder = "Testing information..."
|
||||
name = "testing_input"
|
||||
label = "This is a testing question."
|
||||
value = RwSignal::new("".to_owned())
|
||||
data_member = "testing"
|
||||
data_map = RwSignal::new(HashMap::new())
|
||||
value = RwSignal::new(ValueType::String(None))
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
@@ -37,6 +41,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>
|
||||
|
||||
Reference in new issue
Block a user