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();
|
let mut tag_counts: HashMap<String, usize> = HashMap::new();
|
||||||
|
|
||||||
for task in tasks {
|
for task in tasks {
|
||||||
if !matches!(task.status, task::TaskStatus::Pending) {
|
if matches!(task.status, task::TaskStatus::Pending) {
|
||||||
continue;
|
if let Some(project) = &task.project {
|
||||||
}
|
*project_counts.entry(project.clone()).or_insert(0) += 1;
|
||||||
|
}
|
||||||
|
|
||||||
if let Some(project) = &task.project {
|
for tag in &task.tags {
|
||||||
*project_counts.entry(project.clone()).or_insert(0) += 1;
|
*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>,
|
all_tasks: Vec<task::TaskSummary>,
|
||||||
cx: &mut gpui::Context<Self>,
|
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 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)
|
|
||||||
});
|
|
||||||
|
|
||||||
self.tasks = all_tasks;
|
self.tasks = all_tasks;
|
||||||
let (projects, tags) = Self::build_sidebar_data(&self.tasks);
|
let (projects, tags) = Self::build_sidebar_data(&self.tasks);
|
||||||
@@ -210,9 +200,6 @@ impl App {
|
|||||||
let tasks = self.tasks.clone();
|
let tasks = self.tasks.clone();
|
||||||
self.task_table.update(cx, |table, cx| {
|
self.task_table.update(cx, |table, cx| {
|
||||||
table.reload_tasks_from_all(tasks, 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);
|
self.update_modal_project_suggestions(cx);
|
||||||
|
|||||||
+22
-6
@@ -44,7 +44,7 @@ impl App {
|
|||||||
if self.delete_confirm.is_some() {
|
if self.delete_confirm.is_some() {
|
||||||
let key = event.keystroke.key.as_str().to_lowercase();
|
let key = event.keystroke.key.as_str().to_lowercase();
|
||||||
let mods = &event.keystroke.modifiers;
|
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 {
|
if !has_mods {
|
||||||
match key.as_str() {
|
match key.as_str() {
|
||||||
@@ -300,8 +300,17 @@ impl App {
|
|||||||
|
|
||||||
let updated_task = match self.task_service.get_task(confirm.task_id) {
|
let updated_task = match self.task_service.get_task(confirm.task_id) {
|
||||||
Ok(task) => task,
|
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) => {
|
Err(e) => {
|
||||||
log::error!("[App] Failed to reload deleted task: {}", e);
|
log::warn!("[App] Failed to reload task after delete: {}", e);
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
@@ -329,11 +338,12 @@ impl App {
|
|||||||
self.task_detail_modal.update(cx, |modal, cx| {
|
self.task_detail_modal.update(cx, |modal, cx| {
|
||||||
modal.close(None, 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| {
|
self.status_bar.update(cx, |bar, cx| {
|
||||||
bar.set_dirty(cx);
|
bar.set_dirty(cx);
|
||||||
});
|
});
|
||||||
@@ -498,7 +508,13 @@ impl App {
|
|||||||
|| has_status
|
|| has_status
|
||||||
|| has_annotations
|
|| 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 {
|
} else {
|
||||||
self.task_detail_modal.update(cx, |modal, cx| {
|
self.task_detail_modal.update(cx, |modal, cx| {
|
||||||
modal.cancel_edit(None, cx);
|
modal.cancel_edit(None, cx);
|
||||||
|
|||||||
@@ -172,7 +172,7 @@ impl DueFilter {
|
|||||||
"this_week" => Some(Self::ThisWeek),
|
"this_week" => Some(Self::ThisWeek),
|
||||||
"none" => Some(Self::NoDate),
|
"none" => Some(Self::NoDate),
|
||||||
_ => value.strip_prefix("date:").and_then(|date| {
|
_ => value.strip_prefix("date:").and_then(|date| {
|
||||||
NaiveDate::parse_from_str(date, "%Y-%m-%d")
|
NaiveDate::parse_from_str(date, DATE_FORMAT)
|
||||||
.ok()
|
.ok()
|
||||||
.map(Self::OnDate)
|
.map(Self::OnDate)
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ impl TaskForm {
|
|||||||
if self.due.trim().is_empty() {
|
if self.due.trim().is_empty() {
|
||||||
None
|
None
|
||||||
} else if NaiveDate::parse_from_str(self.due.trim(), DATE_FORMAT).is_err() {
|
} 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 {
|
} else {
|
||||||
None
|
None
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -254,6 +254,7 @@ impl TaskDetailModal {
|
|||||||
let status_items = vec![
|
let status_items = vec![
|
||||||
DropdownItem::new("Pending"),
|
DropdownItem::new("Pending"),
|
||||||
DropdownItem::new("Completed"),
|
DropdownItem::new("Completed"),
|
||||||
|
DropdownItem::new("Recurring"),
|
||||||
DropdownItem::new("Deleted"),
|
DropdownItem::new("Deleted"),
|
||||||
];
|
];
|
||||||
let status_dropdown = {
|
let status_dropdown = {
|
||||||
@@ -268,7 +269,8 @@ impl TaskDetailModal {
|
|||||||
modal.state.form.status = match index {
|
modal.state.form.status = match index {
|
||||||
0 => task::TaskStatus::Pending,
|
0 => task::TaskStatus::Pending,
|
||||||
1 => task::TaskStatus::Completed,
|
1 => task::TaskStatus::Completed,
|
||||||
2 => task::TaskStatus::Deleted,
|
2 => task::TaskStatus::Recurring,
|
||||||
|
3 => task::TaskStatus::Deleted,
|
||||||
_ => task::TaskStatus::Pending,
|
_ => task::TaskStatus::Pending,
|
||||||
};
|
};
|
||||||
cx.notify();
|
cx.notify();
|
||||||
|
|||||||
+16
-4
@@ -227,7 +227,10 @@ impl From<&task::TaskSummary> for TaskRow {
|
|||||||
|
|
||||||
Self {
|
Self {
|
||||||
uuid: value.uuid,
|
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),
|
description: Self::truncate(&value.description, TABLE_MAX_DESCRIPTION_LENGTH),
|
||||||
project: value.project.clone().unwrap_or(String::new()),
|
project: value.project.clone().unwrap_or(String::new()),
|
||||||
due: Self::format_date(&value.due, value.is_due_today()),
|
due: Self::format_date(&value.due, value.is_due_today()),
|
||||||
@@ -430,9 +433,18 @@ impl TaskTable {
|
|||||||
|
|
||||||
fn get_current_page_rows(&self) -> &[TaskRow] {
|
fn get_current_page_rows(&self) -> &[TaskRow] {
|
||||||
let start = self.pagination.first_item_index();
|
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(
|
pub fn reload_tasks_from_all(
|
||||||
@@ -760,7 +772,7 @@ impl TaskTable {
|
|||||||
return false;
|
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);
|
self.pagination.current_page(page);
|
||||||
let page_first_idx = self.pagination.first_item_index();
|
let page_first_idx = self.pagination.first_item_index();
|
||||||
self.selected_global_idx = Some(idx);
|
self.selected_global_idx = Some(idx);
|
||||||
|
|||||||
Reference in New Issue
Block a user