feat: ability to edit a task (#32)
This commit is contained in:
commit
58ed449252
@ -1,9 +1,10 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
use crate::components::navigation::Navigation;
|
use crate::components::navigation::Navigation;
|
||||||
use crate::components::project_form::ProjectForm;
|
use crate::components::project_form::ProjectForm;
|
||||||
use crate::components::task_form::TaskForm;
|
use crate::components::task_form::TaskForm;
|
||||||
use crate::models::project::Project;
|
use crate::models::project::Project;
|
||||||
|
use crate::models::task::Task;
|
||||||
use crate::route::Route;
|
use crate::route::Route;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
|
pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
|
||||||
@ -14,6 +15,7 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
|
|||||||
let current_route = use_route();
|
let current_route = use_route();
|
||||||
|
|
||||||
let mut project_being_edited = use_context::<Signal<Option<Project>>>();
|
let mut project_being_edited = use_context::<Signal<Option<Project>>>();
|
||||||
|
let mut task_being_edited = use_context::<Signal<Option<Task>>>();
|
||||||
|
|
||||||
use_effect(use_reactive(&display_form, move |display_form| {
|
use_effect(use_reactive(&display_form, move |display_form| {
|
||||||
if display_form() {
|
if display_form() {
|
||||||
@ -22,7 +24,11 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
|
|||||||
spawn(async move {
|
spawn(async move {
|
||||||
// Necessary for a smooth – not instant – height transition.
|
// Necessary for a smooth – not instant – height transition.
|
||||||
async_std::task::sleep(std::time::Duration::from_millis(500)).await;
|
async_std::task::sleep(std::time::Duration::from_millis(500)).await;
|
||||||
expanded.set(false);
|
/* The check is necessary for the situation when the user expands the panel while
|
||||||
|
it is being closed. */
|
||||||
|
if !display_form() {
|
||||||
|
expanded.set(false);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}));
|
}));
|
||||||
@ -51,8 +57,10 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
|
|||||||
},
|
},
|
||||||
_ => rsx! {
|
_ => rsx! {
|
||||||
TaskForm {
|
TaskForm {
|
||||||
|
task: task_being_edited(),
|
||||||
on_successful_submit: move |_| {
|
on_successful_submit: move |_| {
|
||||||
display_form.set(false);
|
display_form.set(false);
|
||||||
|
task_being_edited.set(None);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -74,7 +74,7 @@ pub(crate) fn CategoryInput(selected_category: Signal<Category>, class: Option<&
|
|||||||
),
|
),
|
||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
selected_category.set(Category::Calendar {
|
selected_category.set(Category::Calendar {
|
||||||
date: NaiveDate::default(),
|
date: chrono::Local::now().date_naive(),
|
||||||
reoccurrence: None,
|
reoccurrence: None,
|
||||||
time: None,
|
time: None,
|
||||||
});
|
});
|
||||||
|
@ -1,9 +1,11 @@
|
|||||||
use dioxus::prelude::*;
|
|
||||||
use crate::models::project::Project;
|
use crate::models::project::Project;
|
||||||
|
use crate::models::task::Task;
|
||||||
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub(crate) fn FormOpenButton(opened: Signal<bool>) -> Element {
|
pub(crate) fn FormOpenButton(opened: Signal<bool>) -> Element {
|
||||||
let mut project_being_edited = use_context::<Signal<Option<Project>>>();
|
let mut project_being_edited = use_context::<Signal<Option<Project>>>();
|
||||||
|
let mut task_being_edited = use_context::<Signal<Option<Task>>>();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
button {
|
button {
|
||||||
@ -11,6 +13,7 @@ pub(crate) fn FormOpenButton(opened: Signal<bool>) -> Element {
|
|||||||
onclick: move |_| {
|
onclick: move |_| {
|
||||||
if opened() {
|
if opened() {
|
||||||
project_being_edited.set(None);
|
project_being_edited.set(None);
|
||||||
|
task_being_edited.set(None);
|
||||||
}
|
}
|
||||||
opened.set(!opened());
|
opened.set(!opened());
|
||||||
},
|
},
|
||||||
|
@ -1,11 +1,12 @@
|
|||||||
use crate::components::bottom_panel::BottomPanel;
|
use crate::components::bottom_panel::BottomPanel;
|
||||||
|
use crate::components::form_open_button::FormOpenButton;
|
||||||
|
use crate::components::sticky_bottom::StickyBottom;
|
||||||
|
use crate::models::project::Project;
|
||||||
|
use crate::models::task::Task;
|
||||||
use crate::route::Route;
|
use crate::route::Route;
|
||||||
use dioxus::core_macro::rsx;
|
use dioxus::core_macro::rsx;
|
||||||
use dioxus::dioxus_core::Element;
|
use dioxus::dioxus_core::Element;
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use crate::components::form_open_button::FormOpenButton;
|
|
||||||
use crate::components::sticky_bottom::StickyBottom;
|
|
||||||
use crate::models::project::Project;
|
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub(crate) fn Layout() -> Element {
|
pub(crate) fn Layout() -> Element {
|
||||||
@ -13,9 +14,12 @@ pub(crate) fn Layout() -> Element {
|
|||||||
let project_being_edited = use_context_provider::<Signal<Option<Project>>>(
|
let project_being_edited = use_context_provider::<Signal<Option<Project>>>(
|
||||||
|| Signal::new(None)
|
|| Signal::new(None)
|
||||||
);
|
);
|
||||||
|
let task_being_edited = use_context_provider::<Signal<Option<Task>>>(
|
||||||
|
|| Signal::new(None)
|
||||||
|
);
|
||||||
|
|
||||||
use_effect(move || {
|
use_effect(move || {
|
||||||
display_form.set(project_being_edited().is_some());
|
display_form.set(project_being_edited().is_some() || task_being_edited().is_some());
|
||||||
});
|
});
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
|
@ -20,13 +20,11 @@ pub(crate) fn ProjectsPage() -> Element {
|
|||||||
key: "{project.id()}",
|
key: "{project.id()}",
|
||||||
class: format!(
|
class: format!(
|
||||||
"px-8 py-4 select-none {}",
|
"px-8 py-4 select-none {}",
|
||||||
if project_being_edited().map(|p| p.id()) == Some(project.id()) {
|
if project_being_edited().is_some_and(|p| p.id() == project.id()) {
|
||||||
"bg-zinc-700"
|
"bg-zinc-700"
|
||||||
} else { "" }
|
} else { "" }
|
||||||
),
|
),
|
||||||
onclick: move |_| {
|
onclick: move |_| project_being_edited.set(Some(project.clone())),
|
||||||
project_being_edited.set(Some(project.clone()));
|
|
||||||
},
|
|
||||||
{project.title()}
|
{project.title()}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -10,24 +10,20 @@ use crate::query::{QueryErrors, QueryKey, QueryValue};
|
|||||||
pub(crate) fn ProjectForm(project: Option<Project>, on_successful_submit: EventHandler<()>)
|
pub(crate) fn ProjectForm(project: Option<Project>, on_successful_submit: EventHandler<()>)
|
||||||
-> Element {
|
-> Element {
|
||||||
let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>();
|
let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>();
|
||||||
|
|
||||||
let project_for_submit = project.clone();
|
let project_for_submit = project.clone();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
form {
|
form {
|
||||||
onsubmit: move |event| {
|
onsubmit: move |event| {
|
||||||
let project_clone = project_for_submit.clone();
|
let project = project_for_submit.clone();
|
||||||
async move {
|
async move {
|
||||||
let new_project = NewProject::new(
|
let new_project = NewProject::new(
|
||||||
event.values().get("title").unwrap().as_value()
|
event.values().get("title").unwrap().as_value()
|
||||||
);
|
);
|
||||||
match project_clone {
|
if let Some(project) = project {
|
||||||
Some(project) => {
|
let _ = edit_project(project.id(), new_project).await;
|
||||||
let _ = edit_project(project.id(), new_project).await;
|
} else {
|
||||||
}
|
let _ = create_project(new_project).await;
|
||||||
None => {
|
|
||||||
let _ = create_project(new_project).await;
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
query_client.invalidate_queries(&[
|
query_client.invalidate_queries(&[
|
||||||
QueryKey::Projects
|
QueryKey::Projects
|
||||||
|
@ -2,15 +2,16 @@ use crate::components::category_input::CategoryInput;
|
|||||||
use crate::components::reoccurrence_input::ReoccurrenceIntervalInput;
|
use crate::components::reoccurrence_input::ReoccurrenceIntervalInput;
|
||||||
use crate::models::category::{CalendarTime, Category, Reoccurrence};
|
use crate::models::category::{CalendarTime, Category, Reoccurrence};
|
||||||
use crate::models::task::NewTask;
|
use crate::models::task::NewTask;
|
||||||
|
use crate::models::task::Task;
|
||||||
|
use crate::query::{QueryErrors, QueryKey, QueryValue};
|
||||||
|
use crate::route::Route;
|
||||||
use crate::server::projects::get_projects;
|
use crate::server::projects::get_projects;
|
||||||
use crate::server::tasks::create_task;
|
use crate::server::tasks::{create_task, edit_task};
|
||||||
use chrono::{Duration, NaiveDate};
|
use chrono::{Duration};
|
||||||
use dioxus::core_macro::{component, rsx};
|
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::query::{QueryErrors, QueryKey, QueryValue};
|
|
||||||
use crate::route::Route;
|
|
||||||
|
|
||||||
const REMINDER_OFFSETS: [Option<Duration>; 17] = [
|
const REMINDER_OFFSETS: [Option<Duration>; 17] = [
|
||||||
None,
|
None,
|
||||||
@ -33,31 +34,54 @@ const REMINDER_OFFSETS: [Option<Duration>; 17] = [
|
|||||||
];
|
];
|
||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
|
pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()>) -> Element {
|
||||||
let projects = use_server_future(get_projects)?.unwrap().unwrap();
|
let projects = use_server_future(get_projects)?.unwrap().unwrap();
|
||||||
|
|
||||||
let route = use_route::<Route>();
|
let route = use_route::<Route>();
|
||||||
let selected_category = use_signal(|| match route {
|
let selected_category = use_signal(|| if let Some(task) = &task {
|
||||||
Route::CategorySomedayMaybePage => Category::SomedayMaybe,
|
task.category().clone()
|
||||||
Route::CategoryWaitingForPage => Category::WaitingFor(String::new()),
|
} else {
|
||||||
Route::CategoryNextStepsPage => Category::NextSteps,
|
match route {
|
||||||
Route::CategoryCalendarPage | Route::CategoryTodayPage => Category::Calendar {
|
Route::CategorySomedayMaybePage => Category::SomedayMaybe,
|
||||||
date: NaiveDate::default(),
|
Route::CategoryWaitingForPage => Category::WaitingFor(String::new()),
|
||||||
reoccurrence: None,
|
Route::CategoryNextStepsPage => Category::NextSteps,
|
||||||
time: None,
|
Route::CategoryCalendarPage | Route::CategoryTodayPage => Category::Calendar {
|
||||||
},
|
date: chrono::Local::now().date_naive(),
|
||||||
Route::CategoryLongTermPage => Category::LongTerm,
|
reoccurrence: None,
|
||||||
_ => Category::Inbox,
|
time: None,
|
||||||
});
|
},
|
||||||
let category_calendar_reoccurrence_interval = use_signal(|| None);
|
Route::CategoryLongTermPage => Category::LongTerm,
|
||||||
let mut category_calendar_has_time = use_signal(|| false);
|
_ => Category::Inbox,
|
||||||
let mut category_calendar_reminder_offset_index = use_signal(|| REMINDER_OFFSETS.len() - 1);
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
|
let category_calendar_reoccurrence_interval = use_signal(|| task.as_ref().and_then(|task|
|
||||||
|
if let Category::Calendar { reoccurrence: Some(reoccurrence), .. } = task.category() {
|
||||||
|
Some(reoccurrence.interval().clone())
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
));
|
||||||
|
let mut category_calendar_has_time = use_signal(|| task.as_ref().is_some_and(
|
||||||
|
|task| matches!(*task.category(), Category::Calendar { time: Some(_), .. }))
|
||||||
|
);
|
||||||
|
let mut category_calendar_reminder_offset_index = use_signal(|| task.as_ref().and_then(|task|
|
||||||
|
if let Category::Calendar { time: Some(time), .. } = task.category() {
|
||||||
|
REMINDER_OFFSETS.iter().position(|&reminder_offset|
|
||||||
|
reminder_offset == time.reminder_offset()
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
None
|
||||||
|
}
|
||||||
|
).unwrap_or(REMINDER_OFFSETS.len() - 1));
|
||||||
|
|
||||||
let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>();
|
let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>();
|
||||||
|
let task_for_submit = task.clone();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
form {
|
form {
|
||||||
onsubmit: move |event| {
|
onsubmit: move |event| {
|
||||||
|
let task = task_for_submit.clone();
|
||||||
async move {
|
async move {
|
||||||
let new_task = NewTask::new(
|
let new_task = NewTask::new(
|
||||||
event.values().get("title").unwrap().as_value(),
|
event.values().get("title").unwrap().as_value(),
|
||||||
@ -96,7 +120,11 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
|
|||||||
event.values().get("project_id").unwrap()
|
event.values().get("project_id").unwrap()
|
||||||
.as_value().parse::<i32>().ok().filter(|&id| id > 0),
|
.as_value().parse::<i32>().ok().filter(|&id| id > 0),
|
||||||
);
|
);
|
||||||
let _ = create_task(new_task).await;
|
if let Some(task) = task {
|
||||||
|
let _ = edit_task(task.id(), new_task).await;
|
||||||
|
} else {
|
||||||
|
let _ = create_task(new_task).await;
|
||||||
|
}
|
||||||
query_client.invalidate_queries(&[
|
query_client.invalidate_queries(&[
|
||||||
QueryKey::Tasks,
|
QueryKey::Tasks,
|
||||||
QueryKey::TasksInCategory(selected_category())
|
QueryKey::TasksInCategory(selected_category())
|
||||||
@ -115,9 +143,10 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
|
|||||||
},
|
},
|
||||||
},
|
},
|
||||||
input {
|
input {
|
||||||
r#type: "text",
|
|
||||||
name: "title",
|
name: "title",
|
||||||
required: true,
|
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",
|
class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg",
|
||||||
id: "input_title"
|
id: "input_title"
|
||||||
},
|
},
|
||||||
@ -142,6 +171,9 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
|
|||||||
for project in projects {
|
for project in projects {
|
||||||
option {
|
option {
|
||||||
value: project.id().to_string(),
|
value: project.id().to_string(),
|
||||||
|
selected: task.as_ref().is_some_and(
|
||||||
|
|task| task.project_id() == Some(project.id())
|
||||||
|
),
|
||||||
{project.title()}
|
{project.title()}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -157,8 +189,10 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
input {
|
input {
|
||||||
r#type: "date",
|
|
||||||
name: "deadline",
|
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",
|
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow basis-0",
|
||||||
id: "input_deadline"
|
id: "input_deadline"
|
||||||
}
|
}
|
||||||
@ -177,7 +211,7 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
match selected_category() {
|
match selected_category() {
|
||||||
Category::WaitingFor(_) => rsx! {
|
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 {
|
||||||
@ -188,15 +222,16 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
input {
|
input {
|
||||||
r#type: "text",
|
|
||||||
name: "category_waiting_for",
|
name: "category_waiting_for",
|
||||||
required: true,
|
required: true,
|
||||||
|
initial_value: waiting_for,
|
||||||
|
r#type: "text",
|
||||||
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
|
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
|
||||||
id: "input_category_waiting_for"
|
id: "input_category_waiting_for"
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
Category::Calendar { .. } => rsx! {
|
Category::Calendar { date, reoccurrence, time } => rsx! {
|
||||||
div {
|
div {
|
||||||
class: "flex flex-row items-center gap-3",
|
class: "flex flex-row items-center gap-3",
|
||||||
label {
|
label {
|
||||||
@ -212,13 +247,16 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
|
|||||||
r#type: "date",
|
r#type: "date",
|
||||||
name: "category_calendar_date",
|
name: "category_calendar_date",
|
||||||
required: true,
|
required: true,
|
||||||
initial_value: chrono::Local::now().format("%Y-%m-%d").to_string(),
|
initial_value: date.format("%Y-%m-%d").to_string(),
|
||||||
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
|
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
|
||||||
id: "input_category_calendar_date"
|
id: "input_category_calendar_date"
|
||||||
},
|
},
|
||||||
input {
|
input {
|
||||||
r#type: "time",
|
r#type: "time",
|
||||||
name: "category_calendar_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",
|
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
|
||||||
id: "input_category_calendar_time",
|
id: "input_category_calendar_time",
|
||||||
oninput: move |event| {
|
oninput: move |event| {
|
||||||
@ -248,9 +286,9 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
|
|||||||
disabled: category_calendar_reoccurrence_interval().is_none(),
|
disabled: category_calendar_reoccurrence_interval().is_none(),
|
||||||
required: true,
|
required: true,
|
||||||
min: 1,
|
min: 1,
|
||||||
initial_value:
|
initial_value: category_calendar_reoccurrence_interval()
|
||||||
if category_calendar_reoccurrence_interval().is_none() { "" }
|
.map_or(String::new(), |_| reoccurrence.map_or(1, |reoccurrence|
|
||||||
else { "1" },
|
reoccurrence.length()).to_string()),
|
||||||
class: "py-2 px-3 bg-zinc-800/50 rounded-lg col-span-2 text-right",
|
class: "py-2 px-3 bg-zinc-800/50 rounded-lg col-span-2 text-right",
|
||||||
id: "category_calendar_reoccurrence_length"
|
id: "category_calendar_reoccurrence_length"
|
||||||
}
|
}
|
||||||
@ -271,7 +309,8 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
|
|||||||
name: "category_calendar_reminder_offset_index",
|
name: "category_calendar_reminder_offset_index",
|
||||||
min: 0,
|
min: 0,
|
||||||
max: REMINDER_OFFSETS.len() as i64 - 1,
|
max: REMINDER_OFFSETS.len() as i64 - 1,
|
||||||
initial_value: REMINDER_OFFSETS.len() as i64 - 1,
|
initial_value: category_calendar_reminder_offset_index()
|
||||||
|
.to_string(),
|
||||||
class: "grow input-range-reverse",
|
class: "grow input-range-reverse",
|
||||||
id: "category_calendar_has_reminder",
|
id: "category_calendar_has_reminder",
|
||||||
oninput: move |event| {
|
oninput: move |event| {
|
||||||
|
@ -6,6 +6,8 @@ use dioxus::prelude::*;
|
|||||||
|
|
||||||
#[component]
|
#[component]
|
||||||
pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element {
|
pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element {
|
||||||
|
let mut task_being_edited = use_context::<Signal<Option<Task>>>();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
div {
|
div {
|
||||||
class: format!("flex flex-col {}", class.unwrap_or("")),
|
class: format!("flex flex-col {}", class.unwrap_or("")),
|
||||||
@ -13,7 +15,7 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element
|
|||||||
div {
|
div {
|
||||||
key: "{task.id()}",
|
key: "{task.id()}",
|
||||||
class: format!(
|
class: format!(
|
||||||
"px-8 pt-5 {} flex flex-row gap-4",
|
"px-8 pt-5 {} flex flex-row gap-4 select-none {}",
|
||||||
if task.deadline().is_some() {
|
if task.deadline().is_some() {
|
||||||
"pb-0.5"
|
"pb-0.5"
|
||||||
} else if let Category::Calendar { time, .. } = task.category() {
|
} else if let Category::Calendar { time, .. } = task.category() {
|
||||||
@ -24,8 +26,12 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
"pb-5"
|
"pb-5"
|
||||||
}
|
},
|
||||||
|
if task_being_edited().is_some_and(|t| t.id() == task.id()) {
|
||||||
|
"bg-zinc-700"
|
||||||
|
} else { "" }
|
||||||
),
|
),
|
||||||
|
onclick: move |_| task_being_edited.set(Some(task.clone())),
|
||||||
i {
|
i {
|
||||||
class: "fa-regular fa-square text-3xl text-zinc-600",
|
class: "fa-regular fa-square text-3xl text-zinc-600",
|
||||||
},
|
},
|
||||||
|
@ -1,4 +1,4 @@
|
|||||||
pub(crate) mod error;
|
pub(crate) mod error;
|
||||||
pub(crate) mod error_vec;
|
pub(crate) mod error_vec;
|
||||||
pub(crate) mod project_create_error;
|
pub(crate) mod project_error;
|
||||||
pub(crate) mod task_create_error;
|
pub(crate) mod task_error;
|
||||||
|
@ -34,6 +34,12 @@ impl From<ValidationErrors> for ErrorVec<ProjectError> {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<diesel::result::Error> for ProjectError {
|
||||||
|
fn from(_: diesel::result::Error) -> Self {
|
||||||
|
ProjectError::Error(Error::ServerInternal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Has to be implemented for Dioxus server functions.
|
// Has to be implemented for Dioxus server functions.
|
||||||
impl Display for ProjectError {
|
impl Display for ProjectError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
@ -6,13 +6,13 @@ use std::str::FromStr;
|
|||||||
use validator::{ValidationErrors, ValidationErrorsKind};
|
use validator::{ValidationErrors, ValidationErrorsKind};
|
||||||
|
|
||||||
#[derive(Serialize, Deserialize, Debug)]
|
#[derive(Serialize, Deserialize, Debug)]
|
||||||
pub enum TaskCreateError {
|
pub enum TaskError {
|
||||||
TitleLengthInvalid,
|
TitleLengthInvalid,
|
||||||
ProjectNotFound,
|
ProjectNotFound,
|
||||||
Error(Error),
|
Error(Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
impl From<ValidationErrors> for ErrorVec<TaskCreateError> {
|
impl From<ValidationErrors> for ErrorVec<TaskError> {
|
||||||
fn from(validation_errors: ValidationErrors) -> Self {
|
fn from(validation_errors: ValidationErrors) -> Self {
|
||||||
validation_errors.errors()
|
validation_errors.errors()
|
||||||
.iter()
|
.iter()
|
||||||
@ -22,31 +22,49 @@ impl From<ValidationErrors> for ErrorVec<TaskCreateError> {
|
|||||||
.iter()
|
.iter()
|
||||||
.map(|validation_error| validation_error.code.as_ref())
|
.map(|validation_error| validation_error.code.as_ref())
|
||||||
.map(|code| match code {
|
.map(|code| match code {
|
||||||
"title_length" => TaskCreateError::TitleLengthInvalid,
|
"title_length" => TaskError::TitleLengthInvalid,
|
||||||
_ => panic!("Unexpected validation error code: `{code}`."),
|
_ => panic!("Unexpected validation error code: `{code}`."),
|
||||||
})
|
})
|
||||||
.collect::<Vec<TaskCreateError>>(),
|
.collect::<Vec<TaskError>>(),
|
||||||
_ => panic!("Unexpected validation error kind."),
|
_ => panic!("Unexpected validation error kind."),
|
||||||
},
|
},
|
||||||
_ => panic!("Unexpected validation field name: `{field}`."),
|
_ => panic!("Unexpected validation field name: `{field}`."),
|
||||||
})
|
})
|
||||||
.collect::<Vec<TaskCreateError>>()
|
.collect::<Vec<TaskError>>()
|
||||||
.into()
|
.into()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
impl From<diesel::result::Error> for TaskError {
|
||||||
|
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("tasks_project_id_fkey") => TaskError::ProjectNotFound,
|
||||||
|
_ => TaskError::Error(Error::ServerInternal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
_ => {
|
||||||
|
TaskError::Error(Error::ServerInternal)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// Has to be implemented for Dioxus server functions.
|
// Has to be implemented for Dioxus server functions.
|
||||||
impl Display for TaskCreateError {
|
impl Display for TaskError {
|
||||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||||
write!(f, "{:?}", self)
|
write!(f, "{:?}", self)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Has to be implemented for Dioxus server functions.
|
// Has to be implemented for Dioxus server functions.
|
||||||
impl FromStr for TaskCreateError {
|
impl FromStr for TaskError {
|
||||||
type Err = ();
|
type Err = ();
|
||||||
|
|
||||||
fn from_str(_: &str) -> Result<Self, Self::Err> {
|
fn from_str(_: &str) -> Result<Self, Self::Err> {
|
||||||
Ok(TaskCreateError::Error(Error::ServerInternal))
|
Ok(TaskError::Error(Error::ServerInternal))
|
||||||
}
|
}
|
||||||
}
|
}
|
@ -1,6 +1,6 @@
|
|||||||
use crate::errors::error::Error;
|
use crate::errors::error::Error;
|
||||||
use crate::errors::error_vec::ErrorVec;
|
use crate::errors::error_vec::ErrorVec;
|
||||||
use crate::errors::project_create_error::ProjectError;
|
use crate::errors::project_error::ProjectError;
|
||||||
use crate::models::project::{NewProject, Project};
|
use crate::models::project::{NewProject, Project};
|
||||||
use crate::server::database_connection::establish_database_connection;
|
use crate::server::database_connection::establish_database_connection;
|
||||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper};
|
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper};
|
||||||
@ -24,9 +24,7 @@ pub(crate) async fn create_project(new_project: NewProject)
|
|||||||
.values(&new_project)
|
.values(&new_project)
|
||||||
.returning(Project::as_returning())
|
.returning(Project::as_returning())
|
||||||
.get_result(&mut connection)
|
.get_result(&mut connection)
|
||||||
.map_err::<ErrorVec<ProjectError>, _>(
|
.map_err::<ErrorVec<ProjectError>, _>(|error| vec![error.into()].into())?;
|
||||||
|_| vec![ProjectError::Error(Error::ServerInternal)].into()
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(new_project)
|
Ok(new_project)
|
||||||
}
|
}
|
||||||
@ -69,9 +67,7 @@ pub(crate) async fn edit_project(project_id: i32, new_project: NewProject)
|
|||||||
.set(title.eq(new_project.title))
|
.set(title.eq(new_project.title))
|
||||||
.returning(Project::as_returning())
|
.returning(Project::as_returning())
|
||||||
.get_result(&mut connection)
|
.get_result(&mut connection)
|
||||||
.map_err::<ErrorVec<ProjectError>, _>(
|
.map_err::<ErrorVec<ProjectError>, _>(|error| vec![error.into()].into())?;
|
||||||
|_| vec![ProjectError::Error(Error::ServerInternal)].into()
|
|
||||||
)?;
|
|
||||||
|
|
||||||
Ok(updated_project)
|
Ok(updated_project)
|
||||||
}
|
}
|
||||||
|
@ -2,52 +2,37 @@ use crate::errors::error::Error;
|
|||||||
use crate::errors::error_vec::ErrorVec;
|
use crate::errors::error_vec::ErrorVec;
|
||||||
use crate::models::task::{NewTask, Task};
|
use crate::models::task::{NewTask, Task};
|
||||||
use crate::server::database_connection::establish_database_connection;
|
use crate::server::database_connection::establish_database_connection;
|
||||||
use diesel::{QueryDsl, RunQueryDsl, SelectableHelper};
|
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper};
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
use validator::Validate;
|
use validator::Validate;
|
||||||
use crate::errors::task_create_error::TaskCreateError;
|
use crate::errors::task_error::TaskError;
|
||||||
use crate::models::category::Category;
|
use crate::models::category::Category;
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
pub(crate) async fn create_task(new_task: NewTask)
|
pub(crate) async fn create_task(new_task: NewTask)
|
||||||
-> Result<Task, ServerFnError<ErrorVec<TaskCreateError>>> {
|
-> Result<Task, ServerFnError<ErrorVec<TaskError>>> {
|
||||||
use crate::schema::tasks;
|
use crate::schema::tasks;
|
||||||
|
|
||||||
new_task.validate()
|
new_task.validate()
|
||||||
.map_err::<ErrorVec<TaskCreateError>, _>(|errors| errors.into())?;
|
.map_err::<ErrorVec<TaskError>, _>(|errors| errors.into())?;
|
||||||
|
|
||||||
let mut connection = establish_database_connection()
|
let mut connection = establish_database_connection()
|
||||||
.map_err::<ErrorVec<TaskCreateError>, _>(
|
.map_err::<ErrorVec<TaskError>, _>(
|
||||||
|_| vec![TaskCreateError::Error(Error::ServerInternal)].into()
|
|_| vec![TaskError::Error(Error::ServerInternal)].into()
|
||||||
)?;
|
)?;
|
||||||
|
|
||||||
let new_task = diesel::insert_into(tasks::table)
|
let new_task = diesel::insert_into(tasks::table)
|
||||||
.values(&new_task)
|
.values(&new_task)
|
||||||
.returning(Task::as_returning())
|
.returning(Task::as_returning())
|
||||||
.get_result(&mut connection)
|
.get_result(&mut connection)
|
||||||
.map_err::<ErrorVec<TaskCreateError>, _>(|error| {
|
.map_err::<ErrorVec<TaskError>, _>(|error| vec![error.into()].into())?;
|
||||||
let error = match error {
|
|
||||||
diesel::result::Error::DatabaseError(
|
|
||||||
diesel::result::DatabaseErrorKind::ForeignKeyViolation, info
|
|
||||||
) => {
|
|
||||||
match info.constraint_name() {
|
|
||||||
Some("tasks_project_id_fkey") => TaskCreateError::ProjectNotFound,
|
|
||||||
_ => TaskCreateError::Error(Error::ServerInternal)
|
|
||||||
}
|
|
||||||
},
|
|
||||||
_ => {
|
|
||||||
TaskCreateError::Error(Error::ServerInternal)
|
|
||||||
}
|
|
||||||
};
|
|
||||||
vec![error].into()
|
|
||||||
})?;
|
|
||||||
|
|
||||||
Ok(new_task)
|
Ok(new_task)
|
||||||
}
|
}
|
||||||
|
|
||||||
#[server]
|
#[server]
|
||||||
pub(crate) async fn get_tasks_in_category(filtered_category: Category)
|
pub(crate) async fn get_tasks_in_category(filtered_category: Category)
|
||||||
-> Result<Vec<Task>, ServerFnError<ErrorVec<Error>>> {
|
-> Result<Vec<Task>, ServerFnError<ErrorVec<Error>>> {
|
||||||
use crate::schema::tasks::dsl::*;
|
use crate::schema::tasks::dsl::*;
|
||||||
|
|
||||||
let mut connection = establish_database_connection()
|
let mut connection = establish_database_connection()
|
||||||
@ -65,3 +50,31 @@ pub(crate) async fn get_tasks_in_category(filtered_category: Category)
|
|||||||
|
|
||||||
Ok(results)
|
Ok(results)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[server]
|
||||||
|
pub(crate) async fn edit_task(task_id: i32, new_task: NewTask)
|
||||||
|
-> Result<Task, ServerFnError<ErrorVec<TaskError>>> {
|
||||||
|
use crate::schema::tasks::dsl::*;
|
||||||
|
|
||||||
|
new_task.validate()
|
||||||
|
.map_err::<ErrorVec<TaskError>, _>(|errors| errors.into())?;
|
||||||
|
|
||||||
|
let mut connection = establish_database_connection()
|
||||||
|
.map_err::<ErrorVec<TaskError>, _>(
|
||||||
|
|_| vec![TaskError::Error(Error::ServerInternal)].into()
|
||||||
|
)?;
|
||||||
|
|
||||||
|
let updated_task = diesel::update(tasks)
|
||||||
|
.filter(id.eq(task_id))
|
||||||
|
.set((
|
||||||
|
title.eq(new_task.title),
|
||||||
|
deadline.eq(new_task.deadline),
|
||||||
|
category.eq(new_task.category),
|
||||||
|
project_id.eq(new_task.project_id),
|
||||||
|
))
|
||||||
|
.returning(Task::as_returning())
|
||||||
|
.get_result(&mut connection)
|
||||||
|
.map_err::<ErrorVec<TaskError>, _>(|error| vec![error.into()].into())?;
|
||||||
|
|
||||||
|
Ok(updated_task)
|
||||||
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user