Merge branch 'main' into feature/chips-component
This commit is contained in:
21 files changed
+440
-466
No files matched your search
Generated
+1
-65
@@ -5,8 +5,7 @@
|
|||||||
"packages": {
|
"packages": {
|
||||||
"": {
|
"": {
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@tailwindcss/cli": "^4.1.5",
|
"@tailwindcss/cli": "^4.1.5"
|
||||||
"sass": "^1.92.0"
|
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"tailwindcss": "^4.1.13"
|
"tailwindcss": "^4.1.13"
|
||||||
@@ -589,21 +588,6 @@
|
|||||||
"node": ">=8"
|
"node": ">=8"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/chokidar": {
|
|
||||||
"version": "4.0.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/chokidar/-/chokidar-4.0.3.tgz",
|
|
||||||
"integrity": "sha512-Qgzu8kfBvo+cA4962jnP1KkS6Dop5NS6g7R5LFYJr4b8Ub94PPQXUksCw9PvXoeXPRRddRNC5C1JQUR2SMGtnA==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"readdirp": "^4.0.1"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.16.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/detect-libc": {
|
"node_modules/detect-libc": {
|
||||||
"version": "1.0.3",
|
"version": "1.0.3",
|
||||||
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
"resolved": "https://registry.npmjs.org/detect-libc/-/detect-libc-1.0.3.tgz",
|
||||||
@@ -647,12 +631,6 @@
|
|||||||
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
"integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==",
|
||||||
"license": "ISC"
|
"license": "ISC"
|
||||||
},
|
},
|
||||||
"node_modules/immutable": {
|
|
||||||
"version": "5.1.3",
|
|
||||||
"resolved": "https://registry.npmjs.org/immutable/-/immutable-5.1.3.tgz",
|
|
||||||
"integrity": "sha512-+chQdDfvscSF1SJqv2gn4SRO2ZyS3xL3r7IW/wWEEzrzLisnOlKiQu5ytC/BVNcS15C39WT2Hg/bjKjDMcu+zg==",
|
|
||||||
"license": "MIT"
|
|
||||||
},
|
|
||||||
"node_modules/is-extglob": {
|
"node_modules/is-extglob": {
|
||||||
"version": "2.1.1",
|
"version": "2.1.1",
|
||||||
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
"resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz",
|
||||||
@@ -975,48 +953,6 @@
|
|||||||
"url": "https://github.com/sponsors/jonschlinkert"
|
"url": "https://github.com/sponsors/jonschlinkert"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"node_modules/readdirp": {
|
|
||||||
"version": "4.1.2",
|
|
||||||
"resolved": "https://registry.npmjs.org/readdirp/-/readdirp-4.1.2.tgz",
|
|
||||||
"integrity": "sha512-GDhwkLfywWL2s6vEjyhri+eXmfH6j1L7JE27WhqLeYzoh/A3DBaYGEj2H/HFZCn/kMfim73FXxEJTw06WtxQwg==",
|
|
||||||
"license": "MIT",
|
|
||||||
"engines": {
|
|
||||||
"node": ">= 14.18.0"
|
|
||||||
},
|
|
||||||
"funding": {
|
|
||||||
"type": "individual",
|
|
||||||
"url": "https://paulmillr.com/funding/"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/sass": {
|
|
||||||
"version": "1.92.0",
|
|
||||||
"resolved": "https://registry.npmjs.org/sass/-/sass-1.92.0.tgz",
|
|
||||||
"integrity": "sha512-KDNI0BxgIRDAfJgzNm5wuy+4yOCIZyrUbjSpiU/JItfih+KGXAVefKL53MTml054MmBA3DDKIBMSI/7XLxZJ3A==",
|
|
||||||
"license": "MIT",
|
|
||||||
"dependencies": {
|
|
||||||
"chokidar": "^4.0.0",
|
|
||||||
"immutable": "^5.0.2",
|
|
||||||
"source-map-js": ">=0.6.2 <2.0.0"
|
|
||||||
},
|
|
||||||
"bin": {
|
|
||||||
"sass": "sass.js"
|
|
||||||
},
|
|
||||||
"engines": {
|
|
||||||
"node": ">=14.0.0"
|
|
||||||
},
|
|
||||||
"optionalDependencies": {
|
|
||||||
"@parcel/watcher": "^2.4.1"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/source-map-js": {
|
|
||||||
"version": "1.2.1",
|
|
||||||
"resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz",
|
|
||||||
"integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==",
|
|
||||||
"license": "BSD-3-Clause",
|
|
||||||
"engines": {
|
|
||||||
"node": ">=0.10.0"
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"node_modules/tailwindcss": {
|
"node_modules/tailwindcss": {
|
||||||
"version": "4.1.13",
|
"version": "4.1.13",
|
||||||
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
"resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-4.1.13.tgz",
|
||||||
|
|||||||
+1
-2
@@ -7,7 +7,7 @@ use leptos_router::{
|
|||||||
};
|
};
|
||||||
use url::Url;
|
use url::Url;
|
||||||
|
|
||||||
use crate::pages::{AboutPage, HomePage, ProviderEntry, UnauthenticatedPage};
|
use crate::pages::{AboutPage, HomePage, UnauthenticatedPage};
|
||||||
|
|
||||||
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
pub fn shell(options: LeptosOptions) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
@@ -67,7 +67,6 @@ pub fn App() -> impl IntoView {
|
|||||||
<Routes fallback=|| "Page not found.".into_view()>
|
<Routes fallback=|| "Page not found.".into_view()>
|
||||||
<Route path=StaticSegment("") view=HomePage/>
|
<Route path=StaticSegment("") view=HomePage/>
|
||||||
<Route path=StaticSegment("about") view=AboutPage/>
|
<Route path=StaticSegment("about") view=AboutPage/>
|
||||||
<Route path=StaticSegment("providers") view=ProviderEntry/>
|
|
||||||
</Routes>
|
</Routes>
|
||||||
</main>
|
</main>
|
||||||
</Router>
|
</Router>
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
use std::collections::HashMap;
|
use crate::common::ValueType;
|
||||||
use serde::{Deserialize, Serialize};
|
|
||||||
use macros::Reactive;
|
use macros::Reactive;
|
||||||
use crate::components::ValueType;
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// This struct allows for completely open-ended data storage.
|
/// This struct allows for completely open-ended data storage.
|
||||||
///
|
///
|
||||||
/// The `HashMap` allows the application to store an arbitrary number of values which are accessible
|
/// 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
|
/// 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 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
|
/// stored value. It *must* have a main key member name so that a database will know how to store
|
||||||
/// the data.
|
/// the data.
|
||||||
///
|
///
|
||||||
/// In the future, this should also be optimized to store properly within a DynamoDB table. As of
|
/// 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
|
/// 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,
|
/// information in the `HashMap`. We'd prefer to be able to get single values if possible,
|
||||||
|
|||||||
+117
-14
@@ -1,18 +1,121 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use crate::common::ValueType;
|
||||||
|
use crate::components::{CheckboxList, OutlinedTextField, RadioList, Row, Select};
|
||||||
|
use leptos::either::EitherOf5;
|
||||||
|
use leptos::prelude::RwSignal;
|
||||||
|
use leptos::{IntoView, view};
|
||||||
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// Defines a series of options that allow a user to define a form.
|
/// Defines a series of options that allow a user to define a form.
|
||||||
#[derive(Serialize, Deserialize, Clone, Debug)]
|
#[derive(Serialize, Deserialize, Clone, Debug)]
|
||||||
pub enum InputType{
|
pub enum InputType {
|
||||||
/// Represents a text input. Requires a `String` key. The entered value will be returned as a
|
/// Represents a text input. Requires a `String` key, `String` label, and `String` placeholder.
|
||||||
/// single `String` value.
|
/// The entered value will be returned as a single `String` value.
|
||||||
Text,
|
Text(String, String, String),
|
||||||
/// Represents a number input. Requires a `String` key. The entered value will be returned as a
|
/// Represents a number input. Requires a `String` key, `String` label, and a `String` placeholder.
|
||||||
/// single `i32` value.
|
/// The entered value will be returned as a `String`, and will be parseable into any number type
|
||||||
Number,
|
/// (i.e. an `i32`, `i64`, etc)
|
||||||
/// Represents a radio input. Requires a `String` key and a series of options. The selected
|
Number(String, String, String),
|
||||||
/// value will be returned as a single `String` value.
|
/// Represents a radio input. Requires a `String` key, `String` label, and a series of `String`
|
||||||
Radio(Vec<String>),
|
/// options. The selected value will be returned as a `String`.
|
||||||
/// Represents a checkbox input. Requires a `String` key and a series of options. The selected
|
Radio(String, String, Vec<String>),
|
||||||
/// value will be returned as a `Vec<String>`.
|
/// Represents a checkbox input. Requires a `String` key, `String` label, and a series of `String`
|
||||||
Checkbox(Vec<String>)
|
/// options. The selected values will be returned as a `Vec<String>`.
|
||||||
|
Checkbox(String, String, Vec<String>),
|
||||||
|
/// Represents a dropdown input. Requires a `String` key, `String` label, and a series
|
||||||
|
/// of `String` options. The selected value will be returned as a `String`.
|
||||||
|
Select(String, String, Vec<String>),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl InputType {
|
||||||
|
pub fn into_view(self, data_map: RwSignal<HashMap<String, ValueType>>) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
{match self {
|
||||||
|
InputType::Text(member, label, placeholder) =>
|
||||||
|
EitherOf5::A(view! {
|
||||||
|
<Row>
|
||||||
|
<OutlinedTextField
|
||||||
|
label = label.clone()
|
||||||
|
placeholder = placeholder.clone()
|
||||||
|
data_member = member.clone()
|
||||||
|
data_map = data_map
|
||||||
|
/>
|
||||||
|
</Row>
|
||||||
|
}),
|
||||||
|
InputType::Number(member, label, placeholder) =>
|
||||||
|
EitherOf5::B(view! {
|
||||||
|
<OutlinedTextField
|
||||||
|
label = label.clone()
|
||||||
|
placeholder = placeholder.clone()
|
||||||
|
data_member = member.clone()
|
||||||
|
data_map = data_map
|
||||||
|
/>
|
||||||
|
}),
|
||||||
|
InputType::Checkbox(member, label, options) =>
|
||||||
|
EitherOf5::C(view! {
|
||||||
|
<CheckboxList
|
||||||
|
label = label.clone()
|
||||||
|
items = options.clone()
|
||||||
|
data_member = member.clone()
|
||||||
|
data_map = data_map
|
||||||
|
/>
|
||||||
|
}),
|
||||||
|
InputType::Radio(member, label, options) =>
|
||||||
|
EitherOf5::D(view! {
|
||||||
|
<RadioList
|
||||||
|
label = label.clone()
|
||||||
|
data_member = member.clone()
|
||||||
|
data_map = data_map
|
||||||
|
items = options.clone()
|
||||||
|
/>
|
||||||
|
}),
|
||||||
|
InputType::Select(member, label, options) =>
|
||||||
|
EitherOf5::E(view! {
|
||||||
|
<Select
|
||||||
|
label = label.clone()
|
||||||
|
value_list = options.clone()
|
||||||
|
data_member = member.clone()
|
||||||
|
data_map = data_map
|
||||||
|
/>
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn into_view_rows(self, data_map: RwSignal<HashMap<String, ValueType>>) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<Row>
|
||||||
|
{self.into_view(data_map)}
|
||||||
|
</Row>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Shorthand macro for creating `InputType` enums.
|
||||||
|
///
|
||||||
|
/// Example usage:
|
||||||
|
/// ```
|
||||||
|
/// // To create an InputType::Text
|
||||||
|
/// let text_input = input!(Text, "member", "label", "placeholder");
|
||||||
|
///
|
||||||
|
/// // To create an InputType::Select
|
||||||
|
/// let select_input = input!(Select, "member", "label", ["option1", "option2"]);
|
||||||
|
/// ```
|
||||||
|
#[macro_export]
|
||||||
|
macro_rules! input {
|
||||||
|
($variant:ident, $key:expr, $label:expr, [$($option:expr),* $(,)?]) => {
|
||||||
|
$crate::common::InputType::$variant(
|
||||||
|
$key.to_string(),
|
||||||
|
$label.to_string(),
|
||||||
|
vec![$($option.to_string()), *]
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
($variant:ident, $key:expr, $label:expr, $placeholder:expr) => {
|
||||||
|
$crate::common::InputType::$variant(
|
||||||
|
$key.to_string(),
|
||||||
|
$label.to_string(),
|
||||||
|
$placeholder.to_string()
|
||||||
|
)
|
||||||
|
};
|
||||||
}
|
}
|
||||||
+3
-4
@@ -1,7 +1,6 @@
|
|||||||
mod student_info;
|
|
||||||
mod user;
|
|
||||||
mod form_schema;
|
|
||||||
mod expandable_info;
|
mod expandable_info;
|
||||||
|
mod form_schema;
|
||||||
|
mod user;
|
||||||
pub mod value_type;
|
pub mod value_type;
|
||||||
|
|
||||||
pub use self::{expandable_info::*, form_schema::*, student_info::*, user::*};
|
pub use self::{expandable_info::*, form_schema::*, user::*, value_type::*};
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
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, 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.
|
|
||||||
///
|
|
||||||
/// This is the primary key of the table, and should not have a default value.
|
|
||||||
pub Email: String,
|
|
||||||
/// The student's first name.
|
|
||||||
#[serde(default)]
|
|
||||||
pub first_name: ValueType, // String
|
|
||||||
/// The student's last name.
|
|
||||||
#[serde(default)]
|
|
||||||
pub last_name: ValueType, // String
|
|
||||||
/// The student's highest math score on the SAT.
|
|
||||||
#[serde(default = "default_number")]
|
|
||||||
pub math_sat: ValueType, // Number
|
|
||||||
/// The student's desired contact email.
|
|
||||||
#[serde(default)]
|
|
||||||
pub contact_email: ValueType, // String
|
|
||||||
/// The student's address.
|
|
||||||
#[serde(default)]
|
|
||||||
pub address: ValueType, // String
|
|
||||||
/// The student's town of residence.
|
|
||||||
#[serde(default)]
|
|
||||||
pub town: ValueType, // String
|
|
||||||
/// The student's entered phone number.
|
|
||||||
#[serde(default)]
|
|
||||||
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: ValueType, // String
|
|
||||||
/// The student's athletic information.
|
|
||||||
#[serde(default)]
|
|
||||||
pub athletic_info: Vec<MultiEntryData>,
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_number() -> ValueType {
|
|
||||||
ValueType::Number(None)
|
|
||||||
}
|
|
||||||
@@ -1,9 +1,14 @@
|
|||||||
use serde::{Serialize, Deserialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
use std::collections::HashMap;
|
||||||
use std::fmt::Display;
|
use std::fmt::Display;
|
||||||
|
|
||||||
/// Defines a set of possible input types. They allow for the type and value of every data member
|
/// 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
|
/// 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.
|
/// created as a single type, the values are still able to be type safe.
|
||||||
|
///
|
||||||
|
/// A note: other structs cannot be stored in this enum, however it was designed to work with
|
||||||
|
/// a `HashMap` of more `ValueType` enums. There are numerous ways to convert a struct into a
|
||||||
|
/// `HashMap`, specifically storing all the struct's fields as `String` keys in the map.
|
||||||
#[derive(PartialEq, Clone, Serialize, Deserialize, Debug)]
|
#[derive(PartialEq, Clone, Serialize, Deserialize, Debug)]
|
||||||
pub enum ValueType {
|
pub enum ValueType {
|
||||||
/// Indicates a `String` value.
|
/// Indicates a `String` value.
|
||||||
@@ -12,12 +17,18 @@ pub enum ValueType {
|
|||||||
Number(Option<String>),
|
Number(Option<String>),
|
||||||
/// Indicates a `Vec<T>`, where `T` can be any other `ValueType`.
|
/// Indicates a `Vec<T>`, where `T` can be any other `ValueType`.
|
||||||
List(Option<Vec<ValueType>>),
|
List(Option<Vec<ValueType>>),
|
||||||
|
/// Indicates a `HashMap<String, ValueType>`.
|
||||||
|
Map(Option<HashMap<String, ValueType>>),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl ValueType {
|
impl ValueType {
|
||||||
/// Returns true if this is a `String`.
|
/// Returns true if this is a `String`, otherwise false.
|
||||||
pub fn is_string(&self) -> bool {
|
pub fn is_string(&self) -> bool {
|
||||||
if let ValueType::String(_) = self { true } else { false }
|
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.
|
/// Attempts to get this value as a `String` type. This is different from the `to_string` method.
|
||||||
@@ -29,9 +40,13 @@ impl ValueType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if this is a `Number`.
|
/// Returns true if this is a `Number`, otherwise false.
|
||||||
pub fn is_number(&self) -> bool {
|
pub fn is_number(&self) -> bool {
|
||||||
if let ValueType::Number(_) = self { true } else { false }
|
if let ValueType::Number(_) = self {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Attempts to get this value as an `i32`.
|
/// Attempts to get this value as an `i32`.
|
||||||
@@ -43,9 +58,13 @@ impl ValueType {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Returns true if this is a `List`.
|
/// Returns true if this is a `List`, otherwise false.
|
||||||
pub fn is_list(&self) -> bool {
|
pub fn is_list(&self) -> bool {
|
||||||
if let ValueType::List(_) = self { true } else { false }
|
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
|
/// Attempts to get this value as a `Vec<ValueType>`. Each of the values inside the list must
|
||||||
@@ -57,6 +76,24 @@ impl ValueType {
|
|||||||
Err(self.clone())
|
Err(self.clone())
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Returns true if this is a `Map`, otherwise false.
|
||||||
|
pub fn is_map(&self) -> bool {
|
||||||
|
if let ValueType::Map(_) = self {
|
||||||
|
true
|
||||||
|
} else {
|
||||||
|
false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Attempts to get this value as a `HashMap<String, ValueType>`.
|
||||||
|
pub fn as_map(&self) -> Result<Option<HashMap<String, ValueType>>, Self> {
|
||||||
|
if let ValueType::Map(v) = self {
|
||||||
|
Ok(v.clone())
|
||||||
|
} else {
|
||||||
|
Err(self.clone())
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for ValueType {
|
impl Default for ValueType {
|
||||||
@@ -71,6 +108,7 @@ impl Display for ValueType {
|
|||||||
ValueType::String(s) => s.clone().unwrap_or("".to_owned()),
|
ValueType::String(s) => s.clone().unwrap_or("".to_owned()),
|
||||||
ValueType::Number(n) => n.clone().unwrap_or("".to_owned()),
|
ValueType::Number(n) => n.clone().unwrap_or("".to_owned()),
|
||||||
ValueType::List(l) => format!("{l:?}"),
|
ValueType::List(l) => format!("{l:?}"),
|
||||||
|
ValueType::Map(m) => format!("{m:?}"),
|
||||||
};
|
};
|
||||||
write!(f, "{}", str)
|
write!(f, "{}", str)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use crate::common::ValueType;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use crate::components::ValueType;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Checkbox(
|
pub fn Checkbox(
|
||||||
@@ -70,7 +70,7 @@ pub fn CheckboxList(
|
|||||||
.contains(&ValueType::String(Some(item_name.clone()))) // bool
|
.contains(&ValueType::String(Some(item_name.clone()))) // bool
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
let on_change = {
|
let on_change = {
|
||||||
let item_name = item.clone();
|
let item_name = item.clone();
|
||||||
let data_member = data_member.clone();
|
let data_member = data_member.clone();
|
||||||
@@ -102,7 +102,7 @@ pub fn CheckboxList(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Checkbox
|
<Checkbox
|
||||||
checked=checked_signal
|
checked=checked_signal
|
||||||
|
|||||||
@@ -0,0 +1,205 @@
|
|||||||
|
use crate::common::{InputType, ValueType};
|
||||||
|
use crate::components::ActionButton;
|
||||||
|
use leptos::leptos_dom::logging::console_log;
|
||||||
|
use leptos::prelude::*;
|
||||||
|
use std::collections::HashMap;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
// region Helper Functions
|
||||||
|
|
||||||
|
/// Gets the UUID from a given `HashMap<String, ValueType>`, wrapped in an `Option`.
|
||||||
|
fn get_uuid_from_map(map: &HashMap<String, ValueType>) -> Option<String> {
|
||||||
|
map.get("uuid")?.as_string().unwrap_or(None)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Updates an Entry within the given `Vec`.
|
||||||
|
fn update_entry_list(list: &mut Vec<ValueType>, data: &HashMap<String, ValueType>) {
|
||||||
|
if let Some(uuid) = get_uuid_from_map(&data) {
|
||||||
|
if let Some(index) = list.iter().position(|x| {
|
||||||
|
get_uuid_from_map(&x.as_map().unwrap().unwrap_or_default()) == Some(uuid.clone())
|
||||||
|
}) {
|
||||||
|
list[index] = ValueType::Map(Some(data.clone()));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Creates a new entry by creating a `ValueType::Map` with a unique ID at the key `uuid`.
|
||||||
|
/// This key can be found by using the `HashMap::get` function.
|
||||||
|
fn new_entry() -> ValueType {
|
||||||
|
let mut entry_map = HashMap::new();
|
||||||
|
entry_map.insert(
|
||||||
|
"uuid".into(),
|
||||||
|
ValueType::String(Some(Uuid::new_v4().to_string())),
|
||||||
|
);
|
||||||
|
ValueType::Map(Some(entry_map))
|
||||||
|
}
|
||||||
|
|
||||||
|
// endregion
|
||||||
|
|
||||||
|
/// # Multi-Entry Component
|
||||||
|
///
|
||||||
|
/// A rewrite of the `MultiEntry` component. Uses a single source of truth in the form of the `data_map`
|
||||||
|
/// prop. Creates and displays a series of `Entry` components that use their own `HashMap` signals.
|
||||||
|
/// Each of these `Entry` components runs a callback that alters the original `data_map`.
|
||||||
|
///
|
||||||
|
/// The `schema` prop determines what inputs are available to all `Entry` components created from
|
||||||
|
/// this component. For example, using a `schema` that contains an `InputType::Text` and an
|
||||||
|
/// `InputType::Select` means that all `Entry` components will render a text and dropdown input
|
||||||
|
/// component. The users shall be able to use these components to alter the data.
|
||||||
|
///
|
||||||
|
/// Example usage:
|
||||||
|
/// ```
|
||||||
|
/// let data_signal = RwSignal::new(HashMap::new());
|
||||||
|
/// view! {
|
||||||
|
/// <MultiEntryRewrite
|
||||||
|
/// data_member = "example"
|
||||||
|
/// data_map = data_signal
|
||||||
|
/// schema = vec![
|
||||||
|
/// InputType::Text("first_name", "First Name:", "John"),
|
||||||
|
/// InputType::Text("last_name", "Last Name:", "Smith")
|
||||||
|
/// ]
|
||||||
|
/// />
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// In this example, any time a new `Entry` is created, it will contain two text fields that edit the
|
||||||
|
/// `first_name` and `last_name` keys within that `Entry`.
|
||||||
|
#[component]
|
||||||
|
pub fn MultiEntry(
|
||||||
|
/// The data member that contains the list of entries within the `data_map`.
|
||||||
|
#[prop(into)]
|
||||||
|
data_member: String,
|
||||||
|
/// The data map that contains all the information. Data is found within this map using the
|
||||||
|
/// component's `data_member` input.
|
||||||
|
#[prop()]
|
||||||
|
data_map: RwSignal<HashMap<String, ValueType>>,
|
||||||
|
#[prop(into)] name_member: String,
|
||||||
|
/// The type and order of the inputs for each `Entry` component within this `MultiEntry` component.
|
||||||
|
#[prop(optional)]
|
||||||
|
schema: Vec<InputType>,
|
||||||
|
) -> impl IntoView {
|
||||||
|
// Make the values within the list reactive. Each of these values should be a ValueType::Map,
|
||||||
|
// so we can pass these directly to the application.
|
||||||
|
let data_reactive = RwSignal::new(
|
||||||
|
data_map
|
||||||
|
.get_untracked()
|
||||||
|
.get(&data_member)
|
||||||
|
.unwrap_or(&ValueType::List(None))
|
||||||
|
.as_list()
|
||||||
|
.unwrap_or(Some(Vec::<ValueType>::new()))
|
||||||
|
.unwrap_or(Vec::new()),
|
||||||
|
);
|
||||||
|
|
||||||
|
// This effect updates the data_map with any new values from the data_reactive list, every time
|
||||||
|
// the data_reactive list is changed. This allows changes from within the Entry component to
|
||||||
|
// bubble up to the data_map effectively.
|
||||||
|
Effect::new(move |_| {
|
||||||
|
let entry_list = data_reactive.get();
|
||||||
|
data_map.update(|map| {
|
||||||
|
map.insert(data_member.clone(), ValueType::List(Some(entry_list)));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
let add_entry = move |_| data_reactive.update(|list| list.push(new_entry()));
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-col flex-1">
|
||||||
|
<div class="flex flex-col gap-2 p-2">
|
||||||
|
// Render the entries
|
||||||
|
<For
|
||||||
|
each=move || data_reactive.get()
|
||||||
|
key=|entry| get_uuid_from_map(&entry.as_map().unwrap().unwrap()) // Entries MUST be given a unique ID. This is absolutely ridiculous.
|
||||||
|
children=move |mut entry_map| {
|
||||||
|
// The map_signal is the map used for the internal element. This signal will
|
||||||
|
// keep track of the individual entry's data, and whenever it is changed,
|
||||||
|
// the parent data_map will also be updated to reflect these changes.
|
||||||
|
let map_signal = RwSignal::new(entry_map
|
||||||
|
.as_map()
|
||||||
|
.unwrap_or_default()
|
||||||
|
.unwrap_or_default());
|
||||||
|
|
||||||
|
// This effect updates the data_reactive list with the map_signal values, every
|
||||||
|
// time the map_signal is changed.
|
||||||
|
Effect::new(move |_| {
|
||||||
|
let child_map = map_signal.get(); // HashMap
|
||||||
|
data_reactive.update(|mut list| {
|
||||||
|
update_entry_list(&mut list, &child_map);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<Entry
|
||||||
|
data_map = map_signal
|
||||||
|
name_member = name_member.clone()
|
||||||
|
schema = schema.clone()
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<ActionButton on:click = add_entry>"Add entry"</ActionButton>
|
||||||
|
<ActionButton on:click=move |_| {
|
||||||
|
console_log(format!("Entry list: {:?}", data_reactive.get()).as_str());
|
||||||
|
}
|
||||||
|
>"Print Info"</ActionButton>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/// # Entry Component
|
||||||
|
///
|
||||||
|
/// A single entry component. Should only ever be created from within a `MultiEntry` component.
|
||||||
|
/// Stores information within the `data_map` prop by using a series of data members that are found
|
||||||
|
/// within the `schema` list prop.
|
||||||
|
///
|
||||||
|
/// Example usage:
|
||||||
|
/// ```
|
||||||
|
/// // From within a MultiEntry component only
|
||||||
|
/// let data = RwSignal::new(HashMap::new());
|
||||||
|
/// view! {
|
||||||
|
/// <Entry
|
||||||
|
/// data_map = data
|
||||||
|
/// name_member = "first_name"
|
||||||
|
/// schema = vec![
|
||||||
|
/// InputType::Text("first_name", "First Name:", "John"),
|
||||||
|
/// InputType::Text("last_name", "Last Name:", "Smith")
|
||||||
|
/// ]
|
||||||
|
/// />
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
|
///
|
||||||
|
/// In this example, the `Entry` component that is rendered will have two text inputs. Each of these
|
||||||
|
/// inputs will alter the `first_name` and `last_name` keys in the `data_map` respectively, and the
|
||||||
|
/// `Entry` component will run a callback to its parent `MultiEntry` component to update the `data_map`
|
||||||
|
/// accordingly.
|
||||||
|
#[component]
|
||||||
|
fn Entry(
|
||||||
|
/// The data map that contains all information. Data is found within this map using the `member`
|
||||||
|
/// props. This map should be unique to this entry; in other words, it should be contained and
|
||||||
|
/// passed down from within the entry list in the parent `MultiEntry` component.
|
||||||
|
#[prop()]
|
||||||
|
data_map: RwSignal<HashMap<String, ValueType>>,
|
||||||
|
/// The schema of this entry. Should contain all information about what inputs to use and what
|
||||||
|
/// data members those inputs modify.
|
||||||
|
#[prop()]
|
||||||
|
schema: Vec<InputType>,
|
||||||
|
/// The display name member of this entry. Determines where the component should search for the
|
||||||
|
/// name of the entry within the `data_map`.
|
||||||
|
#[prop(into)]
|
||||||
|
name_member: String,
|
||||||
|
/// The info member of this entry. Determines where the component should search for the extra
|
||||||
|
/// information of this entry within the `data_map`.
|
||||||
|
#[prop(optional, into)]
|
||||||
|
info_member: String,
|
||||||
|
) -> impl IntoView {
|
||||||
|
view! {
|
||||||
|
<div class="flex flex-col flex-1 p-2 rounded-sm transition-shadow shadow-sm hover:shadow-lg/30">
|
||||||
|
{schema.iter()
|
||||||
|
.map(|input_type| {
|
||||||
|
input_type.clone().into_view(data_map.clone())
|
||||||
|
}).collect_view()
|
||||||
|
}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,10 +0,0 @@
|
|||||||
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,144 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use std::sync::Arc;
|
|
||||||
use leptos::either::either;
|
|
||||||
use crate::components::{ActionButton, OutlinedTextField};
|
|
||||||
use leptos::prelude::*;
|
|
||||||
use crate::common::InputType;
|
|
||||||
use crate::components::ValueType;
|
|
||||||
use super::{multi_entry_data::*, multi_entry_member::*};
|
|
||||||
|
|
||||||
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! {}
|
|
||||||
);
|
|
||||||
|
|
||||||
temp.into_view()
|
|
||||||
}
|
|
||||||
|
|
||||||
/// Represents a single entry in a MultiEntry component.
|
|
||||||
/// These are displayed in a list, and function a little bit like a dropdown.
|
|
||||||
/// When clicked, they expand to show all the inputs, which can be edited.
|
|
||||||
#[component]
|
|
||||||
pub fn Entry(
|
|
||||||
/// The data for this entry.
|
|
||||||
#[prop()]
|
|
||||||
entry_data: MultiEntryData,
|
|
||||||
/// The main display member for this entry.
|
|
||||||
#[prop()]
|
|
||||||
name_member: Signal<MultiEntryMember>,
|
|
||||||
/// 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.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.
|
|
||||||
#[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(Some(String::from("Testing"))),
|
|
||||||
);
|
|
||||||
new_entry.data.insert(
|
|
||||||
info_member.get().member_name,
|
|
||||||
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
|
|
||||||
<div class="rounded-md bg-red-700">
|
|
||||||
// This div MUST have the same spacing CSS as the main entry div
|
|
||||||
<div class="flex bg-inherit p-3 m-1">
|
|
||||||
<span class="flex-1 text-white font-bold">
|
|
||||||
{name_member.get().display_name}
|
|
||||||
</span>
|
|
||||||
<span class="flex-1 text-white font-bold">
|
|
||||||
{info_member.get().display_name}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
// Entry section
|
|
||||||
<div>
|
|
||||||
<For
|
|
||||||
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
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
<ActionButton on:click=create_entry>Add Entry</ActionButton>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
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(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,45 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,7 @@ use leptos::prelude::*;
|
|||||||
#[component]
|
#[component]
|
||||||
pub fn Panel(
|
pub fn Panel(
|
||||||
#[prop(optional, default = false)] hidden: bool,
|
#[prop(optional, default = false)] hidden: bool,
|
||||||
#[prop(optional)] children: Option<Children>
|
#[prop(optional)] children: Option<Children>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
<div
|
<div
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
use std::collections::HashMap;
|
use crate::common::ValueType;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use crate::components::ValueType;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Radio(
|
pub fn Radio(
|
||||||
|
|||||||
@@ -1,5 +1,20 @@
|
|||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
|
||||||
|
/// # Row Component
|
||||||
|
///
|
||||||
|
/// This component is designed for use within the `Panel` component, however it may be used standalone
|
||||||
|
/// when used within any `div` with the `flex` property set.
|
||||||
|
///
|
||||||
|
/// Example usage (with `Panel`):
|
||||||
|
/// ```
|
||||||
|
/// view! {
|
||||||
|
/// <Panel>
|
||||||
|
/// <Row>
|
||||||
|
/// // More content here...
|
||||||
|
/// </Row>
|
||||||
|
/// </Panel>
|
||||||
|
/// }
|
||||||
|
/// ```
|
||||||
#[component]
|
#[component]
|
||||||
pub fn Row(children: Children) -> impl IntoView {
|
pub fn Row(children: Children) -> impl IntoView {
|
||||||
view! {
|
view! {
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
use std::collections::HashMap;
|
use crate::common::ValueType;
|
||||||
use leptos::ev::{Event, Targeted};
|
use leptos::ev::{Event, Targeted};
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::web_sys::HtmlSelectElement;
|
use leptos::web_sys::HtmlSelectElement;
|
||||||
use crate::components::ValueType;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
/// A custom-styled select input.
|
/// A custom-styled select input.
|
||||||
///
|
///
|
||||||
@@ -30,7 +30,7 @@ pub fn Select(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
<label class="flex flex-col flex-1">
|
<label class="flex flex-col flex-1">
|
||||||
|
|||||||
@@ -1,13 +1,13 @@
|
|||||||
use std::collections::HashMap;
|
use crate::common::ValueType;
|
||||||
use leptos::leptos_dom::logging::console_log;
|
use leptos::leptos_dom::logging::console_log;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use crate::components::ValueType;
|
use std::collections::HashMap;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn OutlinedTextField(
|
pub fn OutlinedTextField(
|
||||||
#[prop(optional, into)] placeholder: String,
|
#[prop(optional, into)] placeholder: String,
|
||||||
// #[prop()] value: Subfield<TStore, TInner, ValueType>,
|
// #[prop()] value: Subfield<TStore, TInner, ValueType>,
|
||||||
#[prop(into)] data_member: String, // should this be an RwSignal??
|
#[prop(into)] data_member: String, // should this be an RwSignal??
|
||||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||||
#[prop(optional)] value: RwSignal<ValueType>,
|
#[prop(optional)] value: RwSignal<ValueType>,
|
||||||
#[prop(default = "text".to_owned(), into)] input_type: String,
|
#[prop(default = "text".to_owned(), into)] input_type: String,
|
||||||
@@ -15,7 +15,7 @@ pub fn OutlinedTextField(
|
|||||||
#[prop(optional)] error: RwSignal<bool>,
|
#[prop(optional)] error: RwSignal<bool>,
|
||||||
#[prop(optional, into)] name: String,
|
#[prop(optional, into)] name: String,
|
||||||
#[prop(optional, into)] label: String,
|
#[prop(optional, into)] label: String,
|
||||||
#[prop(optional, into)] error_text: RwSignal<String>
|
#[prop(optional, into)] error_text: RwSignal<String>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
// This function parses the input into the correct type. It only accepts numbers or strings as
|
// This function parses the input into the correct type. It only accepts numbers or strings as
|
||||||
// valid types, and parses them accordingly.
|
// valid types, and parses them accordingly.
|
||||||
@@ -26,42 +26,36 @@ pub fn OutlinedTextField(
|
|||||||
let to_parse = event_target_value(&e);
|
let to_parse = event_target_value(&e);
|
||||||
match input_type.as_str() {
|
match input_type.as_str() {
|
||||||
"text" => {
|
"text" => {
|
||||||
// String parse function. It will always parse, but make sure the error
|
// String parse function. It will always parse, but make sure the error
|
||||||
// state is set properly anyway.
|
// state is set properly anyway.
|
||||||
error.set(false);
|
error.set(false);
|
||||||
value.set(ValueType::String(Some(to_parse.clone())));
|
value.set(ValueType::String(Some(to_parse.clone())));
|
||||||
data_map.update(|map| {
|
data_map.update(|map| {
|
||||||
if let Some(val) = map.get_mut(&data_member) {
|
map.insert(data_member.clone(), ValueType::String(Some(to_parse)));
|
||||||
|
|
||||||
*val = ValueType::String(Some(to_parse));
|
|
||||||
} else {
|
|
||||||
map.insert(data_member.clone(), ValueType::String(Some(to_parse)));
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
"number" => {
|
"number" => {
|
||||||
// Define number parse function.
|
// Define number parse function.
|
||||||
// If the number parses, Some(num), else None, but we want to keep the value that's
|
// 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.
|
// in the text field at this point, just set the error.
|
||||||
console_log(format!("Parsing {to_parse} to a number").as_str());
|
console_log(format!("Parsing {to_parse} to a number").as_str());
|
||||||
match to_parse.parse::<i32>().map(|n| ValueType::Number(Some(n.to_string()))) {
|
match to_parse
|
||||||
|
.parse::<i32>()
|
||||||
|
.map(|n| ValueType::Number(Some(n.to_string())))
|
||||||
|
{
|
||||||
Ok(value_type) => {
|
Ok(value_type) => {
|
||||||
error.set(false);
|
error.set(false);
|
||||||
value.set(value_type.clone());
|
value.set(value_type.clone());
|
||||||
data_map.update(|map| {
|
data_map.update(|map| {
|
||||||
if let Some(val) = map.get_mut(&data_member) {
|
map.insert(data_member.clone(), value_type);
|
||||||
*val = value_type;
|
|
||||||
} else {
|
|
||||||
map.insert(data_member.clone(), value_type);
|
|
||||||
}
|
|
||||||
});
|
});
|
||||||
},
|
}
|
||||||
Err(_) => {
|
Err(_) => {
|
||||||
error.set(true);
|
error.set(true);
|
||||||
error_text.set("Value is not a valid number.".to_owned());
|
error_text.set("Value is not a valid number.".to_owned());
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
},
|
}
|
||||||
_ => {
|
_ => {
|
||||||
// Create some custom function that reports failure.
|
// Create some custom function that reports failure.
|
||||||
let msg = format!("Input {input_type:?} is not a valid type.");
|
let msg = format!("Input {input_type:?} is not a valid type.");
|
||||||
@@ -72,7 +66,7 @@ pub fn OutlinedTextField(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div class="flex flex-1">
|
<div class="flex flex-1">
|
||||||
<label class="flex flex-col flex-1">
|
<label class="flex flex-col flex-1">
|
||||||
|
|||||||
+22
-7
@@ -1,4 +1,3 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
// Server dependencies
|
// Server dependencies
|
||||||
#[cfg(feature = "ssr")]
|
#[cfg(feature = "ssr")]
|
||||||
use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValue};
|
use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValue};
|
||||||
@@ -7,16 +6,20 @@ use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValu
|
|||||||
use serde_dynamo::{from_item, to_item};
|
use serde_dynamo::{from_item, to_item};
|
||||||
|
|
||||||
use crate::common::{ExpandableInfo, UserClaims};
|
use crate::common::{ExpandableInfo, UserClaims};
|
||||||
use crate::components::{ActionButton, CheckboxList, Loading, OutlinedTextField, Panel, RadioList, Row, Select, ChipsList};
|
use crate::pages::UnauthenticatedPage;
|
||||||
use crate::pages::{UnauthenticatedPage};
|
use crate::components::{
|
||||||
|
ActionButton, CheckboxList, Loading, MultiEntry, OutlinedTextField, Panel, RadioList, Row,
|
||||||
|
Select, ChipsList
|
||||||
|
};
|
||||||
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 std::collections::HashMap;
|
||||||
use traits::{AsReactive, ReactiveCapture};
|
use traits::{AsReactive, ReactiveCapture};
|
||||||
|
|
||||||
/// # Get Student Info
|
/// # Get Student Info
|
||||||
/// Gets a student's information given their `subject`.
|
/// Gets a student's information given their `subject`.
|
||||||
///
|
///
|
||||||
/// All information is found by using a `GetItemCommand` in the student application DynamoDB table.
|
/// 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(subject: String) -> Result<ExpandableInfo, ServerFnError> {
|
pub async fn get_submission(subject: String) -> Result<ExpandableInfo, ServerFnError> {
|
||||||
@@ -24,7 +27,7 @@ pub async fn get_submission(subject: String) -> Result<ExpandableInfo, ServerFnE
|
|||||||
let dbclient = Client::new(&config);
|
let dbclient = Client::new(&config);
|
||||||
|
|
||||||
console_log(format!("Getting values from API using subject {}", subject).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
|
// Gets the item from the database. It only returns an error if the database
|
||||||
// experiences an error - not if the item is not found.
|
// experiences an error - not if the item is not found.
|
||||||
match dbclient
|
match dbclient
|
||||||
@@ -53,7 +56,7 @@ pub async fn get_submission(subject: String) -> Result<ExpandableInfo, ServerFnE
|
|||||||
|
|
||||||
/// # Create Sample Submission
|
/// # Create Sample Submission
|
||||||
/// Creates a submission given a full `StudentInfo` struct.
|
/// Creates a submission given a full `StudentInfo` struct.
|
||||||
///
|
///
|
||||||
/// All information is stored using a `PutItemCommand` in the student application DynamoDB table.
|
/// 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: ExpandableInfo) -> Result<(), ServerFnError> {
|
pub async fn create_sample_submission(student_info: ExpandableInfo) -> Result<(), ServerFnError> {
|
||||||
@@ -92,7 +95,7 @@ pub async fn log_expandable(info: ExpandableInfo) -> Result<(), ServerFnError> {
|
|||||||
// Manually specify the type here since we're not able to infer the type later.
|
// Manually specify the type here since we're not able to infer the type later.
|
||||||
let item: HashMap<String, AttributeValue> = to_item(info)?;
|
let item: HashMap<String, AttributeValue> = to_item(info)?;
|
||||||
console_log(format!("Converted into a DynamoDB item: {:?}", item).as_str());
|
console_log(format!("Converted into a DynamoDB item: {:?}", item).as_str());
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -291,6 +294,18 @@ pub fn HomePage() -> impl IntoView {
|
|||||||
.collect()
|
.collect()
|
||||||
disabled=elements_disabled
|
disabled=elements_disabled
|
||||||
label="Required Community Involvement"
|
label="Required Community Involvement"
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<MultiEntry
|
||||||
|
data_map = expandable_react.data
|
||||||
|
data_member = "test"
|
||||||
|
name_member = "first_name"
|
||||||
|
schema = vec![
|
||||||
|
input!(Text, "first_name", "First Name:", "John"),
|
||||||
|
input!(Text, "last_name", "Last Name:", "Smith"),
|
||||||
|
input!(Select, "gender", "Gender:", ["Male", "Female"]),
|
||||||
|
input!(Checkbox, "candy", "Favorite Candy:", ["Twizzlers", "Starburst"])
|
||||||
|
]
|
||||||
/>
|
/>
|
||||||
</Row>
|
</Row>
|
||||||
<Row>
|
<Row>
|
||||||
|
|||||||
+1
-2
@@ -1,6 +1,5 @@
|
|||||||
mod about_page;
|
mod about_page;
|
||||||
mod home_page;
|
mod home_page;
|
||||||
mod provider_entry;
|
|
||||||
mod unauthenticated_page;
|
mod unauthenticated_page;
|
||||||
|
|
||||||
pub use self::{about_page::*, home_page::*, provider_entry::*, unauthenticated_page::*};
|
pub use self::{about_page::*, home_page::*, unauthenticated_page::*};
|
||||||
@@ -1,50 +0,0 @@
|
|||||||
use std::collections::HashMap;
|
|
||||||
use crate::common::UserClaims;
|
|
||||||
use crate::components::{Loading, MultiEntry, MultiEntryData, MultiEntryMember, OutlinedTextField, ValueType};
|
|
||||||
use crate::pages::{UnauthenticatedPage};
|
|
||||||
use leptos::leptos_dom::logging::console_log;
|
|
||||||
use leptos::prelude::*;
|
|
||||||
use leptos_oidc::{Algorithm, AuthLoaded, AuthSignal, Authenticated, LogoutLink};
|
|
||||||
|
|
||||||
#[component]
|
|
||||||
pub fn ProviderEntry() -> impl IntoView {
|
|
||||||
let auth = use_context::<AuthSignal>().expect("Couldn't find auth signal");
|
|
||||||
let auth_data = auth
|
|
||||||
.get()
|
|
||||||
.authenticated()
|
|
||||||
.and_then(|data| data.decoded_access_token::<UserClaims>(Algorithm::RS256, &["account"]));
|
|
||||||
|
|
||||||
let multi_entries: RwSignal<Vec<MultiEntryData>> = RwSignal::new(vec![]);
|
|
||||||
|
|
||||||
console_log(format!("User claims: {auth_data:?}").as_str());
|
|
||||||
|
|
||||||
view! (
|
|
||||||
<AuthLoaded fallback=Loading>
|
|
||||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
|
||||||
<LogoutLink class="text-logout">"Sign out"</LogoutLink>
|
|
||||||
<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."
|
|
||||||
data_member = "testing"
|
|
||||||
data_map = RwSignal::new(HashMap::new())
|
|
||||||
value = RwSignal::new(ValueType::String(None))
|
|
||||||
/>
|
|
||||||
</div>
|
|
||||||
<div>
|
|
||||||
<p>This is an example of the multientry element.</p>
|
|
||||||
<MultiEntry
|
|
||||||
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>
|
|
||||||
</AuthLoaded>
|
|
||||||
)
|
|
||||||
}
|
|
||||||
Reference in new issue
Block a user