Update and move task order up/down, read ids, more column helper methods
This commit is contained in:
parent
4477bac258
commit
00ac0c351d
1
.gitignore
vendored
1
.gitignore
vendored
@ -2,3 +2,4 @@
|
|||||||
/.idea/
|
/.idea/
|
||||||
/kanban-tui.json
|
/kanban-tui.json
|
||||||
/kanban.json
|
/kanban.json
|
||||||
|
/db.sqlite
|
||||||
|
81
src/app.rs
81
src/app.rs
@ -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
101
src/db.rs
@ -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();
|
||||||
|
}
|
||||||
|
44
src/input.rs
44
src/input.rs
@ -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();
|
||||||
}
|
}
|
||||||
_ => {}
|
_ => {}
|
||||||
},
|
},
|
||||||
|
Loading…
x
Reference in New Issue
Block a user