Skip to main content

tablo_core/column/
editable.rs

1//! [`ToggleColumn`], [`SelectColumn`] and [`TextInputColumn`]: one field edited in its cell,
2//! through the record's edit route.
3
4use derive_where::derive_where;
5use toasty::stmt::OrderByExpr;
6use topcoat::{context::Cx, view::*};
7
8use super::{Column, ColumnBase, ColumnWidth, boolean::boolean_cell, sort_order};
9use crate::{Lens, schema::Options, values::FormScalar};
10
11mod view;
12
13/// The control an editable column renders, which the resource's form must edit its field with.
14#[derive(Clone, Copy, Debug, PartialEq, Eq)]
15pub enum Editor {
16    /// A checkbox or a switch.
17    Toggle,
18    /// A choice over static options.
19    Select,
20    /// A one-line text input.
21    Text,
22}
23
24impl Editor {
25    /// Whether the control saves as soon as it changes, rather than on Enter.
26    pub(crate) fn saves_on_change(self) -> bool {
27        matches!(self, Self::Toggle | Self::Select)
28    }
29
30    /// The control the resource's form must edit the field with, as a mount error names it.
31    pub(crate) fn control(self) -> &'static str {
32        match self {
33            Self::Toggle => "a checkbox or switch",
34            Self::Select => "a choice over static options",
35            Self::Text => "a one-line text field",
36        }
37    }
38}
39
40/// A column of a `bool` field rendered as a switch that saves the record when it flips.
41///
42/// ```rust
43/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
44/// # struct Post { #[key] #[auto] id: uuid::Uuid, featured: bool }
45/// tablo_core::ToggleColumn::new(tablo_core::lens!(Post.featured));
46/// ```
47///
48/// See [Editing in a cell](ToggleColumn#editing-in-a-cell) for where the switch renders and
49/// what it posts.
50///
51/// # Editing in a cell
52///
53/// A list, a relation table or a widget's record table renders the control on a row whose record
54/// the user may update and that is not in the trash, for a field the user may change; anywhere
55/// else, and on a detail page, the cell shows the value. The control posts its field alone to the
56/// record's edit route, with the record's version, so the save runs every check the edit page's
57/// does and lands back on the table. A refused value opens the edit page with its errors.
58///
59/// The mount refuses the column unless the resource's form edits the field with the matching
60/// control, a checkbox or switch here, shown and enabled whatever the other fields hold. Flipping
61/// the switch and changing a select save through the runtime's one line of script; a text input
62/// saves on Enter.
63///
64/// The control spells the value as the field's form spelling, as a derived record form does, so a
65/// hand-written [`RecordForm`](trait@crate::RecordForm) must read the key as that spelling.
66#[derive_where(Clone, Debug)]
67pub struct ToggleColumn<M> {
68    lens: Lens<M, bool>,
69    base: ColumnBase,
70    sortable: bool,
71}
72
73impl<M> ToggleColumn<M>
74where
75    M: toasty::schema::Model,
76{
77    /// Bind the column to the `bool` field `lens` reads.
78    pub fn new(lens: Lens<M, bool>) -> Self {
79        Self {
80            base: ColumnBase::new(lens.path(), ColumnWidth::Auto),
81            lens,
82            sortable: false,
83        }
84    }
85
86    base_builders!(sortable);
87}
88
89impl<M> Column<M> for ToggleColumn<M>
90where
91    M: toasty::schema::Model + Send + Sync + 'static,
92{
93    base_column_methods!();
94
95    fn text(&self, cx: &Cx, row: &M) -> String {
96        crate::Text::message(if *self.lens.read(row) {
97            "tablo-yes"
98        } else {
99            "tablo-no"
100        })
101        .localize(cx)
102    }
103
104    fn cell<'a>(&self, cx: &'a Cx, row: &M) -> BoxView<'a> {
105        boolean_cell(cx, *self.lens.read(row), self.text(cx, row))
106    }
107
108    fn order_by(&self, descending: bool) -> Option<OrderByExpr> {
109        self.sortable
110            .then(|| sort_order(self.lens.path(), descending))
111    }
112}
113
114impl<M> crate::column::ColumnInternals<M> for ToggleColumn<M>
115where
116    M: toasty::schema::Model + Send + Sync + 'static,
117{
118    base_internals!(binding);
119
120    fn edits(&self) -> Option<Editor> {
121        Some(Editor::Toggle)
122    }
123
124    fn editor<'a>(&self, cx: &'a Cx, row: &M) -> Option<BoxView<'a>> {
125        Some(view::toggle(
126            cx,
127            self.base.name(),
128            self.base.label().localize(cx),
129            *self.lens.read(row),
130        ))
131    }
132}
133
134/// A column of an [`Options`] field rendered as a select that saves the record when it changes.
135///
136/// ```rust
137/// # #[derive(Debug, Clone, Copy, PartialEq, Eq, toasty::Embed, tablo_core::Options)]
138/// # enum Status { Draft, Published }
139/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
140/// # struct Post { #[key] #[auto] id: uuid::Uuid, status: Status }
141/// tablo_core::SelectColumn::new(tablo_core::lens!(Post.status));
142/// ```
143///
144/// It offers the type's options, and edits as [`ToggleColumn`](ToggleColumn#editing-in-a-cell)
145/// says, the form's control a choice offering the same options under the same labels.
146#[derive_where(Clone, Debug)]
147pub struct SelectColumn<M, T> {
148    lens: Lens<M, T>,
149    base: ColumnBase,
150    sortable: bool,
151}
152
153impl<M, T> SelectColumn<M, T>
154where
155    M: toasty::schema::Model,
156    T: Options + FormScalar + Send + Sync + 'static,
157{
158    /// Bind the column to the field `lens` reads.
159    pub fn new(lens: Lens<M, T>) -> Self {
160        Self {
161            base: ColumnBase::new(lens.path(), ColumnWidth::Auto),
162            lens,
163            sortable: false,
164        }
165    }
166
167    base_builders!(sortable);
168}
169
170impl<M, T> Column<M> for SelectColumn<M, T>
171where
172    M: toasty::schema::Model + Send + Sync + 'static,
173    T: Options + FormScalar + Send + Sync + 'static,
174{
175    base_column_methods!();
176
177    fn text(&self, cx: &Cx, row: &M) -> String {
178        T::label_of(&self.lens.read(row).to_form()).localize(cx)
179    }
180
181    fn order_by(&self, descending: bool) -> Option<OrderByExpr> {
182        self.sortable
183            .then(|| sort_order(self.lens.path(), descending))
184    }
185}
186
187impl<M, T> crate::column::ColumnInternals<M> for SelectColumn<M, T>
188where
189    M: toasty::schema::Model + Send + Sync + 'static,
190    T: Options + FormScalar + Send + Sync + 'static,
191{
192    base_internals!(binding);
193
194    fn edits(&self) -> Option<Editor> {
195        Some(Editor::Select)
196    }
197
198    fn edit_options(&self) -> Vec<(String, crate::Text)> {
199        T::options()
200    }
201
202    fn editor<'a>(&self, cx: &'a Cx, row: &M) -> Option<BoxView<'a>> {
203        let options = T::options()
204            .into_iter()
205            .map(|(value, label)| (value, label.localize(cx)))
206            .collect();
207        Some(view::select(
208            cx,
209            self.base.name(),
210            self.base.label().localize(cx),
211            self.lens.read(row).to_form(),
212            options,
213        ))
214    }
215}
216
217/// A column of one field rendered as a text input that saves the record on Enter.
218///
219/// ```rust
220/// # #[derive(Debug, Clone, toasty::Model, tablo_core::ModelColumns)]
221/// # struct Product { #[key] #[auto] id: uuid::Uuid, stock: i64 }
222/// tablo_core::TextInputColumn::new(tablo_core::lens!(Product.stock));
223/// ```
224///
225/// The input takes the type's input type, a number for an integer, and edits as
226/// [`ToggleColumn`](ToggleColumn#editing-in-a-cell) says, the form's control a one-line text
227/// field that is no password.
228#[derive_where(Clone, Debug)]
229pub struct TextInputColumn<M, T> {
230    lens: Lens<M, T>,
231    base: ColumnBase,
232    sortable: bool,
233}
234
235impl<M, T> TextInputColumn<M, T>
236where
237    M: toasty::schema::Model,
238    T: FormScalar + Send + Sync + 'static,
239{
240    /// Bind the column to the field `lens` reads.
241    pub fn new(lens: Lens<M, T>) -> Self {
242        Self {
243            base: ColumnBase::new(lens.path(), ColumnWidth::Grow),
244            lens,
245            sortable: false,
246        }
247    }
248
249    base_builders!(sortable);
250}
251
252impl<M, T> Column<M> for TextInputColumn<M, T>
253where
254    M: toasty::schema::Model + Send + Sync + 'static,
255    T: FormScalar + Send + Sync + 'static,
256{
257    base_column_methods!();
258
259    fn text(&self, cx: &Cx, row: &M) -> String {
260        self.lens.read(row).to_label().localize(cx)
261    }
262
263    fn order_by(&self, descending: bool) -> Option<OrderByExpr> {
264        self.sortable
265            .then(|| sort_order(self.lens.path(), descending))
266    }
267}
268
269impl<M, T> crate::column::ColumnInternals<M> for TextInputColumn<M, T>
270where
271    M: toasty::schema::Model + Send + Sync + 'static,
272    T: FormScalar + Send + Sync + 'static,
273{
274    base_internals!(binding);
275
276    fn edits(&self) -> Option<Editor> {
277        Some(Editor::Text)
278    }
279
280    fn editor<'a>(&self, cx: &'a Cx, row: &M) -> Option<BoxView<'a>> {
281        let value = T::input_value(&self.lens.read(row).to_form(), &crate::time_zone(cx));
282        Some(view::text_input(
283            cx,
284            view::TextInput {
285                name: self.base.name(),
286                label: self.base.label().localize(cx),
287                value,
288                input_type: T::INPUT_TYPE,
289                step: T::STEP,
290            },
291        ))
292    }
293}