feat: ability to edit a task #32

Merged
matous-volf merged 2 commits from feat/task-edit into main 2024-09-07 08:26:42 +00:00
8 changed files with 166 additions and 75 deletions
Showing only changes of commit 2152014b7e - Show all commits

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 {
@ -12,8 +13,9 @@ pub(crate) fn BottomPanel(display_form: Signal<bool>) -> Element {
let mut expanded = use_signal(|| display_form()); let mut expanded = use_signal(|| display_form());
let navigation_expanded = use_signal(|| false); let navigation_expanded = use_signal(|| false);
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,16 +1,19 @@
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 {
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", 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 |_| { 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());
coderabbitai[bot] commented 2024-09-07 07:59:55 +00:00 (Migrated from github.com)
Review

Enhanced State Management in FormOpenButton

The addition of task_being_edited alongside project_being_edited effectively allows the component to manage the state of both projects and tasks. The updated onclick handler correctly resets both states, ensuring a comprehensive reset of the component's state when toggled.

Consider adding comments to clarify the purpose of each state management line, especially for new developers or for future maintenance.

**Enhanced State Management in `FormOpenButton`** The addition of `task_being_edited` alongside `project_being_edited` effectively allows the component to manage the state of both projects and tasks. The updated `onclick` handler correctly resets both states, ensuring a comprehensive reset of the component's state when toggled. Consider adding comments to clarify the purpose of each state management line, especially for new developers or for future maintenance. <!-- This is an auto-generated comment by CodeRabbit -->
}, },

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

@ -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() {
coderabbitai[bot] commented 2024-09-07 07:59:56 +00:00 (Migrated from github.com)
Review

Refactor suggestion: Simplify function signature.

The function TaskForm now takes an optional Task parameter to handle both task creation and editing. Consider refactoring to separate concerns, potentially splitting this into two distinct components or functions for clarity and maintainability.

**Refactor suggestion: Simplify function signature.** The function `TaskForm` now takes an optional `Task` parameter to handle both task creation and editing. Consider refactoring to separate concerns, potentially splitting this into two distinct components or functions for clarity and maintainability. <!-- This is an auto-generated comment by CodeRabbit -->
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

@ -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

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