.
This commit is contained in:
@@ -1,2 +1,6 @@
|
||||
[build]
|
||||
rustflags = ["--cfg", "tokio_unstable"]
|
||||
[target.x86_64-unknown-linux-gnu]
|
||||
rustflags = [
|
||||
"--codegen", "linker=clang",
|
||||
"--codegen", "link-arg=--ld-path=/usr/bin/mold",
|
||||
"--cfg", "tokio_unstable"
|
||||
]
|
||||
|
||||
242
src/elements/list.rs
Normal file
242
src/elements/list.rs
Normal 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
61
src/elements/mod.rs
Normal 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,
|
||||
}
|
||||
@@ -5,6 +5,7 @@ use std::fmt;
|
||||
pub mod auth;
|
||||
pub mod cli;
|
||||
pub mod components;
|
||||
pub mod elements;
|
||||
pub mod error;
|
||||
pub mod htmx;
|
||||
pub mod models;
|
||||
|
||||
@@ -2,6 +2,14 @@ use maud::{html, Markup};
|
||||
|
||||
use crate::models;
|
||||
use crate::ClientState;
|
||||
use crate::{
|
||||
elements::{
|
||||
self,
|
||||
list::{self, List},
|
||||
},
|
||||
models::inventory::Item,
|
||||
};
|
||||
|
||||
use uuid::Uuid;
|
||||
|
||||
pub struct Inventory;
|
||||
@@ -58,96 +66,64 @@ impl InventoryCategoryList {
|
||||
.max()
|
||||
.unwrap_or(1);
|
||||
|
||||
html!(
|
||||
table
|
||||
#category-list
|
||||
."table"
|
||||
."table-auto"
|
||||
."border-collapse"
|
||||
."border-spacing-0"
|
||||
."border"
|
||||
."w-full"
|
||||
{
|
||||
struct Row<'a> {
|
||||
category: &'a models::inventory::Category,
|
||||
active: bool,
|
||||
biggest_category_weight: i64,
|
||||
}
|
||||
impl<'a> list::Row for Row<'a> {
|
||||
fn is_active(&self) -> bool {
|
||||
self.active
|
||||
}
|
||||
|
||||
colgroup {
|
||||
col style="width:50%" {}
|
||||
col style="width:50%" {}
|
||||
}
|
||||
thead ."bg-gray-200" {
|
||||
tr ."h-10" {
|
||||
th ."border" ."p-2" ."w-3/5" { "Name" }
|
||||
th ."border" ."p-2" { "Weight" }
|
||||
fn cells(&self) -> Vec<list::Cell> {
|
||||
vec![
|
||||
list::Cell {
|
||||
cell_type: list::CellType::Link(list::Link {
|
||||
text: &self.category.name,
|
||||
href: format!("/inventory/category/{}", self.category.id),
|
||||
hx_config: Some(elements::HxConfig {
|
||||
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
|
||||
class=@if active_category.map_or(false, |c| category.id == c.id) {
|
||||
"border p-0 m-0 font-bold"
|
||||
} @else {
|
||||
"border p-0 m-0"
|
||||
} {
|
||||
a
|
||||
id="select-category"
|
||||
href={
|
||||
"/inventory/category/"
|
||||
(category.id) "/"
|
||||
List {
|
||||
id: Some("category-list"),
|
||||
editing_config: None,
|
||||
header: list::Header {
|
||||
cells: vec![
|
||||
Some(list::HeaderCell { title: "Name" }),
|
||||
Some(list::HeaderCell { title: "Weight" }),
|
||||
],
|
||||
},
|
||||
rows: categories
|
||||
.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/"
|
||||
(category.id)
|
||||
"/select"
|
||||
})
|
||||
.collect(),
|
||||
}
|
||||
hx-swap="outerHTML"
|
||||
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())
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
)
|
||||
.render()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -162,6 +138,60 @@ impl InventoryItemList {
|
||||
)]
|
||||
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);
|
||||
|
||||
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!(
|
||||
div #items {
|
||||
@if items.is_empty() {
|
||||
@@ -176,6 +206,7 @@ impl InventoryItemList {
|
||||
method="post"
|
||||
{}
|
||||
}
|
||||
(table.render())
|
||||
table
|
||||
."table"
|
||||
."table-auto"
|
||||
@@ -352,7 +383,6 @@ impl InventoryNewItemFormName {
|
||||
hx-target="this"
|
||||
hx-params="new-item-name"
|
||||
hx-swap="outerHTML"
|
||||
#abc
|
||||
{
|
||||
label for="name" .font-bold { "Name" }
|
||||
input
|
||||
|
||||
Reference in New Issue
Block a user