feat: sweeping improvements to home_page.rs
Completely refactors the ownership of all data such that server resources are loaded into a signal asynchronously, then displayed in the form properly. All button handlers are also now separately defined functions, instead of being inline.
This commit is contained in:
1 parent
74e7111820
commit
85ec857e38
4 files changed
+243
-306
No files matched your search
+76
-126
@@ -1,136 +1,86 @@
|
||||
Forms should be built for this application with a series of JSON-type files.
|
||||
Each form will have a series of sections, and each section will have a series of
|
||||
questions. Each question has a type and a label - question styles may come later.
|
||||
It may be possible for us to call out specific question types without needing
|
||||
to parse the types separately, but that feels not-really-worth-it.
|
||||
To track errors and force form input validation, we're going to create a custom `ValidatedForm` component.
|
||||
|
||||
Example:
|
||||
This component will do two things:
|
||||
- Provide a submit button (that is disabled if the form is invalid, still thinking about that)
|
||||
- Define a validation method that will be called on submit that's updated on a per-input basis
|
||||
(this likely looks like a `Vec<RwSignal<ValidationState>>`)
|
||||
|
||||
```
|
||||
{
|
||||
"SectionDefinitions": [
|
||||
"SectionTitle": [
|
||||
"text": {
|
||||
"label": "First name question!",
|
||||
"placeholder": "John",
|
||||
},
|
||||
"text": {
|
||||
"label": "Last name question!",
|
||||
"placeholder": "Smith",
|
||||
}
|
||||
],
|
||||
"Section Two": [
|
||||
"text": {
|
||||
"label": "Section two testing question",
|
||||
"placeholder": "Some example here",
|
||||
},
|
||||
"select": {
|
||||
"label": "This is a dropdown element!",
|
||||
"placeholder": "option1",
|
||||
"possibleValues": [
|
||||
"option1": "something",
|
||||
"option2": "another something",
|
||||
]
|
||||
}
|
||||
]
|
||||
]
|
||||
}
|
||||
```
|
||||
In order for this to work, the form will `provide_context` to the `Vec`, and then it will check this
|
||||
vector on submit. It doesn't worry about the states inbetween, only during the submission logic.
|
||||
|
||||
...and so on. Here's a likely set of structures to use:
|
||||
The inputs start off with a default state, which will be defined by that input's validation method.
|
||||
This means that every input will run the validation method on mount and on each change. The first time the
|
||||
function is called should be during mount, which will set the default state. This does mean that any inputs
|
||||
that are marked as required will always be invalid on mount, and so we need to determine when these
|
||||
error messages should be shown and updated.
|
||||
|
||||
```
|
||||
struct FormDefinitionDoc {
|
||||
Vec<Section> section_defs;
|
||||
// and any extra values that should be applied
|
||||
// across the entire form, like settings.
|
||||
I think they should be shown in these situations:
|
||||
- Text inputs: on blur - we don't want to scream at users as they type.
|
||||
- Selectable inputs: on change and form submit - only show the message if the value is invalid, and not at mount.
|
||||
|
||||
To check values for the selectable inputs at submit time, we'll probably need to provide a refresh method
|
||||
that can be called on submit. This method must somehow be callable from the outside of the component and
|
||||
will show the error messages. We'd likely store all this in a struct of some kind, which could contain an
|
||||
`error` and a `show_message` signal.
|
||||
|
||||
We already know that we can send any `Signal` to another component, so we can just send the `show_message`
|
||||
signal to the parent component without any issues. We'll just use that signal to show the error messages
|
||||
in the component as well. Usually we don't want to edit too many signals outside their local context,
|
||||
but this is a special case – documentation is therefore incredibly important! Remember that we may also
|
||||
edit the `show_message` signal within its local context as well, so we need to make sure there are no cyclical
|
||||
dependencies. Alternatively, we could not edit the `show_message` signal at all locally and just use it
|
||||
to trigger side effects that show messages. Have to think about this more before implementation.
|
||||
|
||||
Here's a rough sketch of it all put together:
|
||||
```rust
|
||||
enum ValidationState {
|
||||
/// A valid input!
|
||||
Valid,
|
||||
/// An invalid input with an error message.
|
||||
Invalid(String)
|
||||
}
|
||||
|
||||
struct Section {
|
||||
Vec<QuestionComponent> questions;
|
||||
// each section may have a series of settings including how to submit,
|
||||
// when to submit, and where to submit to at the very least.
|
||||
// We'll store this in a Vec<RwSignal<InputState>>
|
||||
struct InputState {
|
||||
/// Determines if there is an error and provides an error message.
|
||||
error: Signal<ValidationState>,
|
||||
/// Determines if the form has requested to show error messages.
|
||||
show_message: RwSignal<bool>
|
||||
}
|
||||
|
||||
struct QuestionComponent {
|
||||
label: String,
|
||||
placeholder: String,
|
||||
possible_values: HashMap<str, str>,
|
||||
// there are definitely values that i'll find while creating this structure.
|
||||
// Define a new type for ease of use.
|
||||
type ValidationVec = Vec<RwSignal<InputState>>;
|
||||
|
||||
// Parent form component
|
||||
#[component]
|
||||
fn Form() -> impl IntoView {
|
||||
let validation_states: ValidationVec = Vec::new();
|
||||
|
||||
provide_context(validation_states);
|
||||
|
||||
/* All other form logic */
|
||||
}
|
||||
|
||||
fn validation_function(input: String) -> ValidationState {
|
||||
// Validation logic is specific to each input type.
|
||||
}
|
||||
|
||||
// Child component
|
||||
#[component]
|
||||
fn AnyInput() -> impl IntoView {
|
||||
let validation_states = use_context::<ValidationVec>().expect("Couldn't find form validation context");
|
||||
let current_input = RwSignal::new(String::new()); // Obviously, this would have a default value.
|
||||
let error_signal = RwSignal::new(validation_function(current_input.get()));
|
||||
let show_errors = RwSignal::new(false); // Don't show them on mount!
|
||||
|
||||
// See below for InputState definition
|
||||
let error_state = InputState {
|
||||
error: error_signal.clone().into(),
|
||||
show_message: show_errors.clone()
|
||||
};
|
||||
|
||||
validation_states.push(RwSignal::new())
|
||||
|
||||
/* All other input logic */
|
||||
}
|
||||
```
|
||||
|
||||
Types of questions that are required, starting with the providers.
|
||||
|
||||
Providers must be able to:
|
||||
|
||||
- Log into the website using a username and password (handled by AWS)
|
||||
- Create and manage multiple scholarships.
|
||||
- Input scholarship information for each scholarship.
|
||||
- View students that have applied.
|
||||
- View student application information (should just be labels??)
|
||||
- Buttons are a given. Duh.
|
||||
|
||||
Students must be able to:
|
||||
|
||||
- Log in using their Region 15 email (handled by AWS)
|
||||
- Create and manage their general application.
|
||||
- Create and manage applications specific to a single scholarship.
|
||||
- View their acceptance status (need more confirmation)
|
||||
|
||||
Administrators must be able to:
|
||||
|
||||
- Log in using their Region 15 email (handled by AWS)
|
||||
- Manage student scholarship applications.
|
||||
- Manage scholarship provider account information.
|
||||
- Create/manage scholarship information.
|
||||
- Download student applications if needed.
|
||||
- Statistics would be nice, but are likely a future thing.
|
||||
|
||||
Right off the bad, I think I can see a few types of buttons and text inputs,
|
||||
along with dropdowns, checkboxes, our custom `MultiEntry` element from last
|
||||
time. That means no new options, but it does mean that the `FormDefinition`
|
||||
needs to be able to keep track of all this information.
|
||||
|
||||
These are some example `QuestionComponent` objects:
|
||||
|
||||
Text fields:
|
||||
|
||||
```
|
||||
{
|
||||
question_type: "TEXT" | "EMAIL" | "NUMBER",
|
||||
label: "The question label",
|
||||
name: "input_name[jsdoc]",
|
||||
placeholder: "An example input"
|
||||
}
|
||||
```
|
||||
|
||||
Dropdowns/checkboxes/radios:
|
||||
|
||||
```
|
||||
{
|
||||
question_type: "DROPDOWN" | "CHECKBOX" | "RADIO",
|
||||
label: "The question label",
|
||||
name: "input_name[jsdoc]",
|
||||
placeholder: "The pre-selected option value",
|
||||
options: [
|
||||
"optionValue": "Option Text"
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
`MultiEntry`:
|
||||
|
||||
```
|
||||
{
|
||||
question_type: "MULTI",
|
||||
label: "The question label",
|
||||
name: "input_name[jsdoc]"
|
||||
}
|
||||
```
|
||||
|
||||
The real question here: how can we store this information in a database? And
|
||||
this means specifically within DynamoDB, because this is not an easy structure
|
||||
to store in there.
|
||||
That's going to be the hardest apart and I'll likely involve the AI in that
|
||||
planning.
|
||||
@@ -9,7 +9,7 @@ pub fn Checkbox(
|
||||
#[prop(into)] on_change: Callback<(), ()>,
|
||||
#[prop(into)] value: String,
|
||||
#[prop(into)] name: String,
|
||||
#[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>,
|
||||
#[prop(into)] disabled: Signal<bool>,
|
||||
) -> impl IntoView {
|
||||
let id = create_unique_id(&name, &value);
|
||||
|
||||
@@ -53,7 +53,7 @@ pub fn CheckboxList(
|
||||
#[prop(into)] data_member: String,
|
||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
#[prop()] items: Vec<String>,
|
||||
#[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>,
|
||||
#[prop(optional, into)] disabled: Signal<bool>,
|
||||
#[prop(optional, into)] label: String,
|
||||
) -> impl IntoView {
|
||||
view! {
|
||||
|
||||
@@ -15,7 +15,7 @@ pub fn Select(
|
||||
#[prop(into)] data_member: String,
|
||||
#[prop()] data_map: RwSignal<HashMap<String, ValueType>>,
|
||||
#[prop(optional, into)] label: String,
|
||||
#[prop(default = RwSignal::new(false))] disabled: RwSignal<bool>,
|
||||
#[prop(optional, into)] disabled: Signal<bool>,
|
||||
) -> impl IntoView {
|
||||
let on_change = {
|
||||
let data_member = data_member.clone();
|
||||
|
||||
+164
-177
@@ -1,3 +1,4 @@
|
||||
use std::collections::HashMap;
|
||||
// Server dependencies
|
||||
#[cfg(feature = "ssr")]
|
||||
use aws_sdk_dynamodb::{Client, error::ProvideErrorMetadata, types::AttributeValue};
|
||||
@@ -17,10 +18,10 @@ use leptos::leptos_dom::logging::console_log;
|
||||
use leptos::prelude::*;
|
||||
use leptos_oidc::{AuthLoaded, Authenticated};
|
||||
use base64::Engine;
|
||||
use leptos::logging::log;
|
||||
use leptos::task::spawn_local;
|
||||
use leptos::web_sys::HtmlAnchorElement;
|
||||
use leptos::wasm_bindgen::JsCast;
|
||||
use traits::{AsReactive, ReactiveCapture};
|
||||
use crate::pages::utils::get_user_claims;
|
||||
|
||||
/// # Get Student Info
|
||||
@@ -141,189 +142,50 @@ pub async fn create_example_pdf() -> Result<Vec<u8>, ServerFnError> {
|
||||
/// The main home page component. Contains a simple contact form.
|
||||
#[component]
|
||||
pub fn HomePage() -> impl IntoView {
|
||||
// Creates a reactive value to update the button
|
||||
//#region Server Resources
|
||||
let user_claims = get_user_claims();
|
||||
let user_subject = Memo::new(move |_| {
|
||||
user_claims.get()
|
||||
.map(|claim| claim.claims.subject.clone())
|
||||
});
|
||||
|
||||
// Note that the value passed in MUST be equatable.
|
||||
// We get/unwrap the value repeatedly until we get a simple string value, then clone it so that
|
||||
// we don't lose access to it in the future, should we need it again.
|
||||
let server_resource: Resource<ExpandableInfo> = Resource::new(
|
||||
move || user_claims.get().map(|claim| claim.claims.subject.clone()),
|
||||
async |opt_username| match opt_username {
|
||||
Some(subject) => get_submission(subject.clone()).await.unwrap_or_else(|e| {
|
||||
console_log(e.to_string().as_str());
|
||||
ExpandableInfo::new(subject)
|
||||
}),
|
||||
None => ExpandableInfo::new("".to_owned()),
|
||||
},
|
||||
let form_data = RwSignal::new(HashMap::new());
|
||||
|
||||
let server_resource = Resource::new(
|
||||
move || user_subject.get(),
|
||||
async move |subject| get_submission(subject.unwrap_or_default()).await
|
||||
);
|
||||
let submit_action = ServerAction::<CreateSampleSubmission>::new();
|
||||
|
||||
view! {
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||
// Replace this fallback with a real loading screen.
|
||||
<Suspense fallback=Loading>
|
||||
<ValidatedForm on_submit=Callback::new(move |_| {})>
|
||||
{move || {
|
||||
server_resource
|
||||
.get()
|
||||
.map(|submission| {
|
||||
let expandable_react = submission.as_reactive();
|
||||
let elements_disabled = RwSignal::new(false);
|
||||
let result_msg = Signal::derive(move || {
|
||||
if submit_action.pending().get() {
|
||||
elements_disabled.set(true);
|
||||
"Sending request...".to_owned()
|
||||
} else {
|
||||
elements_disabled.set(false);
|
||||
if let Some(result) = submit_action.value().get() {
|
||||
match result {
|
||||
Ok(_) => "Request sent successfully.".to_owned(),
|
||||
Err(e) => format!("Failed to send request: {:?}", e),
|
||||
Effect::new(move || {
|
||||
match server_resource.get() {
|
||||
Some(Ok(submission)) => {
|
||||
form_data.set(submission.data);
|
||||
}
|
||||
} else {
|
||||
"".to_owned()
|
||||
Some(Err(err)) => {
|
||||
log!("Error while getting information: {:?}", err);
|
||||
}
|
||||
_ => {
|
||||
log!("An unknown error occurred.");
|
||||
}
|
||||
}
|
||||
});
|
||||
view! {
|
||||
<Row>
|
||||
<div class="flex flex-col flex-1" />
|
||||
<Panel>
|
||||
<Row>
|
||||
<h1 class="text-3xl font-bold">
|
||||
"Region 15 Scholarship Application (DEMO)"
|
||||
</h1>
|
||||
</Row>
|
||||
<Row>
|
||||
<p class="text-sm">
|
||||
"This is a very simple demonstration of the scholarship application written in Rust."
|
||||
</p>
|
||||
</Row>
|
||||
<Row>
|
||||
// Text { data_member, label, placeholder }
|
||||
<OutlinedTextField
|
||||
label="First Name:"
|
||||
placeholder="John"
|
||||
disabled=elements_disabled
|
||||
data_member="first_name"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Last Name:"
|
||||
placeholder="Smith"
|
||||
disabled=elements_disabled
|
||||
data_member="last_name"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Contact Email:"
|
||||
placeholder="student@region15.org"
|
||||
disabled=elements_disabled
|
||||
data_member="contact_email"
|
||||
data_map=expandable_react.data
|
||||
input_type=TextFieldType::Email
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Phone Number:"
|
||||
placeholder="123-456-7890"
|
||||
disabled=elements_disabled
|
||||
data_member="phone_number"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Street Address:"
|
||||
placeholder="123 Fake Street"
|
||||
disabled=elements_disabled
|
||||
data_member="address"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Highest Math SAT Score:"
|
||||
placeholder="600"
|
||||
disabled=elements_disabled
|
||||
data_member="math_sat"
|
||||
data_map=expandable_react.data
|
||||
input_type=TextFieldType::Number
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
// Radio { data_member, label, items }
|
||||
<RadioList
|
||||
label="Town:"
|
||||
items=vec!["Southbury", "Middlebury"]
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="town"
|
||||
data_map = expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
// Checkbox { data_member, label, items }
|
||||
<CheckboxList
|
||||
label="Favorite Candies:"
|
||||
items=vec!["Twizzlers", "Reese's", "Starburst"]
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="favorite_candies"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
// Select { data_member, label, items }
|
||||
<Select
|
||||
label="Gender:"
|
||||
value_list=vec!["Male", "Female", "Prefer not to answer"]
|
||||
.into_iter()
|
||||
.map(|s| s.to_owned())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="gender"
|
||||
data_map=expandable_react.data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<MultiEntry
|
||||
data_map=expandable_react.data
|
||||
data_member="community_involvement"
|
||||
schema=vec![
|
||||
input!(Text, "service_name", "Activity Name:", "Some service activity..."),
|
||||
input!(Number, "service_hours", "Total Service Hours", "20")
|
||||
]
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<ActionButton
|
||||
on:click=move |_| {
|
||||
let captured_map = expandable_react.capture();
|
||||
console_log(
|
||||
format!("Map values: {:?}", captured_map).as_str(),
|
||||
);
|
||||
submit_action
|
||||
.dispatch(CreateSampleSubmission {
|
||||
student_info: captured_map,
|
||||
//#endregion
|
||||
//#region Component State
|
||||
let elements_disabled = Signal::derive(move || {
|
||||
submit_action.pending().get()
|
||||
});
|
||||
}
|
||||
disabled=elements_disabled
|
||||
>
|
||||
"Submit"
|
||||
</ActionButton>
|
||||
</Row>
|
||||
<Row>
|
||||
<ActionButton on:click=move |_| {
|
||||
//#endregion
|
||||
//#region Event logic
|
||||
let on_submit = move |_| {
|
||||
let mut exp_info = ExpandableInfo::new(user_subject.get().unwrap_or_default());
|
||||
exp_info.data = form_data.get();
|
||||
submit_action.dispatch(CreateSampleSubmission {
|
||||
student_info: exp_info
|
||||
});
|
||||
};
|
||||
|
||||
let pdf_button_click = move |_| {
|
||||
console_log("Attempting to get PDF from server endpoint");
|
||||
spawn_local(async move {
|
||||
let result = create_example_pdf().await;
|
||||
@@ -345,18 +207,143 @@ pub fn HomePage() -> impl IntoView {
|
||||
link.click();
|
||||
}
|
||||
});
|
||||
}>"Get PDF"</ActionButton>
|
||||
};
|
||||
//#endregion
|
||||
|
||||
view! {
|
||||
<AuthLoaded fallback=Loading>
|
||||
<Authenticated unauthenticated=UnauthenticatedPage>
|
||||
// Replace this fallback with a real loading screen.
|
||||
<Suspense fallback=Loading>
|
||||
{move || {
|
||||
server_resource
|
||||
.get()
|
||||
.map(|submission| {
|
||||
view! {
|
||||
<Row>
|
||||
<div class="flex flex-col flex-1" />
|
||||
<Panel>
|
||||
<ValidatedForm on_submit=Callback::new(on_submit)>
|
||||
<Row>
|
||||
<h1 class="text-3xl font-bold">
|
||||
"Region 15 Scholarship Application (DEMO)"
|
||||
</h1>
|
||||
</Row>
|
||||
<Row>
|
||||
<p>{result_msg}</p>
|
||||
<p class="text-sm">
|
||||
"This is a very simple demonstration of the scholarship application written in Rust."
|
||||
</p>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="First Name:"
|
||||
placeholder="John"
|
||||
disabled=elements_disabled
|
||||
data_member="first_name"
|
||||
data_map=form_data
|
||||
/>
|
||||
<OutlinedTextField
|
||||
label="Last Name:"
|
||||
placeholder="Smith"
|
||||
disabled=elements_disabled
|
||||
data_member="last_name"
|
||||
data_map=form_data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Contact Email:"
|
||||
placeholder="student@region15.org"
|
||||
disabled=elements_disabled
|
||||
data_member="contact_email"
|
||||
data_map=form_data
|
||||
input_type=TextFieldType::Email
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Phone Number:"
|
||||
placeholder="123-456-7890"
|
||||
disabled=elements_disabled
|
||||
data_member="phone_number"
|
||||
data_map=form_data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Street Address:"
|
||||
placeholder="123 Fake Street"
|
||||
disabled=elements_disabled
|
||||
data_member="address"
|
||||
data_map=form_data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<OutlinedTextField
|
||||
label="Highest Math SAT Score:"
|
||||
placeholder="600"
|
||||
disabled=elements_disabled
|
||||
data_member="math_sat"
|
||||
data_map=form_data
|
||||
input_type=TextFieldType::Number
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<RadioList
|
||||
label="Town:"
|
||||
items=vec!["Southbury", "Middlebury"]
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="town"
|
||||
data_map = form_data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<CheckboxList
|
||||
label="Favorite Candies:"
|
||||
items=vec!["Twizzlers", "Reese's", "Starburst"]
|
||||
.into_iter()
|
||||
.map(|s| s.into())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="favorite_candies"
|
||||
data_map=form_data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<Select
|
||||
label="Gender:"
|
||||
value_list=vec!["Male", "Female", "Prefer not to answer"]
|
||||
.into_iter()
|
||||
.map(|s| s.to_owned())
|
||||
.collect()
|
||||
disabled=elements_disabled
|
||||
data_member="gender"
|
||||
data_map=form_data
|
||||
/>
|
||||
</Row>
|
||||
<Row>
|
||||
<MultiEntry
|
||||
data_map=form_data
|
||||
data_member="community_involvement"
|
||||
schema=vec![
|
||||
input!(Text, "service_name", "Activity Name:", "Some service activity..."),
|
||||
input!(Number, "service_hours", "Total Service Hours", "20")
|
||||
]
|
||||
/>
|
||||
</Row>
|
||||
</ValidatedForm>
|
||||
</Panel>
|
||||
<div class="flex flex-col flex-1" />
|
||||
</Row>
|
||||
}
|
||||
})
|
||||
}}
|
||||
</ValidatedForm>
|
||||
<Row>
|
||||
<ActionButton on:click=pdf_button_click>"Get PDF"</ActionButton>
|
||||
</Row>
|
||||
</Suspense>
|
||||
</Authenticated>
|
||||
</AuthLoaded>
|
||||
|
||||
Reference in new issue
Block a user