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