From 280b1a5e5a44700ecc5197e6a1b54b5c6170d80b Mon Sep 17 00:00:00 2001 From: Piyush Udhao Date: Tue, 17 Sep 2024 13:54:41 +0530 Subject: [PATCH 1/5] fixed and refactored all of backend and parts of frontend that were affected (basically all pieces calling tauri commands and some small ones using state variables here and there) --- src-tauri/src/db/init.rs | 172 ++++++++--- src-tauri/src/db/mod.rs | 4 +- src-tauri/src/db/ops.rs | 623 -------------------------------------- src-tauri/src/db/todos.rs | 386 +++++++++++++++++++++++ 4 files changed, 514 insertions(+), 671 deletions(-) delete mode 100644 src-tauri/src/db/ops.rs create mode 100644 src-tauri/src/db/todos.rs diff --git a/src-tauri/src/db/init.rs b/src-tauri/src/db/init.rs index dc4c59b..1bf7f43 100644 --- a/src-tauri/src/db/init.rs +++ b/src-tauri/src/db/init.rs @@ -1,63 +1,143 @@ -use std::fs; -use std::path::Path; +use std::{fs, path::Path, sync::{Arc, Mutex}}; +use chrono::Local; +use rusqlite::{Connection, params, Result as SQLiteResult}; -use tauri::AppHandle; +use crate::db::todos::commands::ROOT_GROUP as TODO_ROOT_GROUP; -const DB_NAME: &str = "database.sqlite"; +pub type DbConn = Arc>; -// Initializer struct so that we don't pass AppHandle separately to all helper functions. -pub struct DbInitializer { - pub app_handle: AppHandle, -} +pub fn init(path: &str) -> Connection { + if !db_exists(path) { + create_db(path); + } + + // TODO conditional path for diff OS + let db_name = format!("{path}/database.sqlite"); -impl DbInitializer { - pub fn new(app_handle: AppHandle) -> Self { - Self { app_handle } + match Connection::open(db_name) { + Ok(conn) => conn, + Err(e) => panic!("[ERROR] {e}"), } +} - pub fn init(self) -> Self { - if !self.db_file_exists() { - self.create_db_file(); - } +// Creation functions. +fn db_exists(path: &str) -> bool { + Path::new(path).exists() +} - self +fn create_db(path: &str) { + let db_dir = Path::new(&path).parent().unwrap(); + + // If the parent directory does not exist, create it. + if !db_dir.exists() { + fs::create_dir_all(db_dir).unwrap(); } // Create the database file. - fn create_db_file(&self) { - let db_path = self.get_db_path(); - let db_dir = Path::new(&db_path).parent().unwrap(); + fs::File::create(path).unwrap(); +} - // If the parent directory does not exist, create it. - if !db_dir.exists() { - fs::create_dir_all(db_dir).unwrap(); - } +// Setup functions (to be called in main.rs). +pub fn create_tables(conn: &Connection) -> SQLiteResult<()> { + // TODAY + conn.execute( + "CREATE TABLE IF NOT EXISTS today ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + type TEXT NOT NULL, + is_active INTEGER, + parent_group_id INTEGER, + created_at DATE DEFAULT (datetime('now','localtime')) NOT NULL + )", + [], + )?; + // Only add the root group when the table is created for the first time. + conn.execute( + &format!("INSERT INTO today (id, name, type) VALUES (0, '{TODO_ROOT_GROUP}', 'TaskGroup') ON CONFLICT DO NOTHING"), + [], + )?; - // Create the database file. - fs::File::create(db_path).unwrap(); - } + // TOMORROW. + conn.execute( + "CREATE TABLE IF NOT EXISTS tomorrow ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + name TEXT NOT NULL, + type TEXT NOT NULL, + is_active INTEGER, + parent_group_id INTEGER + )", + [], + )?; + // Only add the root group when the table is created for the first time. + conn.execute( + &format!("INSERT INTO tomorrow (id, name, type) VALUES (0, '{TODO_ROOT_GROUP}', 'TaskGroup') ON CONFLICT DO NOTHING"), + [], + )?; - // Check whether the database file exists. - fn db_file_exists(&self) -> bool { - let db_path = self.get_db_path(); - let res = Path::new(&db_path).exists(); + conn.execute( + "CREATE TABLE IF NOT EXISTS migration_log ( + date TEXT PRIMARY KEY + )", + [], + )?; - return res; - } + Ok(()) +} + +pub fn migrate_todos(conn: &Connection) -> SQLiteResult<()> { + let today = Local::now().naive_local().date(); + + let last_migration_date: Option = + match conn + .query_row("SELECT MAX(date) FROM migration_log", [], |row| row.get(0)) + { + Ok(latest_date) => latest_date, + Err(err) => panic!("{err}"), + }; + + if last_migration_date != Some(today.to_string()) { + // Delete completed tasks from today. + conn + .execute("DELETE FROM today WHERE is_active=0", [])?; + + let max_id: Option = + match conn + .query_row("SELECT MAX(id) from today", [], |row| row.get(0)) + { + Ok(val) => val, + Err(err) => panic!("{err}"), + }; - /// Get the path where the database file should be located. - /// For Linux and macOS only. - pub fn get_db_path(&self) -> String { - let mut res = String::from(""); - let app = &self.app_handle; - if let Some(path) = app.path_resolver().app_data_dir() { - if std::env::consts::OS == "windows" { - res = format!("{}\\{DB_NAME}", path.to_string_lossy().into_owned()); - } else { - res = format!("{}/{DB_NAME}", path.to_string_lossy().into_owned()); - } - } - - res + // Update ids of all tasks in tomorrow so that uniqueness is maintained. + conn.execute( + "UPDATE tomorrow SET id=id+(?1) WHERE id!=0", + params![max_id], + )?; + + // Update parent_group_ids of all migrated rows. + conn.execute( + "UPDATE tomorrow SET parent_group_id=parent_group_id+(?1) WHERE parent_group_id!=0", + params![max_id], + )?; + + // Migrate tasks + conn.execute( + &format!("INSERT INTO today (id, name, type, is_active, parent_group_id) + SELECT id, name, type, is_active, parent_group_id FROM tomorrow WHERE name!='{TODO_ROOT_GROUP}'"), + [], + )?; + + // Clear tomorrow's tasks + conn.execute( + &format!("DELETE FROM tomorrow WHERE name!='{TODO_ROOT_GROUP}'"), + [], + )?; } + // Log the migration + conn.execute( + "INSERT INTO migration_log (date) VALUES (?1) ON CONFLICT DO NOTHING", + params![today.to_string()], + )?; + + Ok(()) } diff --git a/src-tauri/src/db/mod.rs b/src-tauri/src/db/mod.rs index e0d1a96..06325b8 100644 --- a/src-tauri/src/db/mod.rs +++ b/src-tauri/src/db/mod.rs @@ -1,2 +1,2 @@ -pub mod ops; -pub mod init; \ No newline at end of file +pub mod init; +pub mod todos; \ No newline at end of file diff --git a/src-tauri/src/db/ops.rs b/src-tauri/src/db/ops.rs deleted file mode 100644 index 044eb32..0000000 --- a/src-tauri/src/db/ops.rs +++ /dev/null @@ -1,623 +0,0 @@ -use lazy_static::lazy_static; -use std::sync::Mutex; - -const ROOT_GROUP: &str = "/"; -const TASK: &str = "Task"; -const TASK_GROUP: &str = "TaskGroup"; -const TODAY: &str = "today"; - -// Singleton because we need a single point of access to the db across the app. -// Mutex because only one process should be able to use the singleton at a time, -// to prevent race conditions. -// Default because we need to set the db_path according to the specific user of the app. -// We do this in tauri::Builder::default().setup() in main.rs -lazy_static! { - pub static ref DB_SINGLETON: Mutex = Mutex::new(Db::default()); -} - -use chrono::Local; -use core::panic; -use rusqlite::{params, Connection, Result}; -use serde::Serialize; -use std::{collections::HashMap, path::PathBuf}; - -#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)] -#[serde(tag = "type")] -pub enum Type { - Task, - TaskGroup, -} - -#[derive(Serialize, Debug, Clone)] -pub struct TaskRecord { - id: u64, - name: String, - - #[serde(flatten)] - task_record_type: Type, - - // Optional because groups can't be active. - is_active: Option, - // Optional because root group has no parent. - parent_group_id: Option, - // Optional because tasks have no children. - children: Option>, -} - -impl TaskRecord { - fn new( - id: u64, - name: String, - task_record_type: Type, - is_active: Option, - parent_group_id: Option, - children: Option>, - ) -> Self { - TaskRecord { - id, - name, - task_record_type, - is_active, - parent_group_id, - children, - } - } -} - -pub enum FetchBasis { - // Fetch all items. - Active, - // Fetch only active or completed tasks. - Completed, - // Fetch all items under a common parent. - ByParent(u64), -} - -pub struct Db { - pub db_conn: Option, -} - -impl Default for Db { - fn default() -> Self { - Self { db_conn: None } - } -} - -impl Db { - pub fn set_conn(&mut self, db_path: &str) -> Result<()> { - let conn = Connection::open(db_path)?; - self.db_conn = Some(conn); - - self.create_tables()?; - self.migrate_tasks()?; - - Ok(()) - } - - fn create_tables(&self) -> Result<()> { - /* - is_active and parent_group_id can be null. - -> is_active because the record could be a group - -> parent_group_id because the root group "/" isn't in any - other group. - - parent_group_id stores the ID of the parent group. - */ - if let Some(conn) = &self.db_conn { - // TODAY. - conn.execute( - "CREATE TABLE IF NOT EXISTS today ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - type TEXT NOT NULL, - is_active INTEGER, - parent_group_id INTEGER, - created_at DATE DEFAULT (datetime('now','localtime')) NOT NULL - )", - [], - )?; - // Only add the root group when the table is created for the first time. - conn.execute( - &format!("INSERT INTO today (id, name, type) VALUES (0, '{ROOT_GROUP}', 'TaskGroup') ON CONFLICT DO NOTHING"), - [], - )?; - - // TOMORROW. - conn.execute( - "CREATE TABLE IF NOT EXISTS tomorrow ( - id INTEGER PRIMARY KEY AUTOINCREMENT, - name TEXT NOT NULL, - type TEXT NOT NULL, - is_active INTEGER, - parent_group_id INTEGER - )", - [], - )?; - // Only add the root group when the table is created for the first time. - conn.execute( - &format!("INSERT INTO tomorrow (id, name, type) VALUES (0, '{ROOT_GROUP}', 'TaskGroup') ON CONFLICT DO NOTHING"), - [], - )?; - - conn.execute( - "CREATE TABLE IF NOT EXISTS migration_log ( - date TEXT PRIMARY KEY - )", - [], - )?; - } - - Ok(()) - } - - fn migrate_tasks(&self) -> Result<()> { - if let Some(conn) = &self.db_conn { - let today = Local::now().naive_local().date(); - - let last_migration_date: Option = - match conn.query_row("SELECT MAX(date) FROM migration_log", [], |row| row.get(0)) { - Ok(latest_date) => latest_date, - Err(err) => panic!("{err}"), - }; - - if last_migration_date != Some(today.to_string()) { - // Delete completed tasks from today. - conn.execute("DELETE FROM today WHERE is_active=0", [])?; - - let max_id: Option = - match conn.query_row("SELECT MAX(id) from today", [], |row| row.get(0)) { - Ok(val) => val, - Err(err) => panic!("{err}"), - }; - - // Update ids of all tasks in tomorrow so that uniqueness is maintained. - conn.execute( - "UPDATE tomorrow SET id=id+(?1) WHERE id!=0", - params![max_id], - )?; - - // Update parent_group_ids of all migrated rows. - conn.execute("UPDATE tomorrow SET parent_group_id=parent_group_id+(?1) WHERE parent_group_id!=0", params![max_id],)?; - - // Migrate tasks - conn.execute( - &format!("INSERT INTO today (id, name, type, is_active, parent_group_id) - SELECT id, name, type, is_active, parent_group_id FROM tomorrow WHERE name!='{ROOT_GROUP}'"), - [], - )?; - - // Clear tomorrow's tasks - conn.execute( - &format!("DELETE FROM tomorrow WHERE name!='{ROOT_GROUP}'"), - [], - )?; - - // Log the migration - conn.execute( - "INSERT INTO migration_log (date) VALUES (?1)", - params![today.to_string()], - )?; - } else if last_migration_date == None { - // Log the migration - conn.execute( - "INSERT INTO migration_log (date) VALUES (?1)", - params![today.to_string()], - )?; - } - } - - Ok(()) - } - - // TODO: get_final_structure() is recursive, an iterative alternative exists. - - /// Forms the nested root TaskGroup containing all tasks/groups expected by frontend. - /// Uses DFS traversal. - /// Root [task, group[task, task, group [task]], task] - fn get_final_structure( - &self, - id: u64, - children_map: &mut HashMap>, - group_info: &HashMap, - ) -> TaskRecord { - // This will hold all the children (including descendents) of a parent group. - let mut final_children: Vec = Vec::new(); - - // Access the children of the parent whose id = function parameter id. - if let Some(children) = children_map.remove(&id) { - // Iterate through each child. - for child in children { - match child.task_record_type { - // If the child is a task, simply push it in. - Type::Task => final_children.push(child), - // If it's a group, set its children before pushing it in. - Type::TaskGroup => final_children.push(self.get_final_structure( - child.id, - children_map, - group_info, - )), - } - } - } - - let record_name = group_info.get(&id).cloned().unwrap().name; - - // Finally, return the group formed. - // In the end, the root group is returned with all the children nested correctly. - TaskRecord::new( - id, - record_name, - Type::TaskGroup, - None, - None, - Some(final_children), - ) - } - - pub fn fetch_records( - &self, - table: &str, - fetch_basis: FetchBasis, - ) -> Result, Option)>> { - if let Some(conn) = &self.db_conn { - let mut stmt = match fetch_basis { - FetchBasis::Active => conn.prepare(&format!( - "SELECT * FROM {table} WHERE is_active=1 OR type='{TASK_GROUP}'" - ))?, - FetchBasis::Completed => { - conn.prepare(&format!("SELECT * FROM {table} WHERE is_active={}", 0u64))? - } - FetchBasis::ByParent(parent_id) => conn.prepare(&format!( - "SELECT * FROM {table} WHERE parent_group_id={parent_id}" - ))?, - }; - - let task_record_iter = stmt.query_map([], |row| { - let id: u64 = row.get(0)?; - let name: String = row.get(1)?; - let type_str: String = row.get(2)?; - let is_active: Option = row.get(3)?; - let parent_group_id: Option = row.get(4)?; - - let is_active = match is_active { - Some(0) => Some(false), - Some(1) => Some(true), - // If for whatever reason, is_active has a value other than 0 or 1, None is set. - Some(_) => None, - None => None, - }; - - let task_record_type = match type_str.as_str() { - TASK => Type::Task, - TASK_GROUP => Type::TaskGroup, - _ => panic!("Unknown task_record type"), - }; - - Ok((id, name, task_record_type, is_active, parent_group_id)) - })?; - - return task_record_iter.collect(); - } else { - return Err(rusqlite::Error::ExecuteReturnedResults); - } - } - - /// Retrieve the data from the db and return it in the nested, expected format. - pub fn fetch_tasks_view(&self, table: &str) -> Result { - if let Some(_) = &self.db_conn { - let fetched_records = self.fetch_records(table, FetchBasis::Active); - - // Holds the rows mapped by their ids. - let mut task_records: HashMap = HashMap::new(); - let mut parent_map: HashMap> = HashMap::new(); - - match fetched_records { - Err(err) => panic!("idk what error: {err}"), - Ok(task_record_iter) => { - for task_record in task_record_iter { - let (id, name, task_record_type, is_active, parent_group_id) = task_record; - task_records.insert( - id, - TaskRecord::new( - id, - name, - task_record_type, - is_active, - parent_group_id, - { - match task_record_type { - Type::Task => None, - Type::TaskGroup => Some(vec![]), - } - }, - ), - ); - if let Some(pid) = parent_group_id { - parent_map.entry(pid).or_insert_with(Vec::new).push(id); - } - } - } - } - - let temp = task_records.clone(); - - // Holds the children of a group temporarily. - let mut children_map: HashMap> = HashMap::new(); - - // Collect children from task_records into children_map. - for (parent_group_id, children_ids) in parent_map { - for child_id in children_ids { - if let Some(child) = task_records.remove(&child_id) { - children_map - .entry(parent_group_id) - .or_insert_with(Vec::new) - .push(child); - } - } - } - - let root = self.get_final_structure(0, &mut children_map, &temp); - - Ok(root) - } else { - Err(rusqlite::Error::InvalidPath(PathBuf::default())) - } - } -} - -pub mod crud_commands { - use core::panic; - use std::sync::MutexGuard; - - use rusqlite::{params, Connection, Result}; - use serde::{Deserialize, Serialize}; - - - use super::{FetchBasis, Type, DB_SINGLETON, TASK, TASK_GROUP, TODAY}; - - #[tauri::command] - /// C(R)UD - Reads the database and sends appropriate structure to the frontend. - pub fn get_tasks_view(table: &str, status: bool) -> String { - let db = DB_SINGLETON.lock().unwrap(); - - match status { - false => match db.fetch_records(table, FetchBasis::Completed) { - Ok(records) => { - return serde_json::to_string(&records) - .expect("[Error] Couldn't return Vec of records"); - } - Err(err) => panic!("[Error] Could not fetch completed records: {err}"), - }, - true => { - match db.fetch_tasks_view(table) { - Ok(root) => { - let res = serde_json::to_string(&root) - .expect("[ERROR] Cannot parse the root group into JSON."); - - return res; - } - Err(e) => { - // Return a JSON indicating an error occurred - return serde_json::json!({ "error": format!("Failed to fetch records: {}", e) }) - .to_string(); - } - } - } - } - } - - /* - The next two functions return Result. - According to me, they should return rusqlite errors - so that the frontend can handle them by displaying custom - error messages based on the error type. - - But I cannot return a rusqlite::Error because I get the following - error: "the method `blocking_kind` exists for reference `&Result`, but its trait bounds were not satisfied - the following trait bounds were not satisfied: - `rusqlite::error::Error: Into` - which is required by `Result: tauri::command::private::ResultKind` - `Result: serde::ser::Serialize` - which is required by `&Result: tauri::command::private::SerializeKind`" - */ - - #[tauri::command(rename_all = "snake_case")] - // (C)RUD - Adds the specified item to the database. - pub fn add_item( - table: &str, - name: &str, - parent_group_id: u64, - item_type: &str, - ) -> Result { - let db = DB_SINGLETON.lock().unwrap(); - - if let Some(conn) = &db.db_conn { - let command = format!( - "INSERT INTO {table} (name, type, is_active, parent_group_id) VALUES (?1, ?2, ?3, ?4)", - ); - - let is_active = match item_type { - TASK => Some(1u64), - TASK_GROUP => None, - _ => panic!("invalid type"), - }; - - let mut stmt = conn - .prepare(&command) - .expect("[Error] Could not prepare statement"); - match stmt.insert(params![name, item_type, is_active, parent_group_id]) { - Err(err) => println!( - "[ERROR] Error occurred while trying to insert item: {}", - err.to_string() - ), - Ok(id) => return Ok(id), - } - } - - Err(tauri::Error::FailedToExecuteApi( - tauri::api::Error::Command("add_task".to_string()), - )) - } - - #[tauri::command(rename_all = "snake_case")] - // CRU(D) - Deletes the specified item from the database. - pub fn delete_item(table: &str, id: u64, item_type: &str) { - let db = DB_SINGLETON.lock().unwrap(); - - if let Some(conn) = &db.db_conn { - match item_type { - TASK => delete_task(conn, table, id), - TASK_GROUP => delete_group(&db, conn, table, id), - _ => panic!("invalid type"), - } - } - } - - #[derive(Serialize, Deserialize, Debug)] - pub enum UpdateField { - Name(String), - Parent(u64), - Status(bool), - } - - #[tauri::command(rename_all = "snake_case")] - // CR(U)D - Updates the specified item's record in the database. - pub fn update_item(table: &str, id: u64, field: UpdateField) { - let db = DB_SINGLETON.lock().unwrap(); - - if let Some(conn) = &db.db_conn { - match field { - UpdateField::Name(name) => update_name(conn, table, &name, id), - UpdateField::Parent(new_parent_group_id) => { - update_parent(conn, table, id, new_parent_group_id) - } - UpdateField::Status(status) => update_status(conn, table, id, status), - } - } - } - - /* ----------------------------------------------------------------------------- */ - /* -------------------------------HELPER FUNCTIONS------------------------------ */ - /* ----------------------------------------------------------------------------- */ - - fn delete_task(conn: &Connection, table: &str, id: u64) { - let command = format!("DELETE FROM {table} WHERE id={id}"); - match conn.execute(&command, []) { - Err(err) => println!("[ERROR] Could not delete task: {}", err.to_string()), - Ok(_) => (), - } - } - - fn delete_group(db: &MutexGuard, conn: &Connection, table: &str, id: u64) { - // Fetch the children of To-Be-Deleted group. - let children = match db.fetch_records(table, FetchBasis::ByParent(id)) { - Ok(children) => children, - Err(err) => { - panic!("[ERROR] Something went wrong while fetching children: {err}") - } - }; - - if children.is_empty() { - let command = format!("DELETE FROM {table} WHERE id={id}"); - match conn.execute(&command, []) { - Ok(_) => (), - Err(err) => { - panic!("[ERROR] Error occurred while deleting group {id}: {err}") - } - }; - } else { - for (child_id, _, child_type, _, _) in children { - match child_type { - Type::Task => delete_task(conn, table, child_id), - Type::TaskGroup => delete_group(db, conn, table, child_id), - } - } - - let command = format!("DELETE FROM {table} WHERE id={id}"); - match conn.execute(&command, []) { - Ok(_) => (), - Err(err) => panic!("[ERROR] Error occurred while deleting group {id}: {err}"), - } - }; - } - - fn update_name(conn: &Connection, table: &str, name: &str, id: u64) { - let command = format!("UPDATE {table} SET name=(?1) WHERE id=(?2)"); - - match conn.execute(&command, params![name, id]) { - Ok(_) => (), - Err(err) => println!("[ERROR] Could not update task: {}", err.to_string()), - } - } - - fn update_parent(conn: &Connection, table: &str, id: u64, new_parent_group_id: u64) { - let command = format!("UPDATE {table} SET parent_group_id=(?1) WHERE id=(?2)"); - - match conn.execute(&command, params![new_parent_group_id, id]) { - Ok(_) => (), - Err(err) => println!("[ERROR] Could not update task: {}", err.to_string()), - } - } - - fn update_status(conn: &Connection, table: &str, id: u64, status: bool) { - let is_active = match status { - // the status parameter holds the checked status of associated checkbox. - // true = task completed, therefore is_active = false, - true => 0u64, - // false = task incomplete, therefore is_active = true, - false => 1u64, - }; - - let command = format!("UPDATE {table} SET is_active=(?1) WHERE id=(?2)"); - - match conn.execute(&command, params![is_active, id]) { - Ok(_) => (), - Err(err) => panic!( - "[ERROR] could not update status of task: {}", - err.to_string() - ), - } - } - - /// C(R)UD - Reads database and gets all tasks' names and their parent_group_ids. - pub fn get_all_tasks(fetch_basis: FetchBasis) -> Vec<(u64, String)> { - let db = DB_SINGLETON.lock().unwrap(); - let mut all_tasks: Vec<(u64, String)> = Vec::new(); - - match db.fetch_records(TODAY, fetch_basis) { - Ok(records) => { - all_tasks.extend( - records - .into_iter() - .filter(|(_, _, record_type, _, _)| *record_type != Type::TaskGroup) - .map(|(_, name, _, _, parent_id)| { - if let Some(id) = parent_id { - (id, name) - } else { - panic!("[ERROR] While fetching task names, the task: [{name}] had no parent."); - } - }), - ); - } - Err(err) => panic!("{err}"), - } - - all_tasks - } - - pub fn get_item(id: u64) -> String { - let db = DB_SINGLETON.lock().unwrap(); - let mut name = String::new(); - - if let Some(conn) = &db.db_conn { - let res: Option = match conn.query_row("SELECT name FROM today WHERE id=(?1)", params![id], |row| row.get(0)) { - Ok(name) => name, - Err(err) => panic!("{err}"), - }; - - name = res.unwrap(); - } - - name - } -} diff --git a/src-tauri/src/db/todos.rs b/src-tauri/src/db/todos.rs new file mode 100644 index 0000000..5037984 --- /dev/null +++ b/src-tauri/src/db/todos.rs @@ -0,0 +1,386 @@ +use serde::{Deserialize, Serialize}; + +#[derive(Serialize, Deserialize, Debug, Clone, Copy, PartialEq, Eq)] +#[serde(tag = "type")] +pub enum Type { + Task, + TaskGroup, +} + +pub enum FetchBasis { + // Fetch active items. + Active, + // Fetch completed tasks. + Completed, + // Fetch all items under a common parent. + ByParentId(u64), + // Fetch by id. + ById(u64), + // Fetch only tasks. + OnlyTasks, +} + +#[derive(Serialize, Deserialize, Debug)] +pub enum Field { + Name(String), + Parent(u64), + Status(bool), +} + +#[derive(Serialize, Debug, Clone)] +pub struct Todo { + pub id: u64, + pub name: String, + + #[serde(flatten)] + pub todo_type: Type, + + // Optional because groups can't be active. + pub is_active: Option, + // Optional because root group has no parent. + pub parent_group_id: Option, + // Optional because tasks have no children. + pub children: Option>, +} + +impl Todo { + fn new( + id: u64, + name: String, + todo_type: Type, + is_active: Option, + parent_group_id: Option, + children: Option>, + ) -> Self { + Todo { + id, + name, + todo_type, + is_active, + parent_group_id, + children, + } + } +} + +pub mod commands { + use std::collections::HashMap; + + use super::{FetchBasis, Field, Todo, Type}; + use rusqlite::{params, Connection, Result as SQLiteResult}; + use tauri::State; + + use crate::db::init::DbConn; + + pub const ROOT_GROUP: &str = "/"; + pub const TODAY: &str = "today"; + const TASK: &str = "Task"; + const TASK_GROUP: &str = "TaskGroup"; + + #[tauri::command(rename_all = "snake_case")] + pub fn add_todo( + conn: State<'_, DbConn>, + table: &str, + name: &str, + parent_group_id: u64, + todo_type: Type, + ) -> i64 { + let db_conn = conn.lock().unwrap(); + + let command = format!( + "INSERT INTO {table} (name, type, is_active, parent_group_id) VALUES (?1, ?2, ?3, ?4)", + ); + + let is_active = match todo_type { + Type::Task => Some(1u64), + Type::TaskGroup => None, + }; + + let todo_type = match todo_type { + Type::Task => TASK, + Type::TaskGroup => TASK_GROUP, + }; + + let mut stmt = db_conn + .prepare(&command) + .expect("[Error] Could not prepare statement"); + match stmt.insert(params![name, todo_type, is_active, parent_group_id]) { + Err(err) => panic!( + "[ERROR] Error occurred while trying to insert item: {}", + err.to_string() + ), + Ok(id) => id, + } + } + + #[tauri::command(rename_all = "snake_case")] + pub fn get_todos(db_conn: State<'_, DbConn>, table: &str, status: bool) -> String { + let conn = db_conn.lock().unwrap(); + let by = match status { + false => FetchBasis::Completed, + true => FetchBasis::Active, + }; + let rows_option = fetch_todos(&conn, table, by); + + match status { + false => match rows_option { + Err(e) => panic!("[ERROR] Couldn't fetch completed todos from the database: {e}"), + Ok(rows) => { + return serde_json::to_string(&rows) + .expect("[ERROR] Couldn't serialize completed todos!"); + } + }, + true => match create_active_tasks_view(&(rows_option.unwrap())) { + Ok(root) => return serde_json::to_string(&root) + .expect("[ERROR] Cannot parse the root group into JSON."), + Err(e) => return serde_json::json!({ "error": format!("Failed to fetch records: {}", e) }) + .to_string(), + } + } + } + + #[tauri::command(rename_all = "snake_case")] + pub fn update_todo(db_conn: State<'_, DbConn>, table: &str, id: u64, field: Field) { + let conn = db_conn.lock().unwrap(); + + match field { + Field::Name(name) => update_name(&conn, table, &name, id), + Field::Parent(new_pid) => update_parent(&conn, table, id, new_pid), + Field::Status(status) => update_status(&conn, table, id, status), + } + } + + // TODO Remember to change the type parameter in the frontend. + #[tauri::command(rename_all = "snake_case")] + pub fn delete_todo(db_conn: State<'_, DbConn>, table: &str, id: u64, todo_type: Type) { + let conn = db_conn.lock().unwrap(); + + match todo_type { + Type::Task => delete_task(&conn, table, id), + Type::TaskGroup => delete_group(&conn, table, id), + } + } + + /* -------------------------------- Helper Functions -------------------------------- */ + + pub fn fetch_todos(conn: &Connection, table: &str, by: FetchBasis) -> SQLiteResult> { + let mut stmt = match by { + FetchBasis::Active => conn.prepare(&format!( + "SELECT * FROM {table} WHERE is_active=1 OR type='{TASK_GROUP}'" + ))?, + FetchBasis::Completed => { + conn.prepare(&format!("SELECT * FROM {table} WHERE is_active={}", 0u64))? + } + FetchBasis::ByParentId(pid) => conn.prepare(&format!( + "SELECT * FROM {table} WHERE parent_group_id={pid}" + ))?, + FetchBasis::ById(id) => { + conn.prepare(&format!("SELECT * FROM today WHERE id={}", id))? + } + FetchBasis::OnlyTasks => { + conn.prepare(&format!("SELECT * FROM today WHERE type='Task'"))? + } + }; + + let task_record_iter = stmt.query_map([], |row| { + let id: u64 = row.get(0)?; + let name: String = row.get(1)?; + let type_str: String = row.get(2)?; + let is_active: Option = row.get(3)?; + let parent_group_id: Option = row.get(4)?; + + let is_active = match is_active { + Some(0) => Some(false), + Some(1) => Some(true), + // If for whatever reason, is_active has a value other than 0 or 1, None is set. + Some(_) => None, + None => None, + }; + + let todo_type = match type_str.as_str() { + TASK => Type::Task, + TASK_GROUP => Type::TaskGroup, + _ => panic!("Unknown task_record type"), + }; + + Ok(Todo::new( + id, + name, + todo_type, + is_active, + parent_group_id, + None, + )) + })?; + + task_record_iter.collect() + } + + /// Forms the nested root TaskGroup containing all tasks/groups expected by frontend. + /// Uses DFS traversal. + /// Root [task, group[task, task, group [task]], task] + fn build_todos_r( + id: u64, + children_map: &mut HashMap>, + pid: Option, + group_info: &HashMap, + ) -> Todo { + // This will hold all the children (including descendents) of a parent group. + let mut final_children: Vec = Vec::new(); + + // Access the children of the parent whose id = function parameter id. + if let Some(children) = children_map.remove(&id) { + // Iterate through each child. + for child in children { + match child.todo_type { + // If the child is a task, simply push it in. + Type::Task => final_children.push(child), + // If it's a group, set its children before pushing it in. + Type::TaskGroup => final_children.push(build_todos_r( + child.id, + children_map, + child.parent_group_id, + group_info, + )), + } + } + } + + let record_name = group_info.get(&id).cloned().unwrap().name; + + // Finally, return the group formed. + // In the end, the root group is returned with all the children nested correctly. + Todo::new( + id, + record_name, + Type::TaskGroup, + None, + pid, + Some(final_children), + ) + } + + fn create_active_tasks_view(rows: &[Todo]) -> SQLiteResult { + // Holds the rows mapped by their ids. + let mut todo_map: HashMap = HashMap::new(); + let mut parent_map: HashMap> = HashMap::new(); + + for todo in rows { + todo_map.insert( + todo.id, + Todo::new( + todo.id, + todo.name.clone(), + todo.todo_type, + todo.is_active, + todo.parent_group_id, + { + match todo.todo_type { + Type::Task => None, + Type::TaskGroup => Some(vec![]), + } + }, + ), + ); + + if let Some(pid) = todo.parent_group_id { + parent_map.entry(pid).or_insert_with(Vec::new).push(todo.id); + } + } + + let temp = todo_map.clone(); + + let mut children_map: HashMap> = HashMap::new(); + + for (pid, children_ids) in parent_map { + for cid in children_ids { + if let Some(child) = todo_map.remove(&cid) { + children_map.entry(pid).or_insert_with(Vec::new).push(child); + } + } + } + + let root = build_todos_r(0, &mut children_map, None, &temp); + + Ok(root) + } + + fn update_name(conn: &Connection, table: &str, name: &str, id: u64) { + let command = format!("UPDATE {table} SET name=(?1) WHERE id=(?2)"); + + match conn.execute(&command, params![name, id]) { + Ok(_) => (), + Err(err) => println!("[ERROR] Could not update task: {}", err.to_string()), + } + } + + fn update_parent(conn: &Connection, table: &str, id: u64, new_pid: u64) { + let command = format!("UPDATE {table} SET parent_group_id=(?1) WHERE id=(?2)"); + + match conn.execute(&command, params![new_pid, id]) { + Ok(_) => (), + Err(err) => println!("[ERROR] Could not update task: {}", err.to_string()), + } + } + + fn update_status(conn: &Connection, table: &str, id: u64, status: bool) { + let is_active = match status { + // the status parameter holds the checked status of associated checkbox. + // true = task completed, therefore is_active = false, + true => 0u64, + // false = task incomplete, therefore is_active = true, + false => 1u64, + }; + + let command = format!("UPDATE {table} SET is_active=(?1) WHERE id=(?2)"); + + match conn.execute(&command, params![is_active, id]) { + Ok(_) => (), + Err(err) => panic!( + "[ERROR] could not update status of task: {}", + err.to_string() + ), + } + } + + fn delete_task(conn: &Connection, table: &str, id: u64) { + let command = format!("DELETE FROM {table} WHERE id={id}"); + match conn.execute(&command, []) { + Err(err) => println!("[ERROR] Could not delete task: {}", err.to_string()), + Ok(_) => (), + } + } + + fn delete_group(conn: &Connection, table: &str, id: u64) { + // Fetch the children of To-Be-Deleted group. + let children = match fetch_todos(conn, table, FetchBasis::ByParentId(id)) { + Ok(children) => children, + Err(err) => { + panic!("[ERROR] Something went wrong while fetching children: {err}") + } + }; + + if children.is_empty() { + let command = format!("DELETE FROM {table} WHERE id={id}"); + match conn.execute(&command, []) { + Ok(_) => (), + Err(err) => { + panic!("[ERROR] Error occurred while deleting group {id}: {err}") + } + }; + } else { + for child in children { + match child.todo_type { + Type::Task => delete_task(conn, table, child.id), + Type::TaskGroup => delete_group(conn, table, child.id), + } + } + + let command = format!("DELETE FROM {table} WHERE id={id}"); + match conn.execute(&command, []) { + Ok(_) => (), + Err(err) => panic!("[ERROR] Error occurred while deleting group {id}: {err}"), + } + }; + } +} From 0cca2a33ef80577c55503e058abf0ddba052f349 Mon Sep 17 00:00:00 2001 From: Piyush Udhao Date: Tue, 17 Sep 2024 14:07:38 +0530 Subject: [PATCH 2/5] fucking vs code --- src-tauri/src/export.rs | 69 +++++++++++++------ src-tauri/src/main.rs | 31 ++++++--- src/Constants.jsx | 8 +-- src/components/CompletedTasksModal.jsx | 19 ++--- src/views/TasksView/DragDropContext.jsx | 4 ++ src/views/TasksView/Task.jsx | 2 +- src/views/TasksView/TaskGroup.jsx | 2 +- src/views/TasksView/TasksView.jsx | 2 +- src/views/TasksView/Tomorrow/TomorrowView.jsx | 2 +- 9 files changed, 91 insertions(+), 48 deletions(-) diff --git a/src-tauri/src/export.rs b/src-tauri/src/export.rs index 789d226..161c601 100644 --- a/src-tauri/src/export.rs +++ b/src-tauri/src/export.rs @@ -1,17 +1,28 @@ use headless_chrome::{Browser, LaunchOptions}; +use rusqlite::Connection; use std::{collections::HashMap, fs::File, io::Write, path::PathBuf}; -use tauri::api::dialog::FileDialogBuilder; - -use crate::db::ops::{ - crud_commands::{get_all_tasks, get_item}, - FetchBasis, +use tauri::{api::dialog::FileDialogBuilder, State}; + +use crate::db::{ + init::DbConn, + todos::{ + commands::{fetch_todos, TODAY}, + FetchBasis, Todo, + }, }; -fn get_python_input(tasks: &[(u64, String)]) -> Vec<(String, String)> { +fn get_python_input(conn: &Connection, todos: &[Todo]) -> Vec<(String, String)> { let mut res: Vec<(String, String)> = Vec::new(); - for (parent_id, name) in tasks.iter() { - let parent_name = get_item(*parent_id); - res.push((parent_name, name.clone())); + for todo in todos.iter() { + if todo.id == 0 { + continue; + } + let parent = match fetch_todos(conn, TODAY, FetchBasis::ById(todo.parent_group_id.unwrap())) + { + Ok(parent_group) => parent_group, + Err(e) => panic!("[ERROR] why tf is python is still here: {e}"), + }; + res.push((parent[0].name.clone(), todo.name.clone())); } res } @@ -63,9 +74,9 @@ fn generate_ordered_list(map: HashMap>, is_completed: bool) html } -fn generate_html(active_tasks: Vec<(u64, String)>, completed_tasks: Vec<(u64, String)>) -> String { - let active_tasks_inp: Vec<(String, String)> = get_python_input(&active_tasks); - let completed_tasks_inp: Vec<(String, String)> = get_python_input(&completed_tasks); +fn generate_html(conn: &Connection, active_tasks: Vec, completed_tasks: Vec) -> String { + let active_tasks_inp: Vec<(String, String)> = get_python_input(conn, &active_tasks); + let completed_tasks_inp: Vec<(String, String)> = get_python_input(conn, &completed_tasks); let pdf_css = format!( " @@ -132,7 +143,7 @@ fn generate_html(active_tasks: Vec<(u64, String)>, completed_tasks: Vec<(u64, St ) } -fn generate_pdf(html: String, pdf_name: PathBuf) -> Result<(), Box> { +fn generate_pdf(html: String) -> Result, Box> { let browser = Browser::new(LaunchOptions { headless: true, ..Default::default() @@ -146,6 +157,10 @@ fn generate_pdf(html: String, pdf_name: PathBuf) -> Result<(), Box, pdf_name: PathBuf) -> Result<(), Box> { let mut file = File::create(pdf_name)?; file.write_all(&pdf_data)?; @@ -153,9 +168,26 @@ fn generate_pdf(html: String, pdf_name: PathBuf) -> Result<(), Box) { + let conn = db_conn.lock().unwrap(); + let active_tasks = match fetch_todos(&conn, TODAY, FetchBasis::Active) { + Ok(todos) => todos, + Err(_) => return, + }; + let completed_tasks = match fetch_todos(&conn, TODAY, FetchBasis::Completed) { + Ok(todos) => todos, + Err(_) => return, + }; + + // Since we are forming the pdf before we start to save the file, this takes some time + // and the app sort of hangs for a period of time before the file dialog shows up. + let pdf = match generate_pdf(generate_html(&conn, active_tasks, completed_tasks)) { + Ok(data) => data, + Err(e) => { + println!("[ERROR] Couldn't generate pdf: {e}"); + return; + } + }; FileDialogBuilder::new() .set_title("Export Tasks to PDF") @@ -163,10 +195,7 @@ pub fn export_to_pdf() { .set_file_name("Today's Tasks.pdf") .save_file(move |path| { if let Some(path) = path { - match generate_pdf( - generate_html(active_tasks, completed_tasks), - path - ) { + match write_pdf(pdf, path) { Ok(_) => (), Err(err) => println!("{err}"), } diff --git a/src-tauri/src/main.rs b/src-tauri/src/main.rs index 80761ad..6af8b66 100644 --- a/src-tauri/src/main.rs +++ b/src-tauri/src/main.rs @@ -3,28 +3,37 @@ // Learn more about Tauri commands at https://tauri.app/v1/guides/features/command -use habtrack::{db::{init::DbInitializer, ops}, window, export}; +use std::sync::{Arc, Mutex}; + +use habtrack::{db::init, db::todos, window, export}; +use tauri::Manager; // Start work on database. // TODO: Once done, merge with main, and pull changes into FEATURE-add-delete-task. + + fn main() { tauri::Builder::default() .setup(|app| { - let db_init_obj = DbInitializer::new(app.handle()).init(); - ops::DB_SINGLETON - .lock() - .unwrap() - .set_conn(db_init_obj.get_db_path().as_str()) - .expect("i fucked up"); + let db_path = match app.path_resolver().app_data_dir() { + Some(p) => p.to_string_lossy().into_owned(), + None => panic!("[ERROR] Cannot find data directory on this device!"), + }; + + let conn = init::init(&db_path); + init::create_tables(&conn).unwrap(); + init::migrate_todos(&conn).unwrap(); + + app.manage(Arc::new(Mutex::new(conn))); Ok(()) }) .invoke_handler(tauri::generate_handler![ - ops::crud_commands::get_tasks_view, - ops::crud_commands::add_item, - ops::crud_commands::delete_item, - ops::crud_commands::update_item, + todos::commands::get_todos, + todos::commands::add_todo, + todos::commands::delete_todo, + todos::commands::update_todo, window::open_tomorrow_window, window::close_tomorrow_window, export::export_to_pdf, diff --git a/src/Constants.jsx b/src/Constants.jsx index e0f8f65..cca87b8 100644 --- a/src/Constants.jsx +++ b/src/Constants.jsx @@ -16,10 +16,10 @@ const TASK = "Task"; const TASK_GROUP = "TaskGroup"; const ACTIVE_TASKS = true; const COMPLETED_TASKS = false; -const TAURI_FETCH_TASKS_VIEW = "get_tasks_view"; -const TAURI_ADD_ITEM = "add_item"; -const TAURI_DELETE_ITEM = "delete_item"; -const TAURI_UPDATE_ITEM = "update_item"; +const TAURI_FETCH_TASKS_VIEW = "get_todos"; +const TAURI_ADD_ITEM = "add_todo"; +const TAURI_DELETE_ITEM = "delete_todo"; +const TAURI_UPDATE_ITEM = "update_todo"; // New Window. const TAURI_OPEN_TOMORROW_WINDOW = "open_tomorrow_window"; diff --git a/src/components/CompletedTasksModal.jsx b/src/components/CompletedTasksModal.jsx index a77981a..2b0fdab 100644 --- a/src/components/CompletedTasksModal.jsx +++ b/src/components/CompletedTasksModal.jsx @@ -27,6 +27,7 @@ const CompletedTasksModal = ({ onChangeTasksView, onCancel }) => { status: COMPLETED_TASKS, }); const data = JSON.parse(response); + console.log(JSON.stringify(data)); setCompletedStructure(data); } catch (error) { @@ -42,13 +43,13 @@ const CompletedTasksModal = ({ onChangeTasksView, onCancel }) => { // Update db. invoke(TAURI_UPDATE_ITEM, { table: TODAY, - id: item[0], + id: item.id, field: { Status: false }, }); // Update frontend of completed tasks view. setCompletedStructure( - completedStructure.filter((child) => child[0] != item[0]) + completedStructure.filter((child) => child.id != item.id) ); // Update frontend of active tasks view. updateFrontend( @@ -56,12 +57,12 @@ const CompletedTasksModal = ({ onChangeTasksView, onCancel }) => { TASKS_VIEW, onChangeTasksView, { - id: item[0], - name: item[1], + id: item.id, + name: item.name, type: TASK, - parentId: item[item.length - 1], + parentId: item.parent_group_id, }, - item[item.length - 1] + item.parent_group_id ); }; @@ -81,11 +82,11 @@ const CompletedTasksModal = ({ onChangeTasksView, onCancel }) => { >
    {completedStructure.map((node) => { - console.log("this is the node:" + node); + console.log("this is the node:" + JSON.stringify(node)); return ( -
  • +
  • - +