feat: ability to edit a task (#32)

This commit is contained in:
Matouš Volf 2024-09-07 10:26:42 +02:00 committed by GitHub
commit 58ed449252
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
13 changed files with 185 additions and 98 deletions

View File

@ -1,9 +1,10 @@
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::models::project::Project;
use crate::models::task::Task;
use crate::route::Route; use crate::route::Route;
use dioxus::prelude::*;
#[component] #[component]
pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element { pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
@ -14,6 +15,7 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
let current_route = use_route(); let current_route = use_route();
let mut project_being_edited = use_context::<Signal<Option<Project>>>(); let mut project_being_edited = use_context::<Signal<Option<Project>>>();
let mut task_being_edited = use_context::<Signal<Option<Task>>>();
use_effect(use_reactive(&display_form, move |display_form| { use_effect(use_reactive(&display_form, move |display_form| {
if display_form() { if display_form() {
@ -22,7 +24,11 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
spawn(async move { spawn(async move {
// Necessary for a smooth not instant height transition. // Necessary for a smooth not instant height transition.
async_std::task::sleep(std::time::Duration::from_millis(500)).await; async_std::task::sleep(std::time::Duration::from_millis(500)).await;
expanded.set(false); /* The check is necessary for the situation when the user expands the panel while
it is being closed. */
if !display_form() {
expanded.set(false);
}
}); });
} }
})); }));
@ -51,8 +57,10 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
}, },
_ => rsx! { _ => rsx! {
TaskForm { TaskForm {
task: task_being_edited(),
on_successful_submit: move |_| { on_successful_submit: move |_| {
display_form.set(false); display_form.set(false);
task_being_edited.set(None);
} }
} }
} }

View File

