From 4cf0e58cde0e1d083ed8ad4d3a2669ca2a5fb16f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Volf?= Date: Fri, 6 Sep 2024 20:10:54 +0200 Subject: [PATCH 1/3] feat: add a server function for editing a project --- src/errors/project_create_error.rs | 16 ++++++------ src/server/projects.rs | 41 ++++++++++++++++++++++++------ 2 files changed, 41 insertions(+), 16 deletions(-) diff --git a/src/errors/project_create_error.rs b/src/errors/project_create_error.rs index c3dead5..8531ca4 100644 --- a/src/errors/project_create_error.rs +++ b/src/errors/project_create_error.rs @@ -6,12 +6,12 @@ use std::str::FromStr; use validator::{ValidationErrors, ValidationErrorsKind}; #[derive(Serialize, Deserialize, Debug)] -pub enum ProjectCreateError { +pub enum ProjectError { TitleLengthInvalid, Error(Error), } -impl From for ErrorVec { +impl From for ErrorVec { fn from(validation_errors: ValidationErrors) -> Self { validation_errors.errors() .iter() @@ -21,31 +21,31 @@ impl From for ErrorVec { .iter() .map(|validation_error| validation_error.code.as_ref()) .map(|code| match code { - "title_length" => ProjectCreateError::TitleLengthInvalid, + "title_length" => ProjectError::TitleLengthInvalid, _ => panic!("Unexpected validation error code: `{code}`."), }) - .collect::>(), + .collect::>(), _ => panic!("Unexpected validation error kind."), }, _ => panic!("Unexpected validation field name: `{field}`."), }) - .collect::>() + .collect::>() .into() } } // Has to be implemented for Dioxus server functions. -impl Display for ProjectCreateError { +impl Display for ProjectError { 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 ProjectCreateError { +impl FromStr for ProjectError { type Err = (); fn from_str(_: &str) -> Result { - Ok(ProjectCreateError::Error(Error::ServerInternal)) + Ok(ProjectError::Error(Error::ServerInternal)) } } diff --git a/src/server/projects.rs b/src/server/projects.rs index 9644cc8..0d6f623 100644 --- a/src/server/projects.rs +++ b/src/server/projects.rs @@ -1,31 +1,31 @@ use crate::errors::error::Error; use crate::errors::error_vec::ErrorVec; -use crate::errors::project_create_error::ProjectCreateError; +use crate::errors::project_create_error::ProjectError; use crate::models::project::{NewProject, Project}; 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; #[server] pub(crate) async fn create_project(new_project: NewProject) - -> Result>> { + -> Result>> { use crate::schema::projects; new_project.validate() - .map_err::, _>(|errors| errors.into())?; + .map_err::, _>(|errors| errors.into())?; let mut connection = establish_database_connection() - .map_err::, _>( - |_| vec![ProjectCreateError::Error(Error::ServerInternal)].into() + .map_err::, _>( + |_| vec![ProjectError::Error(Error::ServerInternal)].into() )?; let new_project = diesel::insert_into(projects::table) .values(&new_project) .returning(Project::as_returning()) .get_result(&mut connection) - .map_err::, _>( - |_| vec![ProjectCreateError::Error(Error::ServerInternal)].into() + .map_err::, _>( + |_| vec![ProjectError::Error(Error::ServerInternal)].into() )?; Ok(new_project) @@ -50,3 +50,28 @@ pub(crate) async fn get_projects() Ok(results) } + +#[server] +pub(crate) async fn edit_project(project_id: i32, new_project: NewProject) + -> Result>> { + use crate::schema::projects::dsl::*; + + new_project.validate() + .map_err::, _>(|errors| errors.into())?; + + let mut connection = establish_database_connection() + .map_err::, _>( + |_| vec![ProjectError::Error(Error::ServerInternal)].into() + )?; + + let updated_project = diesel::update(projects) + .filter(id.eq(project_id)) + .set(title.eq(new_project.title)) + .returning(Project::as_returning()) + .get_result(&mut connection) + .map_err::, _>( + |_| vec![ProjectError::Error(Error::ServerInternal)].into() + )?; + + Ok(updated_project) +} From 468742c53ea3e05ed4bd8d37ea1a61dbe14dc3eb Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Volf?= Date: Fri, 6 Sep 2024 20:22:36 +0200 Subject: [PATCH 2/3] feat: add the ability to edit a project upon clicking in the list --- src/components/bottom_panel.rs | 9 +++++++-- src/components/create_task_button.rs | 16 ---------------- src/components/form_open_button.rs | 22 ++++++++++++++++++++++ src/components/layout.rs | 16 ++++++++++++---- src/components/mod.rs | 2 +- src/components/pages/projects_page.rs | 16 +++++++++++++--- src/components/project_form.rs | 22 +++++++++++++++++----- 7 files changed, 72 insertions(+), 31 deletions(-) delete mode 100644 src/components/create_task_button.rs create mode 100644 src/components/form_open_button.rs diff --git a/src/components/bottom_panel.rs b/src/components/bottom_panel.rs index c90d542..51edb4d 100644 --- a/src/components/bottom_panel.rs +++ b/src/components/bottom_panel.rs @@ -2,6 +2,7 @@ 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::route::Route; #[component] @@ -11,9 +12,11 @@ pub(crate) fn BottomPanel(display_form: Signal) -> Element { let mut expanded = use_signal(|| display_form()); let navigation_expanded = use_signal(|| false); let current_route = use_route(); + + let mut project_being_edited = use_context::>>(); - use_effect(use_reactive(&display_form, move |creating_task| { - if creating_task() { + use_effect(use_reactive(&display_form, move |display_form| { + if display_form() { expanded.set(true); } else { spawn(async move { @@ -39,8 +42,10 @@ pub(crate) fn BottomPanel(display_form: Signal) -> Element { match current_route { Route::ProjectsPage => rsx! { ProjectForm { + project: project_being_edited(), on_successful_submit: move |_| { display_form.set(false); + project_being_edited.set(None); } } }, diff --git a/src/components/create_task_button.rs b/src/components/create_task_button.rs deleted file mode 100644 index 25d69a2..0000000 --- a/src/components/create_task_button.rs +++ /dev/null @@ -1,16 +0,0 @@ -use dioxus::prelude::*; - -#[component] -pub(crate) fn CreateButton(creating: Signal) -> Element { - rsx! { - button { - class: "m-4 py-3 px-5 self-end text-center bg-zinc-300/50 rounded-xl border-t-zinc-200 border-t backdrop-blur drop-shadow-[0_-5px_10px_rgba(0,0,0,0.2)] text-2xl text-zinc-200", - onclick: move |_| { - creating.set(!creating()); - }, - i { - class: format!("min-w-6 fa-solid {}", if creating() { "fa-xmark" } else { "fa-plus" }), - } - } - } -} diff --git a/src/components/form_open_button.rs b/src/components/form_open_button.rs new file mode 100644 index 0000000..c4935f0 --- /dev/null +++ b/src/components/form_open_button.rs @@ -0,0 +1,22 @@ +use dioxus::prelude::*; +use crate::models::project::Project; + +#[component] +pub(crate) fn FormOpenButton(opened: Signal) -> Element { + let mut project_being_edited = use_context::>>(); + + rsx! { + button { + class: "m-4 py-3 px-5 self-end text-center bg-zinc-300/50 rounded-xl border-t-zinc-200 border-t backdrop-blur drop-shadow-[0_-5px_10px_rgba(0,0,0,0.2)] text-2xl text-zinc-200", + onclick: move |_| { + if opened() { + project_being_edited.set(None); + } + opened.set(!opened()); + }, + i { + class: format!("min-w-6 fa-solid {}", if opened() { "fa-xmark" } else { "fa-plus" }), + } + } + } +} diff --git a/src/components/layout.rs b/src/components/layout.rs index ed89b5f..1e67361 100644 --- a/src/components/layout.rs +++ b/src/components/layout.rs @@ -3,18 +3,26 @@ use crate::route::Route; use dioxus::core_macro::rsx; use dioxus::dioxus_core::Element; use dioxus::prelude::*; -use crate::components::create_task_button::CreateButton; +use crate::components::form_open_button::FormOpenButton; use crate::components::sticky_bottom::StickyBottom; +use crate::models::project::Project; #[component] pub(crate) fn Layout() -> Element { - let display_form = use_signal(|| false); + let mut display_form = use_signal(|| false); + let project_being_edited = use_context_provider::>>( + || Signal::new(None) + ); + + use_effect(move || { + display_form.set(project_being_edited().is_some()); + }); rsx! { Outlet:: {} StickyBottom { - CreateButton { - creating: display_form, + FormOpenButton { + opened: display_form, } BottomPanel { display_form: display_form, diff --git a/src/components/mod.rs b/src/components/mod.rs index bd961d8..3dde58e 100644 --- a/src/components/mod.rs +++ b/src/components/mod.rs @@ -5,7 +5,7 @@ pub(crate) mod task_form; pub(crate) mod task_list; pub(crate) mod pages; pub(crate) mod navigation; -pub(crate) mod create_task_button; +pub(crate) mod form_open_button; pub(crate) mod bottom_panel; pub(crate) mod sticky_bottom; pub(crate) mod category_input; diff --git a/src/components/pages/projects_page.rs b/src/components/pages/projects_page.rs index fd7f34e..5b18b05 100644 --- a/src/components/pages/projects_page.rs +++ b/src/components/pages/projects_page.rs @@ -1,22 +1,32 @@ use dioxus::prelude::*; use dioxus_query::prelude::QueryResult; +use crate::models::project::Project; use crate::query::projects::use_projects_query; use crate::query::QueryValue; #[component] pub(crate) fn ProjectsPage() -> Element { let projects_query = use_projects_query(); - + let mut project_being_edited = use_context::>>(); + rsx! { match projects_query.result().value() { QueryResult::Ok(QueryValue::Projects(projects)) | QueryResult::Loading(Some(QueryValue::Projects(projects))) => rsx! { div { class: "flex flex-col", - for project in projects { + for project in projects.clone() { div { key: "{project.id()}", - class: "px-8 py-4", + class: format!( + "px-8 py-4 select-none {}", + if project_being_edited().map(|p| p.id()) == Some(project.id()) { + "bg-zinc-700" + } else { "" } + ), + onclick: move |_| { + project_being_edited.set(Some(project.clone())); + }, {project.title()} } } diff --git a/src/components/project_form.rs b/src/components/project_form.rs index fc7ad2f..5efb58b 100644 --- a/src/components/project_form.rs +++ b/src/components/project_form.rs @@ -1,5 +1,5 @@ -use crate::models::project::NewProject; -use crate::server::projects::create_project; +use crate::models::project::{NewProject, Project}; +use crate::server::projects::{create_project, edit_project}; use dioxus::core_macro::{component, rsx}; use dioxus::dioxus_core::Element; use dioxus::prelude::*; @@ -7,17 +7,28 @@ use dioxus_query::prelude::use_query_client; use crate::query::{QueryErrors, QueryKey, QueryValue}; #[component] -pub(crate) fn ProjectForm(on_successful_submit: EventHandler<()>) -> Element { +pub(crate) fn ProjectForm(project: Option, on_successful_submit: EventHandler<()>) + -> Element { let query_client = use_query_client::(); + let project_for_submit = project.clone(); + rsx! { form { onsubmit: move |event| { + let project_clone = project_for_submit.clone(); async move { let new_project = NewProject::new( event.values().get("title").unwrap().as_value() ); - let _ = create_project(new_project).await; + match project_clone { + Some(project) => { + let _ = edit_project(project.id(), new_project).await; + } + None => { + let _ = create_project(new_project).await; + } + } query_client.invalidate_queries(&[ QueryKey::Projects ]); @@ -35,9 +46,10 @@ pub(crate) fn ProjectForm(on_successful_submit: EventHandler<()>) -> Element { } } input { - r#type: "text", name: "title", required: true, + initial_value: project.map(|project| project.title().to_owned()), + r#type: "text", class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg", id: "input_title" } From efaf81b938673ee9a9d81c25bf7a000a80076d23 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Matou=C5=A1=20Volf?= Date: Fri, 6 Sep 2024 20:28:39 +0200 Subject: [PATCH 3/3] feat: allow clicking through the sticky bottom component --- src/components/bottom_panel.rs | 2 +- src/components/form_open_button.rs | 2 +- src/components/sticky_bottom.rs | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/components/bottom_panel.rs b/src/components/bottom_panel.rs index 51edb4d..29f9dbe 100644 --- a/src/components/bottom_panel.rs +++ b/src/components/bottom_panel.rs @@ -30,7 +30,7 @@ pub(crate) fn BottomPanel(display_form: Signal) -> Element { rsx! { div { class: format!( - "bg-zinc-700/50 rounded-t-xl border-t-zinc-600 border-t backdrop-blur drop-shadow-[0_-5px_10px_rgba(0,0,0,0.2)] transition-[height] duration-[500ms] ease-[cubic-bezier(0.79,0.14,0.15,0.86)] {}", + "pointer-events-auto bg-zinc-700/50 rounded-t-xl border-t-zinc-600 border-t backdrop-blur drop-shadow-[0_-5px_10px_rgba(0,0,0,0.2)] transition-[height] duration-[500ms] ease-[cubic-bezier(0.79,0.14,0.15,0.86)] {}", match (display_form(), current_route, navigation_expanded()) { (false, _, false) => "h-[64px]", (false, _, true) => "h-[128px]", diff --git a/src/components/form_open_button.rs b/src/components/form_open_button.rs index c4935f0..81c584a 100644 --- a/src/components/form_open_button.rs +++ b/src/components/form_open_button.rs @@ -7,7 +7,7 @@ pub(crate) fn FormOpenButton(opened: Signal) -> Element { rsx! { button { - class: "m-4 py-3 px-5 self-end text-center bg-zinc-300/50 rounded-xl border-t-zinc-200 border-t backdrop-blur drop-shadow-[0_-5px_10px_rgba(0,0,0,0.2)] text-2xl text-zinc-200", + class: "pointer-events-auto m-4 py-3 px-5 self-end text-center bg-zinc-300/50 rounded-xl border-t-zinc-200 border-t backdrop-blur drop-shadow-[0_-5px_10px_rgba(0,0,0,0.2)] text-2xl text-zinc-200", onclick: move |_| { if opened() { project_being_edited.set(None); diff --git a/src/components/sticky_bottom.rs b/src/components/sticky_bottom.rs index 972bc3f..043ba7d 100644 --- a/src/components/sticky_bottom.rs +++ b/src/components/sticky_bottom.rs @@ -4,7 +4,7 @@ use dioxus::prelude::*; pub(crate) fn StickyBottom(children: Element) -> Element { rsx! { div { - class: "fixed bottom-0 left-0 right-0 flex flex-col", + class: "fixed bottom-0 left-0 right-0 flex flex-col pointer-events-none", {children} } }