Expose label generation to frontend

The javascript code is quite horrible, but it works ¯\_(ツ)_/¯
This commit is contained in:
Simon Bruder 2024-07-14 01:28:56 +02:00
parent c67e31fdc5
commit d4b0290bd4
Signed by: simon
GPG key ID: 347FF8699CDA0776
8 changed files with 250 additions and 3 deletions

31
Cargo.lock generated
View file

@ -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"

View file

@ -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"] }

View file

@ -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
View 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" }
}
},
))
}

View file

@ -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("/")]

View file

@ -18,6 +18,7 @@ const NAVBAR_ITEMS: &[(&str, &str)] = &[
("/", "Home"),
("/items", "Items"),
("/item-classes", "Item Classes"),
("/labels", "Labels"),
];
fn navbar(config: &TemplateConfig) -> Markup {

View file

@ -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 {

View file

@ -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(",")
})
}
})()