From 05d8e1527ef367f980795b130b3e2faa56b27ad7 Mon Sep 17 00:00:00 2001 From: Lucas Vas Date: Sun, 21 Dec 2025 13:27:07 -0500 Subject: [PATCH] feat: allow users to confirm deletion of scholarships --- src/common/expandable_info.rs | 2 +- src/pages/scholarship_info.rs | 112 +++++++++++++++++++++++----------- 2 files changed, 78 insertions(+), 36 deletions(-) diff --git a/src/common/expandable_info.rs b/src/common/expandable_info.rs index a209606..0ef8ff0 100644 --- a/src/common/expandable_info.rs +++ b/src/common/expandable_info.rs @@ -15,7 +15,7 @@ use std::collections::HashMap; /// now, the table will simply create a single massive `Map` field that contains all the stored /// information in the `HashMap`. We'd prefer to be able to get single values if possible, /// especially for interoperability with other applications, should they be created. -#[derive(Debug, Clone, Serialize, Deserialize, Reactive)] +#[derive(Debug, Clone, Serialize, Deserialize, Reactive, )] pub struct ExpandableInfo { /// The main key member. Required and used to get/put the information in a database. pub subject: String, diff --git a/src/pages/scholarship_info.rs b/src/pages/scholarship_info.rs index 94550af..07d9b92 100644 --- a/src/pages/scholarship_info.rs +++ b/src/pages/scholarship_info.rs @@ -1,7 +1,7 @@ use leptos::either::Either; +use leptos::html::Dialog; use leptos::logging::log; use leptos::prelude::*; -use leptos::reactive::spawn_local; use leptos_oidc::{AuthLoaded, Authenticated}; use leptos_router::hooks::{use_navigate, use_params}; use crate::common::{ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType}; @@ -10,7 +10,7 @@ use super::UnauthenticatedPage; use traits::{AsReactive, ReactiveCapture}; use crate::common::ComparisonData; use crate::pages::utils::get_user_claims; -use super::api::{get_comparison_info, get_provider_scholarships, get_scholarship_info, delete_provider_scholarship, CreateScholarshipInfo, CreateTestComparisons, RegisterScholarship}; +use super::api::{get_comparison_info, get_provider_scholarships, get_scholarship_info, CreateScholarshipInfo, CreateTestComparisons, RegisterScholarship, DeleteProviderScholarship}; /// # Scholarship Info Page @@ -85,6 +85,10 @@ pub fn ScholarshipInfoPage() -> impl IntoView { /// ``` #[component] fn ScholarshipList() -> impl IntoView { + // List state + let pending_delete = RwSignal::new(None::); + let delete_dialog_ref = NodeRef::::new(); + // Get scholarship provider information from authentication. This element will not display // unless that information is available. let user_claims = get_user_claims(); @@ -109,30 +113,90 @@ fn ScholarshipList() -> impl IntoView { // Register scholarship creation/deletion actions let create_action = ServerAction::::new(); + let delete_action = ServerAction::::new(); // Button handlers let create_on_click = move |_| { - // In the future, we'd actually like to create/show a dialog that gets the - // desired name of the scholarship. - // After the request is sent, we'll handle it in an effect. create_action.dispatch(RegisterScholarship { provider_id: provider_id.get().unwrap(), scholarship_name: "Testing Scholarship".to_string() }); }; + + let on_delete = move |s| { + pending_delete.set(Some(s)); + }; // Register scholarship creation effect Effect::new(move || { // We want to wait for the action to complete first, but then find whether the request // succeeded. if let Some(Ok(_)) = create_action.value().get() { - // Refetch and clear. + // Refetch and clear. We could alter this slightly to get the new element and + // update the list on the client side only (no refetch). We also want to set the name + // of the new scholarship before it's created on the server. scholarships.refetch(); create_action.clear(); } }); + + // Register scholarship deletion effects + let modal_disabled = Signal::derive(move || delete_action.pending().get()); + Effect::new(move || { + if let Some(Ok(_)) = delete_action.value().get() { + scholarships.refetch(); + pending_delete.set(None); + delete_action.clear(); + } + }); + + // Register scholarship pending delete effects + Effect::new(move || { + if let Some(dialog) = delete_dialog_ref.get() { + if pending_delete.get().is_some() { + // Find the modal dialog and show it. Ask for the user's confirmation. Afterward, + // take the correct action. + dialog.show_modal() + .expect("Couldn't show dialog. Is it already open?"); + } else { + dialog.close(); + } + } + }); view! { + +

"Confirm Deletion"

+

+ "Are you sure you want to delete " + {move || { + pending_delete.get().map(|s| { + s.data.get("name").unwrap_or(&ValueType::String(None)) + .as_string() + .unwrap_or_default() + .unwrap_or("".to_string()) + }) + }} + "?" +

+ "Confirm" + "Cancel" +
@@ -145,11 +209,9 @@ fn ScholarshipList() -> impl IntoView { match result { Ok(list) => { let views = list.iter().map(|entry| { - view! { - + view! { + } }); Either::Left(views.collect_view()) @@ -186,7 +248,7 @@ fn ScholarshipList() -> impl IntoView { /// view! { /// /// } /// ``` @@ -195,8 +257,8 @@ fn ScholarshipListEntry( /// The scholarship information. This element will attempt to get the name of the scholarship /// using its `name` field, and also utilizes the `subject` field. #[prop()] scholarship: ExpandableInfo, - /// A [`Callback<()>`] that runs upon successful deletion of the scholarship. - #[prop(into)] on_delete: Callback<()>, + /// A [`Callback`] that runs upon successful deletion of the scholarship. + #[prop(into)] on_delete: Callback, ) -> impl IntoView { let navigate = use_navigate(); let user_claims = get_user_claims(); @@ -221,26 +283,6 @@ fn ScholarshipListEntry( } }; - let delete_click = { - let subject = scholarship.subject.clone(); - move |_| { - // Delete the scholarship. - let subject = subject.clone(); - log!("Deleting scholarship with ID {:?}", subject); - if let Some(user_id) = user_id.get() { - spawn_local(async move { - match delete_provider_scholarship( - user_id, - subject - ).await { - Ok(_) => on_delete.run(()), - _ => {} - } - }); - } - } - }; - view! {
@@ -255,7 +297,7 @@ fn ScholarshipListEntry(
"Delete"