Skip to main content

hyle_dioxus/
filter.rs

1use dioxus::prelude::*;
2
3use hyle::{FieldType, Primitive};
4
5use crate::context::{use_hyle_components, HyleConfig};
6use crate::types::{field_type_key, HyleComponents, HyleFilterField, HyleFilterFieldProps, HyleFiltersState, HyleValueProps};
7
8/// A reactive filter input derived from `HyleFiltersState`.
9///
10/// Looks up the field definition in `state.fields`, reads the current value
11/// from `state.form_data`, and calls `state.set_field` on change.
12///
13/// Rendering priority:
14/// 1. Per-field `render` function stored in the `HyleFilterField` (from `use_filters` change map)
15/// 2. Matching entry in the `HyleComponents` context (registered via `use_hyle_components`)
16/// 3. Built-in default based on field type:
17///    - `Boolean` → `<select>` with Any / Yes / No
18///    - `Reference` → `<select>` populated from pre-resolved options
19///    - `Array<Reference>` → `<fieldset>` with one checkbox per option
20///    - Everything else → `<input type="text">` (or the `input.kind` hint from the blueprint)
21///
22/// # Props
23/// - `state` — the `HyleFiltersState` from `use_filters`
24/// - `field_key` — the field name to render
25#[component]
26pub fn FilterField(
27    state: HyleFiltersState,
28    field_key: String,
29) -> Element {
30    let filter_field: Option<HyleFilterField> = state
31        .fields
32        .read()
33        .iter()
34        .find(|f| f.key == field_key)
35        .cloned();
36
37    let Some(ff) = filter_field else {
38        return rsx! {};
39    };
40
41    let key = ff.key.clone();
42    let value = state
43        .form_data
44        .read()
45        .get(&key)
46        .cloned()
47        .unwrap_or_default();
48
49    let set: Callback<String> = {
50        let key = key.clone();
51        Callback::new(move |v: String| state.set_field.call((key.clone(), v)))
52    };
53
54    // Priority 1: per-field render from the `change` map (highest).
55    if let Some(ref render_fn) = ff.render {
56        return render_fn(HyleFilterFieldProps {
57            key: ff.key,
58            label: ff.label,
59            field: ff.field,
60            options: ff.options,
61            value,
62            set,
63        });
64    }
65
66    // Priority 2: global HyleComponents context.
67    let props = HyleFilterFieldProps {
68        key: ff.key.clone(),
69        label: ff.label.clone(),
70        field: ff.field.clone(),
71        options: ff.options.clone(),
72        value: value.clone(),
73        set: set.clone(),
74    };
75    if let Some(components) = use_hyle_components() {
76        let type_key = field_type_key(&ff.field.field_type);
77        if let Some(render_fn) = components.filters.get(type_key).copied() {
78            return render_fn(props);
79        }
80    }
81
82    // Priority 3: built-in default.
83    default_input(ff, value, set)
84}
85
86/// Like [`FilterField`] but intended for form contexts: boolean fields render
87/// as a self-labelling checkbox (label on the right) instead of a 3-state
88/// select, matching the React `FilterBoolean` checkbox appearance.
89#[component]
90pub fn FormFilterField(
91    state: HyleFiltersState,
92    field_key: String,
93) -> Element {
94    let filter_field: Option<HyleFilterField> = state
95        .fields
96        .read()
97        .iter()
98        .find(|f| f.key == field_key)
99        .cloned();
100
101    let Some(ff) = filter_field else {
102        return rsx! {};
103    };
104
105    let key = ff.key.clone();
106    let value = state
107        .form_data
108        .read()
109        .get(&key)
110        .cloned()
111        .unwrap_or_default();
112
113    let set: Callback<String> = {
114        let key = key.clone();
115        Callback::new(move |v: String| state.set_field.call((key.clone(), v)))
116    };
117
118    if let Some(ref render_fn) = ff.render {
119        return render_fn(HyleFilterFieldProps {
120            key: ff.key, label: ff.label, field: ff.field, options: ff.options, value, set,
121        });
122    }
123
124    form_default_input(ff, value, set)
125}
126
127fn form_default_input(ff: HyleFilterField, value: String, set: Callback<String>) -> Element {
128    match &ff.field.field_type {
129        FieldType::Primitive {
130            primitive: Primitive::Boolean,
131        } => boolean_checkbox(ff.key, ff.label, value, set),
132        _ => default_input(ff, value, set),
133    }
134}
135
136// ── Default input dispatch ────────────────────────────────────────────────────
137
138fn default_input(ff: HyleFilterField, value: String, set: Callback<String>) -> Element {
139    match &ff.field.field_type {
140        FieldType::Primitive {
141            primitive: Primitive::Boolean,
142        } => boolean_select(ff.key, ff.label, value, set),
143
144        FieldType::Reference { .. } => match ff.options {
145            Some(options) => reference_select(ff.key, ff.label, value, options, set),
146            None => reference_select_loading(ff.key, ff.label),
147        },
148
149        FieldType::Array { .. } => {
150            match ff.options {
151                Some(options) => checkbox_reference_fieldset(ff.key, ff.label, value, options, ff.display_field_type.clone(), set),
152                None => rsx! {
153                    fieldset {
154                        legend { "{ff.label}" }
155                        span { aria_busy: "true", "Loading…" }
156                    }
157                },
158            }
159        }
160
161        _ => text_input(ff, value, set),
162    }
163}
164
165fn boolean_select(name: String, label: String, value: String, set: Callback<String>) -> Element {
166    rsx! {
167        label {
168            "{label}"
169            select {
170                name: "{name}",
171                onchange: move |e| set.call(e.value()),
172                option { value: "", selected: value.is_empty(), "Any" }
173                option { value: "true", selected: value == "true", "Yes" }
174                option { value: "false", selected: value == "false", "No" }
175            }
176        }
177    }
178}
179
180pub(crate) fn boolean_checkbox(name: String, label: String, value: String, set: Callback<String>) -> Element {
181    rsx! {
182        fieldset {
183            legend { "" }
184            label {
185                input {
186                    r#type: "checkbox",
187                    name: "{name}",
188                    checked: value == "true",
189                    onchange: move |e| set.call(if e.checked() { "true".to_owned() } else { String::new() }),
190                }
191                "{label}"
192            }
193        }
194    }
195}
196
197fn reference_select_loading(name: String, label: String) -> Element {
198    rsx! {
199        label {
200            "{label}"
201            select { name: "{name}", disabled: true,
202                option { "Loading…" }
203            }
204        }
205    }
206}
207
208fn reference_select(
209    name: String,
210    label: String,
211    value: String,
212    options: Vec<(String, String)>,
213    set: Callback<String>,
214) -> Element {
215    rsx! {
216        label {
217            "{label}"
218            select {
219                name: "{name}",
220                onchange: move |e| set.call(e.value()),
221                option { value: "", selected: value.is_empty(), "All {label}s" }
222                for (id, display) in options {
223                    option { key: "{id}", value: "{id}", selected: value == id, "{display}" }
224                }
225            }
226        }
227    }
228}
229
230fn checkbox_reference_fieldset(
231    name: String,
232    label: String,
233    value: String,
234    options: Vec<(String, String)>,
235    display_field_type: Option<FieldType>,
236    set: Callback<String>,
237) -> Element {
238    let selected: Vec<String> = if value.is_empty() {
239        vec![]
240    } else {
241        value.split(',').map(|s| s.trim().to_owned()).collect()
242    };
243
244    // Look up a value renderer for the display field type — same map as table cells (symmetric with React).
245    let components: Option<HyleComponents> = use_hyle_components();
246    let label_render_fn: Option<fn(HyleValueProps) -> Element> =
247        display_field_type.as_ref().and_then(|ft| {
248            components.as_ref().and_then(|c| {
249                let key = field_type_key(ft);
250                c.values.get(key).copied()
251            })
252        });
253
254    // Blueprint from context — needed to build HyleValueProps for the label renderer.
255    let blueprint = use_context::<HyleConfig>().blueprint;
256
257    rsx! {
258        fieldset {
259            legend { "{label}" }
260            for (id, display) in options {
261                label {
262                    key: "{id}",
263                    input {
264                        r#type: "checkbox",
265                        name: "{name}",
266                        value: "{id}",
267                        checked: selected.contains(&id),
268                        onchange: {
269                            let id = id.clone();
270                            let value = value.clone();
271                            let set = set.clone();
272                            move |e: Event<FormData>| {
273                                let mut current: Vec<String> = if value.is_empty() {
274                                    vec![]
275                                } else {
276                                    value.split(',').map(|s| s.trim().to_owned()).collect()
277                                };
278                                if e.checked() {
279                                    if !current.contains(&id) {
280                                        current.push(id.clone());
281                                    }
282                                } else {
283                                    current.retain(|s| s != &id);
284                                }
285                                set.call(current.join(","));
286                            }
287                        },
288                    }
289                    if let Some(render_fn) = label_render_fn {
290                        {
291                            let ft = display_field_type.clone().unwrap_or(FieldType::Primitive { primitive: Primitive::String });
292                            let field = hyle::Field { label: display.clone(), field_type: ft, options: Default::default() };
293                            let display_val = hyle::Value::String(display.clone());
294                            render_fn(HyleValueProps {
295                                key: id.clone(),
296                                field,
297                                value: display_val,
298                                outcome: hyle::Outcome::empty(),
299                                model_name: String::new(),
300                                blueprint: (*blueprint).clone(),
301                                components: components.clone(),
302                            })
303                        }
304                    } else {
305                        " {display}"
306                    }
307                }
308            }
309        }
310    }
311}
312
313fn text_input(ff: HyleFilterField, value: String, set: Callback<String>) -> Element {
314    let input_type = ff
315        .field
316        .options
317        .input
318        .as_ref()
319        .map(|i| i.kind.clone())
320        .unwrap_or_else(|| "text".to_owned());
321
322    rsx! {
323        label {
324            "{ff.label}"
325            input {
326                r#type: "{input_type}",
327                name: "{ff.key}",
328                placeholder: "{ff.label}",
329                value: "{value}",
330                oninput: move |e| set.call(e.value()),
331            }
332        }
333    }
334}