feat: ability to manage subtasks #40
| @@ -12,3 +12,4 @@ pub(crate) mod category_input; | ||||
| pub(crate) mod reoccurrence_input; | ||||
| pub(crate) mod layout; | ||||
| 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::prelude::*; | ||||
| use dioxus_query::prelude::use_query_client; | ||||
| use crate::components::subtasks_form::SubtasksForm; | ||||
|  | ||||
| const REMINDER_OFFSETS: [Option<Duration>; 17] = [ | ||||
|     None, | ||||
| @@ -79,7 +80,11 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<() | ||||
|     let task_for_submit = task.clone(); | ||||
|  | ||||
|     rsx! { | ||||
|         div { | ||||
|             class: "p-4 flex flex-col gap-4", | ||||
|             form { | ||||
|                 class: "flex flex-col gap-4", | ||||
|                 id: "form_task", | ||||
|                 onsubmit: move |event| { | ||||
|                     let task = task_for_submit.clone(); | ||||
|                     async move { | ||||
| @@ -99,7 +104,8 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<() | ||||
|                                             event.values().get("category_calendar_date").unwrap() | ||||
|                                             .as_value().parse().unwrap(), | ||||
|                                             reoccurrence_interval, | ||||
|                                         event.values().get("category_calendar_reoccurrence_length") | ||||
|                                             event.values() | ||||
|                                             .get("category_calendar_reoccurrence_length") | ||||
|                                             .unwrap().as_value().parse().unwrap() | ||||
|                                         ) | ||||
|                                     ), | ||||
| @@ -109,8 +115,8 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<() | ||||
|                                             time, | ||||
|                                             REMINDER_OFFSETS[ | ||||
|                                                 event.values() | ||||
|                                             .get("category_calendar_reminder_offset_index").unwrap() | ||||
|                                             .as_value().parse::<usize>().unwrap() | ||||
|                                                 .get("category_calendar_reminder_offset_index") | ||||
|                                                 .unwrap().as_value().parse::<usize>().unwrap() | ||||
|                                             ] | ||||
|                                         ) | ||||
|                                     ) | ||||
| @@ -132,7 +138,6 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<() | ||||
|                         on_successful_submit.call(()); | ||||
|                     } | ||||
|                 }, | ||||
|             class: "p-4 flex flex-col gap-4", | ||||
|                 div { | ||||
|                     class: "flex flex-row items-center gap-3", | ||||
|                     label { | ||||
| @@ -286,9 +291,11 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<() | ||||
|                                     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()), | ||||
|                                     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" | ||||
|                                 } | ||||
| @@ -334,7 +341,13 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<() | ||||
|                         } | ||||
|                     }, | ||||
|                     _ => None | ||||
|                 } | ||||
|             }, | ||||
|             if let Some(task) = task.as_ref() { | ||||
|                 SubtasksForm { | ||||
|                     task_id: task.id() | ||||
|                 } | ||||
|             } | ||||
|             div { | ||||
|                 class: "flex flex-row justify-between mt-auto", | ||||
|                 button { | ||||
| @@ -370,6 +383,7 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<() | ||||
|                     } | ||||
|                 } | ||||
|                 button { | ||||
|                     form: "form_task", | ||||
|                     r#type: "submit", | ||||
|                     class: "py-2 px-4 bg-zinc-300/50 rounded-lg", | ||||
|                     i { | ||||
|   | ||||
| @@ -15,9 +15,7 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element | ||||
|     rsx! { | ||||
|         div { | ||||
|             class: format!("flex flex-col {}", class.unwrap_or("")), | ||||
|             {tasks.iter().cloned().map(|task| { | ||||
|                 let task_clone = task.clone(); | ||||
|                 rsx! { | ||||
|             for task in tasks.clone() { | ||||
|                 div { | ||||
|                     key: "{task.id()}", | ||||
|                     class: format!( | ||||
| @@ -37,28 +35,39 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element | ||||
|                             "bg-zinc-700" | ||||
|                         } else { "" } | ||||
|                     ), | ||||
|                         onclick: move |_| task_being_edited.set(Some(task.clone())), | ||||
|                     onclick: { | ||||
|                         let task = task.clone(); | ||||
|                         move |_| task_being_edited.set(Some(task.clone())) | ||||
|                     }, | ||||
|                     i { | ||||
|                         class: format!( | ||||
|                             "{} text-3xl text-zinc-500", | ||||
|                                 if *(task_clone.category()) == Category::Done { | ||||
|                             if *(task.category()) == Category::Done { | ||||
|                                 "fa solid fa-square-check" | ||||
|                             } else { | ||||
|                                 "fa-regular fa-square" | ||||
|                             } | ||||
|                         ), | ||||
|                             onclick: move |event| { | ||||
|                         onclick: { | ||||
|                             let task = task.clone(); | ||||
|                             move |event| { | ||||
|                                 // To prevent editing the task. | ||||
|                                 event.stop_propagation(); | ||||
|                                 let task = task_clone.clone(); | ||||
|                                 let task = task.clone(); | ||||
|                                 async move { | ||||
|                                     let completed_task = complete_task(task.id()).await; | ||||
|                                     query_client.invalidate_queries(&[ | ||||
|                                     let mut query_keys = vec![ | ||||
|                                         QueryKey::Tasks, | ||||
|                                         QueryKey::TasksInCategory( | ||||
|                                             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); | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
| @@ -94,7 +103,6 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|             })} | ||||
|         } | ||||
|     } | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user