From c8f32d027ffdc1c00a0aae3e9dfa6de2f917a44b Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 17 Jan 2025 18:51:59 +0300 Subject: [PATCH 01/20] feat(db): add a database using rusqlite. --- Cargo.lock | 93 ++++++++++++++++++++++++++++++++++++++++++---- Cargo.toml | 1 + src/db.rs | 69 ++++++++++++++++++++++++++++++++++ src/lib.rs | 1 + src/models/item.rs | 2 +- 5 files changed, 158 insertions(+), 8 deletions(-) create mode 100644 src/db.rs diff --git a/Cargo.lock b/Cargo.lock index 9f44832..0603176 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -8,7 +8,7 @@ version = "0.5.2" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5f7b0a21988c1bf877cf4759ef5ddaac04c1c9fe808c9142ecb78ba97d97a28a" dependencies = [ - "bitflags", + "bitflags 2.6.0", "bytes", "futures-core", "futures-sink", @@ -29,7 +29,7 @@ dependencies = [ "actix-service", "actix-utils", "actix-web", - "bitflags", + "bitflags 2.6.0", "bytes", "derive_more", "futures-core", @@ -52,9 +52,9 @@ dependencies = [ "actix-rt", "actix-service", "actix-utils", - "ahash", + "ahash 0.8.11", "base64", - "bitflags", + "bitflags 2.6.0", "brotli", "bytes", "bytestring", @@ -169,7 +169,7 @@ dependencies = [ "actix-service", "actix-utils", "actix-web-codegen", - "ahash", + "ahash 0.8.11", "bytes", "bytestring", "cfg-if", @@ -233,6 +233,17 @@ dependencies = [ "generic-array", ] +[[package]] +name = "ahash" +version = "0.7.8" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "891477e0c6a8957309ee5c45a6368af3ae14bb510732d2684ffa19af310920f9" +dependencies = [ + "getrandom", + "once_cell", + "version_check", +] + [[package]] name = "ahash" version = "0.8.11" @@ -510,6 +521,12 @@ dependencies = [ "serde", ] +[[package]] +name = "bitflags" +version = "1.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "bef38d45163c2f1dde094a7dfd33ccf595c92905c8f8f4fdc18d06fb1037718a" + [[package]] name = "bitflags" version = "2.6.0" @@ -730,6 +747,7 @@ dependencies = [ "leptos_meta", "leptos_router", "nostr-sdk", + "rusqlite", "secp256k1 0.30.0", "serde", "serde_json", @@ -982,6 +1000,18 @@ version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5" +[[package]] +name = "fallible-iterator" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "4443176a9f2c162692bd3d352d745ef9413eec5782a80d8fd6f8a1ac692a07f7" + +[[package]] +name = "fallible-streaming-iterator" +version = "0.1.9" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7360491ce676a36bf9bb3c56c1aa791658183a54d2744120f27285738d90465a" + [[package]] name = "flate2" version = "1.0.35" @@ -1227,6 +1257,15 @@ dependencies = [ "crunchy", ] +[[package]] +name = "hashbrown" +version = "0.11.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ab5ef0d4909ef3724cc8cce6ccc8572c5c817592e9285f5464f8e86f8bd3726e" +dependencies = [ + "ahash 0.7.8", +] + [[package]] name = "hashbrown" version = "0.13.2" @@ -1239,7 +1278,7 @@ version = "0.14.5" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1" dependencies = [ - "ahash", + "ahash 0.8.11", "allocator-api2", ] @@ -1254,6 +1293,15 @@ dependencies = [ "foldhash", ] +[[package]] +name = "hashlink" +version = "0.7.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "7249a3129cbc1ffccd74857f81464a323a152173cdb134e0fd81bc803b29facf" +dependencies = [ + "hashbrown 0.11.2", +] + [[package]] name = "hex-conservative" version = "0.1.2" @@ -1810,6 +1858,16 @@ version = "0.2.168" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "5aaeb2981e0606ca11d79718f8bb01164f1d6ed75080182d3abf017e6d244b6d" +[[package]] +name = "libsqlite3-sys" +version = "0.24.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "898745e570c7d0453cc1fbc4a701eb6c662ed54e8fec8b7d14be137ebeeb9d14" +dependencies = [ + "pkg-config", + "vcpkg", +] + [[package]] name = "linear-map" version = "1.2.0" @@ -2380,7 +2438,7 @@ version = "0.5.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "03a862b389f93e68874fbf580b9de08dd02facb9a788ebadaf4a3fd33cf58834" dependencies = [ - "bitflags", + "bitflags 2.6.0", ] [[package]] @@ -2447,6 +2505,21 @@ dependencies = [ "thiserror 1.0.69", ] +[[package]] +name = "rusqlite" +version = "0.27.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "85127183a999f7db96d1a976a309eebbfb6ea3b0b400ddd8340190129de6eb7a" +dependencies = [ + "bitflags 1.3.2", + "fallible-iterator", + "fallible-streaming-iterator", + "hashlink", + "libsqlite3-sys", + "memchr", + "smallvec", +] + [[package]] name = "rustc-demangle" version = "0.1.24" @@ -3284,6 +3357,12 @@ version = "0.15.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4e8257fbc510f0a46eb602c10215901938b5c2a7d5e70fc11483b1d3c9b5b18c" +[[package]] +name = "vcpkg" +version = "0.2.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "accd4ea62f7bb7a82fe23066fb0957d48ef677f6eeb8215f372f52e48bb32426" + [[package]] name = "version_check" version = "0.9.5" diff --git a/Cargo.toml b/Cargo.toml index d5a1264..1da56b0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,6 +16,7 @@ leptos_meta = { version = "0.6" } leptos_actix = { version = "0.6", optional = true } leptos_router = { version = "0.6" } wasm-bindgen = "=0.2.99" +rusqlite = "0.27.0" serde = { version = "1.0", features = ["derive"] } uuid = { version = "1.0", features = ["v4"] } web-sys = { version = "0.3", features = ["Event"] } diff --git a/src/db.rs b/src/db.rs new file mode 100644 index 0000000..6e72ec3 --- /dev/null +++ b/src/db.rs @@ -0,0 +1,69 @@ +use rusqlite::{Connection, Error}; +use serde::{Deserialize, Serialize}; + +// Define a struct to represent a database connection +#[derive(Debug)] +pub struct Database { + conn: Connection, +} + +impl Database { + // Create a new database connection + pub fn new(db_path: &str) -> Result { + let conn = Connection::open(db_path)?; + Ok(Database { conn }) + } + + // Create the database schema + pub fn create_schema(&self) -> Result<(), Error> { + self.conn.execute_batch(" + CREATE TABLE IF NOT EXISTS items ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + wikidata_id TEXT, + custom_properties TEXT + ); + ")?; + Ok(()) + } + + // Insert a new item into the database + pub fn insert_item(&self, item: &DbItem) -> Result<(), Error> { + let wikidata_id = item.wikidata_id.as_ref().map(|s| s.as_str()).unwrap_or(""); + self.conn.execute( + "INSERT INTO items (id, name, description, wikidata_id, custom_properties) VALUES (?, ?, ?, ?, ?);", + &[&item.id, &item.name, &item.description, &wikidata_id.to_string(), &item.custom_properties], + )?; + Ok(()) + } + + // Retrieve all items from the database + pub fn get_items(&self) -> Result, Error> { + let mut stmt = self.conn.prepare("SELECT * FROM items;")?; + let items = stmt.query_map([], |row| { + Ok(DbItem { + id: row.get(0)?, + name: row.get(1)?, + description: row.get(2)?, + wikidata_id: row.get(3)?, + custom_properties: row.get(4)?, + }) + })?; + let mut result = Vec::new(); + for item in items { + result.push(item?); + } + Ok(result) + } +} + +// Define a struct to represent an item in the database +#[derive(Debug, Deserialize, Serialize)] +pub struct DbItem { + pub id: String, + pub name: String, + pub description: String, + pub wikidata_id: Option, + pub custom_properties: String, +} \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 10f7ef7..9f00436 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod app; pub mod components; pub mod models; pub mod nostr; +pub mod db; #[cfg(feature = "hydrate")] diff --git a/src/models/item.rs b/src/models/item.rs index 9af23ad..11c796b 100644 --- a/src/models/item.rs +++ b/src/models/item.rs @@ -7,7 +7,7 @@ pub struct Item { pub id: String, pub name: String, pub description: String, - pub reviews: Vec, + // pub reviews: Vec, pub wikidata_id: Option, pub custom_properties: HashMap, } From 476036449108f0bf79ff39326db9383eecc650da Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 17 Jan 2025 18:52:20 +0300 Subject: [PATCH 02/20] feat(reviews): remove reviews from Item struct --- src/components/items_list.rs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/components/items_list.rs b/src/components/items_list.rs index 6083fe3..53aae06 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -41,7 +41,7 @@ pub fn ItemsList( id: Uuid::new_v4().to_string(), name: String::new(), description: String::new(), - reviews: vec![], + // reviews: vec![], wikidata_id: None, custom_properties: HashMap::new(), }]); @@ -245,7 +245,7 @@ pub fn ItemsList( id: Uuid::new_v4().to_string(), name: String::new(), description: String::new(), - reviews: vec![], + // reviews: vec![], wikidata_id: None, custom_properties: HashMap::new(), }); From dc70316bae2950c8fea861316132d75fc95b344e Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 20 Jan 2025 18:49:54 +0300 Subject: [PATCH 03/20] fix(db): db debugging (in progress) --- Cargo.toml | 3 +- src/components/items_list.rs | 32 +++++++++ src/db.rs | 130 ++++++++++++++++++----------------- src/lib.rs | 1 + 4 files changed, 103 insertions(+), 63 deletions(-) diff --git a/Cargo.toml b/Cargo.toml index 1da56b0..d011d68 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -16,7 +16,7 @@ leptos_meta = { version = "0.6" } leptos_actix = { version = "0.6", optional = true } leptos_router = { version = "0.6" } wasm-bindgen = "=0.2.99" -rusqlite = "0.27.0" +rusqlite = { version = "0.27.0", optional = true} serde = { version = "1.0", features = ["derive"] } uuid = { version = "1.0", features = ["v4"] } web-sys = { version = "0.3", features = ["Event"] } @@ -40,6 +40,7 @@ ssr = [ "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", + "rusqlite" ] # Override secp256k1's default features diff --git a/src/components/items_list.rs b/src/components/items_list.rs index 53aae06..8f1eaf5 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -8,6 +8,8 @@ use crate::models::item::Item; use std::collections::HashMap; use std::sync::Arc; use wasm_bindgen::JsCast; +#[cfg(feature = "ssr")] +use crate::db::{Database, DbItem}; #[derive(Deserialize, Clone, Debug)] struct WikidataSuggestion { @@ -35,6 +37,26 @@ pub fn ItemsList( //signal to store the fetched property labels let (property_labels, set_property_labels) = create_signal(HashMap::::new()); + + let db_path = "items.db"; // path to the database file + let db = Database::new(db_path).unwrap(); + db.create_schema().unwrap(); + + let db_items = db.get_items().unwrap(); + let loaded_items: Vec = db_items.into_iter().map(|db_item| { + Item { + id: db_item.id, + name: db_item.name, + description: db_item.description, + wikidata_id: db_item.wikidata_id, + custom_properties: serde_json::from_str(&db_item.custom_properties).unwrap(), + } + }).collect(); + + spawn_local(async move { + set_items.set(loaded_items); + }); + // Ensure there's an initial empty row if items.get().is_empty() { set_items.set(vec![Item { @@ -238,6 +260,16 @@ pub fn ItemsList( } } } + // //update items in the database + // let db_item = DbItem { + // id: items[index].id.clone(), + // name: items[index].name.clone(), + // description: items[index].description.clone(), + // wikidata_id: items[index].wikidata_id.clone(), + // custom_properties: serde_json::to_string(&items[index].custom_properties).unwrap(), + // }; + // db.insert_item(&db_item).unwrap(); + // Automatically add a new row when editing the last row if index == items.len() - 1 && !value.is_empty() { diff --git a/src/db.rs b/src/db.rs index 6e72ec3..f9124dd 100644 --- a/src/db.rs +++ b/src/db.rs @@ -1,69 +1,75 @@ -use rusqlite::{Connection, Error}; -use serde::{Deserialize, Serialize}; +#[cfg(feature = "ssr")] +mod db_impl { + use rusqlite::{Connection, Error}; + use serde::{Deserialize, Serialize}; -// Define a struct to represent a database connection -#[derive(Debug)] -pub struct Database { - conn: Connection, -} - -impl Database { - // Create a new database connection - pub fn new(db_path: &str) -> Result { - let conn = Connection::open(db_path)?; - Ok(Database { conn }) + // Define a struct to represent a database connection + #[derive(Debug)] + pub struct Database { + conn: Connection, } - // Create the database schema - pub fn create_schema(&self) -> Result<(), Error> { - self.conn.execute_batch(" - CREATE TABLE IF NOT EXISTS items ( - id TEXT PRIMARY KEY, - name TEXT NOT NULL, - description TEXT, - wikidata_id TEXT, - custom_properties TEXT - ); - ")?; - Ok(()) - } - - // Insert a new item into the database - pub fn insert_item(&self, item: &DbItem) -> Result<(), Error> { - let wikidata_id = item.wikidata_id.as_ref().map(|s| s.as_str()).unwrap_or(""); - self.conn.execute( - "INSERT INTO items (id, name, description, wikidata_id, custom_properties) VALUES (?, ?, ?, ?, ?);", - &[&item.id, &item.name, &item.description, &wikidata_id.to_string(), &item.custom_properties], - )?; - Ok(()) - } - - // Retrieve all items from the database - pub fn get_items(&self) -> Result, Error> { - let mut stmt = self.conn.prepare("SELECT * FROM items;")?; - let items = stmt.query_map([], |row| { - Ok(DbItem { - id: row.get(0)?, - name: row.get(1)?, - description: row.get(2)?, - wikidata_id: row.get(3)?, - custom_properties: row.get(4)?, - }) - })?; - let mut result = Vec::new(); - for item in items { - result.push(item?); + impl Database { + // Create a new database connection + pub fn new(db_path: &str) -> Result { + let conn = Connection::open(db_path)?; + Ok(Database { conn }) } - Ok(result) + + // Create the database schema + pub fn create_schema(&self) -> Result<(), Error> { + self.conn.execute_batch(" + CREATE TABLE IF NOT EXISTS items ( + id TEXT PRIMARY KEY, + name TEXT NOT NULL, + description TEXT, + wikidata_id TEXT, + custom_properties TEXT + ); + ")?; + Ok(()) + } + + // Insert a new item into the database + pub fn insert_item(&self, item: &DbItem) -> Result<(), Error> { + let wikidata_id = item.wikidata_id.as_ref().map(|s| s.as_str()).unwrap_or(""); + self.conn.execute( + "INSERT INTO items (id, name, description, wikidata_id, custom_properties) VALUES (?, ?, ?, ?, ?);", + &[&item.id, &item.name, &item.description, &wikidata_id.to_string(), &item.custom_properties], + )?; + Ok(()) + } + + // Retrieve all items from the database + pub fn get_items(&self) -> Result, Error> { + let mut stmt = self.conn.prepare("SELECT * FROM items;")?; + let items = stmt.query_map([], |row| { + Ok(DbItem { + id: row.get(0)?, + name: row.get(1)?, + description: row.get(2)?, + wikidata_id: row.get(3)?, + custom_properties: row.get(4)?, + }) + })?; + let mut result = Vec::new(); + for item in items { + result.push(item?); + } + Ok(result) + } + } + + // Define a struct to represent an item in the database + #[derive(Debug, Deserialize, Serialize)] + pub struct DbItem { + pub id: String, + pub name: String, + pub description: String, + pub wikidata_id: Option, + pub custom_properties: String, } } -// Define a struct to represent an item in the database -#[derive(Debug, Deserialize, Serialize)] -pub struct DbItem { - pub id: String, - pub name: String, - pub description: String, - pub wikidata_id: Option, - pub custom_properties: String, -} \ No newline at end of file +#[cfg(feature = "ssr")] +pub use db_impl::{Database, DbItem}; \ No newline at end of file diff --git a/src/lib.rs b/src/lib.rs index 9f00436..51d66af 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod app; pub mod components; pub mod models; pub mod nostr; +#[cfg(feature = "ssr")] pub mod db; From 5bd19803fe2c73b88833b5fbb0f789428ffef47c Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 22 Jan 2025 02:50:00 +0300 Subject: [PATCH 04/20] feat(ssr): add SSR feature to ItemsList component --- Cargo.toml | 3 +- items.db | Bin 0 -> 12288 bytes src/components/items_list.rs | 55 +++++++++++++++++++++-------------- 3 files changed, 35 insertions(+), 23 deletions(-) create mode 100644 items.db diff --git a/Cargo.toml b/Cargo.toml index d011d68..db698c2 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,6 +31,7 @@ thiserror = "2.0.9" zerofrom = "0.1" [features] +default = ["ssr"] csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"] hydrate = ["leptos/hydrate", "leptos_meta/hydrate", "leptos_router/hydrate"] ssr = [ @@ -40,7 +41,7 @@ ssr = [ "leptos/ssr", "leptos_meta/ssr", "leptos_router/ssr", - "rusqlite" + "dep:rusqlite" ] # Override secp256k1's default features diff --git a/items.db b/items.db new file mode 100644 index 0000000000000000000000000000000000000000..d0da44d58baefb6e826b432ffab981a092c25868 GIT binary patch literal 12288 zcmeI#yH3L}6b4{BTve%;E$bPngpd$3f`Bq0LV;i;Iz^^2l0|bNaYbi%wvIdiZ^B4y z=tNNW>OYcgo#Z&l`7-(LHqu7XQ&DBo&=DIj&eS-U{9POQI91JoHmlv_ zoyTDyLK4AQ6q36{dmU;CostM21l`0JS3x|c%W%BkTA0gB{gjTb1&!{bsJ$*#^`z3J z(M9gc+pAvnvreUv$?yByt0uGB6j@SMMX4&IRqdYm=kta`Z`(iiIn#Nn=Jm_8X*iKH zQ@A{F?I(wBvFG|->)Ex_z)lDVKmY;|fB*y_009U<00Izz00h=mpko`u`F~x1FZzN2 W1Rwwb2tWV=5P$##AOHafIDu~hjaG#K literal 0 HcmV?d00001 diff --git a/src/components/items_list.rs b/src/components/items_list.rs index 8f1eaf5..a649390 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -8,6 +8,7 @@ use crate::models::item::Item; use std::collections::HashMap; use std::sync::Arc; use wasm_bindgen::JsCast; + #[cfg(feature = "ssr")] use crate::db::{Database, DbItem}; @@ -38,25 +39,23 @@ pub fn ItemsList( //signal to store the fetched property labels let (property_labels, set_property_labels) = create_signal(HashMap::::new()); + #[cfg(feature = "ssr")] + { + log!("SSR feature is enabled, attempting to update database..."); + log!("Is SSR enabled? {}", cfg!(feature = "ssr")); let db_path = "items.db"; // path to the database file let db = Database::new(db_path).unwrap(); db.create_schema().unwrap(); let db_items = db.get_items().unwrap(); let loaded_items: Vec = db_items.into_iter().map(|db_item| { - Item { - id: db_item.id, - name: db_item.name, - description: db_item.description, - wikidata_id: db_item.wikidata_id, - custom_properties: serde_json::from_str(&db_item.custom_properties).unwrap(), - } + serde_json::from_str(&db_item.custom_properties).unwrap() }).collect(); spawn_local(async move { set_items.set(loaded_items); }); - + } // Ensure there's an initial empty row if items.get().is_empty() { set_items.set(vec![Item { @@ -215,7 +214,7 @@ pub fn ItemsList( let property_clone = property.clone(); spawn_local(async move { let properties = fetch_item_properties(&wikidata_id, set_fetched_properties, set_property_labels).await; - log!("Fetched properties for Wikidata ID {}: {:?}", wikidata_id, properties); + // log!("Fetched properties for Wikidata ID {}: {:?}", wikidata_id, properties); if let Some(value) = properties.get(&property_clone) { set_items.update(|items| { if let Some(item) = items.iter_mut().find(|item| item.wikidata_id.as_ref().unwrap() == &wikidata_id) { @@ -231,6 +230,8 @@ pub fn ItemsList( // Update item fields let update_item = move |index: usize, field: &str, value: String| { + log!("Updating item at index {}: {}, {}", index, field, value); + log!("Is SSR enabled? {}", cfg!(feature = "ssr")); set_items.update(|items| { if let Some(item) = items.get_mut(index) { match field { @@ -246,7 +247,7 @@ pub fn ItemsList( let set_property_labels = set_property_labels.clone(); spawn_local(async move { let properties = fetch_item_properties(&wikidata_id, set_fetched_properties, set_property_labels).await; - log!("Fetched properties for index {}: {:?}", index, properties); + // log!("Fetched properties for index {}: {:?}", index, properties); }); } } @@ -260,17 +261,27 @@ pub fn ItemsList( } } } - // //update items in the database - // let db_item = DbItem { - // id: items[index].id.clone(), - // name: items[index].name.clone(), - // description: items[index].description.clone(), - // wikidata_id: items[index].wikidata_id.clone(), - // custom_properties: serde_json::to_string(&items[index].custom_properties).unwrap(), - // }; - // db.insert_item(&db_item).unwrap(); - - + #[cfg(feature = "ssr")] + { + log!("SSR block in update_item is executing"); + // Update items in the database + let db_item = DbItem { + id: items[index].id.clone(), + name: items[index].name.clone(), + description: items[index].description.clone(), + wikidata_id: items[index].wikidata_id.clone(), + custom_properties: serde_json::to_string(&items[index].custom_properties).unwrap(), + }; + let db = Database::new("items.db").expect("Failed to open database"); + match db.insert_item(&db_item) { + Ok(_) => { + log!("Item inserted successfully"); + } + Err(e) => { + log!("Error inserting item: {}", e); + } + } + } // Automatically add a new row when editing the last row if index == items.len() - 1 && !value.is_empty() { items.push(Item { @@ -392,7 +403,7 @@ pub fn ItemsList( let set_property_labels = set_property_labels.clone(); spawn_local(async move { let properties = fetch_item_properties(&wikidata_id, set_fetched_properties, set_property_labels).await; - log!("Fetched properties for Wikidata ID {}: {:?}", wikidata_id, properties); + // log!("Fetched properties for Wikidata ID {}: {:?}", wikidata_id, properties); // Populate the custom properties for the new item set_items.update(|items| { From af3f89c561f108554f236f44ac1a468864f4fd41 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 22 Jan 2025 14:14:18 +0300 Subject: [PATCH 05/20] feat(db): add API endpoint for updating items to db and implement server-side functionality --- src/api.rs | 46 +++++++++++++++++++++++++++++ src/components/items_list.rs | 56 +++++++++++++++++++++--------------- src/db.rs | 2 +- src/lib.rs | 1 + src/main.rs | 18 +++++++++--- 5 files changed, 95 insertions(+), 28 deletions(-) create mode 100644 src/api.rs diff --git a/src/api.rs b/src/api.rs new file mode 100644 index 0000000..bdb08fc --- /dev/null +++ b/src/api.rs @@ -0,0 +1,46 @@ +#[cfg(feature = "ssr")] +use leptos::*; +#[cfg(feature = "ssr")] +use leptos::logging::log; +#[cfg(feature = "ssr")] +use crate::db::{Database, DbItem}; + +#[cfg(feature = "ssr")] +#[server(UpdateItem, "/api")] +pub async fn update_item_db(client_db_item: ClientDbItem) -> Result<(), ServerFnError> { + + let db_item = DbItem { + id: client_db_item.id, + name: client_db_item.name, + description: client_db_item.description, + wikidata_id: client_db_item.wikidata_id, + custom_properties: client_db_item.custom_properties, + }; + + // Log the start of the function + log!("Starting update_item function for item: {:?}", db_item); + + // Open the database + let db = match Database::new("items.db") { + Ok(db) => { + log!("Database opened successfully"); + db + } + Err(e) => { + log!("Failed to open database: {}", e); + return Err(ServerFnError::ServerError(e.to_string())); + } + }; + + // Insert the item into the database + match db.insert_item(&db_item) { + Ok(_) => { + log!("Item inserted successfully: {:?}", db_item); + Ok(()) + } + Err(e) => { + log!("Failed to insert item into database: {}", e); + Err(ServerFnError::ServerError(e.to_string())) + } + } +} \ No newline at end of file diff --git a/src/components/items_list.rs b/src/components/items_list.rs index a649390..8916498 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -1,13 +1,14 @@ use crate::components::editable_cell::EditableCell; use crate::components::editable_cell::InputType; use leptos::*; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use uuid::Uuid; use leptos::logging::log; use crate::models::item::Item; use std::collections::HashMap; use std::sync::Arc; use wasm_bindgen::JsCast; +use crate::api::update_item_db; #[cfg(feature = "ssr")] use crate::db::{Database, DbItem}; @@ -19,6 +20,15 @@ struct WikidataSuggestion { description: Option, } +#[derive(Debug, Serialize)] +struct ClientDbItem { + id: String, + name: String, + description: String, + wikidata_id: Option, + custom_properties: String, +} + #[component] pub fn ItemsList( items: ReadSignal>, @@ -247,7 +257,7 @@ pub fn ItemsList( let set_property_labels = set_property_labels.clone(); spawn_local(async move { let properties = fetch_item_properties(&wikidata_id, set_fetched_properties, set_property_labels).await; - // log!("Fetched properties for index {}: {:?}", index, properties); + log!("Fetched properties for index {}: {:?}", index, properties); }); } } @@ -260,28 +270,28 @@ pub fn ItemsList( item.custom_properties.insert(field.to_string(), value.clone()); } } + + // Send the updated item to the server using the API endpoint + let client_db_item = ClientDbItem { + id: item.id.clone(), + name: item.name.clone(), + description: item.description.clone(), + wikidata_id: item.wikidata_id.clone(), + custom_properties: serde_json::to_string(&item.custom_properties).unwrap(), + }; + + + spawn_local(async move { + match update_item_db(client_db_item).await { + Ok(_) => { + log!("Item updated successfully on the server"); + } + Err(e) => { + log!("Error updating item on the server: {}", e); + } + } + }); } - #[cfg(feature = "ssr")] - { - log!("SSR block in update_item is executing"); - // Update items in the database - let db_item = DbItem { - id: items[index].id.clone(), - name: items[index].name.clone(), - description: items[index].description.clone(), - wikidata_id: items[index].wikidata_id.clone(), - custom_properties: serde_json::to_string(&items[index].custom_properties).unwrap(), - }; - let db = Database::new("items.db").expect("Failed to open database"); - match db.insert_item(&db_item) { - Ok(_) => { - log!("Item inserted successfully"); - } - Err(e) => { - log!("Error inserting item: {}", e); - } - } - } // Automatically add a new row when editing the last row if index == items.len() - 1 && !value.is_empty() { items.push(Item { diff --git a/src/db.rs b/src/db.rs index f9124dd..2bf78a5 100644 --- a/src/db.rs +++ b/src/db.rs @@ -61,7 +61,7 @@ mod db_impl { } // Define a struct to represent an item in the database - #[derive(Debug, Deserialize, Serialize)] + #[derive(Debug, Deserialize, Serialize, Clone)] pub struct DbItem { pub id: String, pub name: String, diff --git a/src/lib.rs b/src/lib.rs index 51d66af..a2a7cab 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -2,6 +2,7 @@ pub mod app; pub mod components; pub mod models; pub mod nostr; +pub mod api; #[cfg(feature = "ssr")] pub mod db; diff --git a/src/main.rs b/src/main.rs index 535b829..2abc297 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,3 +1,6 @@ +use actix_web::{web, App, HttpServer}; +use compareware::api::update_item_db; // Corrected server function name +use compareware::db::Database; #[cfg(feature = "ssr")] #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -7,24 +10,31 @@ async fn main() -> std::io::Result<()> { use leptos_actix::{generate_route_list, LeptosRoutes}; use compareware::app::*; + // Load configuration let conf = get_configuration(None).await.unwrap(); let addr = conf.leptos_options.site_addr; + // Generate the list of routes in your Leptos App let routes = generate_route_list(App); println!("listening on http://{}", &addr); + // Start the Actix Web server HttpServer::new(move || { let leptos_options = &conf.leptos_options; let site_root = &leptos_options.site_root; App::new() - // serve JS/WASM/CSS from `pkg` + // Register server functions + .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) + // Serve JS/WASM/CSS from `pkg` .service(Files::new("/pkg", format!("{site_root}/pkg"))) - // serve other assets from the `assets` directory + // Serve other assets from the `assets` directory .service(Files::new("/assets", site_root)) - // serve the favicon from /favicon.ico + // Serve the favicon from /favicon.ico .service(favicon) + // Register Leptos routes .leptos_routes(leptos_options.to_owned(), routes.to_owned(), App) + // Pass Leptos options to the app .app_data(web::Data::new(leptos_options.to_owned())) //.wrap(middleware::Compress::default()) }) @@ -63,4 +73,4 @@ pub fn main() { console_error_panic_hook::set_once(); leptos::mount_to_body(App); -} +} \ No newline at end of file From 291cb058475d623aa6ee91938c9d43bd82be76a3 Mon Sep 17 00:00:00 2001 From: ryan Date: Wed, 22 Jan 2025 20:16:43 +0300 Subject: [PATCH 06/20] feat(db): run db on backend using actix web --- Cargo.lock | 106 ++++++++++++++++++++++++++++++----- Cargo.toml | 2 + items.db => compareware.db | Bin src/api.rs | 61 +++++++++----------- src/components/items_list.rs | 68 +++++++--------------- src/db.rs | 66 +++++++++++++++++----- src/main.rs | 19 ++++++- 7 files changed, 208 insertions(+), 114 deletions(-) rename items.db => compareware.db (100%) diff --git a/Cargo.lock b/Cargo.lock index 0603176..b9ca793 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -127,7 +127,7 @@ dependencies = [ "actix-utils", "futures-core", "futures-util", - "mio", + "mio 1.0.3", "socket2", "tokio", "tracing", @@ -409,7 +409,7 @@ dependencies = [ "miniz_oxide", "object", "rustc-demangle", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -746,7 +746,9 @@ dependencies = [ "leptos_actix", "leptos_meta", "leptos_router", + "mio 0.8.11", "nostr-sdk", + "paste", "rusqlite", "secp256k1 0.30.0", "serde", @@ -1995,6 +1997,18 @@ dependencies = [ "adler2", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.0.3" @@ -2167,7 +2181,7 @@ dependencies = [ "libc", "redox_syscall", "smallvec", - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3063,7 +3077,7 @@ dependencies = [ "backtrace", "bytes", "libc", - "mio", + "mio 1.0.3", "parking_lot", "pin-project-lite", "signal-hook-registry", @@ -3493,13 +3507,22 @@ dependencies = [ "windows-sys 0.59.0", ] +[[package]] +name = "windows-sys" +version = "0.48.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "677d2418bec65e3338edb076e806bc1ec15693c5d0104683f2efe857f61056a9" +dependencies = [ + "windows-targets 0.48.5", +] + [[package]] name = "windows-sys" version = "0.52.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "282be5f36a8ce781fad8c8ae18fa3f9beff57ec1b52cb3de0789201425d9a33d" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", ] [[package]] @@ -3508,7 +3531,22 @@ version = "0.59.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1e38bc4d79ed67fd075bcc251a1c39b32a1776bbe92e5bef1f0bf1f8c531853b" dependencies = [ - "windows-targets", + "windows-targets 0.52.6", +] + +[[package]] +name = "windows-targets" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9a2fa6e2155d7247be68c096456083145c183cbbbc2764150dda45a87197940c" +dependencies = [ + "windows_aarch64_gnullvm 0.48.5", + "windows_aarch64_msvc 0.48.5", + "windows_i686_gnu 0.48.5", + "windows_i686_msvc 0.48.5", + "windows_x86_64_gnu 0.48.5", + "windows_x86_64_gnullvm 0.48.5", + "windows_x86_64_msvc 0.48.5", ] [[package]] @@ -3517,28 +3555,46 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "9b724f72796e036ab90c1021d4780d4d3d648aca59e491e6b98e725b84e99973" dependencies = [ - "windows_aarch64_gnullvm", - "windows_aarch64_msvc", - "windows_i686_gnu", + "windows_aarch64_gnullvm 0.52.6", + "windows_aarch64_msvc 0.52.6", + "windows_i686_gnu 0.52.6", "windows_i686_gnullvm", - "windows_i686_msvc", - "windows_x86_64_gnu", - "windows_x86_64_gnullvm", - "windows_x86_64_msvc", + "windows_i686_msvc 0.52.6", + "windows_x86_64_gnu 0.52.6", + "windows_x86_64_gnullvm 0.52.6", + "windows_x86_64_msvc 0.52.6", ] +[[package]] +name = "windows_aarch64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b38e32f0abccf9987a4e3079dfb67dcd799fb61361e53e2882c3cbaf0d905d8" + [[package]] name = "windows_aarch64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32a4622180e7a0ec044bb555404c800bc9fd9ec262ec147edd5989ccd0c02cd3" +[[package]] +name = "windows_aarch64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "dc35310971f3b2dbbf3f0690a219f40e2d9afcf64f9ab7cc1be722937c26b4bc" + [[package]] name = "windows_aarch64_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "09ec2a7bb152e2252b53fa7803150007879548bc709c039df7627cabbd05d469" +[[package]] +name = "windows_i686_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a75915e7def60c94dcef72200b9a8e58e5091744960da64ec734a6c6e9b3743e" + [[package]] name = "windows_i686_gnu" version = "0.52.6" @@ -3551,24 +3607,48 @@ version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "0eee52d38c090b3caa76c563b86c3a4bd71ef1a819287c19d586d7334ae8ed66" +[[package]] +name = "windows_i686_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8f55c233f70c4b27f66c523580f78f1004e8b5a8b659e05a4eb49d4166cca406" + [[package]] name = "windows_i686_msvc" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "240948bc05c5e7c6dabba28bf89d89ffce3e303022809e73deaefe4f6ec56c66" +[[package]] +name = "windows_x86_64_gnu" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "53d40abd2583d23e4718fddf1ebec84dbff8381c07cae67ff7768bbf19c6718e" + [[package]] name = "windows_x86_64_gnu" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "147a5c80aabfbf0c7d901cb5895d1de30ef2907eb21fbbab29ca94c5b08b1a78" +[[package]] +name = "windows_x86_64_gnullvm" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0b7b52767868a23d5bab768e390dc5f5c55825b6d30b86c844ff2dc7414044cc" + [[package]] name = "windows_x86_64_gnullvm" version = "0.52.6" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "24d5b23dc417412679681396f2b49f3de8c1473deb516bd34410872eff51ed0d" +[[package]] +name = "windows_x86_64_msvc" +version = "0.48.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ed94fce61571a4006852b7389a063ab983c02eb1bb37b47f8272ce92d06d9538" + [[package]] name = "windows_x86_64_msvc" version = "0.52.6" diff --git a/Cargo.toml b/Cargo.toml index db698c2..3794533 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -15,6 +15,7 @@ leptos = { version = "0.6" } leptos_meta = { version = "0.6" } leptos_actix = { version = "0.6", optional = true } leptos_router = { version = "0.6" } +paste = "1.0" wasm-bindgen = "=0.2.99" rusqlite = { version = "0.27.0", optional = true} serde = { version = "1.0", features = ["derive"] } @@ -29,6 +30,7 @@ wasm-bindgen-futures = "0.4" serde_json="1.0.133" thiserror = "2.0.9" zerofrom = "0.1" +mio = "0.8" [features] default = ["ssr"] diff --git a/items.db b/compareware.db similarity index 100% rename from items.db rename to compareware.db diff --git a/src/api.rs b/src/api.rs index bdb08fc..4ef7ea3 100644 --- a/src/api.rs +++ b/src/api.rs @@ -1,46 +1,35 @@ #[cfg(feature = "ssr")] -use leptos::*; -#[cfg(feature = "ssr")] -use leptos::logging::log; +use actix_web::{web, HttpResponse}; #[cfg(feature = "ssr")] use crate::db::{Database, DbItem}; +#[cfg(feature = "ssr")] +use std::sync::Arc; +#[cfg(feature = "ssr")] +use tokio::sync::Mutex; #[cfg(feature = "ssr")] -#[server(UpdateItem, "/api")] -pub async fn update_item_db(client_db_item: ClientDbItem) -> Result<(), ServerFnError> { - - let db_item = DbItem { - id: client_db_item.id, - name: client_db_item.name, - description: client_db_item.description, - wikidata_id: client_db_item.wikidata_id, - custom_properties: client_db_item.custom_properties, - }; - - // Log the start of the function - log!("Starting update_item function for item: {:?}", db_item); - - // Open the database - let db = match Database::new("items.db") { - Ok(db) => { - log!("Database opened successfully"); - db +pub async fn get_items(db: web::Data>>) -> HttpResponse { + let db = db.lock().await; + match db.get_items().await { + Ok(items) => HttpResponse::Ok().json(items), + Err(err) => { + leptos::logging::error!("Failed to fetch items: {:?}", err); + HttpResponse::InternalServerError().body("Failed to fetch items") } - Err(e) => { - log!("Failed to open database: {}", e); - return Err(ServerFnError::ServerError(e.to_string())); - } - }; + } +} - // Insert the item into the database - match db.insert_item(&db_item) { - Ok(_) => { - log!("Item inserted successfully: {:?}", db_item); - Ok(()) - } - Err(e) => { - log!("Failed to insert item into database: {}", e); - Err(ServerFnError::ServerError(e.to_string())) +#[cfg(feature = "ssr")] +pub async fn create_item( + db: web::Data>>, + item: web::Json, +) -> HttpResponse { + let db = db.lock().await; + match db.insert_item(&item.into_inner()).await { + Ok(_) => HttpResponse::Ok().body("Item inserted"), + Err(err) => { + leptos::logging::error!("Failed to insert item: {:?}", err); + HttpResponse::InternalServerError().body("Failed to insert item") } } } \ No newline at end of file diff --git a/src/components/items_list.rs b/src/components/items_list.rs index 8916498..41dc946 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -1,17 +1,13 @@ use crate::components::editable_cell::EditableCell; use crate::components::editable_cell::InputType; use leptos::*; -use serde::{Deserialize, Serialize}; +use serde::Deserialize; use uuid::Uuid; use leptos::logging::log; use crate::models::item::Item; use std::collections::HashMap; use std::sync::Arc; use wasm_bindgen::JsCast; -use crate::api::update_item_db; - -#[cfg(feature = "ssr")] -use crate::db::{Database, DbItem}; #[derive(Deserialize, Clone, Debug)] struct WikidataSuggestion { @@ -20,15 +16,6 @@ struct WikidataSuggestion { description: Option, } -#[derive(Debug, Serialize)] -struct ClientDbItem { - id: String, - name: String, - description: String, - wikidata_id: Option, - custom_properties: String, -} - #[component] pub fn ItemsList( items: ReadSignal>, @@ -49,23 +36,6 @@ pub fn ItemsList( //signal to store the fetched property labels let (property_labels, set_property_labels) = create_signal(HashMap::::new()); - #[cfg(feature = "ssr")] - { - log!("SSR feature is enabled, attempting to update database..."); - log!("Is SSR enabled? {}", cfg!(feature = "ssr")); - let db_path = "items.db"; // path to the database file - let db = Database::new(db_path).unwrap(); - db.create_schema().unwrap(); - - let db_items = db.get_items().unwrap(); - let loaded_items: Vec = db_items.into_iter().map(|db_item| { - serde_json::from_str(&db_item.custom_properties).unwrap() - }).collect(); - - spawn_local(async move { - set_items.set(loaded_items); - }); - } // Ensure there's an initial empty row if items.get().is_empty() { set_items.set(vec![Item { @@ -271,26 +241,26 @@ pub fn ItemsList( } } - // Send the updated item to the server using the API endpoint - let client_db_item = ClientDbItem { - id: item.id.clone(), - name: item.name.clone(), - description: item.description.clone(), - wikidata_id: item.wikidata_id.clone(), - custom_properties: serde_json::to_string(&item.custom_properties).unwrap(), - }; + // // Send the updated item to the server using the API endpoint + // let client_db_item = ClientDbItem { + // id: item.id.clone(), + // name: item.name.clone(), + // description: item.description.clone(), + // wikidata_id: item.wikidata_id.clone(), + // custom_properties: serde_json::to_string(&item.custom_properties).unwrap(), + // }; - spawn_local(async move { - match update_item_db(client_db_item).await { - Ok(_) => { - log!("Item updated successfully on the server"); - } - Err(e) => { - log!("Error updating item on the server: {}", e); - } - } - }); + // spawn_local(async move { + // match update_item_db(client_db_item).await { + // Ok(_) => { + // log!("Item updated successfully on the server"); + // } + // Err(e) => { + // log!("Error updating item on the server: {}", e); + // } + // } + // }); } // Automatically add a new row when editing the last row if index == items.len() - 1 && !value.is_empty() { diff --git a/src/db.rs b/src/db.rs index 2bf78a5..0f9047f 100644 --- a/src/db.rs +++ b/src/db.rs @@ -2,47 +2,64 @@ mod db_impl { use rusqlite::{Connection, Error}; use serde::{Deserialize, Serialize}; + use std::sync::Arc; + use tokio::sync::Mutex; + use leptos::logging; // Define a struct to represent a database connection #[derive(Debug)] pub struct Database { - conn: Connection, + conn: Arc>, } impl Database { // Create a new database connection pub fn new(db_path: &str) -> Result { let conn = Connection::open(db_path)?; - Ok(Database { conn }) + logging::log!("Database connection established at: {}", db_path); // Log with Leptos + Ok(Database { + conn: Arc::new(Mutex::new(conn)), + }) } // Create the database schema - pub fn create_schema(&self) -> Result<(), Error> { - self.conn.execute_batch(" - CREATE TABLE IF NOT EXISTS items ( + pub async fn create_schema(&self) -> Result<(), Error> { + let conn = self.conn.lock().await; + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS items ( id TEXT PRIMARY KEY, name TEXT NOT NULL, description TEXT, wikidata_id TEXT, custom_properties TEXT - ); - ")?; + );", + )?; + logging::log!("Database schema created or verified"); // Log with Leptos Ok(()) } // Insert a new item into the database - pub fn insert_item(&self, item: &DbItem) -> Result<(), Error> { + pub async fn insert_item(&self, item: &DbItem) -> Result<(), Error> { + let conn = self.conn.lock().await; let wikidata_id = item.wikidata_id.as_ref().map(|s| s.as_str()).unwrap_or(""); - self.conn.execute( + conn.execute( "INSERT INTO items (id, name, description, wikidata_id, custom_properties) VALUES (?, ?, ?, ?, ?);", - &[&item.id, &item.name, &item.description, &wikidata_id.to_string(), &item.custom_properties], + &[ + &item.id, + &item.name, + &item.description, + &wikidata_id.to_string(), + &item.custom_properties, + ], )?; + logging::log!("Item inserted: {}", item.id); // Log with Leptos Ok(()) } // Retrieve all items from the database - pub fn get_items(&self) -> Result, Error> { - let mut stmt = self.conn.prepare("SELECT * FROM items;")?; + pub async fn get_items(&self) -> Result, Error> { + let conn = self.conn.lock().await; + let mut stmt = conn.prepare("SELECT * FROM items;")?; let items = stmt.query_map([], |row| { Ok(DbItem { id: row.get(0)?, @@ -56,6 +73,7 @@ mod db_impl { for item in items { result.push(item?); } + logging::log!("Fetched {} items from the database", result.len()); // Log with Leptos Ok(result) } } @@ -69,7 +87,29 @@ mod db_impl { pub wikidata_id: Option, pub custom_properties: String, } + + // Implement conversion from DbItem to a JSON-friendly format + #[derive(Debug, Deserialize, Serialize, Clone)] + pub struct ItemResponse { + pub id: String, + pub name: String, + pub description: String, + pub wikidata_id: Option, + pub custom_properties: String, + } + + impl From for ItemResponse { + fn from(item: DbItem) -> Self { + ItemResponse { + id: item.id, + name: item.name, + description: item.description, + wikidata_id: item.wikidata_id, + custom_properties: item.custom_properties, + } + } + } } #[cfg(feature = "ssr")] -pub use db_impl::{Database, DbItem}; \ No newline at end of file +pub use db_impl::{Database, DbItem, ItemResponse}; \ No newline at end of file diff --git a/src/main.rs b/src/main.rs index 2abc297..2c34e5c 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,6 +1,3 @@ -use actix_web::{web, App, HttpServer}; -use compareware::api::update_item_db; // Corrected server function name -use compareware::db::Database; #[cfg(feature = "ssr")] #[actix_web::main] async fn main() -> std::io::Result<()> { @@ -9,11 +6,20 @@ async fn main() -> std::io::Result<()> { use leptos::*; use leptos_actix::{generate_route_list, LeptosRoutes}; use compareware::app::*; + use compareware::db::{Database, DbItem}; + use compareware::api::{get_items, create_item}; // Import API handlers + use std::sync::Arc; + use tokio::sync::Mutex; // Load configuration let conf = get_configuration(None).await.unwrap(); let addr = conf.leptos_options.site_addr; + // Initialize the database + let db = Database::new("compareware.db").unwrap(); + db.create_schema().await.unwrap(); // Ensure the schema is created + let db = Arc::new(Mutex::new(db)); // Wrap the database in an Arc> for shared state + // Generate the list of routes in your Leptos App let routes = generate_route_list(App); println!("listening on http://{}", &addr); @@ -22,6 +28,8 @@ async fn main() -> std::io::Result<()> { HttpServer::new(move || { let leptos_options = &conf.leptos_options; let site_root = &leptos_options.site_root; + let db = db.clone(); // Clone the Arc for each worker + App::new() // Register server functions @@ -37,6 +45,11 @@ async fn main() -> std::io::Result<()> { // Pass Leptos options to the app .app_data(web::Data::new(leptos_options.to_owned())) //.wrap(middleware::Compress::default()) + // Pass the database as shared state + .app_data(web::Data::new(db)) + // Register API endpoints + .route("/api/items", web::get().to(get_items)) + .route("/api/items", web::post().to(create_item)) }) .bind(&addr)? .run() From e46b693e5644c3c078694e4aeba7f6049f1ba434 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 23 Jan 2025 00:02:48 +0300 Subject: [PATCH 07/20] build(toolchain): update rust-toolchain.toml to use Rust 1.82.0 instead of 1.83.0 --- rust-toolchain.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 769d2b0..7322ba0 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,3 +1,3 @@ [toolchain] -channel = "1.83.0" +channel = "1.82.0" targets = [ "wasm32-unknown-unknown" ] \ No newline at end of file From 0ac35c3ca5cc6488d9f02eb13b001055cb576005 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 23 Jan 2025 14:29:42 +0300 Subject: [PATCH 08/20] fix(db): register custom API routes before the Leptos server function handler --- src/main.rs | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/main.rs b/src/main.rs index 2c34e5c..412f0e2 100644 --- a/src/main.rs +++ b/src/main.rs @@ -32,6 +32,12 @@ async fn main() -> std::io::Result<()> { App::new() + // Register custom API routes BEFORE Leptos server functions + .service( + web::scope("/api") + .route("/items", web::get().to(get_items)) + .route("/items", web::post().to(create_item)), + ) // Register server functions .route("/api/{tail:.*}", leptos_actix::handle_server_fns()) // Serve JS/WASM/CSS from `pkg` @@ -44,12 +50,9 @@ async fn main() -> std::io::Result<()> { .leptos_routes(leptos_options.to_owned(), routes.to_owned(), App) // Pass Leptos options to the app .app_data(web::Data::new(leptos_options.to_owned())) - //.wrap(middleware::Compress::default()) + //.wrap(middleware::Compress::default()) // Pass the database as shared state .app_data(web::Data::new(db)) - // Register API endpoints - .route("/api/items", web::get().to(get_items)) - .route("/api/items", web::post().to(create_item)) }) .bind(&addr)? .run() From 3ed12c80a60444b9f61ebb2e58209f41603ae6f2 Mon Sep 17 00:00:00 2001 From: ryan Date: Thu, 23 Jan 2025 21:36:30 +0300 Subject: [PATCH 09/20] feat(db): integrate the database with the frontend. --- src/components/items_list.rs | 84 +++++++++++++++++++++++++----------- 1 file changed, 59 insertions(+), 25 deletions(-) diff --git a/src/components/items_list.rs b/src/components/items_list.rs index 41dc946..feea60f 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -1,7 +1,7 @@ use crate::components::editable_cell::EditableCell; use crate::components::editable_cell::InputType; use leptos::*; -use serde::Deserialize; +use serde::{Deserialize, Serialize}; use uuid::Uuid; use leptos::logging::log; use crate::models::item::Item; @@ -48,6 +48,47 @@ pub fn ItemsList( }]); } + // Function to send an item to the backend API + async fn save_item_to_db(item: Item) { + // Serialize `custom_properties` to a JSON string + let custom_properties = serde_json::to_string(&item.custom_properties).unwrap(); + + // Create a new struct to send to the backend + #[derive(Serialize, Debug)] + struct ItemToSend { + id: String, + name: String, + description: String, + wikidata_id: Option, + custom_properties: String, // JSON-encoded string + } + + let item_to_send = ItemToSend { + id: item.id, + name: item.name, + description: item.description, + wikidata_id: item.wikidata_id, + custom_properties, // Use the serialized string + }; + + let response = gloo_net::http::Request::post("/api/items") + .json(&item_to_send) + .unwrap() + .send() + .await; + + match response { + Ok(resp) => { + if resp.status() == 200 { + log!("Item saved to database: {:?}", item_to_send); + } else { + log!("Failed to save item: {}", resp.status_text()); + } + } + Err(err) => log!("Failed to save item: {:?}", err), + } + } + let (wikidata_suggestions, set_wikidata_suggestions) = create_signal(HashMap::>::new()); // Fetch Wikidata suggestions @@ -169,6 +210,11 @@ pub fn ItemsList( set_items.update(|items| { for item in items { item.custom_properties.entry(property.clone()).or_insert_with(|| "".to_string()); + // Save the updated item to the database + let item_clone = item.clone(); + spawn_local(async move { + save_item_to_db(item_clone).await; + }); } }); @@ -210,8 +256,6 @@ pub fn ItemsList( // Update item fields let update_item = move |index: usize, field: &str, value: String| { - log!("Updating item at index {}: {}, {}", index, field, value); - log!("Is SSR enabled? {}", cfg!(feature = "ssr")); set_items.update(|items| { if let Some(item) = items.get_mut(index) { match field { @@ -241,36 +285,26 @@ pub fn ItemsList( } } - // // Send the updated item to the server using the API endpoint - // let client_db_item = ClientDbItem { - // id: item.id.clone(), - // name: item.name.clone(), - // description: item.description.clone(), - // wikidata_id: item.wikidata_id.clone(), - // custom_properties: serde_json::to_string(&item.custom_properties).unwrap(), - // }; - - - // spawn_local(async move { - // match update_item_db(client_db_item).await { - // Ok(_) => { - // log!("Item updated successfully on the server"); - // } - // Err(e) => { - // log!("Error updating item on the server: {}", e); - // } - // } - // }); + // Save the updated item to the database + let item_clone = item.clone(); + spawn_local(async move { + save_item_to_db(item_clone).await; + }); } // Automatically add a new row when editing the last row if index == items.len() - 1 && !value.is_empty() { - items.push(Item { + let new_item = Item { id: Uuid::new_v4().to_string(), name: String::new(), description: String::new(), - // reviews: vec![], wikidata_id: None, custom_properties: HashMap::new(), + }; + items.push(new_item.clone()); + + // Save the new item to the database + spawn_local(async move { + save_item_to_db(new_item).await; }); } log!("Items updated: {:?}", items); From fc13b0dae62205137f5f1850ca1a0e6fc0394f3a Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 24 Jan 2025 01:54:25 +0300 Subject: [PATCH 10/20] feat(db): enable db to update items keeping track of the item's id --- src/db.rs | 30 ++++++++++++++++++------------ 1 file changed, 18 insertions(+), 12 deletions(-) diff --git a/src/db.rs b/src/db.rs index 0f9047f..6aa6ee3 100644 --- a/src/db.rs +++ b/src/db.rs @@ -16,7 +16,7 @@ mod db_impl { // Create a new database connection pub fn new(db_path: &str) -> Result { let conn = Connection::open(db_path)?; - logging::log!("Database connection established at: {}", db_path); // Log with Leptos + logging::log!("Database connection established at: {}", db_path); Ok(Database { conn: Arc::new(Mutex::new(conn)), }) @@ -34,7 +34,7 @@ mod db_impl { custom_properties TEXT );", )?; - logging::log!("Database schema created or verified"); // Log with Leptos + logging::log!("Database schema created or verified"); Ok(()) } @@ -43,16 +43,22 @@ mod db_impl { let conn = self.conn.lock().await; let wikidata_id = item.wikidata_id.as_ref().map(|s| s.as_str()).unwrap_or(""); conn.execute( - "INSERT INTO items (id, name, description, wikidata_id, custom_properties) VALUES (?, ?, ?, ?, ?);", - &[ - &item.id, - &item.name, - &item.description, - &wikidata_id.to_string(), - &item.custom_properties, - ], - )?; - logging::log!("Item inserted: {}", item.id); // Log with Leptos + "INSERT INTO items (id, name, description, wikidata_id, custom_properties) + VALUES (?, ?, ?, ?, ?) + ON CONFLICT(id) DO UPDATE SET + name = excluded.name, + description = excluded.description, + wikidata_id = excluded.wikidata_id, + custom_properties = excluded.custom_properties;", + &[ + &item.id, + &item.name, + &item.description, + &wikidata_id.to_string(), + &item.custom_properties, + ], + )?; + logging::log!("Item inserted: {}", item.id); Ok(()) } From c1207f613d13796fc97aa2783c7b7bdb3056c68b Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 24 Jan 2025 02:23:24 +0300 Subject: [PATCH 11/20] feat(db): add selected properties state and update save_item_to_db function to include selected properties --- compareware.db | Bin 12288 -> 12288 bytes src/components/items_list.rs | 32 +++++++++++++++++++++++++------- 2 files changed, 25 insertions(+), 7 deletions(-) diff --git a/compareware.db b/compareware.db index d0da44d58baefb6e826b432ffab981a092c25868..02e08960ce35dac766f720676e70c2fcf6e4feac 100644 GIT binary patch delta 504 zcma)&F-yZh7={xgr3&d;L>v@64g`fBq`6#^6a=*{I$3COiO_4j)Sy;Pu3#ypt-rt` z4&tO+2M6(=xD;3ahKrXvbS>QQ-OKwv-^bgp^=l7nYI?X{Qqv!`SD=G8Max_$;5+l4 zx%kX3oo0HwTB@q+x?8th%chQl9m}_{MQnp%ot_3z!-P9&)oteM?7xDK5vSMUBaLjHd+yf z*8wb@M`0{N)(k}(ZWZ8l1TDBj8hoO&$oEipda}&O^>Pgs?VX!{Pbm1+B-w0T@hJ3hcqX%^K?(+yyvff) z<$ocebrnUihqr`oswuER^en0HSinQcsho!~l#fA?NC~>rU6g(ej1=$-M&L_!{{l|7 Tw5mEw?^::new()); - //signal to store the fetched property labels + // Signal to store the fetched property labels let (property_labels, set_property_labels) = create_signal(HashMap::::new()); + // State to track selected properties + let (selected_properties, set_selected_properties) = create_signal(HashMap::::new()); // Ensure there's an initial empty row if items.get().is_empty() { @@ -47,11 +49,20 @@ pub fn ItemsList( custom_properties: HashMap::new(), }]); } - + // Function to send an item to the backend API - async fn save_item_to_db(item: Item) { + async fn save_item_to_db(item: Item, selected_properties: ReadSignal>) { + // Use a reactive closure to access `selected_properties` + let custom_properties: HashMap = (move || { + let selected_props = selected_properties.get(); // Access the signal inside a reactive closure + item.custom_properties + .into_iter() + .filter(|(key, _)| selected_props.contains_key(key)) // Use the extracted value + .collect() + })(); + // Serialize `custom_properties` to a JSON string - let custom_properties = serde_json::to_string(&item.custom_properties).unwrap(); + let custom_properties = serde_json::to_string(&custom_properties).unwrap(); // Create a new struct to send to the backend #[derive(Serialize, Debug)] @@ -206,14 +217,21 @@ pub fn ItemsList( set_custom_properties.update(|props| { if !props.contains(&property) && !property.is_empty() { props.push(property.clone()); + + //update the selected_properties state when a new property is added + set_selected_properties.update(|selected| { + selected.insert(property.clone(), true); + }); + // Ensure the grid updates reactively set_items.update(|items| { for item in items { item.custom_properties.entry(property.clone()).or_insert_with(|| "".to_string()); + // Save the updated item to the database let item_clone = item.clone(); spawn_local(async move { - save_item_to_db(item_clone).await; + save_item_to_db(item_clone, selected_properties).await; }); } }); @@ -288,7 +306,7 @@ pub fn ItemsList( // Save the updated item to the database let item_clone = item.clone(); spawn_local(async move { - save_item_to_db(item_clone).await; + save_item_to_db(item_clone, selected_properties).await; }); } // Automatically add a new row when editing the last row @@ -304,7 +322,7 @@ pub fn ItemsList( // Save the new item to the database spawn_local(async move { - save_item_to_db(new_item).await; + save_item_to_db(new_item, selected_properties).await; }); } log!("Items updated: {:?}", items); From e0e5fc49c20d392226cc2c17e33431b0c7b51daf Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 24 Jan 2025 15:10:25 +0300 Subject: [PATCH 12/20] feat(db): load items from database on startup. -successfully loading names and description --- src/components/items_list.rs | 62 ++++++++++++++++++++++++++++++++++++ 1 file changed, 62 insertions(+) diff --git a/src/components/items_list.rs b/src/components/items_list.rs index 9551a9f..56e2c64 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -16,11 +16,34 @@ struct WikidataSuggestion { description: Option, } + +#[derive(Deserialize, Debug)] +struct DbItem { + id: String, + name: String, + description: String, + wikidata_id: Option, + custom_properties: String, +} + #[component] pub fn ItemsList( items: ReadSignal>, set_items: WriteSignal>, ) -> impl IntoView { + + // Load items from the database on startup + spawn_local(async move { + match load_items_from_db().await { + Ok(loaded_items) => { + set_items.set(loaded_items); + } + Err(err) => { + log!("Error loading items: {}", err); + } + } + }); + // State to track the currently focused cell let (focused_cell, set_focused_cell) = create_signal(None::); @@ -100,6 +123,45 @@ pub fn ItemsList( } } + //function to load items from database + async fn load_items_from_db() -> Result, String> { + let response = gloo_net::http::Request::get("/api/items") + .send() + .await + .map_err(|err| format!("Failed to fetch items: {:?}", err))?; + + if response.status() == 200 { + // Deserialize into Vec + let db_items = response + .json::>() + .await + .map_err(|err| format!("Failed to parse items: {:?}", err))?; + + // Convert DbItem to Item + let items = db_items + .into_iter() + .map(|db_item| { + // Deserialize `custom_properties` from a JSON string to a HashMap + let custom_properties: HashMap = + serde_json::from_str(&db_item.custom_properties) + .unwrap_or_default(); // Fallback to an empty HashMap if deserialization fails + + Item { + id: db_item.id, + name: db_item.name, + description: db_item.description, + wikidata_id: db_item.wikidata_id, + custom_properties, // Deserialized HashMap + } + }) + .collect(); + + Ok(items) + } else { + Err(format!("Failed to fetch items: {}", response.status_text())) + } + } + let (wikidata_suggestions, set_wikidata_suggestions) = create_signal(HashMap::>::new()); // Fetch Wikidata suggestions From 3fa56abc83931b25f7418f53e9ecf17bfc3e8c25 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 27 Jan 2025 16:34:25 +0300 Subject: [PATCH 13/20] feat(db): persist custom properties from db --- src/components/items_list.rs | 65 +++++++++++++++++++++++++++--------- 1 file changed, 49 insertions(+), 16 deletions(-) diff --git a/src/components/items_list.rs b/src/components/items_list.rs index 56e2c64..7aa55fd 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -31,19 +31,9 @@ pub fn ItemsList( items: ReadSignal>, set_items: WriteSignal>, ) -> impl IntoView { + // State to track selected properties + let (selected_properties, set_selected_properties) = create_signal(HashMap::::new()); - // Load items from the database on startup - spawn_local(async move { - match load_items_from_db().await { - Ok(loaded_items) => { - set_items.set(loaded_items); - } - Err(err) => { - log!("Error loading items: {}", err); - } - } - }); - // State to track the currently focused cell let (focused_cell, set_focused_cell) = create_signal(None::); @@ -58,8 +48,44 @@ pub fn ItemsList( // Signal to store the fetched property labels let (property_labels, set_property_labels) = create_signal(HashMap::::new()); - // State to track selected properties - let (selected_properties, set_selected_properties) = create_signal(HashMap::::new()); + + spawn_local(async move { + match load_items_from_db().await { + Ok(loaded_items) => { + // Set the loaded items + set_items.set(loaded_items.clone()); + + // Derive selected properties from the loaded items + let mut selected_props = HashMap::new(); + let loaded_items_clone = loaded_items.clone(); + for item in loaded_items { + for (property, _) in item.custom_properties { + selected_props.insert(property, true); + } + } + set_selected_properties.set(selected_props); + + // Update the custom_properties signal + let mut custom_props = Vec::new(); + for item in loaded_items_clone { + for (property, _) in &item.custom_properties { + if !custom_props.iter().any(|p| p == property) { + custom_props.push(property.clone()); + } + } + } + log!("Custom properties: {:?}", custom_props); + log!("Updating custom properties signal: {:?}", custom_props); + set_custom_properties.set(custom_props); + + log!("Items after loading: {:?}", items.get()); + } + Err(err) => { + log!("Error loading items: {}", err); + } + } + }); + // Ensure there's an initial empty row if items.get().is_empty() { @@ -132,10 +158,13 @@ pub fn ItemsList( if response.status() == 200 { // Deserialize into Vec + log!("Loading items from DB..."); let db_items = response .json::>() .await .map_err(|err| format!("Failed to parse items: {:?}", err))?; + + log!("Deserialized DB items: {:?}", db_items); // Convert DbItem to Item let items = db_items @@ -145,7 +174,10 @@ pub fn ItemsList( let custom_properties: HashMap = serde_json::from_str(&db_item.custom_properties) .unwrap_or_default(); // Fallback to an empty HashMap if deserialization fails - + + log!("Loaded item: {:?}", db_item.id); + log!("Custom properties: {:?}", custom_properties); + Item { id: db_item.id, name: db_item.name, @@ -155,7 +187,7 @@ pub fn ItemsList( } }) .collect(); - + log!("Converted items: {:?}", items); Ok(items) } else { Err(format!("Failed to fetch items: {}", response.status_text())) @@ -563,6 +595,7 @@ pub fn ItemsList( // Dynamically adding custom properties as columns {move || { let custom_props = custom_properties.get().clone(); + log!("Rendering custom properties: {:?}", custom_props); custom_props.into_iter().map(move |property| { let property_clone = property.clone(); let property_label = property_labels.get().get(&property_clone).cloned().unwrap_or_else(|| property_clone.clone()); From 2455619735d989474f3f0c2360625529ff843068 Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 27 Jan 2025 16:37:32 +0300 Subject: [PATCH 14/20] build(git): add compareware.db file to .gitignore --- compareware.db | Bin 12288 -> 0 bytes 1 file changed, 0 insertions(+), 0 deletions(-) delete mode 100644 compareware.db diff --git a/compareware.db b/compareware.db deleted file mode 100644 index 02e08960ce35dac766f720676e70c2fcf6e4feac..0000000000000000000000000000000000000000 GIT binary patch literal 0 HcmV?d00001 literal 12288 zcmeI$Pixdb6aetarq){6jUuu~9|56-MzTAb{ZkOTt$V1hwXIoD2rRSN*>>n|vL>_G zQWk9e2;#x3w;uc$J$ms{di7HXUYw+s9$aau9^a5;{^Y%x$?rCKxzQAoLobX5Oadjh ziKdYy07A%&x^;D*7fm&$&O543`j?d%^6>F#*7!j(xnq($Hojh#fbUTN1yBG5Pyhu` z00mG01yBG5P~fiuhc~pF`Fvh`FC^>qxjN7e;`EVO*=*F^2DtU*W&_e8n45(OMRdV! zymDcEbM<+B^EEtcyk3|X7_b393tfBR!dk1@oE+EXu^)+{6k(8tPmX#kc0`v+*8aWz z%W%*hM&Xc0Qt&vP@sI7z&u6CdkMbH3L6`5wZ~E$k+iWDmwBJrYzg;pt?LyKTA4Hj%R{lV{cX+o~bDSbA z`c|jlJ8BZvu^4lhRc4G=xzB55MxXL{6#G1o5$i)wY>grgVGmZeBN0o%0wMQct_)!W zC0Jg8@9#c9{RvPz8KHE3gWAQ5ee8Rt Date: Mon, 27 Jan 2025 16:38:23 +0300 Subject: [PATCH 15/20] build(git): add compareware.db file to .gitignore --- .gitignore | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.gitignore b/.gitignore index ad547b6..a2868de 100644 --- a/.gitignore +++ b/.gitignore @@ -15,4 +15,7 @@ playwright/.cache/ # Sass cache dir .sass-cache/ -.idea/ \ No newline at end of file +.idea/ + +# Ignore database file +compareware.db \ No newline at end of file From c38f19d76cf64c3b18926ffd96efce8ceb27db2d Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 27 Jan 2025 16:48:28 +0300 Subject: [PATCH 16/20] feat(labels): persist property labels on refresh. --- src/components/items_list.rs | 13 +++++++++++-- 1 file changed, 11 insertions(+), 2 deletions(-) diff --git a/src/components/items_list.rs b/src/components/items_list.rs index 7aa55fd..616b738 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -74,10 +74,19 @@ pub fn ItemsList( } } } - log!("Custom properties: {:?}", custom_props); - log!("Updating custom properties signal: {:?}", custom_props); + + let custom_props_clone = custom_props.clone(); set_custom_properties.set(custom_props); + // Fetch labels for the custom properties + let property_ids = custom_props_clone; + let labels = fetch_property_labels(property_ids).await; + set_property_labels.update(|labels_map| { + for (key, value) in labels { + labels_map.insert(key, value); + } + }); + log!("Items after loading: {:?}", items.get()); } Err(err) => { From afa3bd3ecea4ff6a5891eff37e47fb1433caa8ff Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 28 Jan 2025 02:42:16 +0300 Subject: [PATCH 17/20] feat(Item_list): update ItemsList component to include delete button for property input fields --- src/components/items_list.rs | 65 ++++++++++++++++++++++-------------- 1 file changed, 40 insertions(+), 25 deletions(-) diff --git a/src/components/items_list.rs b/src/components/items_list.rs index 616b738..ed1bf86 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -608,33 +608,48 @@ pub fn ItemsList( custom_props.into_iter().map(move |property| { let property_clone = property.clone(); let property_label = property_labels.get().get(&property_clone).cloned().unwrap_or_else(|| property_clone.clone()); + let property_clone_for_button = property_clone.clone(); + let property_clone_for_cells = property_clone.clone(); view! { - { property_label } - {move || { - let property_clone = property_clone.clone(); // Clone `property_clone` again for the inner closure - items.get().iter().enumerate().map(move |(index, item)| { - let property_clone_for_closure = property_clone.clone(); - view! { - - - - } - }).collect::>() - }} + + { property_label } + + + {items.get().iter().enumerate().map(move |(index, item)| { + let property_clone_for_closure = property_clone_for_cells.clone(); + view! { + + + + } + }).collect::>()} } }).collect::>() From 68b458df5ea40226c7f9993c063dbdcdcb6d457d Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 28 Jan 2025 14:36:17 +0300 Subject: [PATCH 18/20] feat(db): enable user to delete items and properties from the database. --- src/api.rs | 30 ++++++++++++++++++ src/components/items_list.rs | 61 +++++++++++++++++++++++++++++++----- src/db.rs | 15 +++++++++ src/main.rs | 8 +++-- 4 files changed, 103 insertions(+), 11 deletions(-) diff --git a/src/api.rs b/src/api.rs index 4ef7ea3..de3d6b7 100644 --- a/src/api.rs +++ b/src/api.rs @@ -32,4 +32,34 @@ pub async fn create_item( HttpResponse::InternalServerError().body("Failed to insert item") } } +} + +#[cfg(feature = "ssr")] +pub async fn delete_item( + db: web::Data>>, + item_id: web::Path, +) -> HttpResponse { + let db = db.lock().await; + match db.delete_item(&item_id).await { + Ok(_) => HttpResponse::Ok().body("Item deleted"), + Err(err) => { + leptos::logging::error!("Failed to delete item: {:?}", err); + HttpResponse::InternalServerError().body("Failed to delete item") + } + } +} + +#[cfg(feature = "ssr")] +pub async fn delete_property( + db: web::Data>>, + property: web::Path, +) -> HttpResponse { + let db = db.lock().await; + match db.delete_property(&property).await { + Ok(_) => HttpResponse::Ok().body("Property deleted"), + Err(err) => { + leptos::logging::error!("Failed to delete property: {:?}", err); + HttpResponse::InternalServerError().body("Failed to delete property") + } + } } \ No newline at end of file diff --git a/src/components/items_list.rs b/src/components/items_list.rs index ed1bf86..51dcc6c 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -203,6 +203,58 @@ pub fn ItemsList( } } + // remove an item + let remove_item = move |index: usize| { + let item_id = items.get()[index].id.clone(); + spawn_local(async move { + let response = gloo_net::http::Request::delete(&format!("/api/items/{}", item_id)) + .send() + .await; + match response { + Ok(resp) => { + if resp.status() == 200 { + set_items.update(|items| { + items.remove(index); + }); + log!("Item deleted: {}", item_id); + } else { + log!("Failed to delete item: {}", resp.status_text()); + } + } + Err(err) => log!("Failed to delete item: {:?}", err), + } + }); + }; + + let remove_property = move |property: String| { + spawn_local(async move { + let response = gloo_net::http::Request::delete(&format!("/api/properties/{}", property)) + .send() + .await; + match response { + Ok(resp) => { + if resp.status() == 200 { + set_custom_properties.update(|props| { + props.retain(|p| p != &property); + }); + set_selected_properties.update(|selected| { + selected.remove(&property); + }); + set_items.update(|items| { + for item in items { + item.custom_properties.remove(&property); + } + }); + log!("Property deleted: {}", property); + } else { + log!("Failed to delete property: {}", resp.status_text()); + } + } + Err(err) => log!("Failed to delete property: {:?}", err), + } + }); + }; + let (wikidata_suggestions, set_wikidata_suggestions) = create_signal(HashMap::>::new()); // Fetch Wikidata suggestions @@ -432,14 +484,6 @@ pub fn ItemsList( }); }; - - // Remove an item - let remove_item = move |index: usize| { - set_items.update(|items| { - items.remove(index); - }); - }; - // List of properties to display as rows let properties = vec!["Name", "Description", "Actions"]; @@ -616,6 +660,7 @@ pub fn ItemsList( { property_label } - {items.get().iter().enumerate().map(move |(index, item)| { + {move || { + let property_clone_for_cells = property_clone.clone(); + items.get().iter().enumerate().map(move |(index, item)| { let property_clone_for_closure = property_clone_for_cells.clone(); view! { @@ -706,6 +708,7 @@ pub fn ItemsList( } }).collect::>()} + } } }).collect::>()