fix(hosts,applications): fix modal re-open bug and autofocus first field

Move the add-modal auto-close Effect from each modal component to its
parent page component. This prevents the stale-value re-trigger bug
where the Effect would immediately close the modal on second open
because action.value() still held the previous Ok result.

Also add autofocus on the first input field of each add modal using
NodeRef<Input> so the user can start typing immediately on open.

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-05-16 21:33:33 +02:00
parent 60e02ca453
commit 5228a76468
2 changed files with 38 additions and 6 deletions

View File

@@ -8,6 +8,7 @@
use leptos::prelude::*; use leptos::prelude::*;
use leptos::form::ActionForm; use leptos::form::ActionForm;
use leptos::html::Input;
use crate::api::applications::{ use crate::api::applications::{
ApplicationWithCounts, CreateApplication, DeleteApplication, ApplicationWithCounts, CreateApplication, DeleteApplication,
@@ -21,9 +22,12 @@ fn AddApplicationModal(
create_action: ServerAction<CreateApplication>, create_action: ServerAction<CreateApplication>,
show_modal: RwSignal<bool>, show_modal: RwSignal<bool>,
) -> impl IntoView { ) -> impl IntoView {
let name_ref = NodeRef::<Input>::new();
// Focus the name field as soon as the modal is mounted.
Effect::new(move |_| { Effect::new(move |_| {
if let Some(Ok(_)) = create_action.value().get() { if let Some(el) = name_ref.get() {
show_modal.set(false); let _ = el.focus();
} }
}); });
@@ -43,6 +47,7 @@ fn AddApplicationModal(
<label> <label>
"Name" "Name"
<input <input
node_ref=name_ref
type="text" type="text"
name="name" name="name"
placeholder="e.g. Nginx, PostgreSQL, Prometheus" placeholder="e.g. Nginx, PostgreSQL, Prometheus"
@@ -132,6 +137,19 @@ pub fn ApplicationsPage() -> impl IntoView {
// Name filter (client-side — list is typically small) // Name filter (client-side — list is typically small)
let name_filter = RwSignal::new(String::new()); let name_filter = RwSignal::new(String::new());
// Close the add modal when the action transitions pending→done with Ok.
// Lives in the parent so it is never recreated across modal open/close cycles,
// which avoids the stale-value re-trigger bug.
Effect::new(move |was_pending: Option<bool>| {
let is_pending = create_action.pending().get();
if was_pending == Some(true) && !is_pending {
if let Some(Ok(_)) = create_action.value().get() {
show_modal.set(false);
}
}
is_pending
});
// Close the delete modal automatically after a successful deletion. // Close the delete modal automatically after a successful deletion.
Effect::new(move |_| { Effect::new(move |_| {
if let Some(Ok(_)) = delete_action.value().get() { if let Some(Ok(_)) = delete_action.value().get() {

View File

@@ -12,6 +12,7 @@
use leptos::prelude::*; use leptos::prelude::*;
use leptos::form::ActionForm; use leptos::form::ActionForm;
use leptos::html::Input;
use crate::api::{ use crate::api::{
applications::get_applications, applications::get_applications,
@@ -76,10 +77,12 @@ fn AddHostModal(
networks_res: LocalResource<Result<Vec<crate::models::Network>, ServerFnError>>, networks_res: LocalResource<Result<Vec<crate::models::Network>, ServerFnError>>,
show_modal: RwSignal<bool>, show_modal: RwSignal<bool>,
) -> impl IntoView { ) -> impl IntoView {
// Close the modal automatically after a successful creation. let name_ref = NodeRef::<Input>::new();
// Focus the name field as soon as the modal is mounted.
Effect::new(move |_| { Effect::new(move |_| {
if let Some(Ok(_)) = create_action.value().get() { if let Some(el) = name_ref.get() {
show_modal.set(false); let _ = el.focus();
} }
}); });
@@ -100,7 +103,7 @@ fn AddHostModal(
<div class="add-form__fields"> <div class="add-form__fields">
<label> <label>
"Name" "Name"
<input type="text" name="name" placeholder="e.g. web-server-01" required/> <input node_ref=name_ref type="text" name="name" placeholder="e.g. web-server-01" required/>
</label> </label>
<label> <label>
"IP address" "IP address"
@@ -364,6 +367,17 @@ pub fn HostsPage() -> impl IntoView {
// None = no modal, Some((id, name)) = delete confirmation open. // None = no modal, Some((id, name)) = delete confirmation open.
let pending_delete: RwSignal<Option<(i64, String)>> = RwSignal::new(None); let pending_delete: RwSignal<Option<(i64, String)>> = RwSignal::new(None);
// Close the add modal on pending→done with Ok (lives in parent to avoid stale-value re-trigger).
Effect::new(move |was_pending: Option<bool>| {
let is_pending = create_action.pending().get();
if was_pending == Some(true) && !is_pending {
if let Some(Ok(_)) = create_action.value().get() {
show_modal.set(false);
}
}
is_pending
});
// Close the delete modal automatically after a successful deletion. // Close the delete modal automatically after a successful deletion.
Effect::new(move |_| { Effect::new(move |_| {
if let Some(Ok(_)) = delete_action.value().get() { if let Some(Ok(_)) = delete_action.value().get() {