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:
parent
4ed61bc62e
commit
bf76820ddd
2 changed files with 113 additions and 37 deletions
|
|
@ -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
|
||||
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();
|
||||
|
|
|
|||
|
|
@ -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!(
|
||||
"<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)
|
||||
)
|
||||
});
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue