Split frontend files
This commit is contained in:
parent
d6a0f0a9ff
commit
f4202a1ed5
|
@ -1,570 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
use std::collections::HashMap;
|
|
||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
use actix_identity::Identity;
|
|
||||||
use actix_web::{error, get, post, web, HttpRequest, Responder};
|
|
||||||
use maud::html;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use sqlx::PgPool;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use super::templates::helpers::{
|
|
||||||
Colour, ItemName, ItemPreview, PageAction, PageActionGroup, PageActionMethod,
|
|
||||||
};
|
|
||||||
use super::templates::{self, datalist, forms, TemplateConfig};
|
|
||||||
use crate::manage;
|
|
||||||
use crate::models::*;
|
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
|
||||||
cfg.service(show)
|
|
||||||
.service(list)
|
|
||||||
.service(add_form)
|
|
||||||
.service(add_post)
|
|
||||||
.service(edit_form)
|
|
||||||
.service(edit)
|
|
||||||
.service(delete);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item/{id}")]
|
|
||||||
async fn show(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
path: web::Path<Uuid>,
|
|
||||||
user: Identity,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let id = path.into_inner();
|
|
||||||
|
|
||||||
let item = manage::item::get(&pool, id)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let item_classes = manage::item_class::get_all_as_map(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let parents = manage::item::get_parents_details(&pool, item.id)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let children = manage::item::get_children(&pool, item.id)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let original_packaging = match item.original_packaging {
|
|
||||||
Some(id) => Some(
|
|
||||||
manage::item::get(&pool, id)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?,
|
|
||||||
),
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let original_packaging_of = manage::item::original_packaging_contents(&pool, id)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let item_class = item_classes.get(&item.class).unwrap();
|
|
||||||
|
|
||||||
let item_name = ItemName::new(item.name.as_ref(), &item_class.name);
|
|
||||||
let mut title = item_name.to_string();
|
|
||||||
title.push_str(" – Item Details");
|
|
||||||
|
|
||||||
Ok(templates::base(
|
|
||||||
TemplateConfig {
|
|
||||||
path: &format!("/item/{}", item.id),
|
|
||||||
title: Some(&title),
|
|
||||||
page_title: Some(Box::new(item_name.clone())),
|
|
||||||
page_actions: vec![
|
|
||||||
(PageActionGroup::Button {
|
|
||||||
action: PageAction {
|
|
||||||
method: PageActionMethod::Get,
|
|
||||||
target: format!("/items/add?parent={}", item.id),
|
|
||||||
name: "Add Child".to_string(),
|
|
||||||
},
|
|
||||||
colour: Colour::Success,
|
|
||||||
}),
|
|
||||||
PageActionGroup::generate_labels(&[item.id]),
|
|
||||||
(PageActionGroup::Button {
|
|
||||||
action: PageAction {
|
|
||||||
method: PageActionMethod::Get,
|
|
||||||
target: format!("/item/{}/edit", item.id),
|
|
||||||
name: "Edit".to_string(),
|
|
||||||
},
|
|
||||||
colour: Colour::Warning,
|
|
||||||
}),
|
|
||||||
(PageActionGroup::Button {
|
|
||||||
action: PageAction {
|
|
||||||
method: PageActionMethod::Post,
|
|
||||||
target: format!("/item/{}/delete", item.id),
|
|
||||||
name: "Delete".to_string(),
|
|
||||||
},
|
|
||||||
colour: Colour::Danger,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
user: Some(user),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
html! {
|
|
||||||
table .table {
|
|
||||||
tr {
|
|
||||||
th { "UUID" }
|
|
||||||
td { (item.id) }
|
|
||||||
}
|
|
||||||
tr {
|
|
||||||
th { "Short ID" }
|
|
||||||
td { (item.short_id) }
|
|
||||||
}
|
|
||||||
tr {
|
|
||||||
th { "Name" }
|
|
||||||
td { (item_name.clone().terse()) }
|
|
||||||
}
|
|
||||||
tr {
|
|
||||||
th { "Class" }
|
|
||||||
td { a href={ "/item-class/" (item.class) } { (item_class.name) } }
|
|
||||||
}
|
|
||||||
tr {
|
|
||||||
th { "Parents" }
|
|
||||||
td {
|
|
||||||
(templates::helpers::parents_breadcrumb(
|
|
||||||
ItemName::new(
|
|
||||||
item.name.as_ref(),
|
|
||||||
&item_class.name
|
|
||||||
),
|
|
||||||
&parents.iter().map(|parent| ItemPreview::from_parts(parent.id, parent.name.as_ref(), &item_classes.get(&parent.class).unwrap().name)).collect::<Vec<ItemPreview>>(),
|
|
||||||
true
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tr {
|
|
||||||
th { "Original Packaging" }
|
|
||||||
td {
|
|
||||||
@if let Some(original_packaging) = original_packaging {
|
|
||||||
a
|
|
||||||
href={ "/item/" (original_packaging.id) }
|
|
||||||
{ (ItemName::new(original_packaging.name.as_ref(), &item_classes.get(&original_packaging.class).unwrap().name)) }
|
|
||||||
} @else {
|
|
||||||
"-"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tr {
|
|
||||||
th { "Description" }
|
|
||||||
td style="white-space: pre-wrap" { (item.description) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@if !children.is_empty() {
|
|
||||||
div .d-flex.justify-content-between.mt-4 {
|
|
||||||
div {
|
|
||||||
h3 { "Direct Children (" (children.len()) ")" }
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
(PageActionGroup::generate_labels(
|
|
||||||
&children.iter().map(|i| i.id).collect::<Vec<Uuid>>(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
@for child in children {
|
|
||||||
li {
|
|
||||||
(ItemPreview::from_parts(child.id, child.name.as_ref(), &item_classes.get(&child.class).unwrap().name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@if !original_packaging_of.is_empty() {
|
|
||||||
h3 .mt-4 { "Original Packaging of" }
|
|
||||||
|
|
||||||
ul {
|
|
||||||
@for item in original_packaging_of {
|
|
||||||
li {
|
|
||||||
(ItemPreview::from_parts(item.id, item.name.as_ref(), &item_classes.get(&item.class).unwrap().name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/items")]
|
|
||||||
async fn list(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> {
|
|
||||||
let item_list = manage::item::get_all(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let items = manage::item::get_all_as_map(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let item_classes = manage::item_class::get_all_as_map(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let item_tree = manage::item::get_all_parents(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
// TODO: remove clone (should be possible without it)
|
|
||||||
let item_parents: HashMap<Uuid, Vec<Item>> = item_tree
|
|
||||||
.iter()
|
|
||||||
.map(|(id, parent_ids)| {
|
|
||||||
(
|
|
||||||
*id,
|
|
||||||
parent_ids
|
|
||||||
.iter()
|
|
||||||
.map(|parent_id| items.get(parent_id).unwrap().clone())
|
|
||||||
.collect(),
|
|
||||||
)
|
|
||||||
})
|
|
||||||
.collect();
|
|
||||||
|
|
||||||
Ok(templates::base(
|
|
||||||
TemplateConfig {
|
|
||||||
path: "/items",
|
|
||||||
title: Some("Item List"),
|
|
||||||
page_title: Some(Box::new("Item List")),
|
|
||||||
page_actions: vec![
|
|
||||||
(PageActionGroup::Button {
|
|
||||||
action: PageAction {
|
|
||||||
method: PageActionMethod::Get,
|
|
||||||
target: "/items/add".to_string(),
|
|
||||||
name: "Add".to_string(),
|
|
||||||
},
|
|
||||||
colour: Colour::Success,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
user: Some(user),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
html! {
|
|
||||||
table .table {
|
|
||||||
thead {
|
|
||||||
tr {
|
|
||||||
th { "Name" }
|
|
||||||
th { "Class" }
|
|
||||||
th { "Parents" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tbody {
|
|
||||||
@for item in item_list {
|
|
||||||
@let class = item_classes.get(&item.class).unwrap();
|
|
||||||
@let parents = item_parents.get(&item.id).unwrap();
|
|
||||||
tr {
|
|
||||||
td {
|
|
||||||
(ItemPreview::new(item.id, ItemName::new(item.name.as_ref(), &class.name).terse()))
|
|
||||||
}
|
|
||||||
td { a href={ "/item-class/" (class.id) } { (class.name) } }
|
|
||||||
td {
|
|
||||||
(templates::helpers::parents_breadcrumb(
|
|
||||||
ItemName::new(
|
|
||||||
item.name.as_ref(),
|
|
||||||
&class.name
|
|
||||||
),
|
|
||||||
&parents.iter().map(|parent| ItemPreview::from_parts(parent.id, parent.name.as_ref(), &item_classes.get(&parent.class).unwrap().name)).collect::<Vec<ItemPreview>>(),
|
|
||||||
false
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn default_quantity() -> usize {
|
|
||||||
1
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct NewItemForm {
|
|
||||||
#[serde(default = "default_quantity")]
|
|
||||||
pub quantity: usize,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub parent: Option<Uuid>,
|
|
||||||
pub class: Uuid,
|
|
||||||
pub original_packaging: Option<Uuid>,
|
|
||||||
pub description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct NewItemFormPrefilled {
|
|
||||||
pub quantity: Option<usize>,
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub parent: Option<Uuid>,
|
|
||||||
pub class: Option<Uuid>,
|
|
||||||
pub original_packaging: Option<Uuid>,
|
|
||||||
pub description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/items/add")]
|
|
||||||
async fn add_form(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
form: web::Query<NewItemFormPrefilled>,
|
|
||||||
user: Identity,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let datalist_items = datalist::items(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let datalist_item_classes = datalist::item_classes(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
Ok(templates::base(
|
|
||||||
TemplateConfig {
|
|
||||||
path: "/items/add",
|
|
||||||
title: Some("Add Item"),
|
|
||||||
page_title: Some(Box::new("Add Item")),
|
|
||||||
datalists: vec![&datalist_items, &datalist_item_classes],
|
|
||||||
user: Some(user),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
html! {
|
|
||||||
form method="POST" {
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "name",
|
|
||||||
title: "Name",
|
|
||||||
optional: true,
|
|
||||||
value: form.name.as_ref().map(|s| s as &dyn Display),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "class",
|
|
||||||
title: "Class",
|
|
||||||
required: true,
|
|
||||||
value: form.class.as_ref().map(|id| id as &dyn Display),
|
|
||||||
datalist: Some(&datalist_item_classes),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "parent",
|
|
||||||
title: "Parent",
|
|
||||||
optional: true,
|
|
||||||
value: form.parent.as_ref().map(|id| id as &dyn Display),
|
|
||||||
datalist: Some(&datalist_items),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "original_packaging",
|
|
||||||
title: "Original Packaging",
|
|
||||||
optional: true,
|
|
||||||
disabled: true,
|
|
||||||
value: form.original_packaging.as_ref().map(|id| id as &dyn Display),
|
|
||||||
datalist: Some(&datalist_items),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Textarea,
|
|
||||||
name: "description",
|
|
||||||
title: "Description ",
|
|
||||||
value: form.description.as_ref().map(|s| s as &dyn Display),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
|
|
||||||
div .input-group {
|
|
||||||
button .btn.btn-primary type="submit" { "Add" }
|
|
||||||
input .form-control.flex-grow-0.w-auto #quantity name="quantity" type="number" value=(form.quantity.unwrap_or(default_quantity())) min="1";
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/items/add")]
|
|
||||||
async fn add_post(
|
|
||||||
req: HttpRequest,
|
|
||||||
data: web::Form<NewItemForm>,
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
user: Identity,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let data = data.into_inner();
|
|
||||||
let new_item = NewItem {
|
|
||||||
name: data.name,
|
|
||||||
class: data.class,
|
|
||||||
parent: data.parent,
|
|
||||||
original_packaging: data.original_packaging,
|
|
||||||
description: data.description,
|
|
||||||
};
|
|
||||||
if data.quantity == 1 {
|
|
||||||
let item = manage::item::add(&pool, new_item)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
Ok(
|
|
||||||
web::Redirect::to("/item/".to_owned() + &item.id.to_string())
|
|
||||||
.see_other()
|
|
||||||
.respond_to(&req)
|
|
||||||
.map_into_boxed_body(),
|
|
||||||
)
|
|
||||||
} else {
|
|
||||||
let items = manage::item::add_multiple(&pool, new_item, data.quantity)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
Ok(templates::base(
|
|
||||||
TemplateConfig {
|
|
||||||
path: "/items/add",
|
|
||||||
title: Some("Added Items"),
|
|
||||||
page_title: Some(Box::new("Added Items")),
|
|
||||||
page_actions: vec![PageActionGroup::generate_labels(
|
|
||||||
&items.iter().map(|i| i.id).collect::<Vec<Uuid>>(),
|
|
||||||
)],
|
|
||||||
user: Some(user),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
html! {
|
|
||||||
ul {
|
|
||||||
@for item in items {
|
|
||||||
li {
|
|
||||||
a href={ "/item/" (item.id) } { (item.id) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
a href="/items" { "Back to all items" }
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.respond_to(&req)
|
|
||||||
.map_into_boxed_body())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item/{id}/edit")]
|
|
||||||
async fn edit_form(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
path: web::Path<Uuid>,
|
|
||||||
user: Identity,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let id = path.into_inner();
|
|
||||||
|
|
||||||
let item = manage::item::get(&pool, id)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let item_class = manage::item_class::get(&pool, item.class)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let datalist_items = datalist::items(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let datalist_item_classes = datalist::item_classes(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let item_name = ItemName::new(item.name.as_ref(), &item_class.name);
|
|
||||||
let mut title = item_name.to_string();
|
|
||||||
title.push_str(" – Edit Item");
|
|
||||||
|
|
||||||
Ok(templates::base(
|
|
||||||
TemplateConfig {
|
|
||||||
path: &format!("/item/{}/edit", item.id),
|
|
||||||
title: Some(&title),
|
|
||||||
page_title: Some(Box::new(item_name.clone())),
|
|
||||||
datalists: vec![&datalist_items, &datalist_item_classes],
|
|
||||||
user: Some(user),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
html! {
|
|
||||||
form method="POST" {
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "id",
|
|
||||||
title: "UUID",
|
|
||||||
required: true,
|
|
||||||
disabled: true,
|
|
||||||
value: Some(&item.id),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "name",
|
|
||||||
title: "Name",
|
|
||||||
optional: true,
|
|
||||||
disabled: item.name.is_none(),
|
|
||||||
value: item.name.as_ref().map(|s| s as &dyn Display),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "class",
|
|
||||||
title: "Class",
|
|
||||||
required: true,
|
|
||||||
value: Some(&item.class),
|
|
||||||
datalist: Some(&datalist_item_classes),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "parent",
|
|
||||||
title: "Parent",
|
|
||||||
optional: true,
|
|
||||||
value: item.parent.as_ref().map(|id| id as &dyn Display),
|
|
||||||
disabled: item.parent.is_none(),
|
|
||||||
datalist: Some(&datalist_items),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "original_packaging",
|
|
||||||
title: "Original Packaging",
|
|
||||||
optional: true,
|
|
||||||
value: item.original_packaging.as_ref().map(|id| id as &dyn Display),
|
|
||||||
disabled: item.original_packaging.is_none(),
|
|
||||||
datalist: Some(&datalist_items),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Textarea,
|
|
||||||
name: "description",
|
|
||||||
title: "Description ",
|
|
||||||
value: Some(&item.description),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
|
|
||||||
button .btn.btn-primary type="submit" { "Edit" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/item/{id}/edit")]
|
|
||||||
async fn edit(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
path: web::Path<Uuid>,
|
|
||||||
data: web::Form<NewItem>,
|
|
||||||
_user: Identity,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let id = path.into_inner();
|
|
||||||
|
|
||||||
let item = manage::item::update(&pool, id, data.into_inner())
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
Ok(web::Redirect::to("/item/".to_owned() + &item.id.to_string()).see_other())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/item/{id}/delete")]
|
|
||||||
async fn delete(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
path: web::Path<Uuid>,
|
|
||||||
_user: Identity,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let id = path.into_inner();
|
|
||||||
|
|
||||||
manage::item::delete(&pool, id)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
Ok(web::Redirect::to("/items").see_other())
|
|
||||||
}
|
|
180
src/frontend/item/add.rs
Normal file
180
src/frontend/item/add.rs
Normal file
|
@ -0,0 +1,180 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use actix_identity::Identity;
|
||||||
|
use actix_web::{error, get, post, web, HttpRequest, Responder};
|
||||||
|
use maud::html;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::frontend::templates::{self, datalist, forms, helpers::PageActionGroup, TemplateConfig};
|
||||||
|
use crate::manage;
|
||||||
|
use crate::models::*;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(get).service(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
fn default_quantity() -> usize {
|
||||||
|
1
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct NewItemForm {
|
||||||
|
#[serde(default = "default_quantity")]
|
||||||
|
pub quantity: usize,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub parent: Option<Uuid>,
|
||||||
|
pub class: Uuid,
|
||||||
|
pub original_packaging: Option<Uuid>,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct NewItemFormPrefilled {
|
||||||
|
pub quantity: Option<usize>,
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub parent: Option<Uuid>,
|
||||||
|
pub class: Option<Uuid>,
|
||||||
|
pub original_packaging: Option<Uuid>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/items/add")]
|
||||||
|
async fn get(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
form: web::Query<NewItemFormPrefilled>,
|
||||||
|
user: Identity,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let datalist_items = datalist::items(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let datalist_item_classes = datalist::item_classes(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(templates::base(
|
||||||
|
TemplateConfig {
|
||||||
|
path: "/items/add",
|
||||||
|
title: Some("Add Item"),
|
||||||
|
page_title: Some(Box::new("Add Item")),
|
||||||
|
datalists: vec![&datalist_items, &datalist_item_classes],
|
||||||
|
user: Some(user),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
form method="POST" {
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "name",
|
||||||
|
title: "Name",
|
||||||
|
optional: true,
|
||||||
|
value: form.name.as_ref().map(|s| s as &dyn Display),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "class",
|
||||||
|
title: "Class",
|
||||||
|
required: true,
|
||||||
|
value: form.class.as_ref().map(|id| id as &dyn Display),
|
||||||
|
datalist: Some(&datalist_item_classes),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "parent",
|
||||||
|
title: "Parent",
|
||||||
|
optional: true,
|
||||||
|
value: form.parent.as_ref().map(|id| id as &dyn Display),
|
||||||
|
datalist: Some(&datalist_items),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "original_packaging",
|
||||||
|
title: "Original Packaging",
|
||||||
|
optional: true,
|
||||||
|
disabled: true,
|
||||||
|
value: form.original_packaging.as_ref().map(|id| id as &dyn Display),
|
||||||
|
datalist: Some(&datalist_items),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Textarea,
|
||||||
|
name: "description",
|
||||||
|
title: "Description ",
|
||||||
|
value: form.description.as_ref().map(|s| s as &dyn Display),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
|
||||||
|
div .input-group {
|
||||||
|
button .btn.btn-primary type="submit" { "Add" }
|
||||||
|
input .form-control.flex-grow-0.w-auto #quantity name="quantity" type="number" value=(form.quantity.unwrap_or(default_quantity())) min="1";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/items/add")]
|
||||||
|
async fn post(
|
||||||
|
req: HttpRequest,
|
||||||
|
data: web::Form<NewItemForm>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
user: Identity,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let data = data.into_inner();
|
||||||
|
let new_item = NewItem {
|
||||||
|
name: data.name,
|
||||||
|
class: data.class,
|
||||||
|
parent: data.parent,
|
||||||
|
original_packaging: data.original_packaging,
|
||||||
|
description: data.description,
|
||||||
|
};
|
||||||
|
if data.quantity == 1 {
|
||||||
|
let item = manage::item::add(&pool, new_item)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
Ok(
|
||||||
|
web::Redirect::to("/item/".to_owned() + &item.id.to_string())
|
||||||
|
.see_other()
|
||||||
|
.respond_to(&req)
|
||||||
|
.map_into_boxed_body(),
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
let items = manage::item::add_multiple(&pool, new_item, data.quantity)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
Ok(templates::base(
|
||||||
|
TemplateConfig {
|
||||||
|
path: "/items/add",
|
||||||
|
title: Some("Added Items"),
|
||||||
|
page_title: Some(Box::new("Added Items")),
|
||||||
|
page_actions: vec![PageActionGroup::generate_labels(
|
||||||
|
&items.iter().map(|i| i.id).collect::<Vec<Uuid>>(),
|
||||||
|
)],
|
||||||
|
user: Some(user),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
ul {
|
||||||
|
@for item in items {
|
||||||
|
li {
|
||||||
|
a href={ "/item/" (item.id) } { (item.id) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
a href="/items" { "Back to all items" }
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.respond_to(&req)
|
||||||
|
.map_into_boxed_body())
|
||||||
|
}
|
||||||
|
}
|
29
src/frontend/item/delete.rs
Normal file
29
src/frontend/item/delete.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use actix_identity::Identity;
|
||||||
|
use actix_web::{error, post, web, Responder};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::manage;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/item/{id}/delete")]
|
||||||
|
async fn post(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
path: web::Path<Uuid>,
|
||||||
|
_user: Identity,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
|
||||||
|
manage::item::delete(&pool, id)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(web::Redirect::to("/items").see_other())
|
||||||
|
}
|
135
src/frontend/item/edit.rs
Normal file
135
src/frontend/item/edit.rs
Normal file
|
@ -0,0 +1,135 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use actix_identity::Identity;
|
||||||
|
use actix_web::{error, get, post, web, Responder};
|
||||||
|
use maud::html;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::frontend::templates::{self, datalist, forms, helpers::ItemName, TemplateConfig};
|
||||||
|
use crate::manage;
|
||||||
|
use crate::models::*;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(get).service(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/item/{id}/edit")]
|
||||||
|
async fn get(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
path: web::Path<Uuid>,
|
||||||
|
user: Identity,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
|
||||||
|
let item = manage::item::get(&pool, id)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let item_class = manage::item_class::get(&pool, item.class)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let datalist_items = datalist::items(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let datalist_item_classes = datalist::item_classes(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let item_name = ItemName::new(item.name.as_ref(), &item_class.name);
|
||||||
|
let mut title = item_name.to_string();
|
||||||
|
title.push_str(" – Edit Item");
|
||||||
|
|
||||||
|
Ok(templates::base(
|
||||||
|
TemplateConfig {
|
||||||
|
path: &format!("/item/{}/edit", item.id),
|
||||||
|
title: Some(&title),
|
||||||
|
page_title: Some(Box::new(item_name.clone())),
|
||||||
|
datalists: vec![&datalist_items, &datalist_item_classes],
|
||||||
|
user: Some(user),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
form method="POST" {
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "id",
|
||||||
|
title: "UUID",
|
||||||
|
required: true,
|
||||||
|
disabled: true,
|
||||||
|
value: Some(&item.id),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "name",
|
||||||
|
title: "Name",
|
||||||
|
optional: true,
|
||||||
|
disabled: item.name.is_none(),
|
||||||
|
value: item.name.as_ref().map(|s| s as &dyn Display),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "class",
|
||||||
|
title: "Class",
|
||||||
|
required: true,
|
||||||
|
value: Some(&item.class),
|
||||||
|
datalist: Some(&datalist_item_classes),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "parent",
|
||||||
|
title: "Parent",
|
||||||
|
optional: true,
|
||||||
|
value: item.parent.as_ref().map(|id| id as &dyn Display),
|
||||||
|
disabled: item.parent.is_none(),
|
||||||
|
datalist: Some(&datalist_items),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "original_packaging",
|
||||||
|
title: "Original Packaging",
|
||||||
|
optional: true,
|
||||||
|
value: item.original_packaging.as_ref().map(|id| id as &dyn Display),
|
||||||
|
disabled: item.original_packaging.is_none(),
|
||||||
|
datalist: Some(&datalist_items),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Textarea,
|
||||||
|
name: "description",
|
||||||
|
title: "Description ",
|
||||||
|
value: Some(&item.description),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
|
||||||
|
button .btn.btn-primary type="submit" { "Edit" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/item/{id}/edit")]
|
||||||
|
async fn post(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
path: web::Path<Uuid>,
|
||||||
|
data: web::Form<NewItem>,
|
||||||
|
_user: Identity,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
|
||||||
|
let item = manage::item::update(&pool, id, data.into_inner())
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(web::Redirect::to("/item/".to_owned() + &item.id.to_string()).see_other())
|
||||||
|
}
|
109
src/frontend/item/list.rs
Normal file
109
src/frontend/item/list.rs
Normal file
|
@ -0,0 +1,109 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::collections::HashMap;
|
||||||
|
|
||||||
|
use actix_identity::Identity;
|
||||||
|
use actix_web::{error, get, web, Responder};
|
||||||
|
use maud::html;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::frontend::templates::{
|
||||||
|
self,
|
||||||
|
helpers::{Colour, ItemName, ItemPreview, PageAction, PageActionGroup, PageActionMethod},
|
||||||
|
TemplateConfig,
|
||||||
|
};
|
||||||
|
use crate::manage;
|
||||||
|
use crate::models::*;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(get);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/items")]
|
||||||
|
async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> {
|
||||||
|
let item_list = manage::item::get_all(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let items = manage::item::get_all_as_map(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let item_classes = manage::item_class::get_all_as_map(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let item_tree = manage::item::get_all_parents(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
// TODO: remove clone (should be possible without it)
|
||||||
|
let item_parents: HashMap<Uuid, Vec<Item>> = item_tree
|
||||||
|
.iter()
|
||||||
|
.map(|(id, parent_ids)| {
|
||||||
|
(
|
||||||
|
*id,
|
||||||
|
parent_ids
|
||||||
|
.iter()
|
||||||
|
.map(|parent_id| items.get(parent_id).unwrap().clone())
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
})
|
||||||
|
.collect();
|
||||||
|
|
||||||
|
Ok(templates::base(
|
||||||
|
TemplateConfig {
|
||||||
|
path: "/items",
|
||||||
|
title: Some("Item List"),
|
||||||
|
page_title: Some(Box::new("Item List")),
|
||||||
|
page_actions: vec![
|
||||||
|
(PageActionGroup::Button {
|
||||||
|
action: PageAction {
|
||||||
|
method: PageActionMethod::Get,
|
||||||
|
target: "/items/add".to_string(),
|
||||||
|
name: "Add".to_string(),
|
||||||
|
},
|
||||||
|
colour: Colour::Success,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
user: Some(user),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
table .table {
|
||||||
|
thead {
|
||||||
|
tr {
|
||||||
|
th { "Name" }
|
||||||
|
th { "Class" }
|
||||||
|
th { "Parents" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
@for item in item_list {
|
||||||
|
@let class = item_classes.get(&item.class).unwrap();
|
||||||
|
@let parents = item_parents.get(&item.id).unwrap();
|
||||||
|
tr {
|
||||||
|
td {
|
||||||
|
(ItemPreview::new(item.id, ItemName::new(item.name.as_ref(), &class.name).terse()))
|
||||||
|
}
|
||||||
|
td { a href={ "/item-class/" (class.id) } { (class.name) } }
|
||||||
|
td {
|
||||||
|
(templates::helpers::parents_breadcrumb(
|
||||||
|
ItemName::new(
|
||||||
|
item.name.as_ref(),
|
||||||
|
&class.name
|
||||||
|
),
|
||||||
|
&parents.iter().map(|parent| ItemPreview::from_parts(parent.id, parent.name.as_ref(), &item_classes.get(&parent.class).unwrap().name)).collect::<Vec<ItemPreview>>(),
|
||||||
|
false
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
19
src/frontend/item/mod.rs
Normal file
19
src/frontend/item/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
mod add;
|
||||||
|
mod delete;
|
||||||
|
mod edit;
|
||||||
|
mod list;
|
||||||
|
mod show;
|
||||||
|
|
||||||
|
use actix_web::web;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.configure(add::config)
|
||||||
|
.configure(delete::config)
|
||||||
|
.configure(edit::config)
|
||||||
|
.configure(list::config)
|
||||||
|
.configure(show::config);
|
||||||
|
}
|
183
src/frontend/item/show.rs
Normal file
183
src/frontend/item/show.rs
Normal file
|
@ -0,0 +1,183 @@
|
||||||
|
// 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, web, Responder};
|
||||||
|
use maud::html;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::frontend::templates::{
|
||||||
|
self,
|
||||||
|
helpers::{Colour, ItemName, ItemPreview, PageAction, PageActionGroup, PageActionMethod},
|
||||||
|
TemplateConfig,
|
||||||
|
};
|
||||||
|
use crate::manage;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(get);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/item/{id}")]
|
||||||
|
async fn get(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
path: web::Path<Uuid>,
|
||||||
|
user: Identity,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
|
||||||
|
let item = manage::item::get(&pool, id)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let item_classes = manage::item_class::get_all_as_map(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let parents = manage::item::get_parents_details(&pool, item.id)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let children = manage::item::get_children(&pool, item.id)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let original_packaging = match item.original_packaging {
|
||||||
|
Some(id) => Some(
|
||||||
|
manage::item::get(&pool, id)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?,
|
||||||
|
),
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let original_packaging_of = manage::item::original_packaging_contents(&pool, id)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let item_class = item_classes.get(&item.class).unwrap();
|
||||||
|
|
||||||
|
let item_name = ItemName::new(item.name.as_ref(), &item_class.name);
|
||||||
|
let mut title = item_name.to_string();
|
||||||
|
title.push_str(" – Item Details");
|
||||||
|
|
||||||
|
Ok(templates::base(
|
||||||
|
TemplateConfig {
|
||||||
|
path: &format!("/item/{}", item.id),
|
||||||
|
title: Some(&title),
|
||||||
|
page_title: Some(Box::new(item_name.clone())),
|
||||||
|
page_actions: vec![
|
||||||
|
(PageActionGroup::Button {
|
||||||
|
action: PageAction {
|
||||||
|
method: PageActionMethod::Get,
|
||||||
|
target: format!("/items/add?parent={}", item.id),
|
||||||
|
name: "Add Child".to_string(),
|
||||||
|
},
|
||||||
|
colour: Colour::Success,
|
||||||
|
}),
|
||||||
|
PageActionGroup::generate_labels(&[item.id]),
|
||||||
|
(PageActionGroup::Button {
|
||||||
|
action: PageAction {
|
||||||
|
method: PageActionMethod::Get,
|
||||||
|
target: format!("/item/{}/edit", item.id),
|
||||||
|
name: "Edit".to_string(),
|
||||||
|
},
|
||||||
|
colour: Colour::Warning,
|
||||||
|
}),
|
||||||
|
(PageActionGroup::Button {
|
||||||
|
action: PageAction {
|
||||||
|
method: PageActionMethod::Post,
|
||||||
|
target: format!("/item/{}/delete", item.id),
|
||||||
|
name: "Delete".to_string(),
|
||||||
|
},
|
||||||
|
colour: Colour::Danger,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
user: Some(user),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
table .table {
|
||||||
|
tr {
|
||||||
|
th { "UUID" }
|
||||||
|
td { (item.id) }
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th { "Short ID" }
|
||||||
|
td { (item.short_id) }
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th { "Name" }
|
||||||
|
td { (item_name.clone().terse()) }
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th { "Class" }
|
||||||
|
td { a href={ "/item-class/" (item.class) } { (item_class.name) } }
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th { "Parents" }
|
||||||
|
td {
|
||||||
|
(templates::helpers::parents_breadcrumb(
|
||||||
|
ItemName::new(
|
||||||
|
item.name.as_ref(),
|
||||||
|
&item_class.name
|
||||||
|
),
|
||||||
|
&parents.iter().map(|parent| ItemPreview::from_parts(parent.id, parent.name.as_ref(), &item_classes.get(&parent.class).unwrap().name)).collect::<Vec<ItemPreview>>(),
|
||||||
|
true
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th { "Original Packaging" }
|
||||||
|
td {
|
||||||
|
@if let Some(original_packaging) = original_packaging {
|
||||||
|
a
|
||||||
|
href={ "/item/" (original_packaging.id) }
|
||||||
|
{ (ItemName::new(original_packaging.name.as_ref(), &item_classes.get(&original_packaging.class).unwrap().name)) }
|
||||||
|
} @else {
|
||||||
|
"-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th { "Description" }
|
||||||
|
td style="white-space: pre-wrap" { (item.description) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if !children.is_empty() {
|
||||||
|
div .d-flex.justify-content-between.mt-4 {
|
||||||
|
div {
|
||||||
|
h3 { "Direct Children (" (children.len()) ")" }
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
(PageActionGroup::generate_labels(
|
||||||
|
&children.iter().map(|i| i.id).collect::<Vec<Uuid>>(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
@for child in children {
|
||||||
|
li {
|
||||||
|
(ItemPreview::from_parts(child.id, child.name.as_ref(), &item_classes.get(&child.class).unwrap().name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if !original_packaging_of.is_empty() {
|
||||||
|
h3 .mt-4 { "Original Packaging of" }
|
||||||
|
|
||||||
|
ul {
|
||||||
|
@for item in original_packaging_of {
|
||||||
|
li {
|
||||||
|
(ItemPreview::from_parts(item.id, item.name.as_ref(), &item_classes.get(&item.class).unwrap().name))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
|
@ -1,412 +0,0 @@
|
||||||
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
|
||||||
//
|
|
||||||
// SPDX-License-Identifier: AGPL-3.0-or-later
|
|
||||||
|
|
||||||
use std::fmt::Display;
|
|
||||||
|
|
||||||
use actix_identity::Identity;
|
|
||||||
use actix_web::{error, get, post, web, Responder};
|
|
||||||
use maud::html;
|
|
||||||
use serde::Deserialize;
|
|
||||||
use sqlx::PgPool;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use super::templates::helpers::{
|
|
||||||
Colour, ItemName, ItemPreview, PageAction, PageActionGroup, PageActionMethod,
|
|
||||||
};
|
|
||||||
use super::templates::{self, datalist, forms, TemplateConfig};
|
|
||||||
use crate::manage;
|
|
||||||
use crate::models::*;
|
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
|
||||||
cfg.service(show)
|
|
||||||
.service(list)
|
|
||||||
.service(add_form)
|
|
||||||
.service(add)
|
|
||||||
.service(edit_form)
|
|
||||||
.service(edit)
|
|
||||||
.service(delete);
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item-class/{id}")]
|
|
||||||
async fn show(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
path: web::Path<Uuid>,
|
|
||||||
user: Identity,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let id = path.into_inner();
|
|
||||||
|
|
||||||
let item_class = manage::item_class::get(&pool, id)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
// TODO: Once async closures are stable, use map_or on item_class.parent instead
|
|
||||||
let parent = match item_class.parent {
|
|
||||||
Some(id) => manage::item_class::get(&pool, id)
|
|
||||||
.await
|
|
||||||
.map(Some)
|
|
||||||
.map_err(error::ErrorInternalServerError)?,
|
|
||||||
None => None,
|
|
||||||
};
|
|
||||||
|
|
||||||
let children = manage::item_class::children(&pool, id)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let items = manage::item_class::items(&pool, id)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let mut title = item_class.name.clone();
|
|
||||||
title.push_str(" – Item Details");
|
|
||||||
|
|
||||||
let mut page_actions = vec![
|
|
||||||
(PageActionGroup::Button {
|
|
||||||
action: PageAction {
|
|
||||||
method: PageActionMethod::Get,
|
|
||||||
target: format!("/items/add?class={}", item_class.id),
|
|
||||||
name: "Add Item".to_string(),
|
|
||||||
},
|
|
||||||
colour: Colour::Success,
|
|
||||||
}),
|
|
||||||
];
|
|
||||||
if item_class.parent.is_none() {
|
|
||||||
page_actions.push(PageActionGroup::Button {
|
|
||||||
action: PageAction {
|
|
||||||
method: PageActionMethod::Get,
|
|
||||||
target: format!("/item-classes/add?parent={}", item_class.id),
|
|
||||||
name: "Add Child".to_string(),
|
|
||||||
},
|
|
||||||
colour: Colour::Primary,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
page_actions.push(PageActionGroup::Button {
|
|
||||||
action: PageAction {
|
|
||||||
method: PageActionMethod::Get,
|
|
||||||
target: format!("/item-class/{}/edit", item_class.id),
|
|
||||||
name: "Edit".to_string(),
|
|
||||||
},
|
|
||||||
colour: Colour::Warning,
|
|
||||||
});
|
|
||||||
page_actions.push(PageActionGroup::Button {
|
|
||||||
action: PageAction {
|
|
||||||
method: PageActionMethod::Post,
|
|
||||||
target: format!("/item-class/{}/delete", item_class.id),
|
|
||||||
name: "Delete".to_string(),
|
|
||||||
},
|
|
||||||
colour: Colour::Danger,
|
|
||||||
});
|
|
||||||
|
|
||||||
Ok(templates::base(
|
|
||||||
TemplateConfig {
|
|
||||||
path: &format!("/item-class/{}", item_class.id),
|
|
||||||
title: Some(&title),
|
|
||||||
page_title: Some(Box::new(item_class.name.clone())),
|
|
||||||
page_actions,
|
|
||||||
user: Some(user),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
html! {
|
|
||||||
table .table {
|
|
||||||
tr {
|
|
||||||
th { "UUID" }
|
|
||||||
td { (item_class.id) }
|
|
||||||
}
|
|
||||||
tr {
|
|
||||||
th { "Name" }
|
|
||||||
td { (item_class.name) }
|
|
||||||
}
|
|
||||||
@if let Some(parent) = parent {
|
|
||||||
tr {
|
|
||||||
th { "Parent" }
|
|
||||||
td { a href={ "/item-class/" (parent.id) } { (parent.name) } }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tr {
|
|
||||||
th { "Description" }
|
|
||||||
td style="white-space: pre-wrap" { (item_class.description) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@if !children.is_empty() {
|
|
||||||
h3 .mt-4 { "Children (" (children.len()) ")" }
|
|
||||||
|
|
||||||
ul {
|
|
||||||
@for child in children {
|
|
||||||
li {
|
|
||||||
a href={ "/item-class/" (child.id) } { (child.name) }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@if !items.is_empty() {
|
|
||||||
div .d-flex.justify-content-between.mt-4 {
|
|
||||||
div {
|
|
||||||
h3 { "Items (" (items.len()) ")" }
|
|
||||||
}
|
|
||||||
div {
|
|
||||||
(PageActionGroup::generate_labels(
|
|
||||||
&items.iter().map(|i| i.id).collect::<Vec<Uuid>>(),
|
|
||||||
))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
ul {
|
|
||||||
@for item in items {
|
|
||||||
li {
|
|
||||||
(ItemPreview::new(item.id, ItemName::new(item.name.as_ref(), &item_class.name).terse()))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item-classes")]
|
|
||||||
async fn list(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> {
|
|
||||||
let item_classes_ids = sqlx::query_scalar!("SELECT id FROM item_classes ORDER BY created_at")
|
|
||||||
.fetch_all(pool.as_ref())
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let item_classes = manage::item_class::get_all_as_map(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
Ok(templates::base(
|
|
||||||
TemplateConfig {
|
|
||||||
path: "/item-classes",
|
|
||||||
title: Some("Item Class List"),
|
|
||||||
page_title: Some(Box::new("Item Class List")),
|
|
||||||
page_actions: vec![
|
|
||||||
(PageActionGroup::Button {
|
|
||||||
action: PageAction {
|
|
||||||
method: PageActionMethod::Get,
|
|
||||||
target: "/item-classes/add".to_string(),
|
|
||||||
name: "Add".to_string(),
|
|
||||||
},
|
|
||||||
colour: Colour::Success,
|
|
||||||
}),
|
|
||||||
],
|
|
||||||
user: Some(user),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
html! {
|
|
||||||
table .table {
|
|
||||||
thead {
|
|
||||||
tr {
|
|
||||||
th { "Name" }
|
|
||||||
th { "Parents" }
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tbody {
|
|
||||||
@for item_class in item_classes_ids {
|
|
||||||
@let item_class = item_classes.get(&item_class).unwrap();
|
|
||||||
tr {
|
|
||||||
td { a href={ "/item-class/" (item_class.id) } { (item_class.name) } }
|
|
||||||
td {
|
|
||||||
@if let Some(parent) = item_class.parent {
|
|
||||||
@let parent = item_classes.get(&parent).unwrap();
|
|
||||||
a href={ "/item-class/" (parent.id) } { (parent.name) }
|
|
||||||
} @else {
|
|
||||||
"-"
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct NewItemClassForm {
|
|
||||||
pub name: String,
|
|
||||||
pub parent: Option<Uuid>,
|
|
||||||
pub description: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[derive(Debug, Deserialize)]
|
|
||||||
pub struct NewItemClassFormPrefilled {
|
|
||||||
pub name: Option<String>,
|
|
||||||
pub parent: Option<Uuid>,
|
|
||||||
pub description: Option<String>,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item-classes/add")]
|
|
||||||
async fn add_form(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
form: web::Query<NewItemClassFormPrefilled>,
|
|
||||||
user: Identity,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let datalist_item_classes = datalist::item_classes(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
Ok(templates::base(
|
|
||||||
TemplateConfig {
|
|
||||||
path: "/items-classes/add",
|
|
||||||
title: Some("Add Item Class"),
|
|
||||||
page_title: Some(Box::new("Add Item Class")),
|
|
||||||
datalists: vec![&datalist_item_classes],
|
|
||||||
user: Some(user),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
html! {
|
|
||||||
form method="POST" {
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "name",
|
|
||||||
title: "Name",
|
|
||||||
required: true,
|
|
||||||
value: form.name.as_ref().map(|s| s as &dyn Display),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "parent",
|
|
||||||
title: "Parent",
|
|
||||||
optional: true,
|
|
||||||
disabled: form.parent.is_none(),
|
|
||||||
value: form.parent.as_ref().map(|id| id as &dyn Display),
|
|
||||||
datalist: Some(&datalist_item_classes),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Textarea,
|
|
||||||
name: "description",
|
|
||||||
title: "Description ",
|
|
||||||
value: form.description.as_ref().map(|s| s as &dyn Display),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
|
|
||||||
button .btn.btn-primary type="submit" { "Add" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/item-classes/add")]
|
|
||||||
async fn add(
|
|
||||||
data: web::Form<NewItemClassForm>,
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
_user: Identity,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let data = data.into_inner();
|
|
||||||
let item = manage::item_class::add(
|
|
||||||
&pool,
|
|
||||||
NewItemClass {
|
|
||||||
name: data.name,
|
|
||||||
parent: data.parent,
|
|
||||||
description: data.description,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
Ok(web::Redirect::to("/item-class/".to_owned() + &item.id.to_string()).see_other())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/item-class/{id}/edit")]
|
|
||||||
async fn edit_form(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
path: web::Path<Uuid>,
|
|
||||||
user: Identity,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let id = path.into_inner();
|
|
||||||
|
|
||||||
let item_class = manage::item_class::get(&pool, id)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let datalist_item_classes = datalist::item_classes(&pool)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
let mut title = item_class.name.clone();
|
|
||||||
title.push_str(" – Item Details");
|
|
||||||
|
|
||||||
Ok(templates::base(
|
|
||||||
TemplateConfig {
|
|
||||||
path: &format!("/items-class/{}/add", id),
|
|
||||||
title: Some(&title),
|
|
||||||
page_title: Some(Box::new(item_class.name.clone())),
|
|
||||||
datalists: vec![&datalist_item_classes],
|
|
||||||
user: Some(user),
|
|
||||||
..Default::default()
|
|
||||||
},
|
|
||||||
html! {
|
|
||||||
form method="POST" {
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "id",
|
|
||||||
title: "UUID",
|
|
||||||
disabled: true,
|
|
||||||
required: true,
|
|
||||||
value: Some(&item_class.id),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "name",
|
|
||||||
title: "Name",
|
|
||||||
required: true,
|
|
||||||
value: Some(&item_class.name),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Text,
|
|
||||||
name: "parent",
|
|
||||||
title: "Parent",
|
|
||||||
optional: true,
|
|
||||||
disabled: item_class.parent.is_none(),
|
|
||||||
value: item_class.parent.as_ref().map(|id| id as &dyn Display),
|
|
||||||
datalist: Some(&datalist_item_classes),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
(forms::InputGroup {
|
|
||||||
r#type: forms::InputType::Textarea,
|
|
||||||
name: "description",
|
|
||||||
title: "Description ",
|
|
||||||
value: Some(&item_class.description),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
|
|
||||||
button .btn.btn-primary type="submit" { "Edit" }
|
|
||||||
}
|
|
||||||
},
|
|
||||||
))
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/item-class/{id}/edit")]
|
|
||||||
async fn edit(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
path: web::Path<Uuid>,
|
|
||||||
data: web::Form<NewItemClass>,
|
|
||||||
_user: Identity,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let id = path.into_inner();
|
|
||||||
|
|
||||||
let item_class = manage::item_class::update(&pool, id, data.into_inner())
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
Ok(web::Redirect::to("/item-class/".to_owned() + &item_class.id.to_string()).see_other())
|
|
||||||
}
|
|
||||||
|
|
||||||
#[post("/item-class/{id}/delete")]
|
|
||||||
async fn delete(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
path: web::Path<Uuid>,
|
|
||||||
_user: Identity,
|
|
||||||
) -> actix_web::Result<impl Responder> {
|
|
||||||
let id = path.into_inner();
|
|
||||||
|
|
||||||
manage::item_class::delete(&pool, id)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?;
|
|
||||||
|
|
||||||
Ok(web::Redirect::to("/item-classes").see_other())
|
|
||||||
}
|
|
107
src/frontend/item_class/add.rs
Normal file
107
src/frontend/item_class/add.rs
Normal file
|
@ -0,0 +1,107 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use actix_identity::Identity;
|
||||||
|
use actix_web::{error, get, post, web, Responder};
|
||||||
|
use maud::html;
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::frontend::templates::{self, datalist, forms, TemplateConfig};
|
||||||
|
use crate::manage;
|
||||||
|
use crate::models::*;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(get).service(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct NewItemClassForm {
|
||||||
|
pub name: String,
|
||||||
|
pub parent: Option<Uuid>,
|
||||||
|
pub description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct NewItemClassFormPrefilled {
|
||||||
|
pub name: Option<String>,
|
||||||
|
pub parent: Option<Uuid>,
|
||||||
|
pub description: Option<String>,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/item-classes/add")]
|
||||||
|
async fn get(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
form: web::Query<NewItemClassFormPrefilled>,
|
||||||
|
user: Identity,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let datalist_item_classes = datalist::item_classes(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(templates::base(
|
||||||
|
TemplateConfig {
|
||||||
|
path: "/items-classes/add",
|
||||||
|
title: Some("Add Item Class"),
|
||||||
|
page_title: Some(Box::new("Add Item Class")),
|
||||||
|
datalists: vec![&datalist_item_classes],
|
||||||
|
user: Some(user),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
form method="POST" {
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "name",
|
||||||
|
title: "Name",
|
||||||
|
required: true,
|
||||||
|
value: form.name.as_ref().map(|s| s as &dyn Display),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "parent",
|
||||||
|
title: "Parent",
|
||||||
|
optional: true,
|
||||||
|
disabled: form.parent.is_none(),
|
||||||
|
value: form.parent.as_ref().map(|id| id as &dyn Display),
|
||||||
|
datalist: Some(&datalist_item_classes),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Textarea,
|
||||||
|
name: "description",
|
||||||
|
title: "Description ",
|
||||||
|
value: form.description.as_ref().map(|s| s as &dyn Display),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
|
||||||
|
button .btn.btn-primary type="submit" { "Add" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/item-classes/add")]
|
||||||
|
async fn post(
|
||||||
|
data: web::Form<NewItemClassForm>,
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
_user: Identity,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let data = data.into_inner();
|
||||||
|
let item = manage::item_class::add(
|
||||||
|
&pool,
|
||||||
|
NewItemClass {
|
||||||
|
name: data.name,
|
||||||
|
parent: data.parent,
|
||||||
|
description: data.description,
|
||||||
|
},
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
Ok(web::Redirect::to("/item-class/".to_owned() + &item.id.to_string()).see_other())
|
||||||
|
}
|
29
src/frontend/item_class/delete.rs
Normal file
29
src/frontend/item_class/delete.rs
Normal file
|
@ -0,0 +1,29 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use actix_identity::Identity;
|
||||||
|
use actix_web::{error, post, web, Responder};
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::manage;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/item-class/{id}/delete")]
|
||||||
|
async fn post(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
path: web::Path<Uuid>,
|
||||||
|
_user: Identity,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
|
||||||
|
manage::item_class::delete(&pool, id)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(web::Redirect::to("/item-classes").see_other())
|
||||||
|
}
|
106
src/frontend/item_class/edit.rs
Normal file
106
src/frontend/item_class/edit.rs
Normal file
|
@ -0,0 +1,106 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
use std::fmt::Display;
|
||||||
|
|
||||||
|
use actix_identity::Identity;
|
||||||
|
use actix_web::{error, get, post, web, Responder};
|
||||||
|
use maud::html;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::frontend::templates::{self, datalist, forms, TemplateConfig};
|
||||||
|
use crate::manage;
|
||||||
|
use crate::models::*;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(get).service(post);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/item-class/{id}/edit")]
|
||||||
|
async fn get(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
path: web::Path<Uuid>,
|
||||||
|
user: Identity,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
|
||||||
|
let item_class = manage::item_class::get(&pool, id)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let datalist_item_classes = datalist::item_classes(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let mut title = item_class.name.clone();
|
||||||
|
title.push_str(" – Item Details");
|
||||||
|
|
||||||
|
Ok(templates::base(
|
||||||
|
TemplateConfig {
|
||||||
|
path: &format!("/items-class/{}/add", id),
|
||||||
|
title: Some(&title),
|
||||||
|
page_title: Some(Box::new(item_class.name.clone())),
|
||||||
|
datalists: vec![&datalist_item_classes],
|
||||||
|
user: Some(user),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
form method="POST" {
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "id",
|
||||||
|
title: "UUID",
|
||||||
|
disabled: true,
|
||||||
|
required: true,
|
||||||
|
value: Some(&item_class.id),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "name",
|
||||||
|
title: "Name",
|
||||||
|
required: true,
|
||||||
|
value: Some(&item_class.name),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Text,
|
||||||
|
name: "parent",
|
||||||
|
title: "Parent",
|
||||||
|
optional: true,
|
||||||
|
disabled: item_class.parent.is_none(),
|
||||||
|
value: item_class.parent.as_ref().map(|id| id as &dyn Display),
|
||||||
|
datalist: Some(&datalist_item_classes),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
(forms::InputGroup {
|
||||||
|
r#type: forms::InputType::Textarea,
|
||||||
|
name: "description",
|
||||||
|
title: "Description ",
|
||||||
|
value: Some(&item_class.description),
|
||||||
|
..Default::default()
|
||||||
|
})
|
||||||
|
|
||||||
|
button .btn.btn-primary type="submit" { "Edit" }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
||||||
|
|
||||||
|
#[post("/item-class/{id}/edit")]
|
||||||
|
async fn post(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
path: web::Path<Uuid>,
|
||||||
|
data: web::Form<NewItemClass>,
|
||||||
|
_user: Identity,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
|
||||||
|
let item_class = manage::item_class::update(&pool, id, data.into_inner())
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(web::Redirect::to("/item-class/".to_owned() + &item_class.id.to_string()).see_other())
|
||||||
|
}
|
77
src/frontend/item_class/list.rs
Normal file
77
src/frontend/item_class/list.rs
Normal file
|
@ -0,0 +1,77 @@
|
||||||
|
// 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, web, Responder};
|
||||||
|
use maud::html;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
|
||||||
|
use crate::frontend::templates::{
|
||||||
|
self,
|
||||||
|
helpers::{Colour, PageAction, PageActionGroup, PageActionMethod},
|
||||||
|
TemplateConfig,
|
||||||
|
};
|
||||||
|
use crate::manage;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(get);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/item-classes")]
|
||||||
|
async fn get(pool: web::Data<PgPool>, user: Identity) -> actix_web::Result<impl Responder> {
|
||||||
|
let item_classes_ids = sqlx::query_scalar!("SELECT id FROM item_classes ORDER BY created_at")
|
||||||
|
.fetch_all(pool.as_ref())
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let item_classes = manage::item_class::get_all_as_map(&pool)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
Ok(templates::base(
|
||||||
|
TemplateConfig {
|
||||||
|
path: "/item-classes",
|
||||||
|
title: Some("Item Class List"),
|
||||||
|
page_title: Some(Box::new("Item Class List")),
|
||||||
|
page_actions: vec![
|
||||||
|
(PageActionGroup::Button {
|
||||||
|
action: PageAction {
|
||||||
|
method: PageActionMethod::Get,
|
||||||
|
target: "/item-classes/add".to_string(),
|
||||||
|
name: "Add".to_string(),
|
||||||
|
},
|
||||||
|
colour: Colour::Success,
|
||||||
|
}),
|
||||||
|
],
|
||||||
|
user: Some(user),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
table .table {
|
||||||
|
thead {
|
||||||
|
tr {
|
||||||
|
th { "Name" }
|
||||||
|
th { "Parents" }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tbody {
|
||||||
|
@for item_class in item_classes_ids {
|
||||||
|
@let item_class = item_classes.get(&item_class).unwrap();
|
||||||
|
tr {
|
||||||
|
td { a href={ "/item-class/" (item_class.id) } { (item_class.name) } }
|
||||||
|
td {
|
||||||
|
@if let Some(parent) = item_class.parent {
|
||||||
|
@let parent = item_classes.get(&parent).unwrap();
|
||||||
|
a href={ "/item-class/" (parent.id) } { (parent.name) }
|
||||||
|
} @else {
|
||||||
|
"-"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
19
src/frontend/item_class/mod.rs
Normal file
19
src/frontend/item_class/mod.rs
Normal file
|
@ -0,0 +1,19 @@
|
||||||
|
// SPDX-FileCopyrightText: 2024 Simon Bruder <simon@sbruder.de>
|
||||||
|
//
|
||||||
|
// SPDX-License-Identifier: AGPL-3.0-or-later
|
||||||
|
|
||||||
|
mod add;
|
||||||
|
mod delete;
|
||||||
|
mod edit;
|
||||||
|
mod list;
|
||||||
|
mod show;
|
||||||
|
|
||||||
|
use actix_web::web;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.configure(add::config)
|
||||||
|
.configure(delete::config)
|
||||||
|
.configure(edit::config)
|
||||||
|
.configure(list::config)
|
||||||
|
.configure(show::config);
|
||||||
|
}
|
156
src/frontend/item_class/show.rs
Normal file
156
src/frontend/item_class/show.rs
Normal file
|
@ -0,0 +1,156 @@
|
||||||
|
// 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, web, Responder};
|
||||||
|
use maud::html;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::frontend::templates::{
|
||||||
|
self,
|
||||||
|
helpers::{Colour, ItemName, ItemPreview, PageAction, PageActionGroup, PageActionMethod},
|
||||||
|
TemplateConfig,
|
||||||
|
};
|
||||||
|
use crate::manage;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(get);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/item-class/{id}")]
|
||||||
|
async fn get(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
path: web::Path<Uuid>,
|
||||||
|
user: Identity,
|
||||||
|
) -> actix_web::Result<impl Responder> {
|
||||||
|
let id = path.into_inner();
|
||||||
|
|
||||||
|
let item_class = manage::item_class::get(&pool, id)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
// TODO: Once async closures are stable, use map_or on item_class.parent instead
|
||||||
|
let parent = match item_class.parent {
|
||||||
|
Some(id) => manage::item_class::get(&pool, id)
|
||||||
|
.await
|
||||||
|
.map(Some)
|
||||||
|
.map_err(error::ErrorInternalServerError)?,
|
||||||
|
None => None,
|
||||||
|
};
|
||||||
|
|
||||||
|
let children = manage::item_class::children(&pool, id)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let items = manage::item_class::items(&pool, id)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?;
|
||||||
|
|
||||||
|
let mut title = item_class.name.clone();
|
||||||
|
title.push_str(" – Item Details");
|
||||||
|
|
||||||
|
let mut page_actions = vec![
|
||||||
|
(PageActionGroup::Button {
|
||||||
|
action: PageAction {
|
||||||
|
method: PageActionMethod::Get,
|
||||||
|
target: format!("/items/add?class={}", item_class.id),
|
||||||
|
name: "Add Item".to_string(),
|
||||||
|
},
|
||||||
|
colour: Colour::Success,
|
||||||
|
}),
|
||||||
|
];
|
||||||
|
if item_class.parent.is_none() {
|
||||||
|
page_actions.push(PageActionGroup::Button {
|
||||||
|
action: PageAction {
|
||||||
|
method: PageActionMethod::Get,
|
||||||
|
target: format!("/item-classes/add?parent={}", item_class.id),
|
||||||
|
name: "Add Child".to_string(),
|
||||||
|
},
|
||||||
|
colour: Colour::Primary,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
page_actions.push(PageActionGroup::Button {
|
||||||
|
action: PageAction {
|
||||||
|
method: PageActionMethod::Get,
|
||||||
|
target: format!("/item-class/{}/edit", item_class.id),
|
||||||
|
name: "Edit".to_string(),
|
||||||
|
},
|
||||||
|
colour: Colour::Warning,
|
||||||
|
});
|
||||||
|
page_actions.push(PageActionGroup::Button {
|
||||||
|
action: PageAction {
|
||||||
|
method: PageActionMethod::Post,
|
||||||
|
target: format!("/item-class/{}/delete", item_class.id),
|
||||||
|
name: "Delete".to_string(),
|
||||||
|
},
|
||||||
|
colour: Colour::Danger,
|
||||||
|
});
|
||||||
|
|
||||||
|
Ok(templates::base(
|
||||||
|
TemplateConfig {
|
||||||
|
path: &format!("/item-class/{}", item_class.id),
|
||||||
|
title: Some(&title),
|
||||||
|
page_title: Some(Box::new(item_class.name.clone())),
|
||||||
|
page_actions,
|
||||||
|
user: Some(user),
|
||||||
|
..Default::default()
|
||||||
|
},
|
||||||
|
html! {
|
||||||
|
table .table {
|
||||||
|
tr {
|
||||||
|
th { "UUID" }
|
||||||
|
td { (item_class.id) }
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th { "Name" }
|
||||||
|
td { (item_class.name) }
|
||||||
|
}
|
||||||
|
@if let Some(parent) = parent {
|
||||||
|
tr {
|
||||||
|
th { "Parent" }
|
||||||
|
td { a href={ "/item-class/" (parent.id) } { (parent.name) } }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tr {
|
||||||
|
th { "Description" }
|
||||||
|
td style="white-space: pre-wrap" { (item_class.description) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if !children.is_empty() {
|
||||||
|
h3 .mt-4 { "Children (" (children.len()) ")" }
|
||||||
|
|
||||||
|
ul {
|
||||||
|
@for child in children {
|
||||||
|
li {
|
||||||
|
a href={ "/item-class/" (child.id) } { (child.name) }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@if !items.is_empty() {
|
||||||
|
div .d-flex.justify-content-between.mt-4 {
|
||||||
|
div {
|
||||||
|
h3 { "Items (" (items.len()) ")" }
|
||||||
|
}
|
||||||
|
div {
|
||||||
|
(PageActionGroup::generate_labels(
|
||||||
|
&items.iter().map(|i| i.id).collect::<Vec<Uuid>>(),
|
||||||
|
))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
ul {
|
||||||
|
@for item in items {
|
||||||
|
li {
|
||||||
|
(ItemPreview::new(item.id, ItemName::new(item.name.as_ref(), &item_class.name).terse()))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
))
|
||||||
|
}
|
61
src/frontend/jump.rs
Normal file
61
src/frontend/jump.rs
Normal file
|
@ -0,0 +1,61 @@
|
||||||
|
// 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, web, Responder};
|
||||||
|
use serde::Deserialize;
|
||||||
|
use sqlx::PgPool;
|
||||||
|
use uuid::Uuid;
|
||||||
|
|
||||||
|
use crate::manage;
|
||||||
|
use crate::models::EntityType;
|
||||||
|
|
||||||
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
|
cfg.service(get);
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize)]
|
||||||
|
struct JumpData {
|
||||||
|
id: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[get("/jump")]
|
||||||
|
async fn get(
|
||||||
|
pool: web::Data<PgPool>,
|
||||||
|
data: web::Query<JumpData>,
|
||||||
|
_user: Identity, // this endpoint leaks information about the existence of items
|
||||||
|
) -> Result<impl Responder, error::Error> {
|
||||||
|
let mut id = data.id.clone();
|
||||||
|
|
||||||
|
let entity_type = if let Ok(id) = Uuid::parse_str(&id) {
|
||||||
|
manage::query_entity_type(&pool, id)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)?
|
||||||
|
} else if let Ok(short_id) = id.parse::<i32>() {
|
||||||
|
if let Ok(item) = manage::item::get_by_short_id(&pool, short_id)
|
||||||
|
.await
|
||||||
|
.map_err(error::ErrorInternalServerError)
|
||||||
|
{
|
||||||
|
id = item.id.to_string();
|
||||||
|
Some(EntityType::Item)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
};
|
||||||
|
|
||||||
|
if let Some(prefix) = entity_type.map(|entity_type| match entity_type {
|
||||||
|
EntityType::Item => "item",
|
||||||
|
EntityType::ItemClass => "item-class",
|
||||||
|
}) {
|
||||||
|
Ok(web::Redirect::to(format!("/{prefix}/{id}")).see_other())
|
||||||
|
} else {
|
||||||
|
Ok(web::Redirect::to(format!(
|
||||||
|
"/items/add?{}",
|
||||||
|
serde_urlencoded::to_string([("name", &id)])?
|
||||||
|
))
|
||||||
|
.see_other())
|
||||||
|
}
|
||||||
|
}
|
|
@ -5,25 +5,20 @@
|
||||||
mod auth;
|
mod auth;
|
||||||
mod item;
|
mod item;
|
||||||
mod item_class;
|
mod item_class;
|
||||||
|
mod jump;
|
||||||
mod labels;
|
mod labels;
|
||||||
mod templates;
|
mod templates;
|
||||||
|
|
||||||
use actix_identity::Identity;
|
use actix_identity::Identity;
|
||||||
use actix_web::{error, get, web, Responder};
|
use actix_web::{get, web, Responder};
|
||||||
use maud::html;
|
use maud::html;
|
||||||
use serde::Deserialize;
|
|
||||||
use sqlx::PgPool;
|
|
||||||
use uuid::Uuid;
|
|
||||||
|
|
||||||
use crate::manage;
|
|
||||||
use crate::models::EntityType;
|
|
||||||
|
|
||||||
pub fn config(cfg: &mut web::ServiceConfig) {
|
pub fn config(cfg: &mut web::ServiceConfig) {
|
||||||
cfg.service(index)
|
cfg.service(index)
|
||||||
.service(jump)
|
|
||||||
.configure(auth::config)
|
.configure(auth::config)
|
||||||
.configure(item::config)
|
.configure(item::config)
|
||||||
.configure(item_class::config)
|
.configure(item_class::config)
|
||||||
|
.configure(jump::config)
|
||||||
.configure(labels::config);
|
.configure(labels::config);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -37,48 +32,3 @@ async fn index(user: Identity) -> impl Responder {
|
||||||
html! {},
|
html! {},
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct JumpData {
|
|
||||||
id: String,
|
|
||||||
}
|
|
||||||
|
|
||||||
#[get("/jump")]
|
|
||||||
async fn jump(
|
|
||||||
pool: web::Data<PgPool>,
|
|
||||||
data: web::Query<JumpData>,
|
|
||||||
_user: Identity, // this endpoint leaks information about the existence of items
|
|
||||||
) -> Result<impl Responder, error::Error> {
|
|
||||||
let mut id = data.id.clone();
|
|
||||||
|
|
||||||
let entity_type = if let Ok(id) = Uuid::parse_str(&id) {
|
|
||||||
manage::query_entity_type(&pool, id)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)?
|
|
||||||
} else if let Ok(short_id) = id.parse::<i32>() {
|
|
||||||
if let Ok(item) = manage::item::get_by_short_id(&pool, short_id)
|
|
||||||
.await
|
|
||||||
.map_err(error::ErrorInternalServerError)
|
|
||||||
{
|
|
||||||
id = item.id.to_string();
|
|
||||||
Some(EntityType::Item)
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
};
|
|
||||||
|
|
||||||
if let Some(prefix) = entity_type.map(|entity_type| match entity_type {
|
|
||||||
EntityType::Item => "item",
|
|
||||||
EntityType::ItemClass => "item-class",
|
|
||||||
}) {
|
|
||||||
Ok(web::Redirect::to(format!("/{prefix}/{id}")).see_other())
|
|
||||||
} else {
|
|
||||||
Ok(web::Redirect::to(format!(
|
|
||||||
"/items/add?{}",
|
|
||||||
serde_urlencoded::to_string([("name", &id)])?
|
|
||||||
))
|
|
||||||
.see_other())
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
Loading…
Reference in a new issue