feat: list sorting (#42)
This commit is contained in:
		| @@ -41,7 +41,7 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element { | ||||
|                     (false, _, false) => "h-[66px]", | ||||
|                     (false, _, true) => "h-[130px]", | ||||
|                     (true, Route::ProjectsPage, _) => "h-[130px]", | ||||
|                     (true, _, _) => "h-[448px]", | ||||
|                     (true, _, _) => "h-[506px]", | ||||
|                 } | ||||
|             ), | ||||
|             if expanded() { | ||||
|   | ||||
| @@ -13,3 +13,4 @@ pub(crate) mod reoccurrence_input; | ||||
| pub(crate) mod layout; | ||||
| pub(crate) mod navigation_item; | ||||
| pub(crate) mod subtasks_form; | ||||
| pub(crate) mod task_list_item; | ||||
|   | ||||
| @@ -6,11 +6,12 @@ use crate::query::QueryValue; | ||||
| use chrono::{Local, Locale}; | ||||
| use dioxus::prelude::*; | ||||
| use dioxus_query::prelude::QueryResult; | ||||
| use crate::components::task_list_item::TaskListItem; | ||||
|  | ||||
| #[component] | ||||
| pub(crate) fn CategoryTodayPage() -> Element { | ||||
|     let today_date = Local::now().date_naive(); | ||||
|      | ||||
|  | ||||
|     let calendar_tasks_query = use_tasks_with_subtasks_in_category_query(Category::Calendar { | ||||
|         date: today_date, | ||||
|         reoccurrence: None, | ||||
| @@ -26,48 +27,36 @@ pub(crate) fn CategoryTodayPage() -> Element { | ||||
|             class: "pt-4 flex flex-col gap-8", | ||||
|             match long_term_tasks_query_result.value() { | ||||
|                 QueryResult::Ok(QueryValue::TasksWithSubtasks(tasks)) | ||||
|                 | QueryResult::Loading(Some(QueryValue::TasksWithSubtasks(tasks))) => rsx! { | ||||
|                     div { | ||||
|                         class: "flex flex-col gap-4", | ||||
|                 | QueryResult::Loading(Some(QueryValue::TasksWithSubtasks(tasks))) => { | ||||
|                     let mut tasks = tasks.clone(); | ||||
|                     tasks.sort(); | ||||
|                     rsx! { | ||||
|                         div { | ||||
|                             class: "px-8 flex flex-row items-center gap-2 font-bold", | ||||
|                             i { | ||||
|                                 class: "fa-solid fa-water text-xl w-6 text-center" | ||||
|                             class: "flex flex-col gap-4", | ||||
|                             div { | ||||
|                                 class: "px-8 flex flex-row items-center gap-2 font-bold", | ||||
|                                 i { | ||||
|                                     class: "fa-solid fa-water text-xl w-6 text-center" | ||||
|                                 } | ||||
|                                 div { | ||||
|                                     class: "mt-1", | ||||
|                                     "Long-term" | ||||
|                                 } | ||||
|                             } | ||||
|                             div { | ||||
|                                 class: "mt-1", | ||||
|                                 "Long-term" | ||||
|                             } | ||||
|                         } | ||||
|                         div { | ||||
|                             for task in tasks { | ||||
|                                 div { | ||||
|                                     key: "{task.task().id()}", | ||||
|                                     class: format!( | ||||
|                                         "px-8 pt-5 {} flex flex-row gap-4", | ||||
|                                         if task.task().deadline().is_some() { | ||||
|                                             "pb-0.5" | ||||
|                                         } else { | ||||
|                                             "pb-5" | ||||
|                                         } | ||||
|                                     ), | ||||
|                                 for task in tasks { | ||||
|                                     div { | ||||
|                                         class: "flex flex-col", | ||||
|                                         div { | ||||
|                                             class: "mt grow font-medium", | ||||
|                                             {task.task().title()} | ||||
|                                         }, | ||||
|                                         div { | ||||
|                                             class: "flex flex-row gap-3", | ||||
|                                             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()} | ||||
|                                                 } | ||||
|                                         key: "{task.task().id()}", | ||||
|                                         class: format!( | ||||
|                                             "px-8 pt-5 {} flex flex-row gap-4", | ||||
|                                             if task.task().deadline().is_some() { | ||||
|                                                 "pb-0.5" | ||||
|                                             } else { | ||||
|                                                 "pb-5" | ||||
|                                             } | ||||
|                                         ), | ||||
|                                         TaskListItem { | ||||
|                                             task: task.clone() | ||||
|                                         } | ||||
|                                     } | ||||
|                                 } | ||||
|   | ||||
| @@ -12,20 +12,24 @@ pub(crate) fn ProjectsPage() -> Element { | ||||
|     rsx! { | ||||
|         match projects_query.result().value() { | ||||
|             QueryResult::Ok(QueryValue::Projects(projects)) | ||||
|             | QueryResult::Loading(Some(QueryValue::Projects(projects))) => rsx! { | ||||
|                 div { | ||||
|                     class: "flex flex-col", | ||||
|                     for project in projects.clone() { | ||||
|                         div { | ||||
|                             key: "{project.id()}", | ||||
|                             class: format!( | ||||
|                                 "px-8 py-4 select-none {}", | ||||
|                                 if project_being_edited().is_some_and(|p| p.id() == project.id()) { | ||||
|                                     "bg-zinc-700" | ||||
|                                 } else { "" } | ||||
|                             ), | ||||
|                             onclick: move |_| project_being_edited.set(Some(project.clone())), | ||||
|                             {project.title()} | ||||
|             | QueryResult::Loading(Some(QueryValue::Projects(projects))) => { | ||||
|                 let mut projects = projects.clone(); | ||||
|                 projects.sort(); | ||||
|                 rsx! { | ||||
|                     div { | ||||
|                         class: "flex flex-col", | ||||
|                         for project in projects { | ||||
|                             div { | ||||
|                                 key: "{project.id()}", | ||||
|                                 class: format!( | ||||
|                                     "px-8 py-4 select-none {}", | ||||
|                                     if project_being_edited().is_some_and(|p| p.id() == project.id()) { | ||||
|                                         "bg-zinc-700" | ||||
|                                     } else { "" } | ||||
|                                 ), | ||||
|                                 onclick: move |_| project_being_edited.set(Some(project.clone())), | ||||
|                                 {project.title()} | ||||
|                             } | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|   | ||||
| @@ -64,8 +64,10 @@ pub(crate) fn SubtasksForm(task: Task) -> Element { | ||||
|         match subtasks_query.result().value() { | ||||
|             QueryResult::Ok(QueryValue::Subtasks(subtasks)) | ||||
|             | QueryResult::Loading(Some(QueryValue::Subtasks(subtasks))) => { | ||||
|                 let mut subtasks = subtasks.clone(); | ||||
|                 subtasks.sort(); | ||||
|                 rsx! { | ||||
|                     for subtask in subtasks.clone() { | ||||
|                     for subtask in subtasks { | ||||
|                         div { | ||||
|                             key: "{subtask.id()}", | ||||
|                             class: "flex flex-row items-center gap-3", | ||||
|   | ||||
| @@ -4,6 +4,7 @@ use dioxus::core_macro::rsx; | ||||
| use dioxus::dioxus_core::Element; | ||||
| use dioxus::prelude::*; | ||||
| use dioxus_query::prelude::use_query_client; | ||||
| use crate::components::task_list_item::TaskListItem; | ||||
| use crate::query::{QueryErrors, QueryKey, QueryValue}; | ||||
| use crate::server::tasks::complete_task; | ||||
|  | ||||
| @@ -12,6 +13,8 @@ pub(crate) fn TaskList(tasks: Vec<TaskWithSubtasks>, class: Option<&'static str> | ||||
|     let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>(); | ||||
|     let mut task_being_edited = use_context::<Signal<Option<Task>>>(); | ||||
|  | ||||
|     tasks.sort(); | ||||
|      | ||||
|     rsx! { | ||||
|         div { | ||||
|             class: format!("flex flex-col {}", class.unwrap_or("")), | ||||
| @@ -74,50 +77,8 @@ pub(crate) fn TaskList(tasks: Vec<TaskWithSubtasks>, class: Option<&'static str> | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|                     div { | ||||
|                         class: "flex flex-col", | ||||
|                         div { | ||||
|                             class: "mt-1 grow font-medium", | ||||
|                             {task.task().title()} | ||||
|                         }, | ||||
|                         div { | ||||
|                             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()} | ||||
|                                 } | ||||
|                             } | ||||
|                             if let Category::Calendar { time, .. } = task.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()} | ||||
|                                     } | ||||
|                                 } | ||||
|                             } | ||||
|                             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() | ||||
|                                     )} | ||||
|                                 } | ||||
|                             } | ||||
|                         } | ||||
|                     TaskListItem { | ||||
|                         task: task.clone() | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|   | ||||
							
								
								
									
										61
									
								
								src/components/task_list_item.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										61
									
								
								src/components/task_list_item.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,61 @@ | ||||
| use chrono::{Datelike, Local}; | ||||
| use crate::models::category::Category; | ||||
| use crate::models::task::TaskWithSubtasks; | ||||
| use dioxus::core_macro::rsx; | ||||
| use dioxus::dioxus_core::Element; | ||||
| use dioxus::prelude::*; | ||||
|  | ||||
| #[component] | ||||
| pub(crate) fn TaskListItem(task: TaskWithSubtasks) -> Element { | ||||
|     rsx! { | ||||
|         div { | ||||
|             class: "flex flex-col", | ||||
|             div { | ||||
|                 class: "mt-1 grow font-medium", | ||||
|                 {task.task().title()} | ||||
|             }, | ||||
|             div { | ||||
|                 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(if deadline.year() == Local::now().year() { | ||||
|                             " %m. %-d." | ||||
|                         } else { | ||||
|                             " %m. %-d. %Y" | ||||
|                         }).to_string()} | ||||
|                     } | ||||
|                 } | ||||
|                 if let Category::Calendar { time, .. } = task.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()} | ||||
|                         } | ||||
|                     } | ||||
|                 } | ||||
|                 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() | ||||
|                         )} | ||||
|                     } | ||||
|                 } | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -53,6 +53,13 @@ impl From<diesel::result::Error> for SubtaskError { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl From<ErrorVec<Error>> for ErrorVec<SubtaskError> { | ||||
|     fn from(error_vec: ErrorVec<Error>) -> Self { | ||||
|         Vec::from(error_vec).into_iter() | ||||
|             .map(SubtaskError::Error).collect::<Vec<SubtaskError>>().into() | ||||
|     } | ||||
| } | ||||
|  | ||||
| // Has to be implemented for Dioxus server functions. | ||||
| impl Display for SubtaskError { | ||||
|     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | ||||
|   | ||||
| @@ -5,6 +5,7 @@ mod route; | ||||
| mod schema; | ||||
| mod server; | ||||
| mod query; | ||||
| mod utils; | ||||
|  | ||||
| use components::app::App; | ||||
| use dioxus::prelude::*; | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| use std::cmp::Ordering; | ||||
| use chrono::NaiveDateTime; | ||||
| use crate::schema::projects; | ||||
| use diesel::prelude::*; | ||||
| @@ -35,6 +36,20 @@ impl Project { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Eq for Project {} | ||||
|  | ||||
| impl PartialOrd<Self> for Project { | ||||
|     fn partial_cmp(&self, other: &Self) -> Option<Ordering> { | ||||
|         Some(self.cmp(other)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Ord for Project { | ||||
|     fn cmp(&self, other: &Self) -> Ordering { | ||||
|         self.title().cmp(other.title()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Insertable, Serialize, Deserialize, Validate, Clone, Debug)] | ||||
| #[diesel(table_name = projects)] | ||||
| pub struct NewProject { | ||||
|   | ||||
| @@ -1,3 +1,4 @@ | ||||
| use std::cmp::Ordering; | ||||
| use crate::models::task::Task; | ||||
| use crate::schema::subtasks; | ||||
| use chrono::NaiveDateTime; | ||||
| @@ -48,6 +49,21 @@ impl Subtask { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Eq for Subtask {} | ||||
|  | ||||
| impl PartialOrd<Self> for Subtask { | ||||
|     fn partial_cmp(&self, other: &Self) -> Option<Ordering> { | ||||
|         Some(self.cmp(other)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Ord for Subtask { | ||||
|     fn cmp(&self, other: &Self) -> Ordering { | ||||
|         self.is_completed().cmp(&other.is_completed()) | ||||
|             .then(self.created_at().cmp(&other.created_at())) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Insertable, Serialize, Deserialize, Validate, Clone, Debug)] | ||||
| #[diesel(table_name = subtasks)] | ||||
| pub struct NewSubtask { | ||||
|   | ||||
| @@ -1,10 +1,12 @@ | ||||
| use chrono::NaiveDateTime; | ||||
| use crate::models::category::Category; | ||||
| use crate::models::subtask::Subtask; | ||||
| use crate::schema::tasks; | ||||
| use crate::utils::reverse_ord_option::ReverseOrdOption; | ||||
| use chrono::NaiveDateTime; | ||||
| use diesel::prelude::*; | ||||
| use serde::{Deserialize, Serialize}; | ||||
| use std::cmp::Ordering; | ||||
| use validator::Validate; | ||||
| use crate::models::subtask::Subtask; | ||||
|  | ||||
| const TITLE_LENGTH_MIN: u64 = 1; | ||||
| const TITLE_LENGTH_MAX: u64 = 255; | ||||
| @@ -52,6 +54,40 @@ impl Task { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Eq for Task {} | ||||
|  | ||||
| impl PartialOrd<Self> for Task { | ||||
|     fn partial_cmp(&self, other: &Self) -> Option<Ordering> { | ||||
|         Some(self.cmp(other)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Ord for Task { | ||||
|     fn cmp(&self, other: &Self) -> Ordering { | ||||
|         match (&self.category, &other.category) { | ||||
|             (Category::Inbox, Category::Inbox) => self.created_at.cmp(&other.created_at), | ||||
|             ( | ||||
|                 Category::Calendar { date: self_date, time: self_time, .. }, | ||||
|                 Category::Calendar { date: other_date, time: other_time, .. } | ||||
|             ) => self_date.cmp(other_date) | ||||
|                 .then(ReverseOrdOption::from( | ||||
|                     &self_time.as_ref().map(|calendar_time| calendar_time.time()) | ||||
|                 ).cmp(&ReverseOrdOption::from( | ||||
|                         &other_time.as_ref().map(|calendar_time| calendar_time.time()) | ||||
|                 ))) | ||||
|                 .then(ReverseOrdOption::from(&self.deadline()).cmp( | ||||
|                     &ReverseOrdOption::from(&other.deadline()) | ||||
|                 )) | ||||
|                 .then(self.created_at.cmp(&other.created_at)), | ||||
|             (Category::Done, Category::Done) | (Category::Trash, Category::Trash) | ||||
|             => self.updated_at.cmp(&other.updated_at).reverse(), | ||||
|             (_, _) => ReverseOrdOption::from(&self.deadline()).cmp( | ||||
|                 &ReverseOrdOption::from(&other.deadline()) | ||||
|             ).then(self.created_at.cmp(&other.created_at)), | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Serialize, Deserialize, PartialEq, Clone, Debug)] | ||||
| pub struct TaskWithSubtasks { | ||||
|     task: Task, | ||||
| @@ -72,6 +108,20 @@ impl TaskWithSubtasks { | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Eq for TaskWithSubtasks {} | ||||
|  | ||||
| impl PartialOrd<Self> for TaskWithSubtasks { | ||||
|     fn partial_cmp(&self, other: &Self) -> Option<Ordering> { | ||||
|         Some(self.cmp(other)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl Ord for TaskWithSubtasks { | ||||
|     fn cmp(&self, other: &Self) -> Ordering { | ||||
|         self.task().cmp(other.task()) | ||||
|     } | ||||
| } | ||||
|  | ||||
| #[derive(Insertable, Serialize, Deserialize, Validate, Clone, Debug)] | ||||
| #[diesel(table_name = tasks)] | ||||
| pub struct NewTask { | ||||
|   | ||||
| @@ -6,6 +6,7 @@ use crate::server::database_connection::establish_database_connection; | ||||
| use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; | ||||
| use dioxus::prelude::*; | ||||
| use validator::Validate; | ||||
| use crate::server::tasks::trigger_task_updated_at; | ||||
|  | ||||
| #[server] | ||||
| pub(crate) async fn create_subtask(new_subtask: NewSubtask) | ||||
| @@ -25,6 +26,9 @@ pub(crate) async fn create_subtask(new_subtask: NewSubtask) | ||||
|         .returning(Subtask::as_returning()) | ||||
|         .get_result(&mut connection) | ||||
|         .map_err::<ErrorVec<SubtaskError>, _>(|error| vec![error.into()].into())?; | ||||
|      | ||||
|     trigger_task_updated_at(new_subtask.task_id).await | ||||
|         .map_err::<ErrorVec<SubtaskError>, _>(|error_vec| error_vec.into())?; | ||||
|  | ||||
|     Ok(created_subtask) | ||||
| } | ||||
| @@ -35,17 +39,13 @@ pub(crate) async fn get_subtasks_of_task(filtered_task_id: i32) | ||||
|     use crate::schema::subtasks::dsl::*; | ||||
|  | ||||
|     let mut connection = establish_database_connection() | ||||
|         .map_err::<ErrorVec<Error>, _>( | ||||
|             |_| vec![Error::ServerInternal].into() | ||||
|         )?; | ||||
|         .map_err::<ErrorVec<Error>, _>(|_| vec![Error::ServerInternal].into())?; | ||||
|  | ||||
|     let results = subtasks | ||||
|         .select(Subtask::as_select()) | ||||
|         .filter(task_id.eq(filtered_task_id)) | ||||
|         .load::<Subtask>(&mut connection) | ||||
|         .map_err::<ErrorVec<Error>, _>( | ||||
|             |_| vec![Error::ServerInternal].into() | ||||
|         )?; | ||||
|         .map_err::<ErrorVec<Error>, _>(|_| vec![Error::ServerInternal].into())?; | ||||
|  | ||||
|     Ok(results) | ||||
| } | ||||
| @@ -73,27 +73,28 @@ pub(crate) async fn edit_subtask(subtask_id: i32, new_subtask: NewSubtask) | ||||
|         .get_result(&mut connection) | ||||
|         .map_err::<ErrorVec<SubtaskError>, _>(|error| vec![error.into()].into())?; | ||||
|  | ||||
|     trigger_task_updated_at(new_subtask.task_id).await | ||||
|         .map_err::<ErrorVec<SubtaskError>, _>(|error_vec| error_vec.into())?; | ||||
|      | ||||
|     Ok(updated_task) | ||||
| } | ||||
|  | ||||
| #[server] | ||||
| pub(crate) async fn restore_subtasks_of_task(filtered_task_id: i32) -> Result< | ||||
|     Vec<Subtask>, | ||||
|     ServerFnError<ErrorVec<SubtaskError>> | ||||
|     ServerFnError<ErrorVec<Error>> | ||||
| > { | ||||
|     use crate::schema::subtasks::dsl::*; | ||||
|  | ||||
|     let mut connection = establish_database_connection() | ||||
|         .map_err::<ErrorVec<SubtaskError>, _>( | ||||
|             |_| vec![SubtaskError::Error(Error::ServerInternal)].into() | ||||
|         )?; | ||||
|         .map_err::<ErrorVec<Error>, _>(|_| vec![Error::ServerInternal].into())?; | ||||
|  | ||||
|     let updated_subtasks = diesel::update(subtasks) | ||||
|         .filter(task_id.eq(filtered_task_id)) | ||||
|         .set(is_completed.eq(false)) | ||||
|         .returning(Subtask::as_returning()) | ||||
|         .get_results(&mut connection) | ||||
|         .map_err::<ErrorVec<SubtaskError>, _>(|error| vec![error.into()].into())?; | ||||
|         .map_err::<ErrorVec<Error>, _>(|error| vec![error.into()].into())?; | ||||
|  | ||||
|     Ok(updated_subtasks) | ||||
| } | ||||
| @@ -108,8 +109,12 @@ pub(crate) async fn delete_subtask(subtask_id: i32) | ||||
|     let mut connection = establish_database_connection() | ||||
|         .map_err::<ErrorVec<Error>, _>(|_| vec![Error::ServerInternal].into())?; | ||||
|  | ||||
|     diesel::delete(subtasks.filter(id.eq(subtask_id))).execute(&mut connection) | ||||
|     let deleted_subtask = diesel::delete(subtasks.filter(id.eq(subtask_id))) | ||||
|         .returning(Subtask::as_returning()) | ||||
|         .get_result(&mut connection) | ||||
|         .map_err::<ErrorVec<Error>, _>(|error| vec![error.into()].into())?; | ||||
|  | ||||
|     trigger_task_updated_at(deleted_subtask.task_id()).await?; | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|   | ||||
| @@ -1,4 +1,4 @@ | ||||
| use chrono::{Datelike, Days, Months, NaiveDate}; | ||||
| use chrono::{Datelike, Days, Local, Months, NaiveDate}; | ||||
| use crate::errors::error::Error; | ||||
| use crate::errors::error_vec::ErrorVec; | ||||
| use crate::models::task::{NewTask, Task, TaskWithSubtasks}; | ||||
| @@ -80,7 +80,7 @@ pub(crate) async fn get_tasks_with_subtasks_in_category(filtered_category: Categ | ||||
|     ServerFnError<ErrorVec<Error>> | ||||
| > { | ||||
|     use crate::schema::tasks; | ||||
|      | ||||
|  | ||||
|     let mut connection = establish_database_connection() | ||||
|         .map_err::<ErrorVec<Error>, _>(|_| vec![Error::ServerInternal].into())?; | ||||
|  | ||||
| @@ -160,8 +160,7 @@ pub(crate) async fn complete_task(task_id: i32) -> Result<Task, ServerFnError<Er | ||||
|                 ).unwrap() | ||||
|             } | ||||
|         } | ||||
|         restore_subtasks_of_task(task_id).await | ||||
|             .map_err::<ErrorVec<Error>, _>(|_| vec![Error::ServerInternal].into())?; | ||||
|         restore_subtasks_of_task(task_id).await?; | ||||
|     } else { | ||||
|         new_task.category = Category::Done; | ||||
|     } | ||||
| @@ -187,3 +186,21 @@ pub(crate) async fn delete_task(task_id: i32) | ||||
|  | ||||
|     Ok(()) | ||||
| } | ||||
|  | ||||
| pub(crate) async fn trigger_task_updated_at(task_id: i32) -> Result<Task, ErrorVec<Error>> { | ||||
|     use crate::schema::tasks::dsl::*; | ||||
|      | ||||
|     let mut connection = establish_database_connection() | ||||
|         .map_err::<ErrorVec<Error>, _>( | ||||
|             |_| vec![Error::ServerInternal].into() | ||||
|         )?; | ||||
|  | ||||
|     let updated_task = diesel::update(tasks) | ||||
|         .filter(id.eq(task_id)) | ||||
|         .set(updated_at.eq(Local::now().naive_local())) | ||||
|         .returning(Task::as_returning()) | ||||
|         .get_result(&mut connection) | ||||
|         .map_err::<ErrorVec<Error>, _>(|error| vec![error.into()].into())?; | ||||
|  | ||||
|     Ok(updated_task) | ||||
| } | ||||
|   | ||||
							
								
								
									
										1
									
								
								src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										1
									
								
								src/utils/mod.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1 @@ | ||||
| pub(crate) mod reverse_ord_option; | ||||
							
								
								
									
										31
									
								
								src/utils/reverse_ord_option.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										31
									
								
								src/utils/reverse_ord_option.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,31 @@ | ||||
| use std::cmp::Ordering; | ||||
|  | ||||
| /* The default ordering of `Option`s is `None` being less than `Some`. The purpose of this struct is | ||||
|    to reverse that. */ | ||||
| #[derive(PartialEq)] | ||||
| pub(crate) struct ReverseOrdOption<'a, T>(&'a Option<T>); | ||||
|  | ||||
| impl<'a, T: Ord> Eq for ReverseOrdOption<'a, T> {} | ||||
|  | ||||
| impl<'a, T: Ord> PartialOrd<Self> for ReverseOrdOption<'a, T> { | ||||
|     fn partial_cmp(&self, other: &Self) -> Option<Ordering> { | ||||
|         Some(self.cmp(other)) | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<'a, T: Ord> Ord for ReverseOrdOption<'a, T> { | ||||
|     fn cmp(&self, other: &Self) -> Ordering { | ||||
|         match (self.0.as_ref(), other.0.as_ref()) { | ||||
|             (None, None) => Ordering::Equal, | ||||
|             (None, Some(_)) => Ordering::Greater, | ||||
|             (Some(_), None) => Ordering::Less, | ||||
|             (Some(self_time), Some(other_time)) => self_time.cmp(other_time) | ||||
|         } | ||||
|     } | ||||
| } | ||||
|  | ||||
| impl<'a, T> From<&'a Option<T>> for ReverseOrdOption<'a, T> { | ||||
|     fn from(value: &'a Option<T>) -> Self { | ||||
|         ReverseOrdOption(value) | ||||
|     } | ||||
| } | ||||
		Reference in New Issue
	
	Block a user