feat: ability to edit a project (#30)
This commit is contained in:
		| @@ -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<bool>) -> 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::<Signal<Option<Project>>>(); | ||||
|  | ||||
|     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 { | ||||
| @@ -27,7 +30,7 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> 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]", | ||||
| @@ -39,8 +42,10 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> 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); | ||||
|                             } | ||||
|                         } | ||||
|                     }, | ||||
|   | ||||
| @@ -1,16 +0,0 @@ | ||||
| use dioxus::prelude::*; | ||||
|  | ||||
| #[component] | ||||
| pub(crate) fn CreateButton(creating: Signal<bool>) -> 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" }), | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
							
								
								
									
										22
									
								
								src/components/form_open_button.rs
									
									
									
									
									
										Normal file
									
								
							
							
						
						
									
										22
									
								
								src/components/form_open_button.rs
									
									
									
									
									
										Normal file
									
								
							| @@ -0,0 +1,22 @@ | ||||
| use dioxus::prelude::*; | ||||
| use crate::models::project::Project; | ||||
|  | ||||
| #[component] | ||||
| pub(crate) fn FormOpenButton(opened: Signal<bool>) -> Element { | ||||
|     let mut project_being_edited = use_context::<Signal<Option<Project>>>(); | ||||
|      | ||||
|     rsx! { | ||||
|         button { | ||||
|             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); | ||||
|                 } | ||||
|                 opened.set(!opened()); | ||||
|             }, | ||||
|             i { | ||||
|                 class: format!("min-w-6 fa-solid {}", if opened() { "fa-xmark" } else { "fa-plus" }), | ||||
|             } | ||||
|         } | ||||
|     } | ||||
| } | ||||
| @@ -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<Option<Project>>>( | ||||
|         || Signal::new(None) | ||||
|     ); | ||||
|      | ||||
|     use_effect(move || { | ||||
|         display_form.set(project_being_edited().is_some()); | ||||
|     }); | ||||
|      | ||||
|     rsx! { | ||||
|         Outlet::<Route> {} | ||||
|         StickyBottom { | ||||
|             CreateButton { | ||||
|                 creating: display_form, | ||||
|             FormOpenButton { | ||||
|                 opened: display_form, | ||||
|             } | ||||
|             BottomPanel { | ||||
|                 display_form: display_form, | ||||
|   | ||||
| @@ -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; | ||||
|   | ||||
| @@ -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::<Signal<Option<Project>>>(); | ||||
|  | ||||
|     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()} | ||||
|                         } | ||||
|                     } | ||||
|   | ||||
| @@ -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<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(); | ||||
|                 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" | ||||
|                 } | ||||
|   | ||||
| @@ -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} | ||||
|         } | ||||
|     } | ||||
|   | ||||
| @@ -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<ValidationErrors> for ErrorVec<ProjectCreateError> { | ||||
| impl From<ValidationErrors> for ErrorVec<ProjectError> { | ||||
|     fn from(validation_errors: ValidationErrors) -> Self { | ||||
|         validation_errors.errors() | ||||
|             .iter() | ||||
| @@ -21,31 +21,31 @@ impl From<ValidationErrors> for ErrorVec<ProjectCreateError> { | ||||
|                         .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::<Vec<ProjectCreateError>>(), | ||||
|                         .collect::<Vec<ProjectError>>(), | ||||
|                     _ => panic!("Unexpected validation error kind."), | ||||
|                 }, | ||||
|                 _ => panic!("Unexpected validation field name: `{field}`."), | ||||
|             }) | ||||
|             .collect::<Vec<ProjectCreateError>>() | ||||
|             .collect::<Vec<ProjectError>>() | ||||
|             .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<Self, Self::Err> { | ||||
|         Ok(ProjectCreateError::Error(Error::ServerInternal)) | ||||
|         Ok(ProjectError::Error(Error::ServerInternal)) | ||||
|     } | ||||
| } | ||||
|   | ||||
| @@ -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<Project, ServerFnError<ErrorVec<ProjectCreateError>>> { | ||||
|                                    -> Result<Project, ServerFnError<ErrorVec<ProjectError>>> { | ||||
|     use crate::schema::projects; | ||||
|  | ||||
|     new_project.validate() | ||||
|         .map_err::<ErrorVec<ProjectCreateError>, _>(|errors| errors.into())?; | ||||
|         .map_err::<ErrorVec<ProjectError>, _>(|errors| errors.into())?; | ||||
|  | ||||
|     let mut connection = establish_database_connection() | ||||
|         .map_err::<ErrorVec<ProjectCreateError>, _>( | ||||
|             |_| vec![ProjectCreateError::Error(Error::ServerInternal)].into() | ||||
|         .map_err::<ErrorVec<ProjectError>, _>( | ||||
|             |_| 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::<ErrorVec<ProjectCreateError>, _>( | ||||
|             |_| vec![ProjectCreateError::Error(Error::ServerInternal)].into() | ||||
|         .map_err::<ErrorVec<ProjectError>, _>( | ||||
|             |_| 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<Project, ServerFnError<ErrorVec<ProjectError>>> { | ||||
|     use crate::schema::projects::dsl::*; | ||||
|  | ||||
|     new_project.validate() | ||||
|         .map_err::<ErrorVec<ProjectError>, _>(|errors| errors.into())?; | ||||
|  | ||||
|     let mut connection = establish_database_connection() | ||||
|         .map_err::<ErrorVec<ProjectError>, _>( | ||||
|             |_| 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::<ErrorVec<ProjectError>, _>( | ||||
|             |_| vec![ProjectError::Error(Error::ServerInternal)].into() | ||||
|         )?; | ||||
|  | ||||
|     Ok(updated_project) | ||||
| } | ||||
|   | ||||
		Reference in New Issue
	
	Block a user