feat: ability to edit a task #32
@@ -1,9 +1,10 @@
 | 
			
		||||
use dioxus::prelude::*;
 | 
			
		||||
use crate::components::navigation::Navigation;
 | 
			
		||||
use crate::components::project_form::ProjectForm;
 | 
			
		||||
use crate::components::task_form::TaskForm;
 | 
			
		||||
use crate::models::project::Project;
 | 
			
		||||
use crate::models::task::Task;
 | 
			
		||||
use crate::route::Route;
 | 
			
		||||
use dioxus::prelude::*;
 | 
			
		||||
 | 
			
		||||
#[component]
 | 
			
		||||
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 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| {
 | 
			
		||||
        if display_form() {
 | 
			
		||||
@@ -22,7 +24,11 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
 | 
			
		||||
            spawn(async move {
 | 
			
		||||
                // Necessary for a smooth – not instant – height transition.
 | 
			
		||||
                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! {
 | 
			
		||||
                        TaskForm {
 | 
			
		||||
                            task: task_being_edited(),
 | 
			
		||||
                            on_successful_submit: move |_| {
 | 
			
		||||
                                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 |_| {
 | 
			
		||||
                    selected_category.set(Category::Calendar {
 | 
			
		||||
                        date: NaiveDate::default(),
 | 
			
		||||
                        date: chrono::Local::now().date_naive(),
 | 
			
		||||
                        reoccurrence: None,
 | 
			
		||||
                        time: None,
 | 
			
		||||
                    });
 | 
			
		||||
 
 | 
			
		||||
@@ -1,9 +1,11 @@
 | 
			
		||||
use dioxus::prelude::*;
 | 
			
		||||
use crate::models::project::Project;
 | 
			
		||||
use crate::models::task::Task;
 | 
			
		||||
use dioxus::prelude::*;
 | 
			
		||||
 | 
			
		||||
#[component]
 | 
			
		||||
pub(crate) fn FormOpenButton(opened: Signal<bool>) -> Element {
 | 
			
		||||
    let mut project_being_edited = use_context::<Signal<Option<Project>>>();
 | 
			
		||||
    let mut task_being_edited = use_context::<Signal<Option<Task>>>();
 | 
			
		||||
 | 
			
		||||
    rsx! {
 | 
			
		||||
        button {
 | 
			
		||||
@@ -11,6 +13,7 @@ pub(crate) fn FormOpenButton(opened: Signal<bool>) -> Element {
 | 
			
		||||
            onclick: move |_| {
 | 
			
		||||
                if opened() {
 | 
			
		||||
                    project_being_edited.set(None);
 | 
			
		||||
                    task_being_edited.set(None);
 | 
			
		||||
                }
 | 
			
		||||
                opened.set(!opened());
 | 
			
		||||
| 
					
	
	
	
	
	
	
	
	 | 
			||||
            },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,11 +1,12 @@
 | 
			
		||||
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 dioxus::core_macro::rsx;
 | 
			
		||||
use dioxus::dioxus_core::Element;
 | 
			
		||||
use dioxus::prelude::*;
 | 
			
		||||
use crate::components::form_open_button::FormOpenButton;
 | 
			
		||||
use crate::components::sticky_bottom::StickyBottom;
 | 
			
		||||
use crate::models::project::Project;
 | 
			
		||||
 | 
			
		||||
#[component]
 | 
			
		||||
pub(crate) fn Layout() -> Element {
 | 
			
		||||
@@ -13,9 +14,12 @@ pub(crate) fn Layout() -> Element {
 | 
			
		||||
    let project_being_edited = use_context_provider::<Signal<Option<Project>>>(
 | 
			
		||||
        || Signal::new(None)
 | 
			
		||||
    );
 | 
			
		||||
    let task_being_edited = use_context_provider::<Signal<Option<Task>>>(
 | 
			
		||||
        || Signal::new(None)
 | 
			
		||||
    );
 | 
			
		||||
    
 | 
			
		||||
    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! {
 | 
			
		||||
 
 | 
			
		||||
@@ -20,13 +20,11 @@ pub(crate) fn ProjectsPage() -> Element {
 | 
			
		||||
                            key: "{project.id()}",
 | 
			
		||||
                            class: format!(
 | 
			
		||||
                                "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"
 | 
			
		||||
                                } else { "" }
 | 
			
		||||
                            ),
 | 
			
		||||
                            onclick: move |_| {
 | 
			
		||||
                                project_being_edited.set(Some(project.clone()));
 | 
			
		||||
                            },
 | 
			
		||||
                            onclick: move |_| project_being_edited.set(Some(project.clone())),
 | 
			
		||||
                            {project.title()}
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
 
 | 
			
		||||
@@ -10,24 +10,20 @@ use crate::query::{QueryErrors, QueryKey, QueryValue};
 | 
			
		||||
pub(crate) fn ProjectForm(project: Option<Project>, on_successful_submit: EventHandler<()>)
 | 
			
		||||
                          -> Element {
 | 
			
		||||
    let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>();
 | 
			
		||||
    
 | 
			
		||||
    let project_for_submit = project.clone();
 | 
			
		||||
 | 
			
		||||
    rsx! {
 | 
			
		||||
        form {
 | 
			
		||||
            onsubmit: move |event| {
 | 
			
		||||
                let project_clone = project_for_submit.clone();
 | 
			
		||||
                let project = project_for_submit.clone();
 | 
			
		||||
                async move {
 | 
			
		||||
                    let new_project = NewProject::new(
 | 
			
		||||
                        event.values().get("title").unwrap().as_value()
 | 
			
		||||
                    );
 | 
			
		||||
                    match project_clone {
 | 
			
		||||
                        Some(project) => {
 | 
			
		||||
                            let _ = edit_project(project.id(), new_project).await;
 | 
			
		||||
                        }
 | 
			
		||||
                        None => {
 | 
			
		||||
                            let _ = create_project(new_project).await;
 | 
			
		||||
                        }
 | 
			
		||||
                    if let Some(project) = project {
 | 
			
		||||
                        let _ = edit_project(project.id(), new_project).await;
 | 
			
		||||
                    } else {
 | 
			
		||||
                        let _ = create_project(new_project).await;
 | 
			
		||||
                    }
 | 
			
		||||
                    query_client.invalidate_queries(&[
 | 
			
		||||
                        QueryKey::Projects
 | 
			
		||||
 
 | 
			
		||||
@@ -2,15 +2,16 @@ use crate::components::category_input::CategoryInput;
 | 
			
		||||
use crate::components::reoccurrence_input::ReoccurrenceIntervalInput;
 | 
			
		||||
use crate::models::category::{CalendarTime, Category, Reoccurrence};
 | 
			
		||||
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::tasks::create_task;
 | 
			
		||||
use chrono::{Duration, NaiveDate};
 | 
			
		||||
use crate::server::tasks::{create_task, edit_task};
 | 
			
		||||
use chrono::{Duration};
 | 
			
		||||
use dioxus::core_macro::{component, rsx};
 | 
			
		||||
use dioxus::dioxus_core::Element;
 | 
			
		||||
use dioxus::prelude::*;
 | 
			
		||||
use dioxus_query::prelude::use_query_client;
 | 
			
		||||
use crate::query::{QueryErrors, QueryKey, QueryValue};
 | 
			
		||||
use crate::route::Route;
 | 
			
		||||
 | 
			
		||||
const REMINDER_OFFSETS: [Option<Duration>; 17] = [
 | 
			
		||||
    None,
 | 
			
		||||
@@ -33,31 +34,54 @@ const REMINDER_OFFSETS: [Option<Duration>; 17] = [
 | 
			
		||||
];
 | 
			
		||||
 | 
			
		||||
#[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 route = use_route::<Route>();
 | 
			
		||||
    let selected_category = use_signal(|| match route { 
 | 
			
		||||
        Route::CategorySomedayMaybePage => Category::SomedayMaybe,
 | 
			
		||||
        Route::CategoryWaitingForPage => Category::WaitingFor(String::new()),
 | 
			
		||||
        Route::CategoryNextStepsPage => Category::NextSteps,
 | 
			
		||||
        Route::CategoryCalendarPage | Route::CategoryTodayPage => Category::Calendar {
 | 
			
		||||
            date: NaiveDate::default(),
 | 
			
		||||
            reoccurrence: None,
 | 
			
		||||
            time: None,
 | 
			
		||||
        },
 | 
			
		||||
        Route::CategoryLongTermPage => Category::LongTerm,
 | 
			
		||||
        _ => Category::Inbox,
 | 
			
		||||
    });
 | 
			
		||||
    let category_calendar_reoccurrence_interval = use_signal(|| None);
 | 
			
		||||
    let mut category_calendar_has_time = use_signal(|| false);
 | 
			
		||||
    let mut category_calendar_reminder_offset_index = use_signal(|| REMINDER_OFFSETS.len() - 1);
 | 
			
		||||
    let selected_category = use_signal(|| if let Some(task) = &task {
 | 
			
		||||
        task.category().clone()
 | 
			
		||||
    } else {
 | 
			
		||||
        match route {
 | 
			
		||||
            Route::CategorySomedayMaybePage => Category::SomedayMaybe,
 | 
			
		||||
            Route::CategoryWaitingForPage => Category::WaitingFor(String::new()),
 | 
			
		||||
            Route::CategoryNextStepsPage => Category::NextSteps,
 | 
			
		||||
            Route::CategoryCalendarPage | Route::CategoryTodayPage => Category::Calendar {
 | 
			
		||||
                date: chrono::Local::now().date_naive(),
 | 
			
		||||
                reoccurrence: None,
 | 
			
		||||
                time: None,
 | 
			
		||||
            },
 | 
			
		||||
            Route::CategoryLongTermPage => Category::LongTerm,
 | 
			
		||||
            _ => Category::Inbox,
 | 
			
		||||
        }
 | 
			
		||||
    }
 | 
			
		||||
    );
 | 
			
		||||
    let category_calendar_reoccurrence_interval = use_signal(|| task.as_ref().and_then(|task|
 | 
			
		||||
        if let Category::Calendar { reoccurrence: Some(reoccurrence), .. } = task.category() {
 | 
			
		||||
| 
					
	
	
	
	
	
	
	
	 Refactor suggestion: Simplify function signature. The function  **Refactor suggestion: Simplify function signature.**
The function `TaskForm` now takes an optional `Task` parameter to handle both task creation and editing. Consider refactoring to separate concerns, potentially splitting this into two distinct components or functions for clarity and maintainability.
<!-- This is an auto-generated comment by CodeRabbit --> 
			
			
		 | 
			||||
            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 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(),
 | 
			
		||||
@@ -96,7 +120,11 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
 | 
			
		||||
                        event.values().get("project_id").unwrap()
 | 
			
		||||
                        .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(&[
 | 
			
		||||
                        QueryKey::Tasks, 
 | 
			
		||||
                        QueryKey::TasksInCategory(selected_category())
 | 
			
		||||
@@ -115,9 +143,10 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
 | 
			
		||||
                    },
 | 
			
		||||
                },
 | 
			
		||||
                input {
 | 
			
		||||
                    r#type: "text",
 | 
			
		||||
                    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"
 | 
			
		||||
                },
 | 
			
		||||
@@ -142,6 +171,9 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
 | 
			
		||||
                    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()}
 | 
			
		||||
                        }
 | 
			
		||||
                    }
 | 
			
		||||
@@ -157,8 +189,10 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
 | 
			
		||||
                    }
 | 
			
		||||
                },
 | 
			
		||||
                input {
 | 
			
		||||
                    r#type: "date",
 | 
			
		||||
                    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"
 | 
			
		||||
                }
 | 
			
		||||
@@ -177,7 +211,7 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
 | 
			
		||||
                }
 | 
			
		||||
            }
 | 
			
		||||
            match selected_category() {
 | 
			
		||||
                Category::WaitingFor(_) => rsx! {
 | 
			
		||||
                Category::WaitingFor(waiting_for) => rsx! {
 | 
			
		||||
                    div {
 | 
			
		||||
                        class: "flex flex-row items-center gap-3",
 | 
			
		||||
                        label {
 | 
			
		||||
@@ -188,15 +222,16 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
 | 
			
		||||
                            }
 | 
			
		||||
                        },
 | 
			
		||||
                        input {
 | 
			
		||||
                            r#type: "text",
 | 
			
		||||
                            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 { .. } => rsx! {
 | 
			
		||||
                Category::Calendar { date, reoccurrence, time } => rsx! {
 | 
			
		||||
                    div {
 | 
			
		||||
                        class: "flex flex-row items-center gap-3",
 | 
			
		||||
                        label {
 | 
			
		||||
@@ -212,13 +247,16 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
 | 
			
		||||
                                r#type: "date",
 | 
			
		||||
                                name: "category_calendar_date",
 | 
			
		||||
                                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",
 | 
			
		||||
                                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| {
 | 
			
		||||
@@ -248,9 +286,9 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
 | 
			
		||||
                                disabled: category_calendar_reoccurrence_interval().is_none(),
 | 
			
		||||
                                required: true,
 | 
			
		||||
                                min: 1,
 | 
			
		||||
                                initial_value:
 | 
			
		||||
                                if category_calendar_reoccurrence_interval().is_none() { "" }
 | 
			
		||||
                                else { "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"
 | 
			
		||||
                            }
 | 
			
		||||
@@ -271,7 +309,8 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
 | 
			
		||||
                                name: "category_calendar_reminder_offset_index",
 | 
			
		||||
                                min: 0,
 | 
			
		||||
                                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",
 | 
			
		||||
                                id: "category_calendar_has_reminder",
 | 
			
		||||
                                oninput: move |event| {
 | 
			
		||||
 
 | 
			
		||||
@@ -6,6 +6,8 @@ use dioxus::prelude::*;
 | 
			
		||||
 | 
			
		||||
#[component]
 | 
			
		||||
pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element {
 | 
			
		||||
    let mut task_being_edited = use_context::<Signal<Option<Task>>>();
 | 
			
		||||
 | 
			
		||||
    rsx! {
 | 
			
		||||
        div {
 | 
			
		||||
            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 {
 | 
			
		||||
                    key: "{task.id()}",
 | 
			
		||||
                    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() {
 | 
			
		||||
                            "pb-0.5"
 | 
			
		||||
                        } 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 {
 | 
			
		||||
                            "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 {
 | 
			
		||||
                        class: "fa-regular fa-square text-3xl text-zinc-600",
 | 
			
		||||
                    },
 | 
			
		||||
 
 | 
			
		||||
@@ -1,4 +1,4 @@
 | 
			
		||||
pub(crate) mod error;
 | 
			
		||||
pub(crate) mod error_vec;
 | 
			
		||||
pub(crate) mod project_create_error;
 | 
			
		||||
pub(crate) mod task_create_error;
 | 
			
		||||
pub(crate) mod project_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.
 | 
			
		||||
impl Display for ProjectError {
 | 
			
		||||
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
 | 
			
		||||
@@ -6,13 +6,13 @@ use std::str::FromStr;
 | 
			
		||||
use validator::{ValidationErrors, ValidationErrorsKind};
 | 
			
		||||
 | 
			
		||||
#[derive(Serialize, Deserialize, Debug)]
 | 
			
		||||
pub enum TaskCreateError {
 | 
			
		||||
pub enum TaskError {
 | 
			
		||||
    TitleLengthInvalid,
 | 
			
		||||
    ProjectNotFound,
 | 
			
		||||
    Error(Error),
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
impl From<ValidationErrors> for ErrorVec<TaskCreateError> {
 | 
			
		||||
impl From<ValidationErrors> for ErrorVec<TaskError> {
 | 
			
		||||
    fn from(validation_errors: ValidationErrors) -> Self {
 | 
			
		||||
        validation_errors.errors()
 | 
			
		||||
            .iter()
 | 
			
		||||
@@ -22,31 +22,49 @@ impl From<ValidationErrors> for ErrorVec<TaskCreateError> {
 | 
			
		||||
                        .iter()
 | 
			
		||||
                        .map(|validation_error| validation_error.code.as_ref())
 | 
			
		||||
                        .map(|code| match code {
 | 
			
		||||
                            "title_length" => TaskCreateError::TitleLengthInvalid,
 | 
			
		||||
                            "title_length" => TaskError::TitleLengthInvalid,
 | 
			
		||||
                            _ => panic!("Unexpected validation error code: `{code}`."),
 | 
			
		||||
                        })
 | 
			
		||||
                        .collect::<Vec<TaskCreateError>>(),
 | 
			
		||||
                        .collect::<Vec<TaskError>>(),
 | 
			
		||||
                    _ => panic!("Unexpected validation error kind."),
 | 
			
		||||
                },
 | 
			
		||||
                _ => panic!("Unexpected validation field name: `{field}`."),
 | 
			
		||||
            })
 | 
			
		||||
            .collect::<Vec<TaskCreateError>>()
 | 
			
		||||
            .collect::<Vec<TaskError>>()
 | 
			
		||||
            .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.
 | 
			
		||||
impl Display for TaskCreateError {
 | 
			
		||||
impl Display for TaskError {
 | 
			
		||||
    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 TaskCreateError {
 | 
			
		||||
impl FromStr for TaskError {
 | 
			
		||||
    type 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_vec::ErrorVec;
 | 
			
		||||
use crate::errors::project_create_error::ProjectError;
 | 
			
		||||
use crate::errors::project_error::ProjectError;
 | 
			
		||||
use crate::models::project::{NewProject, Project};
 | 
			
		||||
use crate::server::database_connection::establish_database_connection;
 | 
			
		||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper};
 | 
			
		||||
@@ -24,9 +24,7 @@ pub(crate) async fn create_project(new_project: NewProject)
 | 
			
		||||
        .values(&new_project)
 | 
			
		||||
        .returning(Project::as_returning())
 | 
			
		||||
        .get_result(&mut connection)
 | 
			
		||||
        .map_err::<ErrorVec<ProjectError>, _>(
 | 
			
		||||
            |_| vec![ProjectError::Error(Error::ServerInternal)].into()
 | 
			
		||||
        )?;
 | 
			
		||||
        .map_err::<ErrorVec<ProjectError>, _>(|error| vec![error.into()].into())?;
 | 
			
		||||
 | 
			
		||||
    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))
 | 
			
		||||
        .returning(Project::as_returning())
 | 
			
		||||
        .get_result(&mut connection)
 | 
			
		||||
        .map_err::<ErrorVec<ProjectError>, _>(
 | 
			
		||||
            |_| vec![ProjectError::Error(Error::ServerInternal)].into()
 | 
			
		||||
        )?;
 | 
			
		||||
        .map_err::<ErrorVec<ProjectError>, _>(|error| vec![error.into()].into())?;
 | 
			
		||||
 | 
			
		||||
    Ok(updated_project)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
@@ -2,52 +2,37 @@ use crate::errors::error::Error;
 | 
			
		||||
use crate::errors::error_vec::ErrorVec;
 | 
			
		||||
use crate::models::task::{NewTask, Task};
 | 
			
		||||
use crate::server::database_connection::establish_database_connection;
 | 
			
		||||
use diesel::{QueryDsl, RunQueryDsl, SelectableHelper};
 | 
			
		||||
use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper};
 | 
			
		||||
use dioxus::prelude::*;
 | 
			
		||||
use validator::Validate;
 | 
			
		||||
use crate::errors::task_create_error::TaskCreateError;
 | 
			
		||||
use crate::errors::task_error::TaskError;
 | 
			
		||||
use crate::models::category::Category;
 | 
			
		||||
 | 
			
		||||
#[server]
 | 
			
		||||
pub(crate) async fn create_task(new_task: NewTask)
 | 
			
		||||
                                -> Result<Task, ServerFnError<ErrorVec<TaskCreateError>>> {
 | 
			
		||||
                                -> Result<Task, ServerFnError<ErrorVec<TaskError>>> {
 | 
			
		||||
    use crate::schema::tasks;
 | 
			
		||||
 | 
			
		||||
    new_task.validate()
 | 
			
		||||
        .map_err::<ErrorVec<TaskCreateError>, _>(|errors| errors.into())?;
 | 
			
		||||
        .map_err::<ErrorVec<TaskError>, _>(|errors| errors.into())?;
 | 
			
		||||
 | 
			
		||||
    let mut connection = establish_database_connection()
 | 
			
		||||
        .map_err::<ErrorVec<TaskCreateError>, _>(
 | 
			
		||||
            |_| vec![TaskCreateError::Error(Error::ServerInternal)].into()
 | 
			
		||||
        .map_err::<ErrorVec<TaskError>, _>(
 | 
			
		||||
            |_| vec![TaskError::Error(Error::ServerInternal)].into()
 | 
			
		||||
        )?;
 | 
			
		||||
 | 
			
		||||
    let new_task = diesel::insert_into(tasks::table)
 | 
			
		||||
        .values(&new_task)
 | 
			
		||||
        .returning(Task::as_returning())
 | 
			
		||||
        .get_result(&mut connection)
 | 
			
		||||
        .map_err::<ErrorVec<TaskCreateError>, _>(|error| {
 | 
			
		||||
            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()
 | 
			
		||||
        })?;
 | 
			
		||||
        .map_err::<ErrorVec<TaskError>, _>(|error| vec![error.into()].into())?;
 | 
			
		||||
 | 
			
		||||
    Ok(new_task)
 | 
			
		||||
}
 | 
			
		||||
 | 
			
		||||
#[server]
 | 
			
		||||
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::*;
 | 
			
		||||
 | 
			
		||||
    let mut connection = establish_database_connection()
 | 
			
		||||
@@ -65,3 +50,31 @@ pub(crate) async fn get_tasks_in_category(filtered_category: Category)
 | 
			
		||||
 | 
			
		||||
    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)
 | 
			
		||||
}
 | 
			
		||||
 
 | 
			
		||||
		Reference in New Issue
	
	Block a user
	
Enhanced State Management in
FormOpenButtonThe addition of
task_being_editedalongsideproject_being_editedeffectively allows the component to manage the state of both projects and tasks. The updatedonclickhandler correctly resets both states, ensuring a comprehensive reset of the component's state when toggled.Consider adding comments to clarify the purpose of each state management line, especially for new developers or for future maintenance.