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 reindexing = use_signal(|| false);
|
||||||
let mut reindex_message = use_signal(|| Option::<(String, bool)>::None); // (message, is_error)
|
let mut reindex_message = use_signal(|| Option::<(String, bool)>::None); // (message, is_error)
|
||||||
|
|
||||||
// Fetch backlinks function
|
// Clone values for manual fetch function (used after reindex)
|
||||||
let fetch_backlinks = {
|
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 client = client.clone();
|
||||||
let id = media_id.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 || {
|
move || {
|
||||||
let client = client.clone();
|
let client = client.clone();
|
||||||
let id = id.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
|
// Reindex links handler
|
||||||
let on_reindex = {
|
let on_reindex = {
|
||||||
let client = client.clone();
|
let client = reindex_client;
|
||||||
let id = media_id.clone();
|
let id = reindex_media_id;
|
||||||
let fetch_backlinks = fetch_backlinks.clone();
|
let fetch_backlinks = fetch_backlinks.clone();
|
||||||
move |evt: MouseEvent| {
|
move |evt: MouseEvent| {
|
||||||
evt.stop_propagation(); // Don't toggle collapse
|
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 collapsed = use_signal(|| true); // Collapsed by default
|
||||||
let mut global_unresolved = use_signal(|| Option::<u64>::None);
|
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
|
||||||
let id = media_id.clone();
|
// This ensures the links are reloaded whenever we navigate to a different note
|
||||||
let client_clone = client.clone();
|
let links_resource = use_resource(move || {
|
||||||
use_effect(move || {
|
let client = client.clone();
|
||||||
let id = id.clone();
|
let id = media_id.clone();
|
||||||
let client = client_clone.clone();
|
async move {
|
||||||
spawn(async move {
|
let links_result = client.get_outgoing_links(&id).await;
|
||||||
loading.set(true);
|
let unresolved_count = client.get_unresolved_links_count().await.ok();
|
||||||
error.set(None);
|
(links_result, unresolved_count)
|
||||||
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
|
// Update local state based on resource state
|
||||||
if let Ok(count) = client.get_unresolved_links_count().await {
|
use_effect(move || match &*links_resource.read_unchecked() {
|
||||||
global_unresolved.set(Some(count));
|
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();
|
let is_loading = *loading.read();
|
||||||
|
|
|
||||||
|
|
@ -1,3 +1,4 @@
|
||||||
|
use dioxus::document::eval;
|
||||||
use dioxus::prelude::*;
|
use dioxus::prelude::*;
|
||||||
|
|
||||||
/// Event handler for wikilink clicks. Called with the target note name.
|
/// 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();
|
let is_loading = *loading.read();
|
||||||
|
|
||||||
rsx! {
|
rsx! {
|
||||||
|
|
@ -159,7 +203,7 @@ fn render_markdown(text: &str) -> String {
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Convert wikilinks [[target]] and [[target|display]] to styled HTML links.
|
/// 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 {
|
fn convert_wikilinks(text: &str) -> String {
|
||||||
use regex::Regex;
|
use regex::Regex;
|
||||||
|
|
||||||
|
|
@ -181,12 +225,13 @@ fn convert_wikilinks(text: &str) -> String {
|
||||||
let text = wikilink_re.replace_all(&text, |caps: ®ex::Captures| {
|
let text = wikilink_re.replace_all(&text, |caps: ®ex::Captures| {
|
||||||
let target = caps.get(1).unwrap().as_str().trim();
|
let target = caps.get(1).unwrap().as_str().trim();
|
||||||
let display = caps.get(2).map(|m| m.as_str().trim()).unwrap_or(target);
|
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.
|
// Create a styled link that uses a special pseudo-protocol scheme
|
||||||
// Event handling is done via event delegation in the frontend.
|
// This makes it easier to intercept clicks via JavaScript
|
||||||
format!(
|
format!(
|
||||||
"<a href=\"#wikilink\" class=\"wikilink\" data-wikilink-target=\"{}\">{}</a>",
|
"<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>",
|
||||||
escape_html_attr(target),
|
target = escape_html_attr(target),
|
||||||
escape_html(display)
|
target_escaped = escape_html_attr(&target.replace('\\', "\\\\").replace('\'', "\\'")),
|
||||||
|
display = escape_html(display)
|
||||||
)
|
)
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue