Skip to main content

zeyn/grid/
column.rs

1//! [`DataGridColumn`]: the builder of the last added column of a [`DataGrid`], with every
2//! `DataGrid` method passed through so a grid reads as one chain.
3
4use super::*;
5
6/// Configures the last added column. DataGrid methods can be called from here too.
7pub struct DataGridColumn<E: Model, T> {
8    pub(super) grid: DataGrid<E>,
9    pub(super) field: Field<E, T>,
10    pub(super) _type: PhantomData<fn() -> T>,
11}
12
13impl<E: Model, T> DataGridColumn<E, T> {
14    fn last(&mut self) -> &mut Column<E> {
15        self.grid
16            .columns
17            .last_mut()
18            .expect("DataGridColumn always has a column")
19    }
20
21    /// Fixed width in pixels.
22    pub fn width(mut self, px: u32) -> Self {
23        self.last().width = Some(px);
24        self
25    }
26
27    /// Takes a share of the remaining width.
28    pub fn flex(mut self, weight: u32) -> Self {
29        self.last().flex = Some(weight);
30        self
31    }
32
33    /// Adds a filter box to the column.
34    pub fn filter(mut self) -> Self {
35        self.last().filter = true;
36        self
37    }
38
39    /// The column cannot be sorted: the header has no sort button, the column menu no sort
40    /// entries, and the server drops sort keys on it before the action runs. Use it when the
41    /// action cannot order by the field (generated rows, a column the database has no index for),
42    /// so the page never offers a sort the data does not follow.
43    pub fn no_sort(mut self) -> Self {
44        self.last().sort = false;
45        self
46    }
47
48    fn aggregate(mut self, op: Aggregate) -> Self {
49        let column = self.last();
50        if !column.aggregates.contains(&op) {
51            column.aggregates.push(op);
52        }
53        self
54    }
55
56    /// Shows the column total in the footer, computed on the server over every matching row.
57    /// Compiles only on numeric and money columns.
58    pub fn sum(self) -> Self
59    where
60        T: SupportsSum,
61    {
62        self.aggregate(Aggregate::Sum)
63    }
64
65    /// Shows the column average in the footer. Compiles only on numeric and money columns.
66    pub fn avg(self) -> Self
67    where
68        T: SupportsSum,
69    {
70        self.aggregate(Aggregate::Avg)
71    }
72
73    /// Shows the smallest value in the footer. Compiles on numbers, money, dates and times.
74    pub fn min(self) -> Self
75    where
76        T: SupportsMinMaxTotal,
77    {
78        self.aggregate(Aggregate::Min)
79    }
80
81    /// Shows the largest value in the footer. Compiles on numbers, money, dates and times.
82    pub fn max(self) -> Self
83    where
84        T: SupportsMinMaxTotal,
85    {
86        self.aggregate(Aggregate::Max)
87    }
88
89    /// Keeps the column at the start edge (left in left to right languages) while the grid
90    /// scrolls sideways. At most three columns per side stay pinned.
91    pub fn pin_start(mut self) -> Self {
92        self.last().pin = Some(Pin::Start);
93        self
94    }
95
96    /// Keeps the column at the end edge while the grid scrolls sideways.
97    pub fn pin_end(mut self) -> Self {
98        self.last().pin = Some(Pin::End);
99        self
100    }
101
102    /// Leaves the column out until the user shows it from the column menu.
103    pub fn hidden(mut self) -> Self {
104        self.last().hidden = true;
105        self
106    }
107
108    /// Lets users edit this column in place when the grid has a save action (`DataGrid::edit_with`).
109    /// Keys, read only fields and row versions never become editable; the server refuses
110    /// values for them.
111    pub fn editable(mut self) -> Self {
112        self.last().editable = true;
113        self
114    }
115
116    /// Draws the cells with a custom renderer. See [`CellRenderer`].
117    pub fn cell(mut self, renderer: impl CellRenderer<T>) -> Self
118    where
119        T: 'static,
120    {
121        let field = self.field;
122        self.last().cell = Some(Box::new(move |row, r| renderer.render(field.get(row), r)));
123        self
124    }
125
126    /// Shows the choice as a colored badge; `tone` picks the color per option:
127    /// `.badge(|u| match u { Unit::Kg => Tone::Info, _ => Tone::Neutral })`. Compiles only on
128    /// `#[choice]` columns.
129    pub fn badge(self, tone: impl Fn(&T) -> Tone + Send + Sync + 'static) -> Self
130    where
131        T: ChoiceType + FieldType + 'static,
132    {
133        let kind = self.field.meta().kind;
134        self.cell(move |v: &T, r: &Render| {
135            format!(
136                r#"<span class="z-badge z-tone-{}">{}</span>"#,
137                tone(v).code(),
138                r.value(&v.to_value(), &kind)
139            )
140        })
141    }
142
143    /// Shows a number as a bar filled up to `max`, with the number beside it. Compiles only on
144    /// numeric and money columns.
145    pub fn progress(self, max: i64) -> Self
146    where
147        T: SupportsSum + FieldType + 'static,
148    {
149        let kind = self.field.meta().kind;
150        self.cell(move |v: &T, r: &Render| {
151            let value = v.to_value();
152            let amount = match value {
153                Value::Int(n) => n as f64,
154                Value::Money(m) => m.minor as f64 / 100.0,
155                _ => return String::new(),
156            };
157            let pct = if max > 0 { (amount / max as f64 * 100.0).clamp(0.0, 100.0) } else { 0.0 };
158            format!(
159                r#"<span class="z-progress"><span class="z-progress-track" aria-hidden="true"><span class="z-progress-bar" style="--z-pct:{pct:.1}%"></span></span><span class="z-progress-text">{}</span></span>"#,
160                r.value(&value, &kind)
161            )
162        })
163    }
164
165    /// Makes the cell a link that runs an effect built from the row, such as opening a window.
166    pub fn link(mut self, effect: impl Fn(&E) -> Effect + Send + Sync + 'static) -> Self
167    where
168        T: FieldType + 'static,
169    {
170        let field = self.field;
171        let kind = field.meta().kind;
172        self.last().cell = Some(Box::new(move |row, r| {
173            format!(
174                r#"<button type="button" class="z-link" tabindex="-1" data-zeyn-effect="{}">{}</button>"#,
175                attr(&effect(row)),
176                r.value(&field.get(row).to_value(), &kind)
177            )
178        }));
179        self
180    }
181
182    /// Shows yes as a check mark and no as a dash, with the words for screen readers. Compiles
183    /// only on yes/no columns.
184    pub fn check_icon(self) -> Self
185    where
186        T: IsBool + FieldType + 'static,
187    {
188        self.cell(|v: &T, r: &Render| match v.to_value() {
189            Value::Bool(b) => format!(
190                r#"<span class="z-bool z-bool-{}" role="img" aria-label="{}"></span>"#,
191                if b { "yes" } else { "no" },
192                r.text(&Text::key(if b { "zeyn.yes" } else { "zeyn.no" }))
193            ),
194            _ => String::new(),
195        })
196    }
197
198    /// Shows a moment as relative time ("3 minutes ago") in the user's language, with the exact
199    /// time in the user's time zone as a tooltip. Compiles only on moment columns.
200    pub fn relative_time(self) -> Self
201    where
202        T: IsMoment + FieldType + 'static,
203    {
204        self.cell(|v: &T, r: &Render| match v.to_value() {
205            Value::DateTime(d) => {
206                let exact = d.format(r.tz, r.locale);
207                format!(r#"<time datetime="{d}" title="{exact}" data-zeyn-relative>{exact}</time>"#)
208            }
209            _ => String::new(),
210        })
211    }
212
213    /// Shows how many matching rows have a value in this column. Works on every column.
214    pub fn count(self) -> Self {
215        self.aggregate(Aggregate::Count)
216    }
217
218    pub fn column<U: FieldType + 'static>(self, field: Field<E, U>) -> DataGridColumn<E, U> {
219        self.grid.column(field)
220    }
221
222    pub fn mode(self, mode: DataMode) -> DataGrid<E> {
223        self.grid.mode(mode)
224    }
225
226    pub fn paging(self, paging: Paging) -> DataGrid<E> {
227        self.grid.paging(paging)
228    }
229
230    pub fn row_style(
231        self,
232        style: impl Fn(&E) -> Option<RowStyle> + Send + Sync + 'static,
233    ) -> DataGrid<E> {
234        self.grid.row_style(style)
235    }
236
237    pub fn on_open(self, open: impl Fn(&E) -> Effect + Send + Sync + 'static) -> DataGrid<E> {
238        self.grid.on_open(open)
239    }
240
241    pub fn row_action<A>(
242        self,
243        label: impl Into<Text>,
244        action: A,
245        input: impl Fn(&E) -> A::Input + Send + Sync + 'static,
246    ) -> DataGrid<E>
247    where
248        A: Action,
249        A::Output: Outcome,
250    {
251        self.grid
252            .row_action(label, action, input)
253    }
254
255    pub fn selectable(self) -> DataGrid<E> {
256        self.grid.selectable()
257    }
258
259    pub fn row_menu(
260        self,
261        menu: impl Fn(&E) -> crate::menu::ContextMenu + Send + Sync + 'static,
262    ) -> DataGrid<E> {
263        self.grid.row_menu(menu)
264    }
265
266    pub fn exportable(self) -> DataGrid<E> {
267        self.grid.exportable()
268    }
269
270    pub fn edit_with<A>(self, save: A) -> DataGrid<E>
271    where
272        A: crate::form::FormAction<E> + Action<Input = Valid<E>>,
273        A::Output: Outcome,
274    {
275        self.grid.edit_with(save)
276    }
277
278    pub fn bulk_action<A, K>(
279        self,
280        label: impl Into<Text>,
281        action: A,
282        key: Field<E, K>,
283    ) -> DataGrid<E>
284    where
285        A: BulkAction<K> + Action<Input = Vec<K>>,
286        A::Output: Outcome,
287        K: serde::de::DeserializeOwned + Send + 'static,
288    {
289        self.grid
290            .bulk_action(label, action, key)
291    }
292
293    pub fn detail<S: ScreenDef>(
294        self,
295        detail: impl Fn(&E) -> (S, S::Params) + Send + Sync + 'static,
296    ) -> DataGrid<E> {
297        self.grid.detail(detail)
298    }
299
300    pub fn scope<U: FieldType>(self, field: Field<E, U>, value: U) -> DataGrid<E> {
301        self.grid.scope(field, value)
302    }
303
304    pub fn sort_by<U: FieldType>(self, field: Field<E, U>, desc: bool) -> DataGrid<E> {
305        self.grid.sort_by(field, desc)
306    }
307
308    pub fn group_by<U: FieldType>(self, field: Field<E, U>) -> DataGrid<E> {
309        self.grid.group_by(field)
310    }
311
312    pub fn tree<K: FieldType>(self, parent: Field<E, Option<K>>) -> DataGrid<E>
313    where
314        Option<K>: FieldType,
315    {
316        self.grid.tree(parent)
317    }
318
319    /// Returns the grid.
320    pub fn done(self) -> DataGrid<E> {
321        self.grid
322    }
323}
324
325impl<E: Model, T> From<DataGridColumn<E, T>> for DataGrid<E> {
326    fn from(c: DataGridColumn<E, T>) -> Self {
327        c.grid
328    }
329}
330
331impl<E: Model, T> From<DataGridColumn<E, T>> for Node {
332    fn from(c: DataGridColumn<E, T>) -> Self {
333        Node::new(c.grid)
334    }
335}