pinakes-ui: fix reactive dependencies in backlinks panel; improve wikilink click handling

Signed-off-by: NotAShelf <raf@notashelf.dev>
Change-Id: Ib9a36bbaa16a7aa46b624027c1eb00fe6a6a6964
This commit is contained in:
raf 2026-02-09 14:26:15 +03:00
commit bf76820ddd
Signed by: NotAShelf
GPG key ID: 29D95B64378DB4BF
2 changed files with 113 additions and 37 deletions

View file

@ -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::<u64>::None);
// Fetch outgoing links on mount
// 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();
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);
// Also fetch global unresolved count
if let Ok(count) = client.get_unresolved_links_count().await {
global_unresolved.set(Some(count));
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)
}
});
// 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();

View file

@ -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: &regex::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!(
"<a href=\"#wikilink\" class=\"wikilink\" data-wikilink-target=\"{}\">{}</a>",
escape_html_attr(target),
escape_html(display)
"<a href=\"javascript:void(0)\" class=\"wikilink\" data-wikilink-target=\"{target}\" onclick=\"if(window.__dioxus_wikilink_click){{window.__dioxus_wikilink_click('{target_escaped}')}}\">{display}</a>",
target = escape_html_attr(target),
target_escaped = escape_html_attr(&target.replace('\\', "\\\\").replace('\'', "\\'")),
display = escape_html(display)
)
});