Skip to main content

tablo_core/
column.rs

1//! Columns, which a [`Table`](crate::Table) lists and a [`Detail`](crate::Detail) shows: the
2//! [`Column`] trait, the built-in [`TextColumn`], [`DateColumn`], [`ComputedColumn`],
3//! [`RelationColumn`], [`CountColumn`], [`BooleanColumn`], [`FileColumn`], [`ColorColumn`],
4//! [`ImageColumn`], [`IconColumn`], [`EmbeddedColumn`], [`ListColumn`], [`RepeaterColumn`] and
5//! [`RichTextColumn`], the [`ToggleColumn`], [`SelectColumn`] and [`TextInputColumn`] that edit
6//! their field in a table's cell, and the [`IntoColumns`] seam.
7//!
8//! The module sits below both: `table` re-exports it, and `detail` implements
9//! [`IntoDetail`](crate::IntoDetail) for each built-in column.
10
11use std::sync::Arc;
12
13use derive_where::derive_where;
14use toasty::stmt::{Expr, List, OrderByExpr, Path, Query};
15use topcoat::{context::Cx, view::*};
16
17use crate::{display::Alignment, schema::Binding};
18
19/// The `label` and `width` builders of a column over a [`ColumnBase`], inside its inherent impl.
20macro_rules! base_builders {
21    () => {
22        /// Replace the label the field's name gives it.
23        pub fn label(mut self, label: impl Into<$crate::Text>) -> Self {
24            self.base.label = Some(label.into());
25            self
26        }
27
28        /// Declare this column's width.
29        pub fn width(mut self, width: $crate::column::ColumnWidth) -> Self {
30            self.base.width = width;
31            self
32        }
33
34        toggle_builders!(base.toggle);
35    };
36    (sortable) => {
37        base_builders!();
38
39        /// Make the header a sort link.
40        pub fn sortable(mut self) -> Self {
41            self.sortable = true;
42            self
43        }
44    };
45}
46
47/// The `toggleable` and `hidden_by_default` builders of a column holding its [`ColumnToggle`] at
48/// the field path given, inside its inherent impl.
49macro_rules! toggle_builders {
50    ($($field:ident).+) => {
51        /// Let each reader hide this column from their list; it starts shown.
52        pub fn toggleable(mut self) -> Self {
53            self.$($field).+ = $crate::column::ColumnToggle::ShownByDefault;
54            self
55        }
56
57        /// Let each reader show this column in their list; it starts hidden.
58        pub fn hidden_by_default(mut self) -> Self {
59            self.$($field).+ = $crate::column::ColumnToggle::HiddenByDefault;
60            self
61        }
62    };
63}
64
65/// The [`Column`] methods a column over a [`ColumnBase`] answers from it, inside its `Column`
66/// impl, and the accessor to its [`ColumnInternals`] impl; `reads` leaves `reads` to the column.
67macro_rules! base_column_methods {
68    () => {
69        base_column_methods!(reads);
70
71        fn reads(&self) -> Option<$crate::lens::Reads<M>> {
72            Some(self.base.reads())
73        }
74    };
75    (reads) => {
76        fn name(&self) -> &str {
77            self.base.name()
78        }
79
80        fn label(&self) -> $crate::Text {
81            self.base.label()
82        }
83
84        fn column_width(&self) -> $crate::column::ColumnWidth {
85            self.base.width
86        }
87
88        fn toggle(&self) -> $crate::column::ColumnToggle {
89            self.base.toggle
90        }
91
92        fn internals(&self) -> &dyn $crate::column::ColumnInternals<M> {
93            self
94        }
95    };
96}
97
98/// The [`ColumnInternals`] methods a column over a [`ColumnBase`] answers from it, inside its
99/// `ColumnInternals` impl: it reads no related record; `binding` adds the binding's
100/// `misdeclared`, and `binding, related` leaves `related` to the column.
101macro_rules! base_internals {
102    () => {
103        fn related(&self) -> $crate::column::Related {
104            $crate::column::Related::None
105        }
106    };
107    (binding) => {
108        base_internals!();
109        base_internals!(binding, related);
110    };
111    (binding, related) => {
112        fn misdeclared(&self) -> Option<$crate::DeclarationErrorKind> {
113            self.base.binding.misdeclared()
114        }
115    };
116}
117
118/// A closure reading a value off a row or a field: a cell's color, icon, link or tooltip.
119pub(crate) type ReadFn<T, V> = Arc<dyn Fn(&T) -> V + Send + Sync>;
120
121fn sort_order<M, T>(path: &Path<M, T>, descending: bool) -> OrderByExpr {
122    if descending {
123        path.clone().desc()
124    } else {
125        path.clone().asc()
126    }
127}
128
129mod related;
130pub use related::Related;
131mod bare;
132mod boolean;
133mod cell;
134mod color;
135mod computed;
136mod date;
137mod editable;
138mod embedded;
139mod file;
140mod icon;
141mod image;
142mod into_columns;
143mod list;
144mod relation;
145mod repeater;
146#[cfg(feature = "rich-text")]
147mod rich_text;
148mod text;
149
150pub(crate) use bare::{bare_column, reads_own_fields};
151pub use boolean::BooleanColumn;
152pub(crate) use cell::link_href;
153pub use color::ColorColumn;
154pub(crate) use color::is_css_color;
155pub use computed::ComputedColumn;
156pub use date::{DateColumn, DateValue};
157pub(crate) use editable::Editor;
158pub use editable::{SelectColumn, TextInputColumn, ToggleColumn};
159pub use embedded::EmbeddedColumn;
160pub use file::FileColumn;
161pub use icon::IconColumn;
162pub use image::ImageColumn;
163pub use into_columns::IntoColumns;
164pub use list::ListColumn;
165pub use relation::{CountColumn, RelationColumn, RelationLens, ToOneRelation};
166pub use repeater::RepeaterColumn;
167#[cfg(feature = "rich-text")]
168pub use rich_text::RichTextColumn;
169pub(crate) use sealed::ColumnInternals;
170#[cfg(test)]
171use text::escape_like_pattern;
172pub(crate) use text::{LIKE_ESCAPE, escape_like};
173pub use text::{TextColumn, contains_expr};
174
175mod sealed {
176    use topcoat::{context::Cx, view::BoxView};
177
178    use super::{Editor, Related};
179
180    /// The [`Column`](super::Column) methods only the framework answers, through
181    /// [`Column::internals`](super::Column::internals). It lives in a private module, so a column
182    /// outside tablo cannot implement it and answers the [`Defaults`].
183    pub trait ColumnInternals<M>: Send + Sync {
184        /// Check which related records the column's includes loaded into `rows` the related
185        /// resource shows the user, before any cell renders: a cell shows only those. Every
186        /// loader that applies the includes runs it through `ex`, the executor that loaded the
187        /// rows.
188        fn restrict<'a>(
189            &'a self,
190            _cx: &'a Cx,
191            _rows: &'a [M],
192            _ex: &'a mut dyn toasty::Executor,
193        ) -> crate::BoxFuture<'a, topcoat::Result<()>> {
194            Box::pin(async { Ok(()) })
195        }
196
197        /// Whether the column shows a field of a related record that the related resource's
198        /// [`field_access`](crate::ResourceDef::field_access) hides from `cx`: a panel drops it
199        /// then, as it drops one reading a hidden field of the row.
200        fn masked(&self, _cx: &Cx) -> bool {
201            false
202        }
203
204        /// What the column reads of the related records it loads: see [`Related`]. A column
205        /// answers [`Related::Row`] unless it says otherwise, since a closure reading its row may
206        /// reach any relation the page loads.
207        fn related(&self) -> Related {
208            Related::Row
209        }
210
211        /// What is wrong with this column's declaration.
212        fn misdeclared(&self) -> Option<crate::DeclarationErrorKind> {
213            None
214        }
215
216        /// The source the column labels its records by, when the context's panel cannot load
217        /// from it.
218        fn unavailable_source(&self, _cx: &Cx) -> Option<&'static str> {
219            None
220        }
221
222        /// The control the column edits its field with in a table's cell, if it does: the mount
223        /// checks the resource's form edits the field the column names with it.
224        fn edits(&self) -> Option<Editor> {
225            None
226        }
227
228        /// The options a [`Editor::Select`] offers, each a value and its label: the mount checks
229        /// the form's choice offers the same.
230        fn edit_options(&self) -> Vec<(String, crate::Text)> {
231            Vec::new()
232        }
233
234        /// The control editing `row`'s field, posting the field's form key, which a table renders
235        /// in place of the cell where the user may change the field. `None` renders the cell.
236        fn editor<'a>(&self, _cx: &'a Cx, _row: &M) -> Option<BoxView<'a>> {
237            None
238        }
239    }
240
241    /// What a column outside tablo answers.
242    pub struct Defaults;
243
244    impl<M> ColumnInternals<M> for Defaults {}
245}
246
247/// One column of a table or a [`Detail`](crate::Detail) declares its label, its value read off
248/// the record, and its query predicates.
249///
250/// ```rust
251/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
252/// # struct User { #[key] #[auto] id: uuid::Uuid, name: String }
253/// # use tablo_core::{Text, extend::Column};
254/// # use topcoat::context::Cx;
255/// struct Initials;
256///
257/// impl Column<User> for Initials {
258///     fn name(&self) -> &str {
259///         "initials"
260///     }
261///     fn label(&self) -> Text {
262///         "Initials".into()
263///     }
264///     fn text(&self, _cx: &Cx, row: &User) -> String {
265///         row.name
266///             .split_whitespace()
267///             .filter_map(|w| w.chars().next())
268///             .collect()
269///     }
270/// }
271/// ```
272///
273/// Only [`name`](Self::name), [`label`](Self::label) and
274/// [`text`](Self::text) are required.
275pub trait Column<M>: Send + Sync {
276    /// The column's identifier, distinct within its table.
277    fn name(&self) -> &str;
278
279    /// The header text.
280    fn label(&self) -> crate::Text;
281
282    /// The row's value as plain text.
283    fn text(&self, cx: &Cx, row: &M) -> String;
284
285    /// The row's table cell.
286    fn cell<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
287        crate::schema::value_cell(cx, &self.text(cx, row))
288    }
289
290    /// The record's entry on a detail page: the [`label`](Self::label) over the
291    /// [`cell`](Self::cell).
292    fn entry<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
293        crate::schema::read_only(cx, &self.label(), self.cell(cx, row))
294    }
295
296    /// The width the column claims.
297    fn column_width(&self) -> ColumnWidth {
298        ColumnWidth::Auto
299    }
300
301    /// Whether each reader may hide or show the column in their list, and how it starts. A
302    /// detail page shows every column.
303    fn toggle(&self) -> ColumnToggle {
304        ColumnToggle::Fixed
305    }
306
307    /// Where the header and the cells align their content.
308    fn alignment(&self) -> Alignment {
309        Alignment::Start
310    }
311
312    /// The predicate a search for `term` adds, or `None` when the column does not join the table's
313    /// search. A column that joins it answers `Some` for every non-empty, trimmed `term`.
314    fn search_expr(&self, _term: &str) -> Option<Expr<bool>> {
315        None
316    }
317
318    /// The ordering a sort on this column applies, or `None` when the header does not link to a
319    /// sort on it. A sortable column answers `Some` either way.
320    fn order_by(&self, _descending: bool) -> Option<OrderByExpr> {
321        None
322    }
323
324    /// The relations [`text`](Self::text), [`cell`](Self::cell) and [`entry`](Self::entry) read.
325    fn includes(&self) -> Includes<M> {
326        Includes::new()
327    }
328
329    /// The fields of the row [`text`](Self::text), [`cell`](Self::cell),
330    /// [`entry`](Self::entry), [`search_expr`](Self::search_expr) and
331    /// [`order_by`](Self::order_by) read, or `None` when the column does not say.
332    ///
333    /// A panel drops a column reading a field
334    /// [`field_access`](crate::ResourceDef::field_access) hides from the user, and refuses to
335    /// mount a panel restricting a field whose columns answer `None`. The built-in columns
336    /// answer their own field.
337    fn reads(&self) -> Option<crate::lens::Reads<M>> {
338        None
339    }
340
341    /// The framework's own column methods. The trait is sealed: a column outside tablo answers
342    /// their defaults.
343    #[doc(hidden)]
344    fn internals(&self) -> &dyn ColumnInternals<M> {
345        &sealed::Defaults
346    }
347}
348
349/// The relations a [`Column`] reads off its row.
350#[derive_where(Clone, Debug, Default)]
351pub struct Includes<M>(
352    Vec<crate::toasty_compat::UntypedInclude>,
353    #[derive_where(skip(Debug))] std::marker::PhantomData<fn() -> M>,
354);
355
356impl<M> Includes<M> {
357    /// No relation.
358    pub fn new() -> Self {
359        Self::default()
360    }
361
362    /// Add `relation`.
363    pub fn with<T>(mut self, relation: impl Into<toasty::stmt::Include<M, T>>) -> Self {
364        let include: crate::toasty_compat::UntypedInclude = relation.into().into();
365        if !self.0.contains(&include) {
366            self.0.push(include);
367        }
368        self
369    }
370
371    /// The relations, in the order they were added.
372    pub(crate) fn into_vec(self) -> Vec<crate::toasty_compat::UntypedInclude> {
373        self.0
374    }
375}
376
377/// One [`UnregisteredLabelSource`](DeclarationErrorKind::UnregisteredLabelSource) per column
378/// labelling its records by a source `cx`'s panel cannot load from.
379pub(crate) fn unavailable_sources<'c, M: 'c>(
380    cx: &Cx,
381    columns: impl IntoIterator<Item = &'c dyn Column<M>>,
382) -> Vec<crate::DeclarationErrorKind> {
383    columns
384        .into_iter()
385        .filter_map(|column| {
386            column.internals().unavailable_source(cx).map(|source| {
387                crate::DeclarationErrorKind::UnregisteredLabelSource {
388                    column: column.name().to_string(),
389                    source,
390                }
391            })
392        })
393        .collect()
394}
395
396pub(crate) fn include_relations<'c, M>(
397    mut query: Query<List<M>>,
398    columns: impl IntoIterator<Item = &'c BoxColumn<M>>,
399) -> Query<List<M>>
400where
401    M: toasty::schema::Model + 'c,
402{
403    let mut seen: Vec<crate::toasty_compat::UntypedInclude> = Vec::new();
404    for include in columns.into_iter().flat_map(|c| c.includes().into_vec()) {
405        if !seen.contains(&include) {
406            query = query.include(include.clone());
407            seen.push(include);
408        }
409    }
410    query
411}
412
413/// Run each column's [`restrict`](ColumnInternals::restrict) over `rows`, which a query through
414/// [`include_relations`] loaded.
415pub(crate) async fn restrict_relations<'c, M>(
416    cx: &Cx,
417    columns: impl IntoIterator<Item = &'c BoxColumn<M>>,
418    rows: &[M],
419    ex: &mut dyn toasty::Executor,
420) -> topcoat::Result<()>
421where
422    M: 'c,
423{
424    for column in columns {
425        column.internals().restrict(cx, rows, &mut *ex).await?;
426    }
427    Ok(())
428}
429
430/// What a column over one field declares beside its lens: the field's binding, the declared label
431/// and the width.
432#[derive(Clone, Debug)]
433struct ColumnBase {
434    binding: Binding,
435    /// The declared label, over the binding's.
436    label: Option<crate::Text>,
437    width: ColumnWidth,
438    toggle: ColumnToggle,
439}
440
441impl ColumnBase {
442    /// Bind `path`, with no declared label.
443    fn new<M, T>(path: &Path<M, T>, width: ColumnWidth) -> Self
444    where
445        M: toasty::schema::Model,
446    {
447        Self {
448            binding: Binding::of(path),
449            label: None,
450            width,
451            toggle: ColumnToggle::Fixed,
452        }
453    }
454
455    fn name(&self) -> &str {
456        self.binding.name()
457    }
458
459    fn label(&self) -> crate::Text {
460        self.label.clone().unwrap_or_else(|| self.binding.label())
461    }
462
463    /// The field the column reads.
464    fn reads<M>(&self) -> crate::lens::Reads<M> {
465        self.binding.reads()
466    }
467}
468
469/// How a [`Column`] takes the table's width.
470///
471/// The table lays out automatically: each column is as wide as its content, and the first column
472/// that [`Grow`](Self::Grow)s takes the width left over. A table wider than its card scrolls
473/// sideways.
474#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
475#[non_exhaustive]
476pub enum ColumnWidth {
477    /// As wide as its content, on one line: a status, date, number or boolean.
478    #[default]
479    Auto,
480    /// As wide as its content up to 20rem, past which a value truncates; the first growing column
481    /// also takes the table's spare width. The default for text and relation columns.
482    Grow,
483    /// Wrap the content onto several lines, between 12rem and 28rem wide.
484    Wrap,
485    /// Exactly this many rem; a value past that truncates.
486    Rem(u8),
487}
488
489/// Whether each reader may hide a [`Column`] from their list, and whether it starts shown.
490///
491/// A list with a toggleable column renders a Columns menu in its toolbar, whose Apply keeps the
492/// checked columns and Reset restores the defaults. A panel keeps each signed-in user's choice per
493/// resource, in the [`ColumnPreference`](crate::auth::ColumnPreference) model, so the relation
494/// tables over a resource show what its list shows; a column added later, or whose default changes,
495/// takes its default until the reader picks otherwise. Search, sort and export still read every
496/// column. A panel without auth, and a table a page renders with
497/// [`wired_table`](crate::wired_table), render no menu and show the defaults.
498///
499/// ```rust
500/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
501/// # struct Post { #[key] #[auto] id: uuid::Uuid, title: String, slug: String }
502/// use tablo_core::{TextColumn, lens};
503///
504/// TextColumn::new(lens!(Post.title)).toggleable();
505/// TextColumn::new(lens!(Post.slug)).hidden_by_default();
506/// ```
507#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
508pub enum ColumnToggle {
509    /// Always shown.
510    #[default]
511    Fixed,
512    /// Shown until the reader hides it: `toggleable()`.
513    ShownByDefault,
514    /// Hidden until the reader shows it: `hidden_by_default()`.
515    HiddenByDefault,
516}
517
518impl ColumnToggle {
519    /// Whether the reader may hide or show the column.
520    pub(crate) fn is_toggleable(self) -> bool {
521        self != Self::Fixed
522    }
523}
524
525impl ColumnWidth {
526    /// The classes of the box a body cell wraps its content in, if any.
527    pub(crate) fn content_class(self) -> Option<&'static str> {
528        match self {
529            Self::Auto => None,
530            Self::Grow => Some("max-w-xs truncate"),
531            Self::Wrap => Some("min-w-48 max-w-md whitespace-normal"),
532            Self::Rem(_) => Some("truncate"),
533        }
534    }
535
536    /// The `style` of a cell of this width, header and body alike.
537    pub(crate) fn style(self) -> Option<String> {
538        match self {
539            Self::Rem(rem) => Some(format!("width: {rem}rem; max-width: {rem}rem")),
540            Self::Auto | Self::Grow | Self::Wrap => None,
541        }
542    }
543}
544
545/// Whether `column` joins a table's search: it answers a term, as it answers every one then.
546pub(crate) fn searches<M>(column: &dyn Column<M>) -> bool {
547    column.search_expr("a").is_some()
548}
549
550/// Whether a table's header links to a sort on `column`.
551pub(crate) fn sorts<M>(column: &dyn Column<M>) -> bool {
552    column.order_by(false).is_some()
553}
554
555/// A table's columns, as the table stores them.
556pub(crate) type BoxColumn<M> = Arc<dyn Column<M>>;
557
558/// Whether `column` shows to `cx`, which may not view the row's `hidden` fields: it reads none of
559/// them, and no field of a related record its resource hides from `cx`.
560pub(crate) fn shows<M>(cx: &Cx, column: &dyn Column<M>, hidden: &[String]) -> bool {
561    crate::lens::shows(column.reads(), hidden) && !column.internals().masked(cx)
562}
563
564#[cfg(test)]
565mod tests;