From 444d77a0a0b2b36f986ba57d29ddff711adf8d4a Mon Sep 17 00:00:00 2001 From: ToBinio Date: Thu, 21 May 2026 14:50:44 +0200 Subject: [PATCH] resolve todo tags & categories server side --- api/importer/src/main.rs | 13 +++- api/services/src/categories.rs | 32 ++++---- api/services/src/tags.rs | 26 +++---- api/services/src/todos.rs | 73 +++++++++++++------ api/src/routes/todo.rs | 4 +- api/tests/todos/add.rs | 15 +++- api/tests/todos/update.rs | 11 ++- app/components/Edit/EditTodoSheet.vue | 12 +-- app/components/Edit/NewTodoSheet.vue | 5 +- app/components/Filter/CategorySelect.vue | 2 +- app/components/Filter/TagSelect.vue | 4 +- app/components/Todo/Todo.vue | 16 +--- app/components/Utils/Label/CategorySelect.vue | 2 +- app/components/Utils/Label/TagSelect.vue | 6 +- app/composables/useFilteredTodos.ts | 4 +- app/composables/useTodoDragging.ts | 8 +- app/stores/useTodoStore.ts | 37 ++++++++-- app/utils/types.ts | 13 +++- libs/types/src/lib.rs | 12 ++- 19 files changed, 190 insertions(+), 105 deletions(-) diff --git a/api/importer/src/main.rs b/api/importer/src/main.rs index 0d2cb29..a35bbaa 100644 --- a/api/importer/src/main.rs +++ b/api/importer/src/main.rs @@ -1,7 +1,7 @@ use orm::init_db; use services::{User, categories::CategoryService, tags::TagService, todos::TodoService}; use tokio::fs::read_to_string; -use types::{Category, Tag, Todo}; +use types::{Category, CreateTodoData, Tag, Todo}; #[tokio::main] async fn main() { @@ -34,7 +34,16 @@ async fn main() { TodoService::create( &user, &db, - todo, + CreateTodoData { + uuid: todo.uuid, + title: todo.title, + note: todo.note, + time: todo.time, + tags: todo + .tags + .map(|tags| tags.into_iter().map(|t| t.uuid).collect()), + category: todo.category.map(|c| c.uuid), + }, services::todos::TodoPosition::Bottom, None, ) diff --git a/api/services/src/categories.rs b/api/services/src/categories.rs index 4c35355..ee24ec8 100644 --- a/api/services/src/categories.rs +++ b/api/services/src/categories.rs @@ -31,7 +31,7 @@ impl CategoryService { .all(db) .await?; - categories.into_iter().map(Self::to_category_dto).collect() + categories.into_iter().map(to_category_dto).collect() } pub async fn get_by_id( @@ -44,7 +44,7 @@ impl CategoryService { .one(db) .await?; - category.map(Self::to_category_dto).transpose() + category.map(to_category_dto).transpose() } pub async fn delete_by_id( @@ -75,7 +75,7 @@ impl CategoryService { .exec_with_returning(db) .await?; - Self::to_category_dto(category) + to_category_dto(category) } pub async fn update( @@ -94,19 +94,19 @@ impl CategoryService { .exec(db) .await?; - Self::to_category_dto(category) + to_category_dto(category) } +} - fn to_category_dto(value: category::Model) -> Result { - let category = CategoryModel { - uuid: value.id, - name: value.name.to_string(), - color: HexColor::from_text(&value.color).ok_or( - CategoryServiceError::InvalidCategory("invalid color".to_string()), - )?, - icon: value.icon.to_string(), - }; - - Ok(category) - } +pub fn to_category_dto(value: category::Model) -> Result { + let category = CategoryModel { + uuid: value.id, + name: value.name.to_string(), + color: HexColor::from_text(&value.color).ok_or(CategoryServiceError::InvalidCategory( + "invalid color".to_string(), + ))?, + icon: value.icon.to_string(), + }; + + Ok(category) } diff --git a/api/services/src/tags.rs b/api/services/src/tags.rs index edbf188..1635e45 100644 --- a/api/services/src/tags.rs +++ b/api/services/src/tags.rs @@ -31,7 +31,7 @@ impl TagService { .all(db) .await?; - tags.into_iter().map(Self::to_tag_dto).collect() + tags.into_iter().map(to_tag_dto).collect() } pub async fn get_by_id( @@ -44,7 +44,7 @@ impl TagService { .one(db) .await?; - tag.map(Self::to_tag_dto).transpose() + tag.map(to_tag_dto).transpose() } pub async fn delete_by_id( @@ -74,7 +74,7 @@ impl TagService { .exec_with_returning(db) .await?; - Self::to_tag_dto(tag) + to_tag_dto(tag) } pub async fn update( @@ -92,17 +92,17 @@ impl TagService { .exec(db) .await?; - Self::to_tag_dto(tag) + to_tag_dto(tag) } +} - fn to_tag_dto(value: tag::Model) -> Result { - let tag = TagModel { - uuid: value.id, - name: value.name, - color: HexColor::from_text(&value.color) - .ok_or(TagServiceError::InvalidTag("invalid color".to_string()))?, - }; +pub fn to_tag_dto(value: tag::Model) -> Result { + let tag = TagModel { + uuid: value.id, + name: value.name, + color: HexColor::from_text(&value.color) + .ok_or(TagServiceError::InvalidTag("invalid color".to_string()))?, + }; - Ok(tag) - } + Ok(tag) } diff --git a/api/services/src/todos.rs b/api/services/src/todos.rs index d35322e..f00975e 100644 --- a/api/services/src/todos.rs +++ b/api/services/src/todos.rs @@ -1,6 +1,6 @@ use std::collections::{HashMap, HashSet}; -use chrono::{Local, NaiveDate, NaiveDateTime}; +use chrono::{Local, NaiveDateTime}; use orm::OrmConnection; use sea_orm::ActiveValue::{NotSet, Set}; use sea_orm::sea_query::Expr; @@ -11,10 +11,12 @@ use sea_orm::{ use serde::Deserialize; use thiserror::Error; use tracing::warn; -use types::{Time as TimeModel, TimeRange, TimeRecurring, Todo as TodoModel}; +use types::{CreateTodoData, Time as TimeModel, TimeRange, TimeRecurring, Todo as TodoModel}; use uuid::Uuid; use crate::User; +use crate::categories::to_category_dto; +use crate::tags::to_tag_dto; use orm::entities::{check, prelude::*, time, todo, todo_tag}; #[derive(Error, Debug, PartialEq)] @@ -22,6 +24,12 @@ pub enum TodoServiceError { #[error("database error - caused by: {0}")] DbError(#[from] sea_orm::DbErr), + #[error("tag service error - caused by: {0}")] + TagServiceError(#[from] crate::tags::TagServiceError), + + #[error("category service error - caused by: {0}")] + CategoryServiceError(#[from] crate::categories::CategoryServiceError), + #[error("invalid time - caused by: {0}")] InvalidTime(String), @@ -45,23 +53,40 @@ impl TodoService { ) -> Result, TodoServiceError> { let todos = Todo::find() .filter(todo::Column::OwnerId.eq(user.id.clone())) + .find_also_related(Category) .order_by_asc(todo::Column::Order) .all(db) - .await?; + .await? + .into_iter() + .map(|(todo, category)| match category { + Some(category) => to_category_dto(category).map(|category| (todo, Some(category))), + None => Ok((todo, None)), + }) + .collect::, _>>()?; - let todo_ids: Vec<_> = todos.iter().map(|t| t.id).collect(); + let todo_ids: Vec<_> = todos.iter().map(|(t, _)| t.id).collect(); - let mut tags: HashMap> = TodoTag::find() + let db_tags = TodoTag::find() .filter(todo_tag::Column::TodoId.is_in(todo_ids.clone())) + .find_also_related(Tag) .all(db) - .await? - .into_iter() - .fold(HashMap::new(), |mut acc, tag| { - acc.entry(tag.todo_id).or_default().push(tag.tag_id); - acc - }); + .await?; - let mut checks: HashMap> = Check::find() + let mapped_tags = db_tags + .into_iter() + .filter_map(|(connection, tag)| tag.map(|tag| (connection, tag))) + .map(|(connection, tag)| to_tag_dto(tag).map(|tag| (connection, tag))) + .collect::, _>>()?; + + let mut tags: HashMap> = + mapped_tags + .into_iter() + .fold(HashMap::new(), |mut acc, (connection, tag)| { + acc.entry(connection.todo_id).or_default().push(tag); + acc + }); + + let mut checks: HashMap> = Check::find() .filter(check::Column::TodoId.is_in(todo_ids.clone())) .all(db) .await? @@ -91,7 +116,7 @@ impl TodoService { let result: Vec = todos .into_iter() - .map(|todo| { + .map(|(todo, category)| { let tags = tags.remove(&todo.id); let checks = checks.remove(&todo.id); let mut times = times.remove(&todo.id).unwrap_or_default(); @@ -106,7 +131,7 @@ impl TodoService { note: todo.note, time: times.pop(), tags, - category: todo.category_id, + category, checks, } }) @@ -138,7 +163,7 @@ impl TodoService { note: todo.note, time: None, tags: None, - category: todo.category_id, + category: None, checks: None, }, todo.owner_id, @@ -163,13 +188,15 @@ impl TodoService { return Ok(None); }; - let tags: Vec<_> = todo - .find_related(TodoTag) + let category = todo.find_related(Category).one(db).await?; + + let tags = todo + .find_related(Tag) .all(db) .await? - .iter() - .map(|tag| tag.tag_id) - .collect(); + .into_iter() + .map(to_tag_dto) + .collect::, _>>()?; let tags = if tags.is_empty() { None } else { Some(tags) }; let checks: Vec<_> = todo .find_related(Check) @@ -192,7 +219,7 @@ impl TodoService { note: todo.note.to_string(), time: time.map(Self::to_time_dto).transpose()?, tags, - category: todo.category_id, + category: category.map(to_category_dto).transpose()?, checks, })) } @@ -200,7 +227,7 @@ impl TodoService { pub async fn create( user: &User, db: &OrmConnection, - todo: TodoModel, + todo: CreateTodoData, positon: TodoPosition, reference: Option, ) -> Result { @@ -275,7 +302,7 @@ impl TodoService { pub async fn update( user: &User, db: &OrmConnection, - todo: TodoModel, + todo: CreateTodoData, ) -> Result { let tx = db.begin().await?; diff --git a/api/src/routes/todo.rs b/api/src/routes/todo.rs index 69e4c97..df2c2c5 100644 --- a/api/src/routes/todo.rs +++ b/api/src/routes/todo.rs @@ -12,7 +12,7 @@ use serde::Deserialize; use thiserror::Error; use tokio_stream::{StreamExt, wrappers::BroadcastStream}; use tracing::warn; -use types::Todo as TodoModel; +use types::{CreateTodoData, Todo as TodoModel}; use uuid::Uuid; use crate::AppState; @@ -68,7 +68,7 @@ async fn get_all_todos( #[derive(Debug, Deserialize)] struct AddTodoModel { - pub data: TodoModel, + pub data: CreateTodoData, pub position: Option, #[serde(rename = "previousId")] pub previous_id: Option, diff --git a/api/tests/todos/add.rs b/api/tests/todos/add.rs index cde9891..e66c363 100644 --- a/api/tests/todos/add.rs +++ b/api/tests/todos/add.rs @@ -1,7 +1,7 @@ use crate::common::client::get_client; use chrono::{DateTime, NaiveDate, NaiveTime, Utc}; use serde_json::json; -use types::{TimePoint, TimeRange, TimeRecurring, Todo}; +use types::{Category, HexColor, Tag, TimePoint, TimeRange, TimeRecurring, Todo}; use uuid::Uuid; #[tokio::test] @@ -295,8 +295,17 @@ async fn add_todo_with_labels() { title: "Test Todo".to_string(), note: "Test Note".to_string(), time: None, - tags: Some(vec![tag_uuid]), - category: Some(category_uuid), + tags: Some(vec![Tag { + uuid: tag_uuid, + name: "Test Tag".to_string(), + color: HexColor::from_text("#233212").unwrap(), + }]), + category: Some(Category { + uuid: category_uuid, + name: "Test Category".to_string(), + color: HexColor::from_text("#233212").unwrap(), + icon: "icon".to_string(), + }), checks: None, }; diff --git a/api/tests/todos/update.rs b/api/tests/todos/update.rs index f75b879..6b0f0f2 100644 --- a/api/tests/todos/update.rs +++ b/api/tests/todos/update.rs @@ -1,7 +1,7 @@ use crate::common::client::get_client; use chrono::{DateTime, NaiveDate, NaiveTime, Utc}; use serde_json::json; -use types::{TimePoint, TimeRange, TimeRecurring, Todo}; +use types::{HexColor, Tag, TimePoint, TimeRange, TimeRecurring, Todo}; use uuid::Uuid; #[tokio::test] @@ -102,7 +102,14 @@ async fn update_todo_with_labels() { assert_eq!(todos[0].uuid, todo_uuid); assert_eq!(todos[0].title, "updated Todo"); assert_eq!(todos[0].tags.as_ref().unwrap().len(), 1); - assert_eq!(todos[0].tags.as_ref().unwrap()[0], tag_uuid); + assert_eq!( + todos[0].tags.as_ref().unwrap()[0], + Tag { + uuid: tag_uuid, + name: "Test Tag".to_string(), + color: HexColor::from_text("#233212").unwrap(), + } + ); client .add_todo_json(json!( diff --git a/app/components/Edit/EditTodoSheet.vue b/app/components/Edit/EditTodoSheet.vue index 2a368c6..c76ffed 100644 --- a/app/components/Edit/EditTodoSheet.vue +++ b/app/components/Edit/EditTodoSheet.vue @@ -1,6 +1,6 @@