Skip to main content

tablo_core/schema/
tree.rs

1//! Composes field slots, layout containers, and embedded values into one tree.
2
3use std::{
4    collections::HashMap,
5    sync::atomic::{AtomicUsize, Ordering},
6};
7
8use topcoat::{Result, context::Cx, view::*};
9
10use super::{
11    Schema,
12    condition::{Condition, disable, watch},
13    embedded::Embedded,
14    fields::{Field, Placement},
15    layouts::{Grid, Group, Section, Step, Tab, Tabs, Wizard, spanned},
16};
17use crate::values::{FieldError, FieldErrors, FormValue, FormValues};
18
19#[derive(Debug)]
20pub(crate) enum Node {
21    /// A field, by its index in the root schema's field list.
22    Field(usize),
23    Section(Box<Section>),
24    Group(Box<Group>),
25    Grid(Box<Grid>),
26    /// A `Tabs` block, whose children are its `Tab` nodes.
27    Tabs(Box<Tabs>),
28    /// One tab, placed only in a `Tabs` block's children.
29    Tab(Box<Tab>),
30    /// A `Wizard` block, whose children are its `Step` nodes.
31    Wizard(Box<Wizard>),
32    /// One step, placed only in a `Wizard` block's children.
33    Step(Box<Step>),
34    Embedded(Box<Embedded>),
35}
36
37/// Holds the values and errors a schema's controls render with.
38pub struct Source<'a> {
39    values: &'a FormValues,
40    errors: &'a FieldErrors,
41    /// The conditions each watched field's key drives.
42    watched: HashMap<String, Vec<Condition>>,
43    /// The prefix of the form's DOM ids and of its signals' keys; empty for a form alone on its
44    /// page.
45    scope: String,
46    /// Where a searchable choice fetches its options, when not the resource's own route.
47    options: Option<String>,
48    /// Whether the form edits a stored record.
49    editing: bool,
50    /// The keys the request's field access hides or locks.
51    mask: super::FormMask,
52    /// What every control's name starts with: a related form's `{slug}.`, empty otherwise.
53    prefix: String,
54    /// How many tabs hold the node rendering now, which tells a nested block's DOM ids from its
55    /// outer tab's.
56    tab_depth: AtomicUsize,
57    /// Whether a dialog frames the form, whose sections then render without a panel.
58    flat: bool,
59}
60
61impl<'a> Source<'a> {
62    /// Renders controls hydrated with `values` and inline `errors`.
63    pub fn form(values: &'a FormValues, errors: &'a FieldErrors) -> Self {
64        Self {
65            values,
66            errors,
67            watched: HashMap::new(),
68            scope: String::new(),
69            options: None,
70            editing: false,
71            mask: super::FormMask::default(),
72            prefix: String::new(),
73            tab_depth: AtomicUsize::new(0),
74            flat: false,
75        }
76    }
77
78    /// Renders sections without a panel when `flat`, for a form a dialog frames.
79    pub(crate) fn flat(mut self, flat: bool) -> Self {
80        self.flat = flat;
81        self
82    }
83
84    /// Whether sections render without a panel.
85    pub(super) fn is_flat(&self) -> bool {
86        self.flat
87    }
88
89    /// Renders the edit form of a stored record, where `disabled_on_edit` disables a control.
90    pub(crate) fn editing(mut self, editing: bool) -> Self {
91        self.editing = editing;
92        self
93    }
94
95    /// Renders nothing for the keys `mask` hides, and disables the controls of the ones it locks.
96    pub(crate) fn masked(mut self, mask: super::FormMask) -> Self {
97        self.mask = mask;
98        self
99    }
100
101    /// Renders a form sharing its page with another: its DOM ids and its signals' keys carry
102    /// `scope`, so neither form's labels, conditions or variants reach the other's.
103    pub(crate) fn scoped(mut self, scope: impl Into<String>) -> Self {
104        self.scope = scope.into();
105        self
106    }
107
108    /// Renders a related form inside its owner's: every control posts under `prefix`, while its
109    /// values, errors, mask and conditions keep the related form's own keys.
110    pub(crate) fn posting_under(mut self, prefix: impl Into<String>) -> Self {
111        self.prefix = prefix.into();
112        self
113    }
114
115    /// Fetches a searchable choice's options from `url` rather than the resource's own route.
116    pub(crate) fn options_at(mut self, url: impl Into<String>) -> Self {
117        self.options = Some(url.into());
118        self
119    }
120
121    /// The prefix of the form's DOM ids and of its signals' keys.
122    pub(crate) fn scope(&self) -> &str {
123        &self.scope
124    }
125
126    /// Renders each watched field with the handlers its conditions follow.
127    pub(crate) fn watching(mut self, watched: HashMap<String, Vec<Condition>>) -> Self {
128        self.watched = watched;
129        self
130    }
131
132    /// Renders `view` guarded by `condition`, or as it is without one.
133    pub(crate) fn guard<'v>(
134        &self,
135        cx: &'v Cx,
136        condition: Option<&Condition>,
137        view: BoxView<'v>,
138    ) -> BoxView<'v> {
139        match condition {
140            Some(condition) => condition.guard(cx, &self.scope, self.values, view),
141            None => view,
142        }
143    }
144
145    /// How many tabs hold the node rendering now.
146    pub(crate) fn tab_depth(&self) -> usize {
147        self.tab_depth.load(Ordering::Relaxed)
148    }
149
150    /// Renders what follows inside `depth` tabs.
151    pub(crate) fn set_tab_depth(&self, depth: usize) {
152        self.tab_depth.store(depth, Ordering::Relaxed);
153    }
154
155    /// Whether `condition` holds in the submission, as the page first renders it: always without
156    /// one.
157    pub(crate) fn holds(&self, condition: Option<&Condition>) -> bool {
158        condition.is_none_or(|condition| condition.holds(self.values))
159    }
160
161    pub(crate) fn value(&self, name: &str) -> Option<&FormValue> {
162        self.values.get(name)
163    }
164
165    /// The text `name` holds: a scalar control's value.
166    pub(crate) fn text(&self, name: &str) -> Option<&str> {
167        self.values.text(name)
168    }
169
170    /// The rows `name` holds: a repeater's, none when it holds none.
171    pub(crate) fn rows(&self, name: &str) -> &[FormValues] {
172        self.values.rows(name).unwrap_or_default()
173    }
174
175    /// The errors the controls render under.
176    pub(crate) fn errors(&self) -> &FieldErrors {
177        self.errors
178    }
179
180    /// Where a searchable choice fetches its options, when not the resource's own route.
181    pub(crate) fn options(&self) -> Option<&str> {
182        self.options.as_deref()
183    }
184
185    /// The DOM id of the control posting `name`.
186    pub(crate) fn id(&self, name: &str) -> String {
187        self.placement(name, None).id(name)
188    }
189
190    /// Where the field posting `name` renders in this form, under its parent's value `parent`.
191    fn placement<'p>(&'p self, name: &str, parent: Option<&'p str>) -> Placement<'p> {
192        Placement {
193            parent,
194            scope: &self.scope,
195            options: self.options.as_deref(),
196            editing: self.editing,
197            locked: self.mask.locks(name),
198            prefix: &self.prefix,
199        }
200    }
201
202    /// Returns the message the field `field` renders under its control.
203    pub(crate) fn error_for(&self, field: &Field) -> Option<&FieldError> {
204        self.errors.first(field.name())
205    }
206}
207
208impl Node {
209    /// Renders this node from `source`, reading field slots from `fields`.
210    pub(crate) async fn render<'a>(
211        &self,
212        cx: &'a Cx,
213        fields: &[Field],
214        source: &Source<'_>,
215    ) -> Result<BoxView<'a>> {
216        match self {
217            Node::Field(index) => {
218                if !shows(&fields[*index], source) {
219                    return Ok(().boxed());
220                }
221                let field = &fields[*index];
222                // A repeater's rows render from the keys and errors of each of their controls.
223                if let Some(repeater) = field.as_repeater() {
224                    let view = Box::pin(repeater.render(cx, field, source)).await?;
225                    let view = spanned(cx, field.span(), view);
226                    return Ok(source.guard(cx, field.condition(), view));
227                }
228                let error = source.error_for(field);
229                let parent = field.parent_key().and_then(|key| source.text(key));
230                let mut view = Box::pin(field.render_under(
231                    cx,
232                    source.value(field.name()),
233                    error,
234                    source.placement(field.name(), parent),
235                ))
236                .await?;
237                if let Some(conditions) = source.watched.get(field.name()) {
238                    let posted = format!("{}{}", source.prefix, field.name());
239                    view = watch(
240                        cx,
241                        field,
242                        &posted,
243                        conditions,
244                        &source.scope,
245                        source.values,
246                        view,
247                    );
248                }
249                if let Some(condition) = field.disabling() {
250                    view = disable(cx, condition, &source.scope, source.values, view);
251                }
252                let view = spanned(cx, field.span(), view);
253                Ok(source.guard(cx, field.condition(), view))
254            }
255            // A block whose every field is masked renders no heading over nothing.
256            Node::Section(_) | Node::Group(_) | Node::Grid(_) | Node::Tabs(_) | Node::Wizard(_)
257                if !self.shows_any(fields, source) =>
258            {
259                Ok(().boxed())
260            }
261            Node::Section(s) => {
262                let view = Box::pin(s.render(cx, fields, source)).await?;
263                Ok(source.guard(cx, s.condition.as_ref(), view))
264            }
265            Node::Group(g) => {
266                let view = Box::pin(g.render(cx, fields, source)).await?;
267                Ok(source.guard(cx, g.condition.as_ref(), view))
268            }
269            Node::Grid(g) => {
270                let view = Box::pin(g.render(cx, fields, source)).await?;
271                Ok(source.guard(cx, g.condition.as_ref(), view))
272            }
273            Node::Tabs(t) => {
274                let view = Box::pin(t.render(cx, fields, source)).await?;
275                Ok(source.guard(cx, t.condition.as_ref(), view))
276            }
277            // A tab renders through its block, which lays out its trigger and its panel.
278            Node::Tab(t) => Box::pin(render_nodes(cx, &t.children.nodes, fields, source)).await,
279            Node::Wizard(w) => {
280                let view = Box::pin(w.render(cx, fields, source)).await?;
281                Ok(source.guard(cx, w.condition.as_ref(), view))
282            }
283            // A step renders through its block, which lays out its trigger and its panel.
284            Node::Step(s) => Box::pin(render_nodes(cx, &s.children.nodes, fields, source)).await,
285            Node::Embedded(e) => Box::pin(e.render(cx, fields, source)).await,
286        }
287    }
288
289    /// Whether a field under this node renders an error from `source`: under its own key, or a
290    /// repeater's under one of its rows' keys.
291    pub(crate) fn has_error(&self, fields: &[Field], source: &Source<'_>) -> bool {
292        let mut any = false;
293        self.visit_fields(&mut |index| {
294            let name = fields[index].name();
295            any |= source.errors.iter().any(|error| {
296                error.key == name
297                    || error
298                        .key
299                        .strip_prefix(name)
300                        .is_some_and(|rest| rest.starts_with('.'))
301            });
302        });
303        any
304    }
305
306    /// The first field slot under this node, in declaration order.
307    pub(crate) fn first_field(&self) -> Option<usize> {
308        let mut first = None;
309        self.visit_fields(&mut |index| {
310            first.get_or_insert(index);
311        });
312        first
313    }
314
315    /// Calls `f` with every field slot under this node, in declaration order.
316    fn visit_fields(&self, f: &mut impl FnMut(usize)) {
317        match self {
318            Node::Field(index) => f(*index),
319            Node::Embedded(e) => e.visit_fields(f),
320            node => {
321                for child in node.children().unwrap_or_default() {
322                    child.visit_fields(f);
323                }
324            }
325        }
326    }
327
328    /// Whether some field under this node renders under `source`'s mask.
329    pub(crate) fn shows_any(&self, fields: &[Field], source: &Source<'_>) -> bool {
330        match self {
331            Node::Field(index) => shows(&fields[*index], source),
332            Node::Embedded(e) => {
333                let mut any = false;
334                e.visit_fields(&mut |index| any |= shows(&fields[index], source));
335                any
336            }
337            node => node
338                .children()
339                .unwrap_or_default()
340                .iter()
341                .any(|child| child.shows_any(fields, source)),
342        }
343    }
344
345    /// Returns the nodes a layout container holds, or `None` for a field slot and an embedded
346    /// value.
347    pub(crate) fn children(&self) -> Option<&[Node]> {
348        match self {
349            Node::Section(s) => Some(&s.children.nodes),
350            Node::Group(g) => Some(&g.children.nodes),
351            Node::Grid(g) => Some(&g.children.nodes),
352            Node::Tabs(t) => Some(&t.children.nodes),
353            Node::Tab(t) => Some(&t.children.nodes),
354            Node::Wizard(w) => Some(&w.children.nodes),
355            Node::Step(s) => Some(&s.children.nodes),
356            Node::Field(_) | Node::Embedded(_) => None,
357        }
358    }
359
360    fn children_mut(&mut self) -> Option<&mut Vec<Node>> {
361        match self {
362            Node::Section(s) => Some(&mut s.children.nodes),
363            Node::Group(g) => Some(&mut g.children.nodes),
364            Node::Grid(g) => Some(&mut g.children.nodes),
365            Node::Tabs(t) => Some(&mut t.children.nodes),
366            Node::Tab(t) => Some(&mut t.children.nodes),
367            Node::Wizard(w) => Some(&mut w.children.nodes),
368            Node::Step(s) => Some(&mut s.children.nodes),
369            Node::Field(_) | Node::Embedded(_) => None,
370        }
371    }
372
373    /// Shifts every field slot under this node by `by`.
374    pub(crate) fn offset(&mut self, by: usize) {
375        match self {
376            Node::Field(index) => *index += by,
377            Node::Embedded(e) => e.offset(by),
378            _ => {
379                for child in self.children_mut().into_iter().flatten() {
380                    child.offset(by);
381                }
382            }
383        }
384    }
385}
386
387impl<F> Schema<F> {
388    /// The first field whose control posts keys besides its own or fetches from its resource's
389    /// routes: a file field, a repeater, an embedded value's leaf, a searchable or dependent
390    /// choice. A related form, posting under its owner's prefix, carries none of them.
391    pub(crate) fn needs_own_keys(&self) -> Option<&str> {
392        let mut embedded = Vec::new();
393        for node in &self.nodes {
394            node.embedded_fields(&mut embedded);
395        }
396        self.fields
397            .iter()
398            .enumerate()
399            .find(|(index, field)| {
400                embedded.contains(index)
401                    || field.is_file()
402                    || field.as_repeater().is_some()
403                    || field.parent_key().is_some()
404                    || field
405                        .as_choice()
406                        .is_some_and(|choice| choice.is_searchable())
407            })
408            .map(|(_, field)| field.name())
409    }
410}
411
412impl Node {
413    /// Adds to `out` every field slot an embedded value under this node holds.
414    fn embedded_fields(&self, out: &mut Vec<usize>) {
415        match self {
416            Node::Embedded(e) => e.visit_fields(&mut |index| out.push(index)),
417            node => {
418                for child in node.children().unwrap_or_default() {
419                    child.embedded_fields(out);
420                }
421            }
422        }
423    }
424}
425
426/// Whether `field` renders under `source`'s mask: not when hidden, nor a locked repeater, whose
427/// rows' order posts its key.
428fn shows(field: &Field, source: &Source<'_>) -> bool {
429    !(source.mask.hides(field.name())
430        || field.is_hidden(source.editing)
431        || (source.mask.locks(field.name()) && field.as_repeater().is_some()))
432}
433
434/// Renders `nodes` in order as one view.
435pub(crate) async fn render_nodes<'a>(
436    cx: &'a Cx,
437    nodes: &[Node],
438    fields: &[Field],
439    source: &Source<'_>,
440) -> Result<BoxView<'a>> {
441    let mut views = Vec::with_capacity(nodes.len());
442    for node in nodes {
443        views.push(node.render(cx, fields, source).await?);
444    }
445    Ok(view! {
446        for v in views {
447            (v)
448        }
449    }
450    .boxed())
451}
452
453/// Converts a field, a layout block, a tuple of either, or a schema into a [`Schema`].
454///
455/// `F` is the record form whose controls the schema places, `()` for a page's or an action's
456/// schema: a control converts into the schema of its own form only.
457#[diagnostic::on_unimplemented(
458    message = "`{Self}` cannot be placed in a `Schema<{F}>`",
459    label = "not a control, layout block, schema, or tuple of 2 to 12 of them for this schema",
460    note = "a resource's form places its record form's own controls, from `controls()`; \
461            `Field::text` and the other constructors build a page's or an action's controls"
462)]
463pub trait IntoSchema<F = ()> {
464    fn into_schema(self) -> Schema<F>;
465}
466
467/// Moves a control or a schema to the form `G`: what `#[derive(RecordForm)]` hands its controls
468/// over with.
469#[doc(hidden)]
470pub trait Retype {
471    type As<G>;
472
473    fn retype<G>(self) -> Self::As<G>;
474}
475
476impl<F> IntoSchema<F> for Schema<F> {
477    fn into_schema(self) -> Schema<F> {
478        self
479    }
480}
481
482impl IntoSchema for Field {
483    fn into_schema(self) -> Schema {
484        Schema::from_parts(vec![Node::Field(0)], vec![self])
485    }
486}
487
488/// Generates the single-node [`IntoSchema`] impls for every layout container, holding a schema or
489/// nothing.
490macro_rules! container_nodes {
491    ($($ty:ident),+ $(,)?) => {
492        $(
493            impl<F> IntoSchema<F> for $ty<Schema<F>> {
494                fn into_schema(self) -> Schema<F> {
495                    let mut fields = Vec::new();
496                    let block = self.map(|mut children| {
497                        fields = std::mem::take(&mut children.fields);
498                        children.retype()
499                    });
500                    Schema::from_parts(vec![Node::$ty(Box::new(block))], fields)
501                }
502            }
503
504            impl<F> IntoSchema<F> for $ty<()> {
505                fn into_schema(self) -> Schema<F> {
506                    self.holding(Schema::<F>::default()).into_schema()
507                }
508            }
509        )+
510    };
511}
512
513container_nodes!(Section, Group, Grid);
514
515impl<F> IntoSchema<F> for Tabs<Schema<F>> {
516    fn into_schema(self) -> Schema<F> {
517        let mut fields = Vec::new();
518        let block = self.map(|mut children| {
519            fields = std::mem::take(&mut children.fields);
520            children.retype()
521        });
522        Schema::from_parts(vec![Node::Tabs(Box::new(block))], fields)
523    }
524}
525
526/// Generates the tuple impls of [`IntoSchema`] from one list per arity.
527macro_rules! into_schema_tuples {
528    ($($T:ident => $v:ident),+ $(,)?) => {
529        impl<Form, $($T),+> IntoSchema<Form> for ($($T,)+)
530        where
531            $($T: IntoSchema<Form>,)+
532        {
533            fn into_schema(self) -> Schema<Form> {
534                let ($($v,)+) = self;
535                let mut schema = Schema::default();
536                $( schema.append($v.into_schema()); )+
537                schema
538            }
539        }
540    };
541}
542
543into_schema_tuples!(A => a, B => b);
544into_schema_tuples!(A => a, B => b, C => c);
545into_schema_tuples!(A => a, B => b, C => c, D => d);
546into_schema_tuples!(A => a, B => b, C => c, D => d, E => e);
547into_schema_tuples!(A => a, B => b, C => c, D => d, E => e, F => f);
548into_schema_tuples!(A => a, B => b, C => c, D => d, E => e, F => f, G => g);
549into_schema_tuples!(
550    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h
551);
552into_schema_tuples!(
553    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i
554);
555into_schema_tuples!(
556    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j
557);
558into_schema_tuples!(
559    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j, K => k
560);
561into_schema_tuples!(
562    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j, K => k, L => l
563);