refactor todo list

This commit is contained in:
2023-09-16 12:55:51 +02:00
parent 1ffb2c5e74
commit 1c26b7de45
8 changed files with 194 additions and 173 deletions

View File

@@ -1,3 +1,5 @@
pub mod trips;
pub mod crud { pub mod crud {
use async_trait::async_trait; use async_trait::async_trait;

View File

@@ -0,0 +1 @@
pub mod todos;

View File

@@ -0,0 +1,109 @@
use maud::{html, Markup};
use uuid::Uuid;
use super::Todo;
use crate::components::view;
use crate::models::trips::Trip;
#[derive(Debug)]
pub struct List<'a> {
pub trip: &'a Trip,
pub todos: &'a Vec<Todo>,
}
#[derive(Debug)]
pub struct BuildInput {
pub edit_todo: Option<Uuid>,
}
impl<'a> view::View for List<'a> {
type Input = BuildInput;
#[tracing::instrument]
fn build(&self, input: Self::Input) -> Markup {
html!(
div #todolist {
h1 ."text-xl" ."mb-5" { "Todos" }
ul
."flex"
."flex-col"
{
@for todo in self.todos {
@let state = input.edit_todo
.map(|id| if todo.id == id {
super::UiState::Edit
} else {
super::UiState::Default
}).unwrap_or(super::UiState::Default);
(todo.build(super::BuildInput{trip_id:self.trip.id, state}))
}
(NewTodo::build(&self.trip.id))
}
}
)
}
}
pub struct NewTodo;
impl NewTodo {
#[tracing::instrument]
pub fn build(trip_id: &Uuid) -> Markup {
html!(
li
."flex"
."flex-row"
."justify-start"
."items-stretch"
."h-full"
{
form
name="new-todo"
id="new-todo"
action={
"/trips/" (trip_id)
"/todo/new"
}
target="_self"
method="post"
hx-post={
"/trips/" (trip_id)
"/todo/new"
}
hx-target="#todolist"
hx-swap="outerHTML"
{}
button
type="submit"
form="new-todo"
."bg-green-200"
."hover:bg-green-300"
."flex"
."flex-row"
."aspect-square"
{
span
."mdi"
."m-auto"
."mdi-plus"
."text-xl"
{}
}
div
."border-4"
."p-1"
.grow
{
input
."appearance-none"
."w-full"
type="text"
form="new-todo"
id="new-todo-description"
name="new-todo-description"
{}
}
}
)
}
}

View File

