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