Enhanced BulkAction: 7 options, arrow navigation, PickList, visual selection

- Added: Mark as uncomplete, Set due Tomorrow, Set due Next Week
- Added: Move to existing list (PickList popup)
- ↑/↓ navigate options, Enter executes, 1-7 for number shortcuts
- Selected tasks now show bg(DarkGray) for clearer visual feedback
- BulkAction popup with 7 options, PickList popup for list selection
This commit is contained in:
Ruben Rosario
2026-06-21 19:21:04 +01:00
parent 2fb550229e
commit d669ca5c05
4 changed files with 258 additions and 37 deletions
+177 -11
View File
@@ -54,6 +54,9 @@ pub struct App {
pending_new_key: bool,
pending_bulk_move: bool,
pub selected_tasks: BTreeSet<usize>,
pub bulk_action_selected: usize,
pub popup_list_indices: Vec<(String, String)>,
pub popup_list_selected: usize,
auth_tx: std_mpsc::Sender<AuthEvent>,
auth_rx: std_mpsc::Receiver<AuthEvent>,
sync_tx: mpsc::Sender<SyncCommand>,
@@ -127,6 +130,9 @@ impl App {
pending_new_key: false,
pending_bulk_move: false,
selected_tasks: BTreeSet::new(),
bulk_action_selected: 0,
popup_list_indices: Vec::new(),
popup_list_selected: 0,
auth_tx,
auth_rx,
sync_tx,
@@ -849,19 +855,52 @@ impl App {
KeyCode::Esc => {
self.show_popup = None;
}
KeyCode::Char('1') => {
self.bulk_mark_completed();
self.show_popup = None;
KeyCode::Up => {
self.bulk_action_selected = self.bulk_action_selected.saturating_sub(1);
}
KeyCode::Char('2') => {
self.bulk_set_due_today();
self.show_popup = None;
KeyCode::Down => {
if self.bulk_action_selected < 6 {
self.bulk_action_selected += 1;
}
}
KeyCode::Char('3') => {
self.popup_input.clear();
self.popup_cursor = 0;
self.pending_bulk_move = true;
self.show_popup = Some(Popup::Input);
KeyCode::Enter => {
let action = self.bulk_action_selected;
self.execute_bulk_action(action);
if action <= 4 {
self.show_popup = None;
}
}
KeyCode::Char(c) => {
if let Some(n) = c.to_digit(10) {
let idx = n as usize - 1;
if idx <= 6 {
self.execute_bulk_action(idx);
if idx <= 4 {
self.show_popup = None;
}
}
}
}
_ => {}
},
Popup::PickList => match key.code {
KeyCode::Esc => {
self.show_popup = Some(Popup::BulkAction);
}
KeyCode::Up => {
self.popup_list_selected = self.popup_list_selected.saturating_sub(1);
}
KeyCode::Down => {
if self.popup_list_selected + 1 < self.popup_list_indices.len() {
self.popup_list_selected += 1;
}
}
KeyCode::Enter => {
if !self.popup_list_indices.is_empty() {
let list_id = self.popup_list_indices[self.popup_list_selected].1.clone();
self.bulk_move_to_existing_list(&list_id);
self.show_popup = None;
}
}
_ => {}
},
@@ -963,6 +1002,133 @@ impl App {
self.load_tasks();
}
fn bulk_mark_uncomplete(&mut self) {
let indices: Vec<usize> = self.selected_tasks.iter().copied().collect();
for &i in &indices {
if i >= self.tasks.len() { continue; }
let task = &mut self.tasks[i];
task.status = TaskStatus::NeedsAction;
self.db.update_task(task).ok();
self.db.push_sync(
SyncAction::Update,
&task.id,
&task.list_id,
&serde_json::to_string(task).unwrap_or_default(),
).ok();
}
self.clear_selection();
self.trigger_sync();
self.load_tasks();
}
fn bulk_set_due_tomorrow(&mut self) {
let tomorrow = chrono::Local::now().naive_local() + chrono::Duration::days(1);
let time = NaiveTime::from_hms_opt(9, 0, 0).unwrap();
let due = chrono::NaiveDateTime::new(tomorrow.date(), time);
let indices: Vec<usize> = self.selected_tasks.iter().copied().collect();
for &i in &indices {
if i >= self.tasks.len() { continue; }
let task = &mut self.tasks[i];
task.due = Some(due);
self.db.update_task(task).ok();
self.db.push_sync(
SyncAction::Update,
&task.id,
&task.list_id,
&serde_json::to_string(task).unwrap_or_default(),
).ok();
}
self.clear_selection();
self.trigger_sync();
self.load_tasks();
}
fn bulk_set_due_next_week(&mut self) {
let next_week = chrono::Local::now().naive_local() + chrono::Duration::days(7);
let time = NaiveTime::from_hms_opt(9, 0, 0).unwrap();
let due = chrono::NaiveDateTime::new(next_week.date(), time);
let indices: Vec<usize> = self.selected_tasks.iter().copied().collect();
for &i in &indices {
if i >= self.tasks.len() { continue; }
let task = &mut self.tasks[i];
task.due = Some(due);
self.db.update_task(task).ok();
self.db.push_sync(
SyncAction::Update,
&task.id,
&task.list_id,
&serde_json::to_string(task).unwrap_or_default(),
).ok();
}
self.clear_selection();
self.trigger_sync();
self.load_tasks();
}
fn bulk_move_to_existing_list(&mut self, target_list_id: &str) {
let indices: Vec<usize> = self.selected_tasks.iter().copied().collect();
for &i in &indices {
if i >= self.tasks.len() { continue; }
let original = &self.tasks[i];
let new_task = Task {
id: uuid_v4(),
list_id: target_list_id.to_string(),
title: original.title.clone(),
notes: original.notes.clone(),
status: original.status.clone(),
due: original.due,
position: 0,
created_at: None,
updated_at: None,
};
self.db.insert_task(&new_task).ok();
self.db.push_sync(
SyncAction::Create,
&new_task.id,
target_list_id,
&serde_json::to_string(&new_task).unwrap_or_default(),
).ok();
self.db.delete_task(&original.id).ok();
self.db.push_sync(
SyncAction::Delete,
&original.id,
&original.list_id,
"",
).ok();
}
self.clear_selection();
self.trigger_sync();
if let Some(pos) = self.lists.iter().position(|l| l.id == target_list_id) {
self.selected_list = pos;
}
self.load_tasks();
}
fn execute_bulk_action(&mut self, action_idx: usize) {
match action_idx {
0 => self.bulk_mark_completed(),
1 => self.bulk_mark_uncomplete(),
2 => self.bulk_set_due_today(),
3 => self.bulk_set_due_tomorrow(),
4 => self.bulk_set_due_next_week(),
5 => {
self.popup_input.clear();
self.popup_cursor = 0;
self.pending_bulk_move = true;
self.show_popup = Some(Popup::Input);
}
6 => {
self.popup_list_indices = self.lists.iter()
.map(|l| (l.title.clone(), l.id.clone()))
.collect();
self.popup_list_selected = 0;
self.show_popup = Some(Popup::PickList);
}
_ => {}
}
}
fn reorder_task(&mut self, direction: i64) {
if self.tasks.is_empty() {
return;