Merge branch 'main' into feature/chips-component

This commit is contained in:
Lucas Vas authored and GitHub committed 2025-10-23 11:56:38 -04:00
commit 2d2bc0f8cc
21 files changed
+440 -466

No files matched your search

+4 -4
View File
@@ -1,6 +1,6 @@
use std::collections::HashMap;
use crate::common::ValueType;
use leptos::prelude::*;
use crate::components::ValueType;
use std::collections::HashMap;
#[component]
pub fn Checkbox(
@@ -70,7 +70,7 @@ pub fn CheckboxList(
.contains(&ValueType::String(Some(item_name.clone()))) // bool
}
});
let on_change = {
let item_name = item.clone();
let data_member = data_member.clone();
@@ -102,7 +102,7 @@ pub fn CheckboxList(
});
}
};
view! {
<Checkbox
checked=checked_signal
+205
View File
@@ -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>
}
}
-10
View File
@@ -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::*,
};
-144
View File
@@ -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
}
}
}
+1 -1
View File
@@ -25,7 +25,7 @@ use leptos::prelude::*;
#[component]
pub fn Panel(
#[prop(optional, default = false)] hidden: bool,
#[prop(optional)] children: Option<Children>
#[prop(optional)] children: Option<Children>,
) -> impl IntoView {
view! {
<div
+2 -2
View File
@@ -1,6 +1,6 @@
use std::collections::HashMap;
use crate::common::ValueType;
use leptos::prelude::*;
use crate::components::ValueType;
use std::collections::HashMap;
#[component]
pub fn Radio(
+15
View File
@@ -1,5 +1,20 @@
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]
pub fn Row(children: Children) -> impl IntoView {
view! {
+3 -3
View File
@@ -1,8 +1,8 @@
use std::collections::HashMap;
use crate::common::ValueType;
use leptos::ev::{Event, Targeted};
use leptos::prelude::*;
use leptos::web_sys::HtmlSelectElement;
use crate::components::ValueType;
use std::collections::HashMap;
/// A custom-styled select input.
///
@@ -30,7 +30,7 @@ pub fn Select(
});
}
};
view! {
<div class="flex flex-1">
<label class="flex flex-col flex-1">
+15 -21
View File
@@ -1,13 +1,13 @@
use std::collections::HashMap;
use crate::common::ValueType;
use leptos::leptos_dom::logging::console_log;
use leptos::prelude::*;
use crate::components::ValueType;
use std::collections::HashMap;
#[component]
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(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,
@@ -15,7 +15,7 @@ pub fn OutlinedTextField(
#[prop(optional)] error: RwSignal<bool>,
#[prop(optional, into)] name: String,
#[prop(optional, into)] label: String,
#[prop(optional, into)] error_text: RwSignal<String>
#[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.
@@ -26,42 +26,36 @@ pub fn OutlinedTextField(
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
// 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)));
}
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()))) {
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);
}
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.");
@@ -72,7 +66,7 @@ pub fn OutlinedTextField(
}
}
};
view! {
<div class="flex flex-1">
<label class="flex flex-col flex-1">