Skip to main content

tablo_core/column/
computed.rs

1//! [`ComputedColumn`]: a display-only column projecting any text off the row.
2
3use std::sync::Arc;
4
5use derive_where::derive_where;
6use topcoat::{context::Cx, view::BoxView};
7
8use super::{
9    Column, ColumnToggle, ColumnWidth, Includes,
10    cell::{TextCell, text_cell},
11};
12use crate::display::{Alignment, Color};
13
14/// A display-only column rendering any text from the row, with no query predicate.
15///
16/// ```rust
17/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
18/// # struct Post { #[key] #[auto] id: uuid::Uuid, body: String }
19/// tablo_core::ComputedColumn::new("Words", |p: &Post| {
20///     p.body.split_whitespace().count().to_string()
21/// });
22/// ```
23///
24/// A closure that reads a relation declares it with [`include_unscoped`](Self::include_unscoped),
25/// so the page loads it. One relation's record or count is a
26/// [`RelationColumn`](super::RelationColumn) or a [`CountColumn`](super::CountColumn), which
27/// declare their own and show only what the related resource shows the user.
28///
29/// It maps to no column, so it neither searches nor sorts.
30///
31/// A [`badge`](Self::badge) shows the text in an outline badge, with a dot in the
32/// [`color`](Self::color) the row gives it:
33///
34/// ```rust
35/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
36/// # struct Order { #[key] #[auto] id: uuid::Uuid, total_cents: i64 }
37/// use tablo_core::{Color, ComputedColumn, Reads, lens};
38///
39/// ComputedColumn::new("Size", |o: &Order| {
40///     if o.total_cents > 100_00 {
41///         "Large"
42///     } else {
43///         "Small"
44///     }
45///     .to_string()
46/// })
47/// .badge()
48/// .color(|o: &Order| (o.total_cents > 100_00).then_some(Color::Success))
49/// .reading(Reads::of(lens!(Order.total_cents)));
50/// ```
51#[derive_where(Clone, Debug)]
52pub struct ComputedColumn<M> {
53    name: String,
54    label: crate::Text,
55    #[derive_where(skip(Debug))]
56    project: Arc<dyn Fn(&M) -> String + Send + Sync>,
57    width: ColumnWidth,
58    alignment: Alignment,
59    toggle: ColumnToggle,
60    /// Relations the projection reads.
61    includes: Includes<M>,
62    /// The fields the projection reads, when declared.
63    reads: Option<crate::lens::Reads<M>>,
64    badge: bool,
65    #[derive_where(skip(Debug))]
66    color: Option<super::ReadFn<M, Option<Color>>>,
67}
68
69impl<M> ComputedColumn<M>
70where
71    M: toasty::schema::Model,
72{
73    /// Declare a column headed `label` rendering `project(row)`, named after the label: its
74    /// plain text in lowercase, or its message's id.
75    pub fn new(
76        label: impl Into<crate::Text>,
77        project: impl Fn(&M) -> String + Send + Sync + 'static,
78    ) -> Self {
79        let label = label.into();
80        Self {
81            name: label.key(),
82            label,
83            project: Arc::new(project),
84            width: ColumnWidth::Auto,
85            alignment: Alignment::Start,
86            toggle: ColumnToggle::Fixed,
87            includes: Includes::new(),
88            reads: None,
89            badge: false,
90            color: None,
91        }
92    }
93
94    /// Show the text as a badge, with a dot in its [`color`](Self::color).
95    pub fn badge(mut self) -> Self {
96        self.badge = true;
97        self
98    }
99
100    /// Color the text by `color(row)`: its badge's dot, or the text itself when it is no badge.
101    /// [`reading`](Self::reading) declares what it reads, beside the projection.
102    pub fn color(mut self, color: impl Fn(&M) -> Option<Color> + Send + Sync + 'static) -> Self {
103        self.color = Some(Arc::new(color));
104        self
105    }
106
107    /// Declare a relation the projection reads, so the table loads it. The projection reads
108    /// whatever the include loads, under no resource's query or policy, as an
109    /// [`unscoped`](super::RelationColumn::unscoped) relation column does: another tenant's record,
110    /// or one the user may not view, reaches it.
111    pub fn include_unscoped<T>(mut self, relation: impl Into<toasty::stmt::Include<M, T>>) -> Self {
112        self.includes = self.includes.with(relation);
113        self
114    }
115
116    /// Declare the fields the projection and its [`color`](Self::color) read, which a resource
117    /// restricting a field with [`field_access`](crate::ResourceDef::field_access) needs: the
118    /// column shows only to a user who may view every one.
119    pub fn reading(mut self, reads: crate::lens::Reads<M>) -> Self {
120        self.reads = Some(reads);
121        self
122    }
123
124    /// Declare this column's width.
125    pub fn width(mut self, width: ColumnWidth) -> Self {
126        self.width = width;
127        self
128    }
129
130    /// Align the header and the cells.
131    pub fn align(mut self, alignment: Alignment) -> Self {
132        self.alignment = alignment;
133        self
134    }
135
136    toggle_builders!(toggle);
137}
138
139impl<M> Column<M> for ComputedColumn<M>
140where
141    M: toasty::schema::Model + Send + Sync + 'static,
142{
143    fn name(&self) -> &str {
144        &self.name
145    }
146
147    fn label(&self) -> crate::Text {
148        self.label.clone()
149    }
150
151    fn text(&self, _cx: &Cx, row: &M) -> String {
152        (self.project)(row)
153    }
154
155    fn cell<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
156        let text = (self.project)(row);
157        let color = self
158            .color
159            .as_ref()
160            .filter(|_| !text.trim().is_empty())
161            .and_then(|color| color(row));
162        text_cell(
163            cx,
164            TextCell {
165                text,
166                color,
167                badge: self.badge,
168                ..TextCell::default()
169            },
170        )
171    }
172
173    fn column_width(&self) -> ColumnWidth {
174        self.width
175    }
176
177    fn toggle(&self) -> ColumnToggle {
178        self.toggle
179    }
180
181    fn alignment(&self) -> Alignment {
182        self.alignment
183    }
184
185    fn includes(&self) -> Includes<M> {
186        self.includes.clone()
187    }
188
189    fn reads(&self) -> Option<crate::lens::Reads<M>> {
190        self.reads.clone()
191    }
192}