diff --git a/src/app.rs b/src/app.rs
index bfd5c97..7a27e15 100644
--- a/src/app.rs
+++ b/src/app.rs
@@ -158,7 +158,6 @@ fn StudentRoutes() -> impl MatchNestedRoutes + Clone {
path=path!("additional/specifics")
view=student::StudentSpecificRequirementsPage
/>
-
}
.into_inner()
diff --git a/src/common/schema_node.rs b/src/common/schema_node.rs
index 2e1f943..922b3de 100644
--- a/src/common/schema_node.rs
+++ b/src/common/schema_node.rs
@@ -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,
+ /// The primary key for iteration over maps. Used when there is a list of maps.
+ pub primary_key: Option,
}
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) -> Self {
@@ -86,13 +124,130 @@ impl SchemaNode {
self
}
- pub fn child(mut self, data_member: impl Into, 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>,
+ 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) -> 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,
+}
+
+impl SchemaMapBuilder {
+ fn new(builder: SchemaBuilder) -> Self {
+ Self {
+ builder,
+ ..Default::default()
+ }
+ }
+
+ pub fn child(mut self, data_member: impl Into, 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()
+ }
+ }
}
diff --git a/src/components/data_display.rs b/src/components/data_display.rs
index e9b7d23..26ef721 100644
--- a/src/components/data_display.rs
+++ b/src/components/data_display.rs
@@ -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`. 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,
- /// 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,
+ /// The starting key in the data map.
#[prop(into, optional)]
data_member: Signal,
- #[prop(into)] data_map: Signal>,
+ /// A `HashMap` containing all the data that should be displayed.
+ #[prop(into)]
+ data_map: Signal>,
) -> 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, 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! {
-
- }
- .into_any()
- }
- SchemaType::List => {
- view! {
-
- }
- .into_any()
- }
- SchemaType::Map => {
- view! {
-
- }
- .into_any()
- }
- }
- }}
+
+
+
}
}
-/// # 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,
- #[prop(into, optional)] nested: Signal,
+fn DataDisplayWrapper(
+ #[prop(into)] container_style: SchemaContainerStyle,
+ #[prop(into)] header_style: SchemaHeaderStyle,
+ #[prop(into)] header_text: Signal,
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! {
-
-
- {header}
-
-
-
-
{children()}
+ 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! {
+
+
+
+
{children()}
+
-
-
- }
-}
-
-/// # Primitive Value Display Component
-/// This component allows for the display of a `ValueType::String` or `ValueType::Number`.
-#[component]
-fn PrimitiveDisplay(
- #[prop(into)] header: Signal,
- #[prop(into)] value: Signal,
- #[prop(into, optional)] nested: 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(),
+ }
+ .into_any()
+ }
+ SchemaContainerStyle::Capsule => {
+ // Creates a capsule container - it is collapsable, with rounded corners and a colored
+ // header.
+ view! {
+
+
+
+
+
+
+
{children()}
+
+
+
+ }
+ .into_any()
}
- });
-
- view! {
-
-
{display_string}
-
}
}
-/// # 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>,
- #[prop(into)] value: Signal,
- #[prop(into)] header: Signal,
- #[prop(into, optional)] nested: Signal,
+fn DataDisplayHeader(
+ #[prop(into)] header_style: SchemaHeaderStyle,
+ #[prop(into)] header_text: 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();
- };
+ // 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::>()
- });
-
- let is_valid = Memo::new(move |_| matches!(value.get(), ValueType::List(_)));
-
- let item_header = Memo::new(move |_| item_template.get().header);
-
- view! {
-
- {
- view! { }.into_any()
- }
- SchemaType::List => {
- view! {
-
"Nested lists are not currently supported for display."
}.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,
- #[prop(into)] header: Signal,
- #[prop(into)] schema: Signal>,
- #[prop(into, optional)] nested: Signal,
+fn DataDisplayView(
+ #[prop(into)] display_data: Signal