@@ -1,3 +1,6 @@
pub mod list;
pub use list::List;
use axum::{ use axum::{
body::BoxBody, body::BoxBody,
extract::{Form, Path}, extract::{Form, Path},
@@ -12,7 +15,7 @@ use uuid::Uuid;
use crate::{ use crate::{
components::{ components::{
crud, route, crud, route,
view::{self, *}, view::{self, View},
}, },
htmx, htmx,
models::Error, models::Error,
@@ -21,7 +24,7 @@ use crate::{
use async_trait::async_trait; use async_trait::async_trait;
use super::Trip; use crate::models::trips::Trip;
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum State { pub enum State {
@@ -74,7 +77,7 @@ impl TryFrom<TodoRow> for Todo {
} }
#[derive(Debug, Clone)] #[derive(Debug, Clone)]
pub struct TodoFilter { pub struct Filter {
pub trip_id: Uuid, pub trip_id: Uuid,
} }
@@ -86,13 +89,13 @@ impl Todo {
#[async_trait] #[async_trait]
impl crud::Read for Todo { impl crud::Read for Todo {
type Filter = TodoFilter; type Filter = Filter;
type Id = Uuid; type Id = Uuid;
async fn findall( async fn findall(
ctx: &Context, ctx: &Context,
pool: &sqlite::Pool, pool: &sqlite::Pool,
filter: TodoFilter, filter: Filter,
) -> Result<Vec<Self>, Error> { ) -> Result<Vec<Self>, Error> {
let trip_id_param = filter.trip_id.to_string(); let trip_id_param = filter.trip_id.to_string();
let user_id = ctx.user.id.to_string(); let user_id = ctx.user.id.to_string();
@@ -129,7 +132,7 @@ impl crud::Read for Todo {
async fn find( async fn find(
ctx: &Context, ctx: &Context,
pool: &sqlite::Pool, pool: &sqlite::Pool,
filter: TodoFilter, filter: Filter,
todo_id: Uuid, todo_id: Uuid,
) -> Result<Option<Self>, Error> { ) -> Result<Option<Self>, Error> {
let trip_id_param = filter.trip_id.to_string(); let trip_id_param = filter.trip_id.to_string();
@@ -171,7 +174,7 @@ pub struct TodoNew {
#[async_trait] #[async_trait]
impl crud::Create for Todo { impl crud::Create for Todo {
type Id = Uuid; type Id = Uuid;
type Filter = TodoFilter; type Filter = Filter;
type Info = TodoNew; type Info = TodoNew;
async fn create( async fn create(
@@ -212,7 +215,7 @@ impl crud::Create for Todo {
} }
#[derive(Debug)] #[derive(Debug)]
pub enum TodoUpdate { pub enum Update {
State(State), State(State),
Description(String), Description(String),
} }
@@ -220,8 +223,8 @@ pub enum TodoUpdate {
#[async_trait] #[async_trait]
impl crud::Update for Todo { impl crud::Update for Todo {
type Id = Uuid; type Id = Uuid;
type Filter = TodoFilter; type Filter = Filter;
type Update = TodoUpdate; type Update = Update;
#[tracing::instrument] #[tracing::instrument]
async fn update( async fn update(
@@ -235,7 +238,7 @@ impl crud::Update for Todo {
let trip_id_param = filter.trip_id.to_string(); let trip_id_param = filter.trip_id.to_string();
let todo_id_param = id.to_string(); let todo_id_param = id.to_string();
match update { match update {
TodoUpdate::State(state) => { Update::State(state) => {
let done = state == State::Done; let done = state == State::Done;
let result = crate::query_one!( let result = crate::query_one!(
@@ -267,7 +270,7 @@ impl crud::Update for Todo {
Ok(result) Ok(result)
} }
TodoUpdate::Description(new_description) => { Update::Description(new_description) => {
let user_id = ctx.user.id.to_string(); let user_id = ctx.user.id.to_string();
let trip_id_param = filter.trip_id.to_string(); let trip_id_param = filter.trip_id.to_string();
let todo_id_param = id.to_string(); let todo_id_param = id.to_string();
@@ -309,15 +312,10 @@ impl crud::Update for Todo {
#[async_trait] #[async_trait]
impl crud::Delete for Todo { impl crud::Delete for Todo {
type Id = Uuid; type Id = Uuid;
type Filter = TodoFilter; type Filter = Filter;
#[tracing::instrument] #[tracing::instrument]
async fn delete<'c, T>( async fn delete<'c, T>(ctx: &Context, db: T, filter: &Filter, id: Uuid) -> Result<bool, Error>
ctx: &Context,
db: T,
filter: &TodoFilter,
id: Uuid,
) -> Result<bool, Error>
where where
T: sqlx::Acquire<'c, Database = sqlx::Sqlite> + Send + std::fmt::Debug, T: sqlx::Acquire<'c, Database = sqlx::Sqlite> + Send + std::fmt::Debug,
{ {
@@ -348,19 +346,19 @@ impl crud::Delete for Todo {
} }
#[derive(Debug, PartialEq, Eq)] #[derive(Debug, PartialEq, Eq)]
pub enum TodoUiState { pub enum UiState {
Default, Default,
Edit, Edit,
} }
#[derive(Debug)] #[derive(Debug)]
pub struct TodoBuildInput { pub struct BuildInput {
pub trip_id: Uuid, pub trip_id: Uuid,
pub state: TodoUiState, pub state: UiState,
} }
impl view::View for Todo { impl view::View for Todo {
type Input = TodoBuildInput; type Input = BuildInput;
#[tracing::instrument] #[tracing::instrument]
fn build(&self, input: Self::Input) -> Markup { fn build(&self, input: Self::Input) -> Markup {
@@ -375,7 +373,7 @@ impl view::View for Todo {
."bg-red-50"[!done] ."bg-red-50"[!done]
."h-full" ."h-full"
{ {
@if input.state == TodoUiState::Edit { @if input.state == UiState::Edit {
form form
name="edit-todo" name="edit-todo"
id="edit-todo" id="edit-todo"
@@ -586,7 +584,7 @@ impl route::Create for Todo {
let _todo_item = <Self as crud::Create>::create( let _todo_item = <Self as crud::Create>::create(
&ctx, &ctx,
&state.database_pool, &state.database_pool,
TodoFilter { trip_id }, Filter { trip_id },
TodoNew { TodoNew {
description: form.description, description: form.description,
}, },
@@ -601,11 +599,11 @@ impl route::Create for Todo {
})), })),
Some(mut trip) => { Some(mut trip) => {
trip.load_todos(&ctx, &state.database_pool).await?; trip.load_todos(&ctx, &state.database_pool).await?;
Ok(crate::models::trips::todos::TodoList { Ok(list::List {
trip: &trip, trip: &trip,
todos: &trip.todos(), todos: &trip.todos(),
} }
.build(None) .build(list::BuildInput { edit_todo: None })
.into_response()) .into_response())
} }
} }
@@ -633,7 +631,7 @@ impl route::Delete for Todo {
let deleted = <Self as crud::Delete>::delete( let deleted = <Self as crud::Delete>::delete(
&ctx, &ctx,
&state.database_pool, &state.database_pool,
&TodoFilter { trip_id }, &Filter { trip_id },
todo_id, todo_id,
) )
.await?; .await?;
@@ -651,109 +649,13 @@ impl route::Delete for Todo {
})), })),
Some(mut trip) => { Some(mut trip) => {
trip.load_todos(&ctx, &state.database_pool).await?; trip.load_todos(&ctx, &state.database_pool).await?;
Ok(TodoList { Ok(list::List {
trip: &trip, trip: &trip,
todos: &trip.todos(), todos: &trip.todos(),
} }
.build(None) .build(list::BuildInput { edit_todo: None })
.into_response()) .into_response())
} }
} }
} }
} }
pub struct NewTodo;
impl NewTodo {
#[tracing::instrument]
pub fn build(trip_id: &Uuid) -> Markup {
html!(
li
."flex"
."flex-row"
."justify-start"
."items-stretch"
."h-full"
{
form
name="new-todo"
id="new-todo"
action={
"/trips/" (trip_id)
"/todo/new"
}
target="_self"
method="post"
hx-post={
"/trips/" (trip_id)
"/todo/new"
}
hx-target="#todolist"
hx-swap="outerHTML"
{}
button
type="submit"
form="new-todo"
."bg-green-200"
."hover:bg-green-300"
."flex"
."flex-row"
."aspect-square"
{
span
."mdi"
."m-auto"
."mdi-plus"
."text-xl"
{}
}
div
."border-4"
."p-1"
.grow
{
input
."appearance-none"
."w-full"
type="text"
form="new-todo"
id="new-todo-description"
name="new-todo-description"
{}
}
}
)
}
}
#[derive(Debug)]
pub struct TodoList<'a> {
pub trip: &'a Trip,
pub todos: &'a Vec<Todo>,
}
impl<'a> TodoList<'a> {
#[tracing::instrument]
pub fn build(&self, edit_todo: Option<Uuid>) -> Markup {
html!(
div #todolist {
h1 ."text-xl" ."mb-5" { "Todos" }
ul
."flex"
."flex-col"
{
@for todo in self.todos {
@let state = edit_todo
.map(|id| if todo.id == id {
TodoUiState::Edit
} else {
TodoUiState::Default
}).unwrap_or(TodoUiState::Default);
(todo.build(TodoBuildInput{trip_id:self.trip.id, state}))
}
(NewTodo::build(&self.trip.id))
}
}
)
}
}

View File

@@ -14,8 +14,6 @@ use serde::{Deserialize, Serialize};
use time; use time;
use uuid::Uuid; use uuid::Uuid;
pub mod todos;
// #[macro_use] // #[macro_use]
// mod macros { // mod macros {
// macro_rules! build_state_query { // macro_rules! build_state_query {
@@ -512,7 +510,7 @@ pub struct Trip {
pub temp_min: Option<i64>, pub temp_min: Option<i64>,
pub temp_max: Option<i64>, pub temp_max: Option<i64>,
pub comment: Option<String>, pub comment: Option<String>,
pub todos: Option<Vec<todos::Todo>>, pub todos: Option<Vec<crate::components::trips::todos::Todo>>,
pub types: Option<Vec<TripType>>, pub types: Option<Vec<TripType>>,
pub categories: Option<Vec<TripCategory>>, pub categories: Option<Vec<TripCategory>>,
} }
@@ -999,7 +997,7 @@ impl Trip {
} }
#[tracing::instrument] #[tracing::instrument]
pub fn todos(&self) -> &Vec<todos::Todo> { pub fn todos(&self) -> &Vec<crate::components::trips::todos::Todo> {
self.todos.as_ref().expect("you need to call load_todos()") self.todos.as_ref().expect("you need to call load_todos()")
} }
@@ -1021,8 +1019,14 @@ impl Trip {
#[tracing::instrument] #[tracing::instrument]
pub async fn load_todos(&mut self, ctx: &Context, pool: &sqlite::Pool) -> Result<(), Error> { pub async fn load_todos(&mut self, ctx: &Context, pool: &sqlite::Pool) -> Result<(), Error> {
self.todos = self.todos = Some(
Some(todos::Todo::findall(ctx, pool, todos::TodoFilter { trip_id: self.id }).await?); crate::components::trips::todos::Todo::findall(
ctx,
pool,
crate::components::trips::todos::Filter { trip_id: self.id },
)
.await?,
);
Ok(()) Ok(())
} }

View File

@@ -152,16 +152,16 @@ pub fn router(state: AppState) -> Router {
.route("/:id/todo/:id/edit/save", post(trip_todo_edit_save)) .route("/:id/todo/:id/edit/save", post(trip_todo_edit_save))
.route("/:id/todo/:id/edit/cancel", post(trip_todo_edit_cancel)) .route("/:id/todo/:id/edit/cancel", post(trip_todo_edit_cancel))
.route( .route(
&<crate::models::trips::todos::Todo as route::Create>::with_prefix( &<crate::components::trips::todos::Todo as route::Create>::with_prefix(
"/:id/todo", "/:id/todo",
), ),
post(<crate::models::trips::todos::Todo as route::Create>::create), post(<crate::components::trips::todos::Todo as route::Create>::create),
) )
.route( .route(
&<crate::models::trips::todos::Todo as route::Delete>::with_prefix( &<crate::components::trips::todos::Todo as route::Delete>::with_prefix(
"/:id/todo", "/:id/todo",
), ),
post(<crate::models::trips::todos::Todo as route::Delete>::delete), post(<crate::components::trips::todos::Todo as route::Delete>::delete),
), ),
) )
.nest( .nest(

View File

@@ -5,9 +5,10 @@ use axum::{
Form, Form,
}; };
use crate::components;
use crate::components::crud::*; use crate::components::crud::*;
use crate::components::trips::todos;
use crate::components::view::*; use crate::components::view::*;
use crate::models::trips::todos::{TodoBuildInput, TodoFilter, TodoUpdate};
use crate::view::Component; use crate::view::Component;
@@ -437,10 +438,10 @@ pub async fn trip(
state.client_state.active_category_id = trip_query.category; state.client_state.active_category_id = trip_query.category;
if let Some(delete_todo) = trip_query.delete_todo { if let Some(delete_todo) = trip_query.delete_todo {
let deleted = models::trips::todos::Todo::delete( let deleted = components::trips::todos::Todo::delete(
&ctx, &ctx,
&state.database_pool, &state.database_pool,
&TodoFilter { trip_id: id }, &todos::Filter { trip_id: id },
delete_todo, delete_todo,
) )
.await?; .await?;
@@ -1275,19 +1276,19 @@ pub async fn trip_todo_done_htmx(
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>, Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let ctx = Context::build(current_user); let ctx = Context::build(current_user);
models::trips::todos::Todo::update( components::trips::todos::Todo::update(
&ctx, &ctx,
&state.database_pool, &state.database_pool,
TodoFilter { trip_id }, todos::Filter { trip_id },
todo_id, todo_id,
TodoUpdate::State(models::trips::todos::State::Done), todos::Update::State(components::trips::todos::State::Done),
) )
.await?; .await?;
let todo_item = models::trips::todos::Todo::find( let todo_item = components::trips::todos::Todo::find(
&ctx, &ctx,
&state.database_pool, &state.database_pool,
TodoFilter { trip_id }, todos::Filter { trip_id },
todo_id, todo_id,
) )
.await? .await?
@@ -1297,9 +1298,9 @@ pub async fn trip_todo_done_htmx(
}) })
})?; })?;
Ok(todo_item.build(TodoBuildInput { Ok(todo_item.build(todos::BuildInput {
trip_id, trip_id,
state: models::trips::todos::TodoUiState::Default, state: components::trips::todos::UiState::Default,
})) }))
} }
@@ -1311,12 +1312,12 @@ pub async fn trip_todo_done(
headers: HeaderMap, headers: HeaderMap,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let ctx = Context::build(current_user); let ctx = Context::build(current_user);
models::trips::todos::Todo::update( components::trips::todos::Todo::update(
&ctx, &ctx,
&state.database_pool, &state.database_pool,
TodoFilter { trip_id }, todos::Filter { trip_id },
todo_id, todo_id,
TodoUpdate::State(models::trips::todos::State::Done), todos::Update::State(components::trips::todos::State::Done),
) )
.await?; .await?;
@@ -1330,19 +1331,19 @@ pub async fn trip_todo_undone_htmx(
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>, Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let ctx = Context::build(current_user); let ctx = Context::build(current_user);
models::trips::todos::Todo::update( components::trips::todos::Todo::update(
&ctx, &ctx,
&state.database_pool, &state.database_pool,
TodoFilter { trip_id }, todos::Filter { trip_id },
todo_id, todo_id,
TodoUpdate::State(models::trips::todos::State::Todo), todos::Update::State(components::trips::todos::State::Todo),
) )
.await?; .await?;
let todo_item = models::trips::todos::Todo::find( let todo_item = components::trips::todos::Todo::find(
&ctx, &ctx,
&state.database_pool, &state.database_pool,
TodoFilter { trip_id }, todos::Filter { trip_id },
todo_id, todo_id,
) )
.await? .await?
@@ -1352,9 +1353,9 @@ pub async fn trip_todo_undone_htmx(
}) })
})?; })?;
Ok(todo_item.build(TodoBuildInput { Ok(todo_item.build(todos::BuildInput {
trip_id, trip_id,
state: models::trips::todos::TodoUiState::Default, state: components::trips::todos::UiState::Default,
})) }))
} }
@@ -1366,12 +1367,12 @@ pub async fn trip_todo_undone(
headers: HeaderMap, headers: HeaderMap,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let ctx = Context::build(current_user); let ctx = Context::build(current_user);
models::trips::todos::Todo::update( components::trips::todos::Todo::update(
&ctx, &ctx,
&state.database_pool, &state.database_pool,
TodoFilter { trip_id }, todos::Filter { trip_id },
todo_id, todo_id,
TodoUpdate::State(models::trips::todos::State::Todo), todos::Update::State(components::trips::todos::State::Todo),
) )
.await?; .await?;
@@ -1393,10 +1394,10 @@ pub async fn trip_todo_edit(
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>, Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let ctx = Context::build(current_user); let ctx = Context::build(current_user);
let todo_item = models::trips::todos::Todo::find( let todo_item = components::trips::todos::Todo::find(
&ctx, &ctx,
&state.database_pool, &state.database_pool,
TodoFilter { trip_id }, todos::Filter { trip_id },
todo_id, todo_id,
) )
.await?; .await?;
@@ -1406,9 +1407,9 @@ pub async fn trip_todo_edit(
message: format!("todo with id {todo_id} not found"), message: format!("todo with id {todo_id} not found"),
})), })),
Some(todo_item) => Ok(todo_item Some(todo_item) => Ok(todo_item
.build(TodoBuildInput { .build(todos::BuildInput {
trip_id, trip_id,
state: models::trips::todos::TodoUiState::Edit, state: components::trips::todos::UiState::Edit,
}) })
.into_response()), .into_response()),
} }
@@ -1423,12 +1424,12 @@ pub async fn trip_todo_edit_save(
Form(form): Form<TripTodoDescription>, Form(form): Form<TripTodoDescription>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let ctx = Context::build(current_user); let ctx = Context::build(current_user);
let todo_item = models::trips::todos::Todo::update( let todo_item = components::trips::todos::Todo::update(
&ctx, &ctx,
&state.database_pool, &state.database_pool,
TodoFilter { trip_id }, todos::Filter { trip_id },
todo_id, todo_id,
TodoUpdate::Description(form.description), todos::Update::Description(form.description),
) )
.await?; .await?;
@@ -1439,9 +1440,9 @@ pub async fn trip_todo_edit_save(
Some(todo_item) => { Some(todo_item) => {
if htmx::is_htmx(&headers) { if htmx::is_htmx(&headers) {
Ok(todo_item Ok(todo_item
.build(TodoBuildInput { .build(todos::BuildInput {
trip_id, trip_id,
state: models::trips::todos::TodoUiState::Default, state: components::trips::todos::UiState::Default,
}) })
.into_response()) .into_response())
} else { } else {
@@ -1459,10 +1460,10 @@ pub async fn trip_todo_edit_cancel(
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>, Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
) -> Result<impl IntoResponse, Error> { ) -> Result<impl IntoResponse, Error> {
let ctx = Context::build(current_user); let ctx = Context::build(current_user);
let todo_item = models::trips::todos::Todo::find( let todo_item = components::trips::todos::Todo::find(
&ctx, &ctx,
&state.database_pool, &state.database_pool,
TodoFilter { trip_id }, todos::Filter { trip_id },
todo_id, todo_id,
) )
.await?; .await?;
@@ -1472,9 +1473,9 @@ pub async fn trip_todo_edit_cancel(
message: format!("todo with id {todo_id} not found"), message: format!("todo with id {todo_id} not found"),
})), })),
Some(todo_item) => Ok(todo_item Some(todo_item) => Ok(todo_item
.build(TodoBuildInput { .build(todos::BuildInput {
trip_id, trip_id,
state: models::trips::todos::TodoUiState::Default, state: components::trips::todos::UiState::Default,
}) })
.into_response()), .into_response()),
} }

View File

@@ -11,6 +11,8 @@ pub struct TripManager;
pub mod packagelist; pub mod packagelist;
pub mod types; pub mod types;
use crate::components::view::View;
impl TripManager { impl TripManager {
#[tracing::instrument] #[tracing::instrument]
pub fn build(trips: Vec<models::trips::Trip>) -> Markup { pub fn build(trips: Vec<models::trips::Trip>) -> Markup {
@@ -371,7 +373,7 @@ impl Trip {
} }
} }
(TripInfo::build(trip_edit_attribute, trip)) (TripInfo::build(trip_edit_attribute, trip))
(crate::models::trips::todos::TodoList{todos: trip.todos(), trip: &trip}.build(edit_todo)) (crate::components::trips::todos::List{todos: trip.todos(), trip: &trip}.build(crate::components::trips::todos::list::BuildInput { edit_todo}))
(TripComment::build(trip)) (TripComment::build(trip))
(TripItems::build(active_category, trip)) (TripItems::build(active_category, trip))
} }