diff --git a/crates/pinakes-ui/src/components/backlinks_panel.rs b/crates/pinakes-ui/src/components/backlinks_panel.rs index 9f4df1f..7f8f9e0 100644 --- a/crates/pinakes-ui/src/components/backlinks_panel.rs +++ b/crates/pinakes-ui/src/components/backlinks_panel.rs @@ -18,10 +18,42 @@ pub fn BacklinksPanel( let mut reindexing = use_signal(|| false); let mut reindex_message = use_signal(|| Option::<(String, bool)>::None); // (message, is_error) - // Fetch backlinks function - let fetch_backlinks = { + // Clone values for manual fetch function (used after reindex) + let fetch_client = client.clone(); + let fetch_media_id = media_id.clone(); + + // Clone for reindex handler + let reindex_client = client.clone(); + let reindex_media_id = media_id.clone(); + + // Fetch backlinks using use_resource to automatically track media_id changes + // This ensures the backlinks are reloaded whenever we navigate to a different note + let backlinks_resource = use_resource(move || { let client = client.clone(); let id = media_id.clone(); + async move { client.get_backlinks(&id).await } + }); + + // Update local state based on resource state + use_effect(move || match &*backlinks_resource.read_unchecked() { + Some(Ok(resp)) => { + backlinks.set(Some(resp.clone())); + loading.set(false); + error.set(None); + } + Some(Err(e)) => { + error.set(Some(format!("Failed to load backlinks: {e}"))); + loading.set(false); + } + None => { + loading.set(true); + } + }); + + // Fetch backlinks function for manual refresh (like after reindex) + let fetch_backlinks = { + let client = fetch_client; + let id = fetch_media_id; move || { let client = client.clone(); let id = id.clone(); @@ -41,16 +73,10 @@ pub fn BacklinksPanel( } }; - // Fetch backlinks on mount - let fetch_on_mount = fetch_backlinks.clone(); - use_effect(move || { - fetch_on_mount(); - }); - // Reindex links handler let on_reindex = { - let client = client.clone(); - let id = media_id.clone(); + let client = reindex_client; + let id = reindex_media_id; let fetch_backlinks = fetch_backlinks.clone(); move |evt: MouseEvent| { evt.stop_propagation(); // Don't toggle collapse @@ -203,30 +229,35 @@ pub fn OutgoingLinksPanel( let mut collapsed = use_signal(|| true); // Collapsed by default let mut global_unresolved = use_signal(|| Option::::None); - // Fetch outgoing links on mount - let id = media_id.clone(); - let client_clone = client.clone(); - use_effect(move || { - let id = id.clone(); - let client = client_clone.clone(); - spawn(async move { - loading.set(true); - error.set(None); - match client.get_outgoing_links(&id).await { - Ok(resp) => { - links.set(Some(resp)); - } - Err(e) => { - error.set(Some(format!("Failed to load links: {e}"))); - } - } - loading.set(false); + // Fetch outgoing links using use_resource to automatically track media_id changes + // This ensures the links are reloaded whenever we navigate to a different note + let links_resource = use_resource(move || { + let client = client.clone(); + let id = media_id.clone(); + async move { + let links_result = client.get_outgoing_links(&id).await; + let unresolved_count = client.get_unresolved_links_count().await.ok(); + (links_result, unresolved_count) + } + }); - // Also fetch global unresolved count - if let Ok(count) = client.get_unresolved_links_count().await { - global_unresolved.set(Some(count)); + // Update local state based on resource state + use_effect(move || match &*links_resource.read_unchecked() { + Some((Ok(resp), unresolved_count)) => { + links.set(Some(resp.clone())); + loading.set(false); + error.set(None); + if let Some(count) = unresolved_count { + global_unresolved.set(Some(*count)); } - }); + } + Some((Err(e), _)) => { + error.set(Some(format!("Failed to load links: {e}"))); + loading.set(false); + } + None => { + loading.set(true); + } }); let is_loading = *loading.read(); diff --git a/crates/pinakes-ui/src/components/markdown_viewer.rs b/crates/pinakes-ui/src/components/markdown_viewer.rs index 1a31980..bcd04d7 100644 --- a/crates/pinakes-ui/src/components/markdown_viewer.rs +++ b/crates/pinakes-ui/src/components/markdown_viewer.rs @@ -1,3 +1,4 @@ +use dioxus::document::eval; use dioxus::prelude::*; /// Event handler for wikilink clicks. Called with the target note name. @@ -43,6 +44,49 @@ pub fn MarkdownViewer( }); }); + // Set up global wikilink click handler that the inline onclick attributes will call + // This bridges JavaScript → Rust communication + use_effect(move || { + if let Some(handler) = on_wikilink_click { + spawn(async move { + // Set up a global function that wikilink onclick handlers can call + // The function stores the clicked target in localStorage + let setup_js = r#" + window.__dioxus_wikilink_click = function(target) { + console.log('Wikilink clicked:', target); + localStorage.setItem('__wikilink_clicked', target); + }; + "#; + + let _ = eval(setup_js).await; + + // Poll localStorage to detect wikilink clicks + loop { + tokio::time::sleep(tokio::time::Duration::from_millis(100)).await; + + let check_js = r#" + (function() { + const target = localStorage.getItem('__wikilink_clicked'); + if (target) { + localStorage.removeItem('__wikilink_clicked'); + return target; + } + return ''; + })(); + "#; + + if let Ok(result) = eval(check_js).await { + if let Some(target) = result.as_str() { + if !target.is_empty() { + handler.call(target.to_string()); + } + } + } + } + }); + } + }); + let is_loading = *loading.read(); rsx! { @@ -159,7 +203,7 @@ fn render_markdown(text: &str) -> String { } /// Convert wikilinks [[target]] and [[target|display]] to styled HTML links. -/// Uses data attributes only - no inline JavaScript for security. +/// Uses a special URL scheme that can be intercepted by click handlers. fn convert_wikilinks(text: &str) -> String { use regex::Regex; @@ -181,12 +225,13 @@ fn convert_wikilinks(text: &str) -> String { let text = wikilink_re.replace_all(&text, |caps: ®ex::Captures| { let target = caps.get(1).unwrap().as_str().trim(); let display = caps.get(2).map(|m| m.as_str().trim()).unwrap_or(target); - // Create a styled link with data attributes only - no inline JavaScript. - // Event handling is done via event delegation in the frontend. + // Create a styled link that uses a special pseudo-protocol scheme + // This makes it easier to intercept clicks via JavaScript format!( - "{}", - escape_html_attr(target), - escape_html(display) + "{display}", + target = escape_html_attr(target), + target_escaped = escape_html_attr(&target.replace('\\', "\\\\").replace('\'', "\\'")), + display = escape_html(display) ) });