.
This commit is contained in:
@@ -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
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 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;
|
||||||
|
|||||||
@@ -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 {
|
|
||||||
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" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
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) "/"
|
|
||||||
}
|
|
||||||
hx-post={
|
|
||||||
"/inventory/categories/"
|
|
||||||
(category.id)
|
|
||||||
"/select"
|
|
||||||
}
|
|
||||||
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())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
)
|
|
||||||
|
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,
|
||||||
|
}),
|
||||||
|
},
|
||||||
|
]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
.collect(),
|
||||||
|
}
|
||||||
|
.render()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user