use crate::components::editable_cell::EditableCell; use crate::components::editable_cell::InputType; use leptos::*; 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 chrono::{DateTime, Utc}; use gloo_net::http::Request; use serde_json::Value; #[derive(Deserialize, Clone, Debug)] struct WikidataSuggestion { id: String, label: String, 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 { // State to track selected properties let (selected_properties, set_selected_properties) = create_signal(HashMap::::new()); // State to track the currently focused cell let (focused_cell, set_focused_cell) = create_signal(None::); // State to manage dynamic property names let (custom_properties, set_custom_properties) = create_signal(Vec::::new()); // State to manage suggestions visibility let (show_suggestions, set_show_suggestions) = create_signal(HashMap::::new()); // Cache to store fetched properties let (fetched_properties, set_fetched_properties) = create_signal(HashMap::::new()); // Signal to store the fetched property labels let (property_labels, set_property_labels) = create_signal(HashMap::::new()); // Load items from the database on component mount spawn_local(async move { match load_items_from_db().await { Ok(loaded_items) => { // Set the loaded items if loaded_items.is_empty() { // Initialize with one empty item if the database is empty set_items.set(vec![Item { id: Uuid::new_v4().to_string(), name: String::new(), description: String::new(), wikidata_id: None, custom_properties: HashMap::new(), }]); } else { set_items.set(loaded_items.clone()); } // Derive selected properties from the loaded items let mut selected_props = HashMap::new(); 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 { for (property, _) in &item.custom_properties { if !custom_props.contains(property) { custom_props.push(property.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) => { log!("Error loading items: {}", err); } } }); // Ensure there's an initial empty row if items.get().is_empty() { set_items.set(vec![Item { id: Uuid::new_v4().to_string(), name: String::new(), description: String::new(), wikidata_id: None, custom_properties: HashMap::new(), }]); } // Function to send an item to the backend API 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(); item.custom_properties .into_iter() .filter(|(key, _)| selected_props.contains_key(key)) .collect() })(); // Serialize `custom_properties` to a JSON string let custom_properties = serde_json::to_string(&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, } let item_to_send = ItemToSend { id: item.id, name: item.name, description: item.description, wikidata_id: item.wikidata_id, custom_properties, }; 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), } } // Function to load items from the 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(); Item { id: db_item.id, name: db_item.name, description: db_item.description, wikidata_id: db_item.wikidata_id, custom_properties, } }) .collect(); Ok(items) } else { Err(format!("Failed to fetch items: {}", response.status_text())) } } // Function to 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), } }); }; // Function to remove a property 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), } }); }; // State to store Wikidata suggestions 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 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 handle different nested JSON types for property values async fn parse_property_value(value: &serde_json::Value) -> String { match value { serde_json::Value::String(text) => text.clone(), serde_json::Value::Number(num) => num.to_string(), serde_json::Value::Object(map) => { // Handle time values if let Some(time_value) = map.get("time") { let precision = map.get("precision").and_then(|p| p.as_u64()).unwrap_or(11); if let Some(time_str) = time_value.as_str() { if let Ok(parsed_date) = chrono::DateTime::parse_from_rfc3339(time_str.trim_start_matches('+')) { return match precision { 9 => parsed_date.format("%Y").to_string(), // Year precision 10 => parsed_date.format("%Y-%m").to_string(), // Month precision 11 => parsed_date.format("%Y-%m-%d").to_string(), // Day precision _ => parsed_date.format("%Y-%m-%d %H:%M:%S").to_string(), }; } } return "Invalid time format".to_string(); } // Handle Wikidata entity references if let Some(id) = map.get("id") { let entity_id = id.as_str().unwrap_or(""); if entity_id.starts_with("Q") { return fetch_entity_labels(vec![entity_id]).await; } } serde_json::to_string(map).unwrap_or("Complex Object".to_string()) } _ => "Unsupported data type".to_string(), } } // Function to fetch labels for multiple Wikidata entities async fn fetch_entity_labels(entity_ids: Vec) -> Result, String> { let query = format!( "PREFIX wdt: PREFIX wd: SELECT ?item ?itemLabel WHERE {{ VALUES ?item {{ {} }} SERVICE wikibase:label {{ bd:serviceParam wikibase:language \"en\". }} }}", entity_ids.join(" ") ); let url = format!("https://query.wikidata.org/sparql?query={}", query); match Request::get(&url).send().await { Ok(response) => { if let Ok(data) = response.json::().await { let results = data["results"]["bindings"].as_array().unwrap_or(&vec![]).to_vec(); let mut labels = Vec::new(); for result in results { let item = result["item"]["value"].as_str().unwrap_or(""); let label = result["itemLabel"]["value"].as_str().unwrap_or(""); labels.push((item.to_string(), label.to_string())); } Ok(labels) } else { Err("Failed to parse response".to_string()) } } Err(err) => Err(format!("Failed to fetch entity labels: {:?}", err)), } } // Function to fetch properties for a specific Wikidata entity async fn fetch_entity_properties(entity_id: String) -> Result, String> { let query = format!( "PREFIX wdt: PREFIX wd: SELECT ?property ?propertyLabel ?value WHERE {{ VALUES ?item {{ wd:{} }} ?item ?property ?value. SERVICE wikibase:label {{ bd:serviceParam wikibase:language \"en\". }} }}", entity_id ); let url = format!("https://query.wikidata.org/sparql?query={}", query); match Request::get(&url).send().await { Ok(response) => { if let Ok(data) = response.json::().await { let results = data["results"]["bindings"].as_array().unwrap_or(&vec![]).to_vec(); let mut properties = Vec::new(); for result in results { let property = result["property"]["value"].as_str().unwrap_or(""); let value = result["value"]["value"].as_str().unwrap_or(""); properties.push((property.to_string(), value.to_string())); } Ok(properties) } else { Err("Failed to parse response".to_string()) } } Err(err) => Err(format!("Failed to fetch entity properties: {:?}", err)), } } // Function to fetch labels for properties async fn fetch_property_labels(property_ids: Vec) -> HashMap { let mut property_labels = HashMap::new(); // Construct the API URL to fetch labels for multiple properties let url = format!( "https://www.wikidata.org/w/api.php?action=wbgetentities&ids={}&props=labels&format=json&languages=en&origin=*", property_ids.join("|") ); match gloo_net::http::Request::get(&url).send().await { Ok(response) => { if let Ok(data) = response.json::().await { if let Some(entities) = data["entities"].as_object() { for (property_id, entity) in entities { if let Some(label) = entity["labels"]["en"]["value"].as_str() { property_labels.insert(property_id.clone(), label.to_string()); } } } } } Err(err) => log!("Error fetching property labels: {:?}", err), } property_labels } // Function to add a new custom property let add_property = move |property: String| { 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, selected_properties).await; }); } }); // Fetch the property label let property_id = property.clone(); spawn_local(async move { let labels = fetch_entity_labels(vec![property_id.clone()]).await; if let Ok(labels) = labels { if let Some((_, label)) = labels.first() { set_property_labels.update(|labels_map| { labels_map.insert(property_id, label.clone()); }); } } }); } }); // Fetch the relevant value for each item and populate the corresponding cells set_items.update(|items| { for item in items { if let Some(wikidata_id) = &item.wikidata_id { let wikidata_id = wikidata_id.clone(); let set_fetched_properties = set_fetched_properties.clone(); let set_property_labels = set_property_labels.clone(); let property_clone = property.clone(); spawn_local(async move { let properties = fetch_entity_properties(wikidata_id).await; if let Ok(properties) = properties { for (property, value) in properties { if property == &property_clone { set_items.update(|items| { if let Some(item) = items.iter_mut().find(|item| item.wikidata_id.as_ref().unwrap() == &wikidata_id) { item.custom_properties.insert(property_clone.clone(), value.clone()); } }); } } } }); } } }); }; // Function to update item fields let update_item = move |index: usize, field: &str, value: String| { set_items.update(|items| { if let Some(item) = items.get_mut(index) { match field { "name" => { item.name = value.clone(); fetch_wikidata_suggestions(format!("name-{}", index), value.clone()); // Fetch Wikidata properties if the field is "name" and the item has a valid Wikidata ID if !value.is_empty() { if let Some(wikidata_id) = &item.wikidata_id { let wikidata_id = wikidata_id.clone(); let set_fetched_properties = set_fetched_properties.clone(); let set_property_labels = set_property_labels.clone(); spawn_local(async move { let properties = fetch_entity_properties(wikidata_id).await; if let Ok(properties) = properties { log!("Fetched properties for index {}: {:?}", index, properties); } }); } } } "description" => { item.description = value.clone(); } _ => { // Update custom property item.custom_properties.insert(field.to_string(), value.clone()); } } // Save the updated item to the database let item_clone = item.clone(); spawn_local(async move { save_item_to_db(item_clone, selected_properties).await; }); } // Automatically add a new row when editing the last row if index == items.len() - 1 && !value.is_empty() { let new_item = Item { id: Uuid::new_v4().to_string(), name: String::new(), description: String::new(), 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, selected_properties).await; }); } log!("Items updated: {:?}", items); }); }; // List of properties to display as rows let properties = vec!["Name", "Description"]; view! {

