Skip to main content

tablo_core/table/
tab.rs

1//! List tabs: the [`ListTab`]s a table offers above its rows, the one `?tab=` picks, and the
2//! counts their badges show.
3
4use derive_where::derive_where;
5use toasty::stmt::{Expr, IntoExpr, List, Path, Query};
6use topcoat::{Result, context::Cx};
7
8use super::{Table, TableState};
9use crate::{DeclarationErrorKind, lens::Reads, schema::Options, values::FormScalar};
10
11/// One tab a table offers above its rows: a preset predicate the reader picks with
12/// `?tab=<key>`, beside the search and the filters it combines with.
13///
14/// The first tab is the one a list without `?tab=` shows, so a list whose first tab narrows its
15/// rows opens on that tab. A `?tab=` naming no tab the table declares is never dropped silently:
16/// the list shows its warning banner, and the export and a selection of every matching record
17/// refuse it, as they do an unknown filter.
18///
19/// A tab narrows what the list shows; it is not access control. A tab whose query reads a field
20/// [`field_access`](crate::ResourceDef::field_access) hides from the user does not show to them:
21/// the panel reads the fields off the query.
22///
23/// ```rust
24/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
25/// # struct Post { #[key] #[auto] id: uuid::Uuid, title: String, published: bool }
26/// use tablo_core::{ListTab, Table, TextColumn, lens};
27///
28/// Table::new(TextColumn::new(lens!(Post.title)))
29///     .tab(ListTab::new("all", "All"))
30///     .tab(
31///         ListTab::new("drafts", "Drafts")
32///             .query(Post::fields().published().eq(false))
33///             .badge(),
34///     );
35/// ```
36#[derive_where(Clone)]
37pub struct ListTab<M> {
38    key: String,
39    label: crate::Text,
40    /// The rows the tab admits; `None` admits every row.
41    query: Option<Expr<bool>>,
42    /// Whether the tab shows how many rows it holds.
43    badge: bool,
44    _model: std::marker::PhantomData<fn() -> M>,
45}
46
47impl<M> ListTab<M> {
48    /// A tab admitting every row, whose `?tab=` value is `key` and whose link reads `label`.
49    pub fn new(key: impl Into<String>, label: impl Into<crate::Text>) -> Self {
50        Self {
51            key: key.into(),
52            label: label.into(),
53            query: None,
54            badge: false,
55            _model: std::marker::PhantomData,
56        }
57    }
58
59    /// Admit only the rows `predicate` selects.
60    pub fn query(mut self, predicate: Expr<bool>) -> Self {
61        self.query = Some(predicate);
62        self
63    }
64
65    /// Show how many rows the tab holds under the list's search and filters: one count query
66    /// per badged tab, each time the list renders.
67    pub fn badge(mut self) -> Self {
68        self.badge = true;
69        self
70    }
71
72    /// The `?tab=` value.
73    pub(crate) fn key(&self) -> &str {
74        &self.key
75    }
76
77    /// The link's label.
78    pub(crate) fn label(&self) -> crate::Text {
79        self.label.clone()
80    }
81
82    /// Whether the tab shows its count.
83    pub(crate) fn has_badge(&self) -> bool {
84        self.badge
85    }
86
87    /// The fields the tab's query reads off a row; a tab with no query reads nothing.
88    pub(crate) fn reads(&self) -> Reads<M>
89    where
90        M: toasty::schema::Model,
91    {
92        self.query
93            .as_ref()
94            .map_or_else(Reads::nothing, Reads::of_predicate)
95    }
96}
97
98impl<M> Table<M> {
99    /// Offer `tab` above the rows, after the declared ones.
100    pub fn tab(mut self, tab: ListTab<M>) -> Self {
101        self.tabs.push(tab);
102        self
103    }
104
105    /// Offer a tab admitting every row, keyed `all`, then a tab per option of the [`Options`]
106    /// field `lens` binds, in declaration order, after the declared ones. An option's tab is keyed
107    /// by its value and labelled by its label, admits the rows holding it, and shows their count.
108    ///
109    /// ```rust
110    /// # #[derive(Debug, Clone, Copy, PartialEq, Eq, toasty::Embed, tablo_core::Options)]
111    /// # enum Status { Draft, Published }
112    /// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
113    /// # struct Post { #[key] #[auto] id: uuid::Uuid, title: String, status: Status }
114    /// use tablo_core::{Table, TextColumn, lens};
115    ///
116    /// // `?tab=all`, `?tab=draft` and `?tab=published`.
117    /// Table::new(TextColumn::new(lens!(Post.title))).option_tabs(lens!(Post.status));
118    /// ```
119    ///
120    /// An option whose value the field's type does not parse is a declaration error, since its
121    /// tab would admit no row.
122    pub fn option_tabs<T>(mut self, lens: impl Into<Path<M, T>>) -> Self
123    where
124        T: Options + FormScalar + IntoExpr<T>,
125    {
126        let path: Path<M, T> = lens.into();
127        self.tabs
128            .push(ListTab::new("all", crate::text!("tablo-list-tab-all")));
129        for (value, label) in T::options() {
130            match T::parse_form(&value) {
131                Ok(option) => self.tabs.push(
132                    ListTab::new(value, label)
133                        .query(path.clone().eq(option))
134                        .badge(),
135                ),
136                Err(_) => self
137                    .misdeclared
138                    .push(DeclarationErrorKind::UnparsedTabOption { value }),
139            }
140        }
141        self
142    }
143
144    /// The declared tabs, in order.
145    pub(crate) fn declared_tabs(&self) -> &[ListTab<M>] {
146        &self.tabs
147    }
148
149    /// The tab `state` shows: the one its `?tab=` names, else the first. `None` when the table
150    /// declares no tab, or `?tab=` names none it declares.
151    pub(crate) fn active_tab(&self, state: &TableState) -> Option<&ListTab<M>> {
152        match &state.tab {
153            Some(key) => self.tabs.iter().find(|tab| tab.key == *key),
154            None => self.tabs.first(),
155        }
156    }
157
158    /// `state`'s `?tab=` when it names no tab the table declares.
159    pub(crate) fn unknown_tab<'s>(&self, state: &'s TableState) -> Option<&'s str> {
160        state
161            .tab
162            .as_deref()
163            .filter(|key| self.tabs.iter().all(|tab| tab.key != *key))
164    }
165
166    /// Narrow `query` to the rows the tab `state` shows admits.
167    pub(super) fn narrow_tab(&self, query: Query<List<M>>, state: &TableState) -> Query<List<M>>
168    where
169        M: toasty::schema::Model,
170    {
171        match self.active_tab(state).and_then(|tab| tab.query.clone()) {
172            Some(predicate) => query.filter(predicate),
173            None => query,
174        }
175    }
176
177    /// How many rows of `query` each badged tab holds under `state`'s search and filters, but the
178    /// `refused` filters: one count query per badged tab, by key, in declaration order.
179    ///
180    /// # Errors
181    ///
182    /// The database failing a count.
183    pub(crate) async fn tab_counts(
184        &self,
185        cx: &Cx,
186        query: &Query<List<M>>,
187        state: &TableState,
188        refused: &[String],
189    ) -> Result<Vec<(String, u64)>>
190    where
191        M: toasty::schema::Model,
192    {
193        let mut counts = Vec::new();
194        if !self.tabs.iter().any(ListTab::has_badge) {
195            return Ok(counts);
196        }
197        let mut db = crate::db::db(cx)?;
198        let narrowed = self.narrow_untabbed(cx, &db, query.clone(), state, refused);
199        for tab in self.tabs.iter().filter(|tab| tab.badge) {
200            let rows = match &tab.query {
201                Some(predicate) => narrowed.clone().filter(predicate.clone()),
202                None => narrowed.clone(),
203            };
204            let count = crate::toasty_compat::count(rows, &mut db)
205                .await
206                .map_err(crate::error::unavailable)?;
207            counts.push((tab.key.clone(), count));
208        }
209        Ok(counts)
210    }
211
212    /// What is wrong with the declared tabs: two sharing a key.
213    pub(super) fn tab_errors(&self) -> Vec<DeclarationErrorKind> {
214        let mut seen = std::collections::HashSet::with_capacity(self.tabs.len());
215        self.tabs
216            .iter()
217            .filter(|tab| !seen.insert(tab.key.as_str()))
218            .map(|tab| DeclarationErrorKind::DuplicateTab {
219                key: tab.key.clone(),
220            })
221            .collect()
222    }
223}
224
225#[cfg(test)]
226mod tests;