todos work
This commit is contained in:
32
.sqlx/query-33cba5d1fbcfb492f8f8443782c45f9326e3fa966b3f8e864b3e01d4fe7b25b8.json
generated
Normal file
32
.sqlx/query-33cba5d1fbcfb492f8f8443782c45f9326e3fa966b3f8e864b3e01d4fe7b25b8.json
generated
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "UPDATE trip_todos\n SET description = ?\n WHERE \n id = ? \n AND trip_id = ?\n AND EXISTS(SELECT 1 FROM trips WHERE trip_id = ? AND user_id = ?)\n RETURNING\n id,\n description,\n done\n ",
|
||||||
|
"describe": {
|
||||||
|
"columns": [
|
||||||
|
{
|
||||||
|
"name": "id",
|
||||||
|
"ordinal": 0,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "description",
|
||||||
|
"ordinal": 1,
|
||||||
|
"type_info": "Text"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"name": "done",
|
||||||
|
"ordinal": 2,
|
||||||
|
"type_info": "Bool"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 5
|
||||||
|
},
|
||||||
|
"nullable": [
|
||||||
|
false,
|
||||||
|
false,
|
||||||
|
false
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"hash": "33cba5d1fbcfb492f8f8443782c45f9326e3fa966b3f8e864b3e01d4fe7b25b8"
|
||||||
|
}
|
||||||
12
.sqlx/query-5ac0e60ed79f626300f0dfde880f92d4eae3aa4281eafc2ac29fdb83525e0536.json
generated
Normal file
12
.sqlx/query-5ac0e60ed79f626300f0dfde880f92d4eae3aa4281eafc2ac29fdb83525e0536.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "DELETE FROM trip_todos\n WHERE \n id = ?\n AND EXISTS (SELECT 1 FROM trips WHERE trip_id = ? AND user_id = ?)",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 3
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "5ac0e60ed79f626300f0dfde880f92d4eae3aa4281eafc2ac29fdb83525e0536"
|
||||||
|
}
|
||||||
12
.sqlx/query-9784595191d25448b2a24c856288d8fa3ba73c423cafcaa555c6f0a588b622a3.json
generated
Normal file
12
.sqlx/query-9784595191d25448b2a24c856288d8fa3ba73c423cafcaa555c6f0a588b622a3.json
generated
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
{
|
||||||
|
"db_name": "SQLite",
|
||||||
|
"query": "INSERT INTO trip_todos\n (id, description, done, trip_id)\n SELECT ?, ?, false, id as trip_id\n FROM trips\n WHERE trip_id = ? AND EXISTS(SELECT 1 FROM trips WHERE id = ? and user_id = ?)\n LIMIT 1",
|
||||||
|
"describe": {
|
||||||
|
"columns": [],
|
||||||
|
"parameters": {
|
||||||
|
"Right": 5
|
||||||
|
},
|
||||||
|
"nullable": []
|
||||||
|
},
|
||||||
|
"hash": "9784595191d25448b2a24c856288d8fa3ba73c423cafcaa555c6f0a588b622a3"
|
||||||
|
}
|
||||||
19
Cargo.toml
19
Cargo.toml
@@ -8,9 +8,14 @@ name = "packager"
|
|||||||
path = "src/main.rs"
|
path = "src/main.rs"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
jaeger = []
|
jaeger = [
|
||||||
prometheus = []
|
"dep:opentelemetry",
|
||||||
tokio-console = []
|
"dep:tracing-opentelemetry",
|
||||||
|
"dep:opentelemetry-jaeger",
|
||||||
|
"tokio/tracing"
|
||||||
|
]
|
||||||
|
prometheus = ["dep:axum-prometheus"]
|
||||||
|
tokio-console = ["dep:console-subscriber"]
|
||||||
|
|
||||||
default = ["jaeger", "prometheus", "tokio-console"]
|
default = ["jaeger", "prometheus", "tokio-console"]
|
||||||
|
|
||||||
@@ -20,9 +25,11 @@ lto = "off"
|
|||||||
|
|
||||||
[dependencies.opentelemetry]
|
[dependencies.opentelemetry]
|
||||||
version = "0.20"
|
version = "0.20"
|
||||||
|
optional = true
|
||||||
|
|
||||||
[dependencies.tracing-opentelemetry]
|
[dependencies.tracing-opentelemetry]
|
||||||
version = "0.21"
|
version = "0.21"
|
||||||
|
optional = true
|
||||||
|
|
||||||
[dependencies.tracing-log]
|
[dependencies.tracing-log]
|
||||||
version = "0.1"
|
version = "0.1"
|
||||||
@@ -30,6 +37,7 @@ version = "0.1"
|
|||||||
[dependencies.opentelemetry-jaeger]
|
[dependencies.opentelemetry-jaeger]
|
||||||
version = "0.19"
|
version = "0.19"
|
||||||
features = ["rt-tokio"]
|
features = ["rt-tokio"]
|
||||||
|
optional = true
|
||||||
|
|
||||||
[dependencies.http]
|
[dependencies.http]
|
||||||
version = "0.2"
|
version = "0.2"
|
||||||
@@ -47,10 +55,11 @@ features = ["headers"]
|
|||||||
|
|
||||||
[dependencies.tokio]
|
[dependencies.tokio]
|
||||||
version = "1"
|
version = "1"
|
||||||
features = ["macros", "rt-multi-thread", "tracing"]
|
features = ["macros", "rt-multi-thread"]
|
||||||
|
|
||||||
[dependencies.console-subscriber]
|
[dependencies.console-subscriber]
|
||||||
version = "0.1"
|
version = "0.1"
|
||||||
|
optional = true
|
||||||
|
|
||||||
[dependencies.hyper]
|
[dependencies.hyper]
|
||||||
version = "0.14"
|
version = "0.14"
|
||||||
@@ -58,6 +67,7 @@ features = ["full"]
|
|||||||
|
|
||||||
[dependencies.tower]
|
[dependencies.tower]
|
||||||
version = "0.4"
|
version = "0.4"
|
||||||
|
features = ["timeout"]
|
||||||
|
|
||||||
[dependencies.tower-http]
|
[dependencies.tower-http]
|
||||||
version = "0.4"
|
version = "0.4"
|
||||||
@@ -114,6 +124,7 @@ version = "0.1"
|
|||||||
|
|
||||||
[dependencies.axum-prometheus]
|
[dependencies.axum-prometheus]
|
||||||
version = "0.4"
|
version = "0.4"
|
||||||
|
optional = true
|
||||||
|
|
||||||
[dependencies.metrics]
|
[dependencies.metrics]
|
||||||
version = "0.21"
|
version = "0.21"
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
use maud::{html, Markup};
|
||||||
use uuid::Uuid;
|
use uuid::Uuid;
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
@@ -5,6 +6,8 @@ use crate::{
|
|||||||
sqlite, Context,
|
sqlite, Context,
|
||||||
};
|
};
|
||||||
|
|
||||||
|
use super::Trip;
|
||||||
|
|
||||||
#[derive(Debug, PartialEq, Eq)]
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
pub enum State {
|
pub enum State {
|
||||||
Todo,
|
Todo,
|
||||||
@@ -173,4 +176,408 @@ impl Todo {
|
|||||||
})
|
})
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn new(
|
||||||
|
ctx: &Context,
|
||||||
|
pool: &sqlite::Pool,
|
||||||
|
trip_id: Uuid,
|
||||||
|
description: String,
|
||||||
|
) -> Result<Uuid, Error> {
|
||||||
|
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();
|
||||||
|
let trip_id_param = trip_id.to_string();
|
||||||
|
crate::execute!(
|
||||||
|
&sqlite::QueryClassification {
|
||||||
|
query_type: sqlite::QueryType::Insert,
|
||||||
|
component: sqlite::Component::Todo,
|
||||||
|
},
|
||||||
|
pool,
|
||||||
|
"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",
|
||||||
|
id_param,
|
||||||
|
description,
|
||||||
|
trip_id_param,
|
||||||
|
trip_id_param,
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn delete(
|
||||||
|
ctx: &Context,
|
||||||
|
pool: &sqlite::Pool,
|
||||||
|
trip_id: Uuid,
|
||||||
|
id: Uuid,
|
||||||
|
) -> Result<bool, Error> {
|
||||||
|
let id_param = id.to_string();
|
||||||
|
let user_id = ctx.user.id.to_string();
|
||||||
|
let trip_id_param = trip_id.to_string();
|
||||||
|
let results = crate::execute!(
|
||||||
|
&sqlite::QueryClassification {
|
||||||
|
query_type: sqlite::QueryType::Delete,
|
||||||
|
component: sqlite::Component::Todo,
|
||||||
|
},
|
||||||
|
pool,
|
||||||
|
"DELETE FROM trip_todos
|
||||||
|
WHERE
|
||||||
|
id = ?
|
||||||
|
AND EXISTS (SELECT 1 FROM trips WHERE trip_id = ? AND user_id = ?)",
|
||||||
|
id_param,
|
||||||
|
trip_id_param,
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(results.rows_affected() != 0)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, PartialEq, Eq)]
|
||||||
|
pub enum TodoUiState {
|
||||||
|
Default,
|
||||||
|
Edit,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Todo {
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub fn build(&self, trip_id: &Uuid, state: TodoUiState) -> Markup {
|
||||||
|
let done = self.is_done();
|
||||||
|
html!(
|
||||||
|
li
|
||||||
|
."flex"
|
||||||
|
."flex-row"
|
||||||
|
."justify-start"
|
||||||
|
."items-stretch"
|
||||||
|
."bg-green-50"[done]
|
||||||
|
."bg-red-50"[!done]
|
||||||
|
."h-full"
|
||||||
|
{
|
||||||
|
@if state == TodoUiState::Edit {
|
||||||
|
form
|
||||||
|
name="edit-todo"
|
||||||
|
id="edit-todo"
|
||||||
|
action={
|
||||||
|
"/trips/" (trip_id)
|
||||||
|
"/todo/" (self.id)
|
||||||
|
"/edit/save"
|
||||||
|
}
|
||||||
|
target="_self"
|
||||||
|
method="post"
|
||||||
|
hx-post={
|
||||||
|
"/trips/" (trip_id)
|
||||||
|
"/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={
|
||||||
|
"/trips/" (trip_id)
|
||||||
|
"/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={
|
||||||
|
"/trips/" (trip_id)
|
||||||
|
"/todo/" (self.id)
|
||||||
|
"/undone"
|
||||||
|
}
|
||||||
|
hx-post={
|
||||||
|
"/trips/" (trip_id)
|
||||||
|
"/todo/" (self.id)
|
||||||
|
"/undone"
|
||||||
|
}
|
||||||
|
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={
|
||||||
|
"/trips/" (trip_id)
|
||||||
|
"/todo/" (self.id)
|
||||||
|
"/done"
|
||||||
|
}
|
||||||
|
hx-post={
|
||||||
|
"/trips/" (trip_id)
|
||||||
|
"/todo/" (self.id)
|
||||||
|
"/done"
|
||||||
|
}
|
||||||
|
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={
|
||||||
|
"/trips/" (trip_id)
|
||||||
|
"/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={
|
||||||
|
"/trips/" (trip_id)
|
||||||
|
"/todo/" (self.id)
|
||||||
|
"/delete"
|
||||||
|
}
|
||||||
|
hx-target="#todolist"
|
||||||
|
hx-swap="outerHTML"
|
||||||
|
{
|
||||||
|
span ."m-auto" ."mdi" ."mdi-delete-outline" ."text-xl" {}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn set_description(
|
||||||
|
ctx: &Context,
|
||||||
|
pool: &sqlite::Pool,
|
||||||
|
trip_id: Uuid,
|
||||||
|
todo_id: Uuid,
|
||||||
|
new_description: String,
|
||||||
|
) -> Result<Option<Self>, Error> {
|
||||||
|
let user_id = ctx.user.id.to_string();
|
||||||
|
let trip_id_param = trip_id.to_string();
|
||||||
|
let todo_id_param = todo_id.to_string();
|
||||||
|
|
||||||
|
let result = crate::query_one!(
|
||||||
|
&sqlite::QueryClassification {
|
||||||
|
query_type: sqlite::QueryType::Update,
|
||||||
|
component: sqlite::Component::Todo,
|
||||||
|
},
|
||||||
|
pool,
|
||||||
|
TodoRow,
|
||||||
|
Todo,
|
||||||
|
"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
|
||||||
|
",
|
||||||
|
new_description,
|
||||||
|
todo_id_param,
|
||||||
|
trip_id_param,
|
||||||
|
trip_id_param,
|
||||||
|
user_id,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
Ok(result)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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(&self.trip.id, state))
|
||||||
|
}
|
||||||
|
(NewTodo::build(&self.trip.id))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -147,7 +147,12 @@ pub fn router(state: AppState) -> Router {
|
|||||||
.route(
|
.route(
|
||||||
"/:id/todo/:id/undone",
|
"/:id/todo/:id/undone",
|
||||||
get(trip_todo_undone).post(trip_todo_undone_htmx),
|
get(trip_todo_undone).post(trip_todo_undone_htmx),
|
||||||
),
|
)
|
||||||
|
.route("/:id/todo/:id/edit", post(trip_todo_edit))
|
||||||
|
.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/new", post(trip_todo_new))
|
||||||
|
.route("/:id/todo/:id/delete", post(trip_todo_delete)),
|
||||||
)
|
)
|
||||||
.nest(
|
.nest(
|
||||||
(&TopLevelPage::Inventory.path()).into(),
|
(&TopLevelPage::Inventory.path()).into(),
|
||||||
|
|||||||
@@ -73,6 +73,8 @@ pub struct NewTrip {
|
|||||||
pub struct TripQuery {
|
pub struct TripQuery {
|
||||||
edit: Option<models::trips::TripAttribute>,
|
edit: Option<models::trips::TripAttribute>,
|
||||||
category: Option<Uuid>,
|
category: Option<Uuid>,
|
||||||
|
edit_todo: Option<Uuid>,
|
||||||
|
delete_todo: Option<Uuid>,
|
||||||
}
|
}
|
||||||
|
|
||||||
#[derive(Deserialize, Debug)]
|
#[derive(Deserialize, Debug)]
|
||||||
@@ -424,11 +426,25 @@ pub async fn trip(
|
|||||||
State(mut state): State<AppState>,
|
State(mut state): State<AppState>,
|
||||||
Path(id): Path<Uuid>,
|
Path(id): Path<Uuid>,
|
||||||
Query(trip_query): Query<TripQuery>,
|
Query(trip_query): Query<TripQuery>,
|
||||||
|
headers: HeaderMap,
|
||||||
) -> Result<impl IntoResponse, Error> {
|
) -> Result<impl IntoResponse, Error> {
|
||||||
let ctx = Context::build(current_user);
|
let ctx = Context::build(current_user);
|
||||||
state.client_state.trip_edit_attribute = trip_query.edit;
|
state.client_state.trip_edit_attribute = trip_query.edit;
|
||||||
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 {
|
||||||
|
let deleted =
|
||||||
|
models::trips::todos::Todo::delete(&ctx, &state.database_pool, id, delete_todo).await?;
|
||||||
|
|
||||||
|
return if deleted {
|
||||||
|
Ok(Redirect::to(get_referer(&headers)?).into_response())
|
||||||
|
} else {
|
||||||
|
Err(Error::Request(RequestError::NotFound {
|
||||||
|
message: format!("todo with id {id} not found"),
|
||||||
|
}))
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
let mut trip: models::trips::Trip = models::trips::Trip::find(&ctx, &state.database_pool, id)
|
let mut trip: models::trips::Trip = models::trips::Trip::find(&ctx, &state.database_pool, id)
|
||||||
.await?
|
.await?
|
||||||
.ok_or(Error::Request(RequestError::NotFound {
|
.ok_or(Error::Request(RequestError::NotFound {
|
||||||
@@ -463,9 +479,11 @@ pub async fn trip(
|
|||||||
&trip,
|
&trip,
|
||||||
state.client_state.trip_edit_attribute.as_ref(),
|
state.client_state.trip_edit_attribute.as_ref(),
|
||||||
active_category,
|
active_category,
|
||||||
|
trip_query.edit_todo,
|
||||||
),
|
),
|
||||||
Some(&TopLevelPage::Trips),
|
Some(&TopLevelPage::Trips),
|
||||||
))
|
)
|
||||||
|
.into_response())
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
@@ -1265,7 +1283,7 @@ pub async fn trip_todo_done_htmx(
|
|||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(view::trip::TripTodo::build(&trip_id, &todo_item))
|
Ok(todo_item.build(&trip_id, models::trips::todos::TodoUiState::Default))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
@@ -1312,7 +1330,7 @@ pub async fn trip_todo_undone_htmx(
|
|||||||
})
|
})
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
Ok(view::trip::TripTodo::build(&trip_id, &todo_item))
|
Ok(todo_item.build(&trip_id, models::trips::todos::TodoUiState::Default))
|
||||||
}
|
}
|
||||||
|
|
||||||
#[tracing::instrument]
|
#[tracing::instrument]
|
||||||
@@ -1334,3 +1352,162 @@ pub async fn trip_todo_undone(
|
|||||||
|
|
||||||
Ok(Redirect::to(get_referer(&headers)?))
|
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<models::user::User>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
|
||||||
|
) -> Result<impl IntoResponse, Error> {
|
||||||
|
let ctx = Context::build(current_user);
|
||||||
|
let todo_item =
|
||||||
|
models::trips::todos::Todo::find(&ctx, &state.database_pool, trip_id, todo_id).await?;
|
||||||
|
|
||||||
|
match todo_item {
|
||||||
|
None => Err(Error::Request(RequestError::NotFound {
|
||||||
|
message: format!("todo with id {todo_id} not found"),
|
||||||
|
})),
|
||||||
|
Some(todo_item) => Ok(todo_item
|
||||||
|
.build(&trip_id, models::trips::todos::TodoUiState::Edit)
|
||||||
|
.into_response()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn trip_todo_edit_save(
|
||||||
|
Extension(current_user): Extension<models::user::User>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
|
||||||
|
Form(form): Form<TripTodoDescription>,
|
||||||
|
) -> Result<impl IntoResponse, Error> {
|
||||||
|
let ctx = Context::build(current_user);
|
||||||
|
let todo_item = models::trips::todos::Todo::set_description(
|
||||||
|
&ctx,
|
||||||
|
&state.database_pool,
|
||||||
|
trip_id,
|
||||||
|
todo_id,
|
||||||
|
form.description,
|
||||||
|
)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
match todo_item {
|
||||||
|
None => Err(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(&trip_id, models::trips::todos::TodoUiState::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<models::user::User>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
|
||||||
|
) -> Result<impl IntoResponse, Error> {
|
||||||
|
let ctx = Context::build(current_user);
|
||||||
|
let todo_item =
|
||||||
|
models::trips::todos::Todo::find(&ctx, &state.database_pool, trip_id, todo_id).await?;
|
||||||
|
|
||||||
|
match todo_item {
|
||||||
|
None => Err(Error::Request(RequestError::NotFound {
|
||||||
|
message: format!("todo with id {todo_id} not found"),
|
||||||
|
})),
|
||||||
|
Some(todo_item) => Ok(todo_item
|
||||||
|
.build(&trip_id, models::trips::todos::TodoUiState::Default)
|
||||||
|
.into_response()),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Deserialize, Debug)]
|
||||||
|
#[serde(deny_unknown_fields)]
|
||||||
|
pub struct TripTodoNew {
|
||||||
|
#[serde(rename = "new-todo-description")]
|
||||||
|
description: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn trip_todo_new(
|
||||||
|
Extension(current_user): Extension<models::user::User>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path(trip_id): Path<Uuid>,
|
||||||
|
Form(form): Form<TripTodoNew>,
|
||||||
|
) -> Result<impl IntoResponse, Error> {
|
||||||
|
let ctx = Context::build(current_user);
|
||||||
|
// method output is not required as we reload the whole trip todos anyway
|
||||||
|
let _todo_item =
|
||||||
|
models::trips::todos::Todo::new(&ctx, &state.database_pool, trip_id, form.description)
|
||||||
|
.await?;
|
||||||
|
|
||||||
|
if htmx::is_htmx(&headers) {
|
||||||
|
let trip = models::trips::Trip::find(&ctx, &state.database_pool, trip_id).await?;
|
||||||
|
match trip {
|
||||||
|
None => Err(Error::Request(RequestError::NotFound {
|
||||||
|
message: format!("trip with id {trip_id} not found"),
|
||||||
|
})),
|
||||||
|
Some(mut trip) => {
|
||||||
|
trip.load_todos(&ctx, &state.database_pool).await?;
|
||||||
|
Ok(models::trips::todos::TodoList {
|
||||||
|
trip: &trip,
|
||||||
|
todos: &trip.todos(),
|
||||||
|
}
|
||||||
|
.build(None)
|
||||||
|
.into_response())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
Ok(Redirect::to(&format!("/trips/{trip_id}/")).into_response())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tracing::instrument]
|
||||||
|
pub async fn trip_todo_delete(
|
||||||
|
Extension(current_user): Extension<models::user::User>,
|
||||||
|
State(state): State<AppState>,
|
||||||
|
headers: HeaderMap,
|
||||||
|
Path((trip_id, todo_id)): Path<(Uuid, Uuid)>,
|
||||||
|
) -> Result<impl IntoResponse, Error> {
|
||||||
|
let ctx = Context::build(current_user);
|
||||||
|
let deleted =
|
||||||
|
models::trips::todos::Todo::delete(&ctx, &state.database_pool, trip_id, todo_id).await?;
|
||||||
|
|
||||||
|
if !deleted {
|
||||||
|
return Err(Error::Request(RequestError::NotFound {
|
||||||
|
message: format!("todo with id {todo_id} not found"),
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
let trip = models::trips::Trip::find(&ctx, &state.database_pool, trip_id).await?;
|
||||||
|
match trip {
|
||||||
|
None => Err(Error::Request(RequestError::NotFound {
|
||||||
|
message: format!("trip with id {trip_id} not found"),
|
||||||
|
})),
|
||||||
|
Some(mut trip) => {
|
||||||
|
trip.load_todos(&ctx, &state.database_pool).await?;
|
||||||
|
Ok(models::trips::todos::TodoList {
|
||||||
|
trip: &trip,
|
||||||
|
todos: &trip.todos(),
|
||||||
|
}
|
||||||
|
.build(None)
|
||||||
|
.into_response())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,2 +1,3 @@
|
|||||||
|
#[cfg(feature = "prometheus")]
|
||||||
pub mod metrics;
|
pub mod metrics;
|
||||||
pub mod tracing;
|
pub mod tracing;
|
||||||
|
|||||||
@@ -257,6 +257,7 @@ impl Trip {
|
|||||||
trip: &models::trips::Trip,
|
trip: &models::trips::Trip,
|
||||||
trip_edit_attribute: Option<&models::trips::TripAttribute>,
|
trip_edit_attribute: Option<&models::trips::TripAttribute>,
|
||||||
active_category: Option<&models::trips::TripCategory>,
|
active_category: Option<&models::trips::TripCategory>,
|
||||||
|
edit_todo: Option<Uuid>,
|
||||||
) -> Markup {
|
) -> Markup {
|
||||||
html!(
|
html!(
|
||||||
div ."p-8" ."flex" ."flex-col" ."gap-8" {
|
div ."p-8" ."flex" ."flex-col" ."gap-8" {
|
||||||
@@ -370,7 +371,7 @@ impl Trip {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
(TripInfo::build(trip_edit_attribute, trip))
|
(TripInfo::build(trip_edit_attribute, trip))
|
||||||
(TripTodoList::build(trip))
|
(crate::models::trips::todos::TodoList{todos: trip.todos(), trip: &trip}.build(edit_todo))
|
||||||
(TripComment::build(trip))
|
(TripComment::build(trip))
|
||||||
(TripItems::build(active_category, trip))
|
(TripItems::build(active_category, trip))
|
||||||
}
|
}
|
||||||
@@ -795,113 +796,6 @@ impl TripInfo {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub struct TripTodo;
|
|
||||||
|
|
||||||
impl TripTodo {
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub fn build(trip_id: &Uuid, todo: &models::trips::todos::Todo) -> Markup {
|
|
||||||
let done = todo.is_done();
|
|
||||||
html!(
|
|
||||||
li
|
|
||||||
."flex"
|
|
||||||
."flex-row"
|
|
||||||
."justify-start"
|
|
||||||
."items-stretch"
|
|
||||||
."bg-green-50"[done]
|
|
||||||
."bg-red-50"[!done]
|
|
||||||
."hover:bg-white"[!done]
|
|
||||||
."h-full"
|
|
||||||
{
|
|
||||||
@if done {
|
|
||||||
a
|
|
||||||
."flex"
|
|
||||||
."flex-row"
|
|
||||||
."aspect-square"
|
|
||||||
href={
|
|
||||||
"/trips/" (trip_id)
|
|
||||||
"/todo/" (todo.id)
|
|
||||||
"/undone"
|
|
||||||
}
|
|
||||||
hx-post={
|
|
||||||
"/trips/" (trip_id)
|
|
||||||
"/todo/" (todo.id)
|
|
||||||
"/undone"
|
|
||||||
}
|
|
||||||
hx-target="closest li"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
{
|
|
||||||
span
|
|
||||||
."mdi"
|
|
||||||
."m-auto"
|
|
||||||
."text-xl"
|
|
||||||
."mdi-check"
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
} @else {
|
|
||||||
a
|
|
||||||
."flex"
|
|
||||||
."flex-row"
|
|
||||||
."aspect-square"
|
|
||||||
href={
|
|
||||||
"/trips/" (trip_id)
|
|
||||||
"/todo/" (todo.id)
|
|
||||||
"/done"
|
|
||||||
}
|
|
||||||
hx-post={
|
|
||||||
"/trips/" (trip_id)
|
|
||||||
"/todo/" (todo.id)
|
|
||||||
"/done"
|
|
||||||
}
|
|
||||||
hx-target="closest li"
|
|
||||||
hx-swap="outerHTML"
|
|
||||||
{
|
|
||||||
span
|
|
||||||
."mdi"
|
|
||||||
."m-auto"
|
|
||||||
."text-xl"
|
|
||||||
."mdi-checkbox-blank-outline"
|
|
||||||
{}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
span
|
|
||||||
."p-2"
|
|
||||||
{
|
|
||||||
(todo.description)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TripTodoList;
|
|
||||||
|
|
||||||
impl TripTodoList {
|
|
||||||
#[tracing::instrument]
|
|
||||||
pub fn build(trip: &models::trips::Trip) -> Markup {
|
|
||||||
let todos = trip.todos();
|
|
||||||
html!(
|
|
||||||
div {
|
|
||||||
h1 ."text-xl" ."mb-5" { "Todos" }
|
|
||||||
|
|
||||||
@if todos.is_empty() {
|
|
||||||
p { "no todos" }
|
|
||||||
|
|
||||||
} @else {
|
|
||||||
|
|
||||||
ul
|
|
||||||
."flex"
|
|
||||||
."flex-col"
|
|
||||||
{
|
|
||||||
@for todo in trip.todos() {
|
|
||||||
(TripTodo::build(&trip.id, &todo))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
pub struct TripComment;
|
pub struct TripComment;
|
||||||
|
|
||||||
impl TripComment {
|
impl TripComment {
|
||||||
|
|||||||
Reference in New Issue
Block a user