Compare commits
6 commits
main
...
2412_edita
Author | SHA1 | Date | |
---|---|---|---|
a02fcabe19 | |||
b84cd37c44 | |||
b616fbb438 | |||
40140b40c4 | |||
c4a45d9185 | |||
8cd277d66a |
5 changed files with 193 additions and 58 deletions
8
Cargo.lock
generated
8
Cargo.lock
generated
|
@ -722,8 +722,10 @@ dependencies = [
|
||||||
"actix-web",
|
"actix-web",
|
||||||
"console_error_panic_hook",
|
"console_error_panic_hook",
|
||||||
"futures",
|
"futures",
|
||||||
|
"futures-timer",
|
||||||
"gloo-net 0.5.0",
|
"gloo-net 0.5.0",
|
||||||
"http 1.2.0",
|
"http 1.2.0",
|
||||||
|
"js-sys",
|
||||||
"leptos",
|
"leptos",
|
||||||
"leptos_actix",
|
"leptos_actix",
|
||||||
"leptos_meta",
|
"leptos_meta",
|
||||||
|
@ -1082,6 +1084,12 @@ version = "0.3.31"
|
||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
checksum = "f90f7dce0722e95104fcb095585910c0977252f286e354b5e3bd38902cd99988"
|
||||||
|
|
||||||
|
[[package]]
|
||||||
|
name = "futures-timer"
|
||||||
|
version = "3.0.3"
|
||||||
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
|
checksum = "f288b0a4f20f9a56b5d1da57e2227c661b7b16168e2f72365f57b63326e29b24"
|
||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "futures-util"
|
name = "futures-util"
|
||||||
version = "0.3.31"
|
version = "0.3.31"
|
||||||
|
|
|
@ -18,7 +18,8 @@ leptos_router = { version = "0.6" }
|
||||||
wasm-bindgen = "=0.2.99"
|
wasm-bindgen = "=0.2.99"
|
||||||
serde = { version = "1.0", features = ["derive"] }
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
uuid = { version = "1.0", features = ["v4"] }
|
uuid = { version = "1.0", features = ["v4"] }
|
||||||
web-sys = { version = "0.3", features = ["Event"] }
|
web-sys = { version = "0.3", features = ["Event", "HtmlElement", "Window", "EventTarget", "Element", "DomRect"] }
|
||||||
|
js-sys = "0.3"
|
||||||
nostr-sdk = "0.37"
|
nostr-sdk = "0.37"
|
||||||
tokio = "1"
|
tokio = "1"
|
||||||
gloo-net = "0.5"
|
gloo-net = "0.5"
|
||||||
|
@ -27,6 +28,7 @@ wasm-bindgen-futures = "0.4"
|
||||||
serde_json="1.0.133"
|
serde_json="1.0.133"
|
||||||
thiserror = "2.0.9"
|
thiserror = "2.0.9"
|
||||||
zerofrom = "0.1"
|
zerofrom = "0.1"
|
||||||
|
futures-timer = "3.0.0"
|
||||||
|
|
||||||
[features]
|
[features]
|
||||||
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
csr = ["leptos/csr", "leptos_meta/csr", "leptos_router/csr"]
|
||||||
|
|
|
@ -68,4 +68,28 @@ th, td {
|
||||||
|
|
||||||
th {
|
th {
|
||||||
background-color: #f2f2f2;
|
background-color: #f2f2f2;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions-popup {
|
||||||
|
background-color: white;
|
||||||
|
border: 1px solid #ccc;
|
||||||
|
box-shadow: 0 2px 10px rgba(0,0,0,0.1);
|
||||||
|
max-height: 200px;
|
||||||
|
overflow-y: auto;
|
||||||
|
z-index: 1000;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions-popup ul {
|
||||||
|
list-style-type: none;
|
||||||
|
padding: 0;
|
||||||
|
margin: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions-popup li {
|
||||||
|
padding: 8px 12px;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.suggestions-popup li:hover {
|
||||||
|
background-color: #f0f0f0;
|
||||||
}
|
}
|
|
@ -1,39 +1,90 @@
|
||||||
use leptos::*;
|
use leptos::*;
|
||||||
|
use leptos::logging::log;
|
||||||
|
use std::rc::Rc;
|
||||||
|
use web_sys::FocusEvent;
|
||||||
#[component]
|
#[component]
|
||||||
pub fn EditableCell(
|
pub fn EditableCell(
|
||||||
value: String,
|
value: String,
|
||||||
on_input: impl Fn(String) + 'static,
|
on_input: Rc<dyn Fn(String)>, // Use `Rc` to allow cloning
|
||||||
|
#[prop(into)] on_focus: Callback<FocusEvent>,
|
||||||
#[prop(optional)] key: Option<String>, // Optional `key` prop
|
#[prop(optional)] key: Option<String>, // Optional `key` prop
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let (input_value, set_input_value) = create_signal(value.clone());
|
let (input_value, set_input_value) = create_signal(value.clone());
|
||||||
let (has_focus, set_has_focus) = create_signal(false); // Track focus state locally
|
let (has_focus, set_has_focus) = create_signal(false); // Track focus state locally
|
||||||
|
let (is_disposed, set_disposed) = create_signal(false); // Track disposal state
|
||||||
|
let (is_editing, set_is_editing) = create_signal(false);
|
||||||
|
|
||||||
|
// persistent default key value
|
||||||
|
let default_key = String::new();
|
||||||
|
let key_ref = key.as_ref().unwrap_or(&default_key);
|
||||||
|
|
||||||
|
// Ensure signals aren't updated after disposal
|
||||||
|
on_cleanup(move || {
|
||||||
|
log!("Component disposed");
|
||||||
|
set_disposed.set(true);
|
||||||
|
});
|
||||||
|
|
||||||
|
let log_signal_get = move |signal_name: &str| {
|
||||||
|
if is_disposed.get() {
|
||||||
|
panic!("Attempted to get disposed signal: {}", signal_name);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
let handle_input = move |e: web_sys::Event| {
|
let handle_input = move |e: web_sys::Event| {
|
||||||
|
let on_input = Rc::clone(&on_input); // Clone `on_input` to use inside the closure
|
||||||
|
log_signal_get("input_value");
|
||||||
|
if is_disposed.get_untracked() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
let new_value = event_target_value(&e);
|
let new_value = event_target_value(&e);
|
||||||
set_input_value.set(new_value.clone());
|
set_input_value.set(new_value.clone());
|
||||||
on_input(new_value);
|
on_input(new_value);
|
||||||
};
|
};
|
||||||
|
|
||||||
let handle_focus = move |_: web_sys::FocusEvent| {
|
let handle_focus = move |ev:FocusEvent| {
|
||||||
|
if is_disposed.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set_is_editing.set(true);
|
||||||
set_has_focus.set(true);
|
set_has_focus.set(true);
|
||||||
|
on_focus.call(ev);
|
||||||
};
|
};
|
||||||
|
|
||||||
let handle_blur = move |_: web_sys::FocusEvent| {
|
let handle_blur = move |_:FocusEvent| {
|
||||||
|
if is_disposed.get() {
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
set_is_editing.set(false);
|
||||||
set_has_focus.set(false);
|
set_has_focus.set(false);
|
||||||
};
|
};
|
||||||
|
|
||||||
// Use key to force updates only when necessary
|
let cell_view = move || {
|
||||||
let _key = key.unwrap_or_default();
|
if is_editing.get() {
|
||||||
|
view! {
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={input_value.get()}
|
||||||
|
on:input=handle_input.clone()
|
||||||
|
on:focus=handle_focus.clone()
|
||||||
|
on:blur=handle_blur.clone()
|
||||||
|
class={if has_focus.get() { "focused" } else { "not-focused" }}
|
||||||
|
/>
|
||||||
|
}.into_view()
|
||||||
|
} else {
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
tabindex="0"
|
||||||
|
on:focus=handle_focus.clone()
|
||||||
|
>
|
||||||
|
{input_value.get()}
|
||||||
|
</div>
|
||||||
|
}.into_view()
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<input
|
<div key={key_ref.clone()}>
|
||||||
type="text"
|
{cell_view}
|
||||||
value={input_value.get()}
|
</div>
|
||||||
on:input=handle_input
|
|
||||||
on:focus=handle_focus
|
|
||||||
on:blur=handle_blur
|
|
||||||
class={if has_focus.get() { "focused" } else { "not-focused" }}
|
|
||||||
/>
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -6,6 +6,11 @@ use uuid::Uuid;
|
||||||
use leptos::logging::log;
|
use leptos::logging::log;
|
||||||
use crate::models::item::Item;
|
use crate::models::item::Item;
|
||||||
use std::sync::{Arc, Mutex};
|
use std::sync::{Arc, Mutex};
|
||||||
|
use wasm_bindgen::JsCast;
|
||||||
|
use web_sys::{FocusEvent, HtmlElement};
|
||||||
|
use futures_timer::Delay;
|
||||||
|
use std::time::Duration;
|
||||||
|
use std::rc::Rc;
|
||||||
|
|
||||||
#[derive(Deserialize, Clone, Debug)]
|
#[derive(Deserialize, Clone, Debug)]
|
||||||
struct WikidataSuggestion {
|
struct WikidataSuggestion {
|
||||||
|
@ -30,12 +35,15 @@ pub fn ItemsList(
|
||||||
wikidata_id: None,
|
wikidata_id: None,
|
||||||
}]);
|
}]);
|
||||||
|
|
||||||
let (wikidata_suggestions, set_wikidata_suggestions) =
|
let (active_cell_position, set_active_cell_position) = create_signal(None::<(f64, f64)>);
|
||||||
create_signal(Vec::<WikidataSuggestion>::new());
|
let (active_row_index, set_active_row_index) = create_signal(None::<usize>);
|
||||||
|
let (wikidata_suggestions, set_wikidata_suggestions) = create_signal(Vec::<WikidataSuggestion>::new());
|
||||||
|
let debounce_duration = Duration::from_millis(300);
|
||||||
|
|
||||||
// Fetch Wikidata suggestions
|
// Fetch Wikidata suggestions
|
||||||
let fetch_wikidata_suggestions = move |query: String| {
|
let fetch_wikidata_suggestions = move |query: String| {
|
||||||
spawn_local(async move {
|
spawn_local(async move {
|
||||||
|
Delay::new(debounce_duration).await;
|
||||||
if query.is_empty() {
|
if query.is_empty() {
|
||||||
set_wikidata_suggestions.set(Vec::new());
|
set_wikidata_suggestions.set(Vec::new());
|
||||||
return;
|
return;
|
||||||
|
@ -52,7 +60,10 @@ pub fn ItemsList(
|
||||||
set_wikidata_suggestions.set(data.search);
|
set_wikidata_suggestions.set(data.search);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Err(_) => log!("Failed to fetch Wikidata suggestions"),
|
Err(err) => {
|
||||||
|
log!("Failed to fetch Wikidata suggestions: {:?}", err);
|
||||||
|
set_wikidata_suggestions.set(Vec::new());
|
||||||
|
}
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
@ -65,13 +76,14 @@ pub fn ItemsList(
|
||||||
"name" => {
|
"name" => {
|
||||||
item.name = value.clone();
|
item.name = value.clone();
|
||||||
fetch_wikidata_suggestions(value.clone());
|
fetch_wikidata_suggestions(value.clone());
|
||||||
|
set_active_row_index.set(Some(index));
|
||||||
}
|
}
|
||||||
"description" => {
|
"description" => {
|
||||||
item.description = value.clone();
|
item.description = value.clone();
|
||||||
}
|
}
|
||||||
_ => (),
|
_ => (),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Automatically add a new row when editing the last row
|
// Automatically add a new row when editing the last row
|
||||||
if index == items.len() - 1 && !value.is_empty() {
|
if index == items.len() - 1 && !value.is_empty() {
|
||||||
|
@ -87,6 +99,21 @@ pub fn ItemsList(
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle focus event for EditableCell
|
||||||
|
let handle_focus = move |index: usize, field: &str, event: FocusEvent| {
|
||||||
|
set_active_row_index.set(Some(index));
|
||||||
|
if field == "name" {
|
||||||
|
if let Some(target) = event.target() {
|
||||||
|
if let Some(element) = target.dyn_ref::<HtmlElement>() {
|
||||||
|
let rect = element.get_bounding_client_rect();
|
||||||
|
set_active_cell_position.set(Some((rect.left(), rect.bottom())));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
set_active_cell_position.set(None);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
// Add a new tag to an item
|
// Add a new tag to an item
|
||||||
let add_tag = move |index: usize, key: String, value: String| {
|
let add_tag = move |index: usize, key: String, value: String| {
|
||||||
set_items.update(|items| {
|
set_items.update(|items| {
|
||||||
|
@ -112,6 +139,59 @@ pub fn ItemsList(
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Position and render the popup
|
||||||
|
let render_popup = move || {
|
||||||
|
view! {
|
||||||
|
<div
|
||||||
|
class="suggestions-popup"
|
||||||
|
style=move || {
|
||||||
|
let suggestions = wikidata_suggestions.get();
|
||||||
|
if !suggestions.is_empty() {
|
||||||
|
if let Some((x, y)) = active_cell_position.get() {
|
||||||
|
format!(
|
||||||
|
"position: absolute; left: {}px; top: {}px; display: block; z-index: 1000;",
|
||||||
|
x, y
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
"display: none;".to_string()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
"display: none;".to_string()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<ul>
|
||||||
|
{move || wikidata_suggestions.get().iter().map(|suggestion| {
|
||||||
|
let label_for_click = suggestion.label.clone();
|
||||||
|
let description_for_click = suggestion.description.clone().unwrap_or_default();
|
||||||
|
let id = suggestion.id.clone();
|
||||||
|
let label_for_display = label_for_click.clone();
|
||||||
|
let description_for_display = description_for_click.clone();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<li on:click=move |_| {
|
||||||
|
if let Some(index) = active_row_index.get() {
|
||||||
|
set_items.update(|items| {
|
||||||
|
if let Some(item) = items.get_mut(index) {
|
||||||
|
item.name = label_for_click.clone();
|
||||||
|
item.description = description_for_click.clone();
|
||||||
|
item.wikidata_id = Some(id.clone());
|
||||||
|
item.tags.push(("wikidata_id".to_string(), id.clone()));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
set_wikidata_suggestions.set(Vec::new());
|
||||||
|
set_active_cell_position.set(None);
|
||||||
|
}>
|
||||||
|
{format!("{} - {}", label_for_display, description_for_display)}
|
||||||
|
</li>
|
||||||
|
}
|
||||||
|
}).collect::<Vec<_>>()}
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
view! {
|
view! {
|
||||||
<div>
|
<div>
|
||||||
<h1>{ "Items List" }</h1>
|
<h1>{ "Items List" }</h1>
|
||||||
|
@ -130,50 +210,19 @@ pub fn ItemsList(
|
||||||
<tr>
|
<tr>
|
||||||
// Editable Name Field with Wikidata Integration
|
// Editable Name Field with Wikidata Integration
|
||||||
<td>
|
<td>
|
||||||
<EditableCell
|
<EditableCell
|
||||||
value=item.name.clone()
|
value=item.name.clone()
|
||||||
on_input=move |value| update_item(index, "name", value)
|
on_input=Rc::new(move |value| update_item(index, "name", value))
|
||||||
key=format!("name-{}", index) // Unique key per cell
|
on_focus=move |event| handle_focus(index, "name", event)
|
||||||
/>
|
key=format!("name-{}", index)
|
||||||
<ul>
|
/>
|
||||||
{move || {
|
|
||||||
let suggestions = wikidata_suggestions.get().to_vec();
|
|
||||||
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();
|
|
||||||
|
|
||||||
// Tags for the item
|
|
||||||
let tags = vec![
|
|
||||||
("source".to_string(), "wikidata".to_string()),
|
|
||||||
("wikidata_id".to_string(), id.clone()),
|
|
||||||
];
|
|
||||||
|
|
||||||
view! {
|
|
||||||
<li on:click=move |_| {
|
|
||||||
set_items.update(|items| {
|
|
||||||
if let Some(item) = items.get_mut(index) {
|
|
||||||
item.description = description_for_click.clone();
|
|
||||||
item.tags.extend(tags.clone());
|
|
||||||
item.wikidata_id = Some(id.clone());
|
|
||||||
item.name = label_for_click.clone();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}>
|
|
||||||
{ format!("{} - {}", label_for_display, description_for_display) }
|
|
||||||
</li>
|
|
||||||
}
|
|
||||||
}).collect::<Vec<_>>()
|
|
||||||
}}
|
|
||||||
</ul>
|
|
||||||
</td>
|
</td>
|
||||||
// Editable Description Field
|
// Editable Description Field
|
||||||
<td>
|
<td>
|
||||||
<EditableCell
|
<EditableCell
|
||||||
value=item.description.clone()
|
value=item.description.clone()
|
||||||
on_input=move |value| update_item(index, "description", value)
|
on_input=Rc::new(move |value| update_item(index, "description", value))
|
||||||
|
on_focus=move |event| handle_focus(index, "description", event)
|
||||||
key=format!("description-{}", index)
|
key=format!("description-{}", index)
|
||||||
/>
|
/>
|
||||||
</td>
|
</td>
|
||||||
|
@ -194,6 +243,7 @@ pub fn ItemsList(
|
||||||
}).collect::<Vec<_>>()}
|
}).collect::<Vec<_>>()}
|
||||||
</tbody>
|
</tbody>
|
||||||
</table>
|
</table>
|
||||||
|
{render_popup()}
|
||||||
</div>
|
</div>
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
Loading…
Add table
Reference in a new issue