feat: create a from for subtasks

This commit is contained in:
Matouš Volf 2024-09-08 19:53:01 +02:00
parent 11bab284d4
commit a05b4f9f66
4 changed files with 475 additions and 294 deletions

View File

@ -12,3 +12,4 @@ pub(crate) mod category_input;
pub(crate) mod reoccurrence_input; pub(crate) mod reoccurrence_input;
pub(crate) mod layout; pub(crate) mod layout;
pub(crate) mod navigation_item; pub(crate) mod navigation_item;
pub(crate) mod subtasks_form;

View 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:?}")
}
}
}

View File

@ -12,6 +12,7 @@ use dioxus::core_macro::{component, rsx};
use dioxus::dioxus_core::Element; use dioxus::dioxus_core::Element;
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_query::prelude::use_query_client; use dioxus_query::prelude::use_query_client;
use crate::components::subtasks_form::SubtasksForm;
const REMINDER_OFFSETS: [Option<Duration>; 17] = [ const REMINDER_OFFSETS: [Option<Duration>; 17] = [
None, None,
@ -79,262 +80,274 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
let task_for_submit = task.clone(); let task_for_submit = task.clone();
rsx! { rsx! {
form { div {
onsubmit: move |event| {
let task = task_for_submit.clone();
async move {
let new_task = NewTask::new(
event.values().get("title").unwrap().as_value(),
event.values().get("deadline").unwrap().as_value().parse().ok(),
match &selected_category() {
Category::WaitingFor(_) => Category::WaitingFor(
event.values().get("category_waiting_for").unwrap()
.as_value()
),
Category::Calendar { .. } => Category::Calendar {
date: event.values().get("category_calendar_date").unwrap()
.as_value().parse().unwrap(),
reoccurrence: category_calendar_reoccurrence_interval().map(
|reoccurrence_interval| Reoccurrence::new(
event.values().get("category_calendar_date").unwrap()
.as_value().parse().unwrap(),
reoccurrence_interval,
event.values().get("category_calendar_reoccurrence_length")
.unwrap().as_value().parse().unwrap()
)
),
time: event.values().get("category_calendar_time").unwrap()
.as_value().parse().ok().map(|time|
CalendarTime::new(
time,
REMINDER_OFFSETS[
event.values()
.get("category_calendar_reminder_offset_index").unwrap()
.as_value().parse::<usize>().unwrap()
]
)
)
},
category => category.clone()
},
event.values().get("project_id").unwrap()
.as_value().parse::<i32>().ok().filter(|&id| id > 0),
);
if let Some(task) = task {
let _ = edit_task(task.id(), new_task).await;
} else {
let _ = create_task(new_task).await;
}
query_client.invalidate_queries(&[
QueryKey::Tasks,
QueryKey::TasksInCategory(selected_category())
]);
on_successful_submit.call(());
}
},
class: "p-4 flex flex-col gap-4", class: "p-4 flex flex-col gap-4",
div { form {
class: "flex flex-row items-center gap-3", class: "flex flex-col gap-4",
label { id: "form_task",
r#for: "input_title", onsubmit: move |event| {
class: "min-w-6 text-center", let task = task_for_submit.clone();
i { async move {
class: "fa-solid fa-pen-clip text-zinc-400/50" let new_task = NewTask::new(
}, event.values().get("title").unwrap().as_value(),
}, event.values().get("deadline").unwrap().as_value().parse().ok(),
input { match &selected_category() {
name: "title", Category::WaitingFor(_) => Category::WaitingFor(
required: true, event.values().get("category_waiting_for").unwrap()
initial_value: task.as_ref().map(|task| task.title().to_owned()), .as_value()
r#type: "text",
class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg",
id: "input_title"
},
},
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_project",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-list text-zinc-400/50"
}
},
select {
name: "project_id",
class: "px-3.5 py-2.5 bg-zinc-800/50 rounded-lg grow",
id: "input_project",
option {
value: 0,
"None"
},
for project in projects {
option {
value: project.id().to_string(),
selected: task.as_ref().is_some_and(
|task| task.project_id() == Some(project.id())
),
{project.title()}
}
}
},
},
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_deadline",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-bomb text-zinc-400/50"
}
},
input {
name: "deadline",
initial_value: task.as_ref().and_then(|task| task.deadline())
.map(|deadline| deadline.format("%Y-%m-%d").to_string()),
r#type: "date",
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow basis-0",
id: "input_deadline"
}
},
div {
class: "flex flex-row items-center gap-3",
label {
class: "min-w-6 text-center",
i {
class: "fa-solid fa-layer-group text-zinc-400/50"
}
},
CategoryInput {
selected_category: selected_category,
class: "grow"
}
}
match selected_category() {
Category::WaitingFor(waiting_for) => rsx! {
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_deadline",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-hourglass-end text-zinc-400/50"
}
},
input {
name: "category_waiting_for",
required: true,
initial_value: waiting_for,
r#type: "text",
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
id: "input_category_waiting_for"
},
}
},
Category::Calendar { date, reoccurrence, time } => rsx! {
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_category_calendar_date",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-clock text-zinc-400/50"
}
},
div {
class: "grow flex flex-row gap-2",
input {
r#type: "date",
name: "category_calendar_date",
required: true,
initial_value: date.format("%Y-%m-%d").to_string(),
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
id: "input_category_calendar_date"
},
input {
r#type: "time",
name: "category_calendar_time",
initial_value: time.map(|calendar_time|
calendar_time.time().format("%H:%M").to_string()
), ),
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow", Category::Calendar { .. } => Category::Calendar {
id: "input_category_calendar_time", date: event.values().get("category_calendar_date").unwrap()
oninput: move |event| { .as_value().parse().unwrap(),
category_calendar_has_time.set(!event.value().is_empty()); reoccurrence: category_calendar_reoccurrence_interval().map(
} |reoccurrence_interval| Reoccurrence::new(
} event.values().get("category_calendar_date").unwrap()
} .as_value().parse().unwrap(),
}, reoccurrence_interval,
div { event.values()
class: "flex flex-row items-center gap-3", .get("category_calendar_reoccurrence_length")
label { .unwrap().as_value().parse().unwrap()
r#for: "category_calendar_reoccurrence_length", )
class: "min-w-6 text-center", ),
i { time: event.values().get("category_calendar_time").unwrap()
class: "fa-solid fa-repeat text-zinc-400/50" .as_value().parse().ok().map(|time|
} CalendarTime::new(
}, time,
div { REMINDER_OFFSETS[
class: "grow grid grid-cols-6 gap-2", event.values()
ReoccurrenceIntervalInput { .get("category_calendar_reminder_offset_index")
reoccurrence_interval: category_calendar_reoccurrence_interval .unwrap().as_value().parse::<usize>().unwrap()
]
)
)
},
category => category.clone()
}, },
input { event.values().get("project_id").unwrap()
r#type: "number", .as_value().parse::<i32>().ok().filter(|&id| id > 0),
inputmode: "numeric", );
name: "category_calendar_reoccurrence_length", if let Some(task) = task {
disabled: category_calendar_reoccurrence_interval().is_none(), let _ = edit_task(task.id(), new_task).await;
required: true, } else {
min: 1, let _ = create_task(new_task).await;
initial_value: category_calendar_reoccurrence_interval() }
.map_or(String::new(), |_| reoccurrence.map_or(1, |reoccurrence| query_client.invalidate_queries(&[
reoccurrence.length()).to_string()), QueryKey::Tasks,
class: "py-2 px-3 bg-zinc-800/50 rounded-lg col-span-2 text-right", QueryKey::TasksInCategory(selected_category())
id: "category_calendar_reoccurrence_length" ]);
on_successful_submit.call(());
}
},
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_title",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-pen-clip text-zinc-400/50"
},
},
input {
name: "title",
required: true,
initial_value: task.as_ref().map(|task| task.title().to_owned()),
r#type: "text",
class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg",
id: "input_title"
},
},
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_project",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-list text-zinc-400/50"
}
},
select {
name: "project_id",
class: "px-3.5 py-2.5 bg-zinc-800/50 rounded-lg grow",
id: "input_project",
option {
value: 0,
"None"
},
for project in projects {
option {
value: project.id().to_string(),
selected: task.as_ref().is_some_and(
|task| task.project_id() == Some(project.id())
),
{project.title()}
} }
} }
}, },
if category_calendar_has_time() { },
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "input_deadline",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-bomb text-zinc-400/50"
}
},
input {
name: "deadline",
initial_value: task.as_ref().and_then(|task| task.deadline())
.map(|deadline| deadline.format("%Y-%m-%d").to_string()),
r#type: "date",
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow basis-0",
id: "input_deadline"
}
},
div {
class: "flex flex-row items-center gap-3",
label {
class: "min-w-6 text-center",
i {
class: "fa-solid fa-layer-group text-zinc-400/50"
}
},
CategoryInput {
selected_category: selected_category,
class: "grow"
}
}
match selected_category() {
Category::WaitingFor(waiting_for) => rsx! {
div { div {
class: "flex flex-row items-center gap-3", class: "flex flex-row items-center gap-3",
label { label {
r#for: "category_calendar_reminder_offset_index", r#for: "input_deadline",
class: "min-w-6 text-center", class: "min-w-6 text-center",
i { i {
class: "fa-solid fa-bell text-zinc-400/50" class: "fa-solid fa-hourglass-end text-zinc-400/50"
} }
}, },
input { input {
r#type: "range", name: "category_waiting_for",
name: "category_calendar_reminder_offset_index", required: true,
min: 0, initial_value: waiting_for,
max: REMINDER_OFFSETS.len() as i64 - 1, r#type: "text",
initial_value: category_calendar_reminder_offset_index() class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
.to_string(), id: "input_category_waiting_for"
class: "grow input-range-reverse", },
id: "category_calendar_has_reminder", }
oninput: move |event| { },
category_calendar_reminder_offset_index.set( Category::Calendar { date, reoccurrence, time } => rsx! {
event.value().parse().unwrap() div {
); class: "flex flex-row items-center gap-3",
label {
r#for: "input_category_calendar_date",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-clock text-zinc-400/50"
} }
}, },
label { div {
r#for: "category_calendar_reminder_offset_index", class: "grow flex flex-row gap-2",
class: "pr-3 min-w-16 text-right", input {
{REMINDER_OFFSETS[category_calendar_reminder_offset_index()].map( r#type: "date",
|offset| if offset.num_hours() < 1 { name: "category_calendar_date",
format!("{} min", offset.num_minutes()) required: true,
} else { initial_value: date.format("%Y-%m-%d").to_string(),
format!("{} h", offset.num_hours()) class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
id: "input_category_calendar_date"
},
input {
r#type: "time",
name: "category_calendar_time",
initial_value: time.map(|calendar_time|
calendar_time.time().format("%H:%M").to_string()
),
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
id: "input_category_calendar_time",
oninput: move |event| {
category_calendar_has_time.set(!event.value().is_empty());
} }
).unwrap_or_else(|| "none".to_string())} }
}
},
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "category_calendar_reoccurrence_length",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-repeat text-zinc-400/50"
}
},
div {
class: "grow grid grid-cols-6 gap-2",
ReoccurrenceIntervalInput {
reoccurrence_interval: category_calendar_reoccurrence_interval
},
input {
r#type: "number",
inputmode: "numeric",
name: "category_calendar_reoccurrence_length",
disabled: category_calendar_reoccurrence_interval().is_none(),
required: true,
min: 1,
initial_value: category_calendar_reoccurrence_interval().map_or(
String::new(),
|_| reoccurrence.map_or(1, |reoccurrence|
reoccurrence.length()).to_string()
),
class: "py-2 px-3 bg-zinc-800/50 rounded-lg col-span-2 text-right",
id: "category_calendar_reoccurrence_length"
}
}
},
if category_calendar_has_time() {
div {
class: "flex flex-row items-center gap-3",
label {
r#for: "category_calendar_reminder_offset_index",
class: "min-w-6 text-center",
i {
class: "fa-solid fa-bell text-zinc-400/50"
}
},
input {
r#type: "range",
name: "category_calendar_reminder_offset_index",
min: 0,
max: REMINDER_OFFSETS.len() as i64 - 1,
initial_value: category_calendar_reminder_offset_index()
.to_string(),
class: "grow input-range-reverse",
id: "category_calendar_has_reminder",
oninput: move |event| {
category_calendar_reminder_offset_index.set(
event.value().parse().unwrap()
);
}
},
label {
r#for: "category_calendar_reminder_offset_index",
class: "pr-3 min-w-16 text-right",
{REMINDER_OFFSETS[category_calendar_reminder_offset_index()].map(
|offset| if offset.num_hours() < 1 {
format!("{} min", offset.num_minutes())
} else {
format!("{} h", offset.num_hours())
}
).unwrap_or_else(|| "none".to_string())}
}
} }
} }
} },
}, _ => None
_ => None }
}, },
if let Some(task) = task.as_ref() {
SubtasksForm {
task_id: task.id()
}
}
div { div {
class: "flex flex-row justify-between mt-auto", class: "flex flex-row justify-between mt-auto",
button { button {
@ -370,6 +383,7 @@ pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()
} }
} }
button { button {
form: "form_task",
r#type: "submit", r#type: "submit",
class: "py-2 px-4 bg-zinc-300/50 rounded-lg", class: "py-2 px-4 bg-zinc-300/50 rounded-lg",
i { i {

View File

@ -15,86 +15,94 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element
rsx! { rsx! {
div { div {
class: format!("flex flex-col {}", class.unwrap_or("")), class: format!("flex flex-col {}", class.unwrap_or("")),
{tasks.iter().cloned().map(|task| { for task in tasks.clone() {
let task_clone = task.clone(); div {
rsx! { key: "{task.id()}",
div { class: format!(
key: "{task.id()}", "px-8 pt-5 {} flex flex-row gap-4 select-none {}",
class: format!( if task.deadline().is_some() {
"px-8 pt-5 {} flex flex-row gap-4 select-none {}", "pb-0.5"
if task.deadline().is_some() { } else if let Category::Calendar { time, .. } = task.category() {
if time.is_some() {
"pb-0.5" "pb-0.5"
} else if let Category::Calendar { time, .. } = task.category() {
if time.is_some() {
"pb-0.5"
} else {
"pb-5"
}
} else { } else {
"pb-5" "pb-5"
}, }
if task_being_edited().is_some_and(|t| t.id() == task.id()) { } else {
"bg-zinc-700" "pb-5"
} else { "" } },
if task_being_edited().is_some_and(|t| t.id() == task.id()) {
"bg-zinc-700"
} else { "" }
),
onclick: {
let task = task.clone();
move |_| task_being_edited.set(Some(task.clone()))
},
i {
class: format!(
"{} text-3xl text-zinc-500",
if *(task.category()) == Category::Done {
"fa solid fa-square-check"
} else {
"fa-regular fa-square"
}
), ),
onclick: move |_| task_being_edited.set(Some(task.clone())), onclick: {
i { let task = task.clone();
class: format!( move |event| {
"{} text-3xl text-zinc-500",
if *(task_clone.category()) == Category::Done {
"fa solid fa-square-check"
} else {
"fa-regular fa-square"
}
),
onclick: move |event| {
// To prevent editing the task. // To prevent editing the task.
event.stop_propagation(); event.stop_propagation();
let task = task_clone.clone(); let task = task.clone();
async move { async move {
let completed_task = complete_task(task.id()).await; let completed_task = complete_task(task.id()).await;
query_client.invalidate_queries(&[ let mut query_keys = vec![
QueryKey::Tasks, QueryKey::Tasks,
QueryKey::TasksInCategory( QueryKey::TasksInCategory(
completed_task.unwrap().category().clone() completed_task.unwrap().category().clone()
), )
]); ];
if let Category::Calendar { reoccurrence: Some(_), .. }
= task.category() {
query_keys.push(QueryKey::SubtasksOfTaskId(task.id()));
}
query_client.invalidate_queries(&query_keys);
} }
} }
}
},
div {
class: "flex flex-col",
div {
class: "mt-1 grow font-medium",
{task.title()}
}, },
div { div {
class: "flex flex-col", class: "flex flex-row gap-3",
div { if let Some(deadline) = task.deadline() {
class: "mt-1 grow font-medium", div {
{task.title()} class: "text-sm text-zinc-400",
}, i {
div { class: "fa-solid fa-bomb"
class: "flex flex-row gap-3", },
if let Some(deadline) = task.deadline() { {deadline.format(" %m. %d.").to_string()}
}
}
if let Category::Calendar { time, .. } = task.category() {
if let Some(calendar_time) = time {
div { div {
class: "text-sm text-zinc-400", class: "text-sm text-zinc-400",
i { i {
class: "fa-solid fa-bomb" class: "fa-solid fa-clock"
}, },
{deadline.format(" %m. %d.").to_string()} {calendar_time.time().format(" %k:%M").to_string()}
}
}
if let Category::Calendar { time, .. } = task.category() {
if let Some(calendar_time) = time {
div {
class: "text-sm text-zinc-400",
i {
class: "fa-solid fa-clock"
},
{calendar_time.time().format(" %k:%M").to_string()}
}
} }
} }
} }
} }
} }
} }
})} }
} }
} }
} }