feat: ability to manage subtasks (#40)
This commit is contained in:
commit
e8b6af1492
4
migrations/2024-09-08-083610_create_subtasks/down.sql
Normal file
4
migrations/2024-09-08-083610_create_subtasks/down.sql
Normal file
@ -0,0 +1,4 @@
|
||||
-- This file should undo anything in `up.sql`
|
||||
|
||||
|
||||
DROP TABLE IF EXISTS "subtasks";
|
15
migrations/2024-09-08-083610_create_subtasks/up.sql
Normal file
15
migrations/2024-09-08-083610_create_subtasks/up.sql
Normal file
@ -0,0 +1,15 @@
|
||||
-- Your SQL goes here
|
||||
|
||||
|
||||
CREATE TABLE "subtasks"(
|
||||
"id" SERIAL NOT NULL PRIMARY KEY,
|
||||
"task_id" INT4 NOT NULL,
|
||||
"title" TEXT NOT NULL,
|
||||
"is_completed" BOOL NOT NULL,
|
||||
"created_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
"updated_at" TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP,
|
||||
FOREIGN KEY ("task_id") REFERENCES "tasks"("id") ON DELETE CASCADE
|
||||
);
|
||||
|
||||
SELECT diesel_manage_updated_at('subtasks');
|
||||
|
@ -36,11 +36,11 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
|
||||
rsx! {
|
||||
div {
|
||||
class: format!(
|
||||
"pointer-events-auto bg-zinc-700/50 rounded-t-xl border-t-zinc-600 border-t backdrop-blur drop-shadow-[0_-5px_10px_rgba(0,0,0,0.2)] transition-[height] duration-[500ms] ease-[cubic-bezier(0.79,0.14,0.15,0.86)] {}",
|
||||
"pointer-events-auto bg-zinc-700/50 rounded-t-xl border-t-zinc-600 border-t backdrop-blur drop-shadow-[0_-5px_10px_rgba(0,0,0,0.2)] transition-[height] duration-[500ms] ease-[cubic-bezier(0.79,0.14,0.15,0.86)] overflow-y-scroll {}",
|
||||
match (display_form(), current_route, navigation_expanded()) {
|
||||
(false, _, false) => "h-[64px]",
|
||||
(false, _, true) => "h-[128px]",
|
||||
(true, Route::ProjectsPage, _) => "h-[128px]",
|
||||
(false, _, false) => "h-[66px]",
|
||||
(false, _, true) => "h-[130px]",
|
||||
(true, Route::ProjectsPage, _) => "h-[130px]",
|
||||
(true, _, _) => "h-[448px]",
|
||||
}
|
||||
),
|
||||
|
@ -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;
|
||||
|
@ -41,8 +41,8 @@ pub(crate) fn CategoryCalendarPage() -> Element {
|
||||
.format_localized(
|
||||
format!(
|
||||
"%A %-d. %B{}",
|
||||
if date_current.year() != today_date.year() {" %Y"}
|
||||
else {""}
|
||||
if date_current.year() != today_date.year()
|
||||
{" %Y"} else {""}
|
||||
).as_str(),
|
||||
Locale::en_US
|
||||
)
|
||||
|
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,262 +80,274 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
|
||||
let task_for_submit = task.clone();
|
||||
|
||||
rsx! {
|
||||
form {
|
||||
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(());
|
||||
}
|
||||
},
|
||||
div {
|
||||
class: "p-4 flex flex-col gap-4",
|
||||
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()}
|
||||
}
|
||||
}
|
||||
},
|
||||
},
|
||||
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()
|
||||
form {
|
||||
class: "flex flex-col gap-4",
|
||||
id: "form_task",
|
||||
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()
|
||||
),
|
||||
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());
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
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
|
||||
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()
|
||||
},
|
||||
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"
|
||||
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(());
|
||||
}
|
||||
},
|
||||
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 {
|
||||
class: "flex flex-row items-center gap-3",
|
||||
label {
|
||||
r#for: "category_calendar_reminder_offset_index",
|
||||
r#for: "input_deadline",
|
||||
class: "min-w-6 text-center",
|
||||
i {
|
||||
class: "fa-solid fa-bell text-zinc-400/50"
|
||||
class: "fa-solid fa-hourglass-end 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()
|
||||
);
|
||||
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"
|
||||
}
|
||||
},
|
||||
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())
|
||||
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",
|
||||
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 {
|
||||
class: "flex flex-row justify-between mt-auto",
|
||||
button {
|
||||
@ -353,10 +366,10 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
|
||||
Category::Trash,
|
||||
task.project_id()
|
||||
);
|
||||
|
||||
|
||||
let _ = edit_task(task.id(), new_task).await;
|
||||
}
|
||||
|
||||
|
||||
query_client.invalidate_queries(&[
|
||||
QueryKey::TasksInCategory(task.category().clone()),
|
||||
QueryKey::Tasks,
|
||||
@ -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,86 +15,94 @@ 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! {
|
||||
div {
|
||||
key: "{task.id()}",
|
||||
class: format!(
|
||||
"px-8 pt-5 {} flex flex-row gap-4 select-none {}",
|
||||
if task.deadline().is_some() {
|
||||
for task in tasks.clone() {
|
||||
div {
|
||||
key: "{task.id()}",
|
||||
class: format!(
|
||||
"px-8 pt-5 {} flex flex-row gap-4 select-none {}",
|
||||
if task.deadline().is_some() {
|
||||
"pb-0.5"
|
||||
} else if let Category::Calendar { time, .. } = task.category() {
|
||||
if time.is_some() {
|
||||
"pb-0.5"
|
||||
} else if let Category::Calendar { time, .. } = task.category() {
|
||||
if time.is_some() {
|
||||
"pb-0.5"
|
||||
} else {
|
||||
"pb-5"
|
||||
}
|
||||
} else {
|
||||
"pb-5"
|
||||
},
|
||||
if task_being_edited().is_some_and(|t| t.id() == task.id()) {
|
||||
"bg-zinc-700"
|
||||
} else { "" }
|
||||
}
|
||||
} else {
|
||||
"pb-5"
|
||||
},
|
||||
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())),
|
||||
i {
|
||||
class: format!(
|
||||
"{} text-3xl text-zinc-500",
|
||||
if *(task_clone.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(&[
|
||||
QueryKey::Tasks,
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
},
|
||||
div {
|
||||
class: "flex flex-col",
|
||||
div {
|
||||
class: "mt-1 grow font-medium",
|
||||
{task.title()}
|
||||
},
|
||||
div {
|
||||
class: "flex flex-col",
|
||||
div {
|
||||
class: "mt-1 grow font-medium",
|
||||
{task.title()}
|
||||
},
|
||||
div {
|
||||
class: "flex flex-row gap-3",
|
||||
if let Some(deadline) = task.deadline() {
|
||||
class: "flex flex-row gap-3",
|
||||
if let Some(deadline) = 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.category() {
|
||||
if let Some(calendar_time) = time {
|
||||
div {
|
||||
class: "text-sm text-zinc-400",
|
||||
i {
|
||||
class: "fa-solid fa-bomb"
|
||||
class: "fa-solid fa-clock"
|
||||
},
|
||||
{deadline.format(" %m. %d.").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()}
|
||||
}
|
||||
{calendar_time.time().format(" %k:%M").to_string()}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -2,3 +2,4 @@ pub(crate) mod error;
|
||||
pub(crate) mod error_vec;
|
||||
pub(crate) mod project_error;
|
||||
pub(crate) mod task_error;
|
||||
pub(crate) mod subtask_error;
|
||||
|
70
src/errors/subtask_error.rs
Normal file
70
src/errors/subtask_error.rs
Normal file
@ -0,0 +1,70 @@
|
||||
use crate::errors::error::Error;
|
||||
use crate::errors::error_vec::ErrorVec;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::fmt::Display;
|
||||
use std::str::FromStr;
|
||||
use validator::{ValidationErrors, ValidationErrorsKind};
|
||||
|
||||
#[derive(Serialize, Deserialize, Debug)]
|
||||
pub enum SubtaskError {
|
||||
TitleLengthInvalid,
|
||||
TaskNotFound,
|
||||
Error(Error),
|
||||
}
|
||||
|
||||
impl From<ValidationErrors> for ErrorVec<SubtaskError> {
|
||||
fn from(validation_errors: ValidationErrors) -> Self {
|
||||
validation_errors.errors()
|
||||
.iter()
|
||||
.flat_map(|(&field, error_kind)| match field {
|
||||
"title" => match error_kind {
|
||||
ValidationErrorsKind::Field(validation_errors) => validation_errors
|
||||
.iter()
|
||||
.map(|validation_error| validation_error.code.as_ref())
|
||||
.map(|code| match code {
|
||||
"title_length" => SubtaskError::TitleLengthInvalid,
|
||||
_ => panic!("Unexpected validation error code: `{code}`."),
|
||||
})
|
||||
.collect::<Vec<SubtaskError>>(),
|
||||
_ => panic!("Unexpected validation error kind."),
|
||||
},
|
||||
_ => panic!("Unexpected validation field name: `{field}`."),
|
||||
})
|
||||
.collect::<Vec<SubtaskError>>()
|
||||
.into()
|
||||
}
|
||||
}
|
||||
|
||||
impl From<diesel::result::Error> for SubtaskError {
|
||||
fn from(diesel_error: diesel::result::Error) -> Self {
|
||||
match diesel_error {
|
||||
diesel::result::Error::DatabaseError(
|
||||
diesel::result::DatabaseErrorKind::ForeignKeyViolation, info
|
||||
) => {
|
||||
match info.constraint_name() {
|
||||
Some("subtasks_task_id_fkey") => Self::TaskNotFound,
|
||||
_ => Self::Error(Error::ServerInternal)
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
Self::Error(Error::ServerInternal)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Has to be implemented for Dioxus server functions.
|
||||
impl Display for SubtaskError {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "{:?}", self)
|
||||
}
|
||||
}
|
||||
|
||||
// Has to be implemented for Dioxus server functions.
|
||||
impl FromStr for SubtaskError {
|
||||
type Err = ();
|
||||
|
||||
fn from_str(_: &str) -> Result<Self, Self::Err> {
|
||||
Ok(Self::Error(Error::ServerInternal))
|
||||
}
|
||||
}
|
@ -1,3 +1,4 @@
|
||||
pub(crate) mod project;
|
||||
pub(crate) mod category;
|
||||
pub(crate) mod task;
|
||||
pub(crate) mod subtask;
|
||||
|
67
src/models/subtask.rs
Normal file
67
src/models/subtask.rs
Normal file
@ -0,0 +1,67 @@
|
||||
use chrono::NaiveDateTime;
|
||||
use crate::schema::subtasks;
|
||||
use diesel::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use validator::Validate;
|
||||
|
||||
const TITLE_LENGTH_MIN: u64 = 1;
|
||||
const TITLE_LENGTH_MAX: u64 = 255;
|
||||
|
||||
#[derive(Queryable, Selectable, Serialize, Deserialize, PartialEq, Clone, Debug)]
|
||||
#[diesel(table_name = subtasks)]
|
||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||
pub struct Subtask {
|
||||
id: i32,
|
||||
task_id: i32,
|
||||
title: String,
|
||||
is_completed: bool,
|
||||
created_at: NaiveDateTime,
|
||||
updated_at: NaiveDateTime,
|
||||
}
|
||||
|
||||
impl Subtask {
|
||||
pub fn id(&self) -> i32 {
|
||||
self.id
|
||||
}
|
||||
|
||||
pub fn task_id(&self) -> i32 {
|
||||
self.task_id
|
||||
}
|
||||
|
||||
pub fn title(&self) -> &str {
|
||||
&self.title
|
||||
}
|
||||
|
||||
pub fn is_completed(&self) -> bool {
|
||||
self.is_completed
|
||||
}
|
||||
|
||||
pub fn created_at(&self) -> NaiveDateTime {
|
||||
self.created_at
|
||||
}
|
||||
|
||||
pub fn updated_at(&self) -> NaiveDateTime {
|
||||
self.updated_at
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Insertable, Serialize, Deserialize, Validate, Clone, Debug)]
|
||||
#[diesel(table_name = subtasks)]
|
||||
pub struct NewSubtask {
|
||||
pub task_id: i32,
|
||||
#[validate(length(min = "TITLE_LENGTH_MIN", max = "TITLE_LENGTH_MAX", code = "title_length"))]
|
||||
pub title: String,
|
||||
pub is_completed: bool,
|
||||
}
|
||||
|
||||
impl NewSubtask {
|
||||
pub fn new(task_id: i32, title: String, is_completed: bool) -> Self {
|
||||
Self { task_id, title, is_completed }
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Subtask> for NewSubtask {
|
||||
fn from(subtask: Subtask) -> Self {
|
||||
Self::new(subtask.task_id, subtask.title, subtask.is_completed)
|
||||
}
|
||||
}
|
@ -9,7 +9,7 @@ const TITLE_LENGTH_MIN: u64 = 1;
|
||||
const TITLE_LENGTH_MAX: u64 = 255;
|
||||
|
||||
#[derive(Queryable, Selectable, Serialize, Deserialize, PartialEq, Clone, Debug)]
|
||||
#[diesel(table_name = crate::schema::tasks)]
|
||||
#[diesel(table_name = tasks)]
|
||||
#[diesel(check_for_backend(diesel::pg::Pg))]
|
||||
pub struct Task {
|
||||
id: i32,
|
||||
|
@ -2,15 +2,18 @@ use crate::errors::error::Error;
|
||||
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;
|
||||
|
||||
pub(crate) mod tasks;
|
||||
pub(crate) mod projects;
|
||||
pub(crate) mod subtasks;
|
||||
|
||||
#[derive(PartialEq, Debug)]
|
||||
pub(crate) enum QueryValue {
|
||||
Tasks(Vec<Task>),
|
||||
Projects(Vec<Project>),
|
||||
Tasks(Vec<Task>),
|
||||
Subtasks(Vec<Subtask>),
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
@ -20,7 +23,8 @@ pub(crate) enum QueryErrors {
|
||||
|
||||
#[derive(PartialEq, Eq, Hash, Clone, Debug)]
|
||||
pub(crate) enum QueryKey {
|
||||
Projects,
|
||||
Tasks,
|
||||
TasksInCategory(Category),
|
||||
Projects,
|
||||
SubtasksOfTaskId(i32),
|
||||
}
|
||||
|
21
src/query/subtasks.rs
Normal file
21
src/query/subtasks.rs
Normal file
@ -0,0 +1,21 @@
|
||||
use crate::query::{QueryErrors, QueryKey, QueryValue};
|
||||
use crate::server::subtasks::get_subtasks_of_task;
|
||||
use dioxus::prelude::ServerFnError;
|
||||
use dioxus_query::prelude::{use_get_query, QueryResult, UseQuery};
|
||||
|
||||
pub(crate) fn use_subtasks_of_task_query(task_id: i32)
|
||||
-> UseQuery<QueryValue, QueryErrors, QueryKey> {
|
||||
use_get_query([QueryKey::SubtasksOfTaskId(task_id)], fetch_subtasks_of_task)
|
||||
}
|
||||
|
||||
async fn fetch_subtasks_of_task(keys: Vec<QueryKey>) -> QueryResult<QueryValue, QueryErrors> {
|
||||
if let Some(QueryKey::SubtasksOfTaskId(task_id)) = keys.first() {
|
||||
match get_subtasks_of_task(*task_id).await {
|
||||
Ok(subtasks) => Ok(QueryValue::Subtasks(subtasks)),
|
||||
Err(ServerFnError::WrappedServerError(errors)) => Err(QueryErrors::Error(errors)),
|
||||
Err(error) => panic!("Unexpected error: {:?}", error)
|
||||
}.into()
|
||||
} else {
|
||||
panic!("Unexpected query keys: {:?}", keys);
|
||||
}
|
||||
}
|
@ -9,6 +9,17 @@ diesel::table! {
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
subtasks (id) {
|
||||
id -> Int4,
|
||||
task_id -> Int4,
|
||||
title -> Text,
|
||||
is_completed -> Bool,
|
||||
created_at -> Timestamp,
|
||||
updated_at -> Timestamp,
|
||||
}
|
||||
}
|
||||
|
||||
diesel::table! {
|
||||
tasks (id) {
|
||||
id -> Int4,
|
||||
@ -21,9 +32,11 @@ diesel::table! {
|
||||
}
|
||||
}
|
||||
|
||||
diesel::joinable!(subtasks -> tasks (task_id));
|
||||
diesel::joinable!(tasks -> projects (project_id));
|
||||
|
||||
diesel::allow_tables_to_appear_in_same_query!(
|
||||
projects,
|
||||
subtasks,
|
||||
tasks,
|
||||
);
|
||||
|
@ -1,3 +1,4 @@
|
||||
mod database_connection;
|
||||
pub(crate) mod projects;
|
||||
pub(crate) mod tasks;
|
||||
pub(crate) mod subtasks;
|
||||
|
115
src/server/subtasks.rs
Normal file
115
src/server/subtasks.rs
Normal file
@ -0,0 +1,115 @@
|
||||
use crate::errors::error::Error;
|
||||
use crate::errors::error_vec::ErrorVec;
|
||||
use crate::errors::subtask_error::SubtaskError;
|
||||
use crate::models::subtask::{NewSubtask, Subtask};
|
||||
use crate::server::database_connection::establish_database_connection;
|
||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper};
|
||||
use dioxus::prelude::*;
|
||||
use validator::Validate;
|
||||
|
||||
#[server]
|
||||
pub(crate) async fn create_subtask(new_subtask: NewSubtask)
|
||||
-> Result<Subtask, ServerFnError<ErrorVec<SubtaskError>>> {
|
||||
use crate::schema::subtasks;
|
||||
|
||||
new_subtask.validate()
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(|errors| errors.into())?;
|
||||
|
||||
let mut connection = establish_database_connection()
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(
|
||||
|_| vec![SubtaskError::Error(Error::ServerInternal)].into()
|
||||
)?;
|
||||
|
||||
let created_subtask = diesel::insert_into(subtasks::table)
|
||||
.values(&new_subtask)
|
||||
.returning(Subtask::as_returning())
|
||||
.get_result(&mut connection)
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(|error| vec![error.into()].into())?;
|
||||
|
||||
Ok(created_subtask)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub(crate) async fn get_subtasks_of_task(filtered_task_id: i32)
|
||||
-> Result<Vec<Subtask>, ServerFnError<ErrorVec<Error>>> {
|
||||
use crate::schema::subtasks::dsl::*;
|
||||
|
||||
let mut connection = establish_database_connection()
|
||||
.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()
|
||||
)?;
|
||||
|
||||
Ok(results)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub(crate) async fn edit_subtask(subtask_id: i32, new_subtask: NewSubtask)
|
||||
-> Result<Subtask, ServerFnError<ErrorVec<SubtaskError>>> {
|
||||
use crate::schema::subtasks::dsl::*;
|
||||
|
||||
new_subtask.validate()
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(|errors| errors.into())?;
|
||||
|
||||
let mut connection = establish_database_connection()
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(
|
||||
|_| vec![SubtaskError::Error(Error::ServerInternal)].into()
|
||||
)?;
|
||||
|
||||
let updated_task = diesel::update(subtasks)
|
||||
.filter(id.eq(subtask_id))
|
||||
.set((
|
||||
title.eq(new_subtask.title),
|
||||
is_completed.eq(new_subtask.is_completed)
|
||||
))
|
||||
.returning(Subtask::as_returning())
|
||||
.get_result(&mut connection)
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(|error| vec![error.into()].into())?;
|
||||
|
||||
Ok(updated_task)
|
||||
}
|
||||
|
||||
#[server]
|
||||
pub(crate) async fn restore_subtasks_of_task(filtered_task_id: i32) -> Result<
|
||||
Vec<Subtask>,
|
||||
ServerFnError<ErrorVec<SubtaskError>>
|
||||
> {
|
||||
use crate::schema::subtasks::dsl::*;
|
||||
|
||||
let mut connection = establish_database_connection()
|
||||
.map_err::<ErrorVec<SubtaskError>, _>(
|
||||
|_| vec![SubtaskError::Error(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())?;
|
||||
|
||||
Ok(updated_subtasks)
|
||||
}
|
||||
|
||||
// TODO: Get rid of this suppression.
|
||||
//noinspection DuplicatedCode
|
||||
#[server]
|
||||
pub(crate) async fn delete_subtask(subtask_id: i32)
|
||||
-> Result<(), ServerFnError<ErrorVec<Error>>> {
|
||||
use crate::schema::subtasks::dsl::*;
|
||||
|
||||
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)
|
||||
.map_err::<ErrorVec<Error>, _>(|error| vec![error.into()].into())?;
|
||||
|
||||
Ok(())
|
||||
}
|
@ -9,6 +9,7 @@ use time::util::days_in_year_month;
|
||||
use validator::Validate;
|
||||
use crate::errors::task_error::TaskError;
|
||||
use crate::models::category::{Category, ReoccurrenceInterval};
|
||||
use crate::server::subtasks::restore_subtasks_of_task;
|
||||
|
||||
#[server]
|
||||
pub(crate) async fn create_task(new_task: NewTask)
|
||||
@ -127,6 +128,8 @@ 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())?;
|
||||
} else {
|
||||
new_task.category = Category::Done;
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user