feat: ability to manage subtasks #40
@ -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;
|
||||||
|
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::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 {
|
||||||
|
@ -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()}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
})}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user
Consider avoiding cloning the entire tasks vector.
Cloning the entire
tasks
vector might lead to performance issues, especially if the vector is large. Consider iterating over references if the task objects do not need to be owned within the loop.