Skip to main content

tablo_core/
detail.rs

1//! A resource's detail page: the [`Detail`] declaration, its columns arranged in layout blocks.
2
3use std::sync::Arc;
4
5use toasty::stmt::{List, Query};
6use topcoat::{context::Cx, view::*};
7
8#[cfg(feature = "rich-text")]
9use crate::column::RichTextColumn;
10use crate::{
11    DeclarationErrorKind,
12    column::{
13        BooleanColumn, BoxColumn, ColorColumn, Column, ComputedColumn, CountColumn, DateColumn,
14        EmbeddedColumn, FileColumn, IconColumn, ImageColumn, ListColumn, RelationColumn,
15        RepeaterColumn, SelectColumn, TextColumn, TextInputColumn, ToggleColumn, include_relations,
16        restrict_relations, shows, unavailable_sources,
17    },
18    schema::{Grid, Group, HoldsTabs, Section, Tab, TabView, Tabs, tab_id, tab_view},
19};
20
21/// The declaration of a detail page: [`Column`]s, the same ones a [`Table`](crate::Table) lists,
22/// arranged in [`Section`], [`Group`], [`Grid`] and [`Tabs`] blocks.
23///
24/// ```rust
25/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
26/// # struct Author { #[key] #[auto] id: uuid::Uuid, name: String }
27/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
28/// # struct Post {
29/// #     #[key] #[auto] id: uuid::Uuid,
30/// #     title: String,
31/// #     featured: bool,
32/// #     author_id: uuid::Uuid,
33/// #     #[belongs_to(key = author_id, references = id)]
34/// #     author: toasty::Deferred<Author>,
35/// # }
36/// use tablo_core::{BooleanColumn, Detail, RelationColumn, Section, TextColumn, lens, relation};
37///
38/// Detail::new(Section::new("Post").columns((
39///     TextColumn::new(lens!(Post.title)),
40///     BooleanColumn::new(lens!(Post.featured)),
41///     RelationColumn::unscoped(relation!(Post.author), |a: &Author| a.name.clone()),
42/// )));
43/// ```
44///
45/// The detail page loads the relations its columns declare ([`Column::includes`]) and renders each
46/// column's [`entry`](Column::entry) for the record.
47pub struct Detail<M> {
48    nodes: Vec<Node<M>>,
49}
50
51/// A column's `entry` under the `data-entry` hook naming the column, which lays out nothing.
52fn entry<'a>(cx: &'a Cx, name: &str, entry: BoxView<'a>) -> BoxView<'a> {
53    let name = name.to_string();
54    view! { <div class="contents" data-entry=(name)>(entry)</div> }.boxed()
55}
56
57/// One block or column of a [`Detail`].
58enum Node<M> {
59    Column(BoxColumn<M>),
60    Section(Box<Section<Detail<M>>>),
61    Group(Box<Group<Detail<M>>>),
62    Grid(Box<Grid<Detail<M>>>),
63    /// A `Tabs` block, whose children are its `Tab` nodes.
64    Tabs(Box<Tabs<Detail<M>>>),
65    /// One tab, placed only in a `Tabs` block's children.
66    Tab(Box<Tab<Detail<M>>>),
67}
68
69impl<M> Detail<M> {
70    /// Declare a detail page of `children`: columns and blocks of them.
71    pub fn new(children: impl IntoDetail<M>) -> Self {
72        children.into_detail()
73    }
74
75    /// A detail page showing nothing, for a resource that serves no view page: mounting refuses
76    /// it while the view page is served, which
77    /// [`without_page(ResourcePage::View)`](crate::ResourceDef::without_page) turns off.
78    pub fn empty() -> Self {
79        Self { nodes: Vec::new() }
80    }
81
82    /// A detail page showing `columns`, in order, outside any block.
83    pub(crate) fn of_columns(columns: &[BoxColumn<M>]) -> Self {
84        Self {
85            nodes: columns.iter().cloned().map(Node::Column).collect(),
86        }
87    }
88
89    /// Whether the declaration shows nothing.
90    pub fn is_empty(&self) -> bool {
91        self.nodes.is_empty()
92    }
93
94    /// Append `column` after the declared columns and blocks.
95    pub fn column(mut self, column: impl Column<M> + 'static) -> Self {
96        self.nodes.push(Node::Column(Arc::new(column)));
97        self
98    }
99
100    /// Appends `other`'s columns and blocks after this one's.
101    fn append(&mut self, other: Self) {
102        self.nodes.extend(other.nodes);
103    }
104
105    /// Have each column reading its row that declares it reads none of the row's relations read
106    /// `bare(row)`, a copy of the row without them, when a column loads one.
107    pub(crate) fn read_bare(&mut self, bare: fn(&M) -> M)
108    where
109        M: toasty::schema::Model + 'static,
110    {
111        fn walk<M: toasty::schema::Model + 'static>(nodes: &mut [Node<M>], bare: fn(&M) -> M) {
112            for node in nodes {
113                match node {
114                    Node::Column(column) => {
115                        *column = crate::column::bare_column(column.clone(), bare);
116                    }
117                    Node::Section(section) => walk(&mut section.children.nodes, bare),
118                    Node::Group(group) => walk(&mut group.children.nodes, bare),
119                    Node::Grid(grid) => walk(&mut grid.children.nodes, bare),
120                    Node::Tabs(tabs) => walk(&mut tabs.children.nodes, bare),
121                    Node::Tab(tab) => walk(&mut tab.children.nodes, bare),
122                }
123            }
124        }
125        let loads = self
126            .columns()
127            .iter()
128            .any(|column| !column.includes().into_vec().is_empty());
129        if loads {
130            walk(&mut self.nodes, bare);
131        }
132    }
133
134    /// Every column, in the order the page renders them.
135    pub(crate) fn columns(&self) -> Vec<&BoxColumn<M>> {
136        fn walk<'d, M>(nodes: &'d [Node<M>], out: &mut Vec<&'d BoxColumn<M>>) {
137            for node in nodes {
138                match node {
139                    Node::Column(column) => out.push(column),
140                    Node::Section(section) => walk(&section.children.nodes, out),
141                    Node::Group(group) => walk(&group.children.nodes, out),
142                    Node::Grid(grid) => walk(&grid.children.nodes, out),
143                    Node::Tabs(tabs) => walk(&tabs.children.nodes, out),
144                    Node::Tab(tab) => walk(&tab.children.nodes, out),
145                }
146            }
147        }
148        let mut out = Vec::new();
149        walk(&self.nodes, &mut out);
150        out
151    }
152
153    /// Load the records `query` names as a page rendering this declaration reads them: with every
154    /// relation the columns declare, once each, and the related records the columns' sources show
155    /// the user checked, so [`render`](Self::render) shows only those. Pass the resource's scope,
156    /// as in `detail.load(cx, scoped_query::<R>(cx)?)`.
157    ///
158    /// # Errors
159    ///
160    /// A source's scope refusing the request, or a database failure.
161    pub async fn load(&self, cx: &Cx, query: Query<List<M>>) -> topcoat::Result<Vec<M>>
162    where
163        M: toasty::schema::Model + Send + Sync + 'static,
164    {
165        let mut db = crate::db::db(cx)?;
166        let records = self
167            .include_relations(query)
168            .exec(&mut db)
169            .await
170            .map_err(crate::error::unavailable)?;
171        self.restrict_relations(cx, &records, &mut db).await?;
172        Ok(records)
173    }
174
175    /// Include in `query` every relation the columns declare, once each.
176    pub(crate) fn include_relations(&self, query: Query<List<M>>) -> Query<List<M>>
177    where
178        M: toasty::schema::Model,
179    {
180        include_relations(query, self.columns())
181    }
182
183    /// Check which related records `records`, loaded through [`Self::include_relations`], hold
184    /// that the columns' sources show the user.
185    pub(crate) async fn restrict_relations(
186        &self,
187        cx: &Cx,
188        records: &[M],
189        ex: &mut dyn toasty::Executor,
190    ) -> topcoat::Result<()> {
191        restrict_relations(cx, self.columns(), records, ex).await
192    }
193
194    /// The columns labelling their records by a source the context's panel cannot load from.
195    pub(crate) fn unavailable_sources(&self, cx: &Cx) -> Vec<DeclarationErrorKind> {
196        unavailable_sources(cx, self.columns().into_iter().map(|c| &**c))
197    }
198
199    /// What is wrong with this declaration: each column whose path binds no single field.
200    pub fn declaration_errors(&self) -> Vec<DeclarationErrorKind> {
201        self.columns()
202            .into_iter()
203            .filter_map(|column| column.internals().misdeclared())
204            .collect()
205    }
206
207    /// Renders `record`'s entries in their blocks: each column's [`entry`](Column::entry),
208    /// read-only.
209    ///
210    /// The panel's detail page renders its view this way. A page of the app's own renders one for
211    /// a record it loaded through [`load`](Self::load): a scoped relation column over a record
212    /// loaded otherwise shows no related record.
213    pub fn render<'a>(&self, cx: &'a Cx, record: &M) -> BoxView<'a> {
214        self.render_without(cx, record, &[])
215    }
216
217    /// Renders as [`render`](Self::render) does, without the columns reading one of the `hidden`
218    /// fields, and without a block left with no column.
219    pub(crate) fn render_without<'a>(
220        &self,
221        cx: &'a Cx,
222        record: &M,
223        hidden: &[String],
224    ) -> BoxView<'a> {
225        self.render_in(cx, record, hidden, 0)
226    }
227
228    /// Renders as [`render_without`](Self::render_without) does, inside `depth` tabs.
229    fn render_in<'a>(
230        &self,
231        cx: &'a Cx,
232        record: &M,
233        hidden: &[String],
234        depth: usize,
235    ) -> BoxView<'a> {
236        let views: Vec<BoxView<'a>> = self
237            .nodes
238            .iter()
239            .filter(|node| match node {
240                Node::Column(column) => shows(cx, &**column, hidden),
241                Node::Section(section) => {
242                    section.children.is_empty() || section.children.shows_any(cx, hidden)
243                }
244                Node::Group(group) => group.children.shows_any(cx, hidden),
245                Node::Grid(grid) => grid.children.shows_any(cx, hidden),
246                Node::Tabs(tabs) => tabs.children.shows_any(cx, hidden),
247                Node::Tab(tab) => tab.children.shows_any(cx, hidden),
248            })
249            .map(|node| match node {
250                Node::Column(column) => entry(cx, column.name(), column.entry(cx, record)),
251                Node::Section(section) => {
252                    let body = (!section.children.is_empty())
253                        .then(|| section.children.render_in(cx, record, hidden, depth));
254                    section.chrome(cx, body)
255                }
256                Node::Group(group) => {
257                    group.chrome(cx, group.children.render_in(cx, record, hidden, depth))
258                }
259                Node::Grid(grid) => {
260                    grid.chrome(cx, grid.children.render_in(cx, record, hidden, depth))
261                }
262                Node::Tabs(tabs) => {
263                    tabs.chrome(cx, tabs.children.render_tabs(cx, record, hidden, depth))
264                }
265                // A tab renders through its block, which lays out its trigger and its panel.
266                Node::Tab(tab) => tab.children.render_in(cx, record, hidden, depth),
267            })
268            .collect();
269        stack(cx, views)
270    }
271
272    /// Renders the tabs this `Tabs` block's children hold, inside `depth` tabs, the first shown
273    /// one active, without those whose every column reads a `hidden` field.
274    fn render_tabs<'a>(
275        &self,
276        cx: &'a Cx,
277        record: &M,
278        hidden: &[String],
279        depth: usize,
280    ) -> Vec<TabView<'a>> {
281        self.nodes
282            .iter()
283            .filter_map(|node| match node {
284                Node::Tab(tab) if tab.children.shows_any(cx, hidden) => Some(tab),
285                _ => None,
286            })
287            .enumerate()
288            .map(|(index, tab)| {
289                let id = tab
290                    .children
291                    .columns()
292                    .first()
293                    .map(|column| tab_id(&format!("detail-{}", column.name()), depth))
294                    .unwrap_or_default();
295                let body = tab.children.render_in(cx, record, hidden, depth + 1);
296                tab_view(cx, &id, tab.face(), index == 0, false, body)
297            })
298            .collect()
299    }
300
301    /// Whether any column shows to `cx`, which may not view the `hidden` fields.
302    fn shows_any(&self, cx: &Cx, hidden: &[String]) -> bool {
303        self.columns()
304            .into_iter()
305            .any(|column| shows(cx, &**column, hidden))
306    }
307
308    /// The names of the columns that do not say what they read.
309    pub(crate) fn undeclared_reads(&self) -> Vec<&str> {
310        self.columns()
311            .into_iter()
312            .filter(|column| column.reads().is_none())
313            .map(|column| column.name())
314            .collect()
315    }
316}
317
318/// `views` one after another. A free function, so tablo-core compiles the view once rather than
319/// once for each model with a detail page.
320fn stack<'a>(cx: &'a Cx, views: Vec<BoxView<'a>>) -> BoxView<'a> {
321    view! {
322        for v in views {
323            (v)
324        }
325    }
326    .boxed()
327}
328
329impl<M> Default for Detail<M> {
330    fn default() -> Self {
331        Self::empty()
332    }
333}
334
335impl<M> std::fmt::Debug for Detail<M> {
336    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
337        f.debug_struct("Detail")
338            .field(
339                "columns",
340                &self
341                    .columns()
342                    .into_iter()
343                    .map(|column| column.name())
344                    .collect::<Vec<_>>(),
345            )
346            .finish()
347    }
348}
349
350/// Converts a column, a layout block of columns, a tuple of either, or a [`Detail`] into a
351/// [`Detail`].
352///
353/// Every built-in column converts, as does a boxed `Arc<dyn Column<M>>`;
354/// [`Detail::column`] appends any other [`Column`]. An app's column type joins a block as the
355/// built-in ones do by implementing this trait:
356///
357/// ```rust
358/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
359/// # struct User { #[key] #[auto] id: uuid::Uuid, name: String }
360/// # struct Initials;
361/// # use topcoat::context::Cx;
362/// # impl tablo_core::extend::Column<User> for Initials {
363/// #     fn name(&self) -> &str { "initials" }
364/// #     fn label(&self) -> tablo_core::Text { "Initials".into() }
365/// #     fn text(&self, _cx: &Cx, row: &User) -> String { row.name.clone() }
366/// # }
367/// use tablo_core::{Detail, IntoDetail, Section};
368///
369/// impl IntoDetail<User> for Initials {
370///     fn into_detail(self) -> Detail<User> {
371///         Detail::empty().column(self)
372///     }
373/// }
374///
375/// Detail::new(Section::new("User").columns(Initials));
376/// ```
377pub trait IntoDetail<M> {
378    fn into_detail(self) -> Detail<M>;
379}
380
381impl<M> IntoDetail<M> for Detail<M> {
382    fn into_detail(self) -> Detail<M> {
383        self
384    }
385}
386
387impl<M> IntoDetail<M> for () {
388    fn into_detail(self) -> Detail<M> {
389        Detail::empty()
390    }
391}
392
393impl<M> IntoDetail<M> for BoxColumn<M> {
394    fn into_detail(self) -> Detail<M> {
395        Detail {
396            nodes: vec![Node::Column(self)],
397        }
398    }
399}
400
401/// The [`IntoDetail`] impls of the built-in columns: each is one column.
402macro_rules! column_details {
403    ($($ty:ident<$($g:ident),+>),+ $(,)?) => {
404        $(
405            impl<$($g),+> IntoDetail<M> for $ty<$($g),+>
406            where
407                Self: Column<M> + 'static,
408            {
409                fn into_detail(self) -> Detail<M> {
410                    Detail::empty().column(self)
411                }
412            }
413        )+
414    };
415}
416
417column_details!(
418    TextColumn<M, T>,
419    DateColumn<M, T>,
420    ComputedColumn<M>,
421    RelationColumn<M>,
422    CountColumn<M>,
423    BooleanColumn<M>,
424    FileColumn<M>,
425    ColorColumn<M, T>,
426    IconColumn<M, T>,
427    ImageColumn<M, T>,
428    ToggleColumn<M>,
429    SelectColumn<M, T>,
430    TextInputColumn<M, T>,
431    EmbeddedColumn<M, T>,
432    ListColumn<M, T>,
433    RepeaterColumn<M, T>,
434);
435#[cfg(feature = "rich-text")]
436column_details!(RichTextColumn<M, T>);
437
438/// The `columns` builders of the layout blocks: each holds a detail page's columns and blocks.
439macro_rules! block_columns {
440    ($($ty:ident),+ $(,)?) => {
441        $(
442            impl $ty<()> {
443                /// Holds a detail page's columns and blocks.
444                pub fn columns<M>(self, children: impl IntoDetail<M>) -> $ty<Detail<M>> {
445                    self.holding(children.into_detail())
446                }
447            }
448        )+
449    };
450}
451
452block_columns!(Section, Group, Grid, Tab);
453
454impl<M> HoldsTabs for Detail<M> {
455    fn push_tab(&mut self, tab: Tab<Self>) {
456        self.nodes.push(Node::Tab(Box::new(tab)));
457    }
458}
459
460impl<M> IntoDetail<M> for Tabs<Detail<M>> {
461    fn into_detail(self) -> Detail<M> {
462        Detail {
463            nodes: vec![Node::Tabs(Box::new(self))],
464        }
465    }
466}
467
468/// The single-node [`IntoDetail`] impls of every layout block, holding columns or nothing.
469macro_rules! block_details {
470    ($($ty:ident),+ $(,)?) => {
471        $(
472            impl<M> IntoDetail<M> for $ty<Detail<M>> {
473                fn into_detail(self) -> Detail<M> {
474                    Detail {
475                        nodes: vec![Node::$ty(Box::new(self))],
476                    }
477                }
478            }
479
480            impl<M> IntoDetail<M> for $ty<()> {
481                fn into_detail(self) -> Detail<M> {
482                    self.holding(Detail::empty()).into_detail()
483                }
484            }
485        )+
486    };
487}
488
489block_details!(Section, Group, Grid);
490
491/// The tuple impls of [`IntoDetail`], one arity per invocation.
492macro_rules! into_detail_tuples {
493    ($($T:ident => $v:ident),+ $(,)?) => {
494        impl<M, $($T),+> IntoDetail<M> for ($($T,)+)
495        where
496            $($T: IntoDetail<M>,)+
497        {
498            fn into_detail(self) -> Detail<M> {
499                let ($($v,)+) = self;
500                let mut detail = Detail::empty();
501                $( detail.append($v.into_detail()); )+
502                detail
503            }
504        }
505    };
506}
507
508into_detail_tuples!(A => a);
509into_detail_tuples!(A => a, B => b);
510into_detail_tuples!(A => a, B => b, C => c);
511into_detail_tuples!(A => a, B => b, C => c, D => d);
512into_detail_tuples!(A => a, B => b, C => c, D => d, E => e);
513into_detail_tuples!(A => a, B => b, C => c, D => d, E => e, F => f);
514into_detail_tuples!(A => a, B => b, C => c, D => d, E => e, F => f, G => g);
515into_detail_tuples!(
516    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h
517);
518into_detail_tuples!(
519    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i
520);
521into_detail_tuples!(
522    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j
523);
524into_detail_tuples!(
525    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j, K => k
526);
527into_detail_tuples!(
528    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j, K => k, L => l
529);
530
531#[cfg(test)]
532mod tests;