feat: allow users to confirm deletion of scholarships
This commit is contained in:
1 parent
f74e5ca093
commit
05d8e1527e
2 files changed
+78
-36
No files matched your search
@@ -15,7 +15,7 @@ use std::collections::HashMap;
|
|||||||
/// now, the table will simply create a single massive `Map` field that contains all the stored
|
/// 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,
|
/// 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.
|
/// 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 {
|
pub struct ExpandableInfo {
|
||||||
/// The main key member. Required and used to get/put the information in a database.
|
/// The main key member. Required and used to get/put the information in a database.
|
||||||
pub subject: String,
|
pub subject: String,
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
use leptos::either::Either;
|
use leptos::either::Either;
|
||||||
|
use leptos::html::Dialog;
|
||||||
use leptos::logging::log;
|
use leptos::logging::log;
|
||||||
use leptos::prelude::*;
|
use leptos::prelude::*;
|
||||||
use leptos::reactive::spawn_local;
|
|
||||||
use leptos_oidc::{AuthLoaded, Authenticated};
|
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||||
use leptos_router::hooks::{use_navigate, use_params};
|
use leptos_router::hooks::{use_navigate, use_params};
|
||||||
use crate::common::{ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType};
|
use crate::common::{ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType};
|
||||||
@@ -10,7 +10,7 @@ use super::UnauthenticatedPage;
|
|||||||
use traits::{AsReactive, ReactiveCapture};
|
use traits::{AsReactive, ReactiveCapture};
|
||||||
use crate::common::ComparisonData;
|
use crate::common::ComparisonData;
|
||||||
use crate::pages::utils::get_user_claims;
|
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
|
/// # Scholarship Info Page
|
||||||
@@ -85,6 +85,10 @@ pub fn ScholarshipInfoPage() -> impl IntoView {
|
|||||||
/// ```
|
/// ```
|
||||||
#[component]
|
#[component]
|
||||||
fn ScholarshipList() -> impl IntoView {
|
fn ScholarshipList() -> impl IntoView {
|
||||||
|
// List state
|
||||||
|
let pending_delete = RwSignal::new(None::<ExpandableInfo>);
|
||||||
|
let delete_dialog_ref = NodeRef::<Dialog>::new();
|
||||||
|
|
||||||
// Get scholarship provider information from authentication. This element will not display
|
// Get scholarship provider information from authentication. This element will not display
|
||||||
// unless that information is available.
|
// unless that information is available.
|
||||||
let user_claims = get_user_claims();
|
let user_claims = get_user_claims();
|
||||||
@@ -109,30 +113,90 @@ fn ScholarshipList() -> impl IntoView {
|
|||||||
|
|
||||||
// Register scholarship creation/deletion actions
|
// Register scholarship creation/deletion actions
|
||||||
let create_action = ServerAction::<RegisterScholarship>::new();
|
let create_action = ServerAction::<RegisterScholarship>::new();
|
||||||
|
let delete_action = ServerAction::<DeleteProviderScholarship>::new();
|
||||||
|
|
||||||
// Button handlers
|
// Button handlers
|
||||||
let create_on_click = move |_| {
|
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 {
|
create_action.dispatch(RegisterScholarship {
|
||||||
provider_id: provider_id.get().unwrap(),
|
provider_id: provider_id.get().unwrap(),
|
||||||
scholarship_name: "Testing Scholarship".to_string()
|
scholarship_name: "Testing Scholarship".to_string()
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
let on_delete = move |s| {
|
||||||
|
pending_delete.set(Some(s));
|
||||||
|
};
|
||||||
|
|
||||||
// Register scholarship creation effect
|
// Register scholarship creation effect
|
||||||
Effect::new(move || {
|
Effect::new(move || {
|
||||||
// We want to wait for the action to complete first, but then find whether the request
|
// We want to wait for the action to complete first, but then find whether the request
|
||||||
// succeeded.
|
// succeeded.
|
||||||
if let Some(Ok(_)) = create_action.value().get() {
|
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();
|
scholarships.refetch();
|
||||||
create_action.clear();
|
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! {
|
view! {
|
||||||
|
<dialog
|
||||||
|
node_ref=delete_dialog_ref
|
||||||
|
class="m-auto p-2"
|
||||||
|
>
|
||||||
|
<h2 class="text-2xl font-bold">"Confirm Deletion"</h2>
|
||||||
|
<p>
|
||||||
|
"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("<no name found>".to_string())
|
||||||
|
})
|
||||||
|
}}
|
||||||
|
"?"
|
||||||
|
</p>
|
||||||
|
<ActionButton
|
||||||
|
on:click=move |_| {
|
||||||
|
let subject = pending_delete.get().map(|s| s.subject.clone()).unwrap_or_default();
|
||||||
|
log!("Deleting scholarship with subject {}", subject);
|
||||||
|
delete_action.dispatch(DeleteProviderScholarship {
|
||||||
|
scholarship_id: subject,
|
||||||
|
provider_id: provider_id.get().unwrap_or_default()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
disabled=modal_disabled
|
||||||
|
>"Confirm"</ActionButton>
|
||||||
|
<ActionButton
|
||||||
|
on:click=move |_| pending_delete.set(None)
|
||||||
|
>"Cancel"</ActionButton>
|
||||||
|
</dialog>
|
||||||
<div class="w-75">
|
<div class="w-75">
|
||||||
<Panel>
|
<Panel>
|
||||||
<div class="pt-2">
|
<div class="pt-2">
|
||||||
@@ -145,11 +209,9 @@ fn ScholarshipList() -> impl IntoView {
|
|||||||
match result {
|
match result {
|
||||||
Ok(list) => {
|
Ok(list) => {
|
||||||
let views = list.iter().map(|entry| {
|
let views = list.iter().map(|entry| {
|
||||||
view! {
|
view! {
|
||||||
<ScholarshipListEntry scholarship=entry.clone()
|
<ScholarshipListEntry scholarship=entry.clone()
|
||||||
on_delete=move || {
|
on_delete=Callback::new(on_delete)/>
|
||||||
scholarships.refetch();
|
|
||||||
}/>
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
Either::Left(views.collect_view())
|
Either::Left(views.collect_view())
|
||||||
@@ -186,7 +248,7 @@ fn ScholarshipList() -> impl IntoView {
|
|||||||
/// view! {
|
/// view! {
|
||||||
/// <ScholarshipListEntry
|
/// <ScholarshipListEntry
|
||||||
/// scholarship= /* An ExpandableInfo object containing scholarship info */
|
/// scholarship= /* An ExpandableInfo object containing scholarship info */
|
||||||
/// on_delete=move || log!("This is a callback on successful deletion.")
|
/// on_delete=move |_| log!("This is a callback on successful deletion.")
|
||||||
/// />
|
/// />
|
||||||
/// }
|
/// }
|
||||||
/// ```
|
/// ```
|
||||||
@@ -195,8 +257,8 @@ fn ScholarshipListEntry(
|
|||||||
/// The scholarship information. This element will attempt to get the name of the scholarship
|
/// The scholarship information. This element will attempt to get the name of the scholarship
|
||||||
/// using its `name` field, and also utilizes the `subject` field.
|
/// using its `name` field, and also utilizes the `subject` field.
|
||||||
#[prop()] scholarship: ExpandableInfo,
|
#[prop()] scholarship: ExpandableInfo,
|
||||||
/// A [`Callback<()>`] that runs upon successful deletion of the scholarship.
|
/// A [`Callback<ExpandableInfo>`] that runs upon successful deletion of the scholarship.
|
||||||
#[prop(into)] on_delete: Callback<()>,
|
#[prop(into)] on_delete: Callback<ExpandableInfo, ()>,
|
||||||
) -> impl IntoView {
|
) -> impl IntoView {
|
||||||
let navigate = use_navigate();
|
let navigate = use_navigate();
|
||||||
let user_claims = get_user_claims();
|
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! {
|
view! {
|
||||||
<div class="flex flex-col rounded-md border-light inset-shadow-xs shadow-md my-2 transition-all hover:shadow-lg/33">
|
<div class="flex flex-col rounded-md border-light inset-shadow-xs shadow-md my-2 transition-all hover:shadow-lg/33">
|
||||||
<div class="p-2 text-center">
|
<div class="p-2 text-center">
|
||||||
@@ -255,7 +297,7 @@ fn ScholarshipListEntry(
|
|||||||
</div>
|
</div>
|
||||||
<div class="p-2 flex-1 bg-red-800 border-l border-white rounded-br-md text-white text-center
|
<div class="p-2 flex-1 bg-red-800 border-l border-white rounded-br-md text-white text-center
|
||||||
hover:bg-red-900 cursor-pointer transition-all"
|
hover:bg-red-900 cursor-pointer transition-all"
|
||||||
on:click=delete_click
|
on:click=move |_| on_delete.run(scholarship.clone())
|
||||||
>
|
>
|
||||||
"Delete"
|
"Delete"
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
Reference in new issue
Block a user