some htmx and alpine

This commit is contained in:
2023-08-29 21:33:59 +02:00
parent 3f834cd7d2
commit e0c9bc542a
6 changed files with 612 additions and 189 deletions

View File

@@ -1,4 +1,4 @@
use maud::{html, Markup}; use maud::{html, Markup, PreEscaped};
use crate::models::*; use crate::models::*;
use crate::ClientState; use crate::ClientState;
@@ -312,12 +312,154 @@ impl InventoryItemList {
} }
} }
pub struct InventoryNewItemFormName;
impl InventoryNewItemFormName {
pub fn build(value: Option<&str>, error: bool) -> Markup {
html!(
div
."grid"
."grid-cols-[2fr,3fr]"
."justify-items-center"
."items-center"
hx-post="/inventory/item/name/validate"
hx-trigger="input delay:1s, every 5s"
hx-params="new-item-name"
hx-swap="outerHTML"
{
label for="name" .font-bold { "Name" }
input
type="text"
id="new-item-name"
name="new-item-name"
x-on:input="(e) => {save_active = inventory_new_item_check_input()}"
."block"
."w-full"
."p-2"
."bg-gray-50"
."border-2"
."border-red-500"[error]
."border-gray-300"[!error]
."rounded"
."focus:outline-none"
."focus:bg-white"
."focus:border-purple-500"[!error]
value=[value]
;
@if error {
div
."col-start-2"
."text-sm"
."text-red-500"
{ "name already exists" }
}
}
)
}
}
pub struct InventoryNewItemFormWeight;
impl InventoryNewItemFormWeight {
pub fn build() -> Markup {
html!(
div
."grid"
."grid-cols-[2fr,3fr]"
."justify-items-center"
."items-center"
{
label for="weight" .font-bold { "Weight" }
input
type="number"
id="new-item-weight"
name="new-item-weight"
min="0"
x-on:input="(e) => {
save_active = inventory_new_item_check_input();
weight_error = !check_weight();
}"
x-bind:class="weight_error && 'border-red-500' || 'border-gray-300 focus:border-purple-500'"
."block"
."w-full"
."p-2"
."bg-gray-50"
."border-2"
."rounded"
."focus:outline-none"
."focus:bg-white"
{}
span
// x-on produces some errors, this works as well
x-bind:class="!weight_error && 'hidden'"
."col-start-2"
."text-sm"
."text-red-500"
{ "invalid input" }
}
)
}
}
pub struct InventoryNewItemFormCategory;
impl InventoryNewItemFormCategory {
pub fn build(state: &ClientState, categories: &Vec<Category>) -> Markup {
html!(
div
."grid"
."grid-cols-[2fr,3fr]"
."justify-items-center"
."items-center"
{
label for="item-category" .font-bold ."w-1/2" .text-center { "Category" }
select
id="new-item-category-id"
name="new-item-category-id"
."block"
."w-full"
."p-2"
."bg-gray-50"
."border-2"
."border-gray-300"
."rounded"
."focus:outline-none"
."focus:bg-white"
."focus:border-purple-500"
autocomplete="off" // https://stackoverflow.com/a/10096033
{
@for category in categories {
option value=(category.id) selected[state.active_category_id.map_or(false, |id| id == category.id)] {
(category.name)
}
}
}
}
)
}
}
pub struct InventoryNewItemForm; pub struct InventoryNewItemForm;
impl InventoryNewItemForm { impl InventoryNewItemForm {
pub fn build(state: &ClientState, categories: &Vec<Category>) -> Markup { pub fn build(state: &ClientState, categories: &Vec<Category>) -> Markup {
html!( html!(
script {
(PreEscaped("
function inventory_new_item_check_input() {
return document.getElementById('new-item-name').value.length != 0
&& is_positive_integer(document.getElementById('new-item-weight').value)
}
function check_weight() {
return document.getElementById('new-item-weight').validity.valid;
}
"))
}
form form
x-data="{
save_active: inventory_new_item_check_input(),
weight_error: !check_weight(),
}"
name="new-item" name="new-item"
id="new-item" id="new-item"
action="/inventory/item/" action="/inventory/item/"
@@ -328,74 +470,14 @@ impl InventoryNewItemForm {
span ."mdi" ."mdi-playlist-plus" ."text-2xl" ."mr-4" {} span ."mdi" ."mdi-playlist-plus" ."text-2xl" ."mr-4" {}
p ."inline" ."text-xl" { "Add new item" } p ."inline" ."text-xl" { "Add new item" }
} }
div ."w-11/12" ."mx-auto" { div ."w-11/12" ."mx-auto" ."flex" ."flex-col" ."gap-8" {
div ."pb-8" { (InventoryNewItemFormName::build(None, false))
div ."flex" ."flex-row" ."justify-center" ."items-start"{ (InventoryNewItemFormWeight::build())
label for="name" .font-bold ."w-1/2" ."p-2" ."text-center" { "Name" } (InventoryNewItemFormCategory::build(&state, categories))
span ."w-1/2" {
input type="text" id="new-item-name" name="new-item-name"
."block"
."w-full"
."p-2"
."bg-gray-50"
."border-2"
."rounded"
."focus:outline-none"
."focus:bg-white"
."focus:border-purple-500"
{
}
}
}
}
div ."flex" ."flex-row" ."justify-center" ."items-center" ."pb-8" {
label for="weight" .font-bold ."w-1/2" .text-center { "Weight" }
span ."w-1/2" {
input
type="text"
id="new-item-weight"
name="new-item-weight"
."block"
."w-full"
."p-2"
."bg-gray-50"
."border-2"
."border-gray-300"
."rounded"
."focus:outline-none"
."focus:bg-white"
."focus:border-purple-500"
{
}
}
}
div ."flex" ."flex-row" ."justify-center" ."items-center" ."pb-8" {
label for="item-category" .font-bold ."w-1/2" .text-center { "Category" }
span ."w-1/2" {
select
id="new-item-category-id"
name="new-item-category-id"
."block"
."w-full"
."p-2"
."bg-gray-50"
."border-2"
."border-gray-300"
."rounded"
."focus:outline-none"
."focus:bg-white"
."focus:border-purple-500"
autocomplete="off" // https://stackoverflow.com/a/10096033
{
@for category in categories {
option value=(category.id) selected[state.active_category_id.map_or(false, |id| id == category.id)] {
(category.name)
}
}
}
}
}
input type="submit" value="Add" input type="submit" value="Add"
x-bind:disabled="!save_active"
."enabled:cursor-pointer"
."disabled:opacity-50"
."py-2" ."py-2"
."border-2" ."border-2"
."rounded" ."rounded"
@@ -415,6 +497,7 @@ impl InventoryNewCategoryForm {
pub fn build() -> Markup { pub fn build() -> Markup {
html!( html!(
form form
x-data="{ save_active: document.getElementById('new-category-name').value.length != 0 }"
name="new-category" name="new-category"
id="new-category" id="new-category"
action="/inventory/category/" action="/inventory/category/"
@@ -431,11 +514,13 @@ impl InventoryNewCategoryForm {
label for="name" .font-bold ."w-1/2" ."p-2" ."text-center" { "Name" } label for="name" .font-bold ."w-1/2" ."p-2" ."text-center" { "Name" }
span ."w-1/2" { span ."w-1/2" {
input type="text" id="new-category-name" name="new-category-name" input type="text" id="new-category-name" name="new-category-name"
x-on:input="(e) => {save_active = e.target.value.length != 0 }"
."block" ."block"
."w-full" ."w-full"
."p-2" ."p-2"
."bg-gray-50" ."bg-gray-50"
."border-2" ."border-2"
."border-gray-300"
."rounded" ."rounded"
."focus:outline-none" ."focus:outline-none"
."focus:bg-white" ."focus:bg-white"
@@ -446,6 +531,9 @@ impl InventoryNewCategoryForm {
} }
} }
input type="submit" value="Add" input type="submit" value="Add"
x-bind:disabled="!save_active"
."enabled:cursor-pointer"
."disabled:opacity-50"
."py-2" ."py-2"
."border-2" ."border-2"
."rounded" ."rounded"

View File

@@ -24,6 +24,7 @@ impl Root {
head { head {
title { "Packager" } title { "Packager" }
script src="https://unpkg.com/htmx.org@1.9.2" {} script src="https://unpkg.com/htmx.org@1.9.2" {}
script src="https://unpkg.com/alpinejs@3.12.1" defer {}
script src="https://cdn.tailwindcss.com" {} script src="https://cdn.tailwindcss.com" {}
script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.js" defer {} script src="https://unpkg.com/alpinejs@3.x.x/dist/cdn.js" defer {}
link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css"; link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/@mdi/font@6.9.96/css/materialdesignicons.min.css";

View File

@@ -9,6 +9,9 @@ use serde_variant::to_variant_name;
use crate::ClientState; use crate::ClientState;
pub struct TripManager; pub struct TripManager;
pub mod types;
pub use types::*;
impl TripManager { impl TripManager {
pub fn build(trips: Vec<models::Trip>) -> Markup { pub fn build(trips: Vec<models::Trip>) -> Markup {
html!( html!(
@@ -557,7 +560,7 @@ impl TripInfo {
."gap-1" ."gap-1"
{ {
span { (triptype.name) } span { (triptype.name) }
span ."mdi" ."mdi-delete" ."text-sm" {} span ."mdi" ."mdi-close" ."text-sm" {}
} }
} }
} }
@@ -584,6 +587,7 @@ impl TripInfo {
."flex-column" ."flex-column"
."items-center" ."items-center"
."hover:bg-green-200" ."hover:bg-green-200"
."hover:opacity-100"
."gap-1" ."gap-1"
."opacity-60" ."opacity-60"
{ {
@@ -623,7 +627,9 @@ pub struct TripComment;
impl TripComment { impl TripComment {
pub fn build(trip: &models::Trip) -> Markup { pub fn build(trip: &models::Trip) -> Markup {
html!( html!(
div { div
x-data="{ save_active: false }"
{
h1 ."text-xl" ."mb-5" { "Comments" } h1 ."text-xl" ."mb-5" { "Comments" }
form form
@@ -636,6 +642,7 @@ impl TripComment {
// https://stackoverflow.com/a/48460773 // https://stackoverflow.com/a/48460773
textarea textarea
#"comment" #"comment"
x-on:input="save_active=true"
."border" ."w-full" ."h-48" ."border" ."w-full" ."h-48"
name="new-comment" name="new-comment"
form="edit-comment" form="edit-comment"
@@ -647,11 +654,14 @@ impl TripComment {
button button
type="submit" type="submit"
form="edit-comment" form="edit-comment"
x-bind:disabled="!save_active"
."enabled:bg-green-200"
."enabled:hover:bg-green-400"
."enabled:cursor-pointer"
."disabled:opacity-50"
."disabled:bg-gray-300"
."mt-2" ."mt-2"
."border" ."border"
."bg-green-200"
."hover:bg-green-400"
."cursor-pointer"
."flex" ."flex"
."flex-column" ."flex-column"
."p-2" ."p-2"

View File

@@ -0,0 +1,163 @@
use crate::models;
use crate::ClientState;
use maud::{html, Markup};
pub struct TypeList;
impl TypeList {
pub fn build(state: &ClientState, trip_types: Vec<models::TripsType>) -> Markup {
html!(
div ."p-8" ."flex" ."flex-col" ."gap-8" {
h1 ."text-2xl" {"Trip Types"}
ul
."flex"
."flex-col"
."items-stretch"
."border-t"
."border-l"
."h-full"
{
@for trip_type in trip_types {
li
."border-b"
."border-r"
."flex"
."flex-row"
."justify-between"
."items-stretch"
{
@if state.trip_type_edit.map_or(false, |id| id == trip_type.id) {
form
."hidden"
id="edit-trip-type"
action={ (trip_type.id) "/edit/name/submit" }
target="_self"
method="post"
{}
div
."bg-blue-200"
."p-2"
."grow"
{
input
."bg-blue-100"
."hover:bg-white"
."w-full"
type="text"
name="new-value"
form="edit-trip-type"
value=(trip_type.name)
;
}
div
."flex"
."flex-row"
{
a
href="."
."bg-red-200"
."hover:bg-red-300"
."w-8"
."flex"
{
span
."mdi"
."mdi-cancel"
."text-xl"
."m-auto"
;
}
button
type="submit"
form="edit-trip-type"
."bg-green-200"
."hover:bg-green-300"
."w-8"
{
span
."mdi"
."mdi-content-save"
."text-xl"
;
}
}
} @else {
span
."p-2"
{
(trip_type.name)
}
div
."bg-blue-100"
."hover:bg-blue-200"
."p-0"
."w-8"
{
a
.flex
."w-full"
."h-full"
href={ "?edit=" (trip_type.id) }
{
span
."m-auto"
."mdi"
."mdi-pencil"
."text-xl";
}
}
}
}
}
}
form
name="new-trip-type"
action="/trips/types/"
target="_self"
method="post"
."mt-8" ."p-5" ."border-2" ."border-gray-200"
{
div ."mb-5" ."flex" ."flex-row" {
span ."mdi" ."mdi-playlist-plus" ."text-2xl" ."mr-4" {}
p ."inline" ."text-xl" { "Add new trip type" }
}
div ."w-11/12" ."m-auto" {
div ."mx-auto" ."pb-8" {
div ."flex" ."flex-row" ."justify-center" {
label for="new-trip-type-name" ."font-bold" ."w-1/2" ."p-2" ."text-center" { "Name" }
span ."w-1/2" {
input
type="text"
id="new-trip-type-name"
name="new-trip-type-name"
."block"
."w-full"
."p-2"
."bg-gray-50"
."border-2"
."rounded"
."focus:outline-none"
."focus:bg-white"
{}
}
}
}
input
type="submit"
value="Add"
."py-2"
."border-2"
."rounded"
."border-gray-300"
."mx-auto"
."w-full"
{}
}
}
}
)
}
}

View File

@@ -47,6 +47,7 @@ pub struct ClientState {
pub active_category_id: Option<Uuid>, pub active_category_id: Option<Uuid>,
pub edit_item: Option<Uuid>, pub edit_item: Option<Uuid>,
pub trip_edit_attribute: Option<TripAttribute>, pub trip_edit_attribute: Option<TripAttribute>,
pub trip_type_edit: Option<Uuid>,
} }
impl ClientState { impl ClientState {
@@ -55,6 +56,7 @@ impl ClientState {
active_category_id: None, active_category_id: None,
edit_item: None, edit_item: None,
trip_edit_attribute: None, trip_edit_attribute: None,
trip_type_edit: None,
} }
} }
} }
@@ -102,6 +104,11 @@ async fn main() -> Result<(), sqlx::Error> {
.route("/assets/luggage.svg", get(icon_handler)) .route("/assets/luggage.svg", get(icon_handler))
.route("/", get(root)) .route("/", get(root))
.route("/trips/", get(trips)) .route("/trips/", get(trips))
.route("/trips/types/", get(trips_types).post(trip_type_create))
.route(
"/trips/types/:id/edit/name/submit",
post(trips_types_edit_name),
)
.route("/trip/", post(trip_create)) .route("/trip/", post(trip_create))
.route("/trip/:id/", get(trip)) .route("/trip/:id/", get(trip))
.route("/trip/:id/comment/submit", post(trip_comment_set)) .route("/trip/:id/comment/submit", post(trip_comment_set))
@@ -119,6 +126,10 @@ async fn main() -> Result<(), sqlx::Error> {
.route("/inventory/", get(inventory_inactive)) .route("/inventory/", get(inventory_inactive))
.route("/inventory/category/", post(inventory_category_create)) .route("/inventory/category/", post(inventory_category_create))
.route("/inventory/item/", post(inventory_item_create)) .route("/inventory/item/", post(inventory_item_create))
.route(
"/inventory/item/name/validate",
post(inventory_item_validate_name),
)
.route("/inventory/category/:id/", get(inventory_active)) .route("/inventory/category/:id/", get(inventory_active))
.route("/inventory/item/:id/delete", get(inventory_item_delete)) .route("/inventory/item/:id/delete", get(inventory_item_delete))
.route("/inventory/item/:id/edit", post(inventory_item_edit)) .route("/inventory/item/:id/edit", post(inventory_item_edit))
@@ -250,10 +261,62 @@ struct NewItem {
category_id: Uuid, category_id: Uuid,
} }
#[derive(Deserialize)]
struct NewItemName {
#[serde(rename = "new-item-name")]
name: String,
}
async fn inventory_item_validate_name(
State(state): State<AppState>,
Form(new_item): Form<NewItemName>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
let results = query!(
"SELECT id
FROM inventory_items
WHERE name = ?",
new_item.name,
)
.fetch(&state.database_pool)
.map_ok(|_| Ok(()))
.try_collect::<Vec<Result<(), models::Error>>>()
.await
// we have two error handling lines here. these are distinct errors
// this one is the SQL error that may arise during the query
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
ErrorPage::build(&e.to_string()),
)
})?
.into_iter()
.collect::<Result<Vec<()>, models::Error>>()
// and this one is the model mapping error that may arise e.g. during
// reading of the rows
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
ErrorPage::build(&e.to_string()),
)
})?;
Ok((
StatusCode::OK,
InventoryNewItemFormName::build(Some(&new_item.name), !results.is_empty()),
))
}
async fn inventory_item_create( async fn inventory_item_create(
State(state): State<AppState>, State(state): State<AppState>,
Form(new_item): Form<NewItem>, Form(new_item): Form<NewItem>,
) -> Result<Redirect, (StatusCode, String)> { ) -> Result<Redirect, (StatusCode, String)> {
if new_item.name.len() == 0 {
return Err((
StatusCode::UNPROCESSABLE_ENTITY,
"name cannot be empty".to_string(),
));
}
let id = Uuid::new_v4(); let id = Uuid::new_v4();
let id_param = id.to_string(); let id_param = id.to_string();
let name = &new_item.name; let name = &new_item.name;
@@ -411,6 +474,13 @@ async fn inventory_item_edit(
Path(id): Path<Uuid>, Path(id): Path<Uuid>,
Form(edit_item): Form<EditItem>, Form(edit_item): Form<EditItem>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, (StatusCode, Markup)> {
if edit_item.name.len() == 0 {
return Err((
StatusCode::UNPROCESSABLE_ENTITY,
ErrorPage::build("name cannot be empty"),
));
}
let id = Item::update( let id = Item::update(
&state.database_pool, &state.database_pool,
id, id,
@@ -469,6 +539,13 @@ async fn trip_create(
State(state): State<AppState>, State(state): State<AppState>,
Form(new_trip): Form<NewTrip>, Form(new_trip): Form<NewTrip>,
) -> Result<Redirect, (StatusCode, String)> { ) -> Result<Redirect, (StatusCode, String)> {
if new_trip.name.len() == 0 {
return Err((
StatusCode::UNPROCESSABLE_ENTITY,
"name cannot be empty".to_string(),
));
}
let id = Uuid::new_v4(); let id = Uuid::new_v4();
let id_param = id.to_string(); let id_param = id.to_string();
let date_start = new_trip let date_start = new_trip
@@ -532,8 +609,6 @@ async fn trip_create(
async fn trips( async fn trips(
State(state): State<AppState>, State(state): State<AppState>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> { ) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
tracing::info!("receiving trips");
let trips: Vec<models::Trip> = query_as!( let trips: Vec<models::Trip> = query_as!(
DbTripRow, DbTripRow,
"SELECT "SELECT
@@ -571,8 +646,6 @@ async fn trips(
) )
})?; })?;
tracing::info!("received trips");
Ok(( Ok((
StatusCode::OK, StatusCode::OK,
Root::build(TripManager::build(trips), &TopLevelPage::Trips), Root::build(TripManager::build(trips), &TopLevelPage::Trips),
@@ -811,6 +884,14 @@ async fn trip_edit_attribute(
Path((trip_id, attribute)): Path<(Uuid, TripAttribute)>, Path((trip_id, attribute)): Path<(Uuid, TripAttribute)>,
Form(trip_update): Form<TripUpdate>, Form(trip_update): Form<TripUpdate>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, (StatusCode, Markup)> {
if let TripAttribute::Name = attribute {
if trip_update.new_value.len() == 0 {
return Err((
StatusCode::UNPROCESSABLE_ENTITY,
ErrorPage::build("name cannot be empty"),
));
}
}
let result = query(&format!( let result = query(&format!(
"UPDATE trips "UPDATE trips
SET {attribute} = ? SET {attribute} = ?
@@ -980,6 +1061,13 @@ async fn inventory_category_create(
State(state): State<AppState>, State(state): State<AppState>,
Form(new_category): Form<NewCategory>, Form(new_category): Form<NewCategory>,
) -> Result<Redirect, (StatusCode, Markup)> { ) -> Result<Redirect, (StatusCode, Markup)> {
if new_category.name.len() == 0 {
return Err((
StatusCode::UNPROCESSABLE_ENTITY,
ErrorPage::build("name cannot be empty"),
));
}
let id = Uuid::new_v4(); let id = Uuid::new_v4();
let id_param = id.to_string(); let id_param = id.to_string();
query!( query!(
@@ -1059,3 +1147,161 @@ async fn trip_state_set(
Ok(Redirect::to(&format!("/trip/{id}/", id = trip_id))) Ok(Redirect::to(&format!("/trip/{id}/", id = trip_id)))
} }
} }
#[derive(Debug, Deserialize)]
struct TripTypeQuery {
edit: Option<Uuid>,
}
async fn trips_types(
State(mut state): State<AppState>,
Query(trip_type_query): Query<TripTypeQuery>,
) -> Result<(StatusCode, Markup), (StatusCode, Markup)> {
state.client_state.trip_type_edit = trip_type_query.edit;
let trip_types: Vec<models::TripsType> = query_as!(
DbTripsTypesRow,
"SELECT
id,
name
FROM trips_types",
)
.fetch(&state.database_pool)
.map_ok(|row| row.try_into())
.try_collect::<Vec<Result<models::TripsType, models::Error>>>()
.await
// we have two error handling lines here. these are distinct errors
// this one is the SQL error that may arise during the query
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
ErrorPage::build(&e.to_string()),
)
})?
.into_iter()
.collect::<Result<Vec<models::TripsType>, models::Error>>()
// and this one is the model mapping error that may arise e.g. during
// reading of the rows
.map_err(|e| {
(
StatusCode::INTERNAL_SERVER_ERROR,
ErrorPage::build(&e.to_string()),
)
})?;
Ok((
StatusCode::OK,
Root::build(
components::trip::TypeList::build(&state.client_state, trip_types),
&TopLevelPage::Trips,
),
))
}
#[derive(Deserialize)]
struct NewTripType {
#[serde(rename = "new-trip-type-name")]
name: String,
}
async fn trip_type_create(
State(state): State<AppState>,
Form(new_trip_type): Form<NewTripType>,
) -> Result<Redirect, (StatusCode, String)> {
if new_trip_type.name.len() == 0 {
return Err((
StatusCode::UNPROCESSABLE_ENTITY,
"name cannot be empty".to_string(),
));
}
let id = Uuid::new_v4();
let id_param = id.to_string();
query!(
"INSERT INTO trips_types
(id, name)
VALUES
(?, ?)",
id_param,
new_trip_type.name,
)
.execute(&state.database_pool)
.await
.map_err(|e| match e {
sqlx::Error::Database(ref error) => {
let sqlite_error = error.downcast_ref::<SqliteError>();
if let Some(code) = sqlite_error.code() {
match &*code {
"2067" => {
// SQLITE_CONSTRAINT_UNIQUE
(
StatusCode::BAD_REQUEST,
format!(
"trip type with name \"{name}\" already exists",
name = new_trip_type.name,
),
)
}
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("got error with unknown code: {}", sqlite_error.to_string()),
),
}
} else {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("got error without code: {}", sqlite_error.to_string()),
)
}
}
_ => (
StatusCode::INTERNAL_SERVER_ERROR,
format!("got unknown error: {}", e.to_string()),
),
})?;
Ok(Redirect::to("/trips/types/"))
}
#[derive(Deserialize)]
struct TripTypeUpdate {
#[serde(rename = "new-value")]
new_value: String,
}
async fn trips_types_edit_name(
State(state): State<AppState>,
Path(trip_type_id): Path<Uuid>,
Form(trip_update): Form<TripTypeUpdate>,
) -> Result<Redirect, (StatusCode, Markup)> {
if trip_update.new_value.len() == 0 {
return Err((
StatusCode::UNPROCESSABLE_ENTITY,
ErrorPage::build("name cannot be empty"),
));
}
let id_param = trip_type_id.to_string();
let result = query!(
"UPDATE trips_types
SET name = ?
WHERE id = ?",
trip_update.new_value,
id_param,
)
.execute(&state.database_pool)
.await
.map_err(|e| (StatusCode::BAD_REQUEST, ErrorPage::build(&e.to_string())))?;
if result.rows_affected() == 0 {
Err((
StatusCode::NOT_FOUND,
ErrorPage::build(&format!(
"tript type with id {id} not found",
id = trip_type_id
)),
))
} else {
Ok(Redirect::to("/trips/types/"))
}
}

View File

@@ -288,69 +288,6 @@ pub enum TripAttribute {
TempMax, TempMax,
} }
// impl std::convert::Into<&'static str> for TripAttribute {
// fn into(self) -> &'static str {
// match self {
// Self::DateStart => "date_start",
// Self::DateEnd => "date_end",
// Self::Location => "location",
// Self::TempMin => "temp_min",
// Self::TempMax => "temp_max",
// }
// }
// }
// impl std::convert::TryFrom<&str> for TripAttribute {
// type Error = Error;
// fn try_from(value: &str) -> Result<Self, Error> {
// Ok(match value {
// "date_start" => Self::DateStart,
// "date_end" => Self::DateEnd,
// "location" => Self::Location,
// "temp_min" => Self::TempMin,
// "temp_max" => Self::TempMax,
// _ => {
// return Err(Error::UnknownAttributeValue {
// attribute: value.to_string(),
// })
// }
// })
// }
// }
// impl TryFrom<SqliteRow> for Trip {
// type Error = Error;
// fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
// let name: &str = row.try_get("name")?;
// let id: &str = row.try_get("id")?;
// let date_start: time::Date = row.try_get("date_start")?;
// let date_end: time::Date = row.try_get("date_end")?;
// let state: TripState = row.try_get("state")?;
// let location = row.try_get("location")?;
// let temp_min = row.try_get("temp_min")?;
// let temp_max = row.try_get("temp_max")?;
// let comment = row.try_get("comment")?;
// let id: Uuid = Uuid::try_parse(id)?;
// Ok(Trip {
// id,
// name: name.to_string(),
// date_start,
// date_end,
// state,
// location,
// temp_min,
// temp_max,
// comment,
// types: None,
// categories: None,
// })
// }
// }
impl<'a> Trip { impl<'a> Trip {
pub fn types(&'a self) -> &Vec<TripType> { pub fn types(&'a self) -> &Vec<TripType> {
self.types self.types
@@ -614,18 +551,6 @@ pub struct TripType {
pub active: bool, pub active: bool,
} }
// impl TryFrom<SqliteRow> for TripType {
// type Error = Error;
// fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
// let id: Uuid = Uuid::try_parse(row.try_get("id")?)?;
// let name: String = row.try_get::<&str, _>("name")?.to_string();
// let active: bool = row.try_get("active")?;
// Ok(Self { id, name, active })
// }
// }
pub struct DbCategoryRow { pub struct DbCategoryRow {
pub id: String, pub id: String,
pub name: String, pub name: String,
@@ -653,23 +578,6 @@ impl TryFrom<DbCategoryRow> for Category {
} }
} }
// impl TryFrom<SqliteRow> for Category {
// type Error = Error;
// fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
// let name: &str = row.try_get("name")?;
// let description: &str = row.try_get("description")?;
// let id: Uuid = Uuid::try_parse(row.try_get("id")?)?;
// Ok(Category {
// id,
// name: name.to_string(),
// description: description.to_string(),
// items: None,
// })
// }
// }
pub struct DbInventoryItemsRow { pub struct DbInventoryItemsRow {
id: String, id: String,
name: String, name: String,
@@ -741,26 +649,6 @@ impl TryFrom<DbInventoryItemsRow> for Item {
} }
} }
// impl TryFrom<SqliteRow> for Item {
// type Error = Error;
// fn try_from(row: SqliteRow) -> Result<Self, Self::Error> {
// let name: &str = row.try_get("name")?;
// let description: &str = row.try_get("description")?;
// let weight: i64 = row.try_get("weight")?;
// let id: Uuid = Uuid::try_parse(row.try_get("id")?)?;
// let category_id: Uuid = Uuid::try_parse(row.try_get("category_id")?)?;
// Ok(Item {
// id,
// name: name.to_string(),
// weight,
// description: description.to_string(),
// category_id,
// })
// }
// }
impl Item { impl Item {
pub async fn find(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<Option<Item>, Error> { pub async fn find(pool: &sqlx::Pool<sqlx::Sqlite>, id: Uuid) -> Result<Option<Item>, Error> {
let id_param = id.to_string(); let id_param = id.to_string();
@@ -820,3 +708,30 @@ impl Item {
} }
} }
} }
pub struct DbTripsTypesRow {
pub id: String,
pub name: String,
}
#[derive(Clone, Debug, Serialize, Deserialize, PartialEq)]
pub enum TripTypeAttribute {
#[serde(rename = "name")]
Name,
}
pub struct TripsType {
pub id: Uuid,
pub name: String,
}
impl TryFrom<DbTripsTypesRow> for TripsType {
type Error = Error;
fn try_from(row: DbTripsTypesRow) -> Result<Self, Self::Error> {
Ok(TripsType {
id: Uuid::try_parse(&row.id)?,
name: row.name,
})
}
}