Skip to main content

tablo_core/column/
date.rs

1//! [`DateColumn`]: a date field rendered as a calendar day, over any [`DateValue`].
2
3use derive_where::derive_where;
4use jiff::tz::TimeZone;
5use toasty::stmt::OrderByExpr;
6use topcoat::{context::Cx, view::*};
7
8use super::{Column, ColumnBase, ColumnWidth, sort_order};
9use crate::{Lens, values::FormScalar};
10
11/// A column of a date field, rendered as a calendar day.
12///
13/// ```rust
14/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
15/// # struct Post { #[key] #[auto] id: uuid::Uuid, published_at: jiff::Timestamp }
16/// tablo_core::DateColumn::new(tablo_core::lens!(Post.published_at)).sortable();
17/// ```
18///
19/// The cell spells the day in the request's locale, `Oct 14, 2026` in English, a timestamp's in
20/// the request's [`time_zone`](crate::time_zone), and a `None` renders the table's dash. The export
21/// writes the stored spelling, `2026-10-14T09:30:00Z` or `2026-10-14`, as a
22/// [`TextColumn`](super::TextColumn) does; a group header follows the field, not the column.
23#[derive_where(Clone, Debug)]
24pub struct DateColumn<M, T> {
25    lens: Lens<M, T>,
26    base: ColumnBase,
27    sortable: bool,
28}
29
30/// The field types a [`DateColumn`] spells as a calendar day: `jiff::civil::Date`,
31/// `jiff::civil::DateTime`, `jiff::Timestamp` (read in the request's
32/// [`time_zone`](crate::time_zone), as a [`DateFilter`](crate::DateFilter) reads it) and an
33/// `Option` of any of them.
34pub trait DateValue: Sized + Send + Sync + 'static {
35    /// The value's calendar day in `zone`, `None` when it holds none. A civil value names its day
36    /// whatever the zone.
37    fn calendar_date(&self, zone: &TimeZone) -> Option<jiff::civil::Date>;
38
39    /// The value's stored spelling, which a column's text and export carry. The default spells
40    /// the calendar day in UTC as an ISO date.
41    fn stored_text(&self) -> String {
42        self.calendar_date(&TimeZone::UTC)
43            .map(|day| day.to_string())
44            .unwrap_or_default()
45    }
46}
47
48impl DateValue for jiff::civil::Date {
49    fn calendar_date(&self, _zone: &TimeZone) -> Option<jiff::civil::Date> {
50        Some(*self)
51    }
52
53    fn stored_text(&self) -> String {
54        FormScalar::to_form(self)
55    }
56}
57
58impl DateValue for jiff::civil::DateTime {
59    fn calendar_date(&self, _zone: &TimeZone) -> Option<jiff::civil::Date> {
60        Some(self.date())
61    }
62
63    fn stored_text(&self) -> String {
64        FormScalar::to_form(self)
65    }
66}
67
68impl DateValue for jiff::Timestamp {
69    fn calendar_date(&self, zone: &TimeZone) -> Option<jiff::civil::Date> {
70        Some(self.to_zoned(zone.clone()).date())
71    }
72
73    fn stored_text(&self) -> String {
74        FormScalar::to_form(self)
75    }
76}
77
78impl<T: DateValue> DateValue for Option<T> {
79    fn calendar_date(&self, zone: &TimeZone) -> Option<jiff::civil::Date> {
80        self.as_ref().and_then(|value| value.calendar_date(zone))
81    }
82
83    fn stored_text(&self) -> String {
84        self.as_ref()
85            .map(DateValue::stored_text)
86            .unwrap_or_default()
87    }
88}
89
90impl<M, T> DateColumn<M, T>
91where
92    M: toasty::schema::Model,
93{
94    /// Bind the column to the date field `lens` reads.
95    pub fn new(lens: Lens<M, T>) -> Self
96    where
97        T: DateValue,
98    {
99        Self {
100            base: ColumnBase::new(lens.path(), ColumnWidth::Auto),
101            lens,
102            sortable: false,
103        }
104    }
105
106    base_builders!(sortable);
107}
108
109impl<M, T> Column<M> for DateColumn<M, T>
110where
111    M: toasty::schema::Model + Send + Sync + 'static,
112    T: DateValue,
113{
114    base_column_methods!();
115
116    fn text(&self, _cx: &Cx, row: &M) -> String {
117        self.lens.read(row).stored_text()
118    }
119
120    fn cell<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
121        let day = self
122            .lens
123            .read(row)
124            .calendar_date(&crate::time_zone(cx))
125            .map(|day| {
126                crate::t!(
127                    cx,
128                    "tablo-column-date",
129                    year = day.year(),
130                    month = day.month(),
131                    day = day.day()
132                )
133            })
134            .unwrap_or_default();
135        crate::schema::value_cell(cx, &day)
136    }
137
138    fn order_by(&self, descending: bool) -> Option<OrderByExpr> {
139        self.sortable
140            .then(|| sort_order(self.lens.path(), descending))
141    }
142}
143
144impl<M, T> crate::column::ColumnInternals<M> for DateColumn<M, T>
145where
146    M: toasty::schema::Model + Send + Sync + 'static,
147    T: DateValue,
148{
149    base_internals!(binding);
150}