1use dioxus_hooks::{use_callback, use_memo, use_signal, use_context};
2use dioxus_signals::{Memo, ReadableExt, WritableExt};
3use indexmap::IndexMap;
4use serde_json::Value as JsonValue;
5
6use hyle::{
7 build_effective_query, build_filter_fields, compute_data, compute_forma_result,
8 compute_manifest, run_purify,
9 Forma, MutateInput, PurifyError, Query, Value,
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]
136pub fn use_filters(
137 query: Query,
138 options: UseFiltersOptions,
139) -> HyleFiltersState {
140 let config = use_hyle_config();
141
142 let initial = options.initial_committed;
143 let initial2 = initial.clone();
144 let mut committed = use_signal(move || initial.clone());
145 let mut form_data = use_signal(move || initial2.clone());
146 let mut filter_reset_key = use_signal(|| 0u32);
147 let mut purify_errors = use_signal(|| Option::<Vec<PurifyError>>::None);
148 let change = options.change;
149
150 let has_id = query.where_.contains_key("id");
154 let seed_query = if has_id {
155 query.clone()
156 } else {
157 Query { model: query.model.clone(), select: query.select.clone(), ..Default::default() }
158 };
159 let seed_data = use_data(seed_query);
160
161 let bp_for_fields = config.blueprint.clone();
162 let bp_for_validate = config.blueprint.clone();
163
164 let fields = use_memo(move || {
167 let raw_fields = match &*seed_data.read() {
168 HyleDataState::Ready { row: Some(r), manifest, outcome, .. } => {
169 let seeded: IndexMap<String, String> = r
171 .iter()
172 .map(|(k, v)| {
173 let s = match v {
174 Value::String(s) => s.clone(),
175 Value::Null => String::new(),
176 Value::Array(arr) => arr
177 .iter()
178 .map(|item| match item {
179 Value::String(s) => s.clone(),
180 other => other.to_string(),
181 })
182 .collect::<Vec<_>>()
183 .join(","),
184 other => other.to_string(),
185 };
186 (k.clone(), s)
187 })
188 .collect();
189 form_data.set(seeded);
190 build_filter_fields(&bp_for_fields, manifest, outcome)
196 .into_iter()
197 .map(|f| HyleFilterField { key: f.key, label: f.label, field: f.field, options: f.options, render: None })
198 .collect()
199 }
200 HyleDataState::Ready { manifest, outcome, .. } => {
201 build_filter_fields(&bp_for_fields, manifest, outcome)
202 .into_iter()
203 .map(|f| HyleFilterField { key: f.key, label: f.label, field: f.field, options: f.options, render: None })
204 .collect()
205 }
206 _ => vec![],
207 };
208 if let Some(ref c) = change {
209 hyle::apply_change(raw_fields, c)
210 } else {
211 raw_fields
212 }
213 });
214
215 let effective_query = use_memo(move || {
216 let q = query.clone();
217 let committed_snapshot = committed.cloned();
218 build_effective_query(&q, &committed_snapshot, q.page.unwrap_or(1), q.per_page.unwrap_or(5), None, true)
219 });
220
221 let set_field = use_callback(move |(name, value): (String, String)| {
222 form_data.with_mut(|m: &mut IndexMap<String, String>| { m.insert(name, value); });
223 });
224
225 let filter_apply = use_callback(move |()| {
226 let snapshot = form_data.cloned();
227 committed.with_mut(|c: &mut IndexMap<String, String>| c.extend(snapshot));
228 });
229
230 let filter_clear = use_callback(move |()| {
231 form_data.set(IndexMap::new());
232 committed.set(IndexMap::new());
233 filter_reset_key.with_mut(|k| *k += 1);
234 });
235
236 let validate = use_callback(move |()| {
237 let snapshot = form_data.cloned();
238 let model_name = effective_query.read().model.clone();
239 let active_keys: std::collections::HashSet<String> =
240 fields.read().iter().map(|f| f.key.clone()).collect();
241 let active_snapshot: IndexMap<String, String> = snapshot
242 .iter()
243 .filter(|(k, _)| active_keys.contains(*k))
244 .map(|(k, v)| (k.clone(), v.clone()))
245 .collect();
246 let errors = run_purify(&bp_for_validate, &model_name, &active_snapshot);
247 purify_errors.set(errors);
248 });
249
250 HyleFiltersState {
251 query: effective_query,
252 fields,
253 form_data,
254 set_field,
255 filter_apply,
256 filter_clear,
257 filter_reset_key,
258 validate,
259 purify_errors,
260 }
261}
262
263#[must_use]
271pub fn use_form(
272 query: Query,
273 opts: crate::types::UseFormOptions,
274) -> crate::types::HyleFormState {
275 use dioxus::prelude::try_consume_context;
276
277 let adapter = try_consume_context::<HyleAdapter>()
278 .expect("HyleAdapter must be provided via use_adapter_config! at the app root");
279
280 let is_edit = query.where_.contains_key("id");
281 let model = query.model.clone();
282 let mutation = if is_edit { adapter.update } else { adapter.create };
283
284 let filters = use_filters(
285 query,
286 crate::types::UseFiltersOptions {
287 initial_committed: opts.initial_committed,
288 change: opts.change,
289 },
290 );
291
292 let is_valid = filters.purify_errors.read().is_none();
293
294 let on_submit = use_callback(move |()| {
295 filters.validate.call(());
296 if filters.purify_errors.read().is_some() {
297 return;
298 }
299 let snapshot = filters.form_data.cloned();
300 let id = snapshot.get("id").map(|v| {
301 v.parse::<u64>().map(JsonValue::from).unwrap_or_else(|_| JsonValue::String(v.clone()))
302 });
303 mutation.mutate.call(MutateInput { model: model.clone(), id, data: snapshot });
304 });
305
306 crate::types::HyleFormState { filters, is_edit, is_valid, on_submit, mutation }
307}
308
309#[must_use]
315pub fn use_mutation(model: &'static str) -> crate::types::BoundMutations {
316 use dioxus::prelude::try_consume_context;
317 use crate::types::{BoundMutation, BoundMutateInput, BoundMutations};
318
319 let adapter = try_consume_context::<HyleAdapter>()
320 .expect("HyleAdapter must be provided via use_adapter_config! at the app root");
321
322 let bind = |hm: crate::types::HyleMutation| -> BoundMutation {
323 let mutate = use_callback(move |input: BoundMutateInput| {
324 hm.mutate.call(MutateInput { model: model.to_owned(), id: input.id, data: input.data });
325 });
326 BoundMutation { mutate, is_pending: hm.is_pending, is_success: hm.is_success, error: hm.error }
327 };
328
329 BoundMutations {
330 create: bind(adapter.create),
331 update: bind(adapter.update),
332 delete: bind(adapter.delete),
333 }
334}
335
336#[must_use]
341pub fn use_forma(
342 table_name: &'static str,
343 id: Option<JsonValue>,
344 opts: UseFormaOptions,
345) -> Memo<(Option<Query>, Option<Forma>)> {
346 use crate::types::FORMA_MODEL;
347
348 let forma_query = Query {
349 model: FORMA_MODEL.to_owned(),
350 where_: indexmap::indexmap! { "id".to_owned() => JsonValue::String(table_name.to_owned()) },
351 method: Some("one".to_owned()),
352 select: vec![
353 "fields".to_owned(),
354 "detail".to_owned(),
355 "form".to_owned(),
356 "column".to_owned(),
357 "filters".to_owned(),
358 ],
359 ..Default::default()
360 };
361
362 let data = use_data(forma_query);
363
364 use_memo(move || {
365 compute_forma_result(&data.cloned(), table_name, id.clone(), &opts.context)
366 })
367}
368
369pub fn form_body(data: &IndexMap<String, String>) -> Value {
384 let map: serde_json::Map<String, Value> = data
385 .iter()
386 .map(|(k, v)| (k.clone(), Value::String(v.clone())))
387 .collect();
388 Value::Object(map)
389}