Expose label generation to frontend
The javascript code is quite horrible, but it works ¯\_(ツ)_/¯
This commit is contained in:
parent
5a80201850
commit
13ce4d0611
31
Cargo.lock
generated
31
Cargo.lock
generated
|
@ -747,6 +747,26 @@ dependencies = [
|
||||||
"cfg-if",
|
"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]]
|
[[package]]
|
||||||
name = "env_filter"
|
name = "env_filter"
|
||||||
version = "0.1.0"
|
version = "0.1.0"
|
||||||
|
@ -1172,6 +1192,7 @@ dependencies = [
|
||||||
"barcoders",
|
"barcoders",
|
||||||
"base64 0.22.1",
|
"base64 0.22.1",
|
||||||
"datamatrix",
|
"datamatrix",
|
||||||
|
"enum-iterator",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
"futures-util",
|
"futures-util",
|
||||||
"log",
|
"log",
|
||||||
|
@ -1179,6 +1200,7 @@ dependencies = [
|
||||||
"mime",
|
"mime",
|
||||||
"printpdf",
|
"printpdf",
|
||||||
"serde",
|
"serde",
|
||||||
|
"serde_variant",
|
||||||
"sqlx",
|
"sqlx",
|
||||||
"thiserror",
|
"thiserror",
|
||||||
"time",
|
"time",
|
||||||
|
@ -1813,6 +1835,15 @@ dependencies = [
|
||||||
"serde",
|
"serde",
|
||||||
]
|
]
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "serde_variant"
|
||||||
|
version = "0.1.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "0a0068df419f9d9b6488fdded3f1c818522cdea328e02ce9d9f147380265a432"
|
||||||
|
dependencies = [
|
||||||
|
"serde",
|
||||||
|
]
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "sha1"
|
name = "sha1"
|
||||||
version = "0.10.6"
|
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"] }
|
barcoders = { version = "2.0.0", default-features = false, features = ["std"] }
|
||||||
base64 = "0.22.1"
|
base64 = "0.22.1"
|
||||||
datamatrix = "0.3.1"
|
datamatrix = "0.3.1"
|
||||||
|
enum-iterator = "2.1.0"
|
||||||
env_logger = "0.11.3"
|
env_logger = "0.11.3"
|
||||||
futures-util = "0.3.30"
|
futures-util = "0.3.30"
|
||||||
log = "0.4.21"
|
log = "0.4.21"
|
||||||
|
@ -24,6 +25,7 @@ maud = { version = "0.26.0", features = ["actix-web"] }
|
||||||
mime = "0.3.17"
|
mime = "0.3.17"
|
||||||
printpdf = "0.7.0"
|
printpdf = "0.7.0"
|
||||||
serde = { version = "1.0.203", features = ["serde_derive"] }
|
serde = { version = "1.0.203", features = ["serde_derive"] }
|
||||||
|
serde_variant = "0.1.3"
|
||||||
sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "uuid", "time"] }
|
sqlx = { version = "0.7.4", features = ["runtime-tokio", "postgres", "uuid", "time"] }
|
||||||
thiserror = "1.0.61"
|
thiserror = "1.0.61"
|
||||||
time = { version = "0.3.36", features = ["serde"] }
|
time = { version = "0.3.36", features = ["serde"] }
|
||||||
|
|
|
@ -5,6 +5,7 @@
|
||||||
[licenses]
|
[licenses]
|
||||||
# Only allow AGPL-3.0-or-later compatible licenses!
|
# Only allow AGPL-3.0-or-later compatible licenses!
|
||||||
allow = [
|
allow = [
|
||||||
|
"0BSD",
|
||||||
"AGPL-3.0",
|
"AGPL-3.0",
|
||||||
"Apache-2.0",
|
"Apache-2.0",
|
||||||
"BSD-3-Clause",
|
"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 auth;
|
||||||
mod item;
|
mod item;
|
||||||
mod item_class;
|
mod item_class;
|
||||||
|
mod labels;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
|
@ -22,7 +23,8 @@ pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
.service(jump)
|
.service(jump)
|
||||||
.configure(auth::config)
|
.configure(auth::config)
|
||||||
.configure(item::config)
|
.configure(item::config)
|
||||||
.configure(item_class::config);
|
.configure(item_class::config)
|
||||||
|
.configure(labels::config);
|
||||||
}
|
}
|
||||||
|
|
||||||
#[get("/")]
|
#[get("/")]
|
||||||
|
|
|
@ -2,12 +2,15 @@
|
||||||
//
|
//
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::fmt;
|
||||||
|
|
||||||
|
use enum_iterator::Sequence;
|
||||||
use printpdf::Mm;
|
use printpdf::Mm;
|
||||||
use serde::Deserialize;
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use super::{Code128Config, DataMatrixConfig, LabelConfig, TextConfig};
|
use super::{Code128Config, DataMatrixConfig, LabelConfig, TextConfig};
|
||||||
|
|
||||||
#[derive(Clone, Debug, Deserialize)]
|
#[derive(Clone, Debug, Deserialize, PartialEq, Serialize, Sequence)]
|
||||||
#[serde(rename_all = "kebab-case")]
|
#[serde(rename_all = "kebab-case")]
|
||||||
pub enum LabelPreset {
|
pub enum LabelPreset {
|
||||||
SeikoSlpMrlDataMatrix,
|
SeikoSlpMrlDataMatrix,
|
||||||
|
@ -15,6 +18,26 @@ pub enum LabelPreset {
|
||||||
SeikoSlpMrl,
|
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)]
|
#[allow(clippy::from_over_into)]
|
||||||
impl Into<LabelConfig> for LabelPreset {
|
impl Into<LabelConfig> for LabelPreset {
|
||||||
fn into(self) -> LabelConfig {
|
fn into(self) -> LabelConfig {
|
||||||
|
|
|
@ -27,4 +27,54 @@
|
||||||
datalistHint(input, hint)
|
datalistHint(input, hint)
|
||||||
input.addEventListener("input", _ => 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