From 0beb4f0921fe967b819ac3b0d8def61d6c1c6ef6 Mon Sep 17 00:00:00 2001 From: Ignacio Perez Date: Mon, 5 Jan 2026 11:37:48 -0300 Subject: [PATCH] Fix task reload edge cases, selection behavior, and minor UX issues - Only count projects and tags from pending tasks - Stop reselecting tasks after sync to avoid unexpected page jumps - Make delete flow resilient when task no longer exists after deletion - Relax modifier detection for delete confirmation - Improve date parsing consistency and validation message - Add Recurring status to task detail modal - Show "-" when task has no working ID - Guard against out-of-bounds pagination slices - Prevent division by zero when computing page index --- src/app.rs | 29 ++++++++--------------------- src/handler.rs | 28 ++++++++++++++++++++++------ src/models/filter_state.rs | 2 +- src/view/task_detail_modal/form.rs | 2 +- src/view/task_detail_modal/mod.rs | 4 +++- src/view/task_table.rs | 20 ++++++++++++++++---- 6 files changed, 51 insertions(+), 34 deletions(-) diff --git a/src/app.rs b/src/app.rs index 736f2fc..ee2d624 100644 --- a/src/app.rs +++ b/src/app.rs @@ -154,16 +154,14 @@ impl App { let mut tag_counts: HashMap = HashMap::new(); for task in tasks { - if !matches!(task.status, task::TaskStatus::Pending) { - continue; - } + if matches!(task.status, task::TaskStatus::Pending) { + if let Some(project) = &task.project { + *project_counts.entry(project.clone()).or_insert(0) += 1; + } - if let Some(project) = &task.project { - *project_counts.entry(project.clone()).or_insert(0) += 1; - } - - for tag in &task.tags { - *tag_counts.entry(tag.clone()).or_insert(0) += 1; + for tag in &task.tags { + *tag_counts.entry(tag.clone()).or_insert(0) += 1; + } } } @@ -186,15 +184,7 @@ impl App { all_tasks: Vec, cx: &mut gpui::Context, ) { - let previous_selection = self.task_table.read(cx).selected_task_uuid(); - let filter_state = self.filter_state.read(cx).clone(); - let filtered = task::TaskFilter::from(&filter_state).apply(&all_tasks); - let reselect_uuid = previous_selection.and_then(|uuid| { - filtered - .iter() - .any(|task| task.uuid == uuid) - .then_some(uuid) - }); + let _filter_state = self.filter_state.read(cx).clone(); self.tasks = all_tasks; let (projects, tags) = Self::build_sidebar_data(&self.tasks); @@ -210,9 +200,6 @@ impl App { let tasks = self.tasks.clone(); self.task_table.update(cx, |table, cx| { table.reload_tasks_from_all(tasks, cx); - if let Some(uuid) = reselect_uuid { - let _ = table.select_task_by_uuid(uuid, cx); - } }); self.update_modal_project_suggestions(cx); diff --git a/src/handler.rs b/src/handler.rs index b8f75d4..5691ebb 100644 --- a/src/handler.rs +++ b/src/handler.rs @@ -44,7 +44,7 @@ impl App { if self.delete_confirm.is_some() { let key = event.keystroke.key.as_str().to_lowercase(); let mods = &event.keystroke.modifiers; - let has_mods = mods.control || mods.alt || mods.shift || mods.platform; + let has_mods = mods.control || mods.alt || mods.shift; if !has_mods { match key.as_str() { @@ -300,8 +300,17 @@ impl App { let updated_task = match self.task_service.get_task(confirm.task_id) { Ok(task) => task, + Err(ref e) + if e.to_string().contains("not found") || e.to_string().contains("No such") => + { + log::info!( + "[App] Task {} not found after deletion (expected)", + confirm.task_id + ); + None + } Err(e) => { - log::error!("[App] Failed to reload deleted task: {}", e); + log::warn!("[App] Failed to reload task after delete: {}", e); None } }; @@ -329,11 +338,12 @@ impl App { self.task_detail_modal.update(cx, |modal, cx| { modal.close(None, cx); }); - self.toast_host.update(cx, |host, cx| { - host.push(ToastKind::Info, "Task deleted", cx); - }); } + self.toast_host.update(cx, |host, cx| { + host.push(ToastKind::Info, "Task deleted", cx); + }); + self.status_bar.update(cx, |bar, cx| { bar.set_dirty(cx); }); @@ -498,7 +508,13 @@ impl App { || has_status || has_annotations { - self.task_service.get_task(task_id).ok().flatten() + match self.task_service.get_task(task_id) { + Ok(task) => task, + Err(e) => { + log::warn!("[App] Failed to sync task after update: {}", e); + None + } + } } else { self.task_detail_modal.update(cx, |modal, cx| { modal.cancel_edit(None, cx); diff --git a/src/models/filter_state.rs b/src/models/filter_state.rs index fe9613f..607750b 100644 --- a/src/models/filter_state.rs +++ b/src/models/filter_state.rs @@ -172,7 +172,7 @@ impl DueFilter { "this_week" => Some(Self::ThisWeek), "none" => Some(Self::NoDate), _ => value.strip_prefix("date:").and_then(|date| { - NaiveDate::parse_from_str(date, "%Y-%m-%d") + NaiveDate::parse_from_str(date, DATE_FORMAT) .ok() .map(Self::OnDate) }), diff --git a/src/view/task_detail_modal/form.rs b/src/view/task_detail_modal/form.rs index 0f90a20..bfdcd02 100644 --- a/src/view/task_detail_modal/form.rs +++ b/src/view/task_detail_modal/form.rs @@ -90,7 +90,7 @@ impl TaskForm { if self.due.trim().is_empty() { None } else if NaiveDate::parse_from_str(self.due.trim(), DATE_FORMAT).is_err() { - Some("Use YYYY-MM-DD".into()) + Some("Invalid date format. Use YYYY-MM-DD".into()) } else { None } diff --git a/src/view/task_detail_modal/mod.rs b/src/view/task_detail_modal/mod.rs index 869060a..95f508a 100644 --- a/src/view/task_detail_modal/mod.rs +++ b/src/view/task_detail_modal/mod.rs @@ -254,6 +254,7 @@ impl TaskDetailModal { let status_items = vec![ DropdownItem::new("Pending"), DropdownItem::new("Completed"), + DropdownItem::new("Recurring"), DropdownItem::new("Deleted"), ]; let status_dropdown = { @@ -268,7 +269,8 @@ impl TaskDetailModal { modal.state.form.status = match index { 0 => task::TaskStatus::Pending, 1 => task::TaskStatus::Completed, - 2 => task::TaskStatus::Deleted, + 2 => task::TaskStatus::Recurring, + 3 => task::TaskStatus::Deleted, _ => task::TaskStatus::Pending, }; cx.notify(); diff --git a/src/view/task_table.rs b/src/view/task_table.rs index fcdb79e..8537e7f 100644 --- a/src/view/task_table.rs +++ b/src/view/task_table.rs @@ -227,7 +227,10 @@ impl From<&task::TaskSummary> for TaskRow { Self { uuid: value.uuid, - id_display: value.working_id.unwrap_or(0).to_string(), + id_display: value + .working_id + .map(|id| id.to_string()) + .unwrap_or_else(|| "-".to_string()), description: Self::truncate(&value.description, TABLE_MAX_DESCRIPTION_LENGTH), project: value.project.clone().unwrap_or(String::new()), due: Self::format_date(&value.due, value.is_due_today()), @@ -430,9 +433,18 @@ impl TaskTable { fn get_current_page_rows(&self) -> &[TaskRow] { let start = self.pagination.first_item_index(); - let end = self.pagination.last_item_index(); + let end = self + .pagination + .last_item_index() + .min(self.cached_rows.len()); + let safe_start = start.min(end).min(self.cached_rows.len()); + let safe_end = end.min(self.cached_rows.len()); - &self.cached_rows[start..end] + if safe_start >= safe_end || self.cached_rows.is_empty() { + &[] + } else { + &self.cached_rows[safe_start..safe_end] + } } pub fn reload_tasks_from_all( @@ -760,7 +772,7 @@ impl TaskTable { return false; }; - let page = idx / self.pagination.page_size + 1; + let page = idx / self.pagination.page_size.max(1) + 1; self.pagination.current_page(page); let page_first_idx = self.pagination.first_item_index(); self.selected_global_idx = Some(idx);