diff --git a/src/api.rs b/src/api.rs index bdb253d..311fcd1 100644 --- a/src/api.rs +++ b/src/api.rs @@ -151,4 +151,32 @@ pub async fn delete_property_by_url( HttpResponse::InternalServerError().body("Failed to delete property by URL") } } +} + +#[cfg(feature = "ssr")] +pub async fn get_selected_properties( + db: web::Data>>, + url: web::Path, +) -> HttpResponse { + let db = db.lock().await; + match db.get_selected_properties(&url).await { + Ok(properties) => HttpResponse::Ok().json(properties), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()) + } +} + +#[cfg(feature = "ssr")] +pub async fn add_selected_property( + db: web::Data>>, + url: web::Path, + property: web::Json, +) -> HttpResponse { + let url = url.into_inner(); + let property = property.into_inner(); + + let db = db.lock().await; + match db.add_selected_property(&url, &property).await { + Ok(_) => HttpResponse::Ok().finish(), + Err(e) => HttpResponse::InternalServerError().body(e.to_string()) + } } \ No newline at end of file diff --git a/src/components/items_list.rs b/src/components/items_list.rs index 4711230..24e4fb1 100644 --- a/src/components/items_list.rs +++ b/src/components/items_list.rs @@ -47,7 +47,60 @@ pub async fn load_items_from_db(current_url: &str) -> Result, String> format!("Failed to parse items: {:?}", err) })?; log!("[DEBUG] Successfully parsed {} items", items.len()); - Ok(items) + + // Get the selected properties for the current URL + let selected_properties_response = gloo_net::http::Request::get( + &format!("/api/urls/{}/properties", encoded_url) + ) + .send() + .await + .map_err(|err| { + log!("[ERROR] Network error: {:?}", err); + format!("Failed to fetch selected properties: {:?}", err) + })?; + if selected_properties_response.status() == 200 { + let selected_properties: Vec = selected_properties_response + .json() + .await + .map_err(|err| { + log!("[ERROR] JSON parsing error: {:?}", err); + format!("Failed to parse selected properties: {:?}", err) + })?; + log!("[DEBUG] Successfully received selected properties"); + + // Filter the items to only include the selected properties + let filtered_items = items + .into_iter() + .map(|item| { + let filtered_custom_properties = item + .custom_properties + .into_iter() + .filter(|(key, _)| selected_properties.contains(key)) + .collect(); + Item { + id: item.id, + name: item.name, + description: item.description, + wikidata_id: item.wikidata_id, + custom_properties: filtered_custom_properties, + } + }) + .collect(); + Ok(filtered_items) + } else { + let body = selected_properties_response.text().await.unwrap_or_default(); + log!("[ERROR] Server error details: + Status: {} + URL: {} + Response Body: {} + Request URL: {}", + selected_properties_response.status(), + api_url, + body, + current_url + ); + Err(format!("Server error ({}): {}", selected_properties_response.status(), body)) + } } else { let body = response.text().await.unwrap_or_default(); log!("[ERROR] Server error details: @@ -465,7 +518,44 @@ pub fn ItemsList( Arc::new(move |property: String| { // Normalize the property ID let normalized_property = property.replace("http://www.wikidata.org/prop/", ""); - + let normalized_property_clone = normalized_property.clone(); + + // Check if property is already selected + if !selected_properties.get().contains_key(&normalized_property) && !normalized_property.is_empty() { + // Add property to selected properties + set_selected_properties.update(|selected| { + selected.insert(normalized_property.clone(), true); + }); + + // Save the selected property to the database + spawn_local({ + let current_url = Rc::clone(¤t_url); + let normalized_property = normalized_property_clone.clone(); + async move { + let response = gloo_net::http::Request::post( + &format!("/api/urls/{}/properties", encode(¤t_url)) + ) + .json(&normalized_property) + .unwrap() + .send() + .await; + + match response { + Ok(resp) => { + if resp.status() == 200 { + log!("Property saved successfully"); + } else { + log!("Error saving property: {}", resp.status_text()); + } + } + Err(err) => { + log!("Error saving property: {:?}", err); + } + } + } + }); + } + set_custom_properties.update(|props| { if !props.contains(&normalized_property) && !normalized_property.is_empty() { props.push(normalized_property.clone()); diff --git a/src/db.rs b/src/db.rs index f942023..048bad8 100644 --- a/src/db.rs +++ b/src/db.rs @@ -67,8 +67,22 @@ mod db_impl { eprintln!("Failed creating items table: {}", e); e })?; + + // 4. Table for selected properties + conn.execute_batch( + "CREATE TABLE IF NOT EXISTS selected_properties ( + url_id INTEGER NOT NULL, + property_id INTEGER NOT NULL, + PRIMARY KEY (url_id, property_id), + FOREIGN KEY (url_id) REFERENCES urls(id) ON DELETE CASCADE, + FOREIGN KEY (property_id) REFERENCES properties(id) ON DELETE CASCADE + );" + ).map_err(|e| { + eprintln!("Failed creating properties table: {}", e); + e + })?; - // 4. Junction table for custom properties + // 5. Junction table for custom properties conn.execute_batch( "CREATE TABLE IF NOT EXISTS item_properties ( item_id TEXT NOT NULL, @@ -373,6 +387,54 @@ mod db_impl { tx.commit()?; Ok(()) } + + pub async fn add_selected_property(&self, url: &str, property: &str) -> Result<(), Error> { + let mut conn = self.conn.lock().await; + let tx = conn.transaction()?; + + // Get URL ID + let url_id = tx.query_row( + "SELECT id FROM urls WHERE url = ?", + [url], + |row| row.get::<_, i64>(0) + )?; + + // Get/Create property + let prop_id = match tx.query_row( + "SELECT id FROM properties WHERE name = ?", + [property], + |row| row.get::<_, i64>(0) + ) { + Ok(id) => id, + Err(_) => { + tx.execute("INSERT INTO properties (name) VALUES (?)", [property])?; + tx.last_insert_rowid() + } + }; + + // Insert into selected_properties + tx.execute( + "INSERT OR IGNORE INTO selected_properties (url_id, property_id) VALUES (?, ?)", + [url_id, prop_id] + )?; + + tx.commit()?; + Ok(()) + } + + pub async fn get_selected_properties(&self, url: &str) -> Result, Error> { + let conn = self.conn.lock().await; + let mut stmt = conn.prepare( + "SELECT p.name + FROM selected_properties sp + JOIN properties p ON sp.property_id = p.id + JOIN urls u ON sp.url_id = u.id + WHERE u.url = ?" + )?; + + let properties = stmt.query_map([url], |row| row.get(0))?; + properties.collect() + } // function to log database state pub async fn debug_dump(&self) -> Result<(), Error> { diff --git a/src/main.rs b/src/main.rs index 0619868..c967b08 100644 --- a/src/main.rs +++ b/src/main.rs @@ -3,7 +3,7 @@ use actix_web::{web, HttpResponse, Responder}; use std::sync::Arc; use tokio::sync::Mutex; use compareware::db::Database; -use compareware::api::{ItemRequest,create_item, get_items, delete_item_by_url}; +use compareware::api::{ItemRequest,create_item, get_items, get_selected_properties, add_selected_property}; use compareware::models::item::Item; #[actix_web::main] @@ -14,7 +14,7 @@ async fn main() -> std::io::Result<()> { use leptos_actix::{generate_route_list, LeptosRoutes}; use compareware::app::*; use compareware::db::Database; - use compareware::api::{get_items, create_item, delete_item, delete_property}; // Import API handlers + use compareware::api::{delete_item, delete_property}; // Import API handlers use std::sync::Arc; use tokio::sync::Mutex; @@ -50,6 +50,8 @@ async fn main() -> std::io::Result<()> { .route("/items", web::get().to(get_items_handler)) // GET items by URL .route("/items", web::post().to(create_item_handler)) // Create item for URL .route("/items/{item_id}", web::delete().to(delete_item)) // Delete item for URL + .route("/properties", web::get().to(get_selected_properties_handler)) + .route("/properties", web::post().to(add_selected_property_handler)) .route("/properties/{property}", web::delete().to(delete_property)) // Delete property for URL ) ) @@ -98,13 +100,30 @@ async fn create_item_handler( create_item(db, web::Json(request)).await } -// Handler to delete an item for a specific URL -async fn delete_item_handler( +// // Handler to delete an item for a specific URL +// async fn delete_item_handler( +// db: web::Data>>, +// path: web::Path<(String, String)>, +// ) -> impl Responder { +// let (url, item_id) = path.into_inner(); +// delete_item_by_url(db, web::Path::from(url), web::Path::from(item_id)).await +// } + +#[cfg(feature = "ssr")] +async fn get_selected_properties_handler( db: web::Data>>, - path: web::Path<(String, String)>, + url: web::Path, ) -> impl Responder { - let (url, item_id) = path.into_inner(); - delete_item_by_url(db, web::Path::from(url), web::Path::from(item_id)).await + get_selected_properties(db, url).await +} + +#[cfg(feature = "ssr")] +async fn add_selected_property_handler( + db: web::Data>>, + url: web::Path, + property: web::Json, +) -> impl Responder { + add_selected_property(db, url, property).await } #[cfg(feature = "ssr")] // Define the index handler