Files
packager/src/components/trips/todos/mod.rs

1013 lines
29 KiB
Rust
Raw Normal View History

2023-09-17 17:34:06 +02:00
#![allow(unused_variables)]
2023-09-16 12:55:51 +02:00
pub mod list;
pub use list::List;
2023-09-16 00:45:51 +02:00
use axum::{
2023-09-17 15:45:02 +02:00
body::{BoxBody, HttpBody},
2023-09-17 16:00:42 +02:00
extract::{Form, Path, State as StateExtractor},
2023-09-16 00:45:51 +02:00
http::HeaderMap,
response::{IntoResponse, Redirect, Response},
2023-09-17 17:34:06 +02:00
routing::post,
2023-09-16 00:45:51 +02:00
Extension,
};
2023-09-13 17:09:09 +02:00
use maud::{html, Markup};
2023-09-16 00:45:51 +02:00
use serde::Deserialize;
2023-09-13 00:44:59 +02:00
use uuid::Uuid;
2023-09-16 00:45:51 +02:00
use crate::{
components::{
2023-09-17 19:00:22 +02:00
self,
2023-09-17 16:00:42 +02:00
crud::{self, Read, Update},
2023-09-17 19:00:22 +02:00
route::{self, Toggle},
2023-09-16 12:55:51 +02:00
view::{self, View},
2023-09-16 00:45:51 +02:00
},
htmx,
2023-09-17 16:00:42 +02:00
models::{user::User, Error},
routing::get_referer,
2023-09-16 00:45:51 +02:00
sqlite, AppState, Context, RequestError,
};
2023-09-15 13:13:56 +02:00
use async_trait::async_trait;
2023-09-16 12:55:51 +02:00
use crate::models::trips::Trip;
2023-09-13 17:09:09 +02:00
2023-09-13 00:44:59 +02:00
#[derive(Debug, PartialEq, Eq)]
pub enum State {
Todo,
Done,
}
impl From<bool> for State {
fn from(done: bool) -> Self {
if done {
Self::Done
} else {
Self::Todo
}
}
}
impl From<State> for bool {
fn from(value: State) -> Self {
match value {
State::Todo => false,
State::Done => true,
}
}
}
#[derive(Debug)]
pub struct Todo {
pub id: Uuid,
pub description: String,
pub state: State,
}
struct TodoRow {
id: String,
description: String,
done: bool,
}
impl TryFrom<TodoRow> for Todo {
type Error = Error;
fn try_from(row: TodoRow) -> Result<Self, Self::Error> {
Ok(Todo {
id: Uuid::try_parse(&row.id)?,
description: row.description,
state: row.done.into(),
})
}
}
2023-09-15 19:24:42 +02:00
#[derive(Debug, Clone)]
2023-09-16 12:55:51 +02:00
pub struct Filter {
2023-09-15 13:13:56 +02:00
pub trip_id: Uuid,
}
2023-09-17 19:00:22 +02:00
impl From<(Uuid, Uuid)> for Filter {
fn from((trip_id, _todo_id): (Uuid, Uuid)) -> Self {
Self { trip_id }
}
}
#[derive(Debug, Copy, Clone)]
pub struct Id(pub Uuid);
impl std::fmt::Display for Id {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.0)
}
}
impl From<(Uuid, Uuid)> for Id {
fn from((_trip_id, todo_id): (Uuid, Uuid)) -> Self {
Self(todo_id)
}
}
2023-09-13 00:44:59 +02:00
impl Todo {
pub fn is_done(&self) -> bool {
self.state == State::Done
}
2023-09-15 13:13:56 +02:00
}
2023-09-13 00:44:59 +02:00
2023-09-15 13:13:56 +02:00
#[async_trait]
impl crud::Read for Todo {
2023-09-16 12:55:51 +02:00
type Filter = Filter;
2023-09-17 19:00:22 +02:00
type Id = Id;
2023-09-15 13:13:56 +02:00
async fn findall(
2023-09-13 00:44:59 +02:00
ctx: &Context,
pool: &sqlite::Pool,
2023-09-16 12:55:51 +02:00
filter: Filter,
2023-09-13 00:44:59 +02:00
) -> Result<Vec<Self>, Error> {
2023-09-15 13:13:56 +02:00
let trip_id_param = filter.trip_id.to_string();
2023-09-13 00:44:59 +02:00
let user_id = ctx.user.id.to_string();
let todos: Vec<Todo> = crate::query_all!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Select,
component: sqlite::Component::Todo,
},
pool,
TodoRow,
Todo,
r#"
SELECT
todo.id AS id,
todo.description AS description,
todo.done AS done
FROM trip_todos AS todo
INNER JOIN trips
ON trips.id = todo.trip_id
WHERE
trips.id = $1
AND trips.user_id = $2
"#,
trip_id_param,
user_id,
)
.await?;
Ok(todos)
}
#[tracing::instrument]
2023-09-15 13:13:56 +02:00
async fn find(
2023-09-13 00:44:59 +02:00
ctx: &Context,
pool: &sqlite::Pool,
2023-09-16 12:55:51 +02:00
filter: Filter,
2023-09-17 19:00:22 +02:00
todo_id: Id,
2023-09-13 00:44:59 +02:00
) -> Result<Option<Self>, Error> {
2023-09-15 13:13:56 +02:00
let trip_id_param = filter.trip_id.to_string();
2023-09-17 19:00:22 +02:00
let todo_id_param = todo_id.0.to_string();
2023-09-13 00:44:59 +02:00
let user_id = ctx.user.id.to_string();
crate::query_one!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Select,
component: sqlite::Component::Todo,
},
pool,
TodoRow,
Self,
r#"
SELECT
todo.id AS id,
todo.description AS description,
todo.done AS done
FROM trip_todos AS todo
INNER JOIN trips
ON trips.id = todo.trip_id
WHERE
trips.id = $1
AND todo.id = $2
AND trips.user_id = $3
"#,
trip_id_param,
todo_id_param,
user_id,
)
.await
}
2023-09-15 13:13:56 +02:00
}
2023-09-13 00:44:59 +02:00
2023-09-15 13:13:56 +02:00
pub struct TodoNew {
pub description: String,
}
2023-09-13 00:44:59 +02:00
2023-09-15 13:13:56 +02:00
#[async_trait]
impl crud::Create for Todo {
2023-09-17 19:00:22 +02:00
type Id = Id;
2023-09-16 12:55:51 +02:00
type Filter = Filter;
2023-09-15 13:13:56 +02:00
type Info = TodoNew;
2023-09-13 17:09:09 +02:00
2023-09-15 13:13:56 +02:00
async fn create(
2023-09-13 17:09:09 +02:00
ctx: &Context,
pool: &sqlite::Pool,
2023-09-15 13:13:56 +02:00
filter: Self::Filter,
info: Self::Info,
) -> Result<Self::Id, Error> {
2023-09-13 17:09:09 +02:00
let user_id = ctx.user.id.to_string();
let id = Uuid::new_v4();
tracing::info!("adding new todo with id {id}");
let id_param = id.to_string();
2023-09-15 13:13:56 +02:00
let trip_id_param = filter.trip_id.to_string();
2023-09-13 17:09:09 +02:00
crate::execute!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Insert,
component: sqlite::Component::Todo,
},
pool,
2023-09-15 13:13:56 +02:00
r#"
INSERT INTO trip_todos
(id, description, done, trip_id)
SELECT ?, ?, false, id as trip_id
FROM trips
WHERE trip_id = ? AND EXISTS(SELECT 1 FROM trips WHERE id = ? and user_id = ?)
LIMIT 1
"#,
2023-09-13 17:09:09 +02:00
id_param,
2023-09-15 13:13:56 +02:00
info.description,
2023-09-13 17:09:09 +02:00
trip_id_param,
trip_id_param,
user_id,
)
.await?;
2023-09-17 19:00:22 +02:00
Ok(components::trips::todos::Id(id))
2023-09-13 17:09:09 +02:00
}
2023-09-15 13:13:56 +02:00
}
2023-09-17 17:34:06 +02:00
#[derive(Debug, PartialEq, Eq)]
pub struct StateUpdate {
new_state: State,
}
impl From<bool> for StateUpdate {
fn from(state: bool) -> Self {
Self {
new_state: state.into(),
}
}
}
impl From<State> for StateUpdate {
fn from(new_state: State) -> Self {
Self { new_state }
}
}
#[derive(Debug, PartialEq, Eq)]
pub struct DescriptionUpdate(String);
impl From<String> for DescriptionUpdate {
fn from(new_description: String) -> Self {
Self(new_description)
}
}
2023-09-15 13:13:56 +02:00
#[derive(Debug)]
2023-09-17 15:45:02 +02:00
pub enum UpdateElement {
2023-09-17 17:34:06 +02:00
State(StateUpdate),
Description(DescriptionUpdate),
2023-09-15 13:13:56 +02:00
}
#[async_trait]
impl crud::Update for Todo {
2023-09-17 19:00:22 +02:00
type Id = Id;
2023-09-16 12:55:51 +02:00
type Filter = Filter;
2023-09-17 15:45:02 +02:00
type UpdateElement = UpdateElement;
2023-09-15 13:13:56 +02:00
#[tracing::instrument]
async fn update(
ctx: &Context,
pool: &sqlite::Pool,
filter: Self::Filter,
id: Self::Id,
2023-09-17 15:45:02 +02:00
update_element: Self::UpdateElement,
2023-09-15 13:13:56 +02:00
) -> Result<Option<Self>, Error> {
let user_id = ctx.user.id.to_string();
let trip_id_param = filter.trip_id.to_string();
let todo_id_param = id.to_string();
2023-09-17 15:45:02 +02:00
match update_element {
UpdateElement::State(state) => {
2023-09-17 17:34:06 +02:00
let done = state == State::Done.into();
2023-09-15 13:13:56 +02:00
let result = crate::query_one!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Update,
component: sqlite::Component::Trips,
},
pool,
TodoRow,
Todo,
r#"
UPDATE trip_todos
SET done = ?
WHERE trip_id = ?
AND id = ?
AND EXISTS(SELECT 1 FROM trips WHERE id = ? AND user_id = ?)
RETURNING
id,
description,
done
"#,
done,
trip_id_param,
todo_id_param,
trip_id_param,
user_id
)
.await?;
Ok(result)
}
2023-09-17 15:45:02 +02:00
UpdateElement::Description(new_description) => {
2023-09-15 13:13:56 +02:00
let user_id = ctx.user.id.to_string();
let trip_id_param = filter.trip_id.to_string();
let todo_id_param = id.to_string();
let result = crate::query_one!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Update,
component: sqlite::Component::Todo,
},
pool,
TodoRow,
Todo,
r#"
UPDATE trip_todos
SET description = ?
WHERE
id = ?
AND trip_id = ?
AND EXISTS(SELECT 1 FROM trips WHERE trip_id = ? AND user_id = ?)
RETURNING
id,
description,
done
"#,
2023-09-17 17:34:06 +02:00
new_description.0,
2023-09-15 13:13:56 +02:00
todo_id_param,
trip_id_param,
trip_id_param,
user_id,
)
.await?;
Ok(result)
}
}
}
}
#[async_trait]
2023-09-15 19:24:42 +02:00
impl crud::Delete for Todo {
2023-09-17 19:00:22 +02:00
type Id = Id;
2023-09-16 12:55:51 +02:00
type Filter = Filter;
2023-09-13 17:09:09 +02:00
#[tracing::instrument]
2023-09-17 19:00:22 +02:00
async fn delete<'c, T>(ctx: &Context, db: T, filter: &Filter, id: Id) -> Result<bool, Error>
2023-09-15 18:30:19 +02:00
where
T: sqlx::Acquire<'c, Database = sqlx::Sqlite> + Send + std::fmt::Debug,
{
2023-09-17 19:00:22 +02:00
let id_param = id.0.to_string();
2023-09-13 17:09:09 +02:00
let user_id = ctx.user.id.to_string();
2023-09-15 13:13:56 +02:00
let trip_id_param = filter.trip_id.to_string();
2023-09-15 18:30:19 +02:00
2023-09-13 17:09:09 +02:00
let results = crate::execute!(
&sqlite::QueryClassification {
query_type: sqlite::QueryType::Delete,
component: sqlite::Component::Todo,
},
2023-09-15 19:24:42 +02:00
&mut *(db.acquire().await?),
2023-09-15 13:13:56 +02:00
r#"
DELETE FROM trip_todos
2023-09-15 18:30:19 +02:00
WHERE
2023-09-15 13:13:56 +02:00
id = ?
AND EXISTS (SELECT 1 FROM trips WHERE trip_id = ? AND user_id = ?)
"#,
2023-09-13 17:09:09 +02:00
id_param,
trip_id_param,
user_id,
)
.await?;
Ok(results.rows_affected() != 0)
}
}
#[derive(Debug, PartialEq, Eq)]
2023-09-16 12:55:51 +02:00
pub enum UiState {
2023-09-13 17:09:09 +02:00
Default,
Edit,
}
2023-09-15 13:13:56 +02:00
#[derive(Debug)]
2023-09-16 12:55:51 +02:00
pub struct BuildInput {
2023-09-15 13:13:56 +02:00
pub trip_id: Uuid,
2023-09-16 12:55:51 +02:00
pub state: UiState,
2023-09-15 13:13:56 +02:00
}
impl view::View for Todo {
2023-09-16 12:55:51 +02:00
type Input = BuildInput;
2023-09-15 13:13:56 +02:00
2023-09-13 17:09:09 +02:00
#[tracing::instrument]
2023-09-15 13:13:56 +02:00
fn build(&self, input: Self::Input) -> Markup {
2023-09-13 17:09:09 +02:00
let done = self.is_done();
html!(
li
."flex"
."flex-row"
."justify-start"
."items-stretch"
."bg-green-50"[done]
."bg-red-50"[!done]
."h-full"
{
2023-09-16 12:55:51 +02:00
@if input.state == UiState::Edit {
2023-09-13 17:09:09 +02:00
form
name="edit-todo"
id="edit-todo"
action={
2023-09-15 13:13:56 +02:00
"/trips/" (input.trip_id)
2023-09-13 17:09:09 +02:00
"/todo/" (self.id)
"/edit/save"
}
target="_self"
method="post"
hx-post={
2023-09-15 13:13:56 +02:00
"/trips/" (input.trip_id)
2023-09-13 17:09:09 +02:00
"/todo/" (self.id)
"/edit/save"
}
hx-target="closest li"
hx-swap="outerHTML"
{}
div
."flex"
."flex-row"
."aspect-square"
{
span
."mdi"
."m-auto"
."text-xl"
."mdi-check"[self.is_done()]
."mdi-checkbox-blank-outline"[!self.is_done()]
{}
}
div
."p-2"
.grow
{
input
."w-full"
type="text"
form="edit-todo"
id="todo-description"
name="todo-description"
value=(self.description)
{}
}
button
type="submit"
form="edit-todo"
."bg-green-200"
."hover:bg-green-300"
."flex"
."flex-row"
."aspect-square"
{
span
."mdi"
."m-auto"
."mdi-content-save"
."text-xl"
{}
}
a
href="."
hx-post={
2023-09-15 13:13:56 +02:00
"/trips/" (input.trip_id)
2023-09-13 17:09:09 +02:00
"/todo/" (self.id)
"/edit/cancel"
}
hx-target="closest li"
hx-swap="outerHTML"
."flex"
."flex-row"
."aspect-square"
."bg-red-200"
."hover:bg-red-300"
{
span
."mdi"
."mdi-cancel"
."text-xl"
."m-auto"
{}
}
} @else {
@if done {
a
."flex"
."flex-row"
."aspect-square"
."hover:bg-red-50"
href={
2023-09-15 13:13:56 +02:00
"/trips/" (input.trip_id)
2023-09-13 17:09:09 +02:00
"/todo/" (self.id)
2023-09-17 17:34:06 +02:00
"/done/false"
2023-09-13 17:09:09 +02:00
}
hx-post={
2023-09-15 13:13:56 +02:00
"/trips/" (input.trip_id)
2023-09-13 17:09:09 +02:00
"/todo/" (self.id)
2023-09-17 17:34:06 +02:00
"/done/htmx/false"
2023-09-13 17:09:09 +02:00
}
hx-target="closest li"
hx-swap="outerHTML"
{
span
."mdi"
."m-auto"
."text-xl"
."mdi-check"
{}
}
} @else {
a
."flex"
."flex-row"
."aspect-square"
."hover:bg-green-50"
href={
2023-09-15 13:13:56 +02:00
"/trips/" (input.trip_id)
2023-09-13 17:09:09 +02:00
"/todo/" (self.id)
2023-09-17 17:34:06 +02:00
"/done/true"
2023-09-13 17:09:09 +02:00
}
hx-post={
2023-09-15 13:13:56 +02:00
"/trips/" (input.trip_id)
2023-09-13 17:09:09 +02:00
"/todo/" (self.id)
2023-09-17 17:34:06 +02:00
"/done/htmx/true"
2023-09-13 17:09:09 +02:00
}
hx-target="closest li"
hx-swap="outerHTML"
{
span
."mdi"
."m-auto"
."text-xl"
."mdi-checkbox-blank-outline"
{}
}
}
span
."p-2"
."grow"
{
(self.description)
}
a
."flex"
."flex-row"
."aspect-square"
."bg-blue-200"
."hover:bg-blue-400"
href=(format!("?edit_todo={id}", id = self.id))
hx-post={
2023-09-15 13:13:56 +02:00
"/trips/" (input.trip_id)
2023-09-13 17:09:09 +02:00
"/todo/" (self.id)
"/edit"
}
hx-target="closest li"
hx-swap="outerHTML"
{
span ."m-auto" ."mdi" ."mdi-pencil" ."text-xl" {}
}
a
."flex"
."flex-row"
."aspect-square"
."bg-red-100"
."hover:bg-red-200"
href=(format!("?delete_todo={id}", id = self.id))
hx-post={
2023-09-15 13:13:56 +02:00
"/trips/" (input.trip_id)
2023-09-13 17:09:09 +02:00
"/todo/" (self.id)
"/delete"
}
hx-target="#todolist"
hx-swap="outerHTML"
{
span ."m-auto" ."mdi" ."mdi-delete-outline" ."text-xl" {}
}
}
}
)
}
}
2023-09-16 00:45:51 +02:00
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct TripTodoNew {
#[serde(rename = "new-todo-description")]
description: String,
}
#[async_trait]
impl route::Create for Todo {
2023-09-16 10:28:50 +02:00
type Form = TripTodoNew;
2023-09-17 15:45:02 +02:00
type UrlParams = (Uuid,);
2023-09-16 00:45:51 +02:00
2023-09-17 15:45:02 +02:00
const URL: &'static str = "/:id/todo/new";
2023-09-16 00:45:51 +02:00
#[tracing::instrument]
async fn create(
2023-09-17 16:00:42 +02:00
Extension(current_user): Extension<User>,
StateExtractor(state): StateExtractor<AppState>,
2023-09-16 00:45:51 +02:00
headers: HeaderMap,
2023-09-17 15:45:02 +02:00
Path((trip_id,)): Path<Self::UrlParams>,
2023-09-16 10:28:50 +02:00
Form(form): Form<Self::Form>,
2023-09-16 00:45:51 +02:00
) -> Result<Response<BoxBody>, crate::Error> {
let ctx = Context::build(current_user);
// method output is not required as we reload the whole trip todos anyway
let _todo_item = <Self as crud::Create>::create(
&ctx,
&state.database_pool,
2023-09-16 12:55:51 +02:00
Filter { trip_id },
2023-09-16 00:45:51 +02:00
TodoNew {
description: form.description,
},
)
.await?;
if htmx::is_htmx(&headers) {
let trip = Trip::find(&ctx, &state.database_pool, trip_id).await?;
match trip {
None => Err(crate::Error::Request(RequestError::NotFound {
message: format!("trip with id {trip_id} not found"),
})),
Some(mut trip) => {
trip.load_todos(&ctx, &state.database_pool).await?;
2023-09-16 12:55:51 +02:00
Ok(list::List {
2023-09-16 00:45:51 +02:00
trip: &trip,
2023-09-16 12:56:42 +02:00
todos: trip.todos(),
2023-09-16 00:45:51 +02:00
}
2023-09-16 12:55:51 +02:00
.build(list::BuildInput { edit_todo: None })
2023-09-16 00:45:51 +02:00
.into_response())
}
}
} else {
Ok(Redirect::to(&format!("/trips/{trip_id}/")).into_response())
}
}
}
2023-09-16 10:28:50 +02:00
#[async_trait]
impl route::Delete for Todo {
2023-09-17 15:45:02 +02:00
type UrlParams = (Uuid, Uuid);
2023-09-16 10:28:50 +02:00
2023-09-17 15:45:02 +02:00
const URL: &'static str = "/:id/todo/:id/delete";
2023-09-16 10:28:50 +02:00
#[tracing::instrument]
async fn delete(
2023-09-17 16:00:42 +02:00
Extension(current_user): Extension<User>,
StateExtractor(state): StateExtractor<AppState>,
2023-09-16 10:28:50 +02:00
_headers: HeaderMap,
2023-09-17 15:45:02 +02:00
Path((trip_id, todo_id)): Path<Self::UrlParams>,
2023-09-16 10:28:50 +02:00
) -> Result<Response<BoxBody>, crate::Error> {
let ctx = Context::build(current_user);
let deleted = <Self as crud::Delete>::delete(
&ctx,
&state.database_pool,
2023-09-16 12:55:51 +02:00
&Filter { trip_id },
2023-09-17 19:00:22 +02:00
components::trips::todos::Id(todo_id),
2023-09-16 10:28:50 +02:00
)
.await?;
if !deleted {
return Err(crate::Error::Request(RequestError::NotFound {
message: format!("todo with id {todo_id} not found"),
}));
}
let trip = crate::models::trips::Trip::find(&ctx, &state.database_pool, trip_id).await?;
match trip {
None => Err(crate::Error::Request(RequestError::NotFound {
message: format!("trip with id {trip_id} not found"),
})),
Some(mut trip) => {
trip.load_todos(&ctx, &state.database_pool).await?;
2023-09-16 12:55:51 +02:00
Ok(list::List {
2023-09-16 10:28:50 +02:00
trip: &trip,
2023-09-16 12:56:42 +02:00
todos: trip.todos(),
2023-09-16 10:28:50 +02:00
}
2023-09-16 12:55:51 +02:00
.build(list::BuildInput { edit_todo: None })
2023-09-16 10:28:50 +02:00
.into_response())
}
}
}
}
2023-09-17 15:45:02 +02:00
impl route::Router for Todo {
2023-09-17 19:00:22 +02:00
fn router<B>() -> axum::Router<AppState, B>
2023-09-17 15:45:02 +02:00
where
B: HttpBody + Send + 'static,
<B as HttpBody>::Data: Send,
<B as HttpBody>::Error: std::error::Error + Sync + Send,
{
axum::Router::new()
.route("/new", axum::routing::post(<Self as route::Create>::create))
.route(
"/:id/delete",
axum::routing::post(<Self as route::Delete>::delete),
)
2023-09-17 19:00:22 +02:00
.merge(StateUpdate::router())
2023-09-17 15:45:02 +02:00
}
}
2023-09-17 16:00:42 +02:00
#[tracing::instrument]
pub async fn trip_todo_done(
Extension(current_user): Extension<User>,
StateExtractor(state): StateExtractor<AppState>,
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
headers: HeaderMap,
) -> Result<impl IntoResponse, crate::Error> {
let ctx = Context::build(current_user);
Todo::update(
&ctx,
&state.database_pool,
Filter { trip_id },
2023-09-17 19:00:22 +02:00
Id(todo_id),
2023-09-17 17:34:06 +02:00
UpdateElement::State(State::Done.into()),
2023-09-17 16:00:42 +02:00
)
.await?;
Ok(Redirect::to(get_referer(&headers)?))
}
#[tracing::instrument]
pub async fn trip_todo_undone_htmx(
Extension(current_user): Extension<User>,
StateExtractor(state): StateExtractor<AppState>,
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
) -> Result<impl IntoResponse, crate::Error> {
let ctx = Context::build(current_user);
Todo::update(
&ctx,
&state.database_pool,
Filter { trip_id },
2023-09-17 19:00:22 +02:00
Id(todo_id),
2023-09-17 17:34:06 +02:00
UpdateElement::State(State::Todo.into()),
2023-09-17 16:00:42 +02:00
)
.await?;
2023-09-17 19:00:22 +02:00
let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, Id(todo_id))
2023-09-17 16:00:42 +02:00
.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_undone(
Extension(current_user): Extension<User>,
StateExtractor(state): StateExtractor<AppState>,
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
headers: HeaderMap,
) -> Result<impl IntoResponse, crate::Error> {
let ctx = Context::build(current_user);
Todo::update(
&ctx,
&state.database_pool,
Filter { trip_id },
2023-09-17 19:00:22 +02:00
Id(todo_id),
2023-09-17 17:34:06 +02:00
UpdateElement::State(State::Todo.into()),
2023-09-17 16:00:42 +02:00
)
.await?;
Ok(Redirect::to(get_referer(&headers)?))
}
#[derive(Deserialize, Debug)]
#[serde(deny_unknown_fields)]
pub struct TripTodoDescription {
#[serde(rename = "todo-description")]
description: String,
}
#[tracing::instrument]
pub async fn trip_todo_edit(
Extension(current_user): Extension<User>,
StateExtractor(state): StateExtractor<AppState>,
headers: HeaderMap,
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
) -> Result<impl IntoResponse, crate::Error> {
let ctx = Context::build(current_user);
2023-09-17 19:00:22 +02:00
let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, Id(todo_id)).await?;
2023-09-17 16:00:42 +02:00
match todo_item {
None => Err(crate::Error::Request(RequestError::NotFound {
message: format!("todo with id {todo_id} not found"),
})),
Some(todo_item) => Ok(todo_item
.build(BuildInput {
trip_id,
state: UiState::Edit,
})
.into_response()),
}
}
#[tracing::instrument]
pub async fn trip_todo_edit_save(
Extension(current_user): Extension<User>,
StateExtractor(state): StateExtractor<AppState>,
headers: HeaderMap,
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
Form(form): Form<TripTodoDescription>,
) -> Result<impl IntoResponse, crate::Error> {
let ctx = Context::build(current_user);
let todo_item = Todo::update(
&ctx,
&state.database_pool,
Filter { trip_id },
2023-09-17 19:00:22 +02:00
Id(todo_id),
2023-09-17 17:34:06 +02:00
UpdateElement::Description(form.description.into()),
2023-09-17 16:00:42 +02:00
)
.await?;
match todo_item {
None => Err(crate::Error::Request(RequestError::NotFound {
message: format!("todo with id {todo_id} not found"),
})),
Some(todo_item) => {
if htmx::is_htmx(&headers) {
Ok(todo_item
.build(BuildInput {
trip_id,
state: UiState::Default,
})
.into_response())
} else {
Ok(Redirect::to(&format!("/trips/{trip_id}/")).into_response())
}
}
}
}
#[tracing::instrument]
pub async fn trip_todo_edit_cancel(
Extension(current_user): Extension<User>,
StateExtractor(state): StateExtractor<AppState>,
headers: HeaderMap,
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
) -> Result<impl IntoResponse, crate::Error> {
let ctx = Context::build(current_user);
2023-09-17 19:00:22 +02:00
let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, Id(todo_id)).await?;
2023-09-17 16:00:42 +02:00
match todo_item {
None => Err(crate::Error::Request(RequestError::NotFound {
message: format!("todo with id {todo_id} not found"),
})),
Some(todo_item) => Ok(todo_item
.build(BuildInput {
trip_id,
state: UiState::Default,
})
.into_response()),
}
}
2023-09-17 17:34:06 +02:00
2023-09-17 19:00:22 +02:00
#[async_trait]
impl crud::Toggle for StateUpdate {
type Id = Id;
type Filter = Filter;
async fn set(
ctx: &Context,
pool: &sqlite::Pool,
filter: Self::Filter,
id: Self::Id,
value: bool,
) -> Result<(), crate::Error> {
Todo::update(&ctx, &pool, filter, id, UpdateElement::State(value.into())).await?;
Ok(())
}
}
2023-09-17 17:34:06 +02:00
#[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<Response<BoxBody>, crate::Error> {
let ctx = Context::build(current_user);
2023-09-17 19:00:22 +02:00
<Self as crud::Toggle>::set(
2023-09-17 17:34:06 +02:00
&ctx,
&state.database_pool,
Filter { trip_id },
2023-09-17 19:00:22 +02:00
Id(todo_id),
value,
2023-09-17 17:34:06 +02:00
)
.await?;
Ok(Redirect::to(get_referer(&headers)?).into_response())
}
fn router<B>() -> axum::Router<AppState, B>
where
B: HttpBody + Send + 'static,
<B as HttpBody>::Data: Send,
<B as HttpBody>::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 {
2023-09-17 19:00:22 +02:00
type Id = Id;
type Filter = Filter;
2023-09-17 17:34:06 +02:00
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,
2023-09-17 19:00:22 +02:00
params: Self::UrlParams,
2023-09-17 17:34:06 +02:00
value: bool,
2023-09-17 19:00:22 +02:00
) -> Result<(crate::Context, AppState, Self::UrlParams, bool), crate::Error> {
2023-09-17 17:34:06 +02:00
let ctx = Context::build(current_user);
2023-09-17 19:00:22 +02:00
<Self as crud::Toggle>::set(
2023-09-17 17:34:06 +02:00
&ctx,
&state.database_pool,
2023-09-17 19:00:22 +02:00
params.into(),
params.into(),
value,
2023-09-17 17:34:06 +02:00
)
.await?;
2023-09-17 19:00:22 +02:00
Ok((ctx, state, params, value))
}
async fn response(
ctx: &Context,
state: AppState,
(trip_id, todo_id): (Uuid, Uuid),
value: bool,
) -> Result<Response<BoxBody>, crate::Error> {
let todo_item = Todo::find(&ctx, &state.database_pool, Filter { trip_id }, Id(todo_id))
2023-09-17 17:34:06 +02:00
.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<B>() -> axum::Router<AppState, B>
where
B: HttpBody + Send + 'static,
<B as HttpBody>::Data: Send,
<B as HttpBody>::Error: std::error::Error + Sync + Send,
{
axum::Router::new()
2023-09-17 19:00:22 +02:00
.route(Self::URL_TRUE, post(Self::on))
.route(Self::URL_FALSE, post(Self::off))
2023-09-17 17:34:06 +02:00
}
}
#[async_trait]
impl route::Toggle for StateUpdate {}