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 mut seeded = use_signal(|| false);
171
172 let fields = use_memo(move || {
175 let raw_fields = match &*seed_data.read() {
176 HyleDataState::Ready { row: Some(r), manifest, outcome, .. } => {
177 if !seeded() {
180 let row_data: IndexMap<String, String> = r
181 .iter()
182 .map(|(k, v)| {
183 let s = match v {
184 Value::String(s) => s.clone(),
185 Value::Null => String::new(),
186 Value::Array(arr) => arr
187 .iter()
188 .map(|item| match item {
189 Value::String(s) => s.clone(),
190 other => other.to_string(),
191 })
192 .collect::<Vec<_>>()
193 .join(","),
194 other => other.to_string(),
195 };
196 (k.clone(), s)
197 })
198 .collect();
199 form_data.set(row_data);
200 seeded.set(true);
201 }
202 build_filter_fields(&bp_for_fields, manifest, outcome)
203 .into_iter()
204 .map(|f| HyleFilterField { key: f.key, label: f.label, field: f.field, options: f.options, display_field_type: f.display_field_type, render: None })
205 .collect()
206 }
207 HyleDataState::Ready { manifest, outcome, .. } => {
208 build_filter_fields(&bp_for_fields, manifest, outcome)
209 .into_iter()
210 .map(|f| HyleFilterField { key: f.key, label: f.label, field: f.field, options: f.options, display_field_type: f.display_field_type, render: None })
211 .collect()
212 }
213 _ => vec![],
214 };
215 if let Some(ref c) = change {
216 hyle::apply_change(raw_fields, c)
217 } else {
218 raw_fields
219 }
220 });
221
222 let effective_query = use_memo(move || {
223 let q = query.clone();
224 let committed_snapshot = committed.cloned();
225 build_effective_query(&q, &committed_snapshot, q.page.unwrap_or(1), q.per_page.unwrap_or(5), None, true)
226 });
227
228 let set_field = use_callback(move |(name, value): (String, String)| {
229 form_data.with_mut(|m: &mut IndexMap<String, String>| { m.insert(name, value); });
230 });
231
232 let filter_apply = use_callback(move |()| {
233 let snapshot = form_data.cloned();
234 committed.with_mut(|c: &mut IndexMap<String, String>| c.extend(snapshot));
235 });
236
237 let filter_clear = use_callback(move |()| {
238 form_data.set(IndexMap::new());
239 committed.set(IndexMap::new());
240 filter_reset_key.with_mut(|k| *k += 1);
241 });
242
243 let validate = use_callback(move |()| {
244 let snapshot = form_data.cloned();
245 let model_name = effective_query.read().model.clone();
246 let active_keys: std::collections::HashSet<String> =
247 fields.read().iter().map(|f| f.key.clone()).collect();
248 let active_snapshot: IndexMap<String, String> = snapshot
249 .iter()
250 .filter(|(k, _)| active_keys.contains(*k))
251 .map(|(k, v)| (k.clone(), v.clone()))
252 .collect();
253 let errors = run_purify(&bp_for_validate, &model_name, &active_snapshot);
254 purify_errors.set(errors);
255 });
256
257 HyleFiltersState {
258 query: effective_query,
259 fields,
260 form_data,
261 set_field,
262 filter_apply,
263 filter_clear,
264 filter_reset_key,
265 validate,
266 purify_errors,
267 }
268}
269
270#[must_use]
278pub fn use_form(
279 query: Query,
280 opts: crate::types::UseFormOptions,
281) -> crate::types::HyleFormState {
282 use dioxus::prelude::try_consume_context;
283
284 let adapter = try_consume_context::<HyleAdapter>()
285 .expect("HyleAdapter must be provided via use_adapter_config! at the app root");
286
287 let is_edit = query.where_.contains_key("id");
288 let model = query.model.clone();
289 let mutation = if is_edit { adapter.update } else { adapter.create };
290
291 let filters = use_filters(
292 query,
293 crate::types::UseFiltersOptions {
294 initial_committed: opts.initial_committed,
295 change: opts.change,
296 },
297 );
298
299 let is_valid = filters.purify_errors.read().is_none();
300
301 let on_submit = use_callback(move |()| {
302 filters.validate.call(());
303 if filters.purify_errors.read().is_some() {
304 return;
305 }
306 let snapshot = filters.form_data.cloned();
307 let id = snapshot.get("id").map(|v| {
308 v.parse::<u64>().map(JsonValue::from).unwrap_or_else(|_| JsonValue::String(v.clone()))
309 });
310 mutation.mutate.call(MutateInput { model: model.clone(), id, data: snapshot });
311 });
312
313 crate::types::HyleFormState { filters, is_edit, is_valid, on_submit, mutation }
314}
315
316#[must_use]
322pub fn use_mutation(model: &'static str) -> crate::types::BoundMutations {
323 use dioxus::prelude::try_consume_context;
324 use crate::types::{BoundMutation, BoundMutateInput, BoundMutations};
325
326 let adapter = try_consume_context::<HyleAdapter>()
327 .expect("HyleAdapter must be provided via use_adapter_config! at the app root");
328
329 let bind = |hm: crate::types::HyleMutation| -> BoundMutation {
330 let mutate = use_callback(move |input: BoundMutateInput| {
331 hm.mutate.call(MutateInput { model: model.to_owned(), id: input.id, data: input.data });
332 });
333 BoundMutation { mutate, is_pending: hm.is_pending, is_success: hm.is_success, error: hm.error }
334 };
335
336 BoundMutations {
337 create: bind(adapter.create),
338 update: bind(adapter.update),
339 delete: bind(adapter.delete),
340 }
341}
342
343#[must_use]
348pub fn use_forma(
349 table_name: &'static str,
350 id: Option<JsonValue>,
351 opts: UseFormaOptions,
352) -> Memo<(Option<Query>, Option<Forma>)> {
353 use crate::types::FORMA_MODEL;
354
355 let forma_query = Query {
356 model: FORMA_MODEL.to_owned(),
357 where_: indexmap::indexmap! { "id".to_owned() => JsonValue::String(table_name.to_owned()) },
358 method: Some("one".to_owned()),
359 select: vec![
360 "fields".to_owned(),
361 "detail".to_owned(),
362 "form".to_owned(),
363 "column".to_owned(),
364 "filters".to_owned(),
365 ],
366 ..Default::default()
367 };
368
369 let data = use_data(forma_query);
370
371 use_memo(move || {
372 compute_forma_result(&data.cloned(), table_name, id.clone(), &opts.context)
373 })
374}
375
376pub fn form_body(data: &IndexMap<String, String>) -> Value {
391 let map: serde_json::Map<String, Value> = data
392 .iter()
393 .map(|(k, v)| (k.clone(), Value::String(v.clone())))
394 .collect();
395 Value::Object(map)
396}