From f662f4fb120714cc190173920f406692c92ef90e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Hannes=20K=C3=B6rber?= Date: Sun, 17 Sep 2023 17:34:06 +0200 Subject: [PATCH] impl toggle --- src/components/mod.rs | 104 +++++++++++++++-- src/components/trips/todos/mod.rs | 178 +++++++++++++++++++++++------- src/routing/mod.rs | 31 ++++-- 3 files changed, 252 insertions(+), 61 deletions(-) diff --git a/src/components/mod.rs b/src/components/mod.rs index d726b1e..26a9632 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -114,7 +114,7 @@ pub mod view { pub mod route { use async_trait::async_trait; - use crate::AppState; + use crate::{models::user::User, AppState}; use axum::{ body::{BoxBody, HttpBody}, extract::{Path, Query, State}, @@ -137,7 +137,7 @@ pub mod route { const URL: &'static str; async fn create( - user: Extension, + user: Extension, state: State, headers: HeaderMap, path: Path, @@ -153,7 +153,7 @@ pub mod route { const URL: &'static str; async fn read( - user: Extension, + user: Extension, state: State, headers: HeaderMap, query: Query, @@ -169,14 +169,14 @@ pub mod route { const URL: &'static str; async fn start( - user: Extension, + user: Extension, state: State, headers: HeaderMap, path: Path, ) -> Result, crate::Error>; async fn save( - user: Extension, + user: Extension, state: State, headers: HeaderMap, path: Path, @@ -184,13 +184,103 @@ pub mod route { ) -> Result, crate::Error>; async fn cancel( - user: Extension, + user: Extension, state: State, headers: HeaderMap, path: Path, ) -> Result, crate::Error>; } + #[async_trait] + pub trait ToggleFallback: Send + Sync + Sized + 'static { + type UrlParams: Clone + Copy + Send + Sync + Sized + 'static; + + const URL_TRUE: &'static str; + const URL_FALSE: &'static str; + + async fn set( + current_user: User, + state: AppState, + headers: HeaderMap, + params: Self::UrlParams, + value: bool, + ) -> Result, crate::Error>; + + async fn set_true( + Extension(user): Extension, + State(state): State, + headers: HeaderMap, + Path(path): Path, + ) -> Result, crate::Error> { + Self::set(user, state, headers, path, true).await + } + + async fn set_false( + Extension(user): Extension, + State(state): State, + headers: HeaderMap, + Path(path): Path, + ) -> Result, crate::Error> { + Self::set(user, state, headers, path, false).await + } + + fn router() -> axum::Router + where + B: HttpBody + Send + 'static, + ::Data: Send, + ::Error: std::error::Error + Sync + Send; + } + + #[async_trait] + pub trait ToggleHtmx { + type UrlParams: Send + Sync + 'static; + + const URL_TRUE: &'static str; + const URL_FALSE: &'static str; + + async fn set( + current_user: User, + state: AppState, + params: Self::UrlParams, + value: bool, + ) -> Result, crate::Error>; + + async fn set_true( + Extension(user): Extension, + State(state): State, + Path(path): Path, + ) -> Result, crate::Error> { + Self::set(user, state, path, true).await + } + + async fn set_false( + Extension(user): Extension, + State(state): State, + Path(path): Path, + ) -> Result, crate::Error> { + Self::set(user, state, path, false).await + } + + fn router() -> axum::Router + where + B: HttpBody + Send + 'static, + ::Data: Send, + ::Error: std::error::Error + Sync + Send; + } + + pub trait Toggle: ToggleHtmx + ToggleFallback { + fn router() -> axum::Router + where + B: HttpBody + Send + 'static, + ::Data: Send, + ::Error: std::error::Error + Sync + Send, + { + axum::Router::new() + .merge(::router()) + .merge(::router()) + } + } + #[async_trait] pub trait Delete: super::crud::Delete { type UrlParams: Send + Sync + 'static; @@ -198,7 +288,7 @@ pub mod route { const URL: &'static str; async fn delete( - user: Extension, + user: Extension, state: State, headers: HeaderMap, path: Path, diff --git a/src/components/trips/todos/mod.rs b/src/components/trips/todos/mod.rs index 665309d..82fbdb5 100644 --- a/src/components/trips/todos/mod.rs +++ b/src/components/trips/todos/mod.rs @@ -1,3 +1,5 @@ +#![allow(unused_variables)] + pub mod list; pub use list::List; @@ -6,6 +8,7 @@ use axum::{ extract::{Form, Path, State as StateExtractor}, http::HeaderMap, response::{IntoResponse, Redirect, Response}, + routing::post, Extension, }; use maud::{html, Markup}; @@ -216,10 +219,38 @@ impl crud::Create for Todo { } } +#[derive(Debug, PartialEq, Eq)] +pub struct StateUpdate { + new_state: State, +} + +impl From for StateUpdate { + fn from(state: bool) -> Self { + Self { + new_state: state.into(), + } + } +} + +impl From for StateUpdate { + fn from(new_state: State) -> Self { + Self { new_state } + } +} + +#[derive(Debug, PartialEq, Eq)] +pub struct DescriptionUpdate(String); + +impl From for DescriptionUpdate { + fn from(new_description: String) -> Self { + Self(new_description) + } +} + #[derive(Debug)] pub enum UpdateElement { - State(State), - Description(String), + State(StateUpdate), + Description(DescriptionUpdate), } #[async_trait] @@ -241,7 +272,7 @@ impl crud::Update for Todo { let todo_id_param = id.to_string(); match update_element { UpdateElement::State(state) => { - let done = state == State::Done; + let done = state == State::Done.into(); let result = crate::query_one!( &sqlite::QueryClassification { @@ -297,7 +328,7 @@ impl crud::Update for Todo { description, done "#, - new_description, + new_description.0, todo_id_param, trip_id_param, trip_id_param, @@ -468,12 +499,12 @@ impl view::View for Todo { href={ "/trips/" (input.trip_id) "/todo/" (self.id) - "/undone" + "/done/false" } hx-post={ "/trips/" (input.trip_id) "/todo/" (self.id) - "/undone" + "/done/htmx/false" } hx-target="closest li" hx-swap="outerHTML" @@ -494,12 +525,12 @@ impl view::View for Todo { href={ "/trips/" (input.trip_id) "/todo/" (self.id) - "/done" + "/done/true" } hx-post={ "/trips/" (input.trip_id) "/todo/" (self.id) - "/done" + "/done/htmx/true" } hx-target="closest li" hx-swap="outerHTML" @@ -676,36 +707,6 @@ impl route::Router for Todo { } } -#[tracing::instrument] -pub async fn trip_todo_done_htmx( - Extension(current_user): Extension, - StateExtractor(state): StateExtractor, - Path((trip_id, todo_id)): Path<(Uuid, Uuid)>, -) -> Result { - let ctx = Context::build(current_user); - Todo::update( - &ctx, - &state.database_pool, - Filter { trip_id }, - todo_id, - UpdateElement::State(State::Done), - ) - .await?; - - let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, todo_id) - .await? - .ok_or_else(|| { - crate::Error::Request(RequestError::NotFound { - message: format!("todo with id {todo_id} not found"), - }) - })?; - - Ok(todo_item.build(BuildInput { - trip_id, - state: UiState::Default, - })) -} - #[tracing::instrument] pub async fn trip_todo_done( Extension(current_user): Extension, @@ -719,7 +720,7 @@ pub async fn trip_todo_done( &state.database_pool, Filter { trip_id }, todo_id, - UpdateElement::State(State::Done), + UpdateElement::State(State::Done.into()), ) .await?; @@ -738,7 +739,7 @@ pub async fn trip_todo_undone_htmx( &state.database_pool, Filter { trip_id }, todo_id, - UpdateElement::State(State::Todo), + UpdateElement::State(State::Todo.into()), ) .await?; @@ -769,7 +770,7 @@ pub async fn trip_todo_undone( &state.database_pool, Filter { trip_id }, todo_id, - UpdateElement::State(State::Todo), + UpdateElement::State(State::Todo.into()), ) .await?; @@ -820,7 +821,7 @@ pub async fn trip_todo_edit_save( &state.database_pool, Filter { trip_id }, todo_id, - UpdateElement::Description(form.description), + UpdateElement::Description(form.description.into()), ) .await?; @@ -865,3 +866,96 @@ pub async fn trip_todo_edit_cancel( .into_response()), } } + +#[async_trait] +impl route::ToggleFallback for StateUpdate { + type UrlParams = (Uuid, Uuid); + + const URL_TRUE: &'static str = "/:id/done/true"; + const URL_FALSE: &'static str = "/:id/done/false"; + + async fn set( + current_user: User, + state: AppState, + headers: HeaderMap, + (trip_id, todo_id): (Uuid, Uuid), + value: bool, + ) -> Result, crate::Error> { + let ctx = Context::build(current_user); + Todo::update( + &ctx, + &state.database_pool, + Filter { trip_id }, + todo_id, + UpdateElement::State(value.into()), + ) + .await?; + + Ok(Redirect::to(get_referer(&headers)?).into_response()) + } + + fn router() -> axum::Router + where + B: HttpBody + Send + 'static, + ::Data: Send, + ::Error: std::error::Error + Sync + Send, + { + axum::Router::new() + .route(Self::URL_TRUE, post(Self::set_true)) + .route(Self::URL_FALSE, post(Self::set_false)) + } +} + +#[async_trait] +impl route::ToggleHtmx for StateUpdate { + type UrlParams = (Uuid, Uuid); + + const URL_TRUE: &'static str = "/:id/done/htmx/true"; + const URL_FALSE: &'static str = "/:id/done/htmx/false"; + + async fn set( + current_user: User, + state: AppState, + (trip_id, todo_id): (Uuid, Uuid), + value: bool, + ) -> Result, crate::Error> { + let ctx = Context::build(current_user); + Todo::update( + &ctx, + &state.database_pool, + Filter { trip_id }, + todo_id, + UpdateElement::State(value.into()), + ) + .await?; + + let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, todo_id) + .await? + .ok_or_else(|| { + crate::Error::Request(RequestError::NotFound { + message: format!("todo with id {todo_id} not found"), + }) + })?; + + Ok(todo_item + .build(BuildInput { + trip_id, + state: UiState::Default, + }) + .into_response()) + } + + fn router() -> axum::Router + where + B: HttpBody + Send + 'static, + ::Data: Send, + ::Error: std::error::Error + Sync + Send, + { + axum::Router::new() + .route(Self::URL_TRUE, post(Self::set_true)) + .route(Self::URL_FALSE, post(Self::set_false)) + } +} + +#[async_trait] +impl route::Toggle for StateUpdate {} diff --git a/src/routing/mod.rs b/src/routing/mod.rs index 73ce6b0..8f669a3 100644 --- a/src/routing/mod.rs +++ b/src/routing/mod.rs @@ -13,7 +13,10 @@ use std::{fmt, time::Duration}; use tower::{timeout::TimeoutLayer, ServiceBuilder}; use crate::{ - components::{self, route::Router as _}, + components::{ + self, + route::{Router as _, Toggle}, + }, AppState, Error, RequestError, TopLevelPage, }; @@ -143,16 +146,16 @@ pub fn router(state: AppState) -> Router { "/:id/items/:id/unready", get(trip_item_set_unready).post(trip_item_set_unready_htmx), ) - .route( - "/:id/todo/:id/done", - get(components::trips::todos::trip_todo_done) - .post(components::trips::todos::trip_todo_done_htmx), - ) - .route( - "/:id/todo/:id/undone", - get(components::trips::todos::trip_todo_undone) - .post(components::trips::todos::trip_todo_undone_htmx), - ) + // .route( + // "/:id/todo/:id/done", + // get(components::trips::todos::trip_todo_done) + // .post(components::trips::todos::trip_todo_done_htmx), + // ) + // .route( + // "/:id/todo/:id/undone", + // get(components::trips::todos::trip_todo_undone) + // .post(components::trips::todos::trip_todo_undone_htmx), + // ) .route( "/:id/todo/:id/edit", post(components::trips::todos::trip_todo_edit), @@ -165,7 +168,11 @@ pub fn router(state: AppState) -> Router { "/:id/todo/:id/edit/cancel", post(components::trips::todos::trip_todo_edit_cancel), ) - .nest("/:id/todo/", components::trips::todos::Todo::get()), + .nest( + "/:id/todo/", + components::trips::todos::Todo::get() + .merge(::router()), + ), ) .nest( (&TopLevelPage::Inventory.path()).into(),