use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};
use quokka::state::FromState;
pub use quokka_admin_macros::{AdminCreateForm, AdminUpdateForm};
#[derive(Clone)]
pub struct FormBuilder<S> {
state: S,
}
pub trait AdminCreateForm<S> {
fn entity_name() -> &'static str;
fn get_form() -> Form<S>;
fn create_query(self, state: &S) -> impl Future<Output = quokka::Result<()>> + Send;
}
pub trait AdminUpdateForm<S>: Sized {
type PrimaryKeys;
fn entity_name() -> &'static str;
fn get_form() -> Form<S>;
fn update_query(self, state: &S) -> impl Future<Output = quokka::Result<()>> + Send;
fn get_query(
state: &S,
pks: Self::PrimaryKeys,
) -> impl Future<Output = quokka::Result<Self>> + Send;
}
#[derive(Clone)]
pub struct Form<S> {
pub entity_name: String,
pub action: String,
pub fields: Vec<Arc<dyn FormField<S> + Send + Sync>>,
}
pub struct FormFieldPreProcessorContext<'a> {
pub field: &'a mut FormFieldData,
}
pub trait FormFieldPreProcessor {
fn process_form_data(
&self,
context: FormFieldPreProcessorContext<'_>,
) -> Pin<Box<dyn Future<Output = quokka::Result<()>> + Send + Sync>>;
}
pub trait FormField<S> {
fn template(&self) -> String;
fn name(&self) -> String;
fn label(&self) -> String;
fn default(&self) -> Option<String> {
None
}
fn required(&self) -> bool {
false
}
fn additional_options(&self) -> HashMap<String, serde_json::Value> {
HashMap::new()
}
fn to_form_field_data(
&self,
state: &S,
) -> Pin<Box<dyn Future<Output = quokka::Result<FormFieldData>> + Send + Sync>> {
let data = FormFieldData {
template: self.template().to_string(),
name: self.name().to_string(),
label: self.label().to_string(),
default: self.default(),
required: self.required(),
additional_options: self.additional_options(),
};
let processor = self.processor(state);
Box::pin(async move {
let mut data = data.clone();
if let Some(processor) = processor {
let ctx = FormFieldPreProcessorContext { field: &mut data };
processor.process_form_data(ctx).await?;
}
Ok(data)
})
}
fn processor(&self, _state: &S) -> Option<Box<dyn FormFieldPreProcessor + Send + Sync>> {
None
}
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct FormFieldData {
pub template: String,
pub name: String,
pub label: String,
pub default: Option<String>,
pub required: bool,
pub additional_options: HashMap<String, serde_json::Value>,
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub struct FormProperties {
pub action: String,
pub entity: String,
pub fields: Vec<FormFieldData>,
}
pub mod fields {
use super::FormField;
use std::collections::HashMap;
macro_rules! input_field {
($(#[$attr:meta])* $field_name:ident, $template:expr) => {
$(#[$attr])*
#[derive(Clone, Debug, Default)]
pub struct $field_name {
pub name: String,
pub label: String,
pub template: String,
pub default: Option<String>,
pub required: bool,
pub additional_options: HashMap<String, serde_json::Value>,
}
impl<S> FormField<S> for $field_name {
fn name(&self) -> String {
self.name.clone()
}
fn label(&self) -> String {
self.label.clone()
}
fn template(&self) -> String {
self.template.clone()
}
fn default(&self) -> Option<String> {
self.default.clone()
}
fn required(&self) -> bool {
self.required
}
fn additional_options(&self) -> HashMap<String, serde_json::Value> {
self.additional_options.clone()
}
}
impl $field_name {
pub fn new(name: impl ToString, label: impl ToString) -> Self {
Self {
name: name.to_string(),
label: label.to_string(),
template: $template.to_string(),
..Default::default()
}
}
pub fn set_template(mut self, new: String) -> Self {
self.template = new;
self
}
pub fn set_label(mut self, new: String) -> Self {
self.label = new;
self
}
pub fn set_default(mut self, new: Option<String>) -> Self {
self.default = new;
self
}
pub fn set_required(mut self, new: bool) -> Self {
self.required = new;
self
}
}
};
}
input_field!(
TextField,
"partials/admin/field/text_field"
);
input_field!(
PasswordField,
"partials/admin/field/text_field/password"
);
input_field!(
NumberField,
"partials/admin/field/number_field"
);
input_field!(
CheckboxField,
"partials/admin/field/checkbox_field"
);
input_field!(
SelectField,
"partials/admin/field/select_field/combo_box"
);
input_field!(
HiddenField,
"partials/admin/field/hidden_field"
);
input_field!(
DisplayField,
"partials/admin/field/display_field"
);
input_field!(
HtmlInputField,
"partials/admin/field/html_input_field"
);
pub enum SelectFieldStyle {
Combobox,
Radio,
}
impl SelectField {
pub fn add_option(mut self, label: impl ToString, value: impl ToString) -> Self {
self.additional_options
.entry("options".to_string())
.and_modify(|entry| {
if let serde_json::Value::Array(values) = entry {
values.push(serde_json::json! {{
"label": label.to_string(),
"value": value.to_string(),
}});
}
})
.or_insert(serde_json::json!([{
"label": label.to_string(),
"value": value.to_string(),
}]));
self
}
pub fn style(mut self, style: SelectFieldStyle) -> Self {
match style {
SelectFieldStyle::Combobox => {
self.template = String::from("partials/admin/field/select_field/combo_box")
}
SelectFieldStyle::Radio => {
self.template = String::from("partials/admin/field/select_field/radio_buttons")
}
}
self
}
}
impl HtmlInputField {
pub fn set_type(mut self, typ: impl ToString) -> Self {
self.additional_options
.insert("type".to_string(), typ.to_string().into());
self
}
pub fn set_attribute(mut self, name: impl ToString, value: impl ToString) -> Self {
self.additional_options
.entry("attributes".to_string())
.and_modify(|entry| {
if let serde_json::Value::Object(object) = entry {
object.insert(name.to_string(), value.to_string().into());
}
})
.or_insert(serde_json::json! {{ name.to_string(): value.to_string() }});
self
}
}
}
impl<S: Clone + Send + Sync + 'static> FromState<S> for FormBuilder<S> {
fn from_state(state: &S) -> Self {
Self {
state: state.clone(),
}
}
}
impl<S: Send + Sync + 'static> FormBuilder<S> {
pub async fn construct_form_data(&self, form: Form<S>) -> quokka::Result<FormProperties> {
let fields = futures::future::try_join_all(
form.fields
.into_iter()
.map(|field| field.to_form_field_data(&self.state)),
)
.await?;
let form = FormProperties {
action: format!("/admin/entity/{}/{}", form.entity_name, form.action),
fields,
entity: form.entity_name,
};
Ok(form)
}
}
impl<S> Form<S> {
pub fn new(entity_name: impl ToString, action: impl ToString) -> Self {
Self {
entity_name: entity_name.to_string(),
action: action.to_string(),
fields: Default::default(),
}
}
pub fn add_field(mut self, field: impl FormField<S> + Send + Sync + 'static) -> Self {
self.fields.push(Arc::new(field));
self
}
}