feat: ability to edit a project #30
@ -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);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
@ -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::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,
|
||||||
|
@ -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;
|
||||||
|
@ -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()}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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"
|
||||||
}
|
}
|
||||||
|
@ -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}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
@ -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)
|
||||||
|
}
|
||||||
|
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?