From 85dce655e4b98281863997a88d948d197cd4f8cd Mon Sep 17 00:00:00 2001 From: ryan Date: Mon, 17 Mar 2025 22:41:23 +0300 Subject: [PATCH 1/5] feat(auto-search): remove search button and have autosearch while typing --- src/components/items_list.rs | 83 +++++++++++++++++++----------------- 1 file changed, 44 insertions(+), 39 deletions(-) diff --git a/src/components/items_list.rs b/src/components/items_list.rs index 24e4fb1..2acffa0 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -352,32 +352,44 @@ pub fn ItemsList( let (wikidata_suggestions, set_wikidata_suggestions) = create_signal(HashMap::>::new()); // Function to fetch Wikidata suggestions - let fetch_wikidata_suggestions = move |key: String, query: String| { - log!("Fetching suggestions for key: {}, query: {}", key, query); - spawn_local(async move { - if query.is_empty() { - set_wikidata_suggestions.update(|suggestions| { - suggestions.remove(&key); - }); - return; - } + let fetch_wikidata_suggestions = { + let mut debounce_timers = std::collections::HashMap::::new(); + Rc::new(move |key: String, query: String| { + log!("Fetching suggestions for key: {}, query: {}", key, query); - let url = format!( - "https://www.wikidata.org/w/api.php?action=wbsearchentities&search={}&language=en&limit=5&format=json&origin=*", - query - ); + // Cancel previous timer for this key + debounce_timers.remove(&key); - match gloo_net::http::Request::get(&url).send().await { - Ok(response) => { - if let Ok(data) = response.json::().await { - set_wikidata_suggestions.update(|suggestions| { - suggestions.insert(key, data.search); - }); - } + // Store new timer + let timer = gloo_timers::future::TimeoutFuture::new(300); + debounce_timers.insert(key.clone(), timer); + + spawn_local(async move { + timer.await; + if query.is_empty() { + set_wikidata_suggestions.update(|suggestions| { + suggestions.remove(&key); + }); + return; } - Err(_) => log!("Failed to fetch Wikidata suggestions"), - } - }); + + let url = format!( + "https://www.wikidata.org/w/api.php?action=wbsearchentities&search={}&language=en&limit=5&format=json&origin=*", + query + ); + + match gloo_net::http::Request::get(&url).send().await { + Ok(response) => { + if let Ok(data) = response.json::().await { + set_wikidata_suggestions.update(|suggestions| { + suggestions.insert(key, data.search); + }); + } + } + Err(_) => log!("Failed to fetch Wikidata suggestions"), + } + }); + }) }; //function to fetch properties @@ -724,6 +736,7 @@ pub fn ItemsList( { property } {move || items.get().iter().enumerate().map(|(index, item)| { let update_item_clone = Arc::clone(&update_item_cloned); + let fetch_wikidata_suggestions_clone = Rc::clone(&fetch_wikidata_suggestions); view! { {match property { @@ -732,8 +745,13 @@ pub fn ItemsList( - {move || { if *show_suggestions.get().get(&format!("name-{}", index)).unwrap_or(&false) { log!("Rendering suggestions list"); From 3126d90f5af29b55be8685360244bf56a2cf07e1 Mon Sep 17 00:00:00 2001 From: ryan Date: Tue, 18 Mar 2025 23:47:47 +0300 Subject: [PATCH 2/5] fix(auto search): retrace my steps to resolve ownership errors --- src/components/items_list.rs | 81 +++++++++++++++++------------------- 1 file changed, 38 insertions(+), 43 deletions(-) diff --git a/src/components/items_list.rs b/src/components/items_list.rs index 2acffa0..24e4fb1 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -352,44 +352,32 @@ pub fn ItemsList( let (wikidata_suggestions, set_wikidata_suggestions) = create_signal(HashMap::>::new()); // Function to fetch Wikidata suggestions - let fetch_wikidata_suggestions = { - let mut debounce_timers = std::collections::HashMap::::new(); - Rc::new(move |key: String, query: String| { - log!("Fetching suggestions for key: {}, query: {}", key, query); + let fetch_wikidata_suggestions = move |key: String, query: String| { + log!("Fetching suggestions for key: {}, query: {}", key, query); + spawn_local(async move { + if query.is_empty() { + set_wikidata_suggestions.update(|suggestions| { + suggestions.remove(&key); + }); + return; + } - // Cancel previous timer for this key - debounce_timers.remove(&key); + let url = format!( + "https://www.wikidata.org/w/api.php?action=wbsearchentities&search={}&language=en&limit=5&format=json&origin=*", + query + ); - // Store new timer - let timer = gloo_timers::future::TimeoutFuture::new(300); - debounce_timers.insert(key.clone(), timer); - - spawn_local(async move { - timer.await; - if query.is_empty() { - set_wikidata_suggestions.update(|suggestions| { - suggestions.remove(&key); - }); - return; - } - - let url = format!( - "https://www.wikidata.org/w/api.php?action=wbsearchentities&search={}&language=en&limit=5&format=json&origin=*", - query - ); - - match gloo_net::http::Request::get(&url).send().await { - Ok(response) => { - if let Ok(data) = response.json::().await { - set_wikidata_suggestions.update(|suggestions| { - suggestions.insert(key, data.search); - }); - } + match gloo_net::http::Request::get(&url).send().await { + Ok(response) => { + if let Ok(data) = response.json::().await { + set_wikidata_suggestions.update(|suggestions| { + suggestions.insert(key, data.search); + }); } - Err(_) => log!("Failed to fetch Wikidata suggestions"), } - }); - }) + Err(_) => log!("Failed to fetch Wikidata suggestions"), + } + }); }; //function to fetch properties @@ -736,7 +724,6 @@ pub fn ItemsList( { property } {move || items.get().iter().enumerate().map(|(index, item)| { let update_item_clone = Arc::clone(&update_item_cloned); - let fetch_wikidata_suggestions_clone = Rc::clone(&fetch_wikidata_suggestions); view! { {match property { @@ -745,13 +732,8 @@ pub fn ItemsList( + {move || { if *show_suggestions.get().get(&format!("name-{}", index)).unwrap_or(&false) { log!("Rendering suggestions list"); From 12f4043e83a75f05edb2389246eac831ba44a18c Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 21 Mar 2025 01:32:32 +0300 Subject: [PATCH 3/5] feat(db): add global_item_id to track items accross urls --- src/db.rs | 70 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 51 insertions(+), 19 deletions(-) diff --git a/src/db.rs b/src/db.rs index 29eacbd..927fedf 100644 --- a/src/db.rs +++ b/src/db.rs @@ -8,7 +8,7 @@ mod db_impl { use std::collections::{HashMap, HashSet}; use std::sync::Arc; use tokio::sync::Mutex; - + use uuid::Uuid; #[cfg(test)] mod tests { use super::*; @@ -271,6 +271,14 @@ mod db_impl { e })?; + // Add a global_item_id column to the items table + conn.execute_batch( + "ALTER TABLE items ADD COLUMN global_item_id TEXT;" + ).map_err(|e| { + eprintln!("Failed adding global_item_id to items table: {}", e); + e + })?; + // 4. Table for selected properties conn.execute_batch( "CREATE TABLE IF NOT EXISTS selected_properties ( @@ -289,11 +297,11 @@ mod db_impl { // 5. Junction table for custom properties conn.execute_batch( "CREATE TABLE IF NOT EXISTS item_properties ( - item_id TEXT NOT NULL, + global_item_id TEXT NOT NULL, property_id INTEGER NOT NULL, value TEXT NOT NULL, - PRIMARY KEY (item_id, property_id), - FOREIGN KEY (item_id) REFERENCES items(id) ON DELETE CASCADE, + PRIMARY KEY (global_item_id, property_id), + FOREIGN KEY (global_item_id) REFERENCES items(global_item_id) ON DELETE CASCADE, FOREIGN KEY (property_id) REFERENCES properties(id) ON DELETE CASCADE );", ) @@ -383,7 +391,8 @@ mod db_impl { SELECT i.id, i.wikidata_id, - i.item_order + i.item_order, + i.global_item_id FROM items i WHERE i.url_id = ? ORDER BY i.item_order ASC @@ -396,17 +405,17 @@ mod db_impl { json_group_object(p.name, ip.value) as custom_properties FROM ordered_items oi LEFT JOIN item_properties ip - ON oi.id = ip.item_id + ON oi.global_item_id = ip.global_item_id AND ip.property_id NOT IN ( SELECT id FROM properties WHERE name IN ('name', 'description') ) LEFT JOIN properties p ON ip.property_id = p.id LEFT JOIN item_properties name_ip - ON oi.id = name_ip.item_id + ON oi.global_item_id = name_ip.global_item_id AND name_ip.property_id = (SELECT id FROM properties WHERE name = 'name') LEFT JOIN item_properties desc_ip - ON oi.id = desc_ip.item_id + ON oi.global_item_id = desc_ip.global_item_id AND desc_ip.property_id = (SELECT id FROM properties WHERE name = 'description') GROUP BY oi.id ORDER BY oi.item_order ASC" @@ -495,18 +504,36 @@ mod db_impl { |row| row.get(0), )?; + let global_item_id = match tx.query_row( + "SELECT ip.global_item_id + FROM item_properties ip + JOIN properties p ON ip.property_id = p.id + WHERE p.name = 'name' AND ip.value = ? LIMIT 1", + [&item.name], + |row| row.get::<_, String>(0), + ) { + Ok(id) => id, // Reuse existing global_item_id + Err(rusqlite::Error::QueryReturnedNoRows) => { + let new_id = Uuid::new_v4().to_string(); // Generate a new global_item_id + new_id + } + Err(e) => return Err(e.into()), + }; + log!("[DB] Upserting item"); tx.execute( - "INSERT INTO items (id, url_id, wikidata_id, item_order) - VALUES (?, ?, ?, ?) + "INSERT INTO items (id, url_id, wikidata_id, item_order, global_item_id) + VALUES (?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET url_id = excluded.url_id, - wikidata_id = excluded.wikidata_id", + wikidata_id = excluded.wikidata_id, + global_item_id = excluded.global_item_id", rusqlite::params![ &item.id, url_id, &item.wikidata_id, - max_order + 1 + max_order + 1, + &global_item_id ], )?; log!("[DB] Item upserted successfully"); @@ -523,11 +550,11 @@ mod db_impl { let prop_id = self.get_or_create_property(&mut tx, prop).await?; tx.execute( - "INSERT INTO item_properties (item_id, property_id, value) + "INSERT INTO item_properties (global_item_id, property_id, value) VALUES (?, ?, ?) - ON CONFLICT(item_id, property_id) DO UPDATE SET + ON CONFLICT(global_item_id, property_id) DO UPDATE SET value = excluded.value", - rusqlite::params![&item.id, prop_id, value], + rusqlite::params![&global_item_id, prop_id, value], )?; } @@ -538,7 +565,7 @@ mod db_impl { "SELECT p.name, ip.value FROM item_properties ip JOIN properties p ON ip.property_id = p.id - WHERE ip.item_id = ?", + WHERE ip.global_item_id = ?", )?; let mapped_rows = stmt.query_map([&item.id], |row| { @@ -588,6 +615,11 @@ mod db_impl { "DELETE FROM items WHERE id = ? AND url_id = ?", [item_id, &url_id.to_string()], )?; + + tx.execute( + "DELETE FROM item_properties WHERE global_item_id = ?", + [item_id], + )?; tx.commit()?; Ok(()) @@ -608,10 +640,10 @@ mod db_impl { WHERE property_id IN ( SELECT id FROM properties WHERE name = ? ) - AND item_id IN ( - SELECT id FROM items WHERE url_id = ? + AND global_item_id IN ( + SELECT global_item_id FROM items WHERE url_id = ? )", - [property, &url_id.to_string()], + [property, &url_id.to_string()], // Use global_item_id instead of item_id )?; tx.commit()?; From fe98c56872e4b4a94070babeee7c6977ac57ea89 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 21 Mar 2025 01:45:16 +0300 Subject: [PATCH 4/5] fix(db): check if the global_item_id column exists before trying to add it. --- src/db.rs | 22 +++++++++++++++------- 1 file changed, 15 insertions(+), 7 deletions(-) diff --git a/src/db.rs b/src/db.rs index 927fedf..059a261 100644 --- a/src/db.rs +++ b/src/db.rs @@ -271,13 +271,21 @@ mod db_impl { e })?; - // Add a global_item_id column to the items table - conn.execute_batch( - "ALTER TABLE items ADD COLUMN global_item_id TEXT;" - ).map_err(|e| { - eprintln!("Failed adding global_item_id to items table: {}", e); - e - })?; + // Check if the global_item_id column exists + let mut stmt = conn.prepare("PRAGMA table_info(items);")?; + let columns: Vec = stmt + .query_map([], |row| row.get(1))? // Column 1 contains the column names + .collect::>()?; + + if !columns.contains(&"global_item_id".to_string()) { + conn.execute_batch( + "ALTER TABLE items ADD COLUMN global_item_id TEXT;" + ) + .map_err(|e| { + eprintln!("Failed adding global_item_id to items table: {}", e); + e + })?; + } // 4. Table for selected properties conn.execute_batch( From 69430fae8a5e1d06f2b7c020b6386b000d0c3665 Mon Sep 17 00:00:00 2001 From: ryan Date: Fri, 21 Mar 2025 16:21:19 +0300 Subject: [PATCH 5/5] fix(db): clean up property deletion to be scoped per url --- src/db.rs | 64 ++++++++++++++++++++++++++++++++++++++++--------------- 1 file changed, 47 insertions(+), 17 deletions(-) diff --git a/src/db.rs b/src/db.rs index 059a261..a59fa76 100644 --- a/src/db.rs +++ b/src/db.rs @@ -276,7 +276,7 @@ mod db_impl { let columns: Vec = stmt .query_map([], |row| row.get(1))? // Column 1 contains the column names .collect::>()?; - + if !columns.contains(&"global_item_id".to_string()) { conn.execute_batch( "ALTER TABLE items ADD COLUMN global_item_id TEXT;" @@ -317,6 +317,23 @@ mod db_impl { eprintln!("Failed creating item_properties table: {}", e); e })?; + + // 6. Junction table for deleted properties + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS deleted_properties ( + url_id INTEGER NOT NULL, + global_item_id TEXT NOT NULL, + property_id INTEGER NOT NULL, + PRIMARY KEY (url_id, global_item_id, property_id), + FOREIGN KEY (url_id) REFERENCES urls(id) ON DELETE CASCADE, + FOREIGN KEY (global_item_id) REFERENCES items(global_item_id) ON DELETE CASCADE, + FOREIGN KEY (property_id) REFERENCES properties(id) ON DELETE CASCADE + );", + ).map_err(|e| { + eprintln!("Failed creating item_properties table: {}", e); + e + })?; + Ok(()) } @@ -415,7 +432,9 @@ mod db_impl { LEFT JOIN item_properties ip ON oi.global_item_id = ip.global_item_id AND ip.property_id NOT IN ( - SELECT id FROM properties WHERE name IN ('name', 'description') + SELECT property_id + FROM deleted_properties + WHERE url_id = ? AND global_item_id = oi.global_item_id ) LEFT JOIN properties p ON ip.property_id = p.id @@ -430,8 +449,7 @@ mod db_impl { )?; // Change from HashMap to Vec to preserve order - - let rows = stmt.query_map([url_id], |row| { + let rows = stmt.query_map([url_id, url_id], |row| { let custom_props_json: String = row.get(4)?; let custom_properties: HashMap = serde_json::from_str(&custom_props_json) .unwrap_or_default(); @@ -637,23 +655,35 @@ mod db_impl { pub async fn delete_property_by_url(&self, url: &str, property: &str) -> Result<(), Error> { let mut conn = self.conn.lock().await; let tx = conn.transaction()?; - + // Get URL ID let url_id: i64 = tx.query_row("SELECT id FROM urls WHERE url = ?", [url], |row| row.get(0))?; - - // Delete property from all items in this URL - tx.execute( - "DELETE FROM item_properties - WHERE property_id IN ( - SELECT id FROM properties WHERE name = ? - ) - AND global_item_id IN ( - SELECT global_item_id FROM items WHERE url_id = ? - )", - [property, &url_id.to_string()], // Use global_item_id instead of item_id + + // Get property ID + let property_id: i64 = tx.query_row( + "SELECT id FROM properties WHERE name = ?", + [property], + |row| row.get(0), )?; - + + // Get all global_item_ids for this URL + { + let mut stmt = tx.prepare("SELECT global_item_id FROM items WHERE url_id = ?")?; + let global_item_ids: Vec = stmt + .query_map([url_id], |row| row.get(0))? + .collect::>()?; + + // Insert into deleted_properties for each global_item_id + for global_item_id in global_item_ids { + tx.execute( + "INSERT OR IGNORE INTO deleted_properties (url_id, global_item_id, property_id) + VALUES (?, ?, ?)", + rusqlite::params![url_id, global_item_id, property_id], + )?; + } + } + tx.commit()?; Ok(()) }