Expose label generation to frontend
The javascript code is quite horrible, but it works ¯\_(ツ)_/¯
This commit is contained in:
parent
c67e31fdc5
commit
d4b0290bd4
31
Cargo.lock
generated
31
Cargo.lock
generated
|
@ -747,6 +747,26 @@ dependencies = [
|
|||
"cfg-if",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator"
|
||||
version = "2.1.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "c280b9e6b3ae19e152d8e31cf47f18389781e119d4013a2a2bb0180e5facc635"
|
||||
dependencies = [
|
||||
"enum-iterator-derive",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "enum-iterator-derive"
|
||||
version = "1.4.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "a1ab991c1362ac86c61ab6f556cff143daa22e5a15e4e189df818b2fd19fe65b"
|
||||
dependencies = [
|
||||
"proc-macro2",
|
||||
"quote",
|
||||
"syn 2.0.68",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "env_filter"
|
||||
version = "0.1.0"
|
||||
|
@ -1172,6 +1192,7 @@ dependencies = [
|
|||
"barcoders",
|
||||
"base64 0.22.1",
|
||||
"datamatrix",
|
||||
"enum-iterator",
|
||||
"env_logger",
|
||||
"futures-util",
|
||||
"log",
|
||||
|
@ -1179,6 +1200,7 @@ dependencies = [
|
|||
"mime",
|
||||
"printpdf",
|
||||
"serde",
|
||||
"serde_variant",
|
||||
"sqlx",
|
||||
"thiserror",
|
||||
"time",
|
||||
|
@ -1813,6 +1835,15 @@ dependencies = [
|
|||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "serde_variant"
|
||||
version = "0.1.3"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "0a0068df419f9d9b6488fdded3f1c818522cdea328e02ce9d9f147380265a432"
|
||||
dependencies = [
|
||||
"serde",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "sha1"
|
||||
version = "0.10.6"
|
||||
|
|
|
@ -17,6 +17,7 @@ actix-web = { version = "4.8.0", features = ["cookies"] }
|
|||
barcoders = { version = "2.0.0", default-features = false, features = ["std"] }
|
||||
base64 = "0.22.1"
|
||||
datamatrix = "0.3.1"
|
||||
enum-iterator = "2.1.0"
|
||||
env_logger = "0.11.3"
|
||||
futures-util = "0.3.30"
|
||||
log = "0.4.21"
|
||||
|
@ -24,6 +25,7 @@ maud = { version = "0.26.0", features = ["actix-web"] }
|
|||
mime = "0.3.17"
|
||||
printpdf = "0.7.0"
|
||||
serde = { version = "1.0.203", features = ["serde_derive"] }
|
||||
serde_variant = "0.1.3"
|
||||
sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "uuid", "time"] }
|
||||
thiserror = "1.0.61"
|
||||
time = { version = "0.3.36", features = ["serde"] }
|
||||
|
|
|
@ -5,6 +5,7 @@
|
|||
[licenses]
|
||||
# Only allow AGPL-3.0-or-later compatible licenses!
|
||||
allow = [
|
||||
"0BSD",
|
||||
"AGPL-3.0",
|
||||
"Apache-2.0",
|
||||
"BSD-3-Clause",
|
||||
|
|
137
src/frontend/labels.rs
Normal file
137
src/frontend/labels.rs
Normal file
|
@ -0,0 +1,137 @@
|
|||
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use actix_identity::Identity;
|
||||
use actix_web::{error, get, post, web, Responder};
|
||||
use maud::html;
|
||||
use serde::Deserialize;
|
||||
use serde_variant::to_variant_name;
|
||||
use sqlx::PgPool;
|
||||
use uuid::Uuid;
|
||||
|
||||
use super::templates::{self, datalist, helpers::ItemName, TemplateConfig};
|
||||
use crate::label::{Label, LabelPreset};
|
||||
use crate::manage;
|
||||
|
||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||
cfg.service(generate_post)
|
||||
.service(generate_get)
|
||||
.service(form);
|
||||
}
|
||||
|
||||
#[derive(Debug, Deserialize)]
|
||||
struct GenerateParams {
|
||||
ids: String,
|
||||
preset: LabelPreset,
|
||||
}
|
||||
|
||||
async fn generate(pool: &PgPool, params: GenerateParams) -> actix_web::Result<impl Responder> {
|
||||
let ids = params
|
||||
.ids
|
||||
.split(',')
|
||||
.skip_while(|s| s.is_empty()) // to make the empty string parse as an empty iterator
|
||||
.map(Uuid::try_parse)
|
||||
.collect::<Result<Vec<Uuid>, uuid::Error>>()
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
let items = manage::item::get_multiple(pool, &ids)
|
||||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
Ok(Label::for_items(&items, params.preset.clone().into()))
|
||||
}
|
||||
|
||||
#[post("/labels/generate")]
|
||||
async fn generate_post(
|
||||
pool: web::Data<PgPool>,
|
||||
_user: Identity,
|
||||
params: web::Form<GenerateParams>,
|
||||
) -> impl Responder {
|
||||
generate(&pool, params.into_inner()).await
|
||||
}
|
||||
|
||||
#[get("/labels/generate")]
|
||||
async fn generate_get(
|
||||
pool: web::Data<PgPool>,
|
||||
_user: Identity,
|
||||
params: web::Query<GenerateParams>,
|
||||
) -> impl Responder {
|
||||
generate(&pool, params.into_inner()).await
|
||||
}
|
||||
|
||||
#[get("/labels")]
|
||||
async fn form(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> {
|
||||
let items = manage::item::get_all(&pool)
|
||||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
let item_classes = manage::item_class::get_all_as_map(&pool)
|
||||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
let datalist_items = datalist::items(&pool)
|
||||
.await
|
||||
.map_err(error::ErrorInternalServerError)?;
|
||||
|
||||
Ok(templates::base(
|
||||
TemplateConfig {
|
||||
path: "/labels",
|
||||
title: Some("Generate Labels"),
|
||||
page_title: Some(Box::new("Generate Labels")),
|
||||
datalists: vec![&datalist_items],
|
||||
user: Some(user),
|
||||
..Default::default()
|
||||
},
|
||||
html! {
|
||||
form #label-form method="POST" action="/labels/generate" {
|
||||
div .mb-3 {
|
||||
label .form-label for="preset" { "Preset" }
|
||||
select #preset name="preset" .form-select {
|
||||
@for preset in enum_iterator::all::<LabelPreset>() {
|
||||
option selected[preset == LabelPreset::default()] value={ (to_variant_name(&preset).unwrap()) } { (preset) }
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
ul .nav.nav-tabs.mb-3 {
|
||||
li .nav-item {
|
||||
button .nav-link.active data-bs-toggle="tab" data-bs-target="#simple-pane" type="button" { "Simple" }
|
||||
}
|
||||
li .nav-item {
|
||||
button .nav-link data-bs-toggle="tab" data-bs-target="#multiselect-pane" type="button" { "Multiselect" }
|
||||
}
|
||||
}
|
||||
|
||||
div .tab-content {
|
||||
div .tab-pane.show.active #simple-pane {
|
||||
div .mb-3 {
|
||||
label .form-label { "Items" }
|
||||
ul #simple-items { }
|
||||
}
|
||||
|
||||
div .input-group.mb-3 {
|
||||
input .form-control #simple-add list="items-datalist" placeholder="Search for Item";
|
||||
button .btn.btn-success #simple-add-button type="button" { "Add" }
|
||||
}
|
||||
}
|
||||
div .tab-pane #multiselect-pane {
|
||||
div .mb-3 {
|
||||
label .form-label for="ids-multiselect" { "Items" }
|
||||
select #ids-multiselect .form-select multiple size="15" {
|
||||
@for item in items {
|
||||
@let item_name = ItemName::new(&item, item_classes.get(&item.class).unwrap());
|
||||
option value={ (item.id) } { (item_name.to_string()) " (" (item.id) ")" }
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
input #ids type="hidden" name="ids";
|
||||
|
||||
button .btn.btn-primary type="submit" { "Generate" }
|
||||
}
|
||||
},
|
||||
))
|
||||
}
|
|
@ -5,6 +5,7 @@
|
|||
mod auth;
|
||||
mod item;
|
||||
mod item_class;
|
||||
mod labels;
|
||||
mod templates;
|
||||
|
||||
use actix_identity::Identity;
|
||||
|
@ -22,7 +23,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
|||
.service(jump)
|
||||
.configure(auth::config)
|
||||
.configure(item::config)
|
||||
.configure(item_class::config);
|
||||
.configure(item_class::config)
|
||||
.configure(labels::config);
|
||||
}
|
||||
|
||||
#[get("/")]
|
||||
|
|
|
@ -18,6 +18,7 @@ const NAVBAR_ITEMS: &[(&str, &str)] = &[
|
|||
("/", "Home"),
|
||||
("/items", "Items"),
|
||||
("/item-classes", "Item Classes"),
|
||||
("/labels", "Labels"),
|
||||
];
|
||||
|
||||
fn navbar(config: &TemplateConfig) -> Markup {
|
||||
|
|
|
@ -2,12 +2,15 @@
|
|||
//
|
||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
|
||||
use std::fmt;
|
||||
|
||||
use enum_iterator::Sequence;
|
||||
use printpdf::Mm;
|
||||
use serde::Deserialize;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
use super::{Code128Config, DataMatrixConfig, LabelConfig, TextConfig};
|
||||
|
||||
#[derive(Clone, Debug, Deserialize)]
|
||||
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Sequence)]
|
||||
#[serde(rename_all = "kebab-case")]
|
||||
pub enum LabelPreset {
|
||||
SeikoSlpMrlDataMatrix,
|
||||
|
@ -15,6 +18,26 @@ pub enum LabelPreset {
|
|||
SeikoSlpMrl,
|
||||
}
|
||||
|
||||
impl fmt::Display for LabelPreset {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
write!(
|
||||
f,
|
||||
"{}",
|
||||
match self {
|
||||
Self::SeikoSlpMrlDataMatrix => "Seiko SLP MRL (Data Matrix only)",
|
||||
Self::SeikoSlpMrlCode128 => "Seiko SLP MRL (Code128 only)",
|
||||
Self::SeikoSlpMrl => "Seiko SLP MRL",
|
||||
}
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for LabelPreset {
|
||||
fn default() -> Self {
|
||||
Self::SeikoSlpMrl
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(clippy::from_over_into)]
|
||||
impl Into<LabelConfig> for LabelPreset {
|
||||
fn into(self) -> LabelConfig {
|
||||
|
|
|
@ -27,4 +27,54 @@
|
|||
datalistHint(input, hint)
|
||||
input.addEventListener("input", _ => datalistHint(input, hint))
|
||||
})
|
||||
|
||||
// there probably is a better way
|
||||
if (document.location.pathname === "/labels") {
|
||||
const multiselect = document.getElementById("ids-multiselect")
|
||||
const updateSelectedItemList = _ => {
|
||||
const list = document.getElementById("simple-items")
|
||||
list.innerHTML = ""
|
||||
Array.from(multiselect.selectedOptions)
|
||||
.map(option => {
|
||||
let li = document.createElement("li")
|
||||
let a = document.createElement("a")
|
||||
a.innerHTML = document.getElementById("items-datalist").querySelector(`option[value="${option.value}"]`).innerHTML
|
||||
a.href = `/item/${option.value}`
|
||||
li.appendChild(a)
|
||||
let remove = document.createElement("a")
|
||||
remove.classList = "btn btn-sm btn-danger ms-2 my-1 simple-items-remove"
|
||||
remove.dataset.id = option.value
|
||||
remove.innerText = "Remove"
|
||||
remove.addEventListener("click", e => {
|
||||
multiselect.querySelector(`option[value="${option.value}"]`).selected = false
|
||||
updateSelectedItemList()
|
||||
})
|
||||
li.appendChild(remove)
|
||||
return li
|
||||
})
|
||||
.forEach(li => list.appendChild(li))
|
||||
}
|
||||
updateSelectedItemList()
|
||||
addEventListener("input", _ => updateSelectedItemList())
|
||||
|
||||
const addFromSimple = _ => {
|
||||
const inputEl = document.getElementById("simple-add")
|
||||
document.getElementById("ids-multiselect").querySelector(`option[value="${inputEl.value}"]`).selected = true
|
||||
inputEl.value = ""
|
||||
updateSelectedItemList()
|
||||
}
|
||||
document.getElementById("simple-add-button").addEventListener("click", addFromSimple)
|
||||
document.getElementById("simple-add").addEventListener("keydown", e => {
|
||||
if (e.key === "Enter") {
|
||||
e.preventDefault() // do not submit form
|
||||
addFromSimple()
|
||||
}
|
||||
})
|
||||
|
||||
document.getElementById("label-form").addEventListener("submit", e => {
|
||||
document.getElementById("ids").value = Array.from(multiselect.selectedOptions)
|
||||
.map(option => option.value)
|
||||
.join(",")
|
||||
})
|
||||
}
|
||||
})()
|
||||
|
|
Loading…
Reference in a new issue