feat: implement new data display component
This commit is contained in:
1 parent
07c1679e1a
commit
eae8d2a26a
6 files changed
+506
-355
No files matched your search
@@ -158,7 +158,6 @@ fn StudentRoutes() -> impl MatchNestedRoutes + Clone {
|
||||
path=path!("additional/specifics")
|
||||
view=student::StudentSpecificRequirementsPage
|
||||
/>
|
||||
<Route path=path!("testing") view=student::StudentTestDisplay />
|
||||
</ParentRoute>
|
||||
}
|
||||
.into_inner()
|
||||
|
||||
+170
-15
@@ -1,13 +1,35 @@
|
||||
use std::collections::HashMap;
|
||||
use indexmap::IndexMap;
|
||||
use indexmap::IndexMap;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug, Clone)]
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
pub enum SchemaType {
|
||||
/// Indicates a `ValueType::String` value.
|
||||
#[default]
|
||||
String,
|
||||
/// Indicates a `ValueType::Number` value.
|
||||
Number,
|
||||
List,
|
||||
Map
|
||||
/// Indicates a `ValueType::List` containing only strings or numbers.
|
||||
PrimitiveList,
|
||||
/// Indicates a `ValueType::List` containing only maps.
|
||||
MapList,
|
||||
/// Indicates a `ValueType::Map`.
|
||||
Map,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
pub enum SchemaContainerStyle {
|
||||
#[default]
|
||||
Header,
|
||||
Capsule,
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
pub enum SchemaHeaderStyle {
|
||||
#[default]
|
||||
None,
|
||||
MainHeader,
|
||||
SubsectionHeader,
|
||||
Bold,
|
||||
}
|
||||
|
||||
/// # Schema Node
|
||||
@@ -55,12 +77,16 @@ pub enum SchemaType {
|
||||
/// 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)]
|
||||
#[derive(Serialize, Deserialize, Default, Debug, Clone)]
|
||||
pub struct SchemaNode {
|
||||
/// The display style of the data. Refer to [`SchemaContainerStyle`] for more information.
|
||||
pub container_style: SchemaContainerStyle,
|
||||
/// The datatype of the data found using the `data_member` key.
|
||||
pub data_type: SchemaType,
|
||||
/// The header of this display.
|
||||
pub header: String,
|
||||
/// The style of the header's display.
|
||||
pub header_style: SchemaHeaderStyle,
|
||||
/// 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,
|
||||
@@ -69,16 +95,28 @@ pub struct 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: IndexMap<String, SchemaNode>,
|
||||
/// The primary key for iteration over maps. Used when there is a list of maps.
|
||||
pub primary_key: Option<String>,
|
||||
}
|
||||
|
||||
impl SchemaNode {
|
||||
pub fn new(data_type: SchemaType) -> Self {
|
||||
SchemaNode {
|
||||
data_type,
|
||||
header: "".to_string(),
|
||||
item_template: None,
|
||||
children: IndexMap::new(),
|
||||
}
|
||||
/// Creates a new [`SchemaBuilder`].
|
||||
pub fn builder() -> SchemaBuilder {
|
||||
SchemaBuilder::default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SchemaBuilder {
|
||||
container_style: SchemaContainerStyle,
|
||||
header: String,
|
||||
header_style: SchemaHeaderStyle,
|
||||
}
|
||||
|
||||
impl SchemaBuilder {
|
||||
pub fn container(mut self, style: SchemaContainerStyle) -> Self {
|
||||
self.container_style = style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn header(mut self, header: impl Into<String>) -> Self {
|
||||
@@ -86,13 +124,130 @@ impl SchemaNode {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn child(mut self, data_member: impl Into<String>, node: SchemaNode) -> Self {
|
||||
self.children.insert(data_member.into(), node);
|
||||
pub fn header_style(mut self, header_style: SchemaHeaderStyle) -> Self {
|
||||
self.header_style = header_style;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn primitive_list(self) -> SchemaPrimitiveListBuilder {
|
||||
SchemaPrimitiveListBuilder::new(self)
|
||||
}
|
||||
|
||||
pub fn map_list(self) -> SchemaMapListBuilder {
|
||||
SchemaMapListBuilder::new(self)
|
||||
}
|
||||
|
||||
pub fn map(self) -> SchemaMapBuilder {
|
||||
SchemaMapBuilder::new(self)
|
||||
}
|
||||
|
||||
pub fn string(self) -> SchemaPrimitiveBuilder {
|
||||
SchemaPrimitiveBuilder::new(self, SchemaType::String)
|
||||
}
|
||||
|
||||
pub fn number(self) -> SchemaPrimitiveBuilder {
|
||||
SchemaPrimitiveBuilder::new(self, SchemaType::Number)
|
||||
}
|
||||
|
||||
fn into_node(self) -> SchemaNode {
|
||||
SchemaNode {
|
||||
container_style: self.container_style,
|
||||
header: self.header,
|
||||
header_style: self.header_style,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SchemaPrimitiveListBuilder {
|
||||
builder: SchemaBuilder,
|
||||
}
|
||||
|
||||
impl SchemaPrimitiveListBuilder {
|
||||
fn new(builder: SchemaBuilder) -> Self {
|
||||
Self { builder }
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SchemaMapListBuilder {
|
||||
builder: SchemaBuilder,
|
||||
item_template: Option<Box<SchemaNode>>,
|
||||
primary_key: String,
|
||||
}
|
||||
|
||||
impl SchemaMapListBuilder {
|
||||
fn new(builder: SchemaBuilder) -> Self {
|
||||
Self {
|
||||
builder,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn item_template(mut self, node: SchemaNode) -> Self {
|
||||
self.item_template = Some(Box::new(node));
|
||||
self
|
||||
}
|
||||
|
||||
pub fn primary_key(mut self, primary_key: impl Into<String>) -> Self {
|
||||
self.primary_key = primary_key.into();
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> SchemaNode {
|
||||
SchemaNode {
|
||||
data_type: SchemaType::MapList,
|
||||
item_template: self.item_template,
|
||||
primary_key: Some(self.primary_key),
|
||||
..self.builder.into_node()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SchemaMapBuilder {
|
||||
builder: SchemaBuilder,
|
||||
children: IndexMap<String, SchemaNode>,
|
||||
}
|
||||
|
||||
impl SchemaMapBuilder {
|
||||
fn new(builder: SchemaBuilder) -> Self {
|
||||
Self {
|
||||
builder,
|
||||
..Default::default()
|
||||
}
|
||||
}
|
||||
|
||||
pub fn child(mut self, data_member: impl Into<String>, node: SchemaNode) -> Self {
|
||||
self.children.insert(data_member.into(), node);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn build(self) -> SchemaNode {
|
||||
SchemaNode {
|
||||
data_type: SchemaType::Map,
|
||||
children: self.children,
|
||||
..self.builder.into_node()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default, Debug, Clone)]
|
||||
pub struct SchemaPrimitiveBuilder {
|
||||
builder: SchemaBuilder,
|
||||
data_type: SchemaType,
|
||||
}
|
||||
|
||||
impl SchemaPrimitiveBuilder {
|
||||
fn new(builder: SchemaBuilder, data_type: SchemaType) -> Self {
|
||||
Self { data_type, builder }
|
||||
}
|
||||
|
||||
pub fn build(self) -> SchemaNode {
|
||||
SchemaNode {
|
||||
data_type: self.data_type,
|
||||
..self.builder.into_node()
|
||||
}
|
||||
}
|
||||
}
|
||||
+205
-217
@@ -1,265 +1,253 @@
|
||||
use crate::common::{SchemaNode, SchemaType, ValueType};
|
||||
use indexmap::IndexMap;
|
||||
use leptos::logging::debug_log;
|
||||
use crate::common::{SchemaContainerStyle, SchemaHeaderStyle, SchemaNode, SchemaType, ValueType};
|
||||
use leptos::prelude::*;
|
||||
use std::collections::HashMap;
|
||||
|
||||
/// # Value Display Component
|
||||
/// This component displays any data from a given `HashMap<String, ValueType>`. It requires a schema
|
||||
/// to tell it how to display the information, which is covered in the [`SchemaNode`] documentation.
|
||||
#[component]
|
||||
pub fn ValueDisplay(
|
||||
#[prop(into)] schema: Signal<SchemaNode>,
|
||||
/// The starting member. Starts display from this member in the `data_map`, if it exists.
|
||||
pub fn DataDisplay(
|
||||
/// The schema that this component should use to display the given data.
|
||||
#[prop(into)]
|
||||
schema: Signal<SchemaNode>,
|
||||
/// The starting key in the data map.
|
||||
#[prop(into, optional)]
|
||||
data_member: Signal<String>,
|
||||
#[prop(into)] data_map: Signal<HashMap<String, ValueType>>,
|
||||
/// A `HashMap` containing all the data that should be displayed.
|
||||
#[prop(into)]
|
||||
data_map: Signal<HashMap<String, ValueType>>,
|
||||
) -> impl IntoView {
|
||||
let data_value = Memo::new(move |_| {
|
||||
if data_member.get().is_empty() {
|
||||
ValueType::Map(Some(data_map.get()))
|
||||
// The schema should contain information about the header type and styling, along with the
|
||||
// information display. This is rendered as part of the DataDisplayWrapper, which is simply a
|
||||
// wrapper around the information to be displayed.
|
||||
|
||||
// The children are all SchemaNodes as well, meaning that this component needs to call itself
|
||||
// for each of these children. They will of course contain all the same information that we
|
||||
// expect here.
|
||||
|
||||
// The actual displayed information is controlled through a DataDisplayView, which will accept
|
||||
// settings for how the information is parsed. For example, rendering a list may be done by
|
||||
// a string join operation, or it may be a list of maps, each of which needs to be displayed with
|
||||
// a specific set of children. The SchemaNode must be able to handle this, and these options
|
||||
// should be given to the DataDisplayView somehow.
|
||||
|
||||
// As the display_value here is actually an Option<ValueType>, we want to be able to handle cases
|
||||
// where the display is given no values. In this case, the SchemaNode needs to specify whether
|
||||
// a missing value is expected or an error.
|
||||
|
||||
let display_value = Memo::new(move |_| {
|
||||
let member = data_member.get();
|
||||
if member.is_empty() {
|
||||
Some(ValueType::Map(Some(data_map.get())))
|
||||
} else {
|
||||
data_map
|
||||
.get()
|
||||
.get(&data_member.get())
|
||||
.map(|v| v.clone())
|
||||
.unwrap_or_default()
|
||||
data_map.get().get(&member).cloned()
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
{move || {
|
||||
match schema.get().data_type {
|
||||
SchemaType::String | SchemaType::Number => {
|
||||
view! {
|
||||
<PrimitiveDisplay
|
||||
header=move || schema.get().header
|
||||
value=data_value
|
||||
nested=true
|
||||
/>
|
||||
}
|
||||
.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)))
|
||||
}
|
||||
nested=true
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
SchemaType::Map => {
|
||||
view! {
|
||||
<MapDisplay
|
||||
value=data_value
|
||||
header=move || schema.get().header
|
||||
schema=move || schema.get().children
|
||||
nested=true
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}}
|
||||
<DataDisplayWrapper
|
||||
container_style=schema.get().container_style
|
||||
header_style=schema.get().header_style
|
||||
header_text=schema.get().header
|
||||
>
|
||||
<DataDisplayView display_data=display_value schema=schema />
|
||||
</DataDisplayWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
/// # Display Wrapper Component
|
||||
/// This component is designed to wrap information for the [`ValueDisplay`] component. It allows
|
||||
/// for simple uniform styling of the header and capsule, along with the ability for users to
|
||||
/// collapse the information.
|
||||
#[component]
|
||||
fn DisplayWrapper(
|
||||
#[prop(into, optional)] header: Signal<String>,
|
||||
#[prop(into, optional)] nested: Signal<bool>,
|
||||
fn DataDisplayWrapper(
|
||||
#[prop(into)] container_style: SchemaContainerStyle,
|
||||
#[prop(into)] header_style: SchemaHeaderStyle,
|
||||
#[prop(into)] header_text: Signal<String>,
|
||||
children: Children,
|
||||
) -> impl IntoView {
|
||||
// This component takes a wrapper style and displays the "chrome" of the interface. It has two
|
||||
// options: Text Headers or Capsules. This will then render a DataDisplayHeader, which handles
|
||||
// the styling for the text inside the header specifically.
|
||||
|
||||
// The capsule style is a colored header and a rounded container.
|
||||
// The text header style is an invisible container.
|
||||
|
||||
let expanded = RwSignal::new(true);
|
||||
|
||||
view! {
|
||||
<div class="m-3 rounded-lg shadow-lg flex flex-col">
|
||||
<div
|
||||
class="p-2 px-3 font-bold rounded-t-lg cursor-pointer"
|
||||
class=(["bg-gray-300"], move || !nested.get())
|
||||
class=(["text-white", "bg-red-900"], move || nested.get())
|
||||
class=(["rounded-lg"], move || !expanded.get())
|
||||
on:click=move |_| expanded.update(|b| *b = !*b)
|
||||
>
|
||||
{header}
|
||||
</div>
|
||||
<div
|
||||
class="grid transition-all ease-in-out"
|
||||
style:grid-template-rows=move || if expanded.get() { "1fr" } else { "0fr" }
|
||||
>
|
||||
<div class="min-h-0 overflow-hidden">
|
||||
<div class="p-2">{children()}</div>
|
||||
match container_style {
|
||||
SchemaContainerStyle::Header => {
|
||||
// Creates a simple container that only has a header and a series of children.
|
||||
// The spacing between the header and children is controlled by the header style.
|
||||
// This type of container is not collapsable at the moment.
|
||||
view! {
|
||||
<div class="flex flex-col w-full text-left">
|
||||
<div class="self-start w-full">
|
||||
<DataDisplayHeader header_text=header_text header_style=header_style />
|
||||
<div class="p-2 flex flex-col gap-1">{children()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
}
|
||||
|
||||
/// # Primitive Value Display Component
|
||||
/// This component allows for the display of a `ValueType::String` or `ValueType::Number`.
|
||||
#[component]
|
||||
fn PrimitiveDisplay(
|
||||
#[prop(into)] header: Signal<String>,
|
||||
#[prop(into)] value: Signal<ValueType>,
|
||||
#[prop(into, optional)] nested: Signal<bool>,
|
||||
) -> 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(),
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
SchemaContainerStyle::Capsule => {
|
||||
// Creates a capsule container - it is collapsable, with rounded corners and a colored
|
||||
// header.
|
||||
view! {
|
||||
<div class="self-stretch rounded-lg shadow-lg flex flex-col w-full text-center">
|
||||
<div
|
||||
class="p-2 px-3 rounded-t-lg cursor-pointer bg-red-800 text-white"
|
||||
class=(["rounded-lg"], move || !expanded.get())
|
||||
on:click=move |_| expanded.update(|b| *b = !*b)
|
||||
>
|
||||
<DataDisplayHeader header_text=header_text header_style=header_style />
|
||||
</div>
|
||||
<div
|
||||
class="grid transition-all ease-in-out"
|
||||
style:grid-template-rows=move || if expanded.get() { "1fr" } else { "0fr" }
|
||||
>
|
||||
<div class="min-h-0 overflow-hidden">
|
||||
<div class="p-2 flex flex-col gap-1 flex-1">{children()}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
});
|
||||
|
||||
view! {
|
||||
<DisplayWrapper header=header nested=nested>
|
||||
<div>{display_string}</div>
|
||||
</DisplayWrapper>
|
||||
}
|
||||
}
|
||||
|
||||
/// # List Value Display Component
|
||||
/// This component allows for the display of a `ValueType::List`. It will show an error if passed
|
||||
/// anything other than a list. It requires an item template that tells the list how to display
|
||||
/// each value inside of it, most specifically the type of data inside of it.
|
||||
#[component]
|
||||
fn ListDisplay(
|
||||
#[prop(into)] item_template: Signal<Box<SchemaNode>>,
|
||||
#[prop(into)] value: Signal<ValueType>,
|
||||
#[prop(into)] header: Signal<String>,
|
||||
#[prop(into, optional)] nested: Signal<bool>,
|
||||
fn DataDisplayHeader(
|
||||
#[prop(into)] header_style: SchemaHeaderStyle,
|
||||
#[prop(into)] header_text: 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();
|
||||
};
|
||||
// This component will take a header style and displays the corresponding text. It has a few
|
||||
// options, all of which are defined in the HeaderStyle enum.
|
||||
|
||||
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! {
|
||||
<DisplayWrapper header=header nested=nested>
|
||||
<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 => {
|
||||
let map_header = format!("{} {}", item_template.get().header, idx + 1);
|
||||
view! {
|
||||
<MapDisplay
|
||||
value=entry
|
||||
header=map_header
|
||||
schema=move || item_template.get().children
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
</DisplayWrapper>
|
||||
match header_style {
|
||||
SchemaHeaderStyle::MainHeader => {
|
||||
view! { <div class="text-3xl font-bold my-2">{header_text}</div> }.into_any()
|
||||
}
|
||||
SchemaHeaderStyle::SubsectionHeader => {
|
||||
view! { <div class="text-xl font-bold">{header_text}</div> }.into_any()
|
||||
}
|
||||
SchemaHeaderStyle::Bold => view! { <div class="font-bold">{header_text}</div> }.into_any(),
|
||||
SchemaHeaderStyle::None => view! {}.into_any(),
|
||||
}
|
||||
}
|
||||
|
||||
/// # Map Data Display Component
|
||||
/// This component allows for the display of a `ValueType::Map`. The nested information must be defined
|
||||
/// by a [`SchemaNode`], which tells the display what keys in the value map to search for the
|
||||
/// display information.
|
||||
#[component]
|
||||
fn MapDisplay(
|
||||
#[prop(into)] value: Signal<ValueType>,
|
||||
#[prop(into)] header: Signal<String>,
|
||||
#[prop(into)] schema: Signal<IndexMap<String, SchemaNode>>,
|
||||
#[prop(into, optional)] nested: Signal<bool>,
|
||||
fn DataDisplayView(
|
||||
#[prop(into)] display_data: Signal<Option<ValueType>>,
|
||||
#[prop(into)] schema: Signal<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();
|
||||
};
|
||||
// This is the part of the data display that does most of the work. It will take the values
|
||||
// found in the DataDisplay component and perform the logic that actually displays the
|
||||
// information. This component is only displayed if there is Some(value) in the DataDisplay.
|
||||
|
||||
map
|
||||
});
|
||||
// In the case that this is displaying a Map or List, it will recursively call the DataDisplay
|
||||
// component.
|
||||
|
||||
Effect::new(move || {
|
||||
debug_log!("Current map values: {:?}", display_map.get());
|
||||
});
|
||||
// If this is displaying a PrimitiveList, it will flatten and join all the values into a single
|
||||
// string, then display that string.
|
||||
|
||||
let is_valid = Memo::new(move |_| matches!(value.get(), ValueType::Map(_)));
|
||||
let data_type = schema.get().data_type;
|
||||
|
||||
view! {
|
||||
<DisplayWrapper header=header nested=nested>
|
||||
<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()
|
||||
// Matches the specific data type. Displays errors if the given display data does not match.
|
||||
match data_type {
|
||||
SchemaType::String => {
|
||||
let value = Signal::derive(move || {
|
||||
display_data
|
||||
.get()
|
||||
.map(|v| v.as_string().ok().flatten())
|
||||
.flatten()
|
||||
.unwrap_or("N/A".to_string())
|
||||
});
|
||||
view! { <div>{value}</div> }.into_any()
|
||||
}
|
||||
SchemaType::Number => {
|
||||
let value = Signal::derive(move || {
|
||||
display_data
|
||||
.get()
|
||||
.map(|v| v.as_number().ok().flatten())
|
||||
.flatten()
|
||||
.unwrap_or("N/A".to_string())
|
||||
});
|
||||
view! { <div>{value}</div> }.into_any()
|
||||
}
|
||||
SchemaType::PrimitiveList => {
|
||||
let value = Signal::derive(move || {
|
||||
display_data
|
||||
.get()
|
||||
.map(|v| v.as_list().ok().flatten())
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.map(|v| v.to_string())
|
||||
.collect::<Vec<String>>()
|
||||
.join(", ")
|
||||
});
|
||||
view! { <div>{value}</div> }.into_any()
|
||||
}
|
||||
SchemaType::MapList => {
|
||||
let list = Signal::derive(move || {
|
||||
display_data
|
||||
.get()
|
||||
.map(|v| v.as_list().ok().flatten())
|
||||
.flatten()
|
||||
.unwrap_or_default()
|
||||
.iter()
|
||||
.filter_map(|v| v.as_map().ok().flatten().clone())
|
||||
.collect::<Vec<HashMap<String, ValueType>>>()
|
||||
});
|
||||
let primary_key = StoredValue::new(schema.get().primary_key.unwrap_or_default());
|
||||
view! {
|
||||
<Show
|
||||
when=move || !list.get().is_empty()
|
||||
fallback=|| view! { <div class="w-full text-center">"List is empty."</div> }
|
||||
>
|
||||
<For
|
||||
each=move || list.get()
|
||||
key=move |map| {
|
||||
map.get(&primary_key.get_value())
|
||||
.map(|v| v.to_string())
|
||||
.unwrap_or_default()
|
||||
}
|
||||
SchemaType::List => {
|
||||
let template = child_schema
|
||||
.item_template
|
||||
.unwrap_or(Box::from(SchemaNode::new(SchemaType::List)));
|
||||
|
||||
children=move |map| {
|
||||
view! {
|
||||
<ListDisplay
|
||||
value=display_value
|
||||
item_template=template
|
||||
header=child_schema.header
|
||||
<DataDisplay
|
||||
schema=*schema.get().item_template.unwrap_or_default()
|
||||
data_map=map
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
SchemaType::Map => {
|
||||
/>
|
||||
</Show>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
SchemaType::Map => {
|
||||
let map = Signal::derive(move || {
|
||||
display_data
|
||||
.get()
|
||||
.map(|v| v.as_map().ok().flatten())
|
||||
.flatten()
|
||||
});
|
||||
view! {
|
||||
<Show
|
||||
when=move || map.get().is_some()
|
||||
fallback=|| view! { <div class="text-red-500">"Map was not found."</div> }
|
||||
>
|
||||
<For
|
||||
each=move || schema.get().children
|
||||
key=|(key, _)| key.clone()
|
||||
children=move |(key, schema)| {
|
||||
view! {
|
||||
<MapDisplay
|
||||
value=display_value
|
||||
schema=child_schema.children
|
||||
header=child_schema.header
|
||||
<DataDisplay
|
||||
schema=schema
|
||||
data_member=key
|
||||
data_map=Signal::derive(move || map.get().unwrap_or_default())
|
||||
/>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
/>
|
||||
</DisplayWrapper>
|
||||
/>
|
||||
</Show>
|
||||
}
|
||||
.into_any()
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
use crate::common::{ComparisonData, ExpandableInfo, SchemaNode, SchemaType, ValueType};
|
||||
use crate::components::{ActionButton, Banner, Loading, ValueDisplay};
|
||||
use crate::common::{
|
||||
ComparisonData, ExpandableInfo, SchemaContainerStyle, SchemaHeaderStyle, SchemaNode,
|
||||
SchemaType, ValueType,
|
||||
};
|
||||
use crate::components::{ActionButton, Banner, DataDisplay, Loading};
|
||||
use crate::pages::UnauthenticatedPage;
|
||||
use crate::pages::api::get_comparison_info;
|
||||
use crate::pages::api::students::{GetStudentFiles, get_all_input_files, get_student_data};
|
||||
@@ -9,7 +12,8 @@ use leptos::html::Dialog;
|
||||
use leptos::logging::debug_log;
|
||||
use leptos::prelude::*;
|
||||
use leptos::wasm_bindgen::JsCast;
|
||||
use leptos::web_sys::HtmlAnchorElement;
|
||||
use leptos::web_sys;
|
||||
use leptos::web_sys::{HtmlAnchorElement, ScrollBehavior};
|
||||
use leptos_icons::Icon;
|
||||
use leptos_oidc::{AuthLoaded, AuthSignal, Authenticated};
|
||||
use leptos_router::components::Outlet;
|
||||
@@ -319,6 +323,14 @@ fn ApplicantsStudentListView(
|
||||
// let (current_student_id, set_current_student_id) = query_signal::<String>("student_id");
|
||||
let current_student_id = RwSignal::new(None);
|
||||
let navigate_student = Callback::new(move |id: String| {
|
||||
let options = web_sys::ScrollToOptions::new();
|
||||
options.set_top(0.0);
|
||||
options.set_behavior(ScrollBehavior::Smooth);
|
||||
|
||||
if let Some(window) = web_sys::window() {
|
||||
window.scroll_to_with_scroll_to_options(&options);
|
||||
}
|
||||
|
||||
current_student_id.set(Some(id));
|
||||
});
|
||||
let dialog_visible =
|
||||
@@ -591,93 +603,172 @@ fn StudentInformationDialog(
|
||||
//#region Schema Definitions
|
||||
|
||||
let demographics_schema = StoredValue::new(
|
||||
SchemaNode::new(SchemaType::Map)
|
||||
SchemaNode::builder()
|
||||
.container(SchemaContainerStyle::Header)
|
||||
.header_style(SchemaHeaderStyle::MainHeader)
|
||||
.header("Demographics Information")
|
||||
.map()
|
||||
.child(
|
||||
"first_name",
|
||||
SchemaNode::new(SchemaType::String).header("First Name"),
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("First Name")
|
||||
.string()
|
||||
.build(),
|
||||
)
|
||||
.child(
|
||||
"last_name",
|
||||
SchemaNode::new(SchemaType::String).header("Last Name"),
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Last Name")
|
||||
.string()
|
||||
.build(),
|
||||
)
|
||||
.child(
|
||||
"dob",
|
||||
SchemaNode::new(SchemaType::String).header("Date of Birth"),
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Date of Birth")
|
||||
.string()
|
||||
.build(),
|
||||
)
|
||||
.child(
|
||||
"gender",
|
||||
SchemaNode::new(SchemaType::String).header("Gender"),
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Gender")
|
||||
.string()
|
||||
.build(),
|
||||
)
|
||||
.child(
|
||||
"phone_number",
|
||||
SchemaNode::new(SchemaType::String).header("Preferred Phone Number"),
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Preferred Phone Number")
|
||||
.string()
|
||||
.build(),
|
||||
)
|
||||
.child(
|
||||
"email",
|
||||
SchemaNode::new(SchemaType::String).header("Preferred Email Address"),
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Preferred Email Address")
|
||||
.string()
|
||||
.build(),
|
||||
)
|
||||
.child(
|
||||
"street_address",
|
||||
SchemaNode::new(SchemaType::String).header("Street Address"),
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Street Address")
|
||||
.string()
|
||||
.build(),
|
||||
)
|
||||
.child("town", SchemaNode::new(SchemaType::String).header("Town")),
|
||||
.child(
|
||||
"town",
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Town")
|
||||
.string()
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let sports_schema = StoredValue::new(
|
||||
SchemaNode::new(SchemaType::List)
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::MainHeader)
|
||||
.header("Sports Activities")
|
||||
.map_list()
|
||||
.primary_key("uuid")
|
||||
.item_template(
|
||||
SchemaNode::new(SchemaType::Map)
|
||||
SchemaNode::builder()
|
||||
.container(SchemaContainerStyle::Capsule)
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Sport")
|
||||
.map()
|
||||
.child(
|
||||
"sport_name",
|
||||
SchemaNode::new(SchemaType::String).header("Sport Name"),
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Sport Name")
|
||||
.string()
|
||||
.build(),
|
||||
)
|
||||
.child(
|
||||
"achievements",
|
||||
SchemaNode::new(SchemaType::String).header("Special Achievements"),
|
||||
),
|
||||
),
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Special Achievements")
|
||||
.string()
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
|
||||
let extracurricular_schema = StoredValue::new(
|
||||
SchemaNode::new(SchemaType::Map)
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::MainHeader)
|
||||
.header("Extracurricular Activities")
|
||||
.map()
|
||||
.child(
|
||||
"service_hours",
|
||||
SchemaNode::new(SchemaType::String).header("Total Number of Service Hours"),
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Total Number of Service Hours")
|
||||
.number()
|
||||
.build(),
|
||||
)
|
||||
.child(
|
||||
"extracurricular",
|
||||
SchemaNode::new(SchemaType::List)
|
||||
.header("List of Activities")
|
||||
SchemaNode::builder()
|
||||
.map_list()
|
||||
.primary_key("uuid")
|
||||
.item_template(
|
||||
SchemaNode::new(SchemaType::Map)
|
||||
SchemaNode::builder()
|
||||
.container(SchemaContainerStyle::Capsule)
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Activity")
|
||||
.map()
|
||||
.child(
|
||||
"activity_name",
|
||||
SchemaNode::new(SchemaType::String).header("Activity Name"),
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Activity Name")
|
||||
.string()
|
||||
.build(),
|
||||
)
|
||||
.child(
|
||||
"num_hours",
|
||||
SchemaNode::new(SchemaType::String)
|
||||
.header("Number of Hours Completed"),
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Approximate Number of Hours Completed")
|
||||
.number()
|
||||
.build(),
|
||||
)
|
||||
.child(
|
||||
"num_weeks",
|
||||
SchemaNode::new(SchemaType::String)
|
||||
.header("Number of Weeks Participated"),
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Approximate Number of Weeks Participated")
|
||||
.number()
|
||||
.build(),
|
||||
)
|
||||
.child(
|
||||
"special_involvement",
|
||||
SchemaNode::new(SchemaType::String).header("Special Involvement"),
|
||||
), // .child(
|
||||
// "grades",
|
||||
// SchemaNode::new(SchemaType::String).header("Grades Participated")
|
||||
// )
|
||||
),
|
||||
),
|
||||
SchemaNode::builder()
|
||||
.header_style(SchemaHeaderStyle::Bold)
|
||||
.header("Special Involvement")
|
||||
.string()
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
)
|
||||
.build(),
|
||||
);
|
||||
|
||||
//#endregion
|
||||
@@ -758,9 +849,6 @@ fn StudentInformationDialog(
|
||||
>
|
||||
<Suspense fallback=Loading>
|
||||
<h1 class="text-2xl font-bold">"Student Information"</h1>
|
||||
<div class="text-lg">
|
||||
"Click on any header to minimize that block of information."
|
||||
</div>
|
||||
<div class="flex flex-row m-1.5">
|
||||
<Show when=move || fafsa_required.get()>
|
||||
<ActionButton on:click=on_click_fafsa disabled=get_buttons_disabled>
|
||||
@@ -781,7 +869,7 @@ fn StudentInformationDialog(
|
||||
.await
|
||||
.map(|student_info| {
|
||||
view! {
|
||||
<ValueDisplay
|
||||
<DataDisplay
|
||||
schema=demographics_schema.get_value()
|
||||
data_map=student_info
|
||||
/>
|
||||
@@ -796,7 +884,7 @@ fn StudentInformationDialog(
|
||||
.await
|
||||
.map(|student_info| {
|
||||
view! {
|
||||
<ValueDisplay
|
||||
<DataDisplay
|
||||
schema=sports_schema.get_value()
|
||||
data_map=student_info
|
||||
data_member="sports_participation"
|
||||
@@ -812,7 +900,7 @@ fn StudentInformationDialog(
|
||||
.await
|
||||
.map(|student_info| {
|
||||
view! {
|
||||
<ValueDisplay
|
||||
<DataDisplay
|
||||
schema=extracurricular_schema.get_value()
|
||||
data_map=student_info
|
||||
/>
|
||||
|
||||
@@ -9,10 +9,9 @@ mod financial_page;
|
||||
mod form_setup;
|
||||
mod home_page;
|
||||
mod shell;
|
||||
mod test_display;
|
||||
mod specific_requirements_page;
|
||||
mod university_page;
|
||||
mod work_experience_page;
|
||||
mod specific_requirements_page;
|
||||
|
||||
pub use academics_page::*;
|
||||
pub use additional_page::*;
|
||||
@@ -24,7 +23,6 @@ pub use family_info_page::*;
|
||||
pub use financial_page::*;
|
||||
pub use home_page::*;
|
||||
pub use shell::*;
|
||||
pub use test_display::*;
|
||||
pub use specific_requirements_page::*;
|
||||
pub use university_page::*;
|
||||
pub use work_experience_page::*;
|
||||
pub use specific_requirements_page::*;
|
||||
@@ -1,77 +0,0 @@
|
||||
use crate::common::{SchemaNode, SchemaType};
|
||||
use crate::components::{Loading, ValueDisplay};
|
||||
use crate::pages::api::students::get_all_student_data;
|
||||
use crate::utils::get_user_claims;
|
||||
use leptos::prelude::*;
|
||||
|
||||
#[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("Sports Activities")
|
||||
.item_template(
|
||||
SchemaNode::new(SchemaType::Map)
|
||||
.header("Sport")
|
||||
.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()));
|
||||
|
||||
let resource = Resource::new(
|
||||
move || user_id.get().unwrap_or_default(),
|
||||
async move |id| get_all_student_data(id).await,
|
||||
);
|
||||
|
||||
let schema = StoredValue::new(create_student_display_schema());
|
||||
|
||||
view! {
|
||||
<div class="flex flex-1" />
|
||||
<Suspense fallback=Loading>
|
||||
<div class="flex flex-2 flex-col">
|
||||
{move || {
|
||||
resource
|
||||
.get()
|
||||
.map(|data_map| {
|
||||
let Ok(map) = data_map else {
|
||||
return view! {}.into_any();
|
||||
};
|
||||
let data_map = StoredValue::new(map);
|
||||
|
||||
view! {
|
||||
<div>
|
||||
<ValueDisplay
|
||||
schema=schema.get_value()
|
||||
data_map=data_map.get_value()
|
||||
/>
|
||||
</div>
|
||||
}
|
||||
.into_any()
|
||||
})
|
||||
}}
|
||||
</div>
|
||||
</Suspense>
|
||||
<div class="flex flex-1" />
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user