From 00ac0c351d3c01f30be9e893b047772cbe575c69 Mon Sep 17 00:00:00 2001 From: Joseph Ferano Date: Tue, 13 Jun 2023 12:41:37 +0700 Subject: [PATCH] Update and move task order up/down, read ids, more column helper methods --- .gitignore | 1 + src/app.rs | 81 +++++++++++++++++++---------------------- src/db.rs | 101 ++++++++++++++++++++++++++++++++++----------------- src/input.rs | 44 +++++++++++++++++----- 4 files changed, 141 insertions(+), 86 deletions(-) diff --git a/.gitignore b/.gitignore index bf8c5c1..4709be5 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,4 @@ /.idea/ /kanban-tui.json /kanban.json +/db.sqlite diff --git a/src/app.rs b/src/app.rs index 2a3b1ff..a73fa7a 100644 --- a/src/app.rs +++ b/src/app.rs @@ -7,13 +7,14 @@ use std::fs::File; use std::io::Read; use tui_textarea::TextArea; -use crate::get_all_tasks; +use crate::db; #[cfg(test)] mod tests; #[derive(Debug, Serialize, Deserialize)] pub struct Column { + pub id: i64, pub name: String, pub selected_task_idx: usize, pub tasks: Vec, @@ -94,6 +95,8 @@ impl<'a> Column { #[must_use] pub fn new(name: &str) -> Self { Column { + // TODO: Get the right ID here + id: 1, name: name.to_owned(), tasks: vec![], selected_task_idx: 0, @@ -115,6 +118,16 @@ impl<'a> Column { self.tasks.get(self.selected_task_idx) } + #[must_use] + pub fn get_previous_task(&self) -> Option<&Task> { + self.tasks.get(self.selected_task_idx - 1) + } + + #[must_use] + pub fn get_next_task(&self) -> Option<&Task> { + self.tasks.get(self.selected_task_idx + 1) + } + pub fn get_selected_task_mut(&mut self) -> Option<&mut Task> { self.tasks.get_mut(self.selected_task_idx) } @@ -137,6 +150,28 @@ impl<'a> Column { self.selected_task_idx = self.tasks.len() - 1; } + pub fn move_task_up(&mut self) -> bool { + if self.selected_task_idx > 0 { + self.tasks + .swap(self.selected_task_idx, self.selected_task_idx - 1); + self.selected_task_idx -= 1; + true + } else { + false + } + } + + pub fn move_task_down(&mut self) -> bool { + if self.selected_task_idx < self.tasks.len() - 1 { + self.tasks + .swap(self.selected_task_idx, self.selected_task_idx + 1); + self.selected_task_idx += 1; + true + } else { + false + } + } + #[must_use] pub fn get_task_state_from_curr_selected_task(&self) -> Option> { self.get_selected_task().map(|t| TaskState { @@ -182,33 +217,16 @@ impl Project { } pub async fn load2(pool: &Connection) -> Result { - let todos = get_all_tasks(&pool).unwrap(); + let columns = db::get_all_columns(&pool).unwrap(); Ok(Project { name: String::from("Kanban Board"), filepath: String::from("path"), - columns: todos - .iter() - .map(|(cname, tasks)| Column { - name: cname.clone(), - // TODO: Figure out how to avoid cloning here - tasks: tasks.to_vec(), - selected_task_idx: 0, - }) - .collect::>(), + columns, selected_column_idx: 0, }) } - /// # Panics - /// - /// Will panic if there's an error serializing the Json or there's an issue - /// writing the file - pub fn save(&self) { - let json = serde_json::to_string_pretty(&self).unwrap(); - std::fs::write(&self.filepath, json).unwrap(); - } - #[must_use] pub fn get_selected_column(&self) -> &Column { &self.columns[self.selected_column_idx] @@ -248,7 +266,6 @@ impl Project { let col = self.get_selected_column_mut(); col.tasks.push(t); col.select_last_task(); - self.save(); } } @@ -259,26 +276,4 @@ impl Project { pub fn move_task_next_column(&mut self) { self.move_task_to_column(true); } - - pub fn move_task_up(&mut self) { - let column = self.get_selected_column_mut(); - if column.selected_task_idx > 0 { - column - .tasks - .swap(column.selected_task_idx, column.selected_task_idx - 1); - column.selected_task_idx -= 1; - self.save(); - } - } - - pub fn move_task_down(&mut self) { - let column = self.get_selected_column_mut(); - if column.selected_task_idx < column.tasks.len() - 1 { - column - .tasks - .swap(column.selected_task_idx, column.selected_task_idx + 1); - column.selected_task_idx += 1; - self.save(); - } - } } diff --git a/src/db.rs b/src/db.rs index f7b55f7..dcc43d5 100644 --- a/src/db.rs +++ b/src/db.rs @@ -7,6 +7,7 @@ pub fn get_tasks_by_column(conn: &Connection, column_name: &String) -> Result Result Result)>> { - let mut stmt = conn.prepare("select name from kb_column")?; - let columns = stmt.query_map((), |row| Ok(row.get::(0)?))?; - let mut tasks_by_column: Vec<(String, Vec)> = Vec::new(); - for col in columns { - let name = &col?; - let tasks = get_tasks_by_column(conn, name).unwrap(); - tasks_by_column.push((name.to_string(), tasks)); +pub fn get_all_columns(conn: &Connection) -> Result> { + let mut stmt = conn.prepare("select id, name from kb_column")?; + let query_rows = stmt.query_map((), |row| { + Ok((row.get::(0)?, row.get::(1)?)) + })?; + let mut columns: Vec = Vec::new(); + for row in query_rows { + let r = &row?; + let name = &r.1; + let tasks = get_tasks_by_column(conn, &name).unwrap(); + let col = Column { + id: r.0, + name: name.clone(), + tasks: tasks, + selected_task_idx: 0, + }; + columns.push(col); } - Ok(tasks_by_column) + Ok(columns) } -pub fn insert_new_task(conn: &Connection, title: String, description: String, _column: &Column) -> Task { +pub fn insert_new_task( + conn: &Connection, + title: String, + description: String, + column: &Column, +) -> Task { let mut stmt = conn .prepare("insert into task(title, description, column_id) values (?1, ?2, ?3)") .unwrap(); - stmt.execute(params![title, description, 1]) + stmt.execute(params![title, description, column.id]) .unwrap(); let id = conn.last_insert_rowid(); - Task { id, title, description } + Task { + id, + title, + description, + } } pub fn delete_task(conn: &Connection, task: &Task) { + let mut stmt = conn.prepare("delete from task where id = ?1").unwrap(); + stmt.execute([task.id]).unwrap(); +} + +pub fn update_task_text(conn: &Connection, task: &Task) { let mut stmt = conn - .prepare("delete from task where id = ?1") + .prepare("update task set title = ?2, description = ?3 where id = ?1") .unwrap(); - stmt.execute([task.id]) + stmt.execute((&task.id, &task.title, &task.description)) .unwrap(); } -// pub async fn update_task(pool: &SqlitePool, task: &Task) { -// sqlx::query!("update task set title = ?1, description = ?2", task.title, task.description) -// .execute(pool) -// .await -// .unwrap(); -// } +pub fn move_task_to_column(conn: &Connection, task: &Task, target_column: &Column) { + let mut stmt = conn + .prepare("update task set column_id = ?2, sort_order = ?3 where task.id = ?1") + .unwrap(); + stmt.execute((&task.id, &target_column.id, &target_column.tasks.len())) + .unwrap(); +} -// pub async fn move_task_to_column(pool: &SqlitePool, task: &Task, target_column: &Column) { -// // TODO: You have to add the id to the column -// sqlx::query!("update task set column_id = ?1", 1) -// .execute(pool) -// .await -// .unwrap(); -// } +pub fn swap_task_order(conn: &mut Connection, task1: &Task, task2: &Task) { + let tx = conn.transaction().unwrap(); -// pub async fn move_task_order(pool: &SqlitePool, task: &Task) { -// // TODO: We have to add some kind of ordering mechanism to tasks -// sqlx::query!("update task set column_id = ?1", 1) -// .execute(pool) -// .await -// .unwrap(); -// } + tx.execute( + "create temp table temp_order as select sort_order from task where id = ?1", + &[&task1.id] + ) + .unwrap(); + tx.execute( + "update task set sort_order = (select sort_order from task where id = ?2) where id = ?1", + (task1.id, task2.id) + ) + .unwrap(); + tx.execute( + "update task set sort_order = (select sort_order from temp_order) where id = ?1", + &[&task2.id] + ) + .unwrap(); + tx.execute("drop table temp_order", ()).unwrap(); + + + tx.commit().unwrap(); +} diff --git a/src/input.rs b/src/input.rs index e0022a7..6593e8d 100644 --- a/src/input.rs +++ b/src/input.rs @@ -1,5 +1,5 @@ use crate::app::{State, TaskEditFocus, TaskState}; -use crate::{db}; +use crate::db; use crossterm::event; use crossterm::event::{Event, KeyCode}; @@ -40,13 +40,18 @@ pub fn handle(state: &mut State<'_>) -> Result<(), std::io::Error> { if let Some(selected_task) = column.get_selected_task_mut() { selected_task.title = title; selected_task.description = description; + db::update_task_text(&state.db_conn, &selected_task); } } else { - let task = db::insert_new_task(&state.db_conn, title, description, &column); + let task = db::insert_new_task( + &state.db_conn, + title, + description, + &column, + ); column.add_task(task); } state.task_edit_state = None; - project.save(); } _ => (), }, @@ -72,18 +77,39 @@ pub fn handle(state: &mut State<'_>) -> Result<(), std::io::Error> { } KeyCode::Char('g') => column.select_first_task(), KeyCode::Char('G') => column.select_last_task(), - KeyCode::Char('H') => project.move_task_previous_column(), - KeyCode::Char('L') => project.move_task_next_column(), - KeyCode::Char('J') => project.move_task_down(), - KeyCode::Char('K') => project.move_task_up(), + KeyCode::Char('H') => { + project.move_task_previous_column(); + let col = project.get_selected_column(); + let t = col.get_selected_task().unwrap(); + db::move_task_to_column(&state.db_conn, &t, &col); + } + KeyCode::Char('L') => { + project.move_task_next_column(); + let col = project.get_selected_column(); + let t = col.get_selected_task().unwrap(); + db::move_task_to_column(&state.db_conn, &t, &col); + } + KeyCode::Char('J') => { + if column.move_task_down() { + let task1 = column.get_selected_task().unwrap(); + let task2 = column.get_previous_task().unwrap(); + db::swap_task_order(&mut state.db_conn, &task1, &task2); + } + } + KeyCode::Char('K') => { + if column.move_task_up() { + let task1 = column.get_selected_task().unwrap(); + let task2 = column.get_next_task().unwrap(); + db::swap_task_order(&mut state.db_conn, &task1, &task2); + } + } KeyCode::Char('n') => state.task_edit_state = Some(TaskState::default()), KeyCode::Char('e') => { state.task_edit_state = column.get_task_state_from_curr_selected_task() } KeyCode::Char('D') => { - column.remove_task(); db::delete_task(&state.db_conn, column.get_selected_task().unwrap()); - // project.save(); + column.remove_task(); } _ => {} },