feat: subtask count in task lists #41

Merged
matous-volf merged 4 commits from feat/subtask-count into main 2024-09-09 06:50:29 +00:00
12 changed files with 298 additions and 167 deletions

View File

@ -6,14 +6,14 @@ use dioxus::prelude::*;
use dioxus_query::prelude::QueryResult;
use crate::components::task_list::TaskList;
use crate::query::QueryValue;
use crate::query::tasks::use_tasks_in_category_query;
use crate::models::task::Task;
use crate::query::tasks::use_tasks_with_subtasks_in_category_query;
use crate::models::task::{TaskWithSubtasks};
const CALENDAR_LENGTH_DAYS: usize = 366 * 3;
#[component]
pub(crate) fn CategoryCalendarPage() -> Element {
let tasks = use_tasks_in_category_query(Category::Calendar {
let tasks = use_tasks_with_subtasks_in_category_query(Category::Calendar {
date: Local::now().date_naive(),
reoccurrence: None,
time: None,
@ -22,8 +22,8 @@ pub(crate) fn CategoryCalendarPage() -> Element {
rsx! {
match tasks_query_result.value() {
QueryResult::Ok(QueryValue::Tasks(tasks))
| QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => {
QueryResult::Ok(QueryValue::TasksWithSubtasks(tasks))
| QueryResult::Loading(Some(QueryValue::TasksWithSubtasks(tasks))) => {
let today_date = Local::now().date_naive();
rsx! {
@ -52,12 +52,13 @@ pub(crate) fn CategoryCalendarPage() -> Element {
}
TaskList {
tasks: tasks.iter().filter(|task| {
if let Category::Calendar { date, .. } = task.category() {
if let Category::Calendar { date, .. }
= task.task().category() {
*date == date_current
} else {
panic!("Unexpected category.");
}
}).cloned().collect::<Vec<Task>>()
}).cloned().collect::<Vec<TaskWithSubtasks>>()
}
}
}

View File

@ -1,6 +1,6 @@
use crate::components::task_list::TaskList;
use crate::models::category::Category;
use crate::query::tasks::use_tasks_in_category_query;
use crate::query::tasks::use_tasks_with_subtasks_in_category_query;
use crate::query::QueryValue;
coderabbitai[bot] commented 2024-09-08 20:27:16 +00:00 (Migrated from github.com)
Review

Approved: Updated query function and result handling in CategoryPage.

The replacement of the query function with use_tasks_with_subtasks_in_category_query and the updated handling of query results are well-aligned with the PR's objectives. The changes enhance the component's ability to present a more comprehensive view of tasks. However, consider implementing the loading indicator as mentioned in the TODO comment to improve user experience during data loading.

Would you like assistance in implementing the loading indicator? If so, I can help draft the necessary code or open a GitHub issue to track this enhancement.

Also applies to: 12-17

**Approved: Updated query function and result handling in `CategoryPage`.** The replacement of the query function with `use_tasks_with_subtasks_in_category_query` and the updated handling of query results are well-aligned with the PR's objectives. The changes enhance the component's ability to present a more comprehensive view of tasks. However, consider implementing the loading indicator as mentioned in the TODO comment to improve user experience during data loading. Would you like assistance in implementing the loading indicator? If so, I can help draft the necessary code or open a GitHub issue to track this enhancement. Also applies to: 12-17 <!-- This is an auto-generated comment by CodeRabbit -->
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
@ -9,12 +9,12 @@ use dioxus_query::prelude::QueryResult;
#[component]
pub(crate) fn CategoryPage(category: Category) -> Element {
let tasks_query = use_tasks_in_category_query(category);
let tasks_query = use_tasks_with_subtasks_in_category_query(category);
let tasks_query_result = tasks_query.result();
match tasks_query_result.value() {
QueryResult::Ok(QueryValue::Tasks(tasks))
| QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => rsx! {
QueryResult::Ok(QueryValue::TasksWithSubtasks(tasks))
| QueryResult::Loading(Some(QueryValue::TasksWithSubtasks(tasks))) => rsx! {
TaskList {
tasks: tasks.clone(),
class: "pb-36"

View File

@ -1,7 +1,7 @@
use crate::components::task_list::TaskList;
use crate::models::category::Category;
use crate::models::task::Task;
use crate::query::tasks::use_tasks_in_category_query;
use crate::models::task::TaskWithSubtasks;
use crate::query::tasks::{use_tasks_with_subtasks_in_category_query};
use crate::query::QueryValue;
use chrono::{Local, Locale};
use dioxus::prelude::*;
@ -11,22 +11,22 @@ use dioxus_query::prelude::QueryResult;
pub(crate) fn CategoryTodayPage() -> Element {
let today_date = Local::now().date_naive();
let calendar_tasks_query = use_tasks_in_category_query(Category::Calendar {
let calendar_tasks_query = use_tasks_with_subtasks_in_category_query(Category::Calendar {
date: today_date,
reoccurrence: None,
time: None,
});
let calendar_tasks_query_result = calendar_tasks_query.result();
let long_term_tasks_query = use_tasks_in_category_query(Category::LongTerm);
let long_term_tasks_query = use_tasks_with_subtasks_in_category_query(Category::LongTerm);
let long_term_tasks_query_result = long_term_tasks_query.result();
rsx! {
div {
coderabbitai[bot] commented 2024-09-08 20:27:16 +00:00 (Migrated from github.com)
Review

Refactor: Updated query functions for task fetching.

The component now uses use_tasks_with_subtasks_in_category_query for fetching tasks, which is aligned with the new model requirements. This change is crucial for supporting the enhanced task management functionality.

Also applies to: 21-21

**Refactor: Updated query functions for task fetching.** The component now uses `use_tasks_with_subtasks_in_category_query` for fetching tasks, which is aligned with the new model requirements. This change is crucial for supporting the enhanced task management functionality. Also applies to: 21-21 <!-- This is an auto-generated comment by CodeRabbit -->
class: "pt-4 flex flex-col gap-8",
match long_term_tasks_query_result.value() {
QueryResult::Ok(QueryValue::Tasks(tasks))
| QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => rsx! {
QueryResult::Ok(QueryValue::TasksWithSubtasks(tasks))
| QueryResult::Loading(Some(QueryValue::TasksWithSubtasks(tasks))) => rsx! {
div {
class: "flex flex-col gap-4",
div {
@ -42,10 +42,10 @@ pub(crate) fn CategoryTodayPage() -> Element {
div {
for task in tasks {
div {
key: "{task.id()}",
key: "{task.task().id()}",
class: format!(
"px-8 pt-5 {} flex flex-row gap-4",
if task.deadline().is_some() {
if task.task().deadline().is_some() {
"pb-0.5"
} else {
"pb-5"
@ -55,11 +55,11 @@ pub(crate) fn CategoryTodayPage() -> Element {
class: "flex flex-col",
div {
class: "mt grow font-medium",
{task.title()}
{task.task().title()}
},
div {
class: "flex flex-row gap-3",
if let Some(deadline) = task.deadline() {
if let Some(deadline) = task.task().deadline() {
div {
class: "text-sm text-zinc-400",
i {
@ -86,22 +86,22 @@ pub(crate) fn CategoryTodayPage() -> Element {
value => panic!("Unexpected query result: {value:?}")
}
coderabbitai[bot] commented 2024-09-08 20:27:16 +00:00 (Migrated from github.com)
Review

Refactor: Enhanced rendering logic for tasks with subtasks.

The rendering logic within the rsx! macro has been updated to accommodate tasks with subtasks. This includes changes in how tasks are accessed (task.task()) and displayed, particularly handling task properties like id, title, and deadline. These changes are crucial for correctly displaying tasks with their subtasks and ensuring the UI is consistent with the new data model.

Also applies to: 58-62, 92-104, 99-104

**Refactor: Enhanced rendering logic for tasks with subtasks.** The rendering logic within the `rsx!` macro has been updated to accommodate tasks with subtasks. This includes changes in how tasks are accessed (`task.task()`) and displayed, particularly handling task properties like `id`, `title`, and `deadline`. These changes are crucial for correctly displaying tasks with their subtasks and ensuring the UI is consistent with the new data model. Also applies to: 58-62, 92-104, 99-104 <!-- This is an auto-generated comment by CodeRabbit -->
match calendar_tasks_query_result.value() {
QueryResult::Ok(QueryValue::Tasks(tasks))
| QueryResult::Loading(Some(QueryValue::Tasks(tasks))) => {
QueryResult::Ok(QueryValue::TasksWithSubtasks(tasks))
| QueryResult::Loading(Some(QueryValue::TasksWithSubtasks(tasks))) => {
let today_tasks = tasks.iter().filter(|task| {
if let Category::Calendar { date, .. } = task.category() {
if let Category::Calendar { date, .. } = task.task().category() {
*date == today_date
} else {
panic!("Unexpected category.");
}
}).cloned().collect::<Vec<Task>>();
}).cloned().collect::<Vec<TaskWithSubtasks>>();
let overdue_tasks = tasks.iter().filter(|task| {
if let Category::Calendar { date, .. } = task.category() {
if let Category::Calendar { date, .. } = task.task().category() {
*date < today_date
} else {
panic!("Unexpected category.");
}
}).cloned().collect::<Vec<Task>>();
}).cloned().collect::<Vec<TaskWithSubtasks>>();
rsx! {
if !overdue_tasks.is_empty() {

View File

@ -1,4 +1,5 @@
use crate::models::subtask::NewSubtask;
use crate::models::task::Task;
use crate::query::subtasks::use_subtasks_of_task_query;
use crate::query::{QueryErrors, QueryKey, QueryValue};
use crate::server::subtasks::{create_subtask, delete_subtask, edit_subtask};
@ -8,151 +9,176 @@ use dioxus::prelude::*;
use dioxus_query::prelude::{use_query_client, QueryResult};
#[component]
pub(crate) fn SubtasksForm(task_id: i32) -> Element {
pub(crate) fn SubtasksForm(task: Task) -> Element {
let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>();
let subtasks_query = use_subtasks_of_task_query(task_id);
let subtasks_query = use_subtasks_of_task_query(task.id());
let mut new_title = use_signal(String::new);
rsx! {
form {
class: "flex flex-row items-center gap-3",
onsubmit: move |event| async move {
form {
coderabbitai[bot] commented 2024-09-08 20:27:16 +00:00 (Migrated from github.com)
Review

Refactor: Updated function signature and form submission handler.

The function signature of SubtasksForm has been updated to accept a Task object, enhancing the component's integration with the task management system. The form submission handler now utilizes task.id() and task.category(), which simplifies the logic and reduces the risk of errors. Additionally, the query invalidation logic has been updated to include the task's category, ensuring more precise cache management.

Also applies to: 14-15, 21-33

**Refactor: Updated function signature and form submission handler.** The function signature of `SubtasksForm` has been updated to accept a `Task` object, enhancing the component's integration with the task management system. The form submission handler now utilizes `task.id()` and `task.category()`, which simplifies the logic and reduces the risk of errors. Additionally, the query invalidation logic has been updated to include the task's category, ensuring more precise cache management. Also applies to: 14-15, 21-33 <!-- This is an auto-generated comment by CodeRabbit -->
class: "flex flex-row items-center gap-3",
onsubmit: move |event| {
let task = task.clone();
async move {
let new_subtask = NewSubtask::new(
task_id,
task.id(),
event.values().get("title").unwrap().as_value(),
false
);
let _ = create_subtask(new_subtask).await;
query_client.invalidate_queries(&[QueryKey::SubtasksOfTaskId(task_id)]);
query_client.invalidate_queries(&[
QueryKey::SubtasksOfTaskId(task.id()),
QueryKey::TasksWithSubtasksInCategory(task.category().clone()),
]);
new_title.set(String::new());
},
label {
r#for: "input_new_title",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-list-check text-zinc-400/50"
}
}
div {
class: "grow grid grid-cols-6 gap-2",
input {
name: "title",
required: true,
value: new_title,
r#type: "text",
class: "grow py-2 px-3 col-span-5 bg-zinc-800/50 rounded-lg",
id: "input_new_title",
onchange: move |event| new_title.set(event.value())
}
button {
r#type: "submit",
class: "py-2 col-span-1 bg-zinc-800/50 rounded-lg",
i {
class: "fa-solid fa-plus"
}
},
label {
r#for: "input_new_title",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-list-check text-zinc-400/50"
}
}
div {
class: "grow grid grid-cols-6 gap-2",
input {
name: "title",
required: true,
value: new_title,
r#type: "text",
class: "grow py-2 px-3 col-span-5 bg-zinc-800/50 rounded-lg",
id: "input_new_title",
onchange: move |event| new_title.set(event.value())
}
button {
r#type: "submit",
class: "py-2 col-span-1 bg-zinc-800/50 rounded-lg",
i {
class: "fa-solid fa-plus"
}
}
}
match subtasks_query.result().value() {
QueryResult::Ok(QueryValue::Subtasks(subtasks))
| QueryResult::Loading(Some(QueryValue::Subtasks(subtasks))) => {
rsx! {
for subtask in subtasks.clone() {
div {
key: "{subtask.id()}",
class: "flex flex-row items-center gap-3",
i {
class: format!(
"{} min-w-6 text-center text-2xl text-zinc-400/50",
if subtask.is_completed() {
"fa solid fa-square-check"
} else {
"fa-regular fa-square"
}
),
onclick: {
}
match subtasks_query.result().value() {
QueryResult::Ok(QueryValue::Subtasks(subtasks))
| QueryResult::Loading(Some(QueryValue::Subtasks(subtasks))) => {
rsx! {
for subtask in subtasks.clone() {
div {
key: "{subtask.id()}",
class: "flex flex-row items-center gap-3",
i {
class: format!(
"{} min-w-6 text-center text-2xl text-zinc-400/50",
if subtask.is_completed() {
"fa solid fa-square-check"
} else {
"fa-regular fa-square"
}
),
onclick: {
let subtask = subtask.clone();
let task = task.clone();
move |_| {
let subtask = subtask.clone();
move |_| {
let task = task.clone();
async move {
let new_subtask = NewSubtask::new(
subtask.task_id(),
subtask.title().to_owned(),
!subtask.is_completed()
);
let _ = edit_subtask(
subtask.id(),
new_subtask
).await;
query_client.invalidate_queries(&[
QueryKey::SubtasksOfTaskId(task.id()),
QueryKey::TasksWithSubtasksInCategory(
task.category().clone()
),
]);
}
}
}
}
div {
class: "grow grid grid-cols-6 gap-2",
input {
r#type: "text",
class: "grow py-2 px-3 col-span-5 bg-zinc-800/50 rounded-lg",
id: "input_title_{subtask.id()}",
initial_value: subtask.title(),
onchange: {
let subtask = subtask.clone();
let task = task.clone();
move |event| {
let subtask = subtask.clone();
let task = task.clone();
async move {
let new_subtask = NewSubtask::new(
subtask.task_id(),
subtask.title().to_owned(),
!subtask.is_completed()
event.value(),
subtask.is_completed()
);
let _ = edit_subtask(
subtask.id(),
new_subtask
).await;
if new_subtask.title.is_empty() {
let _ = delete_subtask(subtask.id()).await;
} else {
let _ = edit_subtask(
subtask.id(),
new_subtask
).await;
}
query_client.invalidate_queries(&[
QueryKey::SubtasksOfTaskId(task_id)
QueryKey::SubtasksOfTaskId(task.id()),
QueryKey::TasksWithSubtasksInCategory(
task.category().clone()
),
]);
}
}
}
}
div {
class: "grow grid grid-cols-6 gap-2",
input {
r#type: "text",
class: "grow py-2 px-3 col-span-5 bg-zinc-800/50 rounded-lg",
id: "input_new_title",
initial_value: subtask.title(),
onchange: {
button {
r#type: "button",
class: "py-2 col-span-1 bg-zinc-800/50 rounded-lg",
onclick: {
let subtask = subtask.clone();
let task = task.clone();
move |_| {
let subtask = subtask.clone();
move |event| {
let subtask = subtask.clone();
async move {
let new_subtask = NewSubtask::new(
subtask.task_id(),
event.value(),
subtask.is_completed()
);
let _ = edit_subtask(
subtask.id(),
new_subtask
).await;
query_client.invalidate_queries(&[
QueryKey::SubtasksOfTaskId(task_id)
]);
}
let task = task.clone();
async move {
let _ = delete_subtask(subtask.id()).await;
query_client.invalidate_queries(&[
QueryKey::SubtasksOfTaskId(task.id()),
QueryKey::TasksWithSubtasksInCategory(
task.category().clone()
),
]);
}
}
}
button {
r#type: "button",
class: "py-2 col-span-1 bg-zinc-800/50 rounded-lg",
onclick: {
let subtask = subtask.clone();
move |_| {
let subtask = subtask.clone();
async move {
let _ = delete_subtask(subtask.id()).await;
query_client.invalidate_queries(&[
QueryKey::SubtasksOfTaskId(task_id)
]);
}
}
},
i {
class: "fa-solid fa-trash-can"
}
},
i {
class: "fa-solid fa-trash-can"
}
}
}
}
}
},
QueryResult::Loading(None) => rsx! {
// TODO: Add a loading indicator.
},
QueryResult::Err(errors) => rsx! {
div {
"Errors occurred: {errors:?}"
}
},
value => panic!("Unexpected query result: {value:?}")
}
}
},
QueryResult::Loading(None) => rsx! {
// TODO: Add a loading indicator.
},
QueryResult::Err(errors) => rsx! {
div {
"Errors occurred: {errors:?}"
}
},
value => panic!("Unexpected query result: {value:?}")
}
}
}

View File

@ -133,7 +133,8 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
}
query_client.invalidate_queries(&[
QueryKey::Tasks,
QueryKey::TasksInCategory(selected_category())
QueryKey::TasksInCategory(selected_category()),
QueryKey::TasksWithSubtasksInCategory(selected_category()),
]);
coderabbitai[bot] commented 2024-09-08 20:27:16 +00:00 (Migrated from github.com)
Review

Refactor: Enhanced query invalidation and input field handling.

The query invalidation logic has been expanded to include QueryKey::TasksWithSubtasksInCategory(selected_category()), which allows the application to refresh the relevant data when a task is submitted. This addition improves the responsiveness of the UI. The handling of the autofocus property for the title input field has been updated to improve user experience by automatically focusing on the title input when creating a new task. Additionally, the SubtasksForm now receives the entire task object, facilitating easier access to task properties within the subtasks context.

Also applies to: 155-155, 350-350

**Refactor: Enhanced query invalidation and input field handling.** The query invalidation logic has been expanded to include `QueryKey::TasksWithSubtasksInCategory(selected_category())`, which allows the application to refresh the relevant data when a task is submitted. This addition improves the responsiveness of the UI. The handling of the `autofocus` property for the title input field has been updated to improve user experience by automatically focusing on the title input when creating a new task. Additionally, the `SubtasksForm` now receives the entire task object, facilitating easier access to task properties within the subtasks context. Also applies to: 155-155, 350-350 <!-- This is an auto-generated comment by CodeRabbit -->
on_successful_submit.call(());
}
@ -345,7 +346,7 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
},
if let Some(task) = task.as_ref() {
SubtasksForm {
task_id: task.id()
task: task.clone()
}
}
div {
@ -371,8 +372,9 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
}
query_client.invalidate_queries(&[
QueryKey::TasksInCategory(task.category().clone()),
QueryKey::Tasks,
QueryKey::TasksInCategory(task.category().clone()),
QueryKey::TasksWithSubtasksInCategory(selected_category()),
]);
}
on_successful_submit.call(());

View File

@ -1,5 +1,5 @@
use crate::models::category::Category;
use crate::models::task::Task;
use crate::models::task::{Task, TaskWithSubtasks};
use dioxus::core_macro::rsx;
use dioxus::dioxus_core::Element;
use dioxus::prelude::*;
@ -8,7 +8,7 @@ use crate::query::{QueryErrors, QueryKey, QueryValue};
use crate::server::tasks::complete_task;
#[component]
pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element {
pub(crate) fn TaskList(tasks: Vec<TaskWithSubtasks>, class: Option<&'static str>) -> Element {
let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>();
let mut task_being_edited = use_context::<Signal<Option<Task>>>();
@ -17,12 +17,12 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element
class: format!("flex flex-col {}", class.unwrap_or("")),
for task in tasks.clone() {
div {
key: "{task.id()}",
key: "{task.task().id()}",
class: format!(
"px-8 pt-5 {} flex flex-row gap-4 select-none {}",
if task.deadline().is_some() {
if task.task().deadline().is_some() || !task.subtasks().is_empty() {
"pb-0.5"
} else if let Category::Calendar { time, .. } = task.category() {
} else if let Category::Calendar { time, .. } = task.task().category() {
if time.is_some() {
"pb-0.5"
} else {
@ -31,18 +31,18 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element
} else {
"pb-5"
},
if task_being_edited().is_some_and(|t| t.id() == task.id()) {
if task_being_edited().is_some_and(|t| t.id() == task.task().id()) {
"bg-zinc-700"
} else { "" }
),
onclick: {
let task = task.clone();
move |_| task_being_edited.set(Some(task.clone()))
move |_| task_being_edited.set(Some(task.task().clone()))
},
i {
class: format!(
"{} text-3xl text-zinc-500",
if *(task.category()) == Category::Done {
if *(task.task().category()) == Category::Done {
"fa solid fa-square-check"
} else {
"fa-regular fa-square"
@ -55,16 +55,19 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element
event.stop_propagation();
let task = task.clone();
async move {
let completed_task = complete_task(task.id()).await;
let completed_task = complete_task(task.task().id()).await.unwrap();
let mut query_keys = vec![
QueryKey::Tasks,
QueryKey::TasksInCategory(
completed_task.unwrap().category().clone()
)
completed_task.category().clone()
),
QueryKey::TasksWithSubtasksInCategory(completed_task.category().clone()),
];
if let Category::Calendar { reoccurrence: Some(_), .. }
= task.category() {
query_keys.push(QueryKey::SubtasksOfTaskId(task.id()));
= task.task().category() {
query_keys.push(
QueryKey::SubtasksOfTaskId(task.task().id())
);
}
query_client.invalidate_queries(&query_keys);
}
@ -75,20 +78,20 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element
class: "flex flex-col",
div {
class: "mt-1 grow font-medium",
{task.title()}
{task.task().title()}
},
div {
class: "flex flex-row gap-3",
if let Some(deadline) = task.deadline() {
class: "flex flex-row gap-4",
if let Some(deadline) = task.task().deadline() {
div {
class: "text-sm text-zinc-400",
i {
class: "fa-solid fa-bomb"
},
{deadline.format(" %m. %d.").to_string()}
{deadline.format(" %m. %-d.").to_string()}
}
}
if let Category::Calendar { time, .. } = task.category() {
if let Category::Calendar { time, .. } = task.task().category() {
if let Some(calendar_time) = time {
div {
class: "text-sm text-zinc-400",
@ -99,6 +102,21 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element
}
}
}
if !task.subtasks().is_empty() {
div {
class: "text-sm text-zinc-400",
i {
class: "fa-solid fa-list-check"
},
{format!(
" {}/{}",
task.subtasks().iter()
.filter(|subtask| subtask.is_completed())
.count(),
task.subtasks().len()
)}
}
}
}
}
}

View File

@ -7,7 +7,7 @@ use validator::Validate;
const TITLE_LENGTH_MIN: u64 = 1;
const TITLE_LENGTH_MAX: u64 = 255;
#[derive(Queryable, Selectable, Serialize, Deserialize, PartialEq, Clone, Debug)]
#[derive(Queryable, Selectable, Identifiable, Serialize, Deserialize, PartialEq, Clone, Debug)]
#[diesel(table_name = crate::schema::projects)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Project {

View File

@ -1,5 +1,6 @@
use chrono::NaiveDateTime;
use crate::models::task::Task;
use crate::schema::subtasks;
use chrono::NaiveDateTime;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use validator::Validate;
@ -7,7 +8,9 @@ use validator::Validate;
const TITLE_LENGTH_MIN: u64 = 1;
const TITLE_LENGTH_MAX: u64 = 255;
#[derive(Queryable, Selectable, Serialize, Deserialize, PartialEq, Clone, Debug)]
#[derive(Queryable, Selectable, Identifiable, Associations, Serialize, Deserialize, PartialEq,
Clone, Debug)]
#[diesel(belongs_to(Task, foreign_key = task_id))]
#[diesel(table_name = subtasks)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Subtask {

View File

@ -4,11 +4,12 @@ use crate::schema::tasks;
use diesel::prelude::*;
use serde::{Deserialize, Serialize};
use validator::Validate;
use crate::models::subtask::Subtask;
const TITLE_LENGTH_MIN: u64 = 1;
const TITLE_LENGTH_MAX: u64 = 255;
#[derive(Queryable, Selectable, Serialize, Deserialize, PartialEq, Clone, Debug)]
#[derive(Queryable, Selectable, Identifiable, Serialize, Deserialize, PartialEq, Clone, Debug)]
#[diesel(table_name = tasks)]
#[diesel(check_for_backend(diesel::pg::Pg))]
pub struct Task {
@ -51,6 +52,26 @@ impl Task {
}
}
#[derive(Serialize, Deserialize, PartialEq, Clone, Debug)]
pub struct TaskWithSubtasks {
task: Task,
subtasks: Vec<Subtask>,
}
impl TaskWithSubtasks {
pub fn new(task: Task, subtasks: Vec<Subtask>) -> Self {
Self { task, subtasks }
}
pub fn task(&self) -> &Task {
&self.task
}
pub fn subtasks(&self) -> &Vec<Subtask> {
&self.subtasks
}
}
#[derive(Insertable, Serialize, Deserialize, Validate, Clone, Debug)]
#[diesel(table_name = tasks)]
pub struct NewTask {

View File

@ -3,7 +3,7 @@ use crate::errors::error_vec::ErrorVec;
use crate::models::category::Category;
use crate::models::project::Project;
use crate::models::subtask::Subtask;
use crate::models::task::Task;
use crate::models::task::{Task, TaskWithSubtasks};
pub(crate) mod tasks;
pub(crate) mod projects;
@ -13,6 +13,7 @@ pub(crate) mod subtasks;
pub(crate) enum QueryValue {
Projects(Vec<Project>),
Tasks(Vec<Task>),
TasksWithSubtasks(Vec<TaskWithSubtasks>),
Subtasks(Vec<Subtask>),
}
@ -26,5 +27,6 @@ pub(crate) enum QueryKey {
Projects,
Tasks,
TasksInCategory(Category),
TasksWithSubtasksInCategory(Category),
SubtasksOfTaskId(i32),
}

View File

@ -2,7 +2,7 @@ use dioxus::prelude::ServerFnError;
use dioxus_query::prelude::{use_get_query, QueryResult, UseQuery};
use crate::models::category::Category;
use crate::query::{QueryErrors, QueryKey, QueryValue};
use crate::server::tasks::get_tasks_in_category;
use crate::server::tasks::{get_tasks_in_category, get_tasks_with_subtasks_in_category};
@ -22,3 +22,29 @@ async fn fetch_tasks_in_category(keys: Vec<QueryKey>) -> QueryResult<QueryValue,
panic!("Unexpected query keys: {:?}", keys);
}
}
pub(crate) fn use_tasks_with_subtasks_in_category_query(category: Category)
-> UseQuery<QueryValue, QueryErrors, QueryKey> {
use_get_query(
[
QueryKey::TasksWithSubtasksInCategory(
category.clone()),
QueryKey::TasksInCategory(category),
QueryKey::Tasks
],
fetch_tasks_with_subtasks_in_category
)
}
async fn fetch_tasks_with_subtasks_in_category(keys: Vec<QueryKey>)
-> QueryResult<QueryValue, QueryErrors> {
if let Some(QueryKey::TasksWithSubtasksInCategory(category)) = keys.first() {
match get_tasks_with_subtasks_in_category(category.clone()).await {
Ok(tasks) => Ok(QueryValue::TasksWithSubtasks(tasks)),
Err(ServerFnError::WrappedServerError(errors)) => Err(QueryErrors::Error(errors)),
Err(error) => panic!("Unexpected error: {:?}", error)
}.into()
} else {
panic!("Unexpected query keys: {:?}", keys);
}
}

View File

@ -1,14 +1,16 @@
use chrono::{Datelike, Days, Months, NaiveDate};
use crate::errors::error::Error;
use crate::errors::error_vec::ErrorVec;
use crate::models::task::{NewTask, Task};
use crate::models::task::{NewTask, Task, TaskWithSubtasks};
use crate::server::database_connection::establish_database_connection;
use diesel::{ExpressionMethods, OptionalExtension, QueryDsl, RunQueryDsl, SelectableHelper};
use dioxus::prelude::*;
use diesel::prelude::*;
use time::util::days_in_year_month;
use validator::Validate;
use crate::errors::task_error::TaskError;
use crate::models::category::{Category, ReoccurrenceInterval};
use crate::models::subtask::Subtask;
use crate::server::subtasks::restore_subtasks_of_task;
#[server]
@ -72,6 +74,36 @@ pub(crate) async fn get_tasks_in_category(filtered_category: Category)
Ok(results)
}
#[server]
pub(crate) async fn get_tasks_with_subtasks_in_category(filtered_category: Category) -> Result<
Vec<TaskWithSubtasks>,
ServerFnError<ErrorVec<Error>>
> {
use crate::schema::tasks;
let mut connection = establish_database_connection()
.map_err::<ErrorVec<Error>, _>(|_| vec![Error::ServerInternal].into())?;
let tasks_in_category = tasks::table
.filter(filtered_category.eq_sql_predicate())
.select(Task::as_select()).load(&mut connection)
.map_err::<ErrorVec<Error>, _>(|_| vec![Error::ServerInternal].into())?;
let subtasks = Subtask::belonging_to(&tasks_in_category)
.select(Subtask::as_select())
.load(&mut connection)
.map_err::<ErrorVec<Error>, _>(|_| vec![Error::ServerInternal].into())?;
let tasks_with_subtasks = subtasks
.grouped_by(&tasks_in_category)
.into_iter()
.zip(tasks_in_category)
.map(|(pages, book)| TaskWithSubtasks::new(book, pages))
.collect();
Ok(tasks_with_subtasks)
}
#[server]
pub(crate) async fn edit_task(task_id: i32, new_task: NewTask)
-> Result<Task, ServerFnError<ErrorVec<TaskError>>> {