feat: ability to manage subtasks #40
| @@ -12,3 +12,4 @@ pub(crate) mod category_input; | |||||||
| pub(crate) mod reoccurrence_input; | pub(crate) mod reoccurrence_input; | ||||||
| pub(crate) mod layout; | pub(crate) mod layout; | ||||||
| pub(crate) mod navigation_item; | pub(crate) mod navigation_item; | ||||||
|  | pub(crate) mod subtasks_form; | ||||||
|   | |||||||
							
								
								
									
										158
									
								
								src/components/subtasks_form.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										158
									
								
								src/components/subtasks_form.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,158 @@ | |||||||
|  | use crate::models::subtask::NewSubtask; | ||||||
|  | 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}; | ||||||
|  | use dioxus::core_macro::{component, rsx}; | ||||||
|  | use dioxus::dioxus_core::Element; | ||||||
|  | use dioxus::prelude::*; | ||||||
|  | use dioxus_query::prelude::{use_query_client, QueryResult}; | ||||||
|  |  | ||||||
|  | #[component] | ||||||
|  | pub(crate) fn SubtasksForm(task_id: i32) -> Element { | ||||||
|  |     let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>(); | ||||||
|  |     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 { | ||||||
|  |                     let new_subtask = NewSubtask::new( | ||||||
|  |                         task_id, | ||||||
|  |                         event.values().get("title").unwrap().as_value(), | ||||||
|  |                         false | ||||||
|  |                     ); | ||||||
|  |                     let _ = create_subtask(new_subtask).await; | ||||||
|  |                     query_client.invalidate_queries(&[QueryKey::SubtasksOfTaskId(task_id)]); | ||||||
|  |                     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" | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|  |             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(); | ||||||
|  |                                         move |_| { | ||||||
|  |                                             let subtask = subtask.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) | ||||||
|  |                                                 ]); | ||||||
|  |                                             } | ||||||
|  |                                         } | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                                 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(); | ||||||
|  |                                             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) | ||||||
|  |                                                     ]); | ||||||
|  |                                                 } | ||||||
|  |                                             } | ||||||
|  |                                         } | ||||||
|  |                                     } | ||||||
|  |                                     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" | ||||||
|  |                                         } | ||||||
|  |                                     } | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         } | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 QueryResult::Loading(None) => rsx! { | ||||||
|  |                     // TODO: Add a loading indicator. | ||||||
|  |                 }, | ||||||
|  |                 QueryResult::Err(errors) => rsx! { | ||||||
|  |                     div { | ||||||
|  |                         "Errors occurred: {errors:?}" | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 value => panic!("Unexpected query result: {value:?}") | ||||||
|  |             } | ||||||
|  |     } | ||||||
|  | } | ||||||
| @@ -12,6 +12,7 @@ use dioxus::core_macro::{component, rsx}; | |||||||
| use dioxus::dioxus_core::Element; | use dioxus::dioxus_core::Element; | ||||||
| use dioxus::prelude::*; | use dioxus::prelude::*; | ||||||
| use dioxus_query::prelude::use_query_client; | use dioxus_query::prelude::use_query_client; | ||||||
|  | use crate::components::subtasks_form::SubtasksForm; | ||||||
|  |  | ||||||
| const REMINDER_OFFSETS: [Option<Duration>; 17] = [ | const REMINDER_OFFSETS: [Option<Duration>; 17] = [ | ||||||
|     None, |     None, | ||||||
| @@ -79,262 +80,274 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<() | |||||||
|     let task_for_submit = task.clone(); |     let task_for_submit = task.clone(); | ||||||
|  |  | ||||||
|     rsx! { |     rsx! { | ||||||
|         form { |         div { | ||||||
|             onsubmit: move |event| { |  | ||||||
|                 let task = task_for_submit.clone(); |  | ||||||
|                 async move { |  | ||||||
|                     let new_task = NewTask::new( |  | ||||||
|                         event.values().get("title").unwrap().as_value(), |  | ||||||
|                         event.values().get("deadline").unwrap().as_value().parse().ok(), |  | ||||||
|                         match &selected_category() { |  | ||||||
|                             Category::WaitingFor(_) => Category::WaitingFor( |  | ||||||
|                                 event.values().get("category_waiting_for").unwrap() |  | ||||||
|                                 .as_value() |  | ||||||
|                             ), |  | ||||||
|                             Category::Calendar { .. } => Category::Calendar { |  | ||||||
|                                 date: event.values().get("category_calendar_date").unwrap() |  | ||||||
|                                 .as_value().parse().unwrap(), |  | ||||||
|                                 reoccurrence: category_calendar_reoccurrence_interval().map( |  | ||||||
|                                     |reoccurrence_interval| Reoccurrence::new( |  | ||||||
|                                         event.values().get("category_calendar_date").unwrap() |  | ||||||
|                                         .as_value().parse().unwrap(), |  | ||||||
|                                         reoccurrence_interval, |  | ||||||
|                                         event.values().get("category_calendar_reoccurrence_length") |  | ||||||
|                                         .unwrap().as_value().parse().unwrap() |  | ||||||
|                                     ) |  | ||||||
|                                 ), |  | ||||||
|                                 time: event.values().get("category_calendar_time").unwrap() |  | ||||||
|                                 .as_value().parse().ok().map(|time| |  | ||||||
|                                     CalendarTime::new( |  | ||||||
|                                         time, |  | ||||||
|                                         REMINDER_OFFSETS[ |  | ||||||
|                                             event.values() |  | ||||||
|                                             .get("category_calendar_reminder_offset_index").unwrap() |  | ||||||
|                                             .as_value().parse::<usize>().unwrap() |  | ||||||
|                                         ] |  | ||||||
|                                     ) |  | ||||||
|                                 ) |  | ||||||
|                             }, |  | ||||||
|                             category => category.clone() |  | ||||||
|                         }, |  | ||||||
|                         event.values().get("project_id").unwrap() |  | ||||||
|                         .as_value().parse::<i32>().ok().filter(|&id| id > 0), |  | ||||||
|                     ); |  | ||||||
|                     if let Some(task) = task { |  | ||||||
|                         let _ = edit_task(task.id(), new_task).await; |  | ||||||
|                     } else { |  | ||||||
|                         let _ = create_task(new_task).await; |  | ||||||
|                     } |  | ||||||
|                     query_client.invalidate_queries(&[ |  | ||||||
|                         QueryKey::Tasks,  |  | ||||||
|                         QueryKey::TasksInCategory(selected_category()) |  | ||||||
|                     ]); |  | ||||||
|                     on_successful_submit.call(()); |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             class: "p-4 flex flex-col gap-4", |             class: "p-4 flex flex-col gap-4", | ||||||
|             div { |             form { | ||||||
|                 class: "flex flex-row items-center gap-3", |                 class: "flex flex-col gap-4", | ||||||
|                 label { |                 id: "form_task", | ||||||
|                     r#for: "input_title", |                 onsubmit: move |event| { | ||||||
|                     class: "min-w-6 text-center", |                     let task = task_for_submit.clone(); | ||||||
|                     i { |                     async move { | ||||||
|                         class: "fa-solid fa-pen-clip text-zinc-400/50" |                         let new_task = NewTask::new( | ||||||
|                     }, |                             event.values().get("title").unwrap().as_value(), | ||||||
|                 }, |                             event.values().get("deadline").unwrap().as_value().parse().ok(), | ||||||
|                 input { |                             match &selected_category() { | ||||||
|                     name: "title", |                                 Category::WaitingFor(_) => Category::WaitingFor( | ||||||
|                     required: true, |                                     event.values().get("category_waiting_for").unwrap() | ||||||
|                     initial_value: task.as_ref().map(|task| task.title().to_owned()), |                                     .as_value() | ||||||
|                     r#type: "text", |  | ||||||
|                     class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg", |  | ||||||
|                     id: "input_title" |  | ||||||
|                 }, |  | ||||||
|             }, |  | ||||||
|             div { |  | ||||||
|                 class: "flex flex-row items-center gap-3", |  | ||||||
|                 label { |  | ||||||
|                     r#for: "input_project", |  | ||||||
|                     class: "min-w-6 text-center", |  | ||||||
|                     i { |  | ||||||
|                         class: "fa-solid fa-list text-zinc-400/50" |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 select { |  | ||||||
|                     name: "project_id", |  | ||||||
|                     class: "px-3.5 py-2.5 bg-zinc-800/50 rounded-lg grow", |  | ||||||
|                     id: "input_project", |  | ||||||
|                     option { |  | ||||||
|                         value: 0, |  | ||||||
|                         "None" |  | ||||||
|                     }, |  | ||||||
|                     for project in projects { |  | ||||||
|                         option { |  | ||||||
|                             value: project.id().to_string(), |  | ||||||
|                             selected: task.as_ref().is_some_and( |  | ||||||
|                                 |task| task.project_id() == Some(project.id()) |  | ||||||
|                             ), |  | ||||||
|                             {project.title()} |  | ||||||
|                         } |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|             }, |  | ||||||
|             div { |  | ||||||
|                 class: "flex flex-row items-center gap-3", |  | ||||||
|                 label { |  | ||||||
|                     r#for: "input_deadline", |  | ||||||
|                     class: "min-w-6 text-center", |  | ||||||
|                     i { |  | ||||||
|                         class: "fa-solid fa-bomb text-zinc-400/50" |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 input { |  | ||||||
|                     name: "deadline", |  | ||||||
|                     initial_value: task.as_ref().and_then(|task| task.deadline()) |  | ||||||
|                         .map(|deadline| deadline.format("%Y-%m-%d").to_string()), |  | ||||||
|                     r#type: "date", |  | ||||||
|                     class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow basis-0", |  | ||||||
|                     id: "input_deadline" |  | ||||||
|                 } |  | ||||||
|             }, |  | ||||||
|             div { |  | ||||||
|                 class: "flex flex-row items-center gap-3", |  | ||||||
|                 label { |  | ||||||
|                     class: "min-w-6 text-center", |  | ||||||
|                     i { |  | ||||||
|                         class: "fa-solid fa-layer-group text-zinc-400/50" |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 CategoryInput { |  | ||||||
|                     selected_category: selected_category, |  | ||||||
|                     class: "grow" |  | ||||||
|                 } |  | ||||||
|             } |  | ||||||
|             match selected_category() { |  | ||||||
|                 Category::WaitingFor(waiting_for) => rsx! { |  | ||||||
|                     div { |  | ||||||
|                         class: "flex flex-row items-center gap-3", |  | ||||||
|                         label { |  | ||||||
|                             r#for: "input_deadline", |  | ||||||
|                             class: "min-w-6 text-center", |  | ||||||
|                             i { |  | ||||||
|                                 class: "fa-solid fa-hourglass-end text-zinc-400/50" |  | ||||||
|                             } |  | ||||||
|                         }, |  | ||||||
|                         input { |  | ||||||
|                             name: "category_waiting_for", |  | ||||||
|                             required: true, |  | ||||||
|                             initial_value: waiting_for, |  | ||||||
|                             r#type: "text", |  | ||||||
|                             class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow", |  | ||||||
|                             id: "input_category_waiting_for" |  | ||||||
|                         }, |  | ||||||
|                     } |  | ||||||
|                 }, |  | ||||||
|                 Category::Calendar { date, reoccurrence, time } => rsx! { |  | ||||||
|                     div { |  | ||||||
|                         class: "flex flex-row items-center gap-3", |  | ||||||
|                         label { |  | ||||||
|                             r#for: "input_category_calendar_date", |  | ||||||
|                             class: "min-w-6 text-center", |  | ||||||
|                             i { |  | ||||||
|                                 class: "fa-solid fa-clock text-zinc-400/50" |  | ||||||
|                             } |  | ||||||
|                         }, |  | ||||||
|                         div { |  | ||||||
|                             class: "grow flex flex-row gap-2", |  | ||||||
|                             input { |  | ||||||
|                                 r#type: "date", |  | ||||||
|                                 name: "category_calendar_date", |  | ||||||
|                                 required: true, |  | ||||||
|                                 initial_value: date.format("%Y-%m-%d").to_string(), |  | ||||||
|                                 class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow", |  | ||||||
|                                 id: "input_category_calendar_date" |  | ||||||
|                             }, |  | ||||||
|                             input { |  | ||||||
|                                 r#type: "time", |  | ||||||
|                                 name: "category_calendar_time", |  | ||||||
|                                 initial_value: time.map(|calendar_time| |  | ||||||
|                                     calendar_time.time().format("%H:%M").to_string() |  | ||||||
|                                 ), |                                 ), | ||||||
|                                 class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow", |                                 Category::Calendar { .. } => Category::Calendar { | ||||||
|                                 id: "input_category_calendar_time", |                                     date: event.values().get("category_calendar_date").unwrap() | ||||||
|                                 oninput: move |event| { |                                     .as_value().parse().unwrap(), | ||||||
|                                     category_calendar_has_time.set(!event.value().is_empty()); |                                     reoccurrence: category_calendar_reoccurrence_interval().map( | ||||||
|                                 } |                                         |reoccurrence_interval| Reoccurrence::new( | ||||||
|                             } |                                             event.values().get("category_calendar_date").unwrap() | ||||||
|                         } |                                             .as_value().parse().unwrap(), | ||||||
|                     }, |                                             reoccurrence_interval, | ||||||
|                     div { |                                             event.values() | ||||||
|                         class: "flex flex-row items-center gap-3", |                                             .get("category_calendar_reoccurrence_length") | ||||||
|                         label { |                                             .unwrap().as_value().parse().unwrap() | ||||||
|                             r#for: "category_calendar_reoccurrence_length", |                                         ) | ||||||
|                             class: "min-w-6 text-center", |                                     ), | ||||||
|                             i { |                                     time: event.values().get("category_calendar_time").unwrap() | ||||||
|                                 class: "fa-solid fa-repeat text-zinc-400/50" |                                     .as_value().parse().ok().map(|time| | ||||||
|                             } |                                         CalendarTime::new( | ||||||
|                         }, |                                             time, | ||||||
|                         div { |                                             REMINDER_OFFSETS[ | ||||||
|                             class: "grow grid grid-cols-6 gap-2", |                                                 event.values() | ||||||
|                             ReoccurrenceIntervalInput { |                                                 .get("category_calendar_reminder_offset_index") | ||||||
|                                 reoccurrence_interval: category_calendar_reoccurrence_interval |                                                 .unwrap().as_value().parse::<usize>().unwrap() | ||||||
|  |                                             ] | ||||||
|  |                                         ) | ||||||
|  |                                     ) | ||||||
|  |                                 }, | ||||||
|  |                                 category => category.clone() | ||||||
|                             }, |                             }, | ||||||
|                             input { |                             event.values().get("project_id").unwrap() | ||||||
|                                 r#type: "number", |                             .as_value().parse::<i32>().ok().filter(|&id| id > 0), | ||||||
|                                 inputmode: "numeric", |                         ); | ||||||
|                                 name: "category_calendar_reoccurrence_length", |                         if let Some(task) = task { | ||||||
|                                 disabled: category_calendar_reoccurrence_interval().is_none(), |                             let _ = edit_task(task.id(), new_task).await; | ||||||
|                                 required: true, |                         } else { | ||||||
|                                 min: 1, |                             let _ = create_task(new_task).await; | ||||||
|                                 initial_value: category_calendar_reoccurrence_interval() |                         } | ||||||
|                                     .map_or(String::new(), |_| reoccurrence.map_or(1, |reoccurrence| |                         query_client.invalidate_queries(&[ | ||||||
|                                         reoccurrence.length()).to_string()), |                             QueryKey::Tasks, | ||||||
|                                 class: "py-2 px-3 bg-zinc-800/50 rounded-lg col-span-2 text-right", |                             QueryKey::TasksInCategory(selected_category()) | ||||||
|                                 id: "category_calendar_reoccurrence_length" |                         ]); | ||||||
|  |                         on_successful_submit.call(()); | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 div { | ||||||
|  |                     class: "flex flex-row items-center gap-3", | ||||||
|  |                     label { | ||||||
|  |                         r#for: "input_title", | ||||||
|  |                         class: "min-w-6 text-center", | ||||||
|  |                         i { | ||||||
|  |                             class: "fa-solid fa-pen-clip text-zinc-400/50" | ||||||
|  |                         }, | ||||||
|  |                     }, | ||||||
|  |                     input { | ||||||
|  |                         name: "title", | ||||||
|  |                         required: true, | ||||||
|  |                         initial_value: task.as_ref().map(|task| task.title().to_owned()), | ||||||
|  |                         r#type: "text", | ||||||
|  |                         class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg", | ||||||
|  |                         id: "input_title" | ||||||
|  |                     }, | ||||||
|  |                 }, | ||||||
|  |                 div { | ||||||
|  |                     class: "flex flex-row items-center gap-3", | ||||||
|  |                     label { | ||||||
|  |                         r#for: "input_project", | ||||||
|  |                         class: "min-w-6 text-center", | ||||||
|  |                         i { | ||||||
|  |                             class: "fa-solid fa-list text-zinc-400/50" | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     select { | ||||||
|  |                         name: "project_id", | ||||||
|  |                         class: "px-3.5 py-2.5 bg-zinc-800/50 rounded-lg grow", | ||||||
|  |                         id: "input_project", | ||||||
|  |                         option { | ||||||
|  |                             value: 0, | ||||||
|  |                             "None" | ||||||
|  |                         }, | ||||||
|  |                         for project in projects { | ||||||
|  |                             option { | ||||||
|  |                                 value: project.id().to_string(), | ||||||
|  |                                 selected: task.as_ref().is_some_and( | ||||||
|  |                                     |task| task.project_id() == Some(project.id()) | ||||||
|  |                                 ), | ||||||
|  |                                 {project.title()} | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|                     }, |                     }, | ||||||
|                     if category_calendar_has_time() { |                 }, | ||||||
|  |                 div { | ||||||
|  |                     class: "flex flex-row items-center gap-3", | ||||||
|  |                     label { | ||||||
|  |                         r#for: "input_deadline", | ||||||
|  |                         class: "min-w-6 text-center", | ||||||
|  |                         i { | ||||||
|  |                             class: "fa-solid fa-bomb text-zinc-400/50" | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     input { | ||||||
|  |                         name: "deadline", | ||||||
|  |                         initial_value: task.as_ref().and_then(|task| task.deadline()) | ||||||
|  |                             .map(|deadline| deadline.format("%Y-%m-%d").to_string()), | ||||||
|  |                         r#type: "date", | ||||||
|  |                         class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow basis-0", | ||||||
|  |                         id: "input_deadline" | ||||||
|  |                     } | ||||||
|  |                 }, | ||||||
|  |                 div { | ||||||
|  |                     class: "flex flex-row items-center gap-3", | ||||||
|  |                     label { | ||||||
|  |                         class: "min-w-6 text-center", | ||||||
|  |                         i { | ||||||
|  |                             class: "fa-solid fa-layer-group text-zinc-400/50" | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     CategoryInput { | ||||||
|  |                         selected_category: selected_category, | ||||||
|  |                         class: "grow" | ||||||
|  |                     } | ||||||
|  |                 } | ||||||
|  |                 match selected_category() { | ||||||
|  |                     Category::WaitingFor(waiting_for) => rsx! { | ||||||
|                         div { |                         div { | ||||||
|                             class: "flex flex-row items-center gap-3", |                             class: "flex flex-row items-center gap-3", | ||||||
|                             label { |                             label { | ||||||
|                                 r#for: "category_calendar_reminder_offset_index", |                                 r#for: "input_deadline", | ||||||
|                                 class: "min-w-6 text-center", |                                 class: "min-w-6 text-center", | ||||||
|                                 i { |                                 i { | ||||||
|                                     class: "fa-solid fa-bell text-zinc-400/50" |                                     class: "fa-solid fa-hourglass-end text-zinc-400/50" | ||||||
|                                 } |                                 } | ||||||
|                             }, |                             }, | ||||||
|                             input { |                             input { | ||||||
|                                 r#type: "range", |                                 name: "category_waiting_for", | ||||||
|                                 name: "category_calendar_reminder_offset_index", |                                 required: true, | ||||||
|                                 min: 0, |                                 initial_value: waiting_for, | ||||||
|                                 max: REMINDER_OFFSETS.len() as i64 - 1, |                                 r#type: "text", | ||||||
|                                 initial_value: category_calendar_reminder_offset_index() |                                 class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow", | ||||||
|                                     .to_string(), |                                 id: "input_category_waiting_for" | ||||||
|                                 class: "grow input-range-reverse", |                             }, | ||||||
|                                 id: "category_calendar_has_reminder", |                         } | ||||||
|                                 oninput: move |event| { |                     }, | ||||||
|                                     category_calendar_reminder_offset_index.set( |                     Category::Calendar { date, reoccurrence, time } => rsx! { | ||||||
|                                         event.value().parse().unwrap() |                         div { | ||||||
|                                     ); |                             class: "flex flex-row items-center gap-3", | ||||||
|  |                             label { | ||||||
|  |                                 r#for: "input_category_calendar_date", | ||||||
|  |                                 class: "min-w-6 text-center", | ||||||
|  |                                 i { | ||||||
|  |                                     class: "fa-solid fa-clock text-zinc-400/50" | ||||||
|                                 } |                                 } | ||||||
|                             }, |                             }, | ||||||
|                             label { |                             div { | ||||||
|                                 r#for: "category_calendar_reminder_offset_index", |                                 class: "grow flex flex-row gap-2", | ||||||
|                                 class: "pr-3 min-w-16 text-right", |                                 input { | ||||||
|                                 {REMINDER_OFFSETS[category_calendar_reminder_offset_index()].map( |                                     r#type: "date", | ||||||
|                                     |offset| if offset.num_hours() < 1 { |                                     name: "category_calendar_date", | ||||||
|                                         format!("{} min", offset.num_minutes()) |                                     required: true, | ||||||
|                                     } else { |                                     initial_value: date.format("%Y-%m-%d").to_string(), | ||||||
|                                         format!("{} h", offset.num_hours()) |                                     class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow", | ||||||
|  |                                     id: "input_category_calendar_date" | ||||||
|  |                                 }, | ||||||
|  |                                 input { | ||||||
|  |                                     r#type: "time", | ||||||
|  |                                     name: "category_calendar_time", | ||||||
|  |                                     initial_value: time.map(|calendar_time| | ||||||
|  |                                         calendar_time.time().format("%H:%M").to_string() | ||||||
|  |                                     ), | ||||||
|  |                                     class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow", | ||||||
|  |                                     id: "input_category_calendar_time", | ||||||
|  |                                     oninput: move |event| { | ||||||
|  |                                         category_calendar_has_time.set(!event.value().is_empty()); | ||||||
|                                     } |                                     } | ||||||
|                                 ).unwrap_or_else(|| "none".to_string())} |                                 } | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         div { | ||||||
|  |                             class: "flex flex-row items-center gap-3", | ||||||
|  |                             label { | ||||||
|  |                                 r#for: "category_calendar_reoccurrence_length", | ||||||
|  |                                 class: "min-w-6 text-center", | ||||||
|  |                                 i { | ||||||
|  |                                     class: "fa-solid fa-repeat text-zinc-400/50" | ||||||
|  |                                 } | ||||||
|  |                             }, | ||||||
|  |                             div { | ||||||
|  |                                 class: "grow grid grid-cols-6 gap-2", | ||||||
|  |                                 ReoccurrenceIntervalInput { | ||||||
|  |                                     reoccurrence_interval: category_calendar_reoccurrence_interval | ||||||
|  |                                 }, | ||||||
|  |                                 input { | ||||||
|  |                                     r#type: "number", | ||||||
|  |                                     inputmode: "numeric", | ||||||
|  |                                     name: "category_calendar_reoccurrence_length", | ||||||
|  |                                     disabled: category_calendar_reoccurrence_interval().is_none(), | ||||||
|  |                                     required: true, | ||||||
|  |                                     min: 1, | ||||||
|  |                                     initial_value: category_calendar_reoccurrence_interval().map_or( | ||||||
|  |                                         String::new(), | ||||||
|  |                                         |_| reoccurrence.map_or(1, |reoccurrence| | ||||||
|  |                                             reoccurrence.length()).to_string() | ||||||
|  |                                     ), | ||||||
|  |                                     class: "py-2 px-3 bg-zinc-800/50 rounded-lg col-span-2 text-right", | ||||||
|  |                                     id: "category_calendar_reoccurrence_length" | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                         }, | ||||||
|  |                         if category_calendar_has_time() { | ||||||
|  |                             div { | ||||||
|  |                                 class: "flex flex-row items-center gap-3", | ||||||
|  |                                 label { | ||||||
|  |                                     r#for: "category_calendar_reminder_offset_index", | ||||||
|  |                                     class: "min-w-6 text-center", | ||||||
|  |                                     i { | ||||||
|  |                                         class: "fa-solid fa-bell text-zinc-400/50" | ||||||
|  |                                     } | ||||||
|  |                                 }, | ||||||
|  |                                 input { | ||||||
|  |                                     r#type: "range", | ||||||
|  |                                     name: "category_calendar_reminder_offset_index", | ||||||
|  |                                     min: 0, | ||||||
|  |                                     max: REMINDER_OFFSETS.len() as i64 - 1, | ||||||
|  |                                     initial_value: category_calendar_reminder_offset_index() | ||||||
|  |                                         .to_string(), | ||||||
|  |                                     class: "grow input-range-reverse", | ||||||
|  |                                     id: "category_calendar_has_reminder", | ||||||
|  |                                     oninput: move |event| { | ||||||
|  |                                         category_calendar_reminder_offset_index.set( | ||||||
|  |                                             event.value().parse().unwrap() | ||||||
|  |                                         ); | ||||||
|  |                                     } | ||||||
|  |                                 }, | ||||||
|  |                                 label { | ||||||
|  |                                     r#for: "category_calendar_reminder_offset_index", | ||||||
|  |                                     class: "pr-3 min-w-16 text-right", | ||||||
|  |                                     {REMINDER_OFFSETS[category_calendar_reminder_offset_index()].map( | ||||||
|  |                                         |offset| if offset.num_hours() < 1 { | ||||||
|  |                                             format!("{} min", offset.num_minutes()) | ||||||
|  |                                         } else { | ||||||
|  |                                             format!("{} h", offset.num_hours()) | ||||||
|  |                                         } | ||||||
|  |                                     ).unwrap_or_else(|| "none".to_string())} | ||||||
|  |                                 } | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|                     } |                     }, | ||||||
|                 }, |                     _ => None | ||||||
|                 _ => None |                 } | ||||||
|             }, |             }, | ||||||
|  |             if let Some(task) = task.as_ref() { | ||||||
|  |                 SubtasksForm { | ||||||
|  |                     task_id: task.id() | ||||||
|  |                 } | ||||||
|  |             } | ||||||
|             div { |             div { | ||||||
|                 class: "flex flex-row justify-between mt-auto", |                 class: "flex flex-row justify-between mt-auto", | ||||||
|                 button { |                 button { | ||||||
| @@ -353,10 +366,10 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<() | |||||||
|                                         Category::Trash, |                                         Category::Trash, | ||||||
|                                         task.project_id() |                                         task.project_id() | ||||||
|                                     ); |                                     ); | ||||||
|                                      |  | ||||||
|                                     let _ = edit_task(task.id(), new_task).await; |                                     let _ = edit_task(task.id(), new_task).await; | ||||||
|                                 } |                                 } | ||||||
|                                  |  | ||||||
|                                 query_client.invalidate_queries(&[ |                                 query_client.invalidate_queries(&[ | ||||||
|                                     QueryKey::TasksInCategory(task.category().clone()), |                                     QueryKey::TasksInCategory(task.category().clone()), | ||||||
|                                     QueryKey::Tasks, |                                     QueryKey::Tasks, | ||||||
| @@ -370,6 +383,7 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<() | |||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|                 button { |                 button { | ||||||
|  |                     form: "form_task", | ||||||
|                     r#type: "submit", |                     r#type: "submit", | ||||||
|                     class: "py-2 px-4 bg-zinc-300/50 rounded-lg", |                     class: "py-2 px-4 bg-zinc-300/50 rounded-lg", | ||||||
|                     i { |                     i { | ||||||
|   | |||||||
| @@ -15,86 +15,94 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element | |||||||
|     rsx! { |     rsx! { | ||||||
|         div { |         div { | ||||||
|             class: format!("flex flex-col {}", class.unwrap_or("")), |             class: format!("flex flex-col {}", class.unwrap_or("")), | ||||||
|             {tasks.iter().cloned().map(|task| { |             for task in tasks.clone() { | ||||||
|                 let task_clone = task.clone(); |                 div { | ||||||
|                 rsx! { |                     key: "{task.id()}", | ||||||
|                     div { |                     class: format!( | ||||||
|                         key: "{task.id()}", |                         "px-8 pt-5 {} flex flex-row gap-4 select-none {}", | ||||||
|                         class: format!( |                         if task.deadline().is_some() { | ||||||
|                             "px-8 pt-5 {} flex flex-row gap-4 select-none {}", |                             "pb-0.5" | ||||||
|                             if task.deadline().is_some() { |                         } else if let Category::Calendar { time, .. } = task.category() { | ||||||
|  |                             if time.is_some() { | ||||||
|  | |||||||
|                                 "pb-0.5" |                                 "pb-0.5" | ||||||
|                             } else if let Category::Calendar { time, .. } = task.category() { |  | ||||||
|                                 if time.is_some() { |  | ||||||
|                                     "pb-0.5" |  | ||||||
|                                 } else { |  | ||||||
|                                     "pb-5" |  | ||||||
|                                 } |  | ||||||
|                             } else { |                             } else { | ||||||
|                                 "pb-5" |                                 "pb-5" | ||||||
|                             }, |                             } | ||||||
|                             if task_being_edited().is_some_and(|t| t.id() == task.id()) { |                         } else { | ||||||
|                                 "bg-zinc-700" |                             "pb-5" | ||||||
|                             } else { "" } |                         }, | ||||||
|  |                         if task_being_edited().is_some_and(|t| t.id() == task.id()) { | ||||||
|  |                             "bg-zinc-700" | ||||||
|  |                         } else { "" } | ||||||
|  |                     ), | ||||||
|  |                     onclick: { | ||||||
|  |                         let task = task.clone(); | ||||||
|  |                         move |_| task_being_edited.set(Some(task.clone())) | ||||||
|  |                     }, | ||||||
|  |                     i { | ||||||
|  |                         class: format!( | ||||||
|  |                             "{} text-3xl text-zinc-500", | ||||||
|  |                             if *(task.category()) == Category::Done { | ||||||
|  |                                 "fa solid fa-square-check" | ||||||
|  |                             } else { | ||||||
|  |                                 "fa-regular fa-square" | ||||||
|  |                             } | ||||||
|                         ), |                         ), | ||||||
|                         onclick: move |_| task_being_edited.set(Some(task.clone())), |                         onclick: { | ||||||
|                         i { |                             let task = task.clone(); | ||||||
|                             class: format!( |                             move |event| { | ||||||
|                                 "{} text-3xl text-zinc-500", |  | ||||||
|                                 if *(task_clone.category()) == Category::Done { |  | ||||||
|                                     "fa solid fa-square-check" |  | ||||||
|                                 } else { |  | ||||||
|                                     "fa-regular fa-square" |  | ||||||
|                                 } |  | ||||||
|                             ), |  | ||||||
|                             onclick: move |event| { |  | ||||||
|                                 // To prevent editing the task. |                                 // To prevent editing the task. | ||||||
|                                 event.stop_propagation(); |                                 event.stop_propagation(); | ||||||
|                                 let task = task_clone.clone(); |                                 let task = task.clone(); | ||||||
|                                 async move { |                                 async move { | ||||||
|                                     let completed_task = complete_task(task.id()).await; |                                     let completed_task = complete_task(task.id()).await; | ||||||
|                                     query_client.invalidate_queries(&[ |                                     let mut query_keys = vec![ | ||||||
|                                         QueryKey::Tasks,  |                                         QueryKey::Tasks, | ||||||
|                                         QueryKey::TasksInCategory( |                                         QueryKey::TasksInCategory( | ||||||
|                                             completed_task.unwrap().category().clone() |                                             completed_task.unwrap().category().clone() | ||||||
|                                         ), |                                         ) | ||||||
|                                     ]); |                                     ]; | ||||||
|  |                                     if let Category::Calendar { reoccurrence: Some(_), .. } | ||||||
|  |                                         = task.category() { | ||||||
|  |                                         query_keys.push(QueryKey::SubtasksOfTaskId(task.id())); | ||||||
|  |                                     } | ||||||
|  |                                     query_client.invalidate_queries(&query_keys); | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
|  |                         } | ||||||
|  |                     }, | ||||||
|  |                     div { | ||||||
|  |                         class: "flex flex-col", | ||||||
|  |                         div { | ||||||
|  |                             class: "mt-1 grow font-medium", | ||||||
|  |                             {task.title()} | ||||||
|                         }, |                         }, | ||||||
|                         div { |                         div { | ||||||
|                             class: "flex flex-col", |                             class: "flex flex-row gap-3", | ||||||
|                             div { |                             if let Some(deadline) = task.deadline() { | ||||||
|                                 class: "mt-1 grow font-medium", |                                 div { | ||||||
|                                 {task.title()} |                                     class: "text-sm text-zinc-400", | ||||||
|                             }, |                                     i { | ||||||
|                             div { |                                         class: "fa-solid fa-bomb" | ||||||
|                                 class: "flex flex-row gap-3", |                                     }, | ||||||
|                                 if let Some(deadline) = task.deadline() { |                                     {deadline.format(" %m. %d.").to_string()} | ||||||
|  |                                 } | ||||||
|  |                             } | ||||||
|  |                             if let Category::Calendar { time, .. } = task.category() { | ||||||
|  |                                 if let Some(calendar_time) = time { | ||||||
|                                     div { |                                     div { | ||||||
|                                         class: "text-sm text-zinc-400", |                                         class: "text-sm text-zinc-400", | ||||||
|                                         i { |                                         i { | ||||||
|                                             class: "fa-solid fa-bomb" |                                             class: "fa-solid fa-clock" | ||||||
|                                         }, |                                         }, | ||||||
|                                         {deadline.format(" %m. %d.").to_string()} |                                         {calendar_time.time().format(" %k:%M").to_string()} | ||||||
|                                     } |  | ||||||
|                                 } |  | ||||||
|                                 if let Category::Calendar { time, .. } = task.category() { |  | ||||||
|                                     if let Some(calendar_time) = time { |  | ||||||
|                                         div { |  | ||||||
|                                             class: "text-sm text-zinc-400", |  | ||||||
|                                             i { |  | ||||||
|                                                 class: "fa-solid fa-clock" |  | ||||||
|                                             }, |  | ||||||
|                                             {calendar_time.time().format(" %k:%M").to_string()} |  | ||||||
|                                         } |  | ||||||
|                                     } |                                     } | ||||||
|                                 } |                                 } | ||||||
|                             } |                             } | ||||||
|                         } |                         } | ||||||
|                     } |                     } | ||||||
|                 } |                 } | ||||||
|             })} |             } | ||||||
|         } |         } | ||||||
|     } |     } | ||||||
| } | } | ||||||
|   | |||||||
		Reference in New Issue
	
	Block a user
	
Consider avoiding cloning the entire tasks vector.
Cloning the entire
tasksvector might lead to performance issues, especially if the vector is large. Consider iterating over references if the task objects do not need to be owned within the loop.