diff --git a/src/client/host_detail.rs b/src/client/host_detail.rs index 37590ce..8587f59 100644 --- a/src/client/host_detail.rs +++ b/src/client/host_detail.rs @@ -20,9 +20,15 @@ use crate::models::Application; // ─── Add applications modal ─────────────────────────────────────────────────── -// Multi-select modal: shows every application not yet linked to the host. -// The auto-close Effect lives in the PARENT so that reopening the modal -// after a previous success does not immediately dismiss it again. +// Scrollable pick list + selected tags: +// - Top: scrollable list of available apps; clicking one moves it to the +// selected section and removes it from the list. +// - Bottom: selected apps shown as removable tags; clicking × puts the app +// back in the list. +// +// The auto-close Effect lives in the PARENT to avoid the re-trigger bug +// (an Effect inside a conditionally-rendered component fires on mount and +// would immediately close the modal if the action already held a past Ok value). #[component] fn AddAppModal( host_id: i64, @@ -30,8 +36,8 @@ fn AddAppModal( add_action: ServerAction, show_modal: RwSignal, ) -> impl IntoView { - // Tracks which application IDs the user has checked. - let selected: RwSignal> = RwSignal::new(vec![]); + // Full Application structs so names are available in the selected tag list. + let selected: RwSignal> = RwSignal::new(vec![]); view! { + }).collect_view()} + + }.into_any() + } + } + } + }} - {move || add_action.value().get() - .and_then(|r| r.err()) - .map(|e| view! {

{e.to_string()}

}) - } + // ── Selected tags (shown once at least one app is chosen) ─ + {move || (!selected.get().is_empty()).then(|| { + let sel = selected.get(); + view! { +
+ "Selected:" +
+ {sel.into_iter().map(|app| { + let app_id = app.id; + view! { + + {app.name} + + + } + }).collect_view()} +
+
+ } + })} + + {move || add_action.value().get() + .and_then(|r| r.err()) + .map(|e| view! {

{e.to_string()}

}) + } +