generated from mitchell/rust_template
doc
This commit is contained in:
parent
3c96a15680
commit
f2397a47c5
156
README.md
156
README.md
@ -2,63 +2,109 @@
|
|||||||
|
|
||||||
A declaritive way to create forms for [leptos](https://leptos.dev/).
|
A declaritive way to create forms for [leptos](https://leptos.dev/).
|
||||||
|
|
||||||
leptos_form_tool allows you to define forms in a declaritive way, without specifying how to render each component. You define what controls and visual components the form should have, as well as how to parse and validate the data. That form definition can then be used to render a `View` for the form, or create a Validator so the client and server can both check the integrity of the data in the same way, without duplicating code.
|
leptos_form_tool allows you to define forms in a declaritive way.
|
||||||
|
Want text box? Just call `.text_input` on the form builder. Then sepperatly,
|
||||||
|
you define a FormStyle to specify how that text box should be rendered.
|
||||||
|
This has a number of advantages:
|
||||||
|
- Sepperates the layout of the form from how it is rendered and styled
|
||||||
|
- Allows different styles to be swapped in and out quickly
|
||||||
|
|
||||||
The rendering of the form controls are defined seperatly from the form itself. This sepperation allows different styles to be swapped in and out, relatively easily and helps reduce code duplication. A particular style can be created by creating a type that implements the FormStyle trait. This FormStyle trait specifies functions for defining all the common form controls such as TextInput, Select, RadioButtons, etc. leptos_form_tool also support custom components if you want more controls than the just the common ones. Custom controls natrually cannot be fully styled with by the FormStyle.
|
## Validations
|
||||||
|
|
||||||
An implmentation of FormStyle might have some variables that should be defined on a per-component basis. Take, for example, a FormStyle that renders it's controls in a grid (like TWGridFormStyle). We would like to be able to define a width for how many columns in the grid that a control will take up. For this, the FormStyle also defines a type for it's attributes:
|
You might find yourself asking, but why not just use components?
|
||||||
```rust
|
|
||||||
pub enum TWGridFormAttributes {
|
|
||||||
Width(u32),
|
|
||||||
}
|
|
||||||
```
|
|
||||||
With this, you add these styling attributes to components with the `style()` method.
|
|
||||||
|
|
||||||
Here is an example of how a form can be defined:
|
The biggest reason for creating leptos_form_tool is support for
|
||||||
```rust
|
validating the fields. This validation logic can get rather complex, for
|
||||||
// all FormData must implement Default and must be 'static
|
instance, you likely want to preform validation on the client when the user
|
||||||
#[derive(Default)]
|
clicks submit to immediatly give the user feedback about any invalid input.
|
||||||
// this struct should contain all the data that needs to be submitted.
|
But you often also want to do the same validation on the server to protect
|
||||||
struct MyFormData {
|
against any requests that don't come from your client or for a user that
|
||||||
name: String,
|
doesn't have wasm enabled.
|
||||||
age: u32,
|
|
||||||
}
|
|
||||||
|
|
||||||
// now implment the FormData trait
|
Additionally, you might want to change the validation of one control based
|
||||||
impl FormData for MyFormData {
|
on the value of another. For example, you might want to make sure the "day"
|
||||||
// the form style must be decided now, as the available attributes depend on the FormStyle
|
field is in a valid range, but that range depends on what the user selects in
|
||||||
type Style = TWGridFormStyle;
|
the "month" field. Or you might want to make sure the "confirm password" field
|
||||||
fn build_form(FormBuilder fb) -> Form {
|
matches the "password" field. leptos_form_tool makes this easy, as the
|
||||||
fb
|
validation function you provide operates on the entire form's data.
|
||||||
.heading(|h| h.title("Tell Me About Yourself"))
|
|
||||||
.text_input(|t| {
|
|
||||||
t.label("Name")
|
|
||||||
.placeholder("Name")
|
|
||||||
.parse_fn(|name, form_data| Ok(form_data.name = name))
|
|
||||||
.validation_fn(|fd| {
|
|
||||||
if fd.name.is_empty() {
|
|
||||||
Err("Name is required")
|
|
||||||
} else if fd.name.len() >= 32 {
|
|
||||||
Err("Name must be shorter than 32 characters")
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.text_input(|t| {
|
|
||||||
t.label("age")
|
|
||||||
.placeholder("age")
|
|
||||||
.parse_fn(|age, form_data| { Ok(form_data.age = age.parse().map_err(|e| e.to_string())?)}
|
|
||||||
.validation_fn(|form_data| {
|
|
||||||
if form_data.age > 99 {
|
|
||||||
Err("Please enter an age <= 99")
|
|
||||||
} else {
|
|
||||||
Ok(())
|
|
||||||
}
|
|
||||||
})
|
|
||||||
})
|
|
||||||
.submit(|s| s.text("Submit"))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
```
|
Sometimes you might not want to show some controls, and validation for those
|
||||||
|
controls should only be done when they are visible.
|
||||||
|
|
||||||
|
lepos_form_tool takes care of all this for you.
|
||||||
|
|
||||||
|
## FormStyle
|
||||||
|
|
||||||
|
To define how to render all the components that a form might use, you define
|
||||||
|
a type that implements the `FormStyle` trait. This trait has a method for each
|
||||||
|
control that the form might need to render. Once you implement that trait you
|
||||||
|
just need to change the `Style` associated trait of your form to use that new
|
||||||
|
style.
|
||||||
|
|
||||||
|
Its actually a little more complicated than that...
|
||||||
|
|
||||||
|
To give custom styles a little more freedom to configure how to render their
|
||||||
|
controls on a per-control-basis, the style will define an associated type
|
||||||
|
(usually an enum) called `StylingAttributes`. A styling attribute can be added
|
||||||
|
to a control by calling `.style(/* style */)` on the control builder. These
|
||||||
|
styling attributes are accessable to the `FormStyle` implementation when
|
||||||
|
rendering that control.
|
||||||
|
|
||||||
|
Therefore, swaping out styles also requires swapping out all the `.style()`
|
||||||
|
calls.
|
||||||
|
|
||||||
|
## Builders
|
||||||
|
|
||||||
|
leptos_form_tool makes heavy use of the builder pattern. You will build the
|
||||||
|
form, controls and sometimes even validation functions by calling methods on
|
||||||
|
a builder that will construct the object based on the values you give it.
|
||||||
|
|
||||||
|
## Context
|
||||||
|
|
||||||
|
Sometimes, you might want to be able to use something from the form's context
|
||||||
|
to render the form. For example, you may want to use a user's token as context
|
||||||
|
and only render part of the form if they are an administrator. Or, you may
|
||||||
|
need to get the options for a certain drop-down dynamically. The form's context
|
||||||
|
is the solution to these problems.
|
||||||
|
|
||||||
|
On the form, you define the associated type `Context`. Then, when you construct
|
||||||
|
the `Form` object, you must provide the context. The context can be used in
|
||||||
|
the building of the form, and can change what is rendered. Each control
|
||||||
|
builder function has a context varient that allows you to use the context in
|
||||||
|
the building of the form.
|
||||||
|
|
||||||
|
To avoid a whole lot of headache, the context is immutable once passed into to
|
||||||
|
the form. However, you can have leptos signals in the context, as they dont
|
||||||
|
require mutable access to call get/set on the signal.
|
||||||
|
|
||||||
|
Since the context can change how the form is rendered, and what controls are
|
||||||
|
shown/hidden (thus changing what controls are validated), the context is
|
||||||
|
needed to validate the form data on the server side. If are sure that the
|
||||||
|
context doesn't change any of the validations, you don't have to make sure
|
||||||
|
the context is the same on client and server. If the context does change
|
||||||
|
how the form is validated, make sure to keep the context the same to maximize
|
||||||
|
the validation that can happen on the clients side, before the user even
|
||||||
|
submits the form.
|
||||||
|
|
||||||
|
## Custom Components
|
||||||
|
|
||||||
|
leptos_form_tool also supports custom components that can be defined in the
|
||||||
|
user space. Though less ergonomic, this keeps leptos_form_tool from putting
|
||||||
|
limits on what you can do. There are `custom_*` methods on the form builder
|
||||||
|
that allow you to add your component. Unfortunatly you cannot define methods
|
||||||
|
on the `ControlBuilder` to help build your controls data, so you must
|
||||||
|
construct the ControlData for your custom type before adding it to the form.
|
||||||
|
|
||||||
|
## Getting Started
|
||||||
|
|
||||||
|
To learn by example, see the example project.
|
||||||
|
|
||||||
|
To follow a Getting Started guide, see [`getting_started.md`].
|
||||||
|
|
||||||
|
## Contributing
|
||||||
|
|
||||||
|
To contribute, fork the repo and make a PR.
|
||||||
|
If you find a bug, feel free to open an issue.
|
||||||
|
|
||||||
|
By contributing, you agree that your changes are
|
||||||
|
subject to the license found in [`/LICENSE`].
|
||||||
|
|||||||
240
getting_started.md
Normal file
240
getting_started.md
Normal file
@ -0,0 +1,240 @@
|
|||||||
|
# Getting Started
|
||||||
|
|
||||||
|
This guide will walk you through creating your first leptos_form_tool form.
|
||||||
|
|
||||||
|
## Form Data
|
||||||
|
|
||||||
|
Start by creating the form data.
|
||||||
|
|
||||||
|
This struct should contain all the data for the entire form.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
// all FormToolData implementors must also implement Clone and be 'static
|
||||||
|
// Default and Debug are not required, but helpful
|
||||||
|
#[derive(Clone, Default, Debug)]
|
||||||
|
struct HelloWorldFormData {
|
||||||
|
first: String,
|
||||||
|
last: String,
|
||||||
|
age: u32,
|
||||||
|
sport: String,
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
## Defining the Form Layout
|
||||||
|
|
||||||
|
Then, to define how the for should be rendered, implement the `FormToolData`
|
||||||
|
trait. You will need to define the style that this form will use and what
|
||||||
|
context it will have.
|
||||||
|
|
||||||
|
It is required to define the style here, as the style needs to be known to
|
||||||
|
add the StylingAttributes to the controls.
|
||||||
|
|
||||||
|
The `build_form` method provides you with a `FormBuilder<Self>`. You can define
|
||||||
|
controls on the form by using the builder methods. Some controls don't accept
|
||||||
|
input from the user, they just display information. These are refered to as
|
||||||
|
`VanityControls` by leptos_form_tool. An example of a vanity control would
|
||||||
|
be a heading. Other controls do accept user input and are just refered to as
|
||||||
|
`Controls` by leptos_form_tool.
|
||||||
|
|
||||||
|
### Defining Controls
|
||||||
|
|
||||||
|
There are other builder methods defined the the ControlBuilder for certain
|
||||||
|
controls. For example, the TextInput builder has a `placeholder` method
|
||||||
|
that sets the placeholder of the text input.
|
||||||
|
|
||||||
|
When building a control, you will need to provide a getter and setter
|
||||||
|
to get the field and set the field on the form data. The getter is a function
|
||||||
|
that takes the full form data, and returns the field. The setter is a function
|
||||||
|
that takes the full form data and a value, and sets the field to that value.
|
||||||
|
Examples for these getters and setters are shown below.
|
||||||
|
|
||||||
|
A VanityControl will never need a setter, as they only display information.
|
||||||
|
Sometimes they need getters if the information they display is based on the
|
||||||
|
form data. For example, the output control can display information from the
|
||||||
|
form data, so it needs a getter.
|
||||||
|
|
||||||
|
#### Parse and Unparse Functions
|
||||||
|
|
||||||
|
Controls also need a set of parse and unparse functions. The type that the
|
||||||
|
control returns could be anything. For example, a range slider might return a
|
||||||
|
`i32`, a text input might return a `String`. leptos_form_tool needs a way to
|
||||||
|
convert the type of the field, to the type of the control and vice versa.
|
||||||
|
This is what the un/parse functions are for.
|
||||||
|
|
||||||
|
The parse and unparse functions can always be specified with the `parse_custom`
|
||||||
|
method, but there are several builder methods that create the parse functions
|
||||||
|
automatically. For example, if the field type and control type can be
|
||||||
|
converted to and from each other with the `From` trait (this is true if the
|
||||||
|
two types are the same) then you can call the `parse_from` method to
|
||||||
|
automatically create the un/parse functions using that conversion.
|
||||||
|
If the control's type is String, and the field type implements `FromStr` and
|
||||||
|
`ToString`, you can call `parse_string` to generate un/parse functions using
|
||||||
|
that trait. `parse_trimmed` does the same, but trims the string before parsing.
|
||||||
|
This should cover most use cases, but you always have the option to define
|
||||||
|
your own.
|
||||||
|
|
||||||
|
It is important to note that parsing from the control's type to the field type
|
||||||
|
IS allowed to fail. If it fails, it will be displayed just like any other
|
||||||
|
validation error. Conversion from the field type to the control type is NOT
|
||||||
|
allowed to fail; the FormData should always be able to be displayed.
|
||||||
|
|
||||||
|
#### Validation Functions
|
||||||
|
|
||||||
|
Validation functions can be defined on a field to add some extra criteria
|
||||||
|
for what counts as a valid entry. The validation function takes the entire
|
||||||
|
forms data. This allows you to use the entire state of the form to decide if
|
||||||
|
this field is valid or not. If any validation function fails, the form will
|
||||||
|
not be allowed to submit. In addition, when you build a validator, it will
|
||||||
|
collect all of the validation functions that you define on these fields.
|
||||||
|
|
||||||
|
leptos_form_tool provides a `ValidationBuilder` to help you build validation
|
||||||
|
functions, which, in some cases, might be easier than defining a closure
|
||||||
|
yourself.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
impl FormToolData for HelloWorldFormData {
|
||||||
|
// The form style to use.
|
||||||
|
type Style = GridFormStyle;
|
||||||
|
// The external context needed for rendering the form.
|
||||||
|
// In this case, nothing.
|
||||||
|
type Context = ();
|
||||||
|
|
||||||
|
fn build_form(fb: FormBuilder<Self>) -> FormBuilder<Self> {
|
||||||
|
fb.heading(|h| h.title("Welcome"))
|
||||||
|
.text_input(|t| {
|
||||||
|
t.named("data[first]")
|
||||||
|
.labeled("First Name")
|
||||||
|
.getter(|fd| fd.first)
|
||||||
|
.setter(|fd, value| fd.first = value)
|
||||||
|
// trim the string before writing to the field
|
||||||
|
.parse_trimmed()
|
||||||
|
.validation_fn(
|
||||||
|
// Using the ValidationBuilder to set a required field
|
||||||
|
ValidationBuilder::for_field(|fd: &HelloWorldFormData| fd.first.as_str())
|
||||||
|
.named("First Name")
|
||||||
|
.required()
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
// width out of 12
|
||||||
|
.style(Width(4))
|
||||||
|
// defines text that shows up when hovering over it
|
||||||
|
.style(Tooltip("Your given first name".to_string()))
|
||||||
|
})
|
||||||
|
.text_input(|t| {
|
||||||
|
t.named("data[last]")
|
||||||
|
.labeled("Last Name")
|
||||||
|
.getter(|fd| fd.last)
|
||||||
|
.setter(|fd, value| fd.last = value)
|
||||||
|
// dont trim the string, just write it to the field
|
||||||
|
.parse_from()
|
||||||
|
.validation_fn(
|
||||||
|
// using the ValidationBuilder to set a required field
|
||||||
|
ValidationBuilder::for_field(|fd: &HelloWorldFormData| fd.last.as_str())
|
||||||
|
.named("Last Name")
|
||||||
|
.required()
|
||||||
|
.build(),
|
||||||
|
)
|
||||||
|
.style(Width(8))
|
||||||
|
.style(Tooltip("Your last name".to_string()))
|
||||||
|
})
|
||||||
|
// using the _cx varient allows access to the context
|
||||||
|
// in this case, its `()` which doesnt help us that much.
|
||||||
|
.stepper_cx(|s, _cx| {
|
||||||
|
s.named("data[age]")
|
||||||
|
.labeled("Age")
|
||||||
|
.getter(|fd| fd.age)
|
||||||
|
.setter(|fd, value| fd.age = value)
|
||||||
|
// trim the string then try to parse to a `u32`
|
||||||
|
.parse_trimmed()
|
||||||
|
.validation_fn(move |fd| {
|
||||||
|
// defining a validation function with a closure
|
||||||
|
(fd.age > 13)
|
||||||
|
.then_some(())
|
||||||
|
.ok_or_else(|| String::from("Too Young"))
|
||||||
|
})
|
||||||
|
.style(Width(6))
|
||||||
|
.style(Tooltip("Your age in years".to_string()))
|
||||||
|
})
|
||||||
|
.select(|s| {
|
||||||
|
s.named("data[sport]")
|
||||||
|
.labeled("Favorite Sport")
|
||||||
|
.getter(|fd| fd.sport)
|
||||||
|
.setter(|fd, value| fd.sport = value)
|
||||||
|
.parse_from()
|
||||||
|
// set the options for the select along with their values
|
||||||
|
.with_options_valued(vec![
|
||||||
|
("Football", "football"),
|
||||||
|
("Soccer", "soccer"),
|
||||||
|
("Ice Hockey", "ice_hockey"),
|
||||||
|
("Golf", "golf"),
|
||||||
|
].into_iter())
|
||||||
|
.style(Width(6))
|
||||||
|
})
|
||||||
|
.submit(|s| s.text("Submit"))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Now, using the form is quite simple. You just need to provide the form data,
|
||||||
|
style, context, and where the form should point to.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
let form = ExampleFormData::default().get_plain_form("/api/endpoint", GridFormStyle::default(), ());
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<h1> "This is My Form!" </h1>
|
||||||
|
{form}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
You can also have the form build to an `ActionForm`, this will be very
|
||||||
|
familiar if you've used an `ActionForm` before.
|
||||||
|
|
||||||
|
You might have noticed the goofy names that we put in our form above, like
|
||||||
|
"data[first]" instead of just "first". This is done to allow the form to use
|
||||||
|
the SubmitForm as an action. See
|
||||||
|
[ActionForms](https://book.leptos.dev/progressive_enhancement/action_form.html#complex-inputs)
|
||||||
|
in the leptos book for more.
|
||||||
|
|
||||||
|
```rust
|
||||||
|
#[component]
|
||||||
|
fn FormWrapper() -> impl IntoView {
|
||||||
|
let server_fn_action = create_server_action::<SubmitForm>();
|
||||||
|
|
||||||
|
let form = HelloWorldFormData::default().get_action_form(server_fn_action, GridFormStyle::default(), ());
|
||||||
|
let response = server_fn_action.value();
|
||||||
|
|
||||||
|
view! {
|
||||||
|
<div>
|
||||||
|
<h1> "This is My Form!" </h1>
|
||||||
|
// display the form
|
||||||
|
{form}
|
||||||
|
// display the result from the server
|
||||||
|
{move || response.get().map(|result| result.ok())}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
#[server(SubmitForm)]
|
||||||
|
async fn submit_form(data: HelloWorldFormData) -> Result<String, ServerFnError> {
|
||||||
|
data.validate(()).map_err(ServerFnError::new)?;
|
||||||
|
|
||||||
|
Ok(format!(
|
||||||
|
"Hello {} {} ({}), You must like {}!",
|
||||||
|
data.first, data.last, data.age, data.sport
|
||||||
|
))
|
||||||
|
}
|
||||||
|
```
|
||||||
|
|
||||||
|
Lastly there is the `get_form` method. This almost identical to the ActionForm
|
||||||
|
version. In fact, if you do everything right, you wont even notice a
|
||||||
|
difference. Under the hood, this sends your FormToolData struct directly
|
||||||
|
by calling the server function, whereas the `ActionForm` version will try
|
||||||
|
to construct your type using the
|
||||||
|
[`FromFormData`](https://docs.rs/leptos_router/latest/leptos_router/trait.FromFormData.html)
|
||||||
|
trait. Using the `get_form` method instead will allow you to name the inputs
|
||||||
|
whatever you want (though you should try to name them correctly anyway to
|
||||||
|
support progressive enhancement) and it will still work. The example is the
|
||||||
|
same as above, just replace `get_action_form` with `get_form`.
|
||||||
@ -153,13 +153,12 @@ impl FormStyle for GridFormStyle {
|
|||||||
id=&control.data.name
|
id=&control.data.name
|
||||||
name=&control.data.name
|
name=&control.data.name
|
||||||
placeholder=control.data.placeholder.as_ref()
|
placeholder=control.data.placeholder.as_ref()
|
||||||
|
class="form_input"
|
||||||
|
class=("form_input_invalid", move || validation_state.get().is_err())
|
||||||
prop:value=move || value_getter.get()
|
prop:value=move || value_getter.get()
|
||||||
on:focusout=move |ev| {
|
on:focusout=move |ev| {
|
||||||
value_setter(event_target_value(&ev));
|
value_setter(event_target_value(&ev));
|
||||||
}
|
}
|
||||||
|
|
||||||
class="form_input"
|
|
||||||
class=("form_input_invalid", move || validation_state.get().is_err())
|
|
||||||
/>
|
/>
|
||||||
}
|
}
|
||||||
.into_view();
|
.into_view();
|
||||||
@ -186,12 +185,11 @@ impl FormStyle for GridFormStyle {
|
|||||||
name=&control.data.name
|
name=&control.data.name
|
||||||
placeholder=control.data.placeholder.as_ref()
|
placeholder=control.data.placeholder.as_ref()
|
||||||
prop:value=move || value_getter.get()
|
prop:value=move || value_getter.get()
|
||||||
|
class="form_input"
|
||||||
|
class=("form_input_invalid", move || validation_state.get().is_err())
|
||||||
on:focusout=move |ev| {
|
on:focusout=move |ev| {
|
||||||
value_setter(event_target_value(&ev));
|
value_setter(event_target_value(&ev));
|
||||||
}
|
}
|
||||||
|
|
||||||
class="form_input"
|
|
||||||
class=("form_input_invalid", move || validation_state.get().is_err())
|
|
||||||
></textarea>
|
></textarea>
|
||||||
}
|
}
|
||||||
.into_view();
|
.into_view();
|
||||||
@ -301,6 +299,7 @@ impl FormStyle for GridFormStyle {
|
|||||||
id=&control.data.name
|
id=&control.data.name
|
||||||
name=&control.data.name
|
name=&control.data.name
|
||||||
class="form_input"
|
class="form_input"
|
||||||
|
class=("form_input_invalid", move || validation_state.get().is_err())
|
||||||
on:input=move |ev| {
|
on:input=move |ev| {
|
||||||
value_setter(event_target_value(&ev));
|
value_setter(event_target_value(&ev));
|
||||||
}
|
}
|
||||||
@ -366,6 +365,7 @@ impl FormStyle for GridFormStyle {
|
|||||||
min=control.data.min
|
min=control.data.min
|
||||||
max=control.data.max
|
max=control.data.max
|
||||||
class="form_input"
|
class="form_input"
|
||||||
|
class=("form_input_invalid", move || validation_state.get().is_err())
|
||||||
prop:value=move || value_getter.get()
|
prop:value=move || value_getter.get()
|
||||||
on:change=move |ev| {
|
on:change=move |ev| {
|
||||||
value_setter(event_target_value(&ev));
|
value_setter(event_target_value(&ev));
|
||||||
@ -398,6 +398,7 @@ impl FormStyle for GridFormStyle {
|
|||||||
min=control.data.min
|
min=control.data.min
|
||||||
max=control.data.max
|
max=control.data.max
|
||||||
class="form_input"
|
class="form_input"
|
||||||
|
class=("form_input_invalid", move || validation_state.get().is_err())
|
||||||
prop:value=move || value_getter.get()
|
prop:value=move || value_getter.get()
|
||||||
on:input=move |ev| {
|
on:input=move |ev| {
|
||||||
let value = event_target_value(&ev).parse::<i32>().ok();
|
let value = event_target_value(&ev).parse::<i32>().ok();
|
||||||
|
|||||||
Reference in New Issue
Block a user