feat: ability to edit a project #30

Merged
matous-volf merged 3 commits from feat/project-edit into main 2024-09-06 20:50:30 +00:00
10 changed files with 115 additions and 49 deletions

View File

@ -2,6 +2,7 @@ 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::route::Route; use crate::route::Route;
#[component] #[component]
@ -12,8 +13,10 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
let navigation_expanded = use_signal(|| false); let navigation_expanded = use_signal(|| false);
let current_route = use_route(); let current_route = use_route();
use_effect(use_reactive(&display_form, move |creating_task| { let mut project_being_edited = use_context::<Signal<Option<Project>>>();
if creating_task() {
use_effect(use_reactive(&display_form, move |display_form| {
if display_form() {
expanded.set(true); expanded.set(true);
} else { } else {
spawn(async move { spawn(async move {
@ -27,7 +30,7 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
rsx! { rsx! {
div { div {
class: format!( 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()) { match (display_form(), current_route, navigation_expanded()) {
(false, _, false) => "h-[64px]", (false, _, false) => "h-[64px]",
(false, _, true) => "h-[128px]", (false, _, true) => "h-[128px]",
@ -39,8 +42,10 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
match current_route { match current_route {
Route::ProjectsPage => rsx! { Route::ProjectsPage => rsx! {
ProjectForm { ProjectForm {
project: project_being_edited(),
on_successful_submit: move |_| { on_successful_submit: move |_| {
display_form.set(false); display_form.set(false);
project_being_edited.set(None);
} }
} }
}, },

View File

@ -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" }),
}
}
}
}

View 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" }),
}
}
}
}
coderabbitai[bot] commented 2024-09-06 18:34:38 +00:00 (Migrated from github.com)
Review

Approve the implementation but suggest adding documentation.

The implementation of the FormOpenButton component is approved as it effectively uses context and signals to manage and toggle the state of a project being edited. However, consider adding documentation to explain the component's functionality and its role within the project management system, especially how it interacts with other components and the state.

Would you like help with drafting the documentation for this component?

**Approve the implementation but suggest adding documentation.** The implementation of the `FormOpenButton` component is approved as it effectively uses context and signals to manage and toggle the state of a project being edited. However, consider adding documentation to explain the component's functionality and its role within the project management system, especially how it interacts with other components and the state. Would you like help with drafting the documentation for this component? <!-- This is an auto-generated comment by CodeRabbit -->

View File

@ -3,18 +3,26 @@ 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::create_task_button::CreateButton; use crate::components::form_open_button::FormOpenButton;
use crate::components::sticky_bottom::StickyBottom; use crate::components::sticky_bottom::StickyBottom;
use crate::models::project::Project;
#[component] #[component]
pub(crate) fn Layout() -> Element { 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! { rsx! {
Outlet::<Route> {} Outlet::<Route> {}
StickyBottom { StickyBottom {
CreateButton { FormOpenButton {
creating: display_form, opened: display_form,
} }
BottomPanel { BottomPanel {
display_form: display_form, display_form: display_form,

View File

@ -5,7 +5,7 @@ pub(crate) mod task_form;
pub(crate) mod task_list; pub(crate) mod task_list;
pub(crate) mod pages; pub(crate) mod pages;
pub(crate) mod navigation; pub(crate) mod navigation;
pub(crate) mod create_task_button; pub(crate) mod form_open_button;
pub(crate) mod bottom_panel; pub(crate) mod bottom_panel;
pub(crate) mod sticky_bottom; pub(crate) mod sticky_bottom;
pub(crate) mod category_input; pub(crate) mod category_input;

View File

@ -1,11 +1,13 @@
use dioxus::prelude::*; use dioxus::prelude::*;
use dioxus_query::prelude::QueryResult; use dioxus_query::prelude::QueryResult;
use crate::models::project::Project;
use crate::query::projects::use_projects_query; use crate::query::projects::use_projects_query;
use crate::query::QueryValue; use crate::query::QueryValue;
#[component] #[component]
pub(crate) fn ProjectsPage() -> Element { pub(crate) fn ProjectsPage() -> Element {
let projects_query = use_projects_query(); let projects_query = use_projects_query();
let mut project_being_edited = use_context::<Signal<Option<Project>>>();
rsx! { rsx! {
match projects_query.result().value() { match projects_query.result().value() {
@ -13,10 +15,18 @@ pub(crate) fn ProjectsPage() -> Element {
| QueryResult::Loading(Some(QueryValue::Projects(projects))) => rsx! { | QueryResult::Loading(Some(QueryValue::Projects(projects))) => rsx! {
div { div {
class: "flex flex-col", class: "flex flex-col",
for project in projects { for project in projects.clone() {
div { div {
key: "{project.id()}", 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()} {project.title()}
} }
} }

View File

@ -1,5 +1,5 @@
use crate::models::project::NewProject; use crate::models::project::{NewProject, Project};
use crate::server::projects::create_project; use crate::server::projects::{create_project, edit_project};
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::*;
@ -7,17 +7,28 @@ use dioxus_query::prelude::use_query_client;
use crate::query::{QueryErrors, QueryKey, QueryValue}; use crate::query::{QueryErrors, QueryKey, QueryValue};
#[component] #[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 query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>();
let project_for_submit = project.clone();
rsx! { rsx! {
form { form {
onsubmit: move |event| { onsubmit: move |event| {
let project_clone = 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 {
Some(project) => {
let _ = edit_project(project.id(), new_project).await;
}
None => {
let _ = create_project(new_project).await; let _ = create_project(new_project).await;
}
}
query_client.invalidate_queries(&[ query_client.invalidate_queries(&[
QueryKey::Projects QueryKey::Projects
]); ]);
@ -35,9 +46,10 @@ pub(crate) fn ProjectForm(on_successful_submit: EventHandler<()>) -> Element {
} }
} }
input { input {
r#type: "text",
name: "title", name: "title",
required: true, 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", class: "py-2 px-3 grow bg-zinc-800/50 rounded-lg",
id: "input_title" id: "input_title"
} }

View File

@ -4,7 +4,7 @@ use dioxus::prelude::*;
pub(crate) fn StickyBottom(children: Element) -> Element { pub(crate) fn StickyBottom(children: Element) -> Element {
rsx! { rsx! {
div { 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} {children}
} }
} }

View File

@ -6,12 +6,12 @@ use std::str::FromStr;
use validator::{ValidationErrors, ValidationErrorsKind}; use validator::{ValidationErrors, ValidationErrorsKind};
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub enum ProjectCreateError { pub enum ProjectError {
TitleLengthInvalid, TitleLengthInvalid,
Error(Error), Error(Error),
} }
impl From<ValidationErrors> for ErrorVec<ProjectCreateError> { impl From<ValidationErrors> for ErrorVec<ProjectError> {
fn from(validation_errors: ValidationErrors) -> Self { fn from(validation_errors: ValidationErrors) -> Self {
validation_errors.errors() validation_errors.errors()
.iter() .iter()
@ -21,31 +21,31 @@ impl From<ValidationErrors> for ErrorVec<ProjectCreateError> {
.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" => ProjectCreateError::TitleLengthInvalid, "title_length" => ProjectError::TitleLengthInvalid,
_ => panic!("Unexpected validation error code: `{code}`."), _ => panic!("Unexpected validation error code: `{code}`."),
}) })
.collect::<Vec<ProjectCreateError>>(), .collect::<Vec<ProjectError>>(),
_ => panic!("Unexpected validation error kind."), _ => panic!("Unexpected validation error kind."),
}, },
_ => panic!("Unexpected validation field name: `{field}`."), _ => panic!("Unexpected validation field name: `{field}`."),
}) })
.collect::<Vec<ProjectCreateError>>() .collect::<Vec<ProjectError>>()
.into() .into()
} }
} }
// Has to be implemented for Dioxus server functions. // 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 { 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 ProjectCreateError { impl FromStr for ProjectError {
type Err = (); type Err = ();
fn from_str(_: &str) -> Result<Self, Self::Err> { fn from_str(_: &str) -> Result<Self, Self::Err> {
Ok(ProjectCreateError::Error(Error::ServerInternal)) Ok(ProjectError::Error(Error::ServerInternal))
} }
} }

View File

@ -1,31 +1,31 @@
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::ProjectCreateError; use crate::errors::project_create_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::{QueryDsl, RunQueryDsl, SelectableHelper}; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper};
use dioxus::prelude::*; use dioxus::prelude::*;
use validator::Validate; use validator::Validate;
#[server] #[server]
pub(crate) async fn create_project(new_project: NewProject) pub(crate) async fn create_project(new_project: NewProject)
-> Result<Project, ServerFnError<ErrorVec<ProjectCreateError>>> { -> Result<Project, ServerFnError<ErrorVec<ProjectError>>> {
use crate::schema::projects; use crate::schema::projects;
new_project.validate() new_project.validate()
.map_err::<ErrorVec<ProjectCreateError>, _>(|errors| errors.into())?; .map_err::<ErrorVec<ProjectError>, _>(|errors| errors.into())?;
let mut connection = establish_database_connection() let mut connection = establish_database_connection()
.map_err::<ErrorVec<ProjectCreateError>, _>( .map_err::<ErrorVec<ProjectError>, _>(
|_| vec![ProjectCreateError::Error(Error::ServerInternal)].into() |_| vec![ProjectError::Error(Error::ServerInternal)].into()
)?; )?;
let new_project = diesel::insert_into(projects::table) let new_project = diesel::insert_into(projects::table)
.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<ProjectCreateError>, _>( .map_err::<ErrorVec<ProjectError>, _>(
|_| vec![ProjectCreateError::Error(Error::ServerInternal)].into() |_| vec![ProjectError::Error(Error::ServerInternal)].into()
)?; )?;
Ok(new_project) Ok(new_project)
@ -50,3 +50,28 @@ pub(crate) async fn get_projects()
Ok(results) 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)
}