From b937d0bcb50054683102f4e2dd3b7e834938b5d8 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Volf?= <66163112+matous-volf@users.noreply.github.com> Date: Sun, 8 Sep 2024 22:19:53 +0200 Subject: [PATCH 1/4] feat: create a model for tasks with subtasks --- src/models/project.rs | 2 +- src/models/subtask.rs | 7 +++++-- src/models/task.rs | 23 ++++++++++++++++++++++- src/query/mod.rs | 4 +++- src/query/tasks.rs | 28 +++++++++++++++++++++++++++- src/server/tasks.rs | 34 +++++++++++++++++++++++++++++++++- 6 files changed, 91 insertions(+), 7 deletions(-) diff --git a/src/models/project.rs b/src/models/project.rs index 6e700d3..fb8da72 100644 --- a/src/models/project.rs +++ b/src/models/project.rs @@ -7,7 +7,7 @@ use validator::Validate; const TITLE_LENGTH_MIN: u64 = 1; const TITLE_LENGTH_MAX: u64 = 255; -#[derive(Queryable, Selectable, Serialize, Deserialize, PartialEq, Clone, Debug)] +#[derive(Queryable, Selectable, Identifiable, Serialize, Deserialize, PartialEq, Clone, Debug)] #[diesel(table_name = crate::schema::projects)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Project { diff --git a/src/models/subtask.rs b/src/models/subtask.rs index fbf9974..7aa9e84 100644 --- a/src/models/subtask.rs +++ b/src/models/subtask.rs @@ -1,5 +1,6 @@ -use chrono::NaiveDateTime; +use crate::models::task::Task; use crate::schema::subtasks; +use chrono::NaiveDateTime; use diesel::prelude::*; use serde::{Deserialize, Serialize}; use validator::Validate; @@ -7,7 +8,9 @@ use validator::Validate; const TITLE_LENGTH_MIN: u64 = 1; const TITLE_LENGTH_MAX: u64 = 255; -#[derive(Queryable, Selectable, Serialize, Deserialize, PartialEq, Clone, Debug)] +#[derive(Queryable, Selectable, Identifiable, Associations, Serialize, Deserialize, PartialEq, + Clone, Debug)] +#[diesel(belongs_to(Task, foreign_key = task_id))] #[diesel(table_name = subtasks)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Subtask { diff --git a/src/models/task.rs b/src/models/task.rs index c75eac9..7b4d7cc 100644 --- a/src/models/task.rs +++ b/src/models/task.rs @@ -4,11 +4,12 @@ use crate::schema::tasks; use diesel::prelude::*; use serde::{Deserialize, Serialize}; use validator::Validate; +use crate::models::subtask::Subtask; const TITLE_LENGTH_MIN: u64 = 1; const TITLE_LENGTH_MAX: u64 = 255; -#[derive(Queryable, Selectable, Serialize, Deserialize, PartialEq, Clone, Debug)] +#[derive(Queryable, Selectable, Identifiable, Serialize, Deserialize, PartialEq, Clone, Debug)] #[diesel(table_name = tasks)] #[diesel(check_for_backend(diesel::pg::Pg))] pub struct Task { @@ -51,6 +52,26 @@ impl Task { } } +#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] +pub struct TaskWithSubtasks { + task: Task, + subtasks: Vec, +} + +impl TaskWithSubtasks { + pub fn new(task: Task, subtasks: Vec) -> Self { + Self { task, subtasks } + } + + pub fn task(&self) -> &Task { + &self.task + } + + pub fn subtasks(&self) -> &Vec { + &self.subtasks + } +} + #[derive(Insertable, Serialize, Deserialize, Validate, Clone, Debug)] #[diesel(table_name = tasks)] pub struct NewTask { diff --git a/src/query/mod.rs b/src/query/mod.rs index 792d45b..a9462ee 100644 --- a/src/query/mod.rs +++ b/src/query/mod.rs @@ -3,7 +3,7 @@ use crate::errors::error_vec::ErrorVec; use crate::models::category::Category; use crate::models::project::Project; use crate::models::subtask::Subtask; -use crate::models::task::Task; +use crate::models::task::{Task, TaskWithSubtasks}; pub(crate) mod tasks; pub(crate) mod projects; @@ -13,6 +13,7 @@ pub(crate) mod subtasks; pub(crate) enum QueryValue { Projects(Vec), Tasks(Vec), + TasksWithSubtasks(Vec), Subtasks(Vec), } @@ -26,5 +27,6 @@ pub(crate) enum QueryKey { Projects, Tasks, TasksInCategory(Category), + TasksWithSubtasksInCategory(Category), SubtasksOfTaskId(i32), } diff --git a/src/query/tasks.rs b/src/query/tasks.rs index 1cf22fc..c29a331 100644 --- a/src/query/tasks.rs +++ b/src/query/tasks.rs @@ -2,7 +2,7 @@ use dioxus::prelude::ServerFnError; use dioxus_query::prelude::{use_get_query, QueryResult, UseQuery}; use crate::models::category::Category; use crate::query::{QueryErrors, QueryKey, QueryValue}; -use crate::server::tasks::get_tasks_in_category; +use crate::server::tasks::{get_tasks_in_category, get_tasks_with_subtasks_in_category}; @@ -22,3 +22,29 @@ async fn fetch_tasks_in_category(keys: Vec) -> QueryResult UseQuery { + use_get_query( + [ + QueryKey::TasksWithSubtasksInCategory( + category.clone()), + QueryKey::TasksInCategory(category), + QueryKey::Tasks + ], + fetch_tasks_with_subtasks_in_category + ) +} + +async fn fetch_tasks_with_subtasks_in_category(keys: Vec) + -> QueryResult { + if let Some(QueryKey::TasksWithSubtasksInCategory(category)) = keys.first() { + match get_tasks_with_subtasks_in_category(category.clone()).await { + Ok(tasks) => Ok(QueryValue::TasksWithSubtasks(tasks)), + Err(ServerFnError::WrappedServerError(errors)) => Err(QueryErrors::Error(errors)), + Err(error) => panic!("Unexpected error: {:?}", error) + }.into() + } else { + panic!("Unexpected query keys: {:?}", keys); + } +} diff --git a/src/server/tasks.rs b/src/server/tasks.rs index 895551c..fb1d747 100644 --- a/src/server/tasks.rs +++ b/src/server/tasks.rs @@ -1,14 +1,16 @@ use chrono::{Datelike, Days, Months, NaiveDate}; use crate::errors::error::Error; use crate::errors::error_vec::ErrorVec; -use crate::models::task::{NewTask, Task}; +use crate::models::task::{NewTask, Task, TaskWithSubtasks}; use crate::server::database_connection::establish_database_connection; use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper}; use dioxus::prelude::*; +use diesel::prelude::*; use time::util::days_in_year_month; use validator::Validate; use crate::errors::task_error::TaskError; use crate::models::category::{Category, ReoccurrenceInterval}; +use crate::models::subtask::Subtask; use crate::server::subtasks::restore_subtasks_of_task; #[server] @@ -72,6 +74,36 @@ pub(crate) async fn get_tasks_in_category(filtered_category: Category) Ok(results) } +#[server] +pub(crate) async fn get_tasks_with_subtasks_in_category(filtered_category: Category) -> Result< + Vec, + ServerFnError> +> { + use crate::schema::tasks; + + let mut connection = establish_database_connection() + .map_err::, _>(|_| vec![Error::ServerInternal].into())?; + + let tasks_in_category = tasks::table + .filter(filtered_category.eq_sql_predicate()) + .select(Task::as_select()).load(&mut connection) + .map_err::, _>(|_| vec![Error::ServerInternal].into())?; + + let subtasks = Subtask::belonging_to(&tasks_in_category) + .select(Subtask::as_select()) + .load(&mut connection) + .map_err::, _>(|_| vec![Error::ServerInternal].into())?; + + let tasks_with_subtasks = subtasks + .grouped_by(&tasks_in_category) + .into_iter() + .zip(tasks_in_category) + .map(|(pages, book)| TaskWithSubtasks::new(book, pages)) + .collect(); + + Ok(tasks_with_subtasks) +} + #[server] pub(crate) async fn edit_task(task_id: i32, new_task: NewTask) -> Result>> { From 024c5a22581a696865ba50a271b07efb954494f9 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Volf?= <66163112+matous-volf@users.noreply.github.com> Date: Sun, 8 Sep 2024 22:20:31 +0200 Subject: [PATCH 2/4] feat: display the subtask count in task lists --- .../pages/category_calendar_page.rs | 15 +- src/components/pages/category_page.rs | 8 +- src/components/pages/category_today_page.rs | 32 +-- src/components/subtasks_form.rs | 238 ++++++++++-------- src/components/task_form.rs | 9 +- src/components/task_list.rs | 54 ++-- 6 files changed, 200 insertions(+), 156 deletions(-) diff --git a/src/components/pages/category_calendar_page.rs b/src/components/pages/category_calendar_page.rs index dba2d59..2a31b42 100644 --- a/src/components/pages/category_calendar_page.rs +++ b/src/components/pages/category_calendar_page.rs @@ -6,14 +6,14 @@ use dioxus::prelude::*; use dioxus_query::prelude::QueryResult; use crate::components::task_list::TaskList; use crate::query::QueryValue; -use crate::query::tasks::use_tasks_in_category_query; -use crate::models::task::Task; +use crate::query::tasks::use_tasks_with_subtasks_in_category_query; +use crate::models::task::{TaskWithSubtasks}; const CALENDAR_LENGTH_DAYS: usize = 366 * 3; #[component] pub(crate) fn CategoryCalendarPage() -> Element { - let tasks = use_tasks_in_category_query(Category::Calendar { + let tasks = use_tasks_with_subtasks_in_category_query(Category::Calendar { date: Local::now().date_naive(), reoccurrence: None, time: None, @@ -22,8 +22,8 @@ pub(crate) fn CategoryCalendarPage() -> Element { rsx! { match tasks_query_result.value() { - QueryResult::Ok(QueryValue::Tasks(tasks)) - | QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => { + QueryResult::Ok(QueryValue::TasksWithSubtasks(tasks)) + | QueryResult::Loading(Some(QueryValue::TasksWithSubtasks(tasks))) => { let today_date = Local::now().date_naive(); rsx! { @@ -52,12 +52,13 @@ pub(crate) fn CategoryCalendarPage() -> Element { } TaskList { tasks: tasks.iter().filter(|task| { - if let Category::Calendar { date, .. } = task.category() { + if let Category::Calendar { date, .. } + = task.task().category() { *date == date_current } else { panic!("Unexpected category."); } - }).cloned().collect::>() + }).cloned().collect::>() } } } diff --git a/src/components/pages/category_page.rs b/src/components/pages/category_page.rs index 0dd2b0c..326bd62 100644 --- a/src/components/pages/category_page.rs +++ b/src/components/pages/category_page.rs @@ -1,6 +1,6 @@ use crate::components::task_list::TaskList; use crate::models::category::Category; -use crate::query::tasks::use_tasks_in_category_query; +use crate::query::tasks::use_tasks_with_subtasks_in_category_query; use crate::query::QueryValue; use dioxus::core_macro::rsx; use dioxus::dioxus_core::Element; @@ -9,12 +9,12 @@ use dioxus_query::prelude::QueryResult; #[component] pub(crate) fn CategoryPage(category: Category) -> Element { - let tasks_query = use_tasks_in_category_query(category); + let tasks_query = use_tasks_with_subtasks_in_category_query(category); let tasks_query_result = tasks_query.result(); match tasks_query_result.value() { - QueryResult::Ok(QueryValue::Tasks(tasks)) - | QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => rsx! { + QueryResult::Ok(QueryValue::TasksWithSubtasks(tasks)) + | QueryResult::Loading(Some(QueryValue::TasksWithSubtasks(tasks))) => rsx! { TaskList { tasks: tasks.clone(), class: "pb-36" diff --git a/src/components/pages/category_today_page.rs b/src/components/pages/category_today_page.rs index 47df487..40c830d 100644 --- a/src/components/pages/category_today_page.rs +++ b/src/components/pages/category_today_page.rs @@ -1,7 +1,7 @@ use crate::components::task_list::TaskList; use crate::models::category::Category; -use crate::models::task::Task; -use crate::query::tasks::use_tasks_in_category_query; +use crate::models::task::TaskWithSubtasks; +use crate::query::tasks::{use_tasks_with_subtasks_in_category_query}; use crate::query::QueryValue; use chrono::{Local, Locale}; use dioxus::prelude::*; @@ -11,22 +11,22 @@ use dioxus_query::prelude::QueryResult; pub(crate) fn CategoryTodayPage() -> Element { let today_date = Local::now().date_naive(); - let calendar_tasks_query = use_tasks_in_category_query(Category::Calendar { + let calendar_tasks_query = use_tasks_with_subtasks_in_category_query(Category::Calendar { date: today_date, reoccurrence: None, time: None, }); let calendar_tasks_query_result = calendar_tasks_query.result(); - let long_term_tasks_query = use_tasks_in_category_query(Category::LongTerm); + let long_term_tasks_query = use_tasks_with_subtasks_in_category_query(Category::LongTerm); let long_term_tasks_query_result = long_term_tasks_query.result(); rsx! { div { class: "pt-4 flex flex-col gap-8", match long_term_tasks_query_result.value() { - QueryResult::Ok(QueryValue::Tasks(tasks)) - | QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => rsx! { + QueryResult::Ok(QueryValue::TasksWithSubtasks(tasks)) + | QueryResult::Loading(Some(QueryValue::TasksWithSubtasks(tasks))) => rsx! { div { class: "flex flex-col gap-4", div { @@ -42,10 +42,10 @@ pub(crate) fn CategoryTodayPage() -> Element { div { for task in tasks { div { - key: "{task.id()}", + key: "{task.task().id()}", class: format!( "px-8 pt-5 {} flex flex-row gap-4", - if task.deadline().is_some() { + if task.task().deadline().is_some() { "pb-0.5" } else { "pb-5" @@ -55,11 +55,11 @@ pub(crate) fn CategoryTodayPage() -> Element { class: "flex flex-col", div { class: "mt grow font-medium", - {task.title()} + {task.task().title()} }, div { class: "flex flex-row gap-3", - if let Some(deadline) = task.deadline() { + if let Some(deadline) = task.task().deadline() { div { class: "text-sm text-zinc-400", i { @@ -86,22 +86,22 @@ pub(crate) fn CategoryTodayPage() -> Element { value => panic!("Unexpected query result: {value:?}") } match calendar_tasks_query_result.value() { - QueryResult::Ok(QueryValue::Tasks(tasks)) - | QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => { + QueryResult::Ok(QueryValue::TasksWithSubtasks(tasks)) + | QueryResult::Loading(Some(QueryValue::TasksWithSubtasks(tasks))) => { let today_tasks = tasks.iter().filter(|task| { - if let Category::Calendar { date, .. } = task.category() { + if let Category::Calendar { date, .. } = task.task().category() { *date == today_date } else { panic!("Unexpected category."); } - }).cloned().collect::>(); + }).cloned().collect::>(); let overdue_tasks = tasks.iter().filter(|task| { - if let Category::Calendar { date, .. } = task.category() { + if let Category::Calendar { date, .. } = task.task().category() { *date < today_date } else { panic!("Unexpected category."); } - }).cloned().collect::>(); + }).cloned().collect::>(); rsx! { if !overdue_tasks.is_empty() { diff --git a/src/components/subtasks_form.rs b/src/components/subtasks_form.rs index db03efb..29fb00f 100644 --- a/src/components/subtasks_form.rs +++ b/src/components/subtasks_form.rs @@ -1,4 +1,5 @@ use crate::models::subtask::NewSubtask; +use crate::models::task::Task; use crate::query::subtasks::use_subtasks_of_task_query; use crate::query::{QueryErrors, QueryKey, QueryValue}; use crate::server::subtasks::{create_subtask, delete_subtask, edit_subtask}; @@ -8,151 +9,172 @@ use dioxus::prelude::*; use dioxus_query::prelude::{use_query_client, QueryResult}; #[component] -pub(crate) fn SubtasksForm(task_id: i32) -> Element { +pub(crate) fn SubtasksForm(task: Task) -> Element { let query_client = use_query_client::(); - let subtasks_query = use_subtasks_of_task_query(task_id); - + let subtasks_query = use_subtasks_of_task_query(task.id()); + let mut new_title = use_signal(String::new); rsx! { - form { - class: "flex flex-row items-center gap-3", - onsubmit: move |event| async move { + form { + class: "flex flex-row items-center gap-3", + onsubmit: move |event| { + let task = task.clone(); + async move { let new_subtask = NewSubtask::new( - task_id, + task.id(), event.values().get("title").unwrap().as_value(), false ); let _ = create_subtask(new_subtask).await; - query_client.invalidate_queries(&[QueryKey::SubtasksOfTaskId(task_id)]); + query_client.invalidate_queries(&[ + QueryKey::SubtasksOfTaskId(task.id()), + QueryKey::TasksWithSubtasksInCategory(task.category().clone()), + ]); new_title.set(String::new()); - }, - label { - r#for: "input_new_title", - class: "min-w-6 text-center", - i { - class: "fa-solid fa-list-check text-zinc-400/50" - } } - div { - class: "grow grid grid-cols-6 gap-2", - input { - name: "title", - required: true, - value: new_title, - r#type: "text", - class: "grow py-2 px-3 col-span-5 bg-zinc-800/50 rounded-lg", - id: "input_new_title", - onchange: move |event| new_title.set(event.value()) - } - button { - r#type: "submit", - class: "py-2 col-span-1 bg-zinc-800/50 rounded-lg", - i { - class: "fa-solid fa-plus" - } + }, + label { + r#for: "input_new_title", + class: "min-w-6 text-center", + i { + class: "fa-solid fa-list-check text-zinc-400/50" + } + } + div { + class: "grow grid grid-cols-6 gap-2", + input { + name: "title", + required: true, + value: new_title, + r#type: "text", + class: "grow py-2 px-3 col-span-5 bg-zinc-800/50 rounded-lg", + id: "input_new_title", + onchange: move |event| new_title.set(event.value()) + } + button { + r#type: "submit", + class: "py-2 col-span-1 bg-zinc-800/50 rounded-lg", + i { + class: "fa-solid fa-plus" } } } - match subtasks_query.result().value() { - QueryResult::Ok(QueryValue::Subtasks(subtasks)) - | QueryResult::Loading(Some(QueryValue::Subtasks(subtasks))) => { - rsx! { - for subtask in subtasks.clone() { - div { - key: "{subtask.id()}", - class: "flex flex-row items-center gap-3", - i { - class: format!( - "{} min-w-6 text-center text-2xl text-zinc-400/50", - if subtask.is_completed() { - "fa solid fa-square-check" - } else { - "fa-regular fa-square" - } - ), - onclick: { + } + match subtasks_query.result().value() { + QueryResult::Ok(QueryValue::Subtasks(subtasks)) + | QueryResult::Loading(Some(QueryValue::Subtasks(subtasks))) => { + rsx! { + for subtask in subtasks.clone() { + div { + key: "{subtask.id()}", + class: "flex flex-row items-center gap-3", + i { + class: format!( + "{} min-w-6 text-center text-2xl text-zinc-400/50", + if subtask.is_completed() { + "fa solid fa-square-check" + } else { + "fa-regular fa-square" + } + ), + onclick: { + let subtask = subtask.clone(); + let task = task.clone(); + move |_| { let subtask = subtask.clone(); - move |_| { + let task = task.clone(); + async move { + let new_subtask = NewSubtask::new( + subtask.task_id(), + subtask.title().to_owned(), + !subtask.is_completed() + ); + let _ = edit_subtask( + subtask.id(), + new_subtask + ).await; + query_client.invalidate_queries(&[ + QueryKey::SubtasksOfTaskId(task.id()), + QueryKey::TasksWithSubtasksInCategory( + task.category().clone() + ), + ]); + } + } + } + } + div { + class: "grow grid grid-cols-6 gap-2", + input { + r#type: "text", + class: "grow py-2 px-3 col-span-5 bg-zinc-800/50 rounded-lg", + id: "input_new_title", + initial_value: subtask.title(), + onchange: { + let subtask = subtask.clone(); + let task = task.clone(); + move |event| { let subtask = subtask.clone(); + let task = task.clone(); async move { let new_subtask = NewSubtask::new( subtask.task_id(), - subtask.title().to_owned(), - !subtask.is_completed() + event.value(), + subtask.is_completed() ); let _ = edit_subtask( subtask.id(), new_subtask ).await; query_client.invalidate_queries(&[ - QueryKey::SubtasksOfTaskId(task_id) + QueryKey::SubtasksOfTaskId(task.id()), + QueryKey::TasksWithSubtasksInCategory( + task.category().clone() + ), ]); } } } } - div { - class: "grow grid grid-cols-6 gap-2", - input { - r#type: "text", - class: "grow py-2 px-3 col-span-5 bg-zinc-800/50 rounded-lg", - id: "input_new_title", - initial_value: subtask.title(), - onchange: { + button { + r#type: "button", + class: "py-2 col-span-1 bg-zinc-800/50 rounded-lg", + onclick: { + let subtask = subtask.clone(); + let task = task.clone(); + move |_| { let subtask = subtask.clone(); - move |event| { - let subtask = subtask.clone(); - async move { - let new_subtask = NewSubtask::new( - subtask.task_id(), - event.value(), - subtask.is_completed() - ); - let _ = edit_subtask( - subtask.id(), - new_subtask - ).await; - query_client.invalidate_queries(&[ - QueryKey::SubtasksOfTaskId(task_id) - ]); - } + let task = task.clone(); + async move { + let _ = delete_subtask(subtask.id()).await; + query_client.invalidate_queries(&[ + QueryKey::SubtasksOfTaskId(task.id()), + QueryKey::TasksWithSubtasksInCategory( + task.category().clone() + ), + ]); } } - } - button { - r#type: "button", - class: "py-2 col-span-1 bg-zinc-800/50 rounded-lg", - onclick: { - let subtask = subtask.clone(); - move |_| { - let subtask = subtask.clone(); - async move { - let _ = delete_subtask(subtask.id()).await; - query_client.invalidate_queries(&[ - QueryKey::SubtasksOfTaskId(task_id) - ]); - } - } - }, - i { - class: "fa-solid fa-trash-can" - } + }, + i { + class: "fa-solid fa-trash-can" } } } } } - }, - QueryResult::Loading(None) => rsx! { - // TODO: Add a loading indicator. - }, - QueryResult::Err(errors) => rsx! { - div { - "Errors occurred: {errors:?}" - } - }, - value => panic!("Unexpected query result: {value:?}") - } + } + }, + QueryResult::Loading(None) => rsx! { + // TODO: Add a loading indicator. + }, + QueryResult::Err(errors) => rsx! { + div { + "Errors occurred: {errors:?}" + } + }, + value => panic!("Unexpected query result: {value:?}") + } } } diff --git a/src/components/task_form.rs b/src/components/task_form.rs index 943ca1a..527819d 100644 --- a/src/components/task_form.rs +++ b/src/components/task_form.rs @@ -133,7 +133,8 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() } query_client.invalidate_queries(&[ QueryKey::Tasks, - QueryKey::TasksInCategory(selected_category()) + QueryKey::TasksInCategory(selected_category()), + QueryKey::TasksWithSubtasksInCategory(selected_category()), ]); on_successful_submit.call(()); } @@ -151,6 +152,7 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() name: "title", required: true, initial_value: task.as_ref().map(|task| task.title().to_owned()), + autofocus: task.is_none(), r#type: "text", class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg", id: "input_title" @@ -345,7 +347,7 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() }, if let Some(task) = task.as_ref() { SubtasksForm { - task_id: task.id() + task: task.clone() } } div { @@ -371,8 +373,9 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() } query_client.invalidate_queries(&[ - QueryKey::TasksInCategory(task.category().clone()), QueryKey::Tasks, + QueryKey::TasksInCategory(task.category().clone()), + QueryKey::TasksWithSubtasksInCategory(selected_category()), ]); } on_successful_submit.call(()); diff --git a/src/components/task_list.rs b/src/components/task_list.rs index 65f00e8..b8d3e78 100644 --- a/src/components/task_list.rs +++ b/src/components/task_list.rs @@ -1,5 +1,5 @@ use crate::models::category::Category; -use crate::models::task::Task; +use crate::models::task::{Task, TaskWithSubtasks}; use dioxus::core_macro::rsx; use dioxus::dioxus_core::Element; use dioxus::prelude::*; @@ -8,7 +8,7 @@ use crate::query::{QueryErrors, QueryKey, QueryValue}; use crate::server::tasks::complete_task; #[component] -pub(crate) fn TaskList(tasks: Vec, class: Option<&'static str>) -> Element { +pub(crate) fn TaskList(tasks: Vec, class: Option<&'static str>) -> Element { let query_client = use_query_client::(); let mut task_being_edited = use_context::>>(); @@ -17,12 +17,12 @@ pub(crate) fn TaskList(tasks: Vec, class: Option<&'static str>) -> Element class: format!("flex flex-col {}", class.unwrap_or("")), for task in tasks.clone() { div { - key: "{task.id()}", + key: "{task.task().id()}", class: format!( "px-8 pt-5 {} flex flex-row gap-4 select-none {}", - if task.deadline().is_some() { + if task.task().deadline().is_some() || !task.subtasks().is_empty() { "pb-0.5" - } else if let Category::Calendar { time, .. } = task.category() { + } else if let Category::Calendar { time, .. } = task.task().category() { if time.is_some() { "pb-0.5" } else { @@ -31,18 +31,18 @@ pub(crate) fn TaskList(tasks: Vec, class: Option<&'static str>) -> Element } else { "pb-5" }, - if task_being_edited().is_some_and(|t| t.id() == task.id()) { + if task_being_edited().is_some_and(|t| t.id() == task.task().id()) { "bg-zinc-700" } else { "" } ), onclick: { let task = task.clone(); - move |_| task_being_edited.set(Some(task.clone())) + move |_| task_being_edited.set(Some(task.task().clone())) }, i { class: format!( "{} text-3xl text-zinc-500", - if *(task.category()) == Category::Done { + if *(task.task().category()) == Category::Done { "fa solid fa-square-check" } else { "fa-regular fa-square" @@ -55,16 +55,19 @@ pub(crate) fn TaskList(tasks: Vec, class: Option<&'static str>) -> Element event.stop_propagation(); let task = task.clone(); async move { - let completed_task = complete_task(task.id()).await; + let completed_task = complete_task(task.task().id()).await.unwrap(); let mut query_keys = vec![ QueryKey::Tasks, QueryKey::TasksInCategory( - completed_task.unwrap().category().clone() - ) + completed_task.category().clone() + ), + QueryKey::TasksWithSubtasksInCategory(completed_task.category().clone()), ]; if let Category::Calendar { reoccurrence: Some(_), .. } - = task.category() { - query_keys.push(QueryKey::SubtasksOfTaskId(task.id())); + = task.task().category() { + query_keys.push( + QueryKey::SubtasksOfTaskId(task.task().id()) + ); } query_client.invalidate_queries(&query_keys); } @@ -75,20 +78,20 @@ pub(crate) fn TaskList(tasks: Vec, class: Option<&'static str>) -> Element class: "flex flex-col", div { class: "mt-1 grow font-medium", - {task.title()} + {task.task().title()} }, div { - class: "flex flex-row gap-3", - if let Some(deadline) = task.deadline() { + class: "flex flex-row gap-4", + if let Some(deadline) = task.task().deadline() { div { class: "text-sm text-zinc-400", i { class: "fa-solid fa-bomb" }, - {deadline.format(" %m. %d.").to_string()} + {deadline.format(" %m. %-d.").to_string()} } } - if let Category::Calendar { time, .. } = task.category() { + if let Category::Calendar { time, .. } = task.task().category() { if let Some(calendar_time) = time { div { class: "text-sm text-zinc-400", @@ -99,6 +102,21 @@ pub(crate) fn TaskList(tasks: Vec, class: Option<&'static str>) -> Element } } } + if !task.subtasks().is_empty() { + div { + class: "text-sm text-zinc-400", + i { + class: "fa-solid fa-list-check" + }, + {format!( + " {}/{}", + task.subtasks().iter() + .filter(|subtask| subtask.is_completed()) + .count(), + task.subtasks().len() + )} + } + } } } } From 1445672dc9e1f874f48cea3faaff4f707030bf25 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Volf?= <66163112+matous-volf@users.noreply.github.com> Date: Mon, 9 Sep 2024 07:33:09 +0200 Subject: [PATCH 3/4] feat: remove the non-functioning input autofocus --- src/components/task_form.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/src/components/task_form.rs b/src/components/task_form.rs index 527819d..b6e6a42 100644 --- a/src/components/task_form.rs +++ b/src/components/task_form.rs @@ -152,7 +152,6 @@ pub(crate) fn TaskForm(task: Option, on_successful_submit: EventHandler<() name: "title", required: true, initial_value: task.as_ref().map(|task| task.title().to_owned()), - autofocus: task.is_none(), r#type: "text", class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg", id: "input_title" From bdfc2bc945a04567ee906d21398603da87b6ab9e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Volf?= <66163112+matous-volf@users.noreply.github.com> Date: Mon, 9 Sep 2024 08:41:57 +0200 Subject: [PATCH 4/4] fix: handle changing a subtask's title to empty --- src/components/subtasks_form.rs | 14 +++++++++----- 1 file changed, 9 insertions(+), 5 deletions(-) diff --git a/src/components/subtasks_form.rs b/src/components/subtasks_form.rs index 29fb00f..3e16e28 100644 --- a/src/components/subtasks_form.rs +++ b/src/components/subtasks_form.rs @@ -109,7 +109,7 @@ pub(crate) fn SubtasksForm(task: Task) -> Element { input { r#type: "text", class: "grow py-2 px-3 col-span-5 bg-zinc-800/50 rounded-lg", - id: "input_new_title", + id: "input_title_{subtask.id()}", initial_value: subtask.title(), onchange: { let subtask = subtask.clone(); @@ -123,10 +123,14 @@ pub(crate) fn SubtasksForm(task: Task) -> Element { event.value(), subtask.is_completed() ); - let _ = edit_subtask( - subtask.id(), - new_subtask - ).await; + if new_subtask.title.is_empty() { + let _ = delete_subtask(subtask.id()).await; + } else { + let _ = edit_subtask( + subtask.id(), + new_subtask + ).await; + } query_client.invalidate_queries(&[ QueryKey::SubtasksOfTaskId(task.id()), QueryKey::TasksWithSubtasksInCategory(