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
8use crate::{
9    DeclarationErrorKind, EmbeddedForm,
10    schema::{FieldResolver, Grid, Group, RepeaterItem, Section},
11    table::{
12        BooleanColumn, BoxColumn, Column, ComputedColumn, CountColumn, EmbeddedColumn, FileColumn,
13        RelationColumn, RepeaterColumn, TextColumn, include_relations,
14    },
15};
16
17/// The declaration of a detail page: [`Column`]s, the same ones a [`Table`](crate::Table) lists,
18/// arranged in [`Section`], [`Group`] and [`Grid`] blocks.
19///
20/// ```rust
21/// # #[derive(Debug, Clone, toasty::Model)]
22/// # struct Author { #[key] #[auto] id: uuid::Uuid, name: String }
23/// # #[derive(Debug, Clone, toasty::Model)]
24/// # struct Post {
25/// #     #[key] #[auto] id: uuid::Uuid,
26/// #     title: String,
27/// #     featured: bool,
28/// #     author_id: uuid::Uuid,
29/// #     #[belongs_to(key = author_id, references = id)]
30/// #     author: toasty::Deferred<Author>,
31/// # }
32/// use tablo_core::{BooleanColumn, Detail, RelationColumn, Section, TextColumn, lens, relation};
33///
34/// Detail::new(Section::new("Post").columns((
35///     TextColumn::new(lens!(Post.title)),
36///     BooleanColumn::new(lens!(Post.featured)),
37///     RelationColumn::new(relation!(Post.author), |a: &Author| a.name.clone()),
38/// )));
39/// ```
40///
41/// The detail page loads the relations its columns declare ([`Column::includes`]) and renders each
42/// column's [`entry`](Column::entry) for the record.
43pub struct Detail<M> {
44    nodes: Vec<Node<M>>,
45}
46
47/// One block or column of a [`Detail`].
48enum Node<M> {
49    Column(BoxColumn<M>),
50    Section(Box<Section<Detail<M>>>),
51    Group(Box<Group<Detail<M>>>),
52    Grid(Box<Grid<Detail<M>>>),
53}
54
55impl<M> Detail<M> {
56    /// Declare a detail page of `children`: columns and blocks of them.
57    pub fn new(children: impl IntoDetail<M>) -> Self {
58        children.into_detail()
59    }
60
61    /// A detail page showing nothing, which turns the page off.
62    pub fn empty() -> Self {
63        Self { nodes: Vec::new() }
64    }
65
66    /// Whether the declaration shows nothing.
67    pub fn is_empty(&self) -> bool {
68        self.nodes.is_empty()
69    }
70
71    /// Append `column` after the declared columns and blocks.
72    pub fn column(mut self, column: impl Column<M> + 'static) -> Self {
73        self.nodes.push(Node::Column(Arc::new(column)));
74        self
75    }
76
77    /// Appends `other`'s columns and blocks after this one's.
78    fn append(&mut self, other: Self) {
79        self.nodes.extend(other.nodes);
80    }
81
82    /// Every column, in the order the page renders them.
83    fn columns(&self) -> Vec<&BoxColumn<M>> {
84        fn walk<'d, M>(nodes: &'d [Node<M>], out: &mut Vec<&'d BoxColumn<M>>) {
85            for node in nodes {
86                match node {
87                    Node::Column(column) => out.push(column),
88                    Node::Section(section) => walk(&section.children.nodes, out),
89                    Node::Group(group) => walk(&group.children.nodes, out),
90                    Node::Grid(grid) => walk(&grid.children.nodes, out),
91                }
92            }
93        }
94        let mut out = Vec::new();
95        walk(&self.nodes, &mut out);
96        out
97    }
98
99    /// Include in `query` every relation the columns declare, once each: what a page rendering
100    /// this declaration loads, as in `detail.include_relations(scoped_query::<R>(cx)?)`.
101    pub fn include_relations(&self, query: Query<List<M>>) -> Query<List<M>>
102    where
103        M: toasty::schema::Model,
104    {
105        include_relations(query, self.columns())
106    }
107
108    /// The columns labelling their records by a source the context's panel cannot load from.
109    pub(crate) fn unavailable_sources(&self, cx: &Cx) -> Vec<DeclarationErrorKind> {
110        crate::table::column::unavailable_sources(cx, self.columns().into_iter().map(|c| &**c))
111    }
112
113    /// What is wrong with this declaration: each column whose path binds no single field.
114    pub fn declaration_errors(&self) -> Vec<DeclarationErrorKind> {
115        self.columns()
116            .into_iter()
117            .filter_map(|column| column.misdeclared())
118            .collect()
119    }
120
121    /// Binds the declaration's embedded paths and values to `db`'s app schema.
122    ///
123    /// A panel binds the detail pages it mounts; bind one built outside a panel so its
124    /// [`declaration_errors`](Self::declaration_errors) reports no unbound path. A detail page with
125    /// no embedded path or value is bound from the start.
126    pub fn bind(self, db: &toasty::Db) -> Self {
127        self.bind_with(&FieldResolver::of_db(db));
128        self
129    }
130
131    /// Binds every column through `resolver`.
132    pub(crate) fn bind_with(&self, resolver: &FieldResolver) {
133        for column in self.columns() {
134            column.bind(resolver);
135        }
136    }
137
138    /// Renders `record`'s entries in their blocks: each column's [`entry`](Column::entry),
139    /// read-only.
140    ///
141    /// The panel's detail page renders its view this way. A page of the app's own renders one for
142    /// a record it loaded through [`include_relations`](Self::include_relations), after
143    /// [`bind`](Self::bind) when it shows an embedded value.
144    pub fn render<'a>(&self, cx: &'a Cx, record: &M) -> BoxView<'a> {
145        let views: Vec<BoxView<'a>> = self
146            .nodes
147            .iter()
148            .map(|node| match node {
149                Node::Column(column) => column.entry(cx, record),
150                Node::Section(section) => {
151                    let body =
152                        (!section.children.is_empty()).then(|| section.children.render(cx, record));
153                    section.chrome(cx, body)
154                }
155                Node::Group(group) => group.chrome(cx, group.children.render(cx, record)),
156                Node::Grid(grid) => grid.chrome(cx, grid.children.render(cx, record)),
157            })
158            .collect();
159        view! {
160            cx =>
161            for v in views {
162                (v)
163            }
164        }
165        .boxed()
166    }
167}
168
169impl<M> Default for Detail<M> {
170    fn default() -> Self {
171        Self::empty()
172    }
173}
174
175impl<M> std::fmt::Debug for Detail<M> {
176    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
177        f.debug_struct("Detail")
178            .field(
179                "columns",
180                &self
181                    .columns()
182                    .into_iter()
183                    .map(|column| column.name())
184                    .collect::<Vec<_>>(),
185            )
186            .finish()
187    }
188}
189
190/// Converts a column, a layout block of columns, a tuple of either, or a [`Detail`] into a
191/// [`Detail`].
192///
193/// Every built-in column converts, as does a boxed `Arc<dyn Column<M>>`;
194/// [`Detail::column`] appends any other [`Column`]. An app's column type joins a block as the
195/// built-in ones do by implementing this trait:
196///
197/// ```rust
198/// # #[derive(Debug, Clone, toasty::Model)]
199/// # struct User { #[key] #[auto] id: uuid::Uuid, name: String }
200/// # struct Initials;
201/// # use topcoat::context::Cx;
202/// # impl tablo_core::extend::Column<User> for Initials {
203/// #     fn name(&self) -> &str { "initials" }
204/// #     fn label(&self) -> &str { "Initials" }
205/// #     fn text(&self, _cx: &Cx, row: &User) -> String { row.name.clone() }
206/// # }
207/// use tablo_core::{Detail, IntoDetail, Section};
208///
209/// impl IntoDetail<User> for Initials {
210///     fn into_detail(self) -> Detail<User> {
211///         Detail::empty().column(self)
212///     }
213/// }
214///
215/// Detail::new(Section::new("User").columns(Initials));
216/// ```
217pub trait IntoDetail<M> {
218    fn into_detail(self) -> Detail<M>;
219}
220
221impl<M> IntoDetail<M> for Detail<M> {
222    fn into_detail(self) -> Detail<M> {
223        self
224    }
225}
226
227impl<M> IntoDetail<M> for () {
228    fn into_detail(self) -> Detail<M> {
229        Detail::empty()
230    }
231}
232
233impl<M> IntoDetail<M> for BoxColumn<M> {
234    fn into_detail(self) -> Detail<M> {
235        Detail {
236            nodes: vec![Node::Column(self)],
237        }
238    }
239}
240
241impl<M, T> IntoDetail<M> for TextColumn<M, T>
242where
243    M: toasty::schema::Model + Send + Sync + 'static,
244    T: Send + Sync + 'static,
245{
246    fn into_detail(self) -> Detail<M> {
247        Detail::empty().column(self)
248    }
249}
250
251impl<M, T> IntoDetail<M> for EmbeddedColumn<M, T>
252where
253    M: toasty::schema::Model + Send + Sync + 'static,
254    T: EmbeddedForm + Send + Sync + 'static,
255{
256    fn into_detail(self) -> Detail<M> {
257        Detail::empty().column(self)
258    }
259}
260
261impl<M, T> IntoDetail<M> for RepeaterColumn<M, T>
262where
263    M: toasty::schema::Model + Send + Sync + 'static,
264    T: RepeaterItem,
265{
266    fn into_detail(self) -> Detail<M> {
267        Detail::empty().column(self)
268    }
269}
270
271/// The [`IntoDetail`] impls of the built-in columns over the model alone.
272macro_rules! column_details {
273    ($($ty:ident),+ $(,)?) => {
274        $(
275            impl<M> IntoDetail<M> for $ty<M>
276            where
277                M: toasty::schema::Model + Send + Sync + 'static,
278            {
279                fn into_detail(self) -> Detail<M> {
280                    Detail::empty().column(self)
281                }
282            }
283        )+
284    };
285}
286
287column_details!(
288    BooleanColumn,
289    ComputedColumn,
290    CountColumn,
291    FileColumn,
292    RelationColumn
293);
294
295/// The single-node [`IntoDetail`] impls of every layout block, holding columns or nothing.
296macro_rules! block_details {
297    ($($ty:ident),+ $(,)?) => {
298        $(
299            impl<M> IntoDetail<M> for $ty<Detail<M>> {
300                fn into_detail(self) -> Detail<M> {
301                    Detail {
302                        nodes: vec![Node::$ty(Box::new(self))],
303                    }
304                }
305            }
306
307            impl<M> IntoDetail<M> for $ty<()> {
308                fn into_detail(self) -> Detail<M> {
309                    self.holding(Detail::empty()).into_detail()
310                }
311            }
312        )+
313    };
314}
315
316block_details!(Section, Group, Grid);
317
318/// The tuple impls of [`IntoDetail`], one arity per invocation.
319macro_rules! into_detail_tuples {
320    ($($T:ident => $v:ident),+ $(,)?) => {
321        impl<M, $($T),+> IntoDetail<M> for ($($T,)+)
322        where
323            $($T: IntoDetail<M>,)+
324        {
325            fn into_detail(self) -> Detail<M> {
326                let ($($v,)+) = self;
327                let mut detail = Detail::empty();
328                $( detail.append($v.into_detail()); )+
329                detail
330            }
331        }
332    };
333}
334
335into_detail_tuples!(A => a);
336into_detail_tuples!(A => a, B => b);
337into_detail_tuples!(A => a, B => b, C => c);
338into_detail_tuples!(A => a, B => b, C => c, D => d);
339into_detail_tuples!(A => a, B => b, C => c, D => d, E => e);
340into_detail_tuples!(A => a, B => b, C => c, D => d, E => e, F => f);
341into_detail_tuples!(A => a, B => b, C => c, D => d, E => e, F => f, G => g);
342into_detail_tuples!(
343    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h
344);
345into_detail_tuples!(
346    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i
347);
348into_detail_tuples!(
349    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j
350);
351into_detail_tuples!(
352    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j, K => k
353);
354into_detail_tuples!(
355    A => a, B => b, C => c, D => d, E => e, F => f, G => g, H => h, I => i, J => j, K => k, L => l
356);
357
358#[cfg(test)]
359mod tests;