@ -74,7 +74,7 @@ pub(crate) fn CategoryInput(selected_category: Signal<Category>, class: Option<&
), ),
onclick: move |_| { onclick: move |_| {
selected_category.set(Category::Calendar { selected_category.set(Category::Calendar {
date: NaiveDate::default(), date: chrono::Local::now().date_naive(),
reoccurrence: None, reoccurrence: None,
time: None, time: None,
}); });

View File

@ -1,9 +1,11 @@
use dioxus::prelude::*;
use crate::models::project::Project; use crate::models::project::Project;
use crate::models::task::Task;
use dioxus::prelude::*;
#[component] #[component]
pub(crate) fn FormOpenButton(opened: Signal<bool>) -> Element { pub(crate) fn FormOpenButton(opened: Signal<bool>) -> Element {
let mut project_being_edited = use_context::<Signal<Option<Project>>>(); let mut project_being_edited = use_context::<Signal<Option<Project>>>();
let mut task_being_edited = use_context::<Signal<Option<Task>>>();
rsx! { rsx! {
button { button {
@ -11,6 +13,7 @@ pub(crate) fn FormOpenButton(opened: Signal<bool>) -> Element {
onclick: move |_| { onclick: move |_| {
if opened() { if opened() {
project_being_edited.set(None); project_being_edited.set(None);
task_being_edited.set(None);
} }
opened.set(!opened()); opened.set(!opened());
}, },

View File

@ -1,11 +1,12 @@
use crate::components::bottom_panel::BottomPanel; use crate::components::bottom_panel::BottomPanel;
use crate::components::form_open_button::FormOpenButton;
use crate::components::sticky_bottom::StickyBottom;
use crate::models::project::Project;
use crate::models::task::Task;
use crate::route::Route; 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::form_open_button::FormOpenButton;
use crate::components::sticky_bottom::StickyBottom;
use crate::models::project::Project;
#[component] #[component]
pub(crate) fn Layout() -> Element { pub(crate) fn Layout() -> Element {
@ -13,9 +14,12 @@ pub(crate) fn Layout() -> Element {
let project_being_edited = use_context_provider::<Signal<Option<Project>>>( let project_being_edited = use_context_provider::<Signal<Option<Project>>>(
|| Signal::new(None) || Signal::new(None)
); );
let task_being_edited = use_context_provider::<Signal<Option<Task>>>(
|| Signal::new(None)
);
use_effect(move || { use_effect(move || {
display_form.set(project_being_edited().is_some()); display_form.set(project_being_edited().is_some() || task_being_edited().is_some());
}); });
rsx! { rsx! {

View File

@ -20,13 +20,11 @@ pub(crate) fn ProjectsPage() -> Element {
key: "{project.id()}", key: "{project.id()}",
class: format!( class: format!(
"px-8 py-4 select-none {}", "px-8 py-4 select-none {}",
if project_being_edited().map(|p| p.id()) == Some(project.id()) { if project_being_edited().is_some_and(|p| p.id() == project.id()) {
"bg-zinc-700" "bg-zinc-700"
} else { "" } } else { "" }
), ),
onclick: move |_| { onclick: move |_| project_being_edited.set(Some(project.clone())),
project_being_edited.set(Some(project.clone()));
},
{project.title()} {project.title()}
} }
} }

View File

@ -10,24 +10,20 @@ use crate::query::{QueryErrors, QueryKey, QueryValue};
pub(crate) fn ProjectForm(project: Option<Project>, on_successful_submit: EventHandler<()>) pub(crate) fn ProjectForm(project: Option<Project>, on_successful_submit: EventHandler<()>)
-> Element { -> 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(); let project_for_submit = project.clone();
rsx! { rsx! {
form { form {
onsubmit: move |event| { onsubmit: move |event| {
let project_clone = project_for_submit.clone(); let project = 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 { if let Some(project) = project {
Some(project) => { let _ = edit_project(project.id(), new_project).await;
let _ = edit_project(project.id(), new_project).await; } else {
} let _ = create_project(new_project).await;
None => {
let _ = create_project(new_project).await;
}
} }
query_client.invalidate_queries(&[ query_client.invalidate_queries(&[
QueryKey::Projects QueryKey::Projects

View File

@ -2,15 +2,16 @@ use crate::components::category_input::CategoryInput;
use crate::components::reoccurrence_input::ReoccurrenceIntervalInput; use crate::components::reoccurrence_input::ReoccurrenceIntervalInput;
use crate::models::category::{CalendarTime, Category, Reoccurrence}; use crate::models::category::{CalendarTime, Category, Reoccurrence};
use crate::models::task::NewTask; use crate::models::task::NewTask;
use crate::models::task::Task;
use crate::query::{QueryErrors, QueryKey, QueryValue};
use crate::route::Route;
use crate::server::projects::get_projects; use crate::server::projects::get_projects;
use crate::server::tasks::create_task; use crate::server::tasks::{create_task, edit_task};
use chrono::{Duration, NaiveDate}; use chrono::{Duration};
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::*;
use dioxus_query::prelude::use_query_client; use dioxus_query::prelude::use_query_client;
use crate::query::{QueryErrors, QueryKey, QueryValue};
use crate::route::Route;
const REMINDER_OFFSETS: [Option<Duration>; 17] = [ const REMINDER_OFFSETS: [Option<Duration>; 17] = [
None, None,
@ -33,31 +34,54 @@ const REMINDER_OFFSETS: [Option<Duration>; 17] = [
]; ];
#[component] #[component]
pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element { pub(crate) fn TaskForm(task: Option<Task>, on_successful_submit: EventHandler<()>) -> Element {
let projects = use_server_future(get_projects)?.unwrap().unwrap(); let projects = use_server_future(get_projects)?.unwrap().unwrap();
let route = use_route::<Route>(); let route = use_route::<Route>();
let selected_category = use_signal(|| match route { let selected_category = use_signal(|| if let Some(task) = &task {
Route::CategorySomedayMaybePage => Category::SomedayMaybe, task.category().clone()
Route::CategoryWaitingForPage => Category::WaitingFor(String::new()), } else {
Route::CategoryNextStepsPage => Category::NextSteps, match route {
Route::CategoryCalendarPage | Route::CategoryTodayPage => Category::Calendar { Route::CategorySomedayMaybePage => Category::SomedayMaybe,
date: NaiveDate::default(), Route::CategoryWaitingForPage => Category::WaitingFor(String::new()),
reoccurrence: None, Route::CategoryNextStepsPage => Category::NextSteps,
time: None, Route::CategoryCalendarPage | Route::CategoryTodayPage => Category::Calendar {
}, date: chrono::Local::now().date_naive(),
Route::CategoryLongTermPage => Category::LongTerm, reoccurrence: None,
_ => Category::Inbox, time: None,
}); },
let category_calendar_reoccurrence_interval = use_signal(|| None); Route::CategoryLongTermPage => Category::LongTerm,
let mut category_calendar_has_time = use_signal(|| false); _ => Category::Inbox,
let mut category_calendar_reminder_offset_index = use_signal(|| REMINDER_OFFSETS.len() - 1); }
}
);
let category_calendar_reoccurrence_interval = use_signal(|| task.as_ref().and_then(|task|
if let Category::Calendar { reoccurrence: Some(reoccurrence), .. } = task.category() {
Some(reoccurrence.interval().clone())
} else {
None
}
));
let mut category_calendar_has_time = use_signal(|| task.as_ref().is_some_and(
|task| matches!(*task.category(), Category::Calendar { time: Some(_), .. }))
);
let mut category_calendar_reminder_offset_index = use_signal(|| task.as_ref().and_then(|task|
if let Category::Calendar { time: Some(time), .. } = task.category() {
REMINDER_OFFSETS.iter().position(|&reminder_offset|
reminder_offset == time.reminder_offset()
)
} else {
None
}
).unwrap_or(REMINDER_OFFSETS.len() - 1));
let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>(); let query_client = use_query_client::<QueryValue, QueryErrors, QueryKey>();
let task_for_submit = task.clone();
rsx! { rsx! {
form { form {
onsubmit: move |event| { onsubmit: move |event| {
let task = task_for_submit.clone();
async move { async move {
let new_task = NewTask::new( let new_task = NewTask::new(
event.values().get("title").unwrap().as_value(), event.values().get("title").unwrap().as_value(),
@ -96,7 +120,11 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
event.values().get("project_id").unwrap() event.values().get("project_id").unwrap()
.as_value().parse::<i32>().ok().filter(|&id| id > 0), .as_value().parse::<i32>().ok().filter(|&id| id > 0),
); );
let _ = create_task(new_task).await; if let Some(task) = task {
let _ = edit_task(task.id(), new_task).await;
} else {
let _ = create_task(new_task).await;
}
query_client.invalidate_queries(&[ query_client.invalidate_queries(&[
QueryKey::Tasks, QueryKey::Tasks,
QueryKey::TasksInCategory(selected_category()) QueryKey::TasksInCategory(selected_category())
@ -115,9 +143,10 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
}, },
}, },
input { input {
r#type: "text",
name: "title", name: "title",
required: true, required: true,
initial_value: task.as_ref().map(|task| task.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"
}, },
@ -142,6 +171,9 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
for project in projects { for project in projects {
option { option {
value: project.id().to_string(), value: project.id().to_string(),
selected: task.as_ref().is_some_and(
|task| task.project_id() == Some(project.id())
),
{project.title()} {project.title()}
} }
} }
@ -157,8 +189,10 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
} }
}, },
input { input {
r#type: "date",
name: "deadline", name: "deadline",
initial_value: task.as_ref().and_then(|task| task.deadline())
.map(|deadline| deadline.format("%Y-%m-%d").to_string()),
r#type: "date",
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow basis-0", class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow basis-0",
id: "input_deadline" id: "input_deadline"
} }
@ -177,7 +211,7 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
} }
} }
match selected_category() { match selected_category() {
Category::WaitingFor(_) => rsx! { Category::WaitingFor(waiting_for) => rsx! {
div { div {
class: "flex flex-row items-center gap-3", class: "flex flex-row items-center gap-3",
label { label {
@ -188,15 +222,16 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
} }
}, },
input { input {
r#type: "text",
name: "category_waiting_for", name: "category_waiting_for",
required: true, required: true,
initial_value: waiting_for,
r#type: "text",
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow", class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
id: "input_category_waiting_for" id: "input_category_waiting_for"
}, },
} }
}, },
Category::Calendar { .. } => rsx! { Category::Calendar { date, reoccurrence, time } => rsx! {
div { div {
class: "flex flex-row items-center gap-3", class: "flex flex-row items-center gap-3",
label { label {
@ -212,13 +247,16 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
r#type: "date", r#type: "date",
name: "category_calendar_date", name: "category_calendar_date",
required: true, required: true,
initial_value: chrono::Local::now().format("%Y-%m-%d").to_string(), initial_value: date.format("%Y-%m-%d").to_string(),
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow", class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
id: "input_category_calendar_date" id: "input_category_calendar_date"
}, },
input { input {
r#type: "time", r#type: "time",
name: "category_calendar_time", name: "category_calendar_time",
initial_value: time.map(|calendar_time|
calendar_time.time().format("%H:%M").to_string()
),
class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow", class: "py-2 px-3 bg-zinc-800/50 rounded-lg grow",
id: "input_category_calendar_time", id: "input_category_calendar_time",
oninput: move |event| { oninput: move |event| {
@ -248,9 +286,9 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
disabled: category_calendar_reoccurrence_interval().is_none(), disabled: category_calendar_reoccurrence_interval().is_none(),
required: true, required: true,
min: 1, min: 1,
initial_value: initial_value: category_calendar_reoccurrence_interval()
if category_calendar_reoccurrence_interval().is_none() { "" } .map_or(String::new(), |_| reoccurrence.map_or(1, |reoccurrence|
else { "1" }, reoccurrence.length()).to_string()),
class: "py-2 px-3 bg-zinc-800/50 rounded-lg col-span-2 text-right", class: "py-2 px-3 bg-zinc-800/50 rounded-lg col-span-2 text-right",
id: "category_calendar_reoccurrence_length" id: "category_calendar_reoccurrence_length"
} }
@ -271,7 +309,8 @@ pub(crate) fn TaskForm(on_successful_submit: EventHandler<()>) -> Element {
name: "category_calendar_reminder_offset_index", name: "category_calendar_reminder_offset_index",
min: 0, min: 0,
max: REMINDER_OFFSETS.len() as i64 - 1, max: REMINDER_OFFSETS.len() as i64 - 1,
initial_value: REMINDER_OFFSETS.len() as i64 - 1, initial_value: category_calendar_reminder_offset_index()
.to_string(),
class: "grow input-range-reverse", class: "grow input-range-reverse",
id: "category_calendar_has_reminder", id: "category_calendar_has_reminder",
oninput: move |event| { oninput: move |event| {

View File

@ -6,6 +6,8 @@ use dioxus::prelude::*;
#[component] #[component]
pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element { pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element {
let mut task_being_edited = use_context::<Signal<Option<Task>>>();
rsx! { rsx! {
div { div {
class: format!("flex flex-col {}", class.unwrap_or("")), class: format!("flex flex-col {}", class.unwrap_or("")),
@ -13,7 +15,7 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element
div { div {
key: "{task.id()}", key: "{task.id()}",
class: format!( class: format!(
"px-8 pt-5 {} flex flex-row gap-4", "px-8 pt-5 {} flex flex-row gap-4 select-none {}",
if task.deadline().is_some() { if task.deadline().is_some() {
"pb-0.5" "pb-0.5"
} else if let Category::Calendar { time, .. } = task.category() { } else if let Category::Calendar { time, .. } = task.category() {
@ -24,8 +26,12 @@ pub(crate) fn TaskList(tasks: Vec<Task>, class: Option<&'static str>) -> Element
} }
} else { } else {
"pb-5" "pb-5"
} },
if task_being_edited().is_some_and(|t| t.id() == task.id()) {
"bg-zinc-700"
} else { "" }
), ),
onclick: move |_| task_being_edited.set(Some(task.clone())),
i { i {
class: "fa-regular fa-square text-3xl text-zinc-600", class: "fa-regular fa-square text-3xl text-zinc-600",
}, },

View File

@ -1,4 +1,4 @@
pub(crate) mod error; pub(crate) mod error;
pub(crate) mod error_vec; pub(crate) mod error_vec;
pub(crate) mod project_create_error; pub(crate) mod project_error;
pub(crate) mod task_create_error; pub(crate) mod task_error;

View File

@ -34,6 +34,12 @@ impl From<ValidationErrors> for ErrorVec<ProjectError> {
} }
} }
impl From<diesel::result::Error> for ProjectError {
fn from(_: diesel::result::Error) -> Self {
ProjectError::Error(Error::ServerInternal)
}
}
// Has to be implemented for Dioxus server functions. // Has to be implemented for Dioxus server functions.
impl Display for ProjectError { 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 {

View File

@ -6,13 +6,13 @@ use std::str::FromStr;
use validator::{ValidationErrors, ValidationErrorsKind}; use validator::{ValidationErrors, ValidationErrorsKind};
#[derive(Serialize, Deserialize, Debug)] #[derive(Serialize, Deserialize, Debug)]
pub enum TaskCreateError { pub enum TaskError {
TitleLengthInvalid, TitleLengthInvalid,
ProjectNotFound, ProjectNotFound,
Error(Error), Error(Error),
} }
impl From<ValidationErrors> for ErrorVec<TaskCreateError> { impl From<ValidationErrors> for ErrorVec<TaskError> {
fn from(validation_errors: ValidationErrors) -> Self { fn from(validation_errors: ValidationErrors) -> Self {
validation_errors.errors() validation_errors.errors()
.iter() .iter()
@ -22,31 +22,49 @@ impl From<ValidationErrors> for ErrorVec<TaskCreateError> {
.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" => TaskCreateError::TitleLengthInvalid, "title_length" => TaskError::TitleLengthInvalid,
_ => panic!("Unexpected validation error code: `{code}`."), _ => panic!("Unexpected validation error code: `{code}`."),
}) })
.collect::<Vec<TaskCreateError>>(), .collect::<Vec<TaskError>>(),
_ => panic!("Unexpected validation error kind."), _ => panic!("Unexpected validation error kind."),
}, },
_ => panic!("Unexpected validation field name: `{field}`."), _ => panic!("Unexpected validation field name: `{field}`."),
}) })
.collect::<Vec<TaskCreateError>>() .collect::<Vec<TaskError>>()
.into() .into()
} }
} }
impl From<diesel::result::Error> for TaskError {
fn from(diesel_error: diesel::result::Error) -> Self {
match diesel_error {
diesel::result::Error::DatabaseError(
diesel::result::DatabaseErrorKind::ForeignKeyViolation, info
) => {
match info.constraint_name() {
Some("tasks_project_id_fkey") => TaskError::ProjectNotFound,
_ => TaskError::Error(Error::ServerInternal)
}
}
_ => {
TaskError::Error(Error::ServerInternal)
}
}
}
}
// Has to be implemented for Dioxus server functions. // Has to be implemented for Dioxus server functions.
impl Display for TaskCreateError { impl Display for TaskError {
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 TaskCreateError { impl FromStr for TaskError {
type Err = (); type Err = ();
fn from_str(_: &str) -> Result<Self, Self::Err> { fn from_str(_: &str) -> Result<Self, Self::Err> {
Ok(TaskCreateError::Error(Error::ServerInternal)) Ok(TaskError::Error(Error::ServerInternal))
} }
} }

View File

@ -1,6 +1,6 @@
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::ProjectError; use crate::errors::project_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::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper}; use diesel::{ExpressionMethods, QueryDsl, RunQueryDsl, SelectableHelper};
@ -24,9 +24,7 @@ pub(crate) async fn create_project(new_project: NewProject)
.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<ProjectError>, _>( .map_err::<ErrorVec<ProjectError>, _>(|error| vec![error.into()].into())?;
|_| vec![ProjectError::Error(Error::ServerInternal)].into()
)?;
Ok(new_project) Ok(new_project)
} }
@ -69,9 +67,7 @@ pub(crate) async fn edit_project(project_id: i32, new_project: NewProject)
.set(title.eq(new_project.title)) .set(title.eq(new_project.title))
.returning(Project::as_returning()) .returning(Project::as_returning())
.get_result(&mut connection) .get_result(&mut connection)
.map_err::<ErrorVec<ProjectError>, _>( .map_err::<ErrorVec<ProjectError>, _>(|error| vec![error.into()].into())?;
|_| vec![ProjectError::Error(Error::ServerInternal)].into()
)?;
Ok(updated_project) Ok(updated_project)
} }

View File

@ -2,52 +2,37 @@ use crate::errors::error::Error;
use crate::errors::error_vec::ErrorVec; use crate::errors::error_vec::ErrorVec;
use crate::models::task::{NewTask, Task}; use crate::models::task::{NewTask, Task};
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;
use crate::errors::task_create_error::TaskCreateError; use crate::errors::task_error::TaskError;
use crate::models::category::Category; use crate::models::category::Category;
#[server] #[server]
pub(crate) async fn create_task(new_task: NewTask) pub(crate) async fn create_task(new_task: NewTask)
-> Result<Task, ServerFnError<ErrorVec<TaskCreateError>>> { -> Result<Task, ServerFnError<ErrorVec<TaskError>>> {
use crate::schema::tasks; use crate::schema::tasks;
new_task.validate() new_task.validate()
.map_err::<ErrorVec<TaskCreateError>, _>(|errors| errors.into())?; .map_err::<ErrorVec<TaskError>, _>(|errors| errors.into())?;
let mut connection = establish_database_connection() let mut connection = establish_database_connection()
.map_err::<ErrorVec<TaskCreateError>, _>( .map_err::<ErrorVec<TaskError>, _>(
|_| vec![TaskCreateError::Error(Error::ServerInternal)].into() |_| vec![TaskError::Error(Error::ServerInternal)].into()
)?; )?;
let new_task = diesel::insert_into(tasks::table) let new_task = diesel::insert_into(tasks::table)
.values(&new_task) .values(&new_task)
.returning(Task::as_returning()) .returning(Task::as_returning())
.get_result(&mut connection) .get_result(&mut connection)
.map_err::<ErrorVec<TaskCreateError>, _>(|error| { .map_err::<ErrorVec<TaskError>, _>(|error| vec![error.into()].into())?;
let error = match error {
diesel::result::Error::DatabaseError(
diesel::result::DatabaseErrorKind::ForeignKeyViolation, info
) => {
match info.constraint_name() {
Some("tasks_project_id_fkey") => TaskCreateError::ProjectNotFound,
_ => TaskCreateError::Error(Error::ServerInternal)
}
},
_ => {
TaskCreateError::Error(Error::ServerInternal)
}
};
vec![error].into()
})?;
Ok(new_task) Ok(new_task)
} }
#[server] #[server]
pub(crate) async fn get_tasks_in_category(filtered_category: Category) pub(crate) async fn get_tasks_in_category(filtered_category: Category)
-> Result<Vec<Task>, ServerFnError<ErrorVec<Error>>> { -> Result<Vec<Task>, ServerFnError<ErrorVec<Error>>> {
use crate::schema::tasks::dsl::*; use crate::schema::tasks::dsl::*;
let mut connection = establish_database_connection() let mut connection = establish_database_connection()
@ -65,3 +50,31 @@ pub(crate) async fn get_tasks_in_category(filtered_category: Category)
Ok(results) Ok(results)
} }
#[server]
pub(crate) async fn edit_task(task_id: i32, new_task: NewTask)
-> Result<Task, ServerFnError<ErrorVec<TaskError>>> {
use crate::schema::tasks::dsl::*;
new_task.validate()
.map_err::<ErrorVec<TaskError>, _>(|errors| errors.into())?;
let mut connection = establish_database_connection()
.map_err::<ErrorVec<TaskError>, _>(
|_| vec![TaskError::Error(Error::ServerInternal)].into()
)?;
let updated_task = diesel::update(tasks)
.filter(id.eq(task_id))
.set((
title.eq(new_task.title),
deadline.eq(new_task.deadline),
category.eq(new_task.category),
project_id.eq(new_task.project_id),
))
.returning(Task::as_returning())
.get_result(&mut connection)
.map_err::<ErrorVec<TaskError>, _>(|error| vec![error.into()].into())?;
Ok(updated_task)
}