Implement template student portal #62
No files matched your search
+2
-1
@@ -8,8 +8,9 @@ mod user;
|
|||||||
mod value_type;
|
mod value_type;
|
||||||
pub mod animations;
|
pub mod animations;
|
||||||
mod tab_info;
|
mod tab_info;
|
||||||
|
mod schema_node;
|
||||||
|
|
||||||
pub use self::{
|
pub use self::{
|
||||||
comparison::*, date_info::*, expandable_info::*, form_params::*, form_schema::*,
|
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::*,
|
||||||
};
|
};
|
||||||
@@ -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<Box<SchemaNode>>,
|
||||||
|
/// 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<String, SchemaNode>,
|
||||||
|
}
|
||||||
|
|
||||||
|
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<String>) -> Self {
|
||||||
|
self.header = header.into();
|
||||||
|
self
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn child(mut self, data_member: impl Into<String>, 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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -105,8 +105,8 @@ impl Default for ValueType {
|
|||||||
impl Display for ValueType {
|
impl Display for ValueType {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
let str = match self {
|
let str = match self {
|
||||||
ValueType::String(s) => s.clone().unwrap_or("".to_owned()),
|
ValueType::String(s) => s.clone().unwrap_or_default(),
|
||||||
ValueType::Number(n) => n.clone().unwrap_or("".to_owned()),
|
ValueType::Number(n) => n.clone().unwrap_or_default(),
|
||||||
ValueType::List(l) => format!("{l:?}"),
|
ValueType::List(l) => format!("{l:?}"),
|
||||||
ValueType::Map(m) => format!("{m:?}"),
|
ValueType::Map(m) => format!("{m:?}"),
|
||||||
};
|
};
|
||||||
|
|||||||
+184
-19
@@ -1,38 +1,203 @@
|
|||||||
use crate::common::ValueType;
|
use crate::common::{SchemaNode, SchemaType, ValueType};
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
|
use leptos::logging::debug_log;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn ValueDisplay(
|
pub fn ValueDisplay(
|
||||||
#[prop(into)] data_member: Signal<String>,
|
#[prop(into)] schema: Signal<SchemaNode>,
|
||||||
|
#[prop(into, optional)] data_member: Signal<String>,
|
||||||
#[prop(into)] data_map: Signal<HashMap<String, ValueType>>,
|
#[prop(into)] data_map: Signal<HashMap<String, ValueType>>,
|
||||||
#[prop(into)] header: Signal<String>,
|
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let display_value = Memo::new(move |_| {
|
let data_value = Memo::new(move |_| {
|
||||||
data_map
|
if data_member.get().is_empty() {
|
||||||
.get()
|
ValueType::Map(Some(data_map.get()))
|
||||||
|
} else {
|
||||||
|
data_map.get()
|
||||||
.get(&data_member.get())
|
.get(&data_member.get())
|
||||||
.unwrap_or(&ValueType::String(None))
|
.map(|v| v.clone())
|
||||||
.clone()
|
.unwrap_or_default()
|
||||||
|
}
|
||||||
});
|
});
|
||||||
|
|
||||||
// I've chosen to display all information - we're not going to worry about the depth of the
|
view! {
|
||||||
// information, but we are going to provide a way to collapse information like maps and lists.
|
{move || {
|
||||||
// This means we need some capsule-like appearance for the information in order to facilitate
|
match schema.get().data_type {
|
||||||
// having a header and a series of information that's buried underneath. Also, we need to consider
|
SchemaType::String | SchemaType::Number => view! {
|
||||||
// how we determine what the header is because that's not really procedurally possible.
|
<PrimitiveDisplay
|
||||||
|
header={move || schema.get().header}
|
||||||
|
value=data_value
|
||||||
|
/>
|
||||||
|
}.into_any(),
|
||||||
|
SchemaType::List => view! {
|
||||||
|
<ListDisplay
|
||||||
|
header=move || schema.get().header
|
||||||
|
value=data_value
|
||||||
|
item_template=move || schema.get().item_template.unwrap_or(Box::from(SchemaNode::new(SchemaType::List)))
|
||||||
|
/>
|
||||||
|
}.into_any(),
|
||||||
|
SchemaType::Map => view! {
|
||||||
|
<MapDisplay
|
||||||
|
value=data_value
|
||||||
|
header=move || schema.get().header
|
||||||
|
schema=move || schema.get().children
|
||||||
|
/>
|
||||||
|
}.into_any()
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// One possible way to handle this is a "schema." We would plan out the information beforehand
|
#[component]
|
||||||
// and pass through this schema struct, allowing us to limit what's displayed, relative to the
|
fn PrimitiveDisplay(
|
||||||
// starting data_map. I'll write this out in a scratch file for now.
|
#[prop(into)] header: Signal<String>,
|
||||||
|
#[prop(into)] value: Signal<ValueType>
|
||||||
|
) -> 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! {
|
view! {
|
||||||
<div class="flex flex-1 flex-col">
|
<div>
|
||||||
|
<Show when=move || !header.get().is_empty()>
|
||||||
<div class="font-bold">{header}</div>
|
<div class="font-bold">{header}</div>
|
||||||
<div>{move || display_value.get().to_string()}</div>
|
</Show>
|
||||||
|
<div>{display_string}</div>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn MapDisplay() {}
|
fn ListDisplay(
|
||||||
|
#[prop(into)] item_template: Signal<Box<SchemaNode>>,
|
||||||
|
#[prop(into)] value: Signal<ValueType>,
|
||||||
|
#[prop(into)] header: Signal<String>,
|
||||||
|
) -> 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::<Vec<(usize, ValueType)>>()
|
||||||
|
});
|
||||||
|
|
||||||
|
let is_valid = Memo::new(move |_| {
|
||||||
|
matches!(value.get(), ValueType::List(_))
|
||||||
|
});
|
||||||
|
|
||||||
|
let item_header = Memo::new(move |_| {
|
||||||
|
item_template.get().header
|
||||||
|
});
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<div class="font-bold">{header}</div>
|
||||||
|
<For
|
||||||
|
each=move || display_list.get()
|
||||||
|
key=|(idx, _)| idx.clone()
|
||||||
|
children=move |(idx, entry)| {
|
||||||
|
match item_template.get().data_type {
|
||||||
|
SchemaType::String | SchemaType::Number =>view! {
|
||||||
|
<PrimitiveDisplay
|
||||||
|
value=entry
|
||||||
|
header=item_header
|
||||||
|
/>
|
||||||
|
}.into_any(),
|
||||||
|
SchemaType::List => view! {
|
||||||
|
<div>"Nested lists are not currently supported for display."</div>
|
||||||
|
}.into_any(),
|
||||||
|
SchemaType::Map => view! {
|
||||||
|
<MapDisplay
|
||||||
|
value=entry
|
||||||
|
header=item_header
|
||||||
|
schema=move || item_template.get().children
|
||||||
|
/>
|
||||||
|
}.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[component]
|
||||||
|
fn MapDisplay(
|
||||||
|
#[prop(into)] value: Signal<ValueType>,
|
||||||
|
#[prop(into)] header: Signal<String>,
|
||||||
|
#[prop(into)] schema: Signal<HashMap<String, SchemaNode>>,
|
||||||
|
) -> 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! {
|
||||||
|
<div class="flex flex-col flex-1 gap-2">
|
||||||
|
<div class="font-bold">{header}</div>
|
||||||
|
<div class="p-2 rounded-md shadow-lg/33 m-2">
|
||||||
|
<For
|
||||||
|
each=move || schema.get()
|
||||||
|
key=|(k, _)| k.clone()
|
||||||
|
children=move |(member, child_schema)| {
|
||||||
|
let display_value = Memo::new(move |_| {
|
||||||
|
display_map.get()
|
||||||
|
.get(&member)
|
||||||
|
.map(|v| v.clone())
|
||||||
|
.unwrap_or_default()
|
||||||
|
});
|
||||||
|
|
||||||
|
match child_schema.data_type {
|
||||||
|
SchemaType::String | SchemaType::Number => view! {
|
||||||
|
<PrimitiveDisplay
|
||||||
|
value=display_value
|
||||||
|
header=child_schema.header
|
||||||
|
/>
|
||||||
|
}.into_any(),
|
||||||
|
SchemaType::List => {
|
||||||
|
let template = child_schema.item_template
|
||||||
|
.unwrap_or(Box::from(SchemaNode::new(SchemaType::List)));
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<ListDisplay
|
||||||
|
value=display_value
|
||||||
|
item_template=template
|
||||||
|
header=child_schema.header
|
||||||
|
/>
|
||||||
|
}
|
||||||
|
}.into_any(),
|
||||||
|
SchemaType::Map => view! {
|
||||||
|
<MapDisplay
|
||||||
|
value=display_value
|
||||||
|
schema=child_schema.children
|
||||||
|
header=child_schema.header
|
||||||
|
/>
|
||||||
|
}.into_any()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -2,9 +2,24 @@ use crate::components::{Loading, ValueDisplay};
|
|||||||
use crate::pages::api::students::get_all_student_data;
|
use crate::pages::api::students::get_all_student_data;
|
||||||
use crate::utils::get_user_claims;
|
use crate::utils::get_user_claims;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
|
use crate::common::{SchemaNode, SchemaType};
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub fn StudentTestDisplay() -> impl IntoView {
|
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_claims = get_user_claims();
|
||||||
let user_id = Memo::new(move |_| user_claims.get().map(|info| info.claims.subject.clone()));
|
let user_id = Memo::new(move |_| user_claims.get().map(|info| info.claims.subject.clone()));
|
||||||
|
|
||||||
@@ -13,8 +28,11 @@ pub fn StudentTestDisplay() -> impl IntoView {
|
|||||||
async move |id| get_all_student_data(id).await,
|
async move |id| get_all_student_data(id).await,
|
||||||
);
|
);
|
||||||
|
|
||||||
|
let schema = StoredValue::new(create_student_display_schema());
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<Suspense fallback=Loading>
|
<Suspense fallback=Loading>
|
||||||
|
<div class="flex flex-1 flex-col">
|
||||||
{move || {
|
{move || {
|
||||||
resource
|
resource
|
||||||
.get()
|
.get()
|
||||||
@@ -26,26 +44,16 @@ pub fn StudentTestDisplay() -> impl IntoView {
|
|||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div>
|
<div>
|
||||||
<For
|
|
||||||
each=move || data_map.get_value()
|
|
||||||
key=|(k, v)| k.clone()
|
|
||||||
children=move |(k, v)| {
|
|
||||||
let member = StoredValue::new(k.clone());
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<ValueDisplay
|
<ValueDisplay
|
||||||
|
schema=schema.get_value()
|
||||||
data_map=data_map.get_value()
|
data_map=data_map.get_value()
|
||||||
data_member=member.get_value()
|
|
||||||
header=member.get_value()
|
|
||||||
/>
|
|
||||||
}
|
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
.into_any()
|
.into_any()
|
||||||
})
|
})
|
||||||
}}
|
}}
|
||||||
|
</div>
|
||||||
</Suspense>
|
</Suspense>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Reference in new issue
Block a user