diff --git a/src/frontend/item.rs b/src/frontend/item.rs index af6e49f..dce2af7 100644 --- a/src/frontend/item.rs +++ b/src/frontend/item.rs @@ -9,7 +9,7 @@ use maud::html; use sqlx::PgPool; use uuid::Uuid; -use super::templates::{self, forms, TemplateConfig}; +use super::templates::{self, datalist, forms, TemplateConfig}; use crate::manage; use crate::models::*; @@ -181,12 +181,24 @@ async fn list_items(pool: web::Data) -> actix_web::Result) -> actix_web::Result { +async fn add_item( + pool: web::Data, + form: web::Query, +) -> actix_web::Result { + let datalist_items = datalist::items(&pool) + .await + .map_err(error::ErrorInternalServerError)?; + + let datalist_item_classes = datalist::item_classes(&pool) + .await + .map_err(error::ErrorInternalServerError)?; + Ok(templates::base( TemplateConfig { path: "/items/add", title: Some("Add Item"), page_title: Some(Box::new("Add Item")), + datalists: vec![&datalist_items, &datalist_item_classes], ..Default::default() }, html! { @@ -204,6 +216,7 @@ async fn add_item(form: web::Query) -> actix_web::Result) -> actix_web::Result) -> actix_web::Result) -> actix_web::Result { +async fn add_item_class( + pool: web::Data, + form: web::Query, +) -> actix_web::Result { + let datalist_item_classes = datalist::item_classes(&pool) + .await + .map_err(error::ErrorInternalServerError)?; + Ok(templates::base( TemplateConfig { path: "/items-classes/add", title: Some("Add Item Class"), page_title: Some(Box::new("Add Item Class")), + datalists: vec![&datalist_item_classes], ..Default::default() }, html! { @@ -183,6 +191,7 @@ async fn add_item_class(form: web::Query) -> actix_web::Result title: "Parent", disabled: form.parent.is_none(), value: form.parent.map(|id| id.to_string()), + datalist: Some(&datalist_item_classes), ..Default::default() }) @@ -214,6 +223,10 @@ async fn edit_item_class( .await .map_err(error::ErrorInternalServerError)?; + let datalist_item_classes = datalist::item_classes(&pool) + .await + .map_err(error::ErrorInternalServerError)?; + let mut title = item_class.name.clone(); title.push_str(" – Item Details"); @@ -222,6 +235,7 @@ async fn edit_item_class( path: &format!("/items-class/{}/add", id), title: Some(&title), page_title: Some(Box::new(item_class.name.clone())), + datalists: vec![&datalist_item_classes], ..Default::default() }, html! { @@ -233,6 +247,7 @@ async fn edit_item_class( disabled: true, required: true, value: Some(item_class.id.to_string()), + ..Default::default() }) (forms::InputGroup { r#type: forms::InputType::Text, @@ -248,6 +263,7 @@ async fn edit_item_class( title: "Parent", disabled: item_class.parent.is_none(), value: item_class.parent.map(|id| id.to_string()), + datalist: Some(&datalist_item_classes), ..Default::default() }) diff --git a/src/frontend/templates/datalist.rs b/src/frontend/templates/datalist.rs new file mode 100644 index 0000000..5fef75c --- /dev/null +++ b/src/frontend/templates/datalist.rs @@ -0,0 +1,74 @@ +// SPDX-FileCopyrightText: 2024 Simon Bruder +// +// SPDX-License-Identifier: AGPL-3.0-or-later + +use maud::{html, Markup, Render}; +use sqlx::PgPool; + +use super::helpers::ItemName; +use crate::manage; + +pub struct Datalist { + name: String, + options: Vec, +} + +impl Datalist { + pub fn name(&self) -> &str { + &self.name + } +} + +impl Render for Datalist { + fn render(&self) -> Markup { + html! { + datalist #{ (self.name) "-datalist" } { + @for option in &self.options { + (option) + } + } + } + } +} + +pub struct DatalistOption { + value: String, + text: Box, +} + +impl Render for DatalistOption { + fn render(&self) -> Markup { + html! { option value=(self.value) { (self.text) } } + } +} + +pub async fn items(pool: &PgPool) -> Result { + let items = manage::item::get_all(pool).await?; + + let item_classes = manage::item_class::get_all_as_map(pool).await?; + + Ok(Datalist { + name: "items".to_string(), + options: items + .iter() + .map(|i| DatalistOption { + value: i.id.to_string(), + text: Box::new(ItemName::new(i, item_classes.get(&i.class).unwrap())), + }) + .collect(), + }) +} + +pub async fn item_classes(pool: &PgPool) -> Result { + Ok(Datalist { + name: "item-classes".to_string(), + options: manage::item_class::get_all(pool) + .await? + .into_iter() + .map(|ic| DatalistOption { + value: ic.id.to_string(), + text: Box::new(ic.name), + }) + .collect(), + }) +} diff --git a/src/frontend/templates/forms.rs b/src/frontend/templates/forms.rs index a5c3ac4..2e3d76f 100644 --- a/src/frontend/templates/forms.rs +++ b/src/frontend/templates/forms.rs @@ -6,6 +6,8 @@ use std::fmt; use maud::{html, Markup, Render}; +use super::datalist::Datalist; + #[derive(Clone)] pub enum InputType { Text, @@ -26,6 +28,7 @@ pub struct InputGroup<'a> { pub required: bool, pub disabled: bool, pub value: Option, + pub datalist: Option<&'a Datalist>, } impl Default for InputGroup<'_> { @@ -37,6 +40,7 @@ impl Default for InputGroup<'_> { required: false, disabled: false, value: None, + datalist: None, } } } @@ -44,7 +48,15 @@ impl Default for InputGroup<'_> { impl InputGroup<'_> { fn main_input(&self, force_required: bool) -> Markup { html! { - input .form-control #(self.name) name={ (self.name) } type={ (self.r#type) } required[self.required || force_required] disabled[self.disabled] value=[self.value.clone()]; + input + .form-control + #(self.name) + name={ (self.name) } + type={ (self.r#type) } + required[self.required || force_required] + disabled[self.disabled] + value=[self.value.clone()] + list=[self.datalist.map(|dl| format!("{}-datalist", dl.name()))]; } } } @@ -64,6 +76,9 @@ impl Render for InputGroup<'_> { (self.main_input(true)) } } + @if self.datalist.is_some() { + div .form-text.datalist-hint #{ (self.name) "-datalist-hint" } { } + } } } } diff --git a/src/frontend/templates/mod.rs b/src/frontend/templates/mod.rs index e9cf786..9b8f5f7 100644 --- a/src/frontend/templates/mod.rs +++ b/src/frontend/templates/mod.rs @@ -2,11 +2,13 @@ // // SPDX-License-Identifier: AGPL-3.0-or-later +pub mod datalist; pub mod forms; pub mod helpers; use maud::{html, Markup, Render, DOCTYPE}; +use datalist::Datalist; use helpers::*; const BRANDING: &str = "li7y"; @@ -56,6 +58,7 @@ pub struct TemplateConfig<'a> { pub page_actions: Vec, pub extra_css: Vec>, pub extra_js: Vec>, + pub datalists: Vec<&'a Datalist>, } impl Default for TemplateConfig<'_> { @@ -67,6 +70,7 @@ impl Default for TemplateConfig<'_> { page_actions: Vec::new(), extra_css: Vec::new(), extra_js: Vec::new(), + datalists: Vec::new(), } } } @@ -116,6 +120,10 @@ pub fn base(config: TemplateConfig, content: Markup) -> Markup { (footer()) + @for datalist in config.datalists { + (datalist) + } + (Js::File("/static/vendor/bootstrap.bundle.min.js")) (Js::File("/static/app.js")) // TODO this is not the best way, but it works for now diff --git a/static/app.js b/static/app.js index 3b233c3..422b04f 100644 --- a/static/app.js +++ b/static/app.js @@ -13,4 +13,18 @@ inputToggle(el) el.addEventListener("change", e => inputToggle(e.target)) }) + + const datalistHint = (input, hint) => { + const selected = input.list.querySelector(`option[value="${input.value}"]`); + if (selected === null) + hint.innerText = "" + else { + hint.innerHTML = selected.innerHTML + } + } + Array.from(document.getElementsByClassName("datalist-hint")).forEach(hint => { + const input = hint.parentElement.querySelector("input[list]") + datalistHint(input, hint) + input.addEventListener("input", _ => datalistHint(input, hint)) + }) })()