feat: ability to edit a project #30
@ -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)
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user
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?