Update and move task order up/down, read ids, more column helper methods

This commit is contained in:
Joseph Ferano 2023-06-13 12:41:37 +07:00
parent 4477bac258
commit 00ac0c351d
4 changed files with 141 additions and 86 deletions

1
.gitignore vendored
View File

@ -2,3 +2,4 @@
/.idea/ /.idea/
/kanban-tui.json /kanban-tui.json
/kanban.json /kanban.json
/db.sqlite

View File

@ -7,13 +7,14 @@ use std::fs::File;
use std::io::Read; use std::io::Read;
use tui_textarea::TextArea; use tui_textarea::TextArea;
use crate::get_all_tasks; use crate::db;
#[cfg(test)] #[cfg(test)]
mod tests; mod tests;
#[derive(Debug, Serialize, Deserialize)] #[derive(Debug, Serialize, Deserialize)]
pub struct Column { pub struct Column {
pub id: i64,
pub name: String, pub name: String,
pub selected_task_idx: usize, pub selected_task_idx: usize,
pub tasks: Vec<Task>, pub tasks: Vec<Task>,
@ -94,6 +95,8 @@ impl<'a> Column {
#[must_use] #[must_use]
pub fn new(name: &str) -> Self { pub fn new(name: &str) -> Self {
Column { Column {
// TODO: Get the right ID here
id: 1,
name: name.to_owned(), name: name.to_owned(),
tasks: vec![], tasks: vec![],
selected_task_idx: 0, selected_task_idx: 0,
@ -115,6 +118,16 @@ impl<'a> Column {
self.tasks.get(self.selected_task_idx) 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> { pub fn get_selected_task_mut(&mut self) -> Option<&mut Task> {
self.tasks.get_mut(self.selected_task_idx) self.tasks.get_mut(self.selected_task_idx)
} }
@ -137,6 +150,28 @@ impl<'a> Column {
self.selected_task_idx = self.tasks.len() - 1; 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] #[must_use]
pub fn get_task_state_from_curr_selected_task(&self) -> Option<TaskState<'a>> { pub fn get_task_state_from_curr_selected_task(&self) -> Option<TaskState<'a>> {
self.get_selected_task().map(|t| TaskState { self.get_selected_task().map(|t| TaskState {
@ -182,33 +217,16 @@ impl Project {
} }
pub async fn load2(pool: &Connection) -> Result<Self, KanbanError> { pub async fn load2(pool: &Connection) -> Result<Self, KanbanError> {
let todos = get_all_tasks(&pool).unwrap(); let columns = db::get_all_columns(&pool).unwrap();
Ok(Project { Ok(Project {
name: String::from("Kanban Board"), name: String::from("Kanban Board"),
filepath: String::from("path"), filepath: String::from("path"),
columns: todos columns,
.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::<Vec<Column>>(),
selected_column_idx: 0, 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] #[must_use]
pub fn get_selected_column(&self) -> &Column { pub fn get_selected_column(&self) -> &Column {
&self.columns[self.selected_column_idx] &self.columns[self.selected_column_idx]
@ -248,7 +266,6 @@ impl Project {
let col = self.get_selected_column_mut(); let col = self.get_selected_column_mut();
col.tasks.push(t); col.tasks.push(t);
col.select_last_task(); col.select_last_task();
self.save();
} }
} }
@ -259,26 +276,4 @@ impl Project {
pub fn move_task_next_column(&mut self) { pub fn move_task_next_column(&mut self) {
self.move_task_to_column(true); 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();
}
}
} }

101
src/db.rs
View File

@ -7,6 +7,7 @@ pub fn get_tasks_by_column(conn: &Connection, column_name: &String) -> Result<Ve
select task.id, title, description from task select task.id, title, description from task
join kb_column on column_id = kb_column.id join kb_column on column_id = kb_column.id
where kb_column.name = ?1 where kb_column.name = ?1
order by sort_order
"#, "#,
)?; )?;
let mut tasks = Vec::new(); let mut tasks = Vec::new();
@ -23,55 +24,87 @@ pub fn get_tasks_by_column(conn: &Connection, column_name: &String) -> Result<Ve
Ok(tasks) Ok(tasks)
} }
pub fn get_all_tasks(conn: &Connection) -> Result<Vec<(String, Vec<Task>)>> { pub fn get_all_columns(conn: &Connection) -> Result<Vec<Column>> {
let mut stmt = conn.prepare("select name from kb_column")?; let mut stmt = conn.prepare("select id, name from kb_column")?;
let columns = stmt.query_map((), |row| Ok(row.get::<usize, String>(0)?))?; let query_rows = stmt.query_map((), |row| {
let mut tasks_by_column: Vec<(String, Vec<Task>)> = Vec::new(); Ok((row.get::<usize, i64>(0)?, row.get::<usize, String>(1)?))
for col in columns { })?;
let name = &col?; let mut columns: Vec<Column> = Vec::new();
let tasks = get_tasks_by_column(conn, name).unwrap(); for row in query_rows {
tasks_by_column.push((name.to_string(), tasks)); 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 let mut stmt = conn
.prepare("insert into task(title, description, column_id) values (?1, ?2, ?3)") .prepare("insert into task(title, description, column_id) values (?1, ?2, ?3)")
.unwrap(); .unwrap();
stmt.execute(params![title, description, 1]) stmt.execute(params![title, description, column.id])
.unwrap(); .unwrap();
let id = conn.last_insert_rowid(); let id = conn.last_insert_rowid();
Task { id, title, description } Task {
id,
title,
description,
}
} }
pub fn delete_task(conn: &Connection, task: &Task) { 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 let mut stmt = conn
.prepare("delete from task where id = ?1") .prepare("update task set title = ?2, description = ?3 where id = ?1")
.unwrap(); .unwrap();
stmt.execute([task.id]) stmt.execute((&task.id, &task.title, &task.description))
.unwrap(); .unwrap();
} }
// pub async fn update_task(pool: &SqlitePool, task: &Task) { pub fn move_task_to_column(conn: &Connection, task: &Task, target_column: &Column) {
// sqlx::query!("update task set title = ?1, description = ?2", task.title, task.description) let mut stmt = conn
// .execute(pool) .prepare("update task set column_id = ?2, sort_order = ?3 where task.id = ?1")
// .await .unwrap();
// .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) { pub fn swap_task_order(conn: &mut Connection, task1: &Task, task2: &Task) {
// // TODO: You have to add the id to the column let tx = conn.transaction().unwrap();
// sqlx::query!("update task set column_id = ?1", 1)
// .execute(pool)
// .await
// .unwrap();
// }
// pub async fn move_task_order(pool: &SqlitePool, task: &Task) { tx.execute(
// // TODO: We have to add some kind of ordering mechanism to tasks "create temp table temp_order as select sort_order from task where id = ?1",
// sqlx::query!("update task set column_id = ?1", 1) &[&task1.id]
// .execute(pool) )
// .await .unwrap();
// .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();
}

View File

@ -1,5 +1,5 @@
use crate::app::{State, TaskEditFocus, TaskState}; use crate::app::{State, TaskEditFocus, TaskState};
use crate::{db}; use crate::db;
use crossterm::event; use crossterm::event;
use crossterm::event::{Event, KeyCode}; 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() { if let Some(selected_task) = column.get_selected_task_mut() {
selected_task.title = title; selected_task.title = title;
selected_task.description = description; selected_task.description = description;
db::update_task_text(&state.db_conn, &selected_task);
} }
} else { } 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); column.add_task(task);
} }
state.task_edit_state = None; 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_first_task(),
KeyCode::Char('G') => column.select_last_task(), KeyCode::Char('G') => column.select_last_task(),
KeyCode::Char('H') => project.move_task_previous_column(), KeyCode::Char('H') => {
KeyCode::Char('L') => project.move_task_next_column(), project.move_task_previous_column();
KeyCode::Char('J') => project.move_task_down(), let col = project.get_selected_column();
KeyCode::Char('K') => project.move_task_up(), 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('n') => state.task_edit_state = Some(TaskState::default()),
KeyCode::Char('e') => { KeyCode::Char('e') => {
state.task_edit_state = column.get_task_state_from_curr_selected_task() state.task_edit_state = column.get_task_state_from_curr_selected_task()
} }
KeyCode::Char('D') => { KeyCode::Char('D') => {
column.remove_task();
db::delete_task(&state.db_conn, column.get_selected_task().unwrap()); db::delete_task(&state.db_conn, column.get_selected_task().unwrap());
// project.save(); column.remove_task();
} }
_ => {} _ => {}
}, },