721 lines
33 KiB
Rust
721 lines
33 KiB
Rust
use std::collections::HashMap;
|
|
use leptos::either::Either;
|
|
use leptos::html::Dialog;
|
|
use leptos::logging::log;
|
|
use leptos::prelude::*;
|
|
use leptos_oidc::{AuthLoaded, Authenticated};
|
|
use leptos_router::hooks::{use_navigate, use_params};
|
|
use crate::common::{ComparisonData, ExpandableInfo, ScholarshipFormParams, SubmitStatus, ValueType};
|
|
use crate::components::{ActionButton, Banner, ChipsList, Header, Loading, OutlinedTextField, Panel, RadioList, Row, TextFieldType, Toast, ToastContext, ToastList, ValidatedForm};
|
|
use super::UnauthenticatedPage;
|
|
use crate::utils::get_user_claims;
|
|
use super::api::{get_provider_scholarships, get_scholarship_info, CreateScholarshipInfo, RegisterScholarship, DeleteProviderScholarship, get_comparisons_categorized};
|
|
|
|
|
|
/// # Scholarship Info Page
|
|
/// This page will handle creating or editing scholarship information given a specific
|
|
/// scholarship ID and scholarship provider subject number. The form itself will only
|
|
/// track a single scholarship (for now) and so selection of what scholarship will be edited
|
|
/// must be handled *before* navigation to this page.
|
|
#[component]
|
|
pub fn ScholarshipInfoPage() -> impl IntoView {
|
|
// Changes to this signal update the scholarship list.
|
|
let list_refresh = RwSignal::new(0);
|
|
|
|
let url_params = use_params::<ScholarshipFormParams>();
|
|
let scholarship_id = Memo::new(move |_| {
|
|
url_params.read()
|
|
.as_ref()
|
|
.ok()
|
|
.and_then(|params| params.id.clone())
|
|
});
|
|
|
|
// let create_test_comps = ServerAction::<CreateTestComparisons>::new();
|
|
|
|
let on_delete = move || {
|
|
let navigate = use_navigate();
|
|
navigate("/providers/scholarships", Default::default());
|
|
};
|
|
|
|
// Remember that we only want to show the page after we've got authentication information
|
|
// and if there's data for us to use.
|
|
view! {
|
|
<Banner title="R15 Scholarships" logo="/PHS_Stacked_Acronym.png" path="/providers" />
|
|
<ToastList>
|
|
<AuthLoaded fallback=Loading>
|
|
<Authenticated unauthenticated=UnauthenticatedPage>
|
|
<div class="flex flex-row align-top items-start">
|
|
<div class="flex flex-col flex-1" />
|
|
<ScholarshipList refresh_token=list_refresh on_delete=on_delete />
|
|
<Transition fallback=Loading>
|
|
<ScholarshipForm
|
|
scholarship_id=scholarship_id
|
|
on_submit=move || {
|
|
list_refresh.update(|n| *n += 1);
|
|
}
|
|
/>
|
|
</Transition>
|
|
<div class="flex flex-col flex-1" />
|
|
</div>
|
|
</Authenticated>
|
|
</AuthLoaded>
|
|
</ToastList>
|
|
}
|
|
}
|
|
|
|
/// # Scholarship List Component
|
|
///
|
|
/// This component should always be wrapped in an Authenticated component. Assuming that a provider
|
|
/// is authenticated, this component will get all the scholarships that this provider owns and
|
|
/// will display them in a list, giving them the option to edit, delete, or create new scholarships.
|
|
///
|
|
/// Example usage:
|
|
/// ```
|
|
/// let refresh = RwSignal::new(0);
|
|
///
|
|
/// view! {
|
|
/// <AuthLoaded>
|
|
/// <Authenticated>
|
|
/// <ScholarshipList
|
|
/// refresh_token=refresh
|
|
/// on_delete=move || log!("This is run when an item is deleted on the server.")
|
|
/// />
|
|
/// </Authenticated>
|
|
/// </AuthLoaded>
|
|
/// }
|
|
/// ```
|
|
#[component]
|
|
fn ScholarshipList(
|
|
/// A signal that refreshes the list when changed. Most times, we don't need to worry about the
|
|
/// user reloading our page 2 billion times.
|
|
#[prop()] refresh_token: RwSignal<i32>,
|
|
/// A callback that is run on successful server-side deletion of an item.
|
|
#[prop(into)] on_delete: Callback<()>
|
|
) -> 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
|
|
// unless that information is available.
|
|
let user_claims = get_user_claims();
|
|
let provider_id = Memo::new(move |_|
|
|
user_claims.get()
|
|
.as_ref()
|
|
.map(|info| info.claims.subject.clone())
|
|
);
|
|
|
|
// Register scholarship server resource
|
|
let scholarships = Resource::new(
|
|
move || {
|
|
(refresh_token.get(), provider_id.get())
|
|
},
|
|
|(_, provider_id)| async move {
|
|
log!("Fetching from scholarship API.");
|
|
let Some(provider_id) = provider_id else {
|
|
return Ok(Vec::new());
|
|
};
|
|
|
|
// Get the scholarships
|
|
get_provider_scholarships(provider_id).await
|
|
}
|
|
);
|
|
|
|
// Register scholarship creation/deletion actions
|
|
let create_action = ServerAction::<RegisterScholarship>::new();
|
|
let delete_action = ServerAction::<DeleteProviderScholarship>::new();
|
|
|
|
// Button handlers
|
|
let create_on_click = move |_| {
|
|
create_action.dispatch(RegisterScholarship {
|
|
provider_id: provider_id.get().unwrap()
|
|
});
|
|
};
|
|
|
|
let on_item_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. 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();
|
|
on_delete.run(());
|
|
}
|
|
});
|
|
|
|
// 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();
|
|
}
|
|
}
|
|
});
|
|
|
|
let modal_on_click_continue = 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(),
|
|
});
|
|
};
|
|
|
|
view! {
|
|
<dialog
|
|
node_ref=delete_dialog_ref
|
|
class="m-auto p-5 rounded-lg shadow-xl/50
|
|
backdrop:backdrop-blur-xs backdrop:transition-backdrop-filter"
|
|
>
|
|
<h2 class="text-2xl font-bold">"Confirm Deletion"</h2>
|
|
<p class="mb-3">
|
|
"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("this unnamed scholarship".to_string())
|
|
})
|
|
}} "?"
|
|
</p>
|
|
<div class="flex mx-auto items-center justify-center">
|
|
<ActionButton on:click=modal_on_click_continue disabled=modal_disabled>
|
|
"Yes"
|
|
</ActionButton>
|
|
<ActionButton on:click=move |_| pending_delete.set(None)>"No"</ActionButton>
|
|
</div>
|
|
</dialog>
|
|
<div class="w-75">
|
|
<Panel>
|
|
<div class="pt-2">
|
|
<h2 class="text-xl font-bold text-center">"Scholarships"</h2>
|
|
</div>
|
|
<Transition fallback=Loading>
|
|
<div class="flex flex-col gap-2">
|
|
{move || {
|
|
scholarships
|
|
.get()
|
|
.map(|result| {
|
|
match result {
|
|
Ok(list) => {
|
|
let views = list
|
|
.iter()
|
|
.map(|entry| {
|
|
view! {
|
|
<ScholarshipListEntry
|
|
scholarship=entry.clone()
|
|
on_delete=Callback::new(on_item_delete)
|
|
/>
|
|
}
|
|
});
|
|
Either::Left(views.collect_view())
|
|
}
|
|
Err(err) => {
|
|
Either::Right(
|
|
view! {
|
|
<div>
|
|
<p>"Failed to load scholarship list: "{err.to_string()}</p>
|
|
</div>
|
|
},
|
|
)
|
|
}
|
|
}
|
|
})
|
|
.collect_view()
|
|
}}
|
|
</div>
|
|
<div>
|
|
<div
|
|
class="p-2 bg-red-800 rounded-md text-center text-white hover:bg-red-900 transition-all cursor-pointer"
|
|
on:click=create_on_click
|
|
>
|
|
"Create New"
|
|
</div>
|
|
</div>
|
|
</Transition>
|
|
</Panel>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
/// # Scholarship List Entry Component
|
|
///
|
|
/// Displays a single scholarship within the [`ScholarshipList`] component.
|
|
///
|
|
/// Example usage (within the [`ScholarshipList`]):
|
|
/// ```
|
|
/// view! {
|
|
/// <ScholarshipListEntry
|
|
/// scholarship= /* An ExpandableInfo object containing scholarship info */
|
|
/// on_delete=move |_| log!("This is a callback on successful deletion.")
|
|
/// />
|
|
/// }
|
|
/// ```
|
|
#[component]
|
|
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<ExpandableInfo>`] that runs upon successful deletion of the scholarship.
|
|
#[prop(into)] on_delete: Callback<ExpandableInfo, ()>,
|
|
) -> impl IntoView {
|
|
let navigate = use_navigate();
|
|
let user_claims = get_user_claims();
|
|
let _user_id = Memo::new(move |_| {
|
|
user_claims.get()
|
|
.map(|info| info.claims.subject.clone())
|
|
});
|
|
|
|
// Get the values that we need out of the info. We just need the subject and the name of
|
|
// the scholarship.
|
|
let name = scholarship.data.get("name")
|
|
.unwrap_or(&ValueType::String(None))
|
|
.as_string().unwrap_or_default()
|
|
.unwrap_or("Unnamed Scholarship".to_string());
|
|
|
|
// Button click handlers
|
|
let edit_click = {
|
|
let subject = scholarship.subject.clone();
|
|
move |_| {
|
|
let path = format!("/providers/scholarships/{}", subject);
|
|
navigate(path.as_str(), Default::default());
|
|
}
|
|
};
|
|
|
|
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="p-2 text-center">
|
|
<span>{name}</span>
|
|
</div>
|
|
<div class="flex flex-row">
|
|
<div
|
|
class="p-2 flex-1 bg-red-800 border-r border-white rounded-bl-md text-white text-center
|
|
hover:bg-red-900 cursor-pointer transition-all"
|
|
on:click=edit_click
|
|
>
|
|
"Edit"
|
|
</div>
|
|
<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"
|
|
on:click=move |_| on_delete.run(scholarship.clone())
|
|
>
|
|
"Delete"
|
|
</div>
|
|
</div>
|
|
</div>
|
|
}
|
|
}
|
|
|
|
/// # Scholarship Edit Form Component
|
|
///
|
|
/// Displays all information about a scholarship and allows the provider to edit that information.
|
|
/// All information is only saved when the submit button is clicked.
|
|
///
|
|
/// The component will call the `on_submit` callback when the API returns an `Ok` result. In the
|
|
/// event that the API call fails, it will not call anything. This is mainly used to allow updates
|
|
/// to neighboring components from the parent.
|
|
///
|
|
/// Example usage:
|
|
/// ```
|
|
/// view! {
|
|
/// <ScholarshipForm
|
|
/// scholarship_id="SomeScholarshipID"
|
|
/// on_submit=move || log!("This is a callback!")
|
|
/// />
|
|
/// }
|
|
/// ```
|
|
#[component]
|
|
fn ScholarshipForm(
|
|
/// The ID of the scholarship to edit. The form will handle getting all information about the
|
|
/// scholarship and submitting new information.
|
|
#[prop(into)] scholarship_id: Signal<Option<String>>,
|
|
/// A callback that is run when the form successfully submits new data. In the event that the
|
|
/// submission fails, this function is not run.
|
|
#[prop(into)] on_submit: Callback<()>
|
|
) -> impl IntoView {
|
|
//#region Form Data Setup
|
|
|
|
let scholarship_info = Resource::new(
|
|
move || scholarship_id.get(),
|
|
async move |opt_id| {
|
|
if let Some(id) = opt_id {
|
|
get_scholarship_info(id).await
|
|
} else {
|
|
Err(ServerFnError::new("Cannot get from server with no ID."))
|
|
}
|
|
}
|
|
);
|
|
|
|
let form_data = RwSignal::new(HashMap::new());
|
|
let chips_data = RwSignal::new(HashMap::new());
|
|
|
|
Effect::new(move || {
|
|
if let Some(Ok(scholarship)) = scholarship_info.get() {
|
|
// Get the default chips data.
|
|
let chips_default = if let Some(ValueType::Map(Some(map))) = scholarship.data.get("requirements") {
|
|
log!("Found requirements map: {:?}", map);
|
|
map.clone()
|
|
} else {
|
|
log!("Couldn't find requirements map in scholarship info. Using empty map.");
|
|
HashMap::new()
|
|
};
|
|
|
|
chips_data.set(chips_default);
|
|
form_data.set(scholarship.data);
|
|
}
|
|
});
|
|
|
|
//#endregion
|
|
//#region Comparison Logic
|
|
|
|
let comparison_lists = Resource::new(
|
|
move || scholarship_id.get().is_some(),
|
|
async move |_| {
|
|
get_comparisons_categorized().await
|
|
}
|
|
);
|
|
|
|
let comparison_lists_sorted = Signal::derive(move || {
|
|
if let Some(Ok(map)) = comparison_lists.get() {
|
|
let mut map = map.clone();
|
|
map
|
|
.into_iter()
|
|
.map(|(cat, mut list)| {
|
|
list.sort_by_key(|comp| comp.display_text.clone());
|
|
(cat, list)
|
|
})
|
|
.collect::<HashMap<String, Vec<ComparisonData>>>()
|
|
} else {
|
|
HashMap::new()
|
|
}
|
|
});
|
|
|
|
let get_comp_category = move |category: &str| {
|
|
comparison_lists_sorted.get()
|
|
.get(category)
|
|
.cloned()
|
|
.unwrap_or(Vec::new())
|
|
};
|
|
|
|
let get_comp_ids = move |category: &str| {
|
|
get_comp_category(category)
|
|
.into_iter()
|
|
.map(|comp| comp.id)
|
|
.collect::<Vec<String>>()
|
|
};
|
|
|
|
let get_comp_text = move |category: &str| {
|
|
get_comp_category(category)
|
|
.into_iter()
|
|
.map(|comp| comp.display_text)
|
|
.collect::<Vec<String>>()
|
|
};
|
|
|
|
// Get comparison lists.
|
|
let sports_ids = Signal::derive(move || get_comp_ids("Sports Participation"));
|
|
let sports_text = Signal::derive(move || get_comp_text("Sports Participation"));
|
|
|
|
let misc_ids = Signal::derive(move || get_comp_ids("Additional Eligibility Factors"));
|
|
let misc_text = Signal::derive(move || get_comp_text("Additional Eligibility Factors"));
|
|
|
|
let comm_ids = Signal::derive(move || get_comp_ids("Community Service"));
|
|
let comm_text = Signal::derive(move || get_comp_text("Community Service"));
|
|
|
|
let res_ids = Signal::derive(move || get_comp_ids("Residency"));
|
|
let res_text = Signal::derive(move || get_comp_text("Residency"));
|
|
|
|
let gpa_ids = Signal::derive(move || get_comp_ids("GPA Limits"));
|
|
let gpa_text = Signal::derive(move || get_comp_text("GPA Limits"));
|
|
|
|
let major_ids = Signal::derive(move || get_comp_ids("Majors"));
|
|
let major_text = Signal::derive(move || get_comp_text("Majors"));
|
|
|
|
//#endregion
|
|
//#region Submission Logic
|
|
|
|
let submit_action = ServerAction::<CreateScholarshipInfo>::new();
|
|
|
|
let submit_status = RwSignal::new(SubmitStatus::Idle);
|
|
let elements_disabled = Signal::derive(move || {
|
|
matches!(submit_action.pending().get(), true)
|
|
});
|
|
|
|
Effect::new(move || {
|
|
if submit_action.pending().get() {
|
|
submit_status.set(SubmitStatus::Sending);
|
|
return;
|
|
}
|
|
|
|
if let Some(result) = submit_action.value().get() {
|
|
match result {
|
|
Ok(()) => {
|
|
submit_status.set(SubmitStatus::Success);
|
|
on_submit.run(());
|
|
},
|
|
Err(_err) => submit_status.set(SubmitStatus::Error(()))
|
|
}
|
|
}
|
|
});
|
|
|
|
let mut toasts = expect_context::<ToastContext>();
|
|
|
|
let on_submit = move |_| {
|
|
let mut info = ExpandableInfo::new(scholarship_id.get().unwrap_or_default());
|
|
info.data = form_data.get();
|
|
info.data.insert("requirements".to_string(), ValueType::Map(Some(chips_data.get())));
|
|
|
|
log!("Map values: {:?}", info.data);
|
|
submit_action.dispatch(CreateScholarshipInfo {
|
|
info
|
|
});
|
|
};
|
|
|
|
Effect::watch(
|
|
move || submit_action.value().get(),
|
|
move |value, _, _| {
|
|
log!("Running submit_action effect");
|
|
let Some(result) = value else {
|
|
return;
|
|
};
|
|
|
|
let toast = match result {
|
|
Ok(_) => Toast::new()
|
|
.id(uuid::Uuid::new_v4())
|
|
.header("Submission Successful")
|
|
.msg("You can go back or continue editing your responses."),
|
|
Err(err) => Toast::new()
|
|
.id(uuid::Uuid::new_v4())
|
|
.header("Submission Failed")
|
|
.msg(err.to_string())
|
|
};
|
|
|
|
untrack(move || {
|
|
submit_action.clear();
|
|
toasts.toast(toast);
|
|
});
|
|
},
|
|
false
|
|
);
|
|
|
|
//#endregion
|
|
|
|
view! {
|
|
<Panel>
|
|
<Show
|
|
when=move || scholarship_id.get().is_some()
|
|
fallback=|| {
|
|
view! { <p>"Choose a scholarship from the right, or create a new one."</p> }
|
|
}
|
|
>
|
|
<Suspense fallback=Loading>
|
|
{move || {
|
|
scholarship_info
|
|
.get()
|
|
.map(|res_scholarship| {
|
|
match res_scholarship {
|
|
Ok(_) => {
|
|
Either::Left(
|
|
view! {
|
|
<ValidatedForm
|
|
on_submit=Callback::new(on_submit)
|
|
title="Scholarship Info Form"
|
|
description="Create or edit a scholarship. Input all information and click Submit."
|
|
>
|
|
<Row>
|
|
<OutlinedTextField
|
|
label="Scholarship Name"
|
|
placeholder="Example Scholarship Name"
|
|
disabled=elements_disabled
|
|
data_member="name"
|
|
data_map=form_data
|
|
/>
|
|
</Row>
|
|
<Row>
|
|
<OutlinedTextField
|
|
label="Amount per award: (if multiple, enter the highest value)"
|
|
placeholder="Enter an amount..."
|
|
disabled=elements_disabled
|
|
data_member="amount_per_award"
|
|
data_map=form_data
|
|
input_type=TextFieldType::Number
|
|
/>
|
|
</Row>
|
|
<Row>
|
|
<OutlinedTextField
|
|
label="Total number of awards:"
|
|
placeholder="Enter an amount..."
|
|
disabled=elements_disabled
|
|
data_member="num_awards"
|
|
data_map=form_data
|
|
input_type=TextFieldType::Number
|
|
/>
|
|
</Row>
|
|
<Row>
|
|
<OutlinedTextField
|
|
label="Total amount of all award(s): (estimates are acceptable)"
|
|
placeholder="Enter an amount..."
|
|
disabled=elements_disabled
|
|
data_member="total_awards"
|
|
data_map=form_data
|
|
input_type=TextFieldType::Number
|
|
/>
|
|
</Row>
|
|
<Row>
|
|
<RadioList
|
|
data_member="fafsa_required"
|
|
data_map=form_data
|
|
items=vec!["Yes".to_string(), "No".to_string()]
|
|
disabled=elements_disabled
|
|
label="Do you require student FAFSA information?"
|
|
/>
|
|
</Row>
|
|
<Row>
|
|
<RadioList
|
|
data_member="transcript_required"
|
|
data_map=form_data
|
|
items=vec!["Yes".to_string(), "No".to_string()]
|
|
disabled=elements_disabled
|
|
label="Do you require a student transcript?"
|
|
/>
|
|
</Row>
|
|
<Row>
|
|
<RadioList
|
|
data_member="award_to"
|
|
data_map=form_data
|
|
items=vec!["University".to_string(), "Student".to_string()]
|
|
disabled=elements_disabled
|
|
label="Will the award be made to the school or the student?"
|
|
/>
|
|
</Row>
|
|
<Row>
|
|
<RadioList
|
|
data_member="essay_prompt"
|
|
data_map=form_data
|
|
items=vec![
|
|
"Write an essay describing your major/career direction. Include any abilities, interests, experiences, employment or clubs you have had that have affected your direction. If you are undecided, indicate why.",
|
|
"Why is community service important? How has it personally affected you and your goals and what role do you hope to play in it in the future?",
|
|
"How has participation in sports been beneficial to your personal, academic, and emotional growth?",
|
|
]
|
|
.iter()
|
|
.map(|prompt| prompt.to_string())
|
|
.collect()
|
|
disabled=elements_disabled
|
|
label="Select an essay prompt, or leave blank if none is required:"
|
|
other_prompt="Custom prompt..."
|
|
/>
|
|
</Row>
|
|
<Header
|
|
title="Eligibility Requirements"
|
|
description="Choose any requirements that this scholarship may have. If there are no extra requirements,
|
|
leave all options blank, however please note that this will apply all students to your scholarship."
|
|
/>
|
|
<Row>
|
|
<ChipsList
|
|
label="GPA Requirements"
|
|
data_member="gpa"
|
|
data_map=chips_data
|
|
values=gpa_ids
|
|
displayed_text=gpa_text
|
|
allows_multiple=false
|
|
/>
|
|
</Row>
|
|
<Row>
|
|
<ChipsList
|
|
label="Sports Participation"
|
|
data_member="sports_participation"
|
|
data_map=chips_data
|
|
values=sports_ids
|
|
displayed_text=sports_text
|
|
/>
|
|
</Row>
|
|
<Row>
|
|
<ChipsList
|
|
label="Community Service"
|
|
data_member="community_service"
|
|
data_map=chips_data
|
|
values=comm_ids
|
|
displayed_text=comm_text
|
|
allows_multiple=false
|
|
/>
|
|
</Row>
|
|
<Row>
|
|
<ChipsList
|
|
label="Residency"
|
|
data_member="residency"
|
|
data_map=chips_data
|
|
values=res_ids
|
|
displayed_text=res_text
|
|
allows_multiple=false
|
|
/>
|
|
</Row>
|
|
<Row>
|
|
<ChipsList
|
|
label="Majors"
|
|
data_member="major"
|
|
data_map=chips_data
|
|
values=major_ids
|
|
displayed_text=major_text
|
|
/>
|
|
</Row>
|
|
<Row>
|
|
<ChipsList
|
|
label="Additional Eligibility Factors"
|
|
data_member="misc"
|
|
data_map=chips_data
|
|
values=misc_ids
|
|
displayed_text=misc_text
|
|
/>
|
|
</Row>
|
|
</ValidatedForm>
|
|
},
|
|
)
|
|
}
|
|
Err(err) => {
|
|
Either::Right(
|
|
view! {
|
|
<p>"Error while getting scholarship: "{err.to_string()}</p>
|
|
},
|
|
)
|
|
}
|
|
}
|
|
})
|
|
.collect_view()
|
|
}}
|
|
</Suspense>
|
|
</Show>
|
|
</Panel>
|
|
}.into_any()
|
|
}
|