Skip to main content

hyle_dioxus/
filter.rs

1use dioxus::prelude::*;
2
3use hyle::{FieldType, Primitive};
4
5use crate::context::use_hyle_components;
6use crate::types::{field_type_key, HyleFilterField, HyleFilterFieldProps, HyleFiltersState};
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/// By default it renders:
14/// - `Boolean` fields → `<select>` with "Any / Yes / No"
15/// - `Reference` fields → `<select>` populated from pre-resolved `options`
16/// - Everything else → `<input type="text">` (or the `input.kind` hint from
17///   the blueprint, e.g. `"search"`, `"number"`)
18///
19/// Pass `render` to replace the default for a specific field:
20///
21/// ```rust,ignore
22/// FilterField {
23///     state: filters,
24///     field_key: "role",
25///     render: |props| rsx! { MySelectInput { props } },
26/// }
27/// ```
28///
29/// # Props
30/// - `state` — the `HyleFiltersState` from `use_filters`
31/// - `field_key` — the field name to render
32/// - `render` — optional custom renderer receiving `HyleFilterFieldProps`
33#[component]
34pub fn FilterField(
35    state: HyleFiltersState,
36    field_key: String,
37    render: Option<fn(HyleFilterFieldProps) -> Element>,
38) -> Element {
39    let filter_field: Option<HyleFilterField> = state
40        .fields
41        .read()
42        .iter()
43        .find(|f| f.key == field_key)
44        .cloned();
45
46    let Some(ff) = filter_field else {
47        return rsx! {};
48    };
49
50    let key = ff.key.clone();
51    let value = state
52        .form_data
53        .read()
54        .get(&key)
55        .cloned()
56        .unwrap_or_default();
57
58    let set: Callback<String> = {
59        let key = key.clone();
60        Callback::new(move |v: String| state.set_field.call((key.clone(), v)))
61    };
62
63    // Priority 1: per-field render from the `change` map (highest).
64    if let Some(ref render_fn) = ff.render {
65        return render_fn(HyleFilterFieldProps {
66            key: ff.key,
67            label: ff.label,
68            field: ff.field,
69            options: ff.options,
70            value,
71            set,
72        });
73    }
74
75    // Priority 2: render prop supplied by the caller.
76    if let Some(render_fn) = render {
77        return render_fn(HyleFilterFieldProps {
78            key: ff.key,
79            label: ff.label,
80            field: ff.field,
81            options: ff.options,
82            value,
83            set,
84        });
85    }
86
87    // Priority 3: global HyleComponents context.
88    let props = HyleFilterFieldProps {
89        key: ff.key.clone(),
90        label: ff.label.clone(),
91        field: ff.field.clone(),
92        options: ff.options.clone(),
93        value: value.clone(),
94        set: set.clone(),
95    };
96    if let Some(components) = use_hyle_components() {
97        let type_key = field_type_key(&ff.field.field_type);
98        if let Some(render_fn) = components.filters.get(type_key).copied() {
99            return render_fn(props);
100        }
101    }
102
103    // Priority 4: built-in default.
104    default_input(ff, value, set)
105}
106
107// ── Default input dispatch ────────────────────────────────────────────────────
108
109fn default_input(ff: HyleFilterField, value: String, set: Callback<String>) -> Element {
110    match &ff.field.field_type {
111        FieldType::Primitive {
112            primitive: Primitive::Boolean,
113        } => boolean_select(ff.key, ff.label, value, set),
114
115        FieldType::Reference { .. } => {
116            reference_select(ff.key, ff.label, value, ff.options.unwrap_or_default(), set)
117        }
118
119        _ => text_input(ff, value, set),
120    }
121}
122
123fn boolean_select(name: String, label: String, value: String, set: Callback<String>) -> Element {
124    rsx! {
125        select {
126            name: "{name}",
127            aria_label: "{label}",
128            onchange: move |e| set.call(e.value()),
129            option { value: "", selected: value.is_empty(), "Any" }
130            option { value: "true", selected: value == "true", "Yes" }
131            option { value: "false", selected: value == "false", "No" }
132        }
133    }
134}
135
136fn reference_select(
137    name: String,
138    label: String,
139    value: String,
140    options: Vec<(String, String)>,
141    set: Callback<String>,
142) -> Element {
143    rsx! {
144        select {
145            name: "{name}",
146            aria_label: "{label}",
147            onchange: move |e| set.call(e.value()),
148            option { value: "", selected: value.is_empty(), "All {label}s" }
149            for (id, display) in options {
150                option { key: "{id}", value: "{id}", selected: value == id, "{display}" }
151            }
152        }
153    }
154}
155
156fn text_input(ff: HyleFilterField, value: String, set: Callback<String>) -> Element {
157    let kind = ff
158        .field
159        .options
160        .input
161        .as_ref()
162        .map(|i| i.kind.clone())
163        .unwrap_or_else(|| "text".to_owned());
164
165    if kind == "textarea" {
166        let rows = ff
167            .field
168            .options
169            .input
170            .as_ref()
171            .and_then(|i| i.props.get("rows"))
172            .and_then(|v| v.as_u64())
173            .unwrap_or(4) as i64;
174        return rsx! {
175            textarea {
176                name: "{ff.key}",
177                aria_label: "{ff.label}",
178                rows,
179                oninput: move |e| set.call(e.value()),
180                "{value}"
181            }
182        };
183    }
184
185    rsx! {
186        input {
187            r#type: "{kind}",
188            name: "{ff.key}",
189            aria_label: "{ff.label}",
190            placeholder: "{ff.label}",
191            value: "{value}",
192            oninput: move |e| set.call(e.value()),
193        }
194    }
195}