{ "Items List" }

{move || items.get().iter().enumerate().map(|(index, item)| { view! { } }).collect::>()} {properties.into_iter().map(|property| { log!("Rendering property: {}", property); view! { {move || items.get().iter().enumerate().map(|(index, item)| { view! { } }).collect::>()} } }).collect::>()} // 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()); let property_clone_for_button = property_clone.clone(); view! { {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! { } }).collect::>()} } } }).collect::>() }}
{ "Property" } { item.name.clone() }
{ property } {match property { "Name" => view! {
{move || { if *show_suggestions.get().get(&format!("name-{}", index)).unwrap_or(&false) { log!("Rendering suggestions list"); view! {
    {move || { let suggestions = wikidata_suggestions.get() .get(&format!("name-{}", index)) .cloned() .unwrap_or_default(); log!("Suggestions for cell {}: {:?}", index, suggestions); suggestions.into_iter().map(|suggestion| { let label_for_click = suggestion.label.clone(); let label_for_display = suggestion.label.clone(); let description_for_click = suggestion.description.clone().unwrap_or_default(); let description_for_display = suggestion.description.clone().unwrap_or_default(); let id = suggestion.id.clone(); view! {
  • { format!("{} - {}", label_for_display, description_for_display) }
  • } }).collect::>() }}
} } else { log!("Suggestions list hidden"); view! {
    } } }}
    }.into_view(), "Description" => view! { }.into_view(), _ => view! { { "" } }.into_view(), }}
    { property_label }
    ().unwrap(); let property = input_element.value(); if !property.is_empty() { // Extract the coded name from the selected value let coded_name = property.split(" - ").next().unwrap_or(&property).to_string(); // Add the property using the coded name add_property(coded_name); // Clear the input field input_element.set_value(""); } } } /> {move || { let properties = fetched_properties.get().clone(); let property_labels = property_labels.get().clone(); properties.into_iter().map(|(key, _)| { let key_clone = key.clone(); let label = property_labels.get(&key_clone).cloned().unwrap_or_else(|| key_clone.clone()); view! { } }).collect::>() }}
    } } #[derive(Deserialize, Clone, Debug)] struct WikidataResponse { search: Vec, }