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}