Skip to main content

quokka_admin/service/
form_builder.rs

1use std::{collections::HashMap, future::Future, pin::Pin, sync::Arc};
2
3use quokka::state::FromState;
4pub use quokka_admin_macros::{AdminCreateForm, AdminUpdateForm};
5
6#[derive(Clone)]
7pub struct FormBuilder<S> {
8    state: S,
9}
10
11pub trait AdminCreateForm<S> {
12    fn entity_name() -> &'static str;
13    fn get_form() -> Form<S>;
14    fn create_query(self, state: &S) -> impl Future<Output = quokka::Result<()>> + Send;
15}
16
17pub trait AdminUpdateForm<S>: Sized {
18    type PrimaryKeys;
19
20    fn entity_name() -> &'static str;
21
22    fn get_form() -> Form<S>;
23
24    fn update_query(self, state: &S) -> impl Future<Output = quokka::Result<()>> + Send;
25
26    fn get_query(
27        state: &S,
28        pks: Self::PrimaryKeys,
29    ) -> impl Future<Output = quokka::Result<Self>> + Send;
30}
31
32#[derive(Clone)]
33pub struct Form<S> {
34    pub entity_name: String,
35    pub action: String,
36    pub fields: Vec<Arc<dyn FormField<S> + Send + Sync>>,
37}
38
39pub struct FormFieldPreProcessorContext<'a> {
40    pub field: &'a mut FormFieldData,
41}
42
43pub trait FormFieldPreProcessor {
44    fn process_form_data(
45        &self,
46        context: FormFieldPreProcessorContext<'_>,
47    ) -> Pin<Box<dyn Future<Output = quokka::Result<()>> + Send + Sync>>;
48}
49
50pub trait FormField<S> {
51    fn template(&self) -> String;
52
53    fn name(&self) -> String;
54
55    fn label(&self) -> String;
56
57    fn default(&self) -> Option<String> {
58        None
59    }
60
61    fn required(&self) -> bool {
62        false
63    }
64
65    fn additional_options(&self) -> HashMap<String, serde_json::Value> {
66        HashMap::new()
67    }
68
69    fn to_form_field_data(
70        &self,
71        state: &S,
72    ) -> Pin<Box<dyn Future<Output = quokka::Result<FormFieldData>> + Send + Sync>> {
73        let data = FormFieldData {
74            template: self.template().to_string(),
75            name: self.name().to_string(),
76            label: self.label().to_string(),
77            default: self.default(),
78            required: self.required(),
79            additional_options: self.additional_options(),
80        };
81
82        let processor = self.processor(state);
83
84        Box::pin(async move {
85            let mut data = data.clone();
86
87            if let Some(processor) = processor {
88                let ctx = FormFieldPreProcessorContext { field: &mut data };
89
90                processor.process_form_data(ctx).await?;
91            }
92
93            Ok(data)
94        })
95    }
96
97    fn processor(&self, _state: &S) -> Option<Box<dyn FormFieldPreProcessor + Send + Sync>> {
98        None
99    }
100}
101
102#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
103pub struct FormFieldData {
104    pub template: String,
105    pub name: String,
106    pub label: String,
107    pub default: Option<String>,
108    pub required: bool,
109    pub additional_options: HashMap<String, serde_json::Value>,
110}
111
112#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
113pub struct FormProperties {
114    pub action: String,
115    pub entity: String,
116    pub fields: Vec<FormFieldData>,
117}
118
119pub mod fields {
120    use super::FormField;
121    use std::collections::HashMap;
122
123    macro_rules! input_field {
124        ($(#[$attr:meta])* $field_name:ident, $template:expr) => {
125            $(#[$attr])*
126            #[derive(Clone, Debug, Default)]
127            pub struct $field_name {
128                pub name: String,
129                pub label: String,
130                pub template: String,
131                pub default: Option<String>,
132                pub required: bool,
133                pub additional_options: HashMap<String, serde_json::Value>,
134            }
135
136            impl<S> FormField<S> for $field_name {
137                fn name(&self) -> String {
138                    self.name.clone()
139                }
140
141                fn label(&self) -> String {
142                    self.label.clone()
143                }
144
145                fn template(&self) -> String {
146                    self.template.clone()
147                }
148
149                fn default(&self) -> Option<String> {
150                    self.default.clone()
151                }
152
153                fn required(&self) -> bool {
154                    self.required
155                }
156
157                fn additional_options(&self) -> HashMap<String, serde_json::Value> {
158                    self.additional_options.clone()
159                }
160            }
161
162            impl $field_name {
163                pub fn new(name: impl ToString, label: impl ToString) -> Self {
164                    Self {
165                        name: name.to_string(),
166                        label: label.to_string(),
167                        template: $template.to_string(),
168                        ..Default::default()
169                    }
170                }
171
172                pub fn set_template(mut self, new: String) -> Self {
173                    self.template = new;
174
175                    self
176                }
177
178                pub fn set_label(mut self, new: String) -> Self {
179                    self.label = new;
180
181                    self
182                }
183
184                pub fn set_default(mut self, new: Option<String>) -> Self {
185                    self.default = new;
186
187                    self
188                }
189
190                pub fn set_required(mut self, new: bool) -> Self {
191                    self.required = new;
192
193                    self
194                }
195            }
196        };
197    }
198
199    input_field!(
200        /// A `<input type=text>`
201        TextField,
202        "partials/admin/field/text_field"
203    );
204    input_field!(
205        /// A `<input type=password>`
206        PasswordField,
207        "partials/admin/field/text_field/password"
208    );
209    input_field!(
210        /// A `<input type=number>`
211        NumberField,
212        "partials/admin/field/number_field"
213    );
214    input_field!(
215        /// A `<input type=checkbox>`
216        CheckboxField,
217        "partials/admin/field/checkbox_field"
218    );
219    input_field!(
220        /// A field for a selection.
221        /// Using the [SelectFieldStyle] it can either be displayed as a combobox or multiple radio buttons
222        SelectField,
223        "partials/admin/field/select_field/combo_box"
224    );
225    input_field!(
226        /// A field that is not visible in the frontend (`type=hidden`). Can be used to keep things like the
227        /// "id" of an entity or other meta data around
228        HiddenField,
229        "partials/admin/field/hidden_field"
230    );
231    input_field!(
232        /// A field that is `<input type=text disabled>`. Used to display some static information that is not
233        /// supposed to be set or edited by the user (like an entity's id)
234        DisplayField,
235        "partials/admin/field/display_field"
236    );
237    input_field!(
238        /// A field that is configurable in the `<input type=$TYPE>`. It comes with a label and except for this
239        /// is just a plain old HTML input element. This can be used for things that do not (yet) have a respective
240        /// `*Field` type.
241        HtmlInputField,
242        "partials/admin/field/html_input_field"
243    );
244
245    /// Set the style of a [SelectField]
246    ///
247    /// - The [Combobox] is the usual drop down view of a <select> field in HTML
248    /// - The [Radio] is shown as a list of radio buttons
249    pub enum SelectFieldStyle {
250        Combobox,
251        Radio,
252    }
253
254    impl SelectField {
255        pub fn add_option(mut self, label: impl ToString, value: impl ToString) -> Self {
256            self.additional_options
257                .entry("options".to_string())
258                .and_modify(|entry| {
259                    if let serde_json::Value::Array(values) = entry {
260                        values.push(serde_json::json! {{
261                            "label": label.to_string(),
262                            "value": value.to_string(),
263                        }});
264                    }
265                })
266                .or_insert(serde_json::json!([{
267                    "label": label.to_string(),
268                    "value": value.to_string(),
269                }]));
270
271            self
272        }
273
274        pub fn style(mut self, style: SelectFieldStyle) -> Self {
275            match style {
276                SelectFieldStyle::Combobox => {
277                    self.template = String::from("partials/admin/field/select_field/combo_box")
278                }
279                SelectFieldStyle::Radio => {
280                    self.template = String::from("partials/admin/field/select_field/radio_buttons")
281                }
282            }
283
284            self
285        }
286    }
287
288    impl HtmlInputField {
289        pub fn set_type(mut self, typ: impl ToString) -> Self {
290            self.additional_options
291                .insert("type".to_string(), typ.to_string().into());
292
293            self
294        }
295
296        pub fn set_attribute(mut self, name: impl ToString, value: impl ToString) -> Self {
297            self.additional_options
298                .entry("attributes".to_string())
299                .and_modify(|entry| {
300                    if let serde_json::Value::Object(object) = entry {
301                        object.insert(name.to_string(), value.to_string().into());
302                    }
303                })
304                .or_insert(serde_json::json! {{ name.to_string(): value.to_string() }});
305
306            self
307        }
308    }
309}
310
311impl<S: Clone + Send + Sync + 'static> FromState<S> for FormBuilder<S> {
312    fn from_state(state: &S) -> Self {
313        Self {
314            state: state.clone(),
315        }
316    }
317}
318
319impl<S: Send + Sync + 'static> FormBuilder<S> {
320    pub async fn construct_form_data(&self, form: Form<S>) -> quokka::Result<FormProperties> {
321        let fields = futures::future::try_join_all(
322            form.fields
323                .into_iter()
324                .map(|field| field.to_form_field_data(&self.state)),
325        )
326        .await?;
327
328        let form = FormProperties {
329            action: format!("/admin/entity/{}/{}", form.entity_name, form.action),
330            fields,
331            entity: form.entity_name,
332        };
333
334        Ok(form)
335    }
336}
337
338impl<S> Form<S> {
339    pub fn new(entity_name: impl ToString, action: impl ToString) -> Self {
340        Self {
341            entity_name: entity_name.to_string(),
342            action: action.to_string(),
343            fields: Default::default(),
344        }
345    }
346
347    pub fn add_field(mut self, field: impl FormField<S> + Send + Sync + 'static) -> Self {
348        self.fields.push(Arc::new(field));
349
350        self
351    }
352}