1use dioxus_hooks::{use_callback, use_memo, use_signal, use_context};
2use dioxus_signals::{Memo, ReadableExt, WritableExt};
3use indexmap::IndexMap;
4use hyle::{
5 Forma, MutateInput, Query, Value,
6};
7use crate::adapter::{
8 build_effective_query, build_filter_fields, compute_data, compute_forma_result,
9 compute_manifest,
10 HyleDataState, HyleManifestState, UseFormaOptions,
11};
12
13use crate::context::use_hyle_config;
14use crate::types::{
15 HyleAdapter, HyleFilterField, HyleFiltersState, HyleListState,
16 HyleSourceState, UseFiltersOptions,
17};
18
19#[must_use]
26pub fn use_manifest(query: Query) -> Memo<HyleManifestState> {
27 let config = use_hyle_config();
28 use_memo(move || compute_manifest(&config.blueprint, &query))
29}
30
31#[must_use]
33pub fn use_data(query: Query) -> Memo<HyleDataState> {
34 let config = use_hyle_config();
35 let adapter = use_context::<HyleAdapter>();
36 use_memo(move || {
37 let bp = config.blueprint.clone();
38 let source = adapter.source;
39
40 let manifest = match bp.manifest(query.clone()) {
41 Ok(m) => m,
42 Err(e) => return HyleDataState::Error { error: e.to_string(), manifest: None },
43 };
44
45 match source.read().clone() {
46 HyleSourceState::Loading => HyleDataState::Loading { manifest: Some(manifest) },
47 HyleSourceState::Error(e) => HyleDataState::Error { error: e, manifest: Some(manifest) },
48 HyleSourceState::Ready(src) => compute_data(bp, manifest, src),
49 }
50 })
51}
52
53fn use_list_data(effective_query: Memo<Query>) -> Memo<HyleDataState> {
55 let config = use_hyle_config();
56 let adapter = use_context::<HyleAdapter>();
57 use_memo(move || {
58 let bp = config.blueprint.clone();
59 let source = adapter.source;
60 let query = effective_query.read().clone();
61
62 let manifest = match bp.manifest(query) {
63 Ok(m) => m,
64 Err(e) => return HyleDataState::Error { error: e.to_string(), manifest: None },
65 };
66
67 match source.read().clone() {
68 HyleSourceState::Loading => HyleDataState::Loading { manifest: Some(manifest) },
69 HyleSourceState::Error(e) => HyleDataState::Error { error: e, manifest: Some(manifest) },
70 HyleSourceState::Ready(src) => compute_data(bp, manifest, src),
71 }
72 })
73}
74
75#[must_use]
77pub fn use_list(query: Query) -> HyleListState {
78 let page = use_signal(|| query.page.unwrap_or(1));
79 let per_page = use_signal(|| query.per_page.unwrap_or(5));
80 let sort_field = use_signal(|| query.sort.as_ref().map(|s| s.field.clone()));
81 let sort_ascending = use_signal(|| query.sort.as_ref().map(|s| s.ascending).unwrap_or(true));
82
83 let effective_query = use_memo(move || {
84 build_effective_query(
85 &query,
86 &IndexMap::new(),
87 page(),
88 per_page(),
89 sort_field().as_deref(),
90 sort_ascending(),
91 )
92 });
93
94 let data = use_list_data(effective_query);
95
96 HyleListState { data, query: effective_query, page, per_page, sort_field, sort_ascending }
97}
98
99#[must_use]
101pub fn use_list_with_filters(filters: HyleFiltersState) -> HyleListState {
102 let base = filters.query.read().clone();
103 let page = use_signal(|| base.page.unwrap_or(1));
104 let per_page = use_signal(|| base.per_page.unwrap_or(5));
105 let sort_field = use_signal(|| base.sort.as_ref().map(|s| s.field.clone()));
106 let sort_ascending = use_signal(|| base.sort.as_ref().map(|s| s.ascending).unwrap_or(true));
107 let filter_query = filters.query;
108
109 let effective_query = use_memo(move || {
110 let base = filter_query.read().clone();
111 build_effective_query(
112 &base,
113 &IndexMap::new(),
114 page(),
115 per_page(),
116 sort_field().as_deref(),
117 sort_ascending(),
118 )
119 });
120
121 let data = use_list_data(effective_query);
122
123 HyleListState { data, query: effective_query, page, per_page, sort_field, sort_ascending }
124}
125
126#[must_use]
135pub fn use_filters(
136 query: Query,
137 options: UseFiltersOptions,
138) -> HyleFiltersState {
139 let config = use_hyle_config();
140
141 let initial = options.initial_committed;
142 let initial2 = initial.clone();
143 let mut committed = use_signal(move || initial.clone());
144 let mut form_data = use_signal(move || initial2.clone());
145 let mut filter_reset_key = use_signal(|| 0u32);
146 let change = options.change;
147
148 let has_id = query.where_.contains_key("id");
152 let seed_query = if has_id {
153 query.clone()
154 } else {
155 Query { model: query.model.clone(), select: query.select.clone(), ..Default::default() }
156 };
157 let seed_data = use_data(seed_query);
158
159 let bp_for_fields = config.blueprint.clone();
160
161 let mut seeded = use_signal(|| false);
168
169 let fields = use_memo(move || {
172 let raw_fields = match &*seed_data.read() {
173 HyleDataState::Ready { row: Some(r), manifest, outcome, .. } => {
174 if !seeded() {
177 let row_data: IndexMap<String, String> = r
178 .iter()
179 .map(|(k, v)| {
180 let s = match v {
181 Value::String(s) => s.clone(),
182 Value::Null => String::new(),
183 Value::Array(arr) => arr
184 .iter()
185 .map(|item| match item {
186 Value::String(s) => s.clone(),
187 other => other.to_string(),
188 })
189 .collect::<Vec<_>>()
190 .join(","),
191 other => other.to_string(),
192 };
193 (k.clone(), s)
194 })
195 .collect();
196 form_data.set(row_data);
197 seeded.set(true);
198 }
199 build_filter_fields(&bp_for_fields, manifest, outcome)
200 .into_iter()
201 .map(|f| HyleFilterField { key: f.key, label: f.label, field: f.field, options: f.options, display_field_type: f.display_field_type, render: None })
202 .collect()
203 }
204 HyleDataState::Ready { manifest, outcome, .. } => {
205 build_filter_fields(&bp_for_fields, manifest, outcome)
206 .into_iter()
207 .map(|f| HyleFilterField { key: f.key, label: f.label, field: f.field, options: f.options, display_field_type: f.display_field_type, render: None })
208 .collect()
209 }
210 _ => vec![],
211 };
212 if let Some(ref c) = change {
213 crate::adapter::apply_change(raw_fields, c)
214 } else {
215 raw_fields
216 }
217 });
218
219 let effective_query = use_memo(move || {
220 let q = query.clone();
221 let committed_snapshot = committed.cloned();
222 build_effective_query(&q, &committed_snapshot, q.page.unwrap_or(1), q.per_page.unwrap_or(5), None, true)
223 });
224
225 let set_field = use_callback(move |(name, value): (String, String)| {
226 form_data.with_mut(|m: &mut IndexMap<String, String>| { m.insert(name, value); });
227 });
228
229 let filter_apply = use_callback(move |()| {
230 let snapshot = form_data.cloned();
231 committed.with_mut(|c: &mut IndexMap<String, String>| c.extend(snapshot));
232 });
233
234 let filter_clear = use_callback(move |()| {
235 form_data.set(IndexMap::new());
236 committed.set(IndexMap::new());
237 filter_reset_key.with_mut(|k| *k += 1);
238 });
239
240 HyleFiltersState {
241 query: effective_query,
242 fields,
243 form_data,
244 set_field,
245 filter_apply,
246 filter_clear,
247 filter_reset_key,
248 }
249}
250
251#[must_use]
259pub fn use_form(
260 query: Query,
261 opts: crate::types::UseFormOptions,
262) -> crate::types::HyleFormState {
263 use dioxus::prelude::try_consume_context;
264
265 let adapter = try_consume_context::<HyleAdapter>()
266 .expect("HyleAdapter must be provided via use_adapter_config! at the app root");
267
268 let is_edit = query.where_.contains_key("id");
269 let model = query.model.clone();
270 let mutation = if is_edit { adapter.update } else { adapter.create };
271
272 let filters = use_filters(
273 query,
274 crate::types::UseFiltersOptions {
275 initial_committed: opts.initial_committed,
276 change: opts.change,
277 },
278 );
279
280 let on_submit = use_callback(move |()| {
281 let snapshot = filters.form_data.cloned();
282 let id = snapshot.get("id").map(|v| {
283 v.parse::<i64>().map(Value::Int).unwrap_or_else(|_| Value::String(v.clone()))
284 });
285 mutation.mutate.call(MutateInput { model: model.clone(), id, data: snapshot });
286 });
287
288 crate::types::HyleFormState { filters, is_edit, is_valid: true, on_submit, mutation }
289}
290
291#[must_use]
297pub fn use_mutation(model: &'static str) -> crate::types::BoundMutations {
298 use dioxus::prelude::try_consume_context;
299 use crate::types::{BoundMutation, BoundMutateInput, BoundMutations};
300
301 let adapter = try_consume_context::<HyleAdapter>()
302 .expect("HyleAdapter must be provided via use_adapter_config! at the app root");
303
304 let bind = |hm: crate::types::HyleMutation| -> BoundMutation {
305 let mutate = use_callback(move |input: BoundMutateInput| {
306 hm.mutate.call(MutateInput { model: model.to_owned(), id: input.id, data: input.data });
307 });
308 BoundMutation { mutate, is_pending: hm.is_pending, is_success: hm.is_success, error: hm.error }
309 };
310
311 BoundMutations {
312 create: bind(adapter.create),
313 update: bind(adapter.update),
314 delete: bind(adapter.delete),
315 }
316}
317
318#[must_use]
323pub fn use_forma(
324 table_name: &'static str,
325 id: Option<Value>,
326 opts: UseFormaOptions,
327) -> Memo<(Option<Query>, Option<Forma>)> {
328 use crate::types::FORMA_MODEL;
329
330 let forma_query = Query {
331 model: FORMA_MODEL.to_owned(),
332 where_: indexmap::indexmap! { "id".to_owned() => Value::String(table_name.to_owned()) },
333 method: Some("one".to_owned()),
334 select: vec![
335 "fields".to_owned(),
336 "detail".to_owned(),
337 "form".to_owned(),
338 "column".to_owned(),
339 "filters".to_owned(),
340 ],
341 ..Default::default()
342 };
343
344 let data = use_data(forma_query);
345
346 use_memo(move || {
347 compute_forma_result(&data.cloned(), table_name, id.clone(), &opts.context)
348 })
349}
350
351pub fn form_body(data: &IndexMap<String, String>) -> Value {
366 Value::Map(data.iter()
367 .map(|(k, v)| (k.clone(), Value::String(v.clone())))
368 .collect())
369}