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