diff --git a/src/common/mod.rs b/src/common/mod.rs index cd627ea..a561e44 100644 --- a/src/common/mod.rs +++ b/src/common/mod.rs @@ -8,8 +8,9 @@ mod user; mod value_type; pub mod animations; mod tab_info; +mod schema_node; pub use self::{ comparison::*, date_info::*, expandable_info::*, form_params::*, form_schema::*, - submit_status::*, user::*, value_type::*, tab_info::*, + submit_status::*, user::*, value_type::*, tab_info::*, schema_node::*, }; diff --git a/src/common/schema_node.rs b/src/common/schema_node.rs new file mode 100644 index 0000000..06062cd --- /dev/null +++ b/src/common/schema_node.rs @@ -0,0 +1,97 @@ +use std::collections::HashMap; +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone)] +pub enum SchemaType { + String, + Number, + List, + Map +} + +/// # Schema Node +/// +/// This is used to create a schema that is used in the [`ValueDisplay`] component. It stores all +/// required information for the display, specifically where to find the data and how to display it. +/// It covers all `ValueType` variants, however for data storage and DX it uses a `SchemaType` +/// instead. +/// +/// Some examples are as follows, using the fluent builder notation: +/// - Displaying a single `ValueType::String` from the data map at the key `example_one`: +/// ``` +/// SchemaNode::new(SchemaType::String).header("The example string") +/// ``` +/// - Displaying a `ValueType::List` which contains only `ValueType::String` entries: +/// ``` +/// SchemaNode::new(SchemaType::List) +/// .header("The example list") +/// .item_template(SchemaNode::new(SchemaType::String).header("")) +/// ``` +/// - Displaying a `ValueType::Map` which contains a name and age: +/// ``` +/// SchemaType::new(SchemaType::Map) +/// .header("The example map") +/// .child("first_name", SchemaNode::new(SchemaType::String).header("First Name:")) +/// .child("age", SchemaNode::new(SchemaType::Number).header("Age:")) +/// ``` +/// - Put it all together: a `ValueType::List` which contains `ValueType::Map` entries, each of +/// which also contains the name and age from the previous example: +/// ``` +/// SchemaNode::new(SchemaType::List) +/// .header("The example map list") +/// .item_template( +/// SchemaNode::new(SchemaType::Map) +/// .header("The example map from earlier") +/// .child("first_name", SchemaNode::new(SchemaType::String).header("First Name:")) +/// .child("age", SchemaNode::new(SchemaType::Number).header("Age:")) +/// ) +/// ``` +/// +/// This schema allows for definitive typing and styling for every level of the data display. +/// +/// ## Important Notes +/// +/// This schema does not currently support display of a single nested member, like a single map +/// within a list. This means that you will have to display any listed or mapped information as a +/// list. Consider this when designing the data. +#[derive(Deserialize, Serialize, Debug, Clone)] +pub struct SchemaNode { + /// The datatype of the data found using the `data_member` key. + pub data_type: SchemaType, + /// The header of this display. + pub header: String, + /// When used with a SchemaType::List, this determines the datatype and children for every entry + /// in the list. Some examples include a list of maps, where we use the `item_template` to define + /// the SchemaType::Map, and all its children. This is then mapped across all entries in the list, + /// where any entries that do not match the template are skipped. + pub item_template: Option>, + /// When used with a SchemaType::Map, this determines what children from the map are displayed + /// and how. It is ignored with all other SchemaType variants. + pub children: HashMap, +} + +impl SchemaNode { + pub fn new(data_type: SchemaType) -> Self { + SchemaNode { + data_type, + header: "".to_string(), + item_template: None, + children: HashMap::new(), + } + } + + pub fn header(mut self, header: impl Into) -> Self { + self.header = header.into(); + self + } + + pub fn child(mut self, data_member: impl Into, node: SchemaNode) -> Self { + self.children.insert(data_member.into(), node); + self + } + + pub fn item_template(mut self, node: SchemaNode) -> Self { + self.item_template = Some(Box::new(node)); + self + } +} diff --git a/src/common/value_type.rs b/src/common/value_type.rs index 6f54016..90e1f2f 100644 --- a/src/common/value_type.rs +++ b/src/common/value_type.rs @@ -105,8 +105,8 @@ impl Default for ValueType { 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::String(s) => s.clone().unwrap_or_default(), + ValueType::Number(n) => n.clone().unwrap_or_default(), ValueType::List(l) => format!("{l:?}"), ValueType::Map(m) => format!("{m:?}"), }; diff --git a/src/components/data_display.rs b/src/components/data_display.rs index 8dca269..036fde6 100644 --- a/src/components/data_display.rs +++ b/src/components/data_display.rs @@ -1,38 +1,203 @@ -use crate::common::ValueType; +use crate::common::{SchemaNode, SchemaType, ValueType}; use leptos::prelude::*; use std::collections::HashMap; +use leptos::logging::debug_log; #[component] pub fn ValueDisplay( - #[prop(into)] data_member: Signal, + #[prop(into)] schema: Signal, + #[prop(into, optional)] data_member: Signal, #[prop(into)] data_map: Signal>, - #[prop(into)] header: Signal, ) -> impl IntoView { - let display_value = Memo::new(move |_| { - data_map - .get() - .get(&data_member.get()) - .unwrap_or(&ValueType::String(None)) - .clone() + let data_value = Memo::new(move |_| { + if data_member.get().is_empty() { + ValueType::Map(Some(data_map.get())) + } else { + data_map.get() + .get(&data_member.get()) + .map(|v| v.clone()) + .unwrap_or_default() + } }); - // I've chosen to display all information - we're not going to worry about the depth of the - // information, but we are going to provide a way to collapse information like maps and lists. - // This means we need some capsule-like appearance for the information in order to facilitate - // having a header and a series of information that's buried underneath. Also, we need to consider - // how we determine what the header is because that's not really procedurally possible. + view! { + {move || { + match schema.get().data_type { + SchemaType::String | SchemaType::Number => view! { + + }.into_any(), + SchemaType::List => view! { + + }.into_any(), + SchemaType::Map => view! { + + }.into_any() + } + }} + } +} - // One possible way to handle this is a "schema." We would plan out the information beforehand - // and pass through this schema struct, allowing us to limit what's displayed, relative to the - // starting data_map. I'll write this out in a scratch file for now. +#[component] +fn PrimitiveDisplay( + #[prop(into)] header: Signal, + #[prop(into)] value: Signal +) -> impl IntoView { + let display_string = Memo::new(move |_| { + let value = value.get(); + match value { + ValueType::String(_) | ValueType::Number(_) => { + value.to_string() + } + _ => { + "Value is not a string.".to_string() + } + } + }); view! { -
-
{header}
-
{move || display_value.get().to_string()}
+
+ +
{header}
+
+
{display_string}
} } #[component] -pub fn MapDisplay() {} +fn ListDisplay( + #[prop(into)] item_template: Signal>, + #[prop(into)] value: Signal, + #[prop(into)] header: Signal, +) -> impl IntoView { + // Unwrap the value and check if it's the correct type. To be displayed with this component, it + // MUST be a ValueType::List. + let display_list = Memo::new(move |_| { + let ValueType::List(Some(list)) = value.get() else { + return Vec::new(); + }; + + list.into_iter().enumerate().collect::>() + }); + + let is_valid = Memo::new(move |_| { + matches!(value.get(), ValueType::List(_)) + }); + + let item_header = Memo::new(move |_| { + item_template.get().header + }); + + view! { +
+
{header}
+ view! { + + }.into_any(), + SchemaType::List => view! { +
"Nested lists are not currently supported for display."
+ }.into_any(), + SchemaType::Map => view! { + + }.into_any() + } + } + /> +
+ } +} + +#[component] +fn MapDisplay( + #[prop(into)] value: Signal, + #[prop(into)] header: Signal, + #[prop(into)] schema: Signal>, +) -> impl IntoView { + // Unwrap the value and check if it is the correct type. It must be a ValueType::Map. + let display_map = Memo::new(move |_| { + let ValueType::Map(Some(map)) = value.get() else { + return HashMap::new(); + }; + + map + }); + + Effect::new(move || { + debug_log!("Current map values: {:?}", display_map.get()); + }); + + let is_valid = Memo::new(move |_| { + matches!(value.get(), ValueType::Map(_)) + }); + + view! { +
+
{header}
+
+ view! { + + }.into_any(), + SchemaType::List => { + let template = child_schema.item_template + .unwrap_or(Box::from(SchemaNode::new(SchemaType::List))); + + view! { + + } + }.into_any(), + SchemaType::Map => view! { + + }.into_any() + } + } + /> +
+
+ } +} diff --git a/src/pages/student/test_display.rs b/src/pages/student/test_display.rs index b5c7cfa..a0fa461 100644 --- a/src/pages/student/test_display.rs +++ b/src/pages/student/test_display.rs @@ -2,9 +2,24 @@ use crate::components::{Loading, ValueDisplay}; use crate::pages::api::students::get_all_student_data; use crate::utils::get_user_claims; use leptos::prelude::*; +use crate::common::{SchemaNode, SchemaType}; #[component] pub fn StudentTestDisplay() -> impl IntoView { + let create_student_display_schema = move || { + SchemaNode::new(SchemaType::Map) + .header("Student Information:") + .child("first_name", SchemaNode::new(SchemaType::String).header("First Name:")) + .child("last_name", SchemaNode::new(SchemaType::String).header("Last Name:")) + .child("sports_participation", SchemaNode::new(SchemaType::List) + .header("List of Sports Activities") + .item_template(SchemaNode::new(SchemaType::Map) + .child("sport_name", SchemaNode::new(SchemaType::String).header("Sport Name:")) + .child("achievements", SchemaNode::new(SchemaType::String).header("Special Achievements")) + ) + ) + }; + let user_claims = get_user_claims(); let user_id = Memo::new(move |_| user_claims.get().map(|info| info.claims.subject.clone())); @@ -13,39 +28,32 @@ pub fn StudentTestDisplay() -> impl IntoView { async move |id| get_all_student_data(id).await, ); + let schema = StoredValue::new(create_student_display_schema()); + view! { - {move || { - resource - .get() - .map(|data_map| { - let Ok(map) = data_map else { - return view! {}.into_any(); - }; - let data_map = StoredValue::new(map); +
+ {move || { + resource + .get() + .map(|data_map| { + let Ok(map) = data_map else { + return view! {}.into_any(); + }; + let data_map = StoredValue::new(map); - view! { -
- - } - } - /> -
- } - .into_any() - }) - }} + view! { +
+ +
+ } + .into_any() + }) + }} +
} }