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
This commit is contained in:
+8
-21
@@ -154,16 +154,14 @@ impl App {
|
||||
let mut tag_counts: HashMap<String, usize> = 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<task::TaskSummary>,
|
||||
cx: &mut gpui::Context<Self>,
|
||||
) {
|
||||
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);
|
||||
|
||||
+22
-6
@@ -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);
|
||||
|
||||
@@ -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)
|
||||
}),
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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();
|
||||
|
||||
+16
-4
@@ -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);
|
||||
|
||||
Reference in New Issue
Block a user