This commit is contained in:
2024-04-29 20:38:17 +02:00
parent 84fdf64793
commit 0949b1452c
5 changed files with 430 additions and 92 deletions

View File

@@ -1,2 +1,6 @@
[build] [target.x86_64-unknown-linux-gnu]
rustflags = ["--cfg", "tokio_unstable"] rustflags = [
"--codegen", "linker=clang",
"--codegen", "link-arg=--ld-path=/usr/bin/mold",
"--cfg", "tokio_unstable"
]

242
src/elements/list.rs Normal file
View File

@@ -0,0 +1,242 @@
use maud::{html, Markup};
use super::HxConfig;
pub struct Link<'a> {
pub text: &'a str,
pub href: String,
pub hx_config: Option<HxConfig>,
}
pub struct NumberWithBar {
pub value: i64,
pub max_value: i64,
}
pub struct Icon {
pub icon: super::Icon,
pub href: String,
pub hx_config: Option<HxConfig>,
}
pub enum CellType<'a> {
Text(&'a str),
Link(Link<'a>),
NumberWithBar(NumberWithBar),
Icon(Icon),
}
pub struct Cell<'a> {
pub cell_type: CellType<'a>,
}
impl<'a> Cell<'a> {
fn render(self) -> Markup {
match self.cell_type {
CellType::Text(text) => html!(
td
."border"
."p-0"
."m-0"
{
p { (text) }
}
),
CellType::Link(link) => {
let (hx_post, hx_swap, hx_target) = if let Some(hx_config) = link.hx_config {
(
Some(hx_config.hx_post),
Some(hx_config.hx_swap),
Some(hx_config.hx_target),
)
} else {
(None, None, None)
};
html!(
td
."border"
."p-0"
."m-0"
{
a
."inline-block"
."p-2"
."m-0"
."w-full"
href=(link.href)
hx-post=[hx_post]
hx-swap=[hx_swap]
hx-target=[hx_target]
{
(link.text)
}
}
)
}
CellType::NumberWithBar(number) => html!(
td
."border"
."p-2"
."m-0"
style="position:relative;"
{
p {
(number.value)
}
div ."bg-blue-600" ."h-1.5"
style=(
format!(
"width: {width}%;position:absolute;left:0;bottom:0;right:0;",
width=(
(number.value as f64)
/ (number.max_value as f64)
* 100.0
)
)
)
{}
}
),
CellType::Icon(icon) => html!(
td
."border-none"
."p-0"
.(icon.icon.background())
.(icon.icon.background_hover())
."h-full"
."w-10"
{
a
href=(icon.href)
."aspect-square"
."flex"
{
span
."m-auto"
."mdi"
."text-xl"
.(icon.icon.mdi_class())
{}
}
}
),
}
}
}
pub struct EditingConfig {
pub edit_href: String,
pub edit_hx_config: Option<HxConfig>,
pub delete_href: String,
pub delete_hx_config: Option<HxConfig>,
}
pub trait Row {
fn is_active(&self) -> bool {
false
}
fn is_edit(&self) -> bool {
false
}
fn editing_config(&self) -> Option<EditingConfig> {
None
}
fn cells(&self) -> Vec<Cell>;
}
pub struct Header<'c> {
pub cells: Vec<Option<HeaderCell<'c>>>,
}
pub struct HeaderCell<'a> {
pub title: &'a str,
}
impl<'c> HeaderCell<'c> {
fn title(&self) -> &str {
&self.title
}
}
pub struct List<'hc, R>
where
R: Row,
{
pub id: Option<&'static str>,
pub header: Header<'hc>,
pub rows: Vec<R>,
pub editing_config: Option<Box<dyn Fn(R) -> EditingConfig>>,
}
impl<'hc, R> List<'hc, R>
where
R: Row,
{
pub fn render(self) -> Markup {
html!(
table
id=[self.id]
."table"
."table-auto"
."border-collapse"
."border-spacing-0"
."border"
."w-full"
{
thead ."bg-gray-200" {
tr
."h-10"
{
@for header_cell in self.header.cells.iter() {
th ."border" ."p-2" { (header_cell.as_ref().map_or("", |c| c.title())) }
}
@if self.editing_config.is_some() {
th {}
th {}
}
}
}
tbody {
@for row in self.rows.into_iter() {
@let active = row.is_active();
tr
."h-10"
."hover:bg-gray-100"
."outline"[active]
."outline-2"[active]
."outline-indigo-300"[active]
."pointer-events-none"[active]
."font-bold"[active]
{
@for cell in row.cells() {
(cell.render())
}
}
@if let Some(ref edit_config) = self.editing_config {
@let edit_config = (*edit_config)(row);
(Cell {
cell_type: CellType::Icon(Icon {
icon: super::Icon::Edit,
href: edit_config.edit_href,
hx_config: edit_config.edit_hx_config,
}),
}.render())
(Cell {
cell_type: CellType::Icon(Icon {
icon: super::Icon::Delete,
href: edit_config.delete_href,
hx_config: edit_config.delete_hx_config,
}),
}.render())
}
}
}
}
)
}
}

61
src/elements/mod.rs Normal file
View File

@@ -0,0 +1,61 @@
use std::fmt::Display;
pub mod list;
pub enum HxSwap {
OuterHtml,
}
pub enum Icon {
Edit,
Delete,
Save,
Cancel,
}
impl Icon {
pub fn mdi_class(&self) -> &'static str {
match self {
Icon::Edit => "mdi-pencil",
Icon::Delete => "mdi-delete",
Icon::Save => "mdi-content-save",
Icon::Cancel => "mdi-cancel",
}
}
pub fn background(&self) -> &'static str {
match self {
Icon::Edit => "bg-blue-200",
Icon::Delete => "bg-red-200",
Icon::Save => "bg-green-100",
Icon::Cancel => "bg-red-100",
}
}
pub fn background_hover(&self) -> &'static str {
match self {
Icon::Edit => "hover:bg-blue-400",
Icon::Delete => "hover:bg-red-400",
Icon::Save => "hover:bg-green-200",
Icon::Cancel => "hover:bg-red-200",
}
}
}
impl Display for HxSwap {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(
f,
"{}",
match self {
HxSwap::OuterHtml => "outerHtml",
}
)
}
}
pub struct HxConfig {
pub hx_post: String,
pub hx_swap: HxSwap,
pub hx_target: &'static str,
}

View File

@@ -5,6 +5,7 @@ use std::fmt;
pub mod auth; pub mod auth;
pub mod cli; pub mod cli;
pub mod components; pub mod components;
pub mod elements;
pub mod error; pub mod error;
pub mod htmx; pub mod htmx;
pub mod models; pub mod models;

View File

@@ -2,6 +2,14 @@ use maud::{html, Markup};
use crate::models; use crate::models;
use crate::ClientState; use crate::ClientState;
use crate::{
elements::{
self,
list::{self, List},
},
models::inventory::Item,
};
use uuid::Uuid; use uuid::Uuid;
pub struct Inventory; pub struct Inventory;
@@ -58,96 +66,64 @@ impl InventoryCategoryList {
.max() .max()
.unwrap_or(1); .unwrap_or(1);
html!( struct Row<'a> {
table category: &'a models::inventory::Category,
#category-list active: bool,
."table" biggest_category_weight: i64,
."table-auto" }
."border-collapse" impl<'a> list::Row for Row<'a> {
."border-spacing-0" fn is_active(&self) -> bool {
."border" self.active
."w-full" }
{
colgroup { fn cells(&self) -> Vec<list::Cell> {
col style="width:50%" {} vec![
col style="width:50%" {} list::Cell {
} cell_type: list::CellType::Link(list::Link {
thead ."bg-gray-200" { text: &self.category.name,
tr ."h-10" { href: format!("/inventory/category/{}", self.category.id),
th ."border" ."p-2" ."w-3/5" { "Name" } hx_config: Some(elements::HxConfig {
th ."border" ."p-2" { "Weight" } hx_post: format!(
"/inventory/categories/{}/select",
self.category.id
),
hx_swap: elements::HxSwap::OuterHtml,
hx_target: "#pkglist-item-manager",
}),
}),
},
list::Cell {
cell_type: list::CellType::NumberWithBar(list::NumberWithBar {
value: self.category.total_weight(),
max_value: self.biggest_category_weight,
}),
},
]
} }
} }
tbody {
@for category in categories {
@let active = active_category.map_or(false, |c| category.id == c.id);
tr
."h-10"
."hover:bg-gray-100"
."m-3"
."h-full"
."outline"[active]
."outline-2"[active]
."outline-indigo-300"[active]
."pointer-events-none"[active]
{
td List {
class=@if active_category.map_or(false, |c| category.id == c.id) { id: Some("category-list"),
"border p-0 m-0 font-bold" editing_config: None,
} @else { header: list::Header {
"border p-0 m-0" cells: vec![
} { Some(list::HeaderCell { title: "Name" }),
a Some(list::HeaderCell { title: "Weight" }),
id="select-category" ],
href={ },
"/inventory/category/" rows: categories
(category.id) "/" .iter()
.map(|category| {
let active = active_category.map_or(false, |c| category.id == c.id);
Row {
category,
active,
biggest_category_weight,
} }
hx-post={ })
"/inventory/categories/" .collect(),
(category.id)
"/select"
} }
hx-swap="outerHTML" .render()
hx-target="#pkglist-item-manager"
."inline-block" ."p-2" ."m-0" ."w-full"
{
(category.name.clone())
}
}
td ."border" ."p-2" ."m-0" style="position:relative;" {
p {
(category.total_weight().to_string())
}
div ."bg-blue-600" ."h-1.5"
style=(
format!(
"width: {width}%;position:absolute;left:0;bottom:0;right:0;",
width=(
(category.total_weight() as f64)
/ (biggest_category_weight as f64)
* 100.0
)
)
) {}
}
}
}
tr ."h-10" ."bg-gray-300" ."font-bold" {
td ."border" ."p-0" ."m-0" {
p ."p-2" ."m-2" { "Sum" }
}
td ."border" ."p-0" ."m-0" {
p ."p-2" ."m-2" {
(categories.iter().map(models::inventory::Category::total_weight).sum::<i64>().to_string())
}
}
}
}
}
)
} }
} }
@@ -162,6 +138,60 @@ impl InventoryItemList {
)] )]
pub fn build(edit_item_id: Option<Uuid>, items: &Vec<models::inventory::Item>) -> Markup { pub fn build(edit_item_id: Option<Uuid>, items: &Vec<models::inventory::Item>) -> Markup {
let biggest_item_weight: i64 = items.iter().map(|item| item.weight).max().unwrap_or(1); let biggest_item_weight: i64 = items.iter().map(|item| item.weight).max().unwrap_or(1);
struct Row<'a> {
item: &'a Item,
biggest_item_weight: i64,
}
impl<'a> list::Row for Row<'a> {
fn cells(&self) -> Vec<list::Cell> {
vec![
list::Cell {
cell_type: list::CellType::Link(list::Link {
text: &self.item.name,
href: format!("/inventory/item/{id}/", id = self.item.id),
hx_config: None,
}),
},
list::Cell {
cell_type: list::CellType::NumberWithBar(list::NumberWithBar {
value: self.item.weight,
max_value: self.biggest_item_weight,
}),
},
]
}
}
fn editing_config(row: Row) -> list::EditingConfig {
list::EditingConfig {
edit_href: format!("?edit_item={id}", id = row.item.id),
edit_hx_config: None,
delete_href: format!("/inventory/item/{id}/delete", id = row.item.id),
delete_hx_config: None,
}
}
let table = list::List {
id: None,
editing_config: Some(Box::new(editing_config)),
header: list::Header {
cells: vec![
Some(list::HeaderCell { title: "Name" }),
Some(list::HeaderCell { title: "Weight" }),
],
},
rows: items
.iter()
.map(|item| Row {
item,
biggest_item_weight,
})
.collect(),
};
html!( html!(
div #items { div #items {
@if items.is_empty() { @if items.is_empty() {
@@ -176,6 +206,7 @@ impl InventoryItemList {
method="post" method="post"
{} {}
} }
(table.render())
table table
."table" ."table"
."table-auto" ."table-auto"
@@ -352,7 +383,6 @@ impl InventoryNewItemFormName {
hx-target="this" hx-target="this"
hx-params="new-item-name" hx-params="new-item-name"
hx-swap="outerHTML" hx-swap="outerHTML"
#abc
{ {
label for="name" .font-bold { "Name" } label for="name" .font-bold { "Name" }